plugin.tsx 13 KB

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