plugin.tsx 10 KB

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