groups-editor-viewer.tsx 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856
  1. import { ReactNode, useEffect, useMemo, useState } from "react";
  2. import { useLockFn } from "ahooks";
  3. import yaml from "js-yaml";
  4. import { useTranslation } from "react-i18next";
  5. import {
  6. DndContext,
  7. closestCenter,
  8. KeyboardSensor,
  9. PointerSensor,
  10. useSensor,
  11. useSensors,
  12. DragEndEvent,
  13. } from "@dnd-kit/core";
  14. import {
  15. SortableContext,
  16. sortableKeyboardCoordinates,
  17. } from "@dnd-kit/sortable";
  18. import {
  19. Autocomplete,
  20. Box,
  21. Button,
  22. Dialog,
  23. DialogActions,
  24. DialogContent,
  25. DialogTitle,
  26. List,
  27. ListItem,
  28. ListItemText,
  29. TextField,
  30. styled,
  31. } from "@mui/material";
  32. import { GroupItem } from "@/components/profile/group-item";
  33. import { readProfileFile, saveProfileFile } from "@/services/cmds";
  34. import { Notice, Switch } from "@/components/base";
  35. import getSystem from "@/utils/get-system";
  36. import { BaseSearchBox } from "../base/base-search-box";
  37. import { Virtuoso } from "react-virtuoso";
  38. import MonacoEditor from "react-monaco-editor";
  39. import { useThemeMode } from "@/services/states";
  40. import { Controller, useForm } from "react-hook-form";
  41. interface Props {
  42. proxiesUid: string;
  43. mergeUid: string;
  44. profileUid: string;
  45. property: string;
  46. open: boolean;
  47. onClose: () => void;
  48. onSave?: (prev?: string, curr?: string) => void;
  49. }
  50. const builtinProxyPolicies = ["DIRECT", "REJECT", "REJECT-DROP", "PASS"];
  51. export const GroupsEditorViewer = (props: Props) => {
  52. const { mergeUid, proxiesUid, profileUid, property, open, onClose, onSave } =
  53. props;
  54. const { t } = useTranslation();
  55. const themeMode = useThemeMode();
  56. const [prevData, setPrevData] = useState("");
  57. const [currData, setCurrData] = useState("");
  58. const [visualization, setVisualization] = useState(true);
  59. const [match, setMatch] = useState(() => (_: string) => true);
  60. const { control, watch, register, ...formIns } = useForm<IProxyGroupConfig>({
  61. defaultValues: {
  62. type: "select",
  63. name: "",
  64. interval: 300,
  65. timeout: 5000,
  66. "max-failed-times": 5,
  67. lazy: true,
  68. },
  69. });
  70. const [groupList, setGroupList] = useState<IProxyGroupConfig[]>([]);
  71. const [proxyPolicyList, setProxyPolicyList] = useState<string[]>([]);
  72. const [proxyProviderList, setProxyProviderList] = useState<string[]>([]);
  73. const [prependSeq, setPrependSeq] = useState<IProxyGroupConfig[]>([]);
  74. const [appendSeq, setAppendSeq] = useState<IProxyGroupConfig[]>([]);
  75. const [deleteSeq, setDeleteSeq] = useState<string[]>([]);
  76. const filteredGroupList = useMemo(
  77. () => groupList.filter((group) => match(group.name)),
  78. [groupList, match]
  79. );
  80. const sensors = useSensors(
  81. useSensor(PointerSensor),
  82. useSensor(KeyboardSensor, {
  83. coordinateGetter: sortableKeyboardCoordinates,
  84. })
  85. );
  86. const reorder = (
  87. list: IProxyGroupConfig[],
  88. startIndex: number,
  89. endIndex: number
  90. ) => {
  91. const result = Array.from(list);
  92. const [removed] = result.splice(startIndex, 1);
  93. result.splice(endIndex, 0, removed);
  94. return result;
  95. };
  96. const onPrependDragEnd = async (event: DragEndEvent) => {
  97. const { active, over } = event;
  98. if (over) {
  99. if (active.id !== over.id) {
  100. let activeIndex = 0;
  101. let overIndex = 0;
  102. prependSeq.forEach((item, index) => {
  103. if (item.name === active.id) {
  104. activeIndex = index;
  105. }
  106. if (item.name === over.id) {
  107. overIndex = index;
  108. }
  109. });
  110. setPrependSeq(reorder(prependSeq, activeIndex, overIndex));
  111. }
  112. }
  113. };
  114. const onAppendDragEnd = async (event: DragEndEvent) => {
  115. const { active, over } = event;
  116. if (over) {
  117. if (active.id !== over.id) {
  118. let activeIndex = 0;
  119. let overIndex = 0;
  120. appendSeq.forEach((item, index) => {
  121. if (item.name === active.id) {
  122. activeIndex = index;
  123. }
  124. if (item.name === over.id) {
  125. overIndex = index;
  126. }
  127. });
  128. setAppendSeq(reorder(appendSeq, activeIndex, overIndex));
  129. }
  130. }
  131. };
  132. const fetchContent = async () => {
  133. let data = await readProfileFile(property);
  134. let obj = yaml.load(data) as ISeqProfileConfig | null;
  135. setPrependSeq(obj?.prepend || []);
  136. setAppendSeq(obj?.append || []);
  137. setDeleteSeq(obj?.delete || []);
  138. setPrevData(data);
  139. setCurrData(data);
  140. };
  141. useEffect(() => {
  142. if (currData === "") return;
  143. if (visualization !== true) return;
  144. let obj = yaml.load(currData) as {
  145. prepend: [];
  146. append: [];
  147. delete: [];
  148. } | null;
  149. setPrependSeq(obj?.prepend || []);
  150. setAppendSeq(obj?.append || []);
  151. setDeleteSeq(obj?.delete || []);
  152. }, [visualization]);
  153. useEffect(() => {
  154. if (prependSeq && appendSeq && deleteSeq)
  155. setCurrData(
  156. yaml.dump(
  157. { prepend: prependSeq, append: appendSeq, delete: deleteSeq },
  158. {
  159. forceQuotes: true,
  160. }
  161. )
  162. );
  163. }, [prependSeq, appendSeq, deleteSeq]);
  164. const fetchProxyPolicy = async () => {
  165. let data = await readProfileFile(profileUid);
  166. let proxiesData = await readProfileFile(proxiesUid);
  167. let originGroupsObj = yaml.load(data) as {
  168. "proxy-groups": IProxyGroupConfig[];
  169. } | null;
  170. let originProxiesObj = yaml.load(data) as { proxies: [] } | null;
  171. let originProxies = originProxiesObj?.proxies || [];
  172. let moreProxiesObj = yaml.load(proxiesData) as ISeqProfileConfig | null;
  173. let morePrependProxies = moreProxiesObj?.prepend || [];
  174. let moreAppendProxies = moreProxiesObj?.append || [];
  175. let moreDeleteProxies =
  176. moreProxiesObj?.delete || ([] as string[] | { name: string }[]);
  177. let proxies = morePrependProxies.concat(
  178. originProxies.filter((proxy: any) => {
  179. if (proxy.name) {
  180. return !moreDeleteProxies.includes(proxy.name);
  181. } else {
  182. return !moreDeleteProxies.includes(proxy);
  183. }
  184. }),
  185. moreAppendProxies
  186. );
  187. setProxyPolicyList(
  188. builtinProxyPolicies.concat(
  189. prependSeq.map((group: IProxyGroupConfig) => group.name),
  190. originGroupsObj?.["proxy-groups"]
  191. .map((group: IProxyGroupConfig) => group.name)
  192. .filter((name) => !deleteSeq.includes(name)) || [],
  193. appendSeq.map((group: IProxyGroupConfig) => group.name),
  194. proxies.map((proxy: any) => proxy.name)
  195. )
  196. );
  197. };
  198. const fetchProfile = async () => {
  199. let data = await readProfileFile(profileUid);
  200. let mergeData = await readProfileFile(mergeUid);
  201. let globalMergeData = await readProfileFile("Merge");
  202. let originGroupsObj = yaml.load(data) as {
  203. "proxy-groups": IProxyGroupConfig[];
  204. } | null;
  205. let originProviderObj = yaml.load(data) as { "proxy-providers": {} } | null;
  206. let originProvider = originProviderObj?.["proxy-providers"] || {};
  207. let moreProviderObj = yaml.load(mergeData) as {
  208. "proxy-providers": {};
  209. } | null;
  210. let moreProvider = moreProviderObj?.["proxy-providers"] || {};
  211. let globalProviderObj = yaml.load(globalMergeData) as {
  212. "proxy-providers": {};
  213. } | null;
  214. let globalProvider = globalProviderObj?.["proxy-providers"] || {};
  215. let provider = Object.assign(
  216. {},
  217. originProvider,
  218. moreProvider,
  219. globalProvider
  220. );
  221. setProxyProviderList(Object.keys(provider));
  222. setGroupList(originGroupsObj?.["proxy-groups"] || []);
  223. };
  224. useEffect(() => {
  225. fetchProxyPolicy();
  226. }, [prependSeq, appendSeq, deleteSeq]);
  227. useEffect(() => {
  228. if (!open) return;
  229. fetchContent();
  230. fetchProxyPolicy();
  231. fetchProfile();
  232. }, [open]);
  233. const validateGroup = () => {
  234. let group = formIns.getValues();
  235. if (group.name === "") {
  236. throw new Error(t("Group Name Cannot Be Empty"));
  237. }
  238. };
  239. const handleSave = useLockFn(async () => {
  240. try {
  241. await saveProfileFile(property, currData);
  242. onSave?.(prevData, currData);
  243. onClose();
  244. } catch (err: any) {
  245. Notice.error(err.message || err.toString());
  246. }
  247. });
  248. return (
  249. <Dialog open={open} onClose={onClose} maxWidth="xl" fullWidth>
  250. <DialogTitle>
  251. {
  252. <Box display="flex" justifyContent="space-between">
  253. {t("Edit Groups")}
  254. <Box>
  255. <Button
  256. variant="contained"
  257. size="small"
  258. onClick={() => {
  259. setVisualization((prev) => !prev);
  260. }}
  261. >
  262. {visualization ? t("Advanced") : t("Visualization")}
  263. </Button>
  264. </Box>
  265. </Box>
  266. }
  267. </DialogTitle>
  268. <DialogContent
  269. sx={{ display: "flex", width: "auto", height: "calc(100vh - 185px)" }}
  270. >
  271. {visualization ? (
  272. <>
  273. <List
  274. sx={{
  275. width: "50%",
  276. padding: "0 10px",
  277. }}
  278. >
  279. <Box
  280. sx={{
  281. height: "calc(100% - 80px)",
  282. overflowY: "auto",
  283. }}
  284. >
  285. <Controller
  286. name="type"
  287. control={control}
  288. render={({ field }) => (
  289. <Item>
  290. <ListItemText primary={t("Group Type")} />
  291. <Autocomplete
  292. size="small"
  293. sx={{ width: "calc(100% - 150px)" }}
  294. options={[
  295. "select",
  296. "url-test",
  297. "fallback",
  298. "load-balance",
  299. "relay",
  300. ]}
  301. value={field.value}
  302. onChange={(_, value) => value && field.onChange(value)}
  303. renderInput={(params) => <TextField {...params} />}
  304. />
  305. </Item>
  306. )}
  307. />
  308. <Controller
  309. name="name"
  310. control={control}
  311. render={({ field }) => (
  312. <Item>
  313. <ListItemText primary={t("Group Name")} />
  314. <TextField
  315. autoComplete="off"
  316. size="small"
  317. sx={{ width: "calc(100% - 150px)" }}
  318. {...field}
  319. error={field.value === ""}
  320. required={true}
  321. />
  322. </Item>
  323. )}
  324. />
  325. <Controller
  326. name="icon"
  327. control={control}
  328. render={({ field }) => (
  329. <Item>
  330. <ListItemText primary={t("Icon")} />
  331. <TextField
  332. autoComplete="off"
  333. size="small"
  334. sx={{ width: "calc(100% - 150px)" }}
  335. {...field}
  336. />
  337. </Item>
  338. )}
  339. />
  340. <Controller
  341. name="proxies"
  342. control={control}
  343. render={({ field }) => (
  344. <Item>
  345. <ListItemText primary={t("Use Proxies")} />
  346. <Autocomplete
  347. size="small"
  348. sx={{
  349. width: "calc(100% - 150px)",
  350. }}
  351. multiple
  352. options={proxyPolicyList}
  353. onChange={(_, value) => value && field.onChange(value)}
  354. renderInput={(params) => <TextField {...params} />}
  355. />
  356. </Item>
  357. )}
  358. />
  359. <Controller
  360. name="use"
  361. control={control}
  362. render={({ field }) => (
  363. <Item>
  364. <ListItemText primary={t("Use Provider")} />
  365. <Autocomplete
  366. size="small"
  367. sx={{ width: "calc(100% - 150px)" }}
  368. multiple
  369. options={proxyProviderList}
  370. onChange={(_, value) => value && field.onChange(value)}
  371. renderInput={(params) => <TextField {...params} />}
  372. />
  373. </Item>
  374. )}
  375. />
  376. <Controller
  377. name="url"
  378. control={control}
  379. render={({ field }) => (
  380. <Item>
  381. <ListItemText primary={t("Health Check Url")} />
  382. <TextField
  383. autoComplete="off"
  384. size="small"
  385. sx={{ width: "calc(100% - 150px)" }}
  386. {...field}
  387. />
  388. </Item>
  389. )}
  390. />
  391. <Controller
  392. name="interval"
  393. control={control}
  394. render={({ field }) => (
  395. <Item>
  396. <ListItemText primary={t("Interval")} />
  397. <TextField
  398. autoComplete="off"
  399. type="number"
  400. size="small"
  401. sx={{ width: "calc(100% - 150px)" }}
  402. onChange={(e) => {
  403. field.onChange(parseInt(e.target.value));
  404. }}
  405. />
  406. </Item>
  407. )}
  408. />
  409. <Controller
  410. name="timeout"
  411. control={control}
  412. render={({ field }) => (
  413. <Item>
  414. <ListItemText primary={t("Timeout")} />
  415. <TextField
  416. autoComplete="off"
  417. type="number"
  418. size="small"
  419. sx={{ width: "calc(100% - 150px)" }}
  420. onChange={(e) => {
  421. field.onChange(parseInt(e.target.value));
  422. }}
  423. />
  424. </Item>
  425. )}
  426. />
  427. <Controller
  428. name="max-failed-times"
  429. control={control}
  430. render={({ field }) => (
  431. <Item>
  432. <ListItemText primary={t("Max Failed Times")} />
  433. <TextField
  434. autoComplete="off"
  435. type="number"
  436. size="small"
  437. sx={{ width: "calc(100% - 150px)" }}
  438. onChange={(e) => {
  439. field.onChange(parseInt(e.target.value));
  440. }}
  441. />
  442. </Item>
  443. )}
  444. />
  445. <Controller
  446. name="interface-name"
  447. control={control}
  448. render={({ field }) => (
  449. <Item>
  450. <ListItemText primary={t("Interface Name")} />
  451. <TextField
  452. autoComplete="off"
  453. size="small"
  454. sx={{ width: "calc(100% - 150px)" }}
  455. {...field}
  456. />
  457. </Item>
  458. )}
  459. />
  460. <Controller
  461. name="routing-mark"
  462. control={control}
  463. render={({ field }) => (
  464. <Item>
  465. <ListItemText primary={t("Routing Mark")} />
  466. <TextField
  467. autoComplete="off"
  468. type="number"
  469. size="small"
  470. sx={{ width: "calc(100% - 150px)" }}
  471. onChange={(e) => {
  472. field.onChange(parseInt(e.target.value));
  473. }}
  474. />
  475. </Item>
  476. )}
  477. />
  478. <Controller
  479. name="filter"
  480. control={control}
  481. render={({ field }) => (
  482. <Item>
  483. <ListItemText primary={t("Filter")} />
  484. <TextField
  485. autoComplete="off"
  486. size="small"
  487. sx={{ width: "calc(100% - 150px)" }}
  488. {...field}
  489. />
  490. </Item>
  491. )}
  492. />
  493. <Controller
  494. name="exclude-filter"
  495. control={control}
  496. render={({ field }) => (
  497. <Item>
  498. <ListItemText primary={t("Exclude Filter")} />
  499. <TextField
  500. autoComplete="off"
  501. size="small"
  502. sx={{ width: "calc(100% - 150px)" }}
  503. {...field}
  504. />
  505. </Item>
  506. )}
  507. />
  508. <Controller
  509. name="exclude-type"
  510. control={control}
  511. render={({ field }) => (
  512. <Item>
  513. <ListItemText primary={t("Exclude Type")} />
  514. <Autocomplete
  515. multiple
  516. options={[
  517. "ss",
  518. "ssr",
  519. "direct",
  520. "dns",
  521. "snell",
  522. "http",
  523. "trojan",
  524. "hysteria",
  525. "hysteria2",
  526. "tuic",
  527. "wireguard",
  528. "ssh",
  529. "socks5",
  530. "vmess",
  531. "vless",
  532. ]}
  533. size="small"
  534. sx={{ width: "calc(100% - 150px)" }}
  535. value={field.value?.split("|")}
  536. onChange={(_, value) => {
  537. field.onChange(value.join("|"));
  538. }}
  539. renderInput={(params) => <TextField {...params} />}
  540. />
  541. </Item>
  542. )}
  543. />
  544. <Controller
  545. name="expected-status"
  546. control={control}
  547. render={({ field }) => (
  548. <Item>
  549. <ListItemText primary={t("Expected Status")} />
  550. <TextField
  551. autoComplete="off"
  552. type="number"
  553. size="small"
  554. sx={{ width: "calc(100% - 150px)" }}
  555. onChange={(e) => {
  556. field.onChange(parseInt(e.target.value));
  557. }}
  558. />
  559. </Item>
  560. )}
  561. />
  562. <Controller
  563. name="include-all"
  564. control={control}
  565. render={({ field }) => (
  566. <Item>
  567. <ListItemText primary={t("Include All")} />
  568. <Switch checked={field.value} {...field} />
  569. </Item>
  570. )}
  571. />
  572. <Controller
  573. name="include-all-proxies"
  574. control={control}
  575. render={({ field }) => (
  576. <Item>
  577. <ListItemText primary={t("Include All Proxies")} />
  578. <Switch checked={field.value} {...field} />
  579. </Item>
  580. )}
  581. />
  582. <Controller
  583. name="include-all-providers"
  584. control={control}
  585. render={({ field }) => (
  586. <Item>
  587. <ListItemText primary={t("Include All Providers")} />
  588. <Switch checked={field.value} {...field} />
  589. </Item>
  590. )}
  591. />
  592. <Controller
  593. name="lazy"
  594. control={control}
  595. render={({ field }) => (
  596. <Item>
  597. <ListItemText primary={t("Lazy")} />
  598. <Switch checked={field.value} {...field} />
  599. </Item>
  600. )}
  601. />
  602. <Controller
  603. name="disable-udp"
  604. control={control}
  605. render={({ field }) => (
  606. <Item>
  607. <ListItemText primary={t("Disable UDP")} />
  608. <Switch checked={field.value} {...field} />
  609. </Item>
  610. )}
  611. />
  612. <Controller
  613. name="hidden"
  614. control={control}
  615. render={({ field }) => (
  616. <Item>
  617. <ListItemText primary={t("Hidden")} />
  618. <Switch checked={field.value} {...field} />
  619. </Item>
  620. )}
  621. />
  622. </Box>
  623. <Item>
  624. <Button
  625. fullWidth
  626. variant="contained"
  627. onClick={() => {
  628. try {
  629. validateGroup();
  630. for (const item of prependSeq) {
  631. if (item.name === formIns.getValues().name) {
  632. throw new Error(t("Group Name Already Exists"));
  633. }
  634. }
  635. setPrependSeq([...prependSeq, formIns.getValues()]);
  636. } catch (err: any) {
  637. Notice.error(err.message || err.toString());
  638. }
  639. }}
  640. >
  641. {t("Prepend Group")}
  642. </Button>
  643. </Item>
  644. <Item>
  645. <Button
  646. fullWidth
  647. variant="contained"
  648. onClick={() => {
  649. try {
  650. validateGroup();
  651. for (const item of appendSeq) {
  652. if (item.name === formIns.getValues().name) {
  653. throw new Error(t("Group Name Already Exists"));
  654. }
  655. }
  656. setAppendSeq([...appendSeq, formIns.getValues()]);
  657. } catch (err: any) {
  658. Notice.error(err.message || err.toString());
  659. }
  660. }}
  661. >
  662. {t("Append Group")}
  663. </Button>
  664. </Item>
  665. </List>
  666. <List
  667. sx={{
  668. width: "50%",
  669. padding: "0 10px",
  670. }}
  671. >
  672. <BaseSearchBox
  673. matchCase={false}
  674. onSearch={(match) => setMatch(() => match)}
  675. />
  676. <Virtuoso
  677. style={{ height: "calc(100% - 24px)", marginTop: "8px" }}
  678. totalCount={
  679. filteredGroupList.length +
  680. (prependSeq.length > 0 ? 1 : 0) +
  681. (appendSeq.length > 0 ? 1 : 0)
  682. }
  683. increaseViewportBy={256}
  684. itemContent={(index) => {
  685. let shift = prependSeq.length > 0 ? 1 : 0;
  686. if (prependSeq.length > 0 && index === 0) {
  687. return (
  688. <DndContext
  689. sensors={sensors}
  690. collisionDetection={closestCenter}
  691. onDragEnd={onPrependDragEnd}
  692. >
  693. <SortableContext
  694. items={prependSeq.map((x) => {
  695. return x.name;
  696. })}
  697. >
  698. {prependSeq.map((item, index) => {
  699. return (
  700. <GroupItem
  701. key={`${item.name}-${index}`}
  702. type="prepend"
  703. group={item}
  704. onDelete={() => {
  705. setPrependSeq(
  706. prependSeq.filter(
  707. (v) => v.name !== item.name
  708. )
  709. );
  710. }}
  711. />
  712. );
  713. })}
  714. </SortableContext>
  715. </DndContext>
  716. );
  717. } else if (index < filteredGroupList.length + shift) {
  718. let newIndex = index - shift;
  719. return (
  720. <GroupItem
  721. key={`${filteredGroupList[newIndex].name}-${index}`}
  722. type={
  723. deleteSeq.includes(filteredGroupList[newIndex].name)
  724. ? "delete"
  725. : "original"
  726. }
  727. group={filteredGroupList[newIndex]}
  728. onDelete={() => {
  729. if (
  730. deleteSeq.includes(filteredGroupList[newIndex].name)
  731. ) {
  732. setDeleteSeq(
  733. deleteSeq.filter(
  734. (v) => v !== filteredGroupList[newIndex].name
  735. )
  736. );
  737. } else {
  738. setDeleteSeq((prev) => [
  739. ...prev,
  740. filteredGroupList[newIndex].name,
  741. ]);
  742. }
  743. }}
  744. />
  745. );
  746. } else {
  747. return (
  748. <DndContext
  749. sensors={sensors}
  750. collisionDetection={closestCenter}
  751. onDragEnd={onAppendDragEnd}
  752. >
  753. <SortableContext
  754. items={appendSeq.map((x) => {
  755. return x.name;
  756. })}
  757. >
  758. {appendSeq.map((item, index) => {
  759. return (
  760. <GroupItem
  761. key={`${item.name}-${index}`}
  762. type="append"
  763. group={item}
  764. onDelete={() => {
  765. setAppendSeq(
  766. appendSeq.filter(
  767. (v) => v.name !== item.name
  768. )
  769. );
  770. }}
  771. />
  772. );
  773. })}
  774. </SortableContext>
  775. </DndContext>
  776. );
  777. }
  778. }}
  779. />
  780. </List>
  781. </>
  782. ) : (
  783. <MonacoEditor
  784. height="100%"
  785. language="yaml"
  786. value={currData}
  787. theme={themeMode === "light" ? "vs" : "vs-dark"}
  788. options={{
  789. tabSize: 2, // 根据语言类型设置缩进大小
  790. minimap: {
  791. enabled: document.documentElement.clientWidth >= 1500, // 超过一定宽度显示minimap滚动条
  792. },
  793. mouseWheelZoom: true, // 按住Ctrl滚轮调节缩放比例
  794. quickSuggestions: {
  795. strings: true, // 字符串类型的建议
  796. comments: true, // 注释类型的建议
  797. other: true, // 其他类型的建议
  798. },
  799. padding: {
  800. top: 33, // 顶部padding防止遮挡snippets
  801. },
  802. fontFamily: `Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji"${
  803. getSystem() === "windows" ? ", twemoji mozilla" : ""
  804. }`,
  805. fontLigatures: true, // 连字符
  806. smoothScrolling: true, // 平滑滚动
  807. }}
  808. onChange={(value) => setCurrData(value)}
  809. />
  810. )}
  811. </DialogContent>
  812. <DialogActions>
  813. <Button onClick={onClose} variant="outlined">
  814. {t("Cancel")}
  815. </Button>
  816. <Button onClick={handleSave} variant="contained">
  817. {t("Save")}
  818. </Button>
  819. </DialogActions>
  820. </Dialog>
  821. );
  822. };
  823. const Item = styled(ListItem)(() => ({
  824. padding: "5px 2px",
  825. }));