plugin.tsx 12 KB

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