rules-editor-viewer.tsx 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718
  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 { readProfileFile, saveProfileFile } from "@/services/cmds";
  33. import { Notice, Switch } from "@/components/base";
  34. import getSystem from "@/utils/get-system";
  35. import { RuleItem } from "@/components/profile/rule-item";
  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. interface Props {
  41. groupsUid: string;
  42. mergeUid: string;
  43. profileUid: string;
  44. property: string;
  45. open: boolean;
  46. onClose: () => void;
  47. onSave?: (prev?: string, curr?: string) => void;
  48. }
  49. const portValidator = (value: string): boolean => {
  50. return new RegExp(
  51. "^(?:[1-9]\\d{0,3}|[1-5]\\d{4}|6[0-4]\\d{3}|65[0-4]\\d{2}|655[0-2]\\d|6553[0-5])$"
  52. ).test(value);
  53. };
  54. const ipv4CIDRValidator = (value: string): boolean => {
  55. return new RegExp(
  56. "^(?:(?:[1-9]?[0-9]|1[0-9][0-9]|2(?:[0-4][0-9]|5[0-5]))\\.){3}(?:[1-9]?[0-9]|1[0-9][0-9]|2(?:[0-4][0-9]|5[0-5]))(?:\\/(?:[12]?[0-9]|3[0-2]))$"
  57. ).test(value);
  58. };
  59. const ipv6CIDRValidator = (value: string): boolean => {
  60. return new RegExp(
  61. "^([0-9a-fA-F]{1,4}(?::[0-9a-fA-F]{1,4}){7}|::|:(?::[0-9a-fA-F]{1,4}){1,6}|[0-9a-fA-F]{1,4}:(?::[0-9a-fA-F]{1,4}){1,5}|(?:[0-9a-fA-F]{1,4}:){2}(?::[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){3}(?::[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){4}(?::[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){5}:[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,6}:)\\/(?:12[0-8]|1[01][0-9]|[1-9]?[0-9])$"
  62. ).test(value);
  63. };
  64. const rules: {
  65. name: string;
  66. required?: boolean;
  67. example?: string;
  68. noResolve?: boolean;
  69. validator?: (value: string) => boolean;
  70. }[] = [
  71. {
  72. name: "DOMAIN",
  73. example: "example.com",
  74. },
  75. {
  76. name: "DOMAIN-SUFFIX",
  77. example: "example.com",
  78. },
  79. {
  80. name: "DOMAIN-KEYWORD",
  81. example: "example",
  82. },
  83. {
  84. name: "DOMAIN-REGEX",
  85. example: "example.*",
  86. },
  87. {
  88. name: "GEOSITE",
  89. example: "youtube",
  90. },
  91. {
  92. name: "GEOIP",
  93. example: "CN",
  94. noResolve: true,
  95. },
  96. {
  97. name: "SRC-GEOIP",
  98. example: "CN",
  99. },
  100. {
  101. name: "IP-ASN",
  102. example: "13335",
  103. noResolve: true,
  104. validator: (value) => (+value ? true : false),
  105. },
  106. {
  107. name: "SRC-IP-ASN",
  108. example: "9808",
  109. validator: (value) => (+value ? true : false),
  110. },
  111. {
  112. name: "IP-CIDR",
  113. example: "127.0.0.0/8",
  114. noResolve: true,
  115. validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value),
  116. },
  117. {
  118. name: "IP-CIDR6",
  119. example: "2620:0:2d0:200::7/32",
  120. noResolve: true,
  121. validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value),
  122. },
  123. {
  124. name: "SRC-IP-CIDR",
  125. example: "192.168.1.201/32",
  126. validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value),
  127. },
  128. {
  129. name: "IP-SUFFIX",
  130. example: "8.8.8.8/24",
  131. noResolve: true,
  132. validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value),
  133. },
  134. {
  135. name: "SRC-IP-SUFFIX",
  136. example: "192.168.1.201/8",
  137. validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value),
  138. },
  139. {
  140. name: "SRC-PORT",
  141. example: "7777",
  142. validator: (value) => portValidator(value),
  143. },
  144. {
  145. name: "DST-PORT",
  146. example: "80",
  147. validator: (value) => portValidator(value),
  148. },
  149. {
  150. name: "IN-PORT",
  151. example: "7890",
  152. validator: (value) => portValidator(value),
  153. },
  154. {
  155. name: "DSCP",
  156. example: "4",
  157. },
  158. {
  159. name: "PROCESS-NAME",
  160. example: getSystem() === "windows" ? "chrome.exe" : "curl",
  161. },
  162. {
  163. name: "PROCESS-PATH",
  164. example:
  165. getSystem() === "windows"
  166. ? "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe"
  167. : "/usr/bin/wget",
  168. },
  169. {
  170. name: "PROCESS-NAME-REGEX",
  171. example: ".*telegram.*",
  172. },
  173. {
  174. name: "PROCESS-PATH-REGEX",
  175. example:
  176. getSystem() === "windows" ? "(?i).*Application\\chrome.*" : ".*bin/wget",
  177. },
  178. {
  179. name: "NETWORK",
  180. example: "udp",
  181. validator: (value) => ["tcp", "udp"].includes(value),
  182. },
  183. {
  184. name: "UID",
  185. example: "1001",
  186. validator: (value) => (+value ? true : false),
  187. },
  188. {
  189. name: "IN-TYPE",
  190. example: "SOCKS/HTTP",
  191. },
  192. {
  193. name: "IN-USER",
  194. example: "mihomo",
  195. },
  196. {
  197. name: "IN-NAME",
  198. example: "ss",
  199. },
  200. {
  201. name: "SUB-RULE",
  202. example: "(NETWORK,tcp)",
  203. },
  204. {
  205. name: "RULE-SET",
  206. example: "providername",
  207. noResolve: true,
  208. },
  209. {
  210. name: "AND",
  211. example: "((DOMAIN,baidu.com),(NETWORK,UDP))",
  212. },
  213. {
  214. name: "OR",
  215. example: "((NETWORK,UDP),(DOMAIN,baidu.com))",
  216. },
  217. {
  218. name: "NOT",
  219. example: "((DOMAIN,baidu.com))",
  220. },
  221. {
  222. name: "MATCH",
  223. required: false,
  224. },
  225. ];
  226. const builtinProxyPolicies = ["DIRECT", "REJECT", "REJECT-DROP", "PASS"];
  227. export const RulesEditorViewer = (props: Props) => {
  228. const { groupsUid, mergeUid, profileUid, property, open, onClose, onSave } =
  229. props;
  230. const { t } = useTranslation();
  231. const themeMode = useThemeMode();
  232. const [prevData, setPrevData] = useState("");
  233. const [currData, setCurrData] = useState("");
  234. const [visualization, setVisualization] = useState(true);
  235. const [match, setMatch] = useState(() => (_: string) => true);
  236. const [ruleType, setRuleType] = useState<(typeof rules)[number]>(rules[0]);
  237. const [ruleContent, setRuleContent] = useState("");
  238. const [noResolve, setNoResolve] = useState(false);
  239. const [proxyPolicy, setProxyPolicy] = useState(builtinProxyPolicies[0]);
  240. const [proxyPolicyList, setProxyPolicyList] = useState<string[]>([]);
  241. const [ruleList, setRuleList] = useState<string[]>([]);
  242. const [ruleSetList, setRuleSetList] = useState<string[]>([]);
  243. const [subRuleList, setSubRuleList] = useState<string[]>([]);
  244. const [prependSeq, setPrependSeq] = useState<string[]>([]);
  245. const [appendSeq, setAppendSeq] = useState<string[]>([]);
  246. const [deleteSeq, setDeleteSeq] = useState<string[]>([]);
  247. const filteredRuleList = useMemo(
  248. () => ruleList.filter((rule) => match(rule)),
  249. [ruleList, match]
  250. );
  251. const sensors = useSensors(
  252. useSensor(PointerSensor),
  253. useSensor(KeyboardSensor, {
  254. coordinateGetter: sortableKeyboardCoordinates,
  255. })
  256. );
  257. const reorder = (list: string[], startIndex: number, endIndex: number) => {
  258. const result = Array.from(list);
  259. const [removed] = result.splice(startIndex, 1);
  260. result.splice(endIndex, 0, removed);
  261. return result;
  262. };
  263. const onPrependDragEnd = async (event: DragEndEvent) => {
  264. const { active, over } = event;
  265. if (over) {
  266. if (active.id !== over.id) {
  267. let activeIndex = prependSeq.indexOf(active.id.toString());
  268. let overIndex = prependSeq.indexOf(over.id.toString());
  269. setPrependSeq(reorder(prependSeq, activeIndex, overIndex));
  270. }
  271. }
  272. };
  273. const onAppendDragEnd = async (event: DragEndEvent) => {
  274. const { active, over } = event;
  275. if (over) {
  276. if (active.id !== over.id) {
  277. let activeIndex = appendSeq.indexOf(active.id.toString());
  278. let overIndex = appendSeq.indexOf(over.id.toString());
  279. setAppendSeq(reorder(appendSeq, activeIndex, overIndex));
  280. }
  281. }
  282. };
  283. const fetchContent = async () => {
  284. let data = await readProfileFile(property);
  285. let obj = yaml.load(data) as ISeqProfileConfig | null;
  286. setPrependSeq(obj?.prepend || []);
  287. setAppendSeq(obj?.append || []);
  288. setDeleteSeq(obj?.delete || []);
  289. setPrevData(data);
  290. setCurrData(data);
  291. };
  292. useEffect(() => {
  293. if (currData === "") return;
  294. if (visualization !== true) return;
  295. let obj = yaml.load(currData) as ISeqProfileConfig | null;
  296. setPrependSeq(obj?.prepend || []);
  297. setAppendSeq(obj?.append || []);
  298. setDeleteSeq(obj?.delete || []);
  299. }, [visualization]);
  300. useEffect(() => {
  301. if (prependSeq && appendSeq && deleteSeq)
  302. setCurrData(
  303. yaml.dump(
  304. { prepend: prependSeq, append: appendSeq, delete: deleteSeq },
  305. {
  306. forceQuotes: true,
  307. }
  308. )
  309. );
  310. }, [prependSeq, appendSeq, deleteSeq]);
  311. const fetchProfile = async () => {
  312. let data = await readProfileFile(profileUid); // 原配置文件
  313. let groupsData = await readProfileFile(groupsUid); // groups配置文件
  314. let mergeData = await readProfileFile(mergeUid); // merge配置文件
  315. let globalMergeData = await readProfileFile("Merge"); // global merge配置文件
  316. let rulesObj = yaml.load(data) as { rules: [] } | null;
  317. let originGroupsObj = yaml.load(data) as { "proxy-groups": [] } | null;
  318. let originGroups = originGroupsObj?.["proxy-groups"] || [];
  319. let moreGroupsObj = yaml.load(groupsData) as ISeqProfileConfig | null;
  320. let morePrependGroups = moreGroupsObj?.["prepend"] || [];
  321. let moreAppendGroups = moreGroupsObj?.["append"] || [];
  322. let moreDeleteGroups =
  323. moreGroupsObj?.["delete"] || ([] as string[] | { name: string }[]);
  324. let groups = morePrependGroups.concat(
  325. originGroups.filter((group: any) => {
  326. if (group.name) {
  327. return !moreDeleteGroups.includes(group.name);
  328. } else {
  329. return !moreDeleteGroups.includes(group);
  330. }
  331. }),
  332. moreAppendGroups
  333. );
  334. let originRuleSetObj = yaml.load(data) as { "rule-providers": {} } | null;
  335. let originRuleSet = originRuleSetObj?.["rule-providers"] || {};
  336. let moreRuleSetObj = yaml.load(mergeData) as {
  337. "rule-providers": {};
  338. } | null;
  339. let moreRuleSet = moreRuleSetObj?.["rule-providers"] || {};
  340. let globalRuleSetObj = yaml.load(globalMergeData) as {
  341. "rule-providers": {};
  342. } | null;
  343. let globalRuleSet = globalRuleSetObj?.["rule-providers"] || {};
  344. let ruleSet = Object.assign({}, originRuleSet, moreRuleSet, globalRuleSet);
  345. let originSubRuleObj = yaml.load(data) as { "sub-rules": {} } | null;
  346. let originSubRule = originSubRuleObj?.["sub-rules"] || {};
  347. let moreSubRuleObj = yaml.load(mergeData) as { "sub-rules": {} } | null;
  348. let moreSubRule = moreSubRuleObj?.["sub-rules"] || {};
  349. let globalSubRuleObj = yaml.load(globalMergeData) as {
  350. "sub-rules": {};
  351. } | null;
  352. let globalSubRule = globalSubRuleObj?.["sub-rules"] || {};
  353. let subRule = Object.assign({}, originSubRule, moreSubRule, globalSubRule);
  354. setProxyPolicyList(
  355. builtinProxyPolicies.concat(groups.map((group: any) => group.name))
  356. );
  357. setRuleSetList(Object.keys(ruleSet));
  358. setSubRuleList(Object.keys(subRule));
  359. setRuleList(rulesObj?.rules || []);
  360. };
  361. useEffect(() => {
  362. if (!open) return;
  363. fetchContent();
  364. fetchProfile();
  365. }, [open]);
  366. const validateRule = () => {
  367. if ((ruleType.required ?? true) && !ruleContent) {
  368. throw new Error(t("Rule Condition Required"));
  369. }
  370. if (ruleType.validator && !ruleType.validator(ruleContent)) {
  371. throw new Error(t("Invalid Rule"));
  372. }
  373. const condition = ruleType.required ?? true ? ruleContent : "";
  374. return `${ruleType.name}${condition ? "," + condition : ""},${proxyPolicy}${
  375. ruleType.noResolve && noResolve ? ",no-resolve" : ""
  376. }`;
  377. };
  378. const handleSave = useLockFn(async () => {
  379. try {
  380. await saveProfileFile(property, currData);
  381. onSave?.(prevData, currData);
  382. onClose();
  383. } catch (err: any) {
  384. Notice.error(err.message || err.toString());
  385. }
  386. });
  387. return (
  388. <Dialog open={open} onClose={onClose} maxWidth="xl" fullWidth>
  389. <DialogTitle>
  390. {
  391. <Box display="flex" justifyContent="space-between">
  392. {t("Edit Rules")}
  393. <Box>
  394. <Button
  395. variant="contained"
  396. size="small"
  397. onClick={() => {
  398. setVisualization((prev) => !prev);
  399. }}
  400. >
  401. {visualization ? t("Advanced") : t("Visualization")}
  402. </Button>
  403. </Box>
  404. </Box>
  405. }
  406. </DialogTitle>
  407. <DialogContent
  408. sx={{ display: "flex", width: "auto", height: "calc(100vh - 185px)" }}
  409. >
  410. {visualization ? (
  411. <>
  412. <List
  413. sx={{
  414. width: "50%",
  415. padding: "0 10px",
  416. }}
  417. >
  418. <Item>
  419. <ListItemText primary={t("Rule Type")} />
  420. <Autocomplete
  421. size="small"
  422. sx={{ minWidth: "240px" }}
  423. renderInput={(params) => <TextField {...params} />}
  424. options={rules}
  425. value={ruleType}
  426. getOptionLabel={(option) => option.name}
  427. renderOption={(props, option) => (
  428. <li {...props} title={t(option.name)}>
  429. {option.name}
  430. </li>
  431. )}
  432. onChange={(_, value) => value && setRuleType(value)}
  433. />
  434. </Item>
  435. <Item
  436. sx={{ display: !(ruleType.required ?? true) ? "none" : "" }}
  437. >
  438. <ListItemText primary={t("Rule Content")} />
  439. {ruleType.name === "RULE-SET" && (
  440. <Autocomplete
  441. size="small"
  442. sx={{ minWidth: "240px" }}
  443. renderInput={(params) => <TextField {...params} />}
  444. options={ruleSetList}
  445. value={ruleContent}
  446. onChange={(_, value) => value && setRuleContent(value)}
  447. />
  448. )}
  449. {ruleType.name === "SUB-RULE" && (
  450. <Autocomplete
  451. size="small"
  452. sx={{ minWidth: "240px" }}
  453. renderInput={(params) => <TextField {...params} />}
  454. options={subRuleList}
  455. value={ruleContent}
  456. onChange={(_, value) => value && setRuleContent(value)}
  457. />
  458. )}
  459. {ruleType.name !== "RULE-SET" &&
  460. ruleType.name !== "SUB-RULE" && (
  461. <TextField
  462. autoComplete="off"
  463. size="small"
  464. sx={{ minWidth: "240px" }}
  465. value={ruleContent}
  466. required={ruleType.required ?? true}
  467. error={(ruleType.required ?? true) && !ruleContent}
  468. placeholder={ruleType.example}
  469. onChange={(e) => setRuleContent(e.target.value)}
  470. />
  471. )}
  472. </Item>
  473. <Item>
  474. <ListItemText primary={t("Proxy Policy")} />
  475. <Autocomplete
  476. size="small"
  477. sx={{ minWidth: "240px" }}
  478. renderInput={(params) => <TextField {...params} />}
  479. options={proxyPolicyList}
  480. value={proxyPolicy}
  481. renderOption={(props, option) => (
  482. <li {...props} title={t(option)}>
  483. {option}
  484. </li>
  485. )}
  486. onChange={(_, value) => value && setProxyPolicy(value)}
  487. />
  488. </Item>
  489. {ruleType.noResolve && (
  490. <Item>
  491. <ListItemText primary={t("No Resolve")} />
  492. <Switch
  493. checked={noResolve}
  494. onChange={() => setNoResolve(!noResolve)}
  495. />
  496. </Item>
  497. )}
  498. <Item>
  499. <Button
  500. fullWidth
  501. variant="contained"
  502. onClick={() => {
  503. try {
  504. let raw = validateRule();
  505. if (prependSeq.includes(raw)) return;
  506. setPrependSeq([...prependSeq, raw]);
  507. } catch (err: any) {
  508. Notice.error(err.message || err.toString());
  509. }
  510. }}
  511. >
  512. {t("Prepend Rule")}
  513. </Button>
  514. </Item>
  515. <Item>
  516. <Button
  517. fullWidth
  518. variant="contained"
  519. onClick={() => {
  520. try {
  521. let raw = validateRule();
  522. if (appendSeq.includes(raw)) return;
  523. setAppendSeq([...appendSeq, raw]);
  524. } catch (err: any) {
  525. Notice.error(err.message || err.toString());
  526. }
  527. }}
  528. >
  529. {t("Append Rule")}
  530. </Button>
  531. </Item>
  532. </List>
  533. <List
  534. sx={{
  535. width: "50%",
  536. padding: "0 10px",
  537. }}
  538. >
  539. <BaseSearchBox
  540. matchCase={false}
  541. onSearch={(match) => setMatch(() => match)}
  542. />
  543. <Virtuoso
  544. style={{ height: "calc(100% - 24px)", marginTop: "8px" }}
  545. totalCount={
  546. filteredRuleList.length +
  547. (prependSeq.length > 0 ? 1 : 0) +
  548. (appendSeq.length > 0 ? 1 : 0)
  549. }
  550. increaseViewportBy={256}
  551. itemContent={(index) => {
  552. let shift = prependSeq.length > 0 ? 1 : 0;
  553. if (prependSeq.length > 0 && index === 0) {
  554. return (
  555. <DndContext
  556. sensors={sensors}
  557. collisionDetection={closestCenter}
  558. onDragEnd={onPrependDragEnd}
  559. >
  560. <SortableContext
  561. items={prependSeq.map((x) => {
  562. return x;
  563. })}
  564. >
  565. {prependSeq.map((item, index) => {
  566. return (
  567. <RuleItem
  568. key={`${item}-${index}`}
  569. type="prepend"
  570. ruleRaw={item}
  571. onDelete={() => {
  572. setPrependSeq(
  573. prependSeq.filter((v) => v !== item)
  574. );
  575. }}
  576. />
  577. );
  578. })}
  579. </SortableContext>
  580. </DndContext>
  581. );
  582. } else if (index < filteredRuleList.length + shift) {
  583. let newIndex = index - shift;
  584. return (
  585. <RuleItem
  586. key={`${filteredRuleList[newIndex]}-${index}`}
  587. type={
  588. deleteSeq.includes(filteredRuleList[newIndex])
  589. ? "delete"
  590. : "original"
  591. }
  592. ruleRaw={filteredRuleList[newIndex]}
  593. onDelete={() => {
  594. if (deleteSeq.includes(filteredRuleList[newIndex])) {
  595. setDeleteSeq(
  596. deleteSeq.filter(
  597. (v) => v !== filteredRuleList[newIndex]
  598. )
  599. );
  600. } else {
  601. setDeleteSeq((prev) => [
  602. ...prev,
  603. filteredRuleList[newIndex],
  604. ]);
  605. }
  606. }}
  607. />
  608. );
  609. } else {
  610. return (
  611. <DndContext
  612. sensors={sensors}
  613. collisionDetection={closestCenter}
  614. onDragEnd={onAppendDragEnd}
  615. >
  616. <SortableContext
  617. items={appendSeq.map((x) => {
  618. return x;
  619. })}
  620. >
  621. {appendSeq.map((item, index) => {
  622. return (
  623. <RuleItem
  624. key={`${item}-${index}`}
  625. type="append"
  626. ruleRaw={item}
  627. onDelete={() => {
  628. setAppendSeq(
  629. appendSeq.filter((v) => v !== item)
  630. );
  631. }}
  632. />
  633. );
  634. })}
  635. </SortableContext>
  636. </DndContext>
  637. );
  638. }
  639. }}
  640. />
  641. </List>
  642. </>
  643. ) : (
  644. <MonacoEditor
  645. height="100%"
  646. language="yaml"
  647. value={currData}
  648. theme={themeMode === "light" ? "vs" : "vs-dark"}
  649. options={{
  650. tabSize: 2, // 根据语言类型设置缩进大小
  651. minimap: {
  652. enabled: document.documentElement.clientWidth >= 1500, // 超过一定宽度显示minimap滚动条
  653. },
  654. mouseWheelZoom: true, // 按住Ctrl滚轮调节缩放比例
  655. quickSuggestions: {
  656. strings: true, // 字符串类型的建议
  657. comments: true, // 注释类型的建议
  658. other: true, // 其他类型的建议
  659. },
  660. padding: {
  661. top: 33, // 顶部padding防止遮挡snippets
  662. },
  663. fontFamily: `Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji"${
  664. getSystem() === "windows" ? ", twemoji mozilla" : ""
  665. }`,
  666. fontLigatures: true, // 连字符
  667. smoothScrolling: true, // 平滑滚动
  668. }}
  669. onChange={(value) => setCurrData(value)}
  670. />
  671. )}
  672. </DialogContent>
  673. <DialogActions>
  674. <Button onClick={onClose} variant="outlined">
  675. {t("Cancel")}
  676. </Button>
  677. <Button onClick={handleSave} variant="contained">
  678. {t("Save")}
  679. </Button>
  680. </DialogActions>
  681. </Dialog>
  682. );
  683. };
  684. const Item = styled(ListItem)(() => ({
  685. padding: "5px 2px",
  686. }));