Files
clash-verge-rev/src/components/profile/groups-editor-viewer.tsx

1144 lines
38 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
DndContext,
DragEndEvent,
KeyboardSensor,
PointerSensor,
closestCenter,
useSensor,
useSensors,
} from "@dnd-kit/core";
import {
SortableContext,
sortableKeyboardCoordinates,
} from "@dnd-kit/sortable";
import MonacoEditor from "@monaco-editor/react";
import {
VerticalAlignBottomRounded,
VerticalAlignTopRounded,
} from "@mui/icons-material";
import {
Autocomplete,
Box,
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
InputAdornment,
List,
ListItem,
ListItemText,
TextField,
styled,
} from "@mui/material";
import { useLockFn } from "ahooks";
import {
cancelIdleCallback,
requestIdleCallback,
} from "foxact/request-idle-callback";
import yaml from "js-yaml";
import {
startTransition,
useCallback,
useEffect,
useMemo,
useState,
} from "react";
import { Controller, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { Virtuoso } from "react-virtuoso";
import { BaseSearchBox, Switch } from "@/components/base";
import { GroupItem } from "@/components/profile/group-item";
import {
getNetworkInterfaces,
readProfileFile,
saveProfileFile,
} from "@/services/cmds";
import { showNotice } from "@/services/notice-service";
import { useThemeMode } from "@/services/states";
import type { TranslationKey } from "@/types/generated/i18n-keys";
import getSystem from "@/utils/get-system";
interface Props {
proxiesUid: string;
mergeUid: string;
profileUid: string;
property: string;
open: boolean;
onClose: () => void;
onSave?: (prev?: string, curr?: string) => void;
}
const builtinProxyPolicies = ["DIRECT", "REJECT", "REJECT-DROP", "PASS"];
const PROXY_STRATEGY_LABEL_KEYS: Record<string, TranslationKey> = {
select: "proxies.components.enums.strategies.select",
"url-test": "proxies.components.enums.strategies.url-test",
fallback: "proxies.components.enums.strategies.fallback",
"load-balance": "proxies.components.enums.strategies.load-balance",
relay: "proxies.components.enums.strategies.relay",
};
const PROXY_POLICY_LABEL_KEYS: Record<string, TranslationKey> =
builtinProxyPolicies.reduce(
(acc, policy) => {
acc[policy] =
`proxies.components.enums.policies.${policy}` as TranslationKey;
return acc;
},
{} as Record<string, TranslationKey>,
);
const normalizeDeleteSeq = (input?: unknown): string[] => {
if (!Array.isArray(input)) {
return [];
}
const names = input
.map((item) => {
if (typeof item === "string") {
return item;
}
if (
item &&
typeof item === "object" &&
"name" in item &&
typeof (item as { name: unknown }).name === "string"
) {
return (item as { name: string }).name;
}
return undefined;
})
.filter(
(name): name is string => typeof name === "string" && name.length > 0,
);
return Array.from(new Set(names));
};
const buildGroupsYaml = (
prepend: IProxyGroupConfig[],
append: IProxyGroupConfig[],
deleteList: string[],
) => {
return yaml.dump(
{
prepend,
append,
delete: deleteList,
},
{ forceQuotes: true },
);
};
export const GroupsEditorViewer = (props: Props) => {
const { mergeUid, proxiesUid, profileUid, property, open, onClose, onSave } =
props;
const { t } = useTranslation();
const translateStrategy = useCallback(
(value: string) =>
PROXY_STRATEGY_LABEL_KEYS[value]
? t(PROXY_STRATEGY_LABEL_KEYS[value])
: value,
[t],
);
const translatePolicy = useCallback(
(value: string) =>
PROXY_POLICY_LABEL_KEYS[value]
? t(PROXY_POLICY_LABEL_KEYS[value])
: value,
[t],
);
const themeMode = useThemeMode();
const [prevData, setPrevData] = useState("");
const [currData, setCurrData] = useState("");
const [visualization, setVisualization] = useState(true);
const [match, setMatch] = useState(() => (_: string) => true);
const [interfaceNameList, setInterfaceNameList] = useState<string[]>([]);
const { control, ...formIns } = useForm<IProxyGroupConfig>({
defaultValues: {
type: "select",
name: "",
interval: 300,
timeout: 5000,
"max-failed-times": 5,
lazy: true,
},
});
const [groupList, setGroupList] = useState<IProxyGroupConfig[]>([]);
const [proxyPolicyList, setProxyPolicyList] = useState<string[]>([]);
const [proxyProviderList, setProxyProviderList] = useState<string[]>([]);
const [prependSeq, setPrependSeq] = useState<IProxyGroupConfig[]>([]);
const [appendSeq, setAppendSeq] = useState<IProxyGroupConfig[]>([]);
const [deleteSeq, setDeleteSeq] = useState<string[]>([]);
const filteredPrependSeq = useMemo(
() => prependSeq.filter((group) => match(group.name)),
[prependSeq, match],
);
const filteredGroupList = useMemo(
() => groupList.filter((group) => match(group.name)),
[groupList, match],
);
const filteredAppendSeq = useMemo(
() => appendSeq.filter((group) => match(group.name)),
[appendSeq, match],
);
const sensors = useSensors(
useSensor(PointerSensor),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
}),
);
const reorder = (
list: IProxyGroupConfig[],
startIndex: number,
endIndex: number,
) => {
const result = Array.from(list);
const [removed] = result.splice(startIndex, 1);
result.splice(endIndex, 0, removed);
return result;
};
const onPrependDragEnd = async (event: DragEndEvent) => {
const { active, over } = event;
if (over) {
if (active.id !== over.id) {
let activeIndex = 0;
let overIndex = 0;
prependSeq.forEach((item, index) => {
if (item.name === active.id) {
activeIndex = index;
}
if (item.name === over.id) {
overIndex = index;
}
});
setPrependSeq(reorder(prependSeq, activeIndex, overIndex));
}
}
};
const onAppendDragEnd = async (event: DragEndEvent) => {
const { active, over } = event;
if (over) {
if (active.id !== over.id) {
let activeIndex = 0;
let overIndex = 0;
appendSeq.forEach((item, index) => {
if (item.name === active.id) {
activeIndex = index;
}
if (item.name === over.id) {
overIndex = index;
}
});
setAppendSeq(reorder(appendSeq, activeIndex, overIndex));
}
}
};
const fetchContent = useCallback(async () => {
const data = await readProfileFile(property);
const obj = yaml.load(data) as ISeqProfileConfig | null;
setPrependSeq(obj?.prepend || []);
setAppendSeq(obj?.append || []);
setDeleteSeq((prev) => {
const normalized = normalizeDeleteSeq(obj?.delete);
if (
normalized.length === prev.length &&
normalized.every((item, index) => item === prev[index])
) {
return prev;
}
return normalized;
});
setPrevData(data);
setCurrData(data);
}, [property]);
useEffect(() => {
if (currData === "" || visualization !== true) {
return;
}
const obj = yaml.load(currData) as ISeqProfileConfig | null;
startTransition(() => {
setPrependSeq(obj?.prepend ?? []);
setAppendSeq(obj?.append ?? []);
setDeleteSeq((prev) => {
const normalized = normalizeDeleteSeq(obj?.delete);
if (
normalized.length === prev.length &&
normalized.every((item, index) => item === prev[index])
) {
return prev;
}
return normalized;
});
});
}, [currData, visualization]);
// 优化异步处理大数据yaml.dump避免UI卡死
useEffect(() => {
if (prependSeq && appendSeq && deleteSeq) {
const serialize = () => {
try {
setCurrData(buildGroupsYaml(prependSeq, appendSeq, deleteSeq));
} catch (e) {
console.warn("[GroupsEditorViewer] yaml.dump failed:", e);
// 防止异常导致UI卡死
}
};
const handle = requestIdleCallback(serialize);
return () => {
cancelIdleCallback(handle);
};
}
}, [prependSeq, appendSeq, deleteSeq]);
const fetchProxyPolicy = useCallback(async () => {
const data = await readProfileFile(profileUid);
const proxiesData = await readProfileFile(proxiesUid);
const originGroupsObj = yaml.load(data) as {
"proxy-groups": IProxyGroupConfig[];
} | null;
const originProxiesObj = yaml.load(data) as { proxies: [] } | null;
const originProxies = originProxiesObj?.proxies || [];
const moreProxiesObj = yaml.load(proxiesData) as ISeqProfileConfig | null;
const morePrependProxies = moreProxiesObj?.prepend || [];
const moreAppendProxies = moreProxiesObj?.append || [];
const moreDeleteProxies = normalizeDeleteSeq(moreProxiesObj?.delete);
const proxies = morePrependProxies.concat(
originProxies.filter((proxy: any) => {
const proxyName =
typeof proxy === "string"
? proxy
: (proxy?.name as string | undefined);
return proxyName ? !moreDeleteProxies.includes(proxyName) : true;
}),
moreAppendProxies,
);
const proxyNames = proxies
.map((proxy: any) =>
typeof proxy === "string" ? proxy : (proxy?.name as string | undefined),
)
.filter(
(name): name is string => typeof name === "string" && name.length > 0,
);
const computedPolicyList = builtinProxyPolicies.concat(
prependSeq.map((group: IProxyGroupConfig) => group.name),
(originGroupsObj?.["proxy-groups"] || [])
.map((group: IProxyGroupConfig) => group.name)
.filter((name) => !deleteSeq.includes(name)),
appendSeq.map((group: IProxyGroupConfig) => group.name),
proxyNames,
);
setProxyPolicyList(Array.from(new Set(computedPolicyList)));
}, [appendSeq, deleteSeq, prependSeq, profileUid, proxiesUid]);
const fetchProfile = useCallback(async () => {
const data = await readProfileFile(profileUid);
const mergeData = await readProfileFile(mergeUid);
const globalMergeData = await readProfileFile("Merge");
const originGroupsObj = yaml.load(data) as {
"proxy-groups": IProxyGroupConfig[];
} | null;
const originProviderObj = yaml.load(data) as {
"proxy-providers": Record<string, unknown>;
} | null;
const originProvider = originProviderObj?.["proxy-providers"] || {};
const moreProviderObj = yaml.load(mergeData) as {
"proxy-providers": Record<string, unknown>;
} | null;
const moreProvider = moreProviderObj?.["proxy-providers"] || {};
const globalProviderObj = yaml.load(globalMergeData) as {
"proxy-providers": Record<string, unknown>;
} | null;
const globalProvider = globalProviderObj?.["proxy-providers"] || {};
const provider = Object.assign(
{},
originProvider,
moreProvider,
globalProvider,
);
setProxyProviderList(Object.keys(provider));
setGroupList(originGroupsObj?.["proxy-groups"] || []);
}, [mergeUid, profileUid]);
const getInterfaceNameList = useCallback(async () => {
const list = await getNetworkInterfaces();
setInterfaceNameList(list);
}, []);
useEffect(() => {
if (!open) return;
fetchProxyPolicy();
}, [fetchProxyPolicy, open]);
useEffect(() => {
if (!open) return;
fetchContent();
fetchProfile();
getInterfaceNameList();
}, [fetchContent, fetchProfile, getInterfaceNameList, open]);
const validateGroup = () => {
const group = formIns.getValues();
if (group.name === "") {
throw new Error(t("profiles.modals.groupsEditor.errors.nameRequired"));
}
};
const handleSave = useLockFn(async () => {
try {
const nextData = visualization
? buildGroupsYaml(prependSeq, appendSeq, deleteSeq)
: currData;
if (visualization) {
setCurrData(nextData);
}
await saveProfileFile(property, nextData);
showNotice.success("shared.feedback.notifications.saved");
setPrevData(nextData);
onSave?.(prevData, nextData);
onClose();
} catch (err) {
showNotice.error(err);
}
});
return (
<Dialog
open={open}
onClose={onClose}
maxWidth="xl"
fullWidth
disableEnforceFocus={!visualization}
>
<DialogTitle>
{
<Box display="flex" justifyContent="space-between">
{t("profiles.modals.groupsEditor.title")}
<Box>
<Button
variant="contained"
size="small"
onClick={() => {
setVisualization((prev) => !prev);
}}
>
{visualization
? t("shared.editorModes.advanced")
: t("shared.editorModes.visualization")}
</Button>
</Box>
</Box>
}
</DialogTitle>
<DialogContent
sx={{ display: "flex", width: "auto", height: "calc(100vh - 185px)" }}
>
{visualization ? (
<>
<List
sx={{
width: "50%",
padding: "0 10px",
}}
>
<Box
sx={{
height: "calc(100% - 80px)",
overflowY: "auto",
}}
>
<Controller
name="type"
control={control}
render={({ field }) => (
<Item>
<ListItemText
primary={t("profiles.modals.groupsEditor.fields.type")}
/>
<Autocomplete
size="small"
sx={{ width: "calc(100% - 150px)" }}
options={[
"select",
"url-test",
"fallback",
"load-balance",
"relay",
]}
value={field.value}
getOptionLabel={translateStrategy}
renderOption={(props, option) => (
<li {...props} title={translateStrategy(option)}>
{translateStrategy(option)}
</li>
)}
onChange={(_, value) => value && field.onChange(value)}
renderInput={(params) => <TextField {...params} />}
/>
</Item>
)}
/>
<Controller
name="name"
control={control}
render={({ field }) => (
<Item>
<ListItemText
primary={t("profiles.modals.groupsEditor.fields.name")}
/>
<TextField
autoComplete="new-password"
size="small"
sx={{ width: "calc(100% - 150px)" }}
{...field}
error={field.value === ""}
required={true}
/>
</Item>
)}
/>
<Controller
name="icon"
control={control}
render={({ field }) => (
<Item>
<ListItemText
primary={t("profiles.modals.groupsEditor.fields.icon")}
/>
<TextField
autoComplete="new-password"
size="small"
sx={{ width: "calc(100% - 150px)" }}
{...field}
/>
</Item>
)}
/>
<Controller
name="proxies"
control={control}
render={({ field }) => (
<Item>
<ListItemText
primary={t(
"profiles.modals.groupsEditor.fields.proxies",
)}
/>
<Autocomplete
size="small"
sx={{
width: "calc(100% - 150px)",
}}
multiple
options={proxyPolicyList}
disableCloseOnSelect
onChange={(_, value) => value && field.onChange(value)}
renderInput={(params) => <TextField {...params} />}
renderOption={(props, option) => (
<li {...props} title={translatePolicy(option)}>
{translatePolicy(option)}
</li>
)}
getOptionLabel={translatePolicy}
/>
</Item>
)}
/>
<Controller
name="use"
control={control}
render={({ field }) => (
<Item>
<ListItemText
primary={t(
"profiles.modals.groupsEditor.fields.provider",
)}
/>
<Autocomplete
size="small"
sx={{ width: "calc(100% - 150px)" }}
multiple
options={proxyProviderList}
disableCloseOnSelect
onChange={(_, value) => value && field.onChange(value)}
renderInput={(params) => <TextField {...params} />}
/>
</Item>
)}
/>
<Controller
name="url"
control={control}
render={({ field }) => (
<Item>
<ListItemText
primary={t(
"profiles.modals.groupsEditor.fields.healthCheckUrl",
)}
/>
<TextField
autoComplete="new-password"
placeholder="https://cp.cloudflare.com/generate_204"
size="small"
sx={{ width: "calc(100% - 150px)" }}
{...field}
/>
</Item>
)}
/>
<Controller
name="expected-status"
control={control}
render={({ field }) => (
<Item>
<ListItemText
primary={t(
"profiles.modals.groupsEditor.fields.expectedStatus",
)}
/>
<TextField
autoComplete="new-password"
placeholder="*"
size="small"
sx={{ width: "calc(100% - 150px)" }}
onChange={(e) => {
field.onChange(parseInt(e.target.value));
}}
/>
</Item>
)}
/>
<Controller
name="interval"
control={control}
render={({ field }) => (
<Item>
<ListItemText
primary={t(
"profiles.modals.groupsEditor.fields.interval",
)}
/>
<TextField
autoComplete="new-password"
placeholder="300"
type="number"
size="small"
sx={{ width: "calc(100% - 150px)" }}
onChange={(e) => {
field.onChange(parseInt(e.target.value));
}}
slotProps={{
input: {
endAdornment: (
<InputAdornment position="end">
{t("shared.units.seconds")}
</InputAdornment>
),
},
}}
/>
</Item>
)}
/>
<Controller
name="timeout"
control={control}
render={({ field }) => (
<Item>
<ListItemText primary={t("shared.labels.timeout")} />
<TextField
autoComplete="new-password"
placeholder="5000"
type="number"
size="small"
sx={{ width: "calc(100% - 150px)" }}
onChange={(e) => {
field.onChange(parseInt(e.target.value));
}}
slotProps={{
input: {
endAdornment: (
<InputAdornment position="end">
{t("shared.units.milliseconds")}
</InputAdornment>
),
},
}}
/>
</Item>
)}
/>
<Controller
name="max-failed-times"
control={control}
render={({ field }) => (
<Item>
<ListItemText
primary={t(
"profiles.modals.groupsEditor.fields.maxFailedTimes",
)}
/>
<TextField
autoComplete="new-password"
placeholder="5"
type="number"
size="small"
sx={{ width: "calc(100% - 150px)" }}
onChange={(e) => {
field.onChange(parseInt(e.target.value));
}}
/>
</Item>
)}
/>
<Controller
name="interface-name"
control={control}
render={({ field }) => (
<Item>
<ListItemText
primary={t(
"profiles.modals.groupsEditor.fields.interfaceName",
)}
/>
<Autocomplete
size="small"
sx={{ width: "calc(100% - 150px)" }}
options={interfaceNameList}
value={field.value}
onChange={(_, value) => value && field.onChange(value)}
renderInput={(params) => <TextField {...params} />}
/>
</Item>
)}
/>
<Controller
name="routing-mark"
control={control}
render={({ field }) => (
<Item>
<ListItemText
primary={t(
"profiles.modals.groupsEditor.fields.routingMark",
)}
/>
<TextField
autoComplete="new-password"
type="number"
size="small"
sx={{ width: "calc(100% - 150px)" }}
onChange={(e) => {
field.onChange(parseInt(e.target.value));
}}
/>
</Item>
)}
/>
<Controller
name="filter"
control={control}
render={({ field }) => (
<Item>
<ListItemText
primary={t(
"profiles.modals.groupsEditor.fields.filter",
)}
/>
<TextField
autoComplete="new-password"
size="small"
sx={{ width: "calc(100% - 150px)" }}
{...field}
/>
</Item>
)}
/>
<Controller
name="exclude-filter"
control={control}
render={({ field }) => (
<Item>
<ListItemText
primary={t(
"profiles.modals.groupsEditor.fields.excludeFilter",
)}
/>
<TextField
autoComplete="new-password"
size="small"
sx={{ width: "calc(100% - 150px)" }}
{...field}
/>
</Item>
)}
/>
<Controller
name="exclude-type"
control={control}
render={({ field }) => (
<Item>
<ListItemText
primary={t(
"profiles.modals.groupsEditor.fields.excludeType",
)}
/>
<Autocomplete
multiple
options={[
"Direct",
"Reject",
"RejectDrop",
"Compatible",
"Pass",
"Dns",
"Shadowsocks",
"ShadowsocksR",
"Snell",
"Socks5",
"Http",
"Vmess",
"Vless",
"Trojan",
"Hysteria",
"Hysteria2",
"WireGuard",
"Tuic",
"Mieru",
"AnyTLS",
"Sudoku",
"Relay",
"Selector",
"Fallback",
"URLTest",
"LoadBalance",
"Ssh",
]}
size="small"
disableCloseOnSelect
sx={{ width: "calc(100% - 150px)" }}
value={field.value?.split("|")}
onChange={(_, value) => {
field.onChange(value.join("|"));
}}
renderInput={(params) => <TextField {...params} />}
/>
</Item>
)}
/>
<Controller
name="include-all"
control={control}
render={({ field }) => (
<Item>
<ListItemText
primary={t(
"profiles.modals.groupsEditor.fields.includeAll",
)}
/>
<Switch checked={field.value} {...field} />
</Item>
)}
/>
<Controller
name="include-all-proxies"
control={control}
render={({ field }) => (
<Item>
<ListItemText
primary={t(
"profiles.modals.groupsEditor.fields.includeAllProxies",
)}
/>
<Switch checked={field.value} {...field} />
</Item>
)}
/>
<Controller
name="include-all-providers"
control={control}
render={({ field }) => (
<Item>
<ListItemText
primary={t(
"profiles.modals.groupsEditor.fields.includeAllProviders",
)}
/>
<Switch checked={field.value} {...field} />
</Item>
)}
/>
<Controller
name="lazy"
control={control}
render={({ field }) => (
<Item>
<ListItemText
primary={t("profiles.modals.groupsEditor.toggles.lazy")}
/>
<Switch checked={field.value} {...field} />
</Item>
)}
/>
<Controller
name="disable-udp"
control={control}
render={({ field }) => (
<Item>
<ListItemText
primary={t(
"profiles.modals.groupsEditor.toggles.disableUdp",
)}
/>
<Switch checked={field.value} {...field} />
</Item>
)}
/>
<Controller
name="hidden"
control={control}
render={({ field }) => (
<Item>
<ListItemText
primary={t(
"profiles.modals.groupsEditor.toggles.hidden",
)}
/>
<Switch checked={field.value} {...field} />
</Item>
)}
/>
</Box>
<Item>
<Button
fullWidth
variant="contained"
startIcon={<VerticalAlignTopRounded />}
onClick={() => {
try {
validateGroup();
for (const item of [...prependSeq, ...groupList]) {
if (item.name === formIns.getValues().name) {
throw new Error(
t("profiles.modals.groupsEditor.errors.nameExists"),
);
}
}
setPrependSeq([formIns.getValues(), ...prependSeq]);
} catch (err) {
showNotice.error(err);
}
}}
>
{t("profiles.modals.groupsEditor.actions.prepend")}
</Button>
</Item>
<Item>
<Button
fullWidth
variant="contained"
startIcon={<VerticalAlignBottomRounded />}
onClick={() => {
try {
validateGroup();
for (const item of [...appendSeq, ...groupList]) {
if (item.name === formIns.getValues().name) {
throw new Error(
t("profiles.modals.groupsEditor.errors.nameExists"),
);
}
}
setAppendSeq([...appendSeq, formIns.getValues()]);
} catch (err) {
showNotice.error(err);
}
}}
>
{t("profiles.modals.groupsEditor.actions.append")}
</Button>
</Item>
</List>
<List
sx={{
width: "50%",
padding: "0 10px",
}}
>
<BaseSearchBox onSearch={(match) => setMatch(() => match)} />
<Virtuoso
style={{ height: "calc(100% - 24px)", marginTop: "8px" }}
totalCount={
filteredGroupList.length +
(filteredPrependSeq.length > 0 ? 1 : 0) +
(filteredAppendSeq.length > 0 ? 1 : 0)
}
increaseViewportBy={256}
itemContent={(index) => {
const shift = filteredPrependSeq.length > 0 ? 1 : 0;
if (filteredPrependSeq.length > 0 && index === 0) {
return (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={onPrependDragEnd}
>
<SortableContext
items={filteredPrependSeq.map((x) => {
return x.name;
})}
>
{filteredPrependSeq.map((item) => {
return (
<GroupItem
key={item.name}
type="prepend"
group={item}
onDelete={() => {
setPrependSeq(
prependSeq.filter(
(v) => v.name !== item.name,
),
);
}}
/>
);
})}
</SortableContext>
</DndContext>
);
} else if (index < filteredGroupList.length + shift) {
const newIndex = index - shift;
return (
<GroupItem
key={filteredGroupList[newIndex].name}
type={
deleteSeq.includes(filteredGroupList[newIndex].name)
? "delete"
: "original"
}
group={filteredGroupList[newIndex]}
onDelete={() => {
if (
deleteSeq.includes(filteredGroupList[newIndex].name)
) {
setDeleteSeq(
deleteSeq.filter(
(v) => v !== filteredGroupList[newIndex].name,
),
);
} else {
setDeleteSeq((prev) => [
...prev,
filteredGroupList[newIndex].name,
]);
}
}}
/>
);
} else {
return (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={onAppendDragEnd}
>
<SortableContext
items={filteredAppendSeq.map((x) => {
return x.name;
})}
>
{filteredAppendSeq.map((item) => {
return (
<GroupItem
key={item.name}
type="append"
group={item}
onDelete={() => {
setAppendSeq(
appendSeq.filter(
(v) => v.name !== item.name,
),
);
}}
/>
);
})}
</SortableContext>
</DndContext>
);
}
}}
/>
</List>
</>
) : (
<MonacoEditor
height="100%"
language="yaml"
value={currData}
theme={themeMode === "light" ? "light" : "vs-dark"}
options={{
tabSize: 2, // 根据语言类型设置缩进大小
minimap: {
enabled: document.documentElement.clientWidth >= 1500, // 超过一定宽度显示minimap滚动条
},
mouseWheelZoom: true, // 按住Ctrl滚轮调节缩放比例
quickSuggestions: {
strings: true, // 字符串类型的建议
comments: true, // 注释类型的建议
other: true, // 其他类型的建议
},
padding: {
top: 33, // 顶部padding防止遮挡snippets
},
fontFamily: `Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji"${
getSystem() === "windows" ? ", twemoji mozilla" : ""
}`,
fontLigatures: false, // 连字符
smoothScrolling: true, // 平滑滚动
}}
onChange={(value) => setCurrData(value ?? "")}
/>
)}
</DialogContent>
<DialogActions>
<Button onClick={onClose} variant="outlined">
{t("shared.actions.cancel")}
</Button>
<Button onClick={handleSave} variant="contained">
{t("shared.actions.save")}
</Button>
</DialogActions>
</Dialog>
);
};
const Item = styled(ListItem)(() => ({
padding: "5px 2px",
}));