plugin.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359
  1. import { useDebouncedCallback } from "use-debounce";
  2. import OpenAPIClientAxios from "openapi-client-axios";
  3. import yaml from "js-yaml";
  4. import { IconButton } from "./button";
  5. import { ErrorBoundary } from "./error";
  6. import styles from "./mask.module.scss";
  7. import pluginStyles from "./plugin.module.scss";
  8. import EditIcon from "../icons/edit.svg";
  9. import AddIcon from "../icons/add.svg";
  10. import CloseIcon from "../icons/close.svg";
  11. import DeleteIcon from "../icons/delete.svg";
  12. import EyeIcon from "../icons/eye.svg";
  13. import ConfirmIcon from "../icons/confirm.svg";
  14. import ReloadIcon from "../icons/reload.svg";
  15. import { Plugin, usePluginStore, FunctionToolService } from "../store/plugin";
  16. import {
  17. PasswordInput,
  18. List,
  19. ListItem,
  20. Modal,
  21. showConfirm,
  22. showToast,
  23. } from "./ui-lib";
  24. import Locale from "../locales";
  25. import { useNavigate } from "react-router-dom";
  26. import { useEffect, useState } from "react";
  27. export function PluginPage() {
  28. const navigate = useNavigate();
  29. const pluginStore = usePluginStore();
  30. const allPlugins = pluginStore.getAll();
  31. const [searchPlugins, setSearchPlugins] = useState<Plugin[]>([]);
  32. const [searchText, setSearchText] = useState("");
  33. const plugins = searchText.length > 0 ? searchPlugins : allPlugins;
  34. // refactored already, now it accurate
  35. const onSearch = (text: string) => {
  36. setSearchText(text);
  37. if (text.length > 0) {
  38. const result = allPlugins.filter(
  39. (m) => m?.title.toLowerCase().includes(text.toLowerCase()),
  40. );
  41. setSearchPlugins(result);
  42. } else {
  43. setSearchPlugins(allPlugins);
  44. }
  45. };
  46. const [editingPluginId, setEditingPluginId] = useState<string | undefined>();
  47. const editingPlugin = pluginStore.get(editingPluginId);
  48. const editingPluginTool = FunctionToolService.get(editingPlugin?.id);
  49. const closePluginModal = () => setEditingPluginId(undefined);
  50. const onChangePlugin = useDebouncedCallback((editingPlugin, e) => {
  51. const content = e.target.innerText;
  52. try {
  53. const api = new OpenAPIClientAxios({
  54. definition: yaml.load(content) as any,
  55. });
  56. api
  57. .init()
  58. .then(() => {
  59. if (content != editingPlugin.content) {
  60. pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
  61. plugin.content = content;
  62. const tool = FunctionToolService.add(plugin, true);
  63. plugin.title = tool.api.definition.info.title;
  64. plugin.version = tool.api.definition.info.version;
  65. });
  66. }
  67. })
  68. .catch((e) => {
  69. console.error(e);
  70. showToast(Locale.Plugin.EditModal.Error);
  71. });
  72. } catch (e) {
  73. console.error(e);
  74. showToast(Locale.Plugin.EditModal.Error);
  75. }
  76. }, 100).bind(null, editingPlugin);
  77. const [loadUrl, setLoadUrl] = useState<string>("");
  78. const loadFromUrl = (loadUrl: string) =>
  79. fetch(loadUrl)
  80. .catch((e) => {
  81. const p = new URL(loadUrl);
  82. return fetch(`/api/proxy/${p.pathname}?${p.search}`, {
  83. headers: {
  84. "X-Base-URL": p.origin,
  85. },
  86. });
  87. })
  88. .then((res) => res.text())
  89. .then((content) => {
  90. try {
  91. return JSON.stringify(JSON.parse(content), null, " ");
  92. } catch (e) {
  93. return content;
  94. }
  95. })
  96. .then((content) => {
  97. pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
  98. plugin.content = content;
  99. const tool = FunctionToolService.add(plugin, true);
  100. plugin.title = tool.api.definition.info.title;
  101. plugin.version = tool.api.definition.info.version;
  102. });
  103. })
  104. .catch((e) => {
  105. showToast(Locale.Plugin.EditModal.Error);
  106. });
  107. return (
  108. <ErrorBoundary>
  109. <div className={styles["mask-page"]}>
  110. <div className="window-header">
  111. <div className="window-header-title">
  112. <div className="window-header-main-title">
  113. {Locale.Plugin.Page.Title}
  114. </div>
  115. <div className="window-header-submai-title">
  116. {Locale.Plugin.Page.SubTitle(plugins.length)}
  117. </div>
  118. </div>
  119. <div className="window-actions">
  120. <div className="window-action-button">
  121. <IconButton
  122. icon={<CloseIcon />}
  123. bordered
  124. onClick={() => navigate(-1)}
  125. />
  126. </div>
  127. </div>
  128. </div>
  129. <div className={styles["mask-page-body"]}>
  130. <div className={styles["mask-filter"]}>
  131. <input
  132. type="text"
  133. className={styles["search-bar"]}
  134. placeholder={Locale.Plugin.Page.Search}
  135. autoFocus
  136. onInput={(e) => onSearch(e.currentTarget.value)}
  137. />
  138. <IconButton
  139. className={styles["mask-create"]}
  140. icon={<AddIcon />}
  141. text={Locale.Plugin.Page.Create}
  142. bordered
  143. onClick={() => {
  144. const createdPlugin = pluginStore.create();
  145. setEditingPluginId(createdPlugin.id);
  146. }}
  147. />
  148. </div>
  149. <div>
  150. {plugins.map((m) => (
  151. <div className={styles["mask-item"]} key={m.id}>
  152. <div className={styles["mask-header"]}>
  153. <div className={styles["mask-icon"]}></div>
  154. <div className={styles["mask-title"]}>
  155. <div className={styles["mask-name"]}>
  156. {m.title}@<small>{m.version}</small>
  157. </div>
  158. <div className={styles["mask-info"] + " one-line"}>
  159. {Locale.Plugin.Item.Info(
  160. FunctionToolService.add(m).length,
  161. )}
  162. </div>
  163. </div>
  164. </div>
  165. <div className={styles["mask-actions"]}>
  166. {m.builtin ? (
  167. <IconButton
  168. icon={<EyeIcon />}
  169. text={Locale.Plugin.Item.View}
  170. onClick={() => setEditingPluginId(m.id)}
  171. />
  172. ) : (
  173. <IconButton
  174. icon={<EditIcon />}
  175. text={Locale.Plugin.Item.Edit}
  176. onClick={() => setEditingPluginId(m.id)}
  177. />
  178. )}
  179. {!m.builtin && (
  180. <IconButton
  181. icon={<DeleteIcon />}
  182. text={Locale.Plugin.Item.Delete}
  183. onClick={async () => {
  184. if (
  185. await showConfirm(Locale.Plugin.Item.DeleteConfirm)
  186. ) {
  187. pluginStore.delete(m.id);
  188. }
  189. }}
  190. />
  191. )}
  192. </div>
  193. </div>
  194. ))}
  195. </div>
  196. </div>
  197. </div>
  198. {editingPlugin && (
  199. <div className="modal-mask">
  200. <Modal
  201. title={Locale.Plugin.EditModal.Title(editingPlugin?.builtin)}
  202. onClose={closePluginModal}
  203. actions={[
  204. <IconButton
  205. icon={<ConfirmIcon />}
  206. text={Locale.UI.Confirm}
  207. key="export"
  208. bordered
  209. onClick={() => setEditingPluginId("")}
  210. />,
  211. ]}
  212. >
  213. <List>
  214. <ListItem title={Locale.Plugin.EditModal.Auth}>
  215. <select
  216. value={editingPlugin?.authType}
  217. onChange={(e) => {
  218. pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
  219. plugin.authType = e.target.value;
  220. });
  221. }}
  222. >
  223. <option value="">{Locale.Plugin.Auth.None}</option>
  224. <option value="bearer">{Locale.Plugin.Auth.Bearer}</option>
  225. <option value="basic">{Locale.Plugin.Auth.Basic}</option>
  226. <option value="custom">{Locale.Plugin.Auth.Custom}</option>
  227. </select>
  228. </ListItem>
  229. {["bearer", "basic", "custom"].includes(
  230. editingPlugin.authType as string,
  231. ) && (
  232. <ListItem title={Locale.Plugin.Auth.Location}>
  233. <select
  234. value={editingPlugin?.authLocation}
  235. onChange={(e) => {
  236. pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
  237. plugin.authLocation = e.target.value;
  238. });
  239. }}
  240. >
  241. <option value="header">
  242. {Locale.Plugin.Auth.LocationHeader}
  243. </option>
  244. <option value="query">
  245. {Locale.Plugin.Auth.LocationQuery}
  246. </option>
  247. <option value="body">
  248. {Locale.Plugin.Auth.LocationBody}
  249. </option>
  250. </select>
  251. </ListItem>
  252. )}
  253. {editingPlugin.authType == "custom" && (
  254. <ListItem title={Locale.Plugin.Auth.CustomHeader}>
  255. <input
  256. type="text"
  257. value={editingPlugin?.authHeader}
  258. onChange={(e) => {
  259. pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
  260. plugin.authHeader = e.target.value;
  261. });
  262. }}
  263. ></input>
  264. </ListItem>
  265. )}
  266. {["bearer", "basic", "custom"].includes(
  267. editingPlugin.authType as string,
  268. ) && (
  269. <ListItem title={Locale.Plugin.Auth.Token}>
  270. <PasswordInput
  271. type="text"
  272. value={editingPlugin?.authToken}
  273. onChange={(e) => {
  274. pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
  275. plugin.authToken = e.currentTarget.value;
  276. });
  277. }}
  278. ></PasswordInput>
  279. </ListItem>
  280. )}
  281. <ListItem
  282. title={Locale.Plugin.Auth.Proxy}
  283. subTitle={Locale.Plugin.Auth.ProxyDescription}
  284. >
  285. <input
  286. type="checkbox"
  287. checked={editingPlugin?.usingProxy}
  288. style={{ minWidth: 16 }}
  289. onChange={(e) => {
  290. pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
  291. plugin.usingProxy = e.currentTarget.checked;
  292. });
  293. }}
  294. ></input>
  295. </ListItem>
  296. </List>
  297. <List>
  298. <ListItem title={Locale.Plugin.EditModal.Content}>
  299. <div style={{ display: "flex", justifyContent: "flex-end" }}>
  300. <input
  301. type="text"
  302. style={{ minWidth: 200, marginRight: 20 }}
  303. onInput={(e) => setLoadUrl(e.currentTarget.value)}
  304. ></input>
  305. <IconButton
  306. icon={<ReloadIcon />}
  307. text={Locale.Plugin.EditModal.Load}
  308. bordered
  309. onClick={() => loadFromUrl(loadUrl)}
  310. />
  311. </div>
  312. </ListItem>
  313. <ListItem
  314. subTitle={
  315. <div
  316. className={`markdown-body ${pluginStyles["plugin-content"]}`}
  317. dir="auto"
  318. >
  319. <pre>
  320. <code
  321. contentEditable={true}
  322. dangerouslySetInnerHTML={{
  323. __html: editingPlugin.content,
  324. }}
  325. onBlur={onChangePlugin}
  326. ></code>
  327. </pre>
  328. </div>
  329. }
  330. ></ListItem>
  331. {editingPluginTool?.tools.map((tool, index) => (
  332. <ListItem
  333. key={index}
  334. title={tool?.function?.name}
  335. subTitle={tool?.function?.description}
  336. />
  337. ))}
  338. </List>
  339. </Modal>
  340. </div>
  341. )}
  342. </ErrorBoundary>
  343. );
  344. }