update:原生确认框替换为自定义弹窗
This commit is contained in:
@@ -39,7 +39,7 @@ function ConfirmDialog({
|
|||||||
}: ConfirmDialogProps) {
|
}: ConfirmDialogProps) {
|
||||||
return (
|
return (
|
||||||
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
||||||
<AlertDialogContent className="rounded-[11px] border-[#dce4eb] bg-white">
|
<AlertDialogContent className="rounded-[11px] border border-[#dce4eb] bg-white ring-0">
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle className="text-[#1c2d42]">{title}</AlertDialogTitle>
|
<AlertDialogTitle className="text-[#1c2d42]">{title}</AlertDialogTitle>
|
||||||
<AlertDialogDescription className="text-[#56687c]">
|
<AlertDialogDescription className="text-[#56687c]">
|
||||||
|
|||||||
@@ -13,9 +13,10 @@ import {
|
|||||||
import { WelcomePanel } from "./WelcomePanel";
|
import { WelcomePanel } from "./WelcomePanel";
|
||||||
import { PublishModal } from "./PublishModal";
|
import { PublishModal } from "./PublishModal";
|
||||||
import { ScriptExplorer } from "./ScriptExplorer";
|
import { ScriptExplorer } from "./ScriptExplorer";
|
||||||
|
import { ScriptsPendingConfirmDialog } from "./ScriptsPendingConfirm";
|
||||||
import { TreeContextMenu } from "./TreeContextMenu";
|
import { TreeContextMenu } from "./TreeContextMenu";
|
||||||
import { VersionReceiptModal } from "./VersionReceiptModal";
|
import { VersionReceiptModal } from "./VersionReceiptModal";
|
||||||
|
import { useScriptsPendingConfirm } from "./useScriptsPendingConfirm";
|
||||||
import { getSessionCache, useScriptWorkspaceStore } from "./state/scriptWorkspaceStore";
|
import { getSessionCache, useScriptWorkspaceStore } from "./state/scriptWorkspaceStore";
|
||||||
import { useUiStore } from "./state/uiStore";
|
import { useUiStore } from "./state/uiStore";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -75,6 +76,18 @@ export default function ScriptsPage() {
|
|||||||
const submitPublish = useScriptWorkspaceStore((s) => s.submitPublish);
|
const submitPublish = useScriptWorkspaceStore((s) => s.submitPublish);
|
||||||
const refreshReadOnlyContent = useScriptWorkspaceStore((s) => s.refreshReadOnlyContent);
|
const refreshReadOnlyContent = useScriptWorkspaceStore((s) => s.refreshReadOnlyContent);
|
||||||
|
|
||||||
|
const {
|
||||||
|
pendingConfirm,
|
||||||
|
setPendingConfirm,
|
||||||
|
requestCloseTab,
|
||||||
|
handleConfirm,
|
||||||
|
} = useScriptsPendingConfirm(scripts, {
|
||||||
|
deleteScript,
|
||||||
|
deleteDataResource,
|
||||||
|
deleteDirectory,
|
||||||
|
closeTab,
|
||||||
|
});
|
||||||
|
|
||||||
// ui store
|
// ui store
|
||||||
const pushToast = (notice: { tone: "success" | "error" | "info"; message: string }) => {
|
const pushToast = (notice: { tone: "success" | "error" | "info"; message: string }) => {
|
||||||
if (notice.tone === "error") toast.error(notice.message);
|
if (notice.tone === "error") toast.error(notice.message);
|
||||||
@@ -206,27 +219,11 @@ export default function ScriptsPage() {
|
|||||||
};
|
};
|
||||||
}, [contextMenu, closeContextMenu]);
|
}, [contextMenu, closeContextMenu]);
|
||||||
|
|
||||||
// python editor handlers with scriptId forwarding
|
// 5) handlers
|
||||||
const handleSetPythonEditorContent = (scriptId: string, value: string) => {
|
|
||||||
setPythonEditorContent(scriptId, value);
|
|
||||||
};
|
|
||||||
const handleSavePythonEditor = (scriptId: string) => {
|
|
||||||
void savePythonEditor(scriptId);
|
|
||||||
};
|
|
||||||
const handleExitPythonEditor = (scriptId: string) => {
|
|
||||||
exitPythonEditor(scriptId);
|
|
||||||
};
|
|
||||||
const handleClosePythonTab = (scriptId: string) => {
|
|
||||||
void closeTab(scriptId);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 5) handlers
|
|
||||||
const SCRIPT_EXTS = [".py", ".ipynb"];
|
const SCRIPT_EXTS = [".py", ".ipynb"];
|
||||||
const DATA_EXTS = [".csv", ".xlsx", ".xls", ".tsv", ".json", ".parquet", ".txt"];
|
const DATA_EXTS = [".csv", ".xlsx", ".xls", ".tsv", ".json", ".parquet", ".txt"];
|
||||||
|
|
||||||
const matchesExt = (name: string, exts: string[]) =>
|
const matchesExt = (name: string, exts: string[]) =>
|
||||||
exts.some((ext) => name.toLowerCase().endsWith(ext));
|
exts.some((ext) => name.toLowerCase().endsWith(ext));
|
||||||
|
|
||||||
const handleUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const files = Array.from(event.target.files ?? []);
|
const files = Array.from(event.target.files ?? []);
|
||||||
event.target.value = "";
|
event.target.value = "";
|
||||||
@@ -381,17 +378,17 @@ export default function ScriptsPage() {
|
|||||||
void endEditing();
|
void endEditing();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onClose={(scriptId, event) => void closeTab(scriptId, event)}
|
onClose={(scriptId, event) => void requestCloseTab(scriptId, event)}
|
||||||
onSwitchTab={switchTab}
|
onSwitchTab={switchTab}
|
||||||
onNewTab={() => openCreateDialog("")}
|
onNewTab={() => openCreateDialog("")}
|
||||||
onPublish={() => openPublishDialog(selected)}
|
onPublish={() => openPublishDialog(selected)}
|
||||||
scripts={scripts}
|
scripts={scripts}
|
||||||
pythonEditorBuffers={pythonEditorBuffers}
|
pythonEditorBuffers={pythonEditorBuffers}
|
||||||
onOpenPythonEditor={() => void openPythonEditor(selected)}
|
onOpenPythonEditor={() => void openPythonEditor(selected)}
|
||||||
onSetPythonEditorContent={handleSetPythonEditorContent}
|
onSetPythonEditorContent={setPythonEditorContent}
|
||||||
onSavePythonEditor={handleSavePythonEditor}
|
onSavePythonEditor={(scriptId) => void savePythonEditor(scriptId)}
|
||||||
onExitPythonEditor={handleExitPythonEditor}
|
onExitPythonEditor={exitPythonEditor}
|
||||||
onClosePythonTab={handleClosePythonTab}
|
onClosePythonTab={(scriptId) => void requestCloseTab(scriptId)}
|
||||||
onInfo={(t) => pushToast(t)}
|
onInfo={(t) => pushToast(t)}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
@@ -429,19 +426,35 @@ export default function ScriptsPage() {
|
|||||||
selectScript(scriptId);
|
selectScript(scriptId);
|
||||||
closeContextMenu();
|
closeContextMenu();
|
||||||
}}
|
}}
|
||||||
onRemoveScript={(s) => void deleteScript(s)}
|
onRemoveScript={(s) => setPendingConfirm({ kind: "script", script: s })}
|
||||||
onToggleLock={(s) => void toggleScriptLock(s)}
|
onToggleLock={(s) => void toggleScriptLock(s)}
|
||||||
onOpenCreateDialog={(parentPath, scriptType) =>
|
onOpenCreateDialog={(parentPath, scriptType) =>
|
||||||
openCreateDialog(parentPath, scriptType)}
|
openCreateDialog(parentPath, scriptType)}
|
||||||
onOpenFolderDialog={(parentPath) => openFolderDialog(parentPath)}
|
onOpenFolderDialog={(parentPath) => openFolderDialog(parentPath)}
|
||||||
onChooseUpload={(parentPath) => triggerUpload(parentPath ?? "")}
|
onChooseUpload={(parentPath) => triggerUpload(parentPath ?? "")}
|
||||||
onRemoveDirectory={(p) => void deleteDirectory(p)}
|
onRemoveDirectory={(p) =>
|
||||||
|
setPendingConfirm({ kind: "directory", path: p })}
|
||||||
onCopyResourcePath={(p) => void handleCopyResourcePath(p)}
|
onCopyResourcePath={(p) => void handleCopyResourcePath(p)}
|
||||||
onPreviewResource={openResourcePreview}
|
onPreviewResource={openResourcePreview}
|
||||||
onRemoveResource={(id) => void deleteDataResource(id)}
|
onRemoveResource={(id) => {
|
||||||
|
const resource = dataResources.find((item) => item.resource_id === id);
|
||||||
|
setPendingConfirm({
|
||||||
|
kind: "resource",
|
||||||
|
id,
|
||||||
|
name: resource?.resource_name ?? id,
|
||||||
|
});
|
||||||
|
}}
|
||||||
onClose={closeContextMenu}
|
onClose={closeContextMenu}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ScriptsPendingConfirmDialog
|
||||||
|
pending={pendingConfirm}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) setPendingConfirm(null);
|
||||||
|
}}
|
||||||
|
onConfirm={() => void handleConfirm()}
|
||||||
|
/>
|
||||||
|
|
||||||
<DataResourcePreviewDialog
|
<DataResourcePreviewDialog
|
||||||
target={resourcePreview}
|
target={resourcePreview}
|
||||||
onClose={() => setResourcePreview(null)}
|
onClose={() => setResourcePreview(null)}
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { ConfirmDialog } from "~/components/common/ConfirmDialog";
|
||||||
|
import type { ScriptItem } from "~/services/api";
|
||||||
|
|
||||||
|
export type ScriptsPendingConfirm =
|
||||||
|
| { kind: "script"; script: ScriptItem }
|
||||||
|
| { kind: "resource"; id: string; name: string }
|
||||||
|
| { kind: "directory"; path: string }
|
||||||
|
| { kind: "close-tab"; id: string; name: string };
|
||||||
|
|
||||||
|
type ScriptsPendingConfirmDialogProps = {
|
||||||
|
pending: ScriptsPendingConfirm | null;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
onConfirm: () => void | Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
function copyFor(pending: ScriptsPendingConfirm) {
|
||||||
|
switch (pending.kind) {
|
||||||
|
case "script":
|
||||||
|
return {
|
||||||
|
title: "确定删除文件?",
|
||||||
|
description: `确定删除文件"${pending.script.script_name}"吗?稳定版本会保留。`,
|
||||||
|
confirmLabel: "删除",
|
||||||
|
destructive: true,
|
||||||
|
};
|
||||||
|
case "resource":
|
||||||
|
return {
|
||||||
|
title: "确定删除数据资源?",
|
||||||
|
description: `确定删除数据资源"${pending.name}"吗?稳定版本会保留。`,
|
||||||
|
confirmLabel: "删除",
|
||||||
|
destructive: true,
|
||||||
|
};
|
||||||
|
case "directory":
|
||||||
|
return {
|
||||||
|
title: "确定删除文件夹?",
|
||||||
|
description: `确定递归删除文件夹"${pending.path}"及其内容吗?稳定版本会保留。`,
|
||||||
|
confirmLabel: "删除",
|
||||||
|
destructive: true,
|
||||||
|
};
|
||||||
|
case "close-tab":
|
||||||
|
return {
|
||||||
|
title: "确定关闭标签?",
|
||||||
|
description: `当前脚本有未保存修改,确定关闭 "${pending.name}" 吗?`,
|
||||||
|
confirmLabel: "关闭",
|
||||||
|
destructive: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ScriptsPendingConfirmDialog({
|
||||||
|
pending,
|
||||||
|
onOpenChange,
|
||||||
|
onConfirm,
|
||||||
|
}: ScriptsPendingConfirmDialogProps) {
|
||||||
|
const copy = pending ? copyFor(pending) : null;
|
||||||
|
return (
|
||||||
|
<ConfirmDialog
|
||||||
|
open={pending !== null}
|
||||||
|
onOpenChange={onOpenChange}
|
||||||
|
title={copy?.title ?? ""}
|
||||||
|
description={copy?.description ?? ""}
|
||||||
|
confirmLabel={copy?.confirmLabel}
|
||||||
|
destructive={copy?.destructive}
|
||||||
|
onConfirm={onConfirm}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -151,7 +151,10 @@ export function TreeContextMenu({
|
|||||||
className={dangerItemClass}
|
className={dangerItemClass}
|
||||||
type="button"
|
type="button"
|
||||||
role="menuitem"
|
role="menuitem"
|
||||||
onClick={() => onRemoveScript(contextMenu.script!)}
|
onClick={() => {
|
||||||
|
onRemoveScript(contextMenu.script!);
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<X size={16} />
|
<X size={16} />
|
||||||
删除文件
|
删除文件
|
||||||
@@ -204,7 +207,10 @@ export function TreeContextMenu({
|
|||||||
className={dangerItemClass}
|
className={dangerItemClass}
|
||||||
type="button"
|
type="button"
|
||||||
role="menuitem"
|
role="menuitem"
|
||||||
onClick={() => onRemoveDirectory(contextMenu.path)}
|
onClick={() => {
|
||||||
|
onRemoveDirectory(contextMenu.path);
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<X size={16} />
|
<X size={16} />
|
||||||
删除文件夹
|
删除文件夹
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ export const createEditSessionSlice: StateCreator<
|
|||||||
const scriptId = active?.script_id;
|
const scriptId = active?.script_id;
|
||||||
if (!active) {
|
if (!active) {
|
||||||
if (closeTabFlag && scriptId) {
|
if (closeTabFlag && scriptId) {
|
||||||
await get().closeTab(scriptId);
|
await get().closeTab(scriptId, undefined, { discardDirty: true });
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -174,7 +174,7 @@ export const createEditSessionSlice: StateCreator<
|
|||||||
applyEditSessionState((p) => set(p), null, null);
|
applyEditSessionState((p) => set(p), null, null);
|
||||||
if (scriptId) sessionCache.delete(scriptId);
|
if (scriptId) sessionCache.delete(scriptId);
|
||||||
if (closeTabFlag && scriptId) {
|
if (closeTabFlag && scriptId) {
|
||||||
await get().closeTab(scriptId);
|
await get().closeTab(scriptId, undefined, { discardDirty: true });
|
||||||
}
|
}
|
||||||
if (showToast) {
|
if (showToast) {
|
||||||
pushToast("success", `${active.script_name} 的编辑锁已释放`);
|
pushToast("success", `${active.script_name} 的编辑锁已释放`);
|
||||||
|
|||||||
@@ -215,9 +215,6 @@ export const createMutationsSlice: StateCreator<
|
|||||||
deleteDataResource: async (resourceId) => {
|
deleteDataResource: async (resourceId) => {
|
||||||
const api = requireApi();
|
const api = requireApi();
|
||||||
useUiStore.getState().closeContextMenu();
|
useUiStore.getState().closeContextMenu();
|
||||||
if (!window.confirm(`确定删除数据资源吗?稳定版本会保留。`)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
await api.deleteResource(resourceId);
|
await api.deleteResource(resourceId);
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
@@ -287,11 +284,6 @@ export const createMutationsSlice: StateCreator<
|
|||||||
deleteScript: async (script: ScriptItem) => {
|
deleteScript: async (script: ScriptItem) => {
|
||||||
const api = requireApi();
|
const api = requireApi();
|
||||||
useUiStore.getState().closeContextMenu();
|
useUiStore.getState().closeContextMenu();
|
||||||
if (
|
|
||||||
!window.confirm(`确定删除文件"${script.script_name}"吗?稳定版本会保留。`)
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const editSession = getEditSession();
|
const editSession = getEditSession();
|
||||||
if (editSession?.script_id === script.script_id) {
|
if (editSession?.script_id === script.script_id) {
|
||||||
await get().endEditing(false, false);
|
await get().endEditing(false, false);
|
||||||
@@ -323,11 +315,6 @@ export const createMutationsSlice: StateCreator<
|
|||||||
deleteDirectory: async (path) => {
|
deleteDirectory: async (path) => {
|
||||||
const api = requireApi();
|
const api = requireApi();
|
||||||
useUiStore.getState().closeContextMenu();
|
useUiStore.getState().closeContextMenu();
|
||||||
if (
|
|
||||||
!window.confirm(`确定递归删除文件夹"${path}"及其内容吗?稳定版本会保留。`)
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const activeScript = get().scripts.find(
|
const activeScript = get().scripts.find(
|
||||||
(item) => item.script_id === getEditSession()?.script_id,
|
(item) => item.script_id === getEditSession()?.script_id,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -62,14 +62,11 @@ export const createSelectionSlice: StateCreator<
|
|||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
|
||||||
closeTab: async (id, event) => {
|
closeTab: async (id, event, options) => {
|
||||||
event?.stopPropagation();
|
event?.stopPropagation();
|
||||||
const buffer = get().pythonEditorBuffers[id];
|
const buffer = get().pythonEditorBuffers[id];
|
||||||
if (buffer?.dirty && !buffer.saving) {
|
if (buffer?.dirty && !buffer.saving && !options?.discardDirty) {
|
||||||
const name =
|
return false;
|
||||||
get().scripts.find((s) => s.script_id === id)?.script_name ?? "该脚本";
|
|
||||||
const ok = window.confirm(`当前脚本有未保存修改,确定关闭 "${name}" 吗?`);
|
|
||||||
if (!ok) return;
|
|
||||||
}
|
}
|
||||||
if (buffer) {
|
if (buffer) {
|
||||||
get().exitPythonEditor(id);
|
get().exitPythonEditor(id);
|
||||||
@@ -91,6 +88,7 @@ export const createSelectionSlice: StateCreator<
|
|||||||
}
|
}
|
||||||
return { openTabIds: newTabs, selectedId: nextSelected };
|
return { openTabIds: newTabs, selectedId: nextSelected };
|
||||||
});
|
});
|
||||||
|
return true;
|
||||||
},
|
},
|
||||||
|
|
||||||
switchTab: (id) => {
|
switchTab: (id) => {
|
||||||
|
|||||||
@@ -92,7 +92,8 @@ export type SelectionSliceActions = {
|
|||||||
closeTab: (
|
closeTab: (
|
||||||
id: string,
|
id: string,
|
||||||
event?: { stopPropagation: () => void },
|
event?: { stopPropagation: () => void },
|
||||||
) => Promise<void>;
|
options?: { discardDirty?: boolean },
|
||||||
|
) => Promise<boolean>;
|
||||||
switchTab: (id: string) => void;
|
switchTab: (id: string) => void;
|
||||||
openPublishDialog: (script: ScriptItem) => void;
|
openPublishDialog: (script: ScriptItem) => void;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import type { ScriptItem } from "~/services/api";
|
||||||
|
import type { ScriptsPendingConfirm } from "./ScriptsPendingConfirm";
|
||||||
|
|
||||||
|
type DeleteActions = {
|
||||||
|
deleteScript: (script: ScriptItem) => Promise<void>;
|
||||||
|
deleteDataResource: (resourceId: string) => Promise<void>;
|
||||||
|
deleteDirectory: (path: string) => Promise<void>;
|
||||||
|
closeTab: (
|
||||||
|
id: string,
|
||||||
|
event?: { stopPropagation: () => void },
|
||||||
|
options?: { discardDirty?: boolean },
|
||||||
|
) => Promise<boolean>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useScriptsPendingConfirm(
|
||||||
|
scripts: ScriptItem[],
|
||||||
|
actions: DeleteActions,
|
||||||
|
) {
|
||||||
|
const [pendingConfirm, setPendingConfirm] =
|
||||||
|
useState<ScriptsPendingConfirm | null>(null);
|
||||||
|
|
||||||
|
const requestCloseTab = async (
|
||||||
|
scriptId: string,
|
||||||
|
event?: { stopPropagation: () => void },
|
||||||
|
) => {
|
||||||
|
const closed = await actions.closeTab(scriptId, event);
|
||||||
|
if (closed) return;
|
||||||
|
const name =
|
||||||
|
scripts.find((s) => s.script_id === scriptId)?.script_name ?? "该脚本";
|
||||||
|
setPendingConfirm({ kind: "close-tab", id: scriptId, name });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirm = async () => {
|
||||||
|
if (!pendingConfirm) return;
|
||||||
|
const target = pendingConfirm;
|
||||||
|
setPendingConfirm(null);
|
||||||
|
if (target.kind === "script") await actions.deleteScript(target.script);
|
||||||
|
else if (target.kind === "resource") {
|
||||||
|
await actions.deleteDataResource(target.id);
|
||||||
|
} else if (target.kind === "directory") {
|
||||||
|
await actions.deleteDirectory(target.path);
|
||||||
|
} else {
|
||||||
|
await actions.closeTab(target.id, undefined, { discardDirty: true });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
pendingConfirm,
|
||||||
|
setPendingConfirm,
|
||||||
|
requestCloseTab,
|
||||||
|
handleConfirm,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -28,6 +28,8 @@ import {
|
|||||||
type ScheduleNode,
|
type ScheduleNode,
|
||||||
} from "~/services/api";
|
} from "~/services/api";
|
||||||
|
|
||||||
|
import { SchedulePendingConfirmDialog, type SchedulePendingConfirm } from "./SchedulePendingConfirm";
|
||||||
|
import { ScheduleRenameDialog } from "./ScheduleRenameDialog";
|
||||||
import { useApi, useAuth } from "~/context/AuthContext";
|
import { useApi, useAuth } from "~/context/AuthContext";
|
||||||
import {
|
import {
|
||||||
formFieldClass,
|
formFieldClass,
|
||||||
@@ -236,6 +238,8 @@ export default function SchedulePage({
|
|||||||
const setRuns = useSchedulesStore((s) => s.setRuns);
|
const setRuns = useSchedulesStore((s) => s.setRuns);
|
||||||
const setRunsLoading = useSchedulesStore((s) => s.setRunsLoading);
|
const setRunsLoading = useSchedulesStore((s) => s.setRunsLoading);
|
||||||
const canvasRef = useRef<HTMLDivElement | null>(null);
|
const canvasRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const [pendingConfirm, setPendingConfirm] =
|
||||||
|
useState<SchedulePendingConfirm | null>(null);
|
||||||
|
|
||||||
const selectedNode = schedule?.nodes.find(
|
const selectedNode = schedule?.nodes.find(
|
||||||
(item) => item.node_id === selectedNodeId,
|
(item) => item.node_id === selectedNodeId,
|
||||||
@@ -366,6 +370,30 @@ export default function SchedulePage({
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const requestRemoveNode = async (node: ScheduleNode) => {
|
||||||
|
const result = await useSchedulesStore.getState().removeNode(node);
|
||||||
|
if (result === "needs_history_confirm") {
|
||||||
|
setPendingConfirm({ kind: "node-history", node });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirm = async () => {
|
||||||
|
if (!pendingConfirm) return;
|
||||||
|
const target = pendingConfirm;
|
||||||
|
setPendingConfirm(null);
|
||||||
|
if (target.kind === "schedule") {
|
||||||
|
await useSchedulesStore.getState().removeSchedule(target.schedule);
|
||||||
|
} else if (target.kind === "artifact") {
|
||||||
|
await useSchedulesStore.getState().removeArtifact(target.artifact);
|
||||||
|
} else if (target.kind === "node") {
|
||||||
|
await requestRemoveNode(target.node);
|
||||||
|
} else {
|
||||||
|
await useSchedulesStore
|
||||||
|
.getState()
|
||||||
|
.removeNode(target.node, { deleteExecutionHistory: true });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="relative flex min-h-0 flex-1 flex-col gap-[10px] overflow-hidden p-[10px]">
|
<section className="relative flex min-h-0 flex-1 flex-col gap-[10px] overflow-hidden p-[10px]">
|
||||||
<header className="flex min-h-[56px] items-center justify-between rounded-[7px] border border-line bg-white px-[14px] shadow-md">
|
<header className="flex min-h-[56px] items-center justify-between rounded-[7px] border border-line bg-white px-[14px] shadow-md">
|
||||||
@@ -416,7 +444,9 @@ export default function SchedulePage({
|
|||||||
variant="destructive"
|
variant="destructive"
|
||||||
size="sm"
|
size="sm"
|
||||||
disabled={!schedule || Boolean(busy)}
|
disabled={!schedule || Boolean(busy)}
|
||||||
onClick={() => useSchedulesStore.getState().removeSchedule(schedule ?? undefined)}
|
onClick={() => {
|
||||||
|
if (schedule) setPendingConfirm({ kind: "schedule", schedule });
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
删除
|
删除
|
||||||
</Button>
|
</Button>
|
||||||
@@ -662,7 +692,8 @@ export default function SchedulePage({
|
|||||||
busy={Boolean(busy)}
|
busy={Boolean(busy)}
|
||||||
onChange={setNodeForm}
|
onChange={setNodeForm}
|
||||||
onSave={() => useSchedulesStore.getState().saveNode(selectedNode)}
|
onSave={() => useSchedulesStore.getState().saveNode(selectedNode)}
|
||||||
onDelete={() => useSchedulesStore.getState().removeNode(selectedNode)}
|
onDelete={() =>
|
||||||
|
setPendingConfirm({ kind: "node", node: selectedNode })}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<ScheduleInspector
|
<ScheduleInspector
|
||||||
@@ -722,7 +753,8 @@ export default function SchedulePage({
|
|||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
role="menuitem"
|
role="menuitem"
|
||||||
onClick={() => useSchedulesStore.getState().renameSchedule(contextMenu.schedule)}
|
onClick={() =>
|
||||||
|
useSchedulesStore.getState().openRenameDialog(contextMenu.schedule)}
|
||||||
>
|
>
|
||||||
<Settings size={14} />
|
<Settings size={14} />
|
||||||
改名
|
改名
|
||||||
@@ -732,21 +764,47 @@ export default function SchedulePage({
|
|||||||
type="button"
|
type="button"
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
role="menuitem"
|
role="menuitem"
|
||||||
onClick={() => useSchedulesStore.getState().removeSchedule(contextMenu.schedule)}
|
onClick={() => {
|
||||||
|
useSchedulesStore.getState().setContextMenu(null);
|
||||||
|
setPendingConfirm({
|
||||||
|
kind: "schedule",
|
||||||
|
schedule: contextMenu.schedule,
|
||||||
|
});
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<X size={14} />
|
<X size={14} />
|
||||||
删除调度方案
|
删除调度方案
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{contextMenu.kind === "artifact" && null}
|
{contextMenu.kind === "artifact" && (
|
||||||
|
<Button
|
||||||
|
className="w-full justify-start gap-[9px] rounded-[5px] p-[9px_10px] text-[12px] text-[#c23b3b] hover:bg-[#fff0f0]"
|
||||||
|
type="button"
|
||||||
|
variant="destructive"
|
||||||
|
role="menuitem"
|
||||||
|
onClick={() => {
|
||||||
|
useSchedulesStore.getState().setContextMenu(null);
|
||||||
|
setPendingConfirm({
|
||||||
|
kind: "artifact",
|
||||||
|
artifact: contextMenu.artifact,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<X size={14} />
|
||||||
|
移出调度列表
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
{contextMenu.kind === "node" && (
|
{contextMenu.kind === "node" && (
|
||||||
<Button
|
<Button
|
||||||
className="w-full justify-start gap-[9px] rounded-[5px] p-[9px_10px] text-[12px] text-[#c23b3b] hover:bg-[#fff0f0]"
|
className="w-full justify-start gap-[9px] rounded-[5px] p-[9px_10px] text-[12px] text-[#c23b3b] hover:bg-[#fff0f0]"
|
||||||
type="button"
|
type="button"
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
role="menuitem"
|
role="menuitem"
|
||||||
onClick={() => useSchedulesStore.getState().removeNode(contextMenu.node)}
|
onClick={() => {
|
||||||
|
useSchedulesStore.getState().setContextMenu(null);
|
||||||
|
setPendingConfirm({ kind: "node", node: contextMenu.node });
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<X size={14} />
|
<X size={14} />
|
||||||
删除画布节点
|
删除画布节点
|
||||||
@@ -816,6 +874,16 @@ export default function SchedulePage({
|
|||||||
</form>
|
</form>
|
||||||
</AppFormDialog>
|
</AppFormDialog>
|
||||||
|
|
||||||
|
<ScheduleRenameDialog />
|
||||||
|
|
||||||
|
<SchedulePendingConfirmDialog
|
||||||
|
pending={pendingConfirm}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) setPendingConfirm(null);
|
||||||
|
}}
|
||||||
|
onConfirm={() => void handleConfirm()}
|
||||||
|
/>
|
||||||
|
|
||||||
{busy && (
|
{busy && (
|
||||||
<div
|
<div
|
||||||
className="absolute bottom-[22px] right-[22px] z-[20] flex min-h-[38px] items-center gap-2 rounded-[7px] border border-[#c9dcee] bg-white px-[13px] text-[11px] text-[#4e657d] shadow-lg"
|
className="absolute bottom-[22px] right-[22px] z-[20] flex min-h-[38px] items-center gap-2 rounded-[7px] border border-[#c9dcee] bg-white px-[13px] text-[11px] text-[#4e657d] shadow-lg"
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { ConfirmDialog } from "~/components/common/ConfirmDialog";
|
||||||
|
import type {
|
||||||
|
Schedule,
|
||||||
|
ScheduleArtifact,
|
||||||
|
ScheduleNode,
|
||||||
|
} from "~/services/api";
|
||||||
|
|
||||||
|
export type SchedulePendingConfirm =
|
||||||
|
| { kind: "schedule"; schedule: Schedule }
|
||||||
|
| { kind: "artifact"; artifact: ScheduleArtifact }
|
||||||
|
| { kind: "node"; node: ScheduleNode }
|
||||||
|
| { kind: "node-history"; node: ScheduleNode };
|
||||||
|
|
||||||
|
type SchedulePendingConfirmDialogProps = {
|
||||||
|
pending: SchedulePendingConfirm | null;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
onConfirm: () => void | Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
function copyFor(pending: SchedulePendingConfirm) {
|
||||||
|
switch (pending.kind) {
|
||||||
|
case "schedule":
|
||||||
|
return {
|
||||||
|
title: "确定删除调度?",
|
||||||
|
description: `确定删除调度"${pending.schedule.schedule_name}"吗?`,
|
||||||
|
confirmLabel: "删除",
|
||||||
|
};
|
||||||
|
case "artifact":
|
||||||
|
return {
|
||||||
|
title: "确定移出调度列表?",
|
||||||
|
description:
|
||||||
|
`确定将"${pending.artifact.script_name} ${pending.artifact.version_label}"移出调度列表吗?稳定版本本身和历史运行记录不会被删除。`,
|
||||||
|
confirmLabel: "移出",
|
||||||
|
};
|
||||||
|
case "node":
|
||||||
|
return {
|
||||||
|
title: "确定删除节点?",
|
||||||
|
description: `确定删除节点"${pending.node.node_name}"吗?`,
|
||||||
|
confirmLabel: "删除",
|
||||||
|
};
|
||||||
|
case "node-history":
|
||||||
|
return {
|
||||||
|
title: "一并删除运行日志?",
|
||||||
|
description: "该节点有运行日志,是否一并删除?",
|
||||||
|
confirmLabel: "删除",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SchedulePendingConfirmDialog({
|
||||||
|
pending,
|
||||||
|
onOpenChange,
|
||||||
|
onConfirm,
|
||||||
|
}: SchedulePendingConfirmDialogProps) {
|
||||||
|
const copy = pending ? copyFor(pending) : null;
|
||||||
|
return (
|
||||||
|
<ConfirmDialog
|
||||||
|
open={pending !== null}
|
||||||
|
onOpenChange={onOpenChange}
|
||||||
|
title={copy?.title ?? ""}
|
||||||
|
description={copy?.description ?? ""}
|
||||||
|
confirmLabel={copy?.confirmLabel}
|
||||||
|
destructive
|
||||||
|
onConfirm={onConfirm}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import { type FormEvent } from "react";
|
||||||
|
import { Check, Loader2Icon } from "lucide-react";
|
||||||
|
|
||||||
|
import { Button } from "~/components/ui/button";
|
||||||
|
import {
|
||||||
|
AppFormDialog,
|
||||||
|
dialogPrimaryButtonClass,
|
||||||
|
dialogSecondaryButtonClass,
|
||||||
|
} from "~/components/common/AppFormDialog";
|
||||||
|
import {
|
||||||
|
formFieldClass,
|
||||||
|
formInputClass,
|
||||||
|
modalFormClass,
|
||||||
|
} from "~/features/platform/modalUi";
|
||||||
|
|
||||||
|
import { useSchedulesStore } from "./state/useSchedulesStore";
|
||||||
|
|
||||||
|
export function ScheduleRenameDialog() {
|
||||||
|
const renameTarget = useSchedulesStore((s) => s.renameTarget);
|
||||||
|
const renameName = useSchedulesStore((s) => s.renameName);
|
||||||
|
const busy = useSchedulesStore((s) => s.busy);
|
||||||
|
const setRenameName = useSchedulesStore((s) => s.setRenameName);
|
||||||
|
const closeRenameDialog = useSchedulesStore((s) => s.closeRenameDialog);
|
||||||
|
|
||||||
|
const renaming = busy === "rename-schedule";
|
||||||
|
const trimmed = renameName.trim();
|
||||||
|
const unchanged = Boolean(
|
||||||
|
renameTarget && trimmed === renameTarget.schedule_name,
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
if (!renameTarget || !trimmed || unchanged) return;
|
||||||
|
void useSchedulesStore.getState().renameSchedule(renameTarget, trimmed);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppFormDialog
|
||||||
|
open={renameTarget !== null}
|
||||||
|
onOpenChange={(nextOpen) => {
|
||||||
|
if (!nextOpen) closeRenameDialog();
|
||||||
|
}}
|
||||||
|
eyebrow="SCHEDULE"
|
||||||
|
title="改名调度方案"
|
||||||
|
titleId="rename-schedule-title"
|
||||||
|
>
|
||||||
|
<form className={modalFormClass} onSubmit={handleSubmit}>
|
||||||
|
<label className={formFieldClass}>
|
||||||
|
<span>调度方案名称</span>
|
||||||
|
<input
|
||||||
|
className={formInputClass}
|
||||||
|
autoFocus
|
||||||
|
maxLength={255}
|
||||||
|
placeholder="请输入新的调度方案名称"
|
||||||
|
value={renameName}
|
||||||
|
onChange={(event) => setRenameName(event.target.value)}
|
||||||
|
onFocus={(event) => event.currentTarget.select()}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<div className="mt-[3px] flex shrink-0 justify-end gap-2 border-t border-line bg-white -mx-[22px] px-[22px] py-3.5">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
disabled={renaming}
|
||||||
|
onClick={closeRenameDialog}
|
||||||
|
className={dialogSecondaryButtonClass}
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant="default"
|
||||||
|
size="sm"
|
||||||
|
disabled={renaming || !trimmed || unchanged}
|
||||||
|
className={dialogPrimaryButtonClass}
|
||||||
|
>
|
||||||
|
{renaming
|
||||||
|
? <Loader2Icon className="size-4 animate-spin" />
|
||||||
|
: <Check size={15} />}
|
||||||
|
{renaming ? "正在保存…" : "保存"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</AppFormDialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,7 +1,4 @@
|
|||||||
// Dialog slice: create-schedule dialog state + cron preview.
|
import type { CronPreview, Schedule } from "../../../services/api";
|
||||||
// Pure setters + small mutations that read/write only dialog-owned fields.
|
|
||||||
|
|
||||||
import type { CronPreview } from "../../../services/api";
|
|
||||||
import type { StateCreator } from "zustand";
|
import type { StateCreator } from "zustand";
|
||||||
|
|
||||||
import { handleError, notify, requireApi } from "./helpers";
|
import { handleError, notify, requireApi } from "./helpers";
|
||||||
@@ -12,6 +9,8 @@ import type { SchedulesStore } from "./useSchedulesStore";
|
|||||||
export type DialogSliceState = {
|
export type DialogSliceState = {
|
||||||
createDialogOpen: boolean;
|
createDialogOpen: boolean;
|
||||||
newScheduleName: string;
|
newScheduleName: string;
|
||||||
|
renameTarget: Schedule | null;
|
||||||
|
renameName: string;
|
||||||
cronResult: CronPreview | null;
|
cronResult: CronPreview | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -21,10 +20,13 @@ export type DialogSliceActions = {
|
|||||||
// pure setters
|
// pure setters
|
||||||
setCreateDialogOpen: (open: boolean) => void;
|
setCreateDialogOpen: (open: boolean) => void;
|
||||||
setNewScheduleName: (name: string) => void;
|
setNewScheduleName: (name: string) => void;
|
||||||
|
setRenameName: (name: string) => void;
|
||||||
setCronResult: (result: CronPreview | null) => void;
|
setCronResult: (result: CronPreview | null) => void;
|
||||||
|
|
||||||
// mutations
|
// mutations
|
||||||
openCreateDialog: () => void;
|
openCreateDialog: () => void;
|
||||||
|
openRenameDialog: (target: Schedule) => void;
|
||||||
|
closeRenameDialog: () => void;
|
||||||
runCronPreview: () => Promise<void>;
|
runCronPreview: () => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -35,12 +37,15 @@ export const createDialogSlice: StateCreator<SchedulesStore, [], [], DialogSlice
|
|||||||
// ---- initial state ----
|
// ---- initial state ----
|
||||||
createDialogOpen: false,
|
createDialogOpen: false,
|
||||||
newScheduleName: "",
|
newScheduleName: "",
|
||||||
|
renameTarget: null,
|
||||||
|
renameName: "",
|
||||||
cronResult: null,
|
cronResult: null,
|
||||||
|
|
||||||
// ---- pure setters ----
|
// ---- pure setters ----
|
||||||
|
|
||||||
setCreateDialogOpen: (open) => set({ createDialogOpen: open }),
|
setCreateDialogOpen: (open) => set({ createDialogOpen: open }),
|
||||||
setNewScheduleName: (name) => set({ newScheduleName: name }),
|
setNewScheduleName: (name) => set({ newScheduleName: name }),
|
||||||
|
setRenameName: (name) => set({ renameName: name }),
|
||||||
setCronResult: (result) => set({ cronResult: result }),
|
setCronResult: (result) => set({ cronResult: result }),
|
||||||
|
|
||||||
// ---- mutations ----
|
// ---- mutations ----
|
||||||
@@ -54,6 +59,20 @@ export const createDialogSlice: StateCreator<SchedulesStore, [], [], DialogSlice
|
|||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
openRenameDialog: (target) => {
|
||||||
|
if (get().busy) return;
|
||||||
|
set({
|
||||||
|
contextMenu: null,
|
||||||
|
renameTarget: target,
|
||||||
|
renameName: target.schedule_name,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
closeRenameDialog: () => {
|
||||||
|
if (get().busy === "rename-schedule") return;
|
||||||
|
set({ renameTarget: null, renameName: "" });
|
||||||
|
},
|
||||||
|
|
||||||
runCronPreview: async () => {
|
runCronPreview: async () => {
|
||||||
const api = requireApi();
|
const api = requireApi();
|
||||||
const { scheduleForm } = get();
|
const { scheduleForm } = get();
|
||||||
@@ -75,4 +94,4 @@ export const createDialogSlice: StateCreator<SchedulesStore, [], [], DialogSlice
|
|||||||
set({ busy: null });
|
set({ busy: null });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ export type ListSliceActions = {
|
|||||||
// mutations
|
// mutations
|
||||||
addSchedule: (name: string) => Promise<void>;
|
addSchedule: (name: string) => Promise<void>;
|
||||||
removeSchedule: (target?: Schedule) => Promise<void>;
|
removeSchedule: (target?: Schedule) => Promise<void>;
|
||||||
renameSchedule: (target: Schedule) => Promise<void>;
|
renameSchedule: (target: Schedule, scheduleName: string) => Promise<void>;
|
||||||
removeArtifact: (artifact: ScheduleArtifact) => Promise<void>;
|
removeArtifact: (artifact: ScheduleArtifact) => Promise<void>;
|
||||||
saveSchedule: () => Promise<void>;
|
saveSchedule: () => Promise<void>;
|
||||||
runNow: () => Promise<void>;
|
runNow: () => Promise<void>;
|
||||||
@@ -70,7 +70,10 @@ export type ListSliceActions = {
|
|||||||
positionY: number,
|
positionY: number,
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
saveNode: (selectedNode: ScheduleNode | null) => Promise<void>;
|
saveNode: (selectedNode: ScheduleNode | null) => Promise<void>;
|
||||||
removeNode: (target?: ScheduleNode) => Promise<void>;
|
removeNode: (
|
||||||
|
target?: ScheduleNode,
|
||||||
|
options?: { deleteExecutionHistory?: boolean },
|
||||||
|
) => Promise<"ok" | "needs_history_confirm" | "noop">;
|
||||||
removeEdge: (target?: ScheduleEdge) => Promise<void>;
|
removeEdge: (target?: ScheduleEdge) => Promise<void>;
|
||||||
checkDag: () => Promise<void>;
|
checkDag: () => Promise<void>;
|
||||||
connectTo: (targetNodeId: string) => Promise<void>;
|
connectTo: (targetNodeId: string) => Promise<void>;
|
||||||
@@ -234,7 +237,6 @@ export const createListSlice: StateCreator<SchedulesStore, [], [], ListSliceStat
|
|||||||
const target_ = target ?? get().schedule;
|
const target_ = target ?? get().schedule;
|
||||||
if (!target_ || get().busy) return;
|
if (!target_ || get().busy) return;
|
||||||
set({ contextMenu: null });
|
set({ contextMenu: null });
|
||||||
if (!window.confirm(`确定删除调度"${target_.schedule_name}"吗?`)) return;
|
|
||||||
set({ busy: "delete-schedule" });
|
set({ busy: "delete-schedule" });
|
||||||
try {
|
try {
|
||||||
await api.deleteSchedule(target_.schedule_id, target_.workflow_version);
|
await api.deleteSchedule(target_.schedule_id, target_.workflow_version);
|
||||||
@@ -263,19 +265,15 @@ export const createListSlice: StateCreator<SchedulesStore, [], [], ListSliceStat
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
renameSchedule: async (target) => {
|
renameSchedule: async (target, scheduleName) => {
|
||||||
const api = requireApi();
|
const api = requireApi();
|
||||||
if (get().busy) return;
|
const trimmed = scheduleName.trim();
|
||||||
set({ contextMenu: null });
|
if (get().busy || !trimmed || trimmed === target.schedule_name) return;
|
||||||
const scheduleName = window
|
|
||||||
.prompt("请输入新的调度方案名称", target.schedule_name)
|
|
||||||
?.trim();
|
|
||||||
if (!scheduleName || scheduleName === target.schedule_name) return;
|
|
||||||
set({ busy: "rename-schedule" });
|
set({ busy: "rename-schedule" });
|
||||||
try {
|
try {
|
||||||
const updated = await api.updateSchedule(target.schedule_id, {
|
const updated = await api.updateSchedule(target.schedule_id, {
|
||||||
workflow_version: target.workflow_version,
|
workflow_version: target.workflow_version,
|
||||||
schedule_name: scheduleName,
|
schedule_name: trimmed,
|
||||||
});
|
});
|
||||||
set((s: any) => ({
|
set((s: any) => ({
|
||||||
schedules: s.schedules.map((item: Schedule) =>
|
schedules: s.schedules.map((item: Schedule) =>
|
||||||
@@ -287,6 +285,8 @@ export const createListSlice: StateCreator<SchedulesStore, [], [], ListSliceStat
|
|||||||
s.schedule?.schedule_id === updated.schedule_id
|
s.schedule?.schedule_id === updated.schedule_id
|
||||||
? updated
|
? updated
|
||||||
: s.schedule,
|
: s.schedule,
|
||||||
|
renameTarget: null,
|
||||||
|
renameName: "",
|
||||||
}));
|
}));
|
||||||
notify({ tone: "success", message: "调度方案已改名" });
|
notify({ tone: "success", message: "调度方案已改名" });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -301,12 +301,6 @@ export const createListSlice: StateCreator<SchedulesStore, [], [], ListSliceStat
|
|||||||
const api = requireApi();
|
const api = requireApi();
|
||||||
if (get().busy) return;
|
if (get().busy) return;
|
||||||
set({ contextMenu: null });
|
set({ contextMenu: null });
|
||||||
if (
|
|
||||||
!window.confirm(
|
|
||||||
`确定将"${artifact.script_name} ${artifact.version_label}"移出调度列表吗?\n`
|
|
||||||
+ "稳定版本本身和历史运行记录不会被删除。",
|
|
||||||
)
|
|
||||||
) return;
|
|
||||||
set({ busy: "delete-artifact" });
|
set({ busy: "delete-artifact" });
|
||||||
try {
|
try {
|
||||||
await api.hideScheduleArtifact(artifact.versions_id);
|
await api.hideScheduleArtifact(artifact.versions_id);
|
||||||
@@ -531,16 +525,15 @@ export const createListSlice: StateCreator<SchedulesStore, [], [], ListSliceStat
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
removeNode: async (target) => {
|
removeNode: async (target, options) => {
|
||||||
const api = requireApi();
|
const api = requireApi();
|
||||||
const state_ = get();
|
const state_ = get();
|
||||||
const { schedule, selectedNodeId } = state_;
|
const { schedule, selectedNodeId } = state_;
|
||||||
const node = target
|
const node = target
|
||||||
?? schedule?.nodes.find((item) => item.node_id === selectedNodeId)
|
?? schedule?.nodes.find((item) => item.node_id === selectedNodeId)
|
||||||
?? null;
|
?? null;
|
||||||
if (!schedule || !node || state_.busy) return;
|
if (!schedule || !node || state_.busy) return "noop";
|
||||||
set({ contextMenu: null });
|
set({ contextMenu: null });
|
||||||
if (!window.confirm(`确定删除节点"${node.node_name}"吗?`)) return;
|
|
||||||
set({ busy: "delete-node" });
|
set({ busy: "delete-node" });
|
||||||
try {
|
try {
|
||||||
let updated: Schedule;
|
let updated: Schedule;
|
||||||
@@ -550,6 +543,9 @@ export const createListSlice: StateCreator<SchedulesStore, [], [], ListSliceStat
|
|||||||
schedule.schedule_id,
|
schedule.schedule_id,
|
||||||
node.node_id,
|
node.node_id,
|
||||||
schedule.workflow_version,
|
schedule.workflow_version,
|
||||||
|
options?.deleteExecutionHistory
|
||||||
|
? { delete_execution_history: true }
|
||||||
|
: undefined,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const requiresHistoryConfirmation =
|
const requiresHistoryConfirmation =
|
||||||
@@ -557,7 +553,10 @@ export const createListSlice: StateCreator<SchedulesStore, [], [], ListSliceStat
|
|||||||
&& error.status === 409
|
&& error.status === 409
|
||||||
&& error.code === "node_execution_history_exists";
|
&& error.code === "node_execution_history_exists";
|
||||||
if (!requiresHistoryConfirmation) throw error;
|
if (!requiresHistoryConfirmation) throw error;
|
||||||
if (!window.confirm("该节点有运行日志,是否一并删除?")) return;
|
// 交给 UI 弹二次确认;已确认则带 delete_execution_history 重试。
|
||||||
|
if (!options?.deleteExecutionHistory) {
|
||||||
|
return "needs_history_confirm";
|
||||||
|
}
|
||||||
updated = await api.deleteScheduleNode(
|
updated = await api.deleteScheduleNode(
|
||||||
schedule.schedule_id,
|
schedule.schedule_id,
|
||||||
node.node_id,
|
node.node_id,
|
||||||
@@ -568,9 +567,11 @@ export const createListSlice: StateCreator<SchedulesStore, [], [], ListSliceStat
|
|||||||
applyServerUpdatedSchedule(set, updated);
|
applyServerUpdatedSchedule(set, updated);
|
||||||
if (selectedNodeId === node.node_id) set({ selectedNodeId: null });
|
if (selectedNodeId === node.node_id) set({ selectedNodeId: null });
|
||||||
notify({ tone: "success", message: "节点及其运行日志已删除" });
|
notify({ tone: "success", message: "节点及其运行日志已删除" });
|
||||||
|
return "ok";
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const { handleError } = await import("./helpers");
|
const { handleError } = await import("./helpers");
|
||||||
await handleError(get, error, "删除节点失败");
|
await handleError(get, error, "删除节点失败");
|
||||||
|
return "noop";
|
||||||
} finally {
|
} finally {
|
||||||
set({ busy: null });
|
set({ busy: null });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,6 +64,8 @@ const INITIAL: Omit<SchedulesState, "schedule" | "busy"> = {
|
|||||||
// Dialog
|
// Dialog
|
||||||
createDialogOpen: false,
|
createDialogOpen: false,
|
||||||
newScheduleName: "",
|
newScheduleName: "",
|
||||||
|
renameTarget: null,
|
||||||
|
renameName: "",
|
||||||
cronResult: null,
|
cronResult: null,
|
||||||
// Runs
|
// Runs
|
||||||
runs: [],
|
runs: [],
|
||||||
|
|||||||
Reference in New Issue
Block a user