diff --git a/frontend/app/features/schedules/SchedulePage.tsx b/frontend/app/features/schedules/SchedulePage.tsx index 70a211c..9c5b244 100644 --- a/frontend/app/features/schedules/SchedulePage.tsx +++ b/frontend/app/features/schedules/SchedulePage.tsx @@ -10,23 +10,26 @@ import { } from "react"; import { - ApiRequestError, - type CronPreview, type Schedule, type ScheduleArtifact, type ScheduleEdge, type ScheduleNode, - type ScheduleNodeRun, - type ScheduleRunDetail, - type ScheduleRunSummary, } from "../../services/api"; import { useApi, useAuth } from "../../context/AuthContext"; import Icon from "../../components/common/Icon"; +import { NodeInspector } from "./NodeInspector"; +import { RunHistory } from "./RunHistory"; +import { ScheduleInspector } from "./ScheduleInspector"; import { ScheduleList } from "./ScheduleList"; import { ArtifactList } from "./ArtifactList"; import { ScheduleCanvasHeader } from "./ScheduleCanvasHeader"; +import { + EMPTY_NODE_FORM, + type ScheduleContextMenu, +} from "./state/schedulesStore"; import { useSchedulesStore } from "./state/schedulesStore"; +import { edgePath, nodeToForm, scheduleToForm } from "./utils"; import "../../styles/schedule.css"; type Notice = { @@ -34,26 +37,6 @@ type Notice = { message: string; }; -type ScheduleForm = { - scheduleName: string; - description: string; - triggerType: "manual" | "cron" | "api"; - cronExpression: string; - timezone: string; - enabled: boolean; - maxConcurrency: string; - failurePolicy: "stop" | "continue"; -}; - -type NodeForm = { - nodeName: string; - timeoutSeconds: string; - retryCount: string; - retryIntervalSec: string; - argumentsJson: string; - envRefsJson: string; -}; - type DragState = { nodeId: string; pointerId: number; @@ -64,18 +47,6 @@ type DragState = { moved: boolean; }; -type NodePositionDraft = { - position_x: number; - position_y: number; -}; - -type ScheduleContextMenu = - | { kind: "schedule-list"; x: number; y: number } - | { kind: "schedule"; x: number; y: number; schedule: Schedule } - | { kind: "artifact"; x: number; y: number; artifact: ScheduleArtifact } - | { kind: "node"; x: number; y: number; node: ScheduleNode } - | { kind: "edge"; x: number; y: number; edge: ScheduleEdge }; - type ScheduleContextMenuTarget = | { kind: "schedule-list" } | { kind: "schedule"; schedule: Schedule } @@ -83,148 +54,12 @@ type ScheduleContextMenuTarget = | { kind: "node"; node: ScheduleNode } | { kind: "edge"; edge: ScheduleEdge }; -const EMPTY_SCHEDULE_FORM: ScheduleForm = { - scheduleName: "", - description: "", - triggerType: "manual", - cronExpression: "0 9 * * *", - timezone: "Asia/Shanghai", - enabled: false, - maxConcurrency: "1", - failurePolicy: "stop", -}; - -const EMPTY_NODE_FORM: NodeForm = { - nodeName: "", - timeoutSeconds: "600", - retryCount: "0", - retryIntervalSec: "5", - argumentsJson: "{}", - envRefsJson: "{}", -}; - const CANVAS_WIDTH = 1400; const CANVAS_HEIGHT = 860; const NODE_WIDTH = 218; const NODE_HEIGHT = 104; const ARTIFACT_MIME = "application/x-model-platform-version"; - -function formatTime(value: string | null): string { - if (!value) return "尚未执行"; - return new Intl.DateTimeFormat("zh-CN", { - month: "2-digit", - day: "2-digit", - hour: "2-digit", - minute: "2-digit", - hour12: false, - }).format(new Date(value)); -} - -function shortHash(value: string): string { - return value ? `${value.slice(0, 7)}…${value.slice(-5)}` : "—"; -} - -const RUN_STATUS_LABELS: Record = { - queued: "排队中", - running: "运行中", - succeeded: "成功", - failed: "失败", - cancelled: "已取消", - timed_out: "已超时", -}; - -const NODE_STATUS_LABELS: Record = { - ...RUN_STATUS_LABELS, - skipped: "已跳过", -}; - -const TRIGGER_TYPE_LABELS: Record = { - manual: "手动触发", - cron: "Cron 定时", - api: "API 触发", - retry: "失败重试", -}; - -function formatDuration(value: number | null): string { - if (value === null) return "—"; - if (value < 1000) return `${value} ms`; - if (value < 60_000) return `${(value / 1000).toFixed(1)} s`; - return `${Math.floor(value / 60_000)}m ${Math.round((value % 60_000) / 1000)}s`; -} - -function formatSize(value: number): string { - if (value < 1024) return `${value} B`; - if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)} KB`; - return `${(value / 1024 / 1024).toFixed(1)} MB`; -} - -function parseObject(text: string, label: string): Record { - let value: unknown; - try { - value = JSON.parse(text); - } catch { - throw new Error(`${label}必须是合法 JSON`); - } - if (!value || Array.isArray(value) || typeof value !== "object") { - throw new Error(`${label}必须是 JSON 对象`); - } - return value as Record; -} - -function scheduleToForm(schedule: Schedule): ScheduleForm { - return { - scheduleName: schedule.schedule_name, - description: schedule.description ?? "", - triggerType: schedule.trigger_type, - cronExpression: schedule.cron_expression ?? "0 9 * * *", - timezone: schedule.timezone, - enabled: schedule.enabled, - maxConcurrency: String(schedule.max_concurrency), - failurePolicy: schedule.failure_policy, - }; -} - -function nodeToForm(node: ScheduleNode): NodeForm { - return { - nodeName: node.node_name, - timeoutSeconds: String(node.timeout_seconds), - retryCount: String(node.retry_count), - retryIntervalSec: String(node.retry_interval_sec), - argumentsJson: JSON.stringify(node.arguments_json ?? {}, null, 2), - envRefsJson: JSON.stringify(node.env_refs_json ?? {}, null, 2), - }; -} - -function artifactNodeKey( - artifact: ScheduleArtifact, - schedule: Schedule, -): string { - const ascii = artifact.script_name - .replace(/\.[^.]+$/, "") - .replace(/[^A-Za-z0-9_-]+/g, "_") - .replace(/^([^A-Za-z])/, "n_$1") - .replace(/^_+|_+$/g, "") - .slice(0, 48); - const base = ascii || `node_${artifact.versions_id.slice(-6).toLowerCase()}`; - const existing = new Set(schedule.nodes.map((item) => item.node_key)); - if (!existing.has(base)) return base; - let index = 2; - while (existing.has(`${base}_${index}`)) index += 1; - return `${base}_${index}`.slice(0, 64); -} - -function edgePath( - source: ScheduleNode, - target: ScheduleNode, -): string { - const x1 = source.position_x + NODE_WIDTH; - const y1 = source.position_y + NODE_HEIGHT / 2; - const x2 = target.position_x; - const y2 = target.position_y + NODE_HEIGHT / 2; - const curve = Math.max(70, Math.abs(x2 - x1) * 0.45); - return `M ${x1} ${y1} C ${x1 + curve} ${y1}, ${x2 - curve} ${y2}, ${x2} ${y2}`; -} export default function SchedulePage({ onNotify, onConnectionChange, @@ -234,6 +69,7 @@ export default function SchedulePage({ }) { const { currentWorkspace } = useAuth(); const workspaceId = currentWorkspace?.workspace_id; + const api = useApi(); // 核心数据 state 迁 store(其余 form/dialog/selected/runs 仍用 useState) const schedules = useSchedulesStore((s) => s.schedules); @@ -247,23 +83,35 @@ export default function SchedulePage({ const setLoading = useSchedulesStore((s) => s.setLoading); const setBusy = useSchedulesStore((s) => s.setBusy); - const [selectedNodeId, setSelectedNodeId] = useState(null); - const [selectedEdgeId, setSelectedEdgeId] = useState(null); - const [linkSourceId, setLinkSourceId] = useState(null); - const [scheduleKeyword, setScheduleKeyword] = useState(""); - const [artifactKeyword, setArtifactKeyword] = useState(""); - const [createDialogOpen, setCreateDialogOpen] = useState(false); - const [newScheduleName, setNewScheduleName] = useState(""); - const [contextMenu, setContextMenu] = useState(null); - const [scheduleForm, setScheduleForm] = useState(EMPTY_SCHEDULE_FORM); - const [nodeForm, setNodeForm] = useState(EMPTY_NODE_FORM); - const [cronResult, setCronResult] = useState(null); - const [runs, setRuns] = useState([]); - const [runsLoading, setRunsLoading] = useState(false); + const selectedNodeId = useSchedulesStore((s) => s.selectedNodeId); + const setSelectedNodeId = useSchedulesStore((s) => s.setSelectedNodeId); + const selectedEdgeId = useSchedulesStore((s) => s.selectedEdgeId); + const setSelectedEdgeId = useSchedulesStore((s) => s.setSelectedEdgeId); + const linkSourceId = useSchedulesStore((s) => s.linkSourceId); + const setLinkSourceId = useSchedulesStore((s) => s.setLinkSourceId); + const scheduleKeyword = useSchedulesStore((s) => s.scheduleKeyword); + const setScheduleKeyword = useSchedulesStore((s) => s.setScheduleKeyword); + const artifactKeyword = useSchedulesStore((s) => s.artifactKeyword); + const setArtifactKeyword = useSchedulesStore((s) => s.setArtifactKeyword); + const createDialogOpen = useSchedulesStore((s) => s.createDialogOpen); + const setCreateDialogOpen = useSchedulesStore((s) => s.setCreateDialogOpen); + const newScheduleName = useSchedulesStore((s) => s.newScheduleName); + const setNewScheduleName = useSchedulesStore((s) => s.setNewScheduleName); + const contextMenu = useSchedulesStore((s) => s.contextMenu); + const setContextMenu = useSchedulesStore((s) => s.setContextMenu); + const scheduleForm = useSchedulesStore((s) => s.scheduleForm); + const setScheduleForm = useSchedulesStore((s) => s.setScheduleForm); + const nodeForm = useSchedulesStore((s) => s.nodeForm); + const setNodeForm = useSchedulesStore((s) => s.setNodeForm); + const cronResult = useSchedulesStore((s) => s.cronResult); + const setCronResult = useSchedulesStore((s) => s.setCronResult); + const runs = useSchedulesStore((s) => s.runs); + const setRuns = useSchedulesStore((s) => s.setRuns); + const runsLoading = useSchedulesStore((s) => s.runsLoading); + const setRunsLoading = useSchedulesStore((s) => s.setRunsLoading); const canvasRef = useRef(null); const dragRef = useRef(null); const positionDraftCount = useSchedulesStore((state) => state.positionDraftCount); - const api = useApi(); const selectedNode = schedule?.nodes.find( (item) => item.node_id === selectedNodeId, @@ -316,478 +164,30 @@ export default function SchedulePage({ } as ScheduleContextMenu); }; - const applyPositionDrafts = (serverSchedule: Schedule): Schedule => { - const drafts = useSchedulesStore.getState().getPositionDrafts(); - if (drafts.size === 0) return serverSchedule; - return { - ...serverSchedule, - nodes: serverSchedule.nodes.map((node) => { - const draft = drafts.get(node.node_id); - return draft ? { ...node, ...draft } : node; - }), - }; - }; - - const refreshRuns = async ( - scheduleId: string, - showLoading = false, - ): Promise => { - if (showLoading) setRunsLoading(true); - try { - const items = await api.listScheduleRuns({ - scheduleId, - limit: 20, - }); - if (useSchedulesStore.getState().schedule?.schedule_id === scheduleId) { - setRuns(items); - } - onConnectionChange(true); - } catch (error) { - if (showLoading) { - await handleError(error, "运行记录加载失败"); - } - } finally { - if (showLoading) setRunsLoading(false); - } - }; - - const refreshLists = async ( - preferredScheduleId?: string | null, - ): Promise => { - const [scheduleItems, artifactItems] = await Promise.all([ - api.listSchedules(), - api.listScheduleArtifacts(), - ]); - setSchedules(scheduleItems); - setArtifacts(artifactItems); - const targetId = preferredScheduleId - ?? useSchedulesStore.getState().schedule?.schedule_id - ?? scheduleItems[0]?.schedule_id - ?? null; - if (!targetId) { - setSchedule(null); - return; - } - const detail = await api.getSchedule(targetId); - setSchedule(applyPositionDrafts(detail)); - }; - - useEffect(() => { - let cancelled = false; - setLoading(true); - Promise.all([api.listSchedules(), api.listScheduleArtifacts()]) - .then(async ([scheduleItems, artifactItems]) => { - if (cancelled) return; - setSchedules(scheduleItems); - setArtifacts(artifactItems); - if (scheduleItems[0]) { - const detail = await api.getSchedule(scheduleItems[0].schedule_id); - if (!cancelled) setSchedule(applyPositionDrafts(detail)); - } - onConnectionChange(true); - }) - .catch((error: unknown) => { - if (cancelled) return; - onConnectionChange(false); - onNotify({ - tone: "error", - message: error instanceof Error ? error.message : "调度数据加载失败", - }); - }) - .finally(() => { - if (!cancelled) setLoading(false); - }); - return () => { - cancelled = true; - }; - }, []); - - // 切 workspace 时清空 store 与 local state 并重新加载 - useEffect(() => { - if (!workspaceId) return; + useEffect(() => { + if (!workspaceId || !api) return; setSchedules([]); setArtifacts([]); setSchedule(null); setRuns([]); setRunsLoading(true); void useSchedulesStore.getState().loadInitial(); - }, [workspaceId, setSchedules, setArtifacts, setSchedule]); + }, [workspaceId, api, setSchedules, setArtifacts, setSchedule]); useEffect(() => { const scheduleId = schedule?.schedule_id; - if (!scheduleId) { - setRuns([]); - return; - } - let cancelled = false; - setRunsLoading(true); - api.listScheduleRuns({ scheduleId, limit: 20 }) - .then((items) => { - if (!cancelled) setRuns(items); - }) - .catch((error: unknown) => { - if (!cancelled) { - onNotify({ - tone: "error", - message: error instanceof Error ? error.message : "运行记录加载失败", - }); - } - }) - .finally(() => { - if (!cancelled) setRunsLoading(false); - }); - return () => { - cancelled = true; - }; - }, [schedule?.schedule_id]); - - useEffect(() => { - const scheduleId = schedule?.schedule_id; - if (!scheduleId || !runs.some((item) => item.run_status === "queued" || item.run_status === "running")) return; + if (!scheduleId || !api) return; + if (!runs.some((item) => item.run_status === "queued" || item.run_status === "running")) return; const timer = window.setInterval(() => { - void refreshRuns(scheduleId); + void useSchedulesStore.getState().refreshRuns(scheduleId); }, 1500); return () => window.clearInterval(timer); - }, [schedule?.schedule_id, runs]); + }, [schedule?.schedule_id, runs, api]); - const handleError = async ( - error: unknown, - fallback: string, - ): Promise => { - if (error instanceof ApiRequestError && error.status === 412) { - const currentId = useSchedulesStore.getState().schedule?.schedule_id; - if (currentId) { - withSuppressedError(() => refreshLists(currentId)); - } - onNotify({ - tone: "error", - message: "调度已被其他操作更新,已重新加载最新版本", - }); - return; - } - onNotify({ - tone: "error", - message: error instanceof Error ? error.message : fallback, - }); - }; - - const withMutation = async ( - label: string, - action: () => Promise, - successMessage: string, - ): Promise => { - if (busy) return null; - setBusy(label); - try { - const serverUpdated = await action(); - const validNodeIds = new Set( - serverUpdated.nodes.map((node) => node.node_id), - ); - useSchedulesStore.getState().prunePositionDrafts(validNodeIds); - const updated = applyPositionDrafts(serverUpdated); - setSchedule(updated); - setSchedules((current) => { - const summary = { ...updated, nodes: [], edges: [] }; - const index = current.findIndex( - (item) => item.schedule_id === updated.schedule_id, - ); - if (index < 0) return [summary, ...current]; - return current.map((item) => ( - item.schedule_id === updated.schedule_id ? summary : item - )); - }); - onNotify({ tone: "success", message: successMessage }); - return updated; - } catch (error) { - await handleError(error, `${successMessage}失败`); - return null; - } finally { - setBusy(null); - } - }; - - const chooseSchedule = async (scheduleId: string): Promise => { - if (scheduleId === schedule?.schedule_id || busy) return; - setBusy("load-schedule"); - useSchedulesStore.getState().clearAllPositionDrafts(); - setSelectedNodeId(null); - setSelectedEdgeId(null); - setLinkSourceId(null); - setCronResult(null); - try { - setSchedule(await api.getSchedule(scheduleId)); - onConnectionChange(true); - } catch (error) { - await handleError(error, "调度详情加载失败"); - } finally { - setBusy(null); - } - }; - - const openCreateScheduleDialog = (): void => { - if (busy) return; - setContextMenu(null); - setNewScheduleName(`新建调度 ${schedules.length + 1}`); - setCreateDialogOpen(true); - }; - - const addSchedule = async (event: FormEvent): Promise => { + // Form submit wrapper for create-schedule modal — keeps the FormEvent flow out of JSX. + const handleCreateScheduleSubmit = (event: FormEvent): void => { event.preventDefault(); - const scheduleName = newScheduleName.trim(); - if (busy || !scheduleName) return; - setBusy("create-schedule"); - try { - const created = await api.createSchedule({ - schedule_name: scheduleName, - description: "在画布中拖入稳定版本并配置执行顺序", - trigger_type: "manual", - timezone: "Asia/Shanghai", - enabled: false, - }); - setSchedules((current) => [created, ...current]); - setSchedule(created); - useSchedulesStore.getState().clearAllPositionDrafts(); - setSelectedNodeId(null); - setCreateDialogOpen(false); - setNewScheduleName(""); - onNotify({ tone: "success", message: "调度方案已创建" }); - } catch (error) { - await handleError(error, "创建调度失败"); - } finally { - setBusy(null); - } - }; - - const removeSchedule = async (target?: Schedule): Promise => { - const selectedSchedule = target ?? schedule; - if (!selectedSchedule || busy) return; - setContextMenu(null); - if (!window.confirm(`确定删除调度"${selectedSchedule.schedule_name}"吗?`)) return; - setBusy("delete-schedule"); - try { - await api.deleteSchedule( - selectedSchedule.schedule_id, - selectedSchedule.workflow_version, - ); - const remaining = schedules.filter( - (item) => item.schedule_id !== selectedSchedule.schedule_id, - ); - setSchedules(remaining); - if (schedule?.schedule_id === selectedSchedule.schedule_id) { - setSchedule(null); - useSchedulesStore.getState().clearAllPositionDrafts(); - setSelectedNodeId(null); - setSelectedEdgeId(null); - if (remaining[0]) { - setSchedule(await api.getSchedule(remaining[0].schedule_id)); - } - } - onNotify({ tone: "success", message: "调度方案已删除" }); - } catch (error) { - await handleError(error, "删除调度失败"); - } finally { - setBusy(null); - } - }; - - const renameSchedule = async (target: Schedule): Promise => { - if (busy) return; - setContextMenu(null); - const scheduleName = window.prompt("请输入新的调度方案名称", target.schedule_name)?.trim(); - if (!scheduleName || scheduleName === target.schedule_name) return; - setBusy("rename-schedule"); - try { - const updated = await api.updateSchedule(target.schedule_id, { - workflow_version: target.workflow_version, - schedule_name: scheduleName, - }); - setSchedules((current) => current.map((item) => ( - item.schedule_id === updated.schedule_id - ? { ...updated, nodes: [], edges: [] } - : item - ))); - if (schedule?.schedule_id === updated.schedule_id) setSchedule(updated); - onNotify({ tone: "success", message: "调度方案已改名" }); - } catch (error) { - await handleError(error, "调度方案改名失败"); - } finally { - setBusy(null); - } - }; - - const removeArtifact = async ( - artifact: ScheduleArtifact, - ): Promise => { - if (busy) return; - setContextMenu(null); - if ( - !window.confirm( - `确定将"${artifact.script_name} ${artifact.version_label}"移出调度列表吗?\n` - + "稳定版本本身和历史运行记录不会被删除。", - ) - ) return; - setBusy("delete-artifact"); - try { - await api.hideScheduleArtifact(artifact.versions_id); - setArtifacts((current) => current.filter( - (item) => item.versions_id !== artifact.versions_id, - )); - onNotify({ - tone: "success", - message: "已移出调度列表,稳定版本和历史记录保持不变", - }); - } catch (error) { - await handleError(error, "移出调度列表失败"); - } finally { - setBusy(null); - } - }; - - const saveSchedule = async (): Promise => { - if (!schedule || busy) return; - const maxConcurrency = Number(scheduleForm.maxConcurrency); - if (!scheduleForm.scheduleName.trim()) { - onNotify({ tone: "error", message: "调度名称不能为空" }); - return; - } - if (!Number.isInteger(maxConcurrency) || maxConcurrency < 1) { - onNotify({ tone: "error", message: "最大并发数必须是正整数" }); - return; - } - setBusy("save-schedule"); - let updated = useSchedulesStore.getState().schedule ?? schedule; - try { - const drafts = useSchedulesStore.getState().getPositionDrafts(); - for (const [nodeId, position] of drafts.entries()) { - updated = await api.updateScheduleNode(updated.schedule_id, nodeId, { - workflow_version: updated.workflow_version, - position_x: position.position_x, - position_y: position.position_y, - }); - } - updated = await api.updateSchedule(updated.schedule_id, { - workflow_version: updated.workflow_version, - schedule_name: scheduleForm.scheduleName.trim(), - description: scheduleForm.description.trim() || null, - trigger_type: scheduleForm.triggerType, - cron_expression: scheduleForm.triggerType === "cron" - ? scheduleForm.cronExpression.trim() - : null, - timezone: scheduleForm.timezone.trim(), - enabled: scheduleForm.enabled, - max_concurrency: maxConcurrency, - failure_policy: scheduleForm.failurePolicy, - }); - useSchedulesStore.getState().clearAllPositionDrafts(); - setSchedule(updated); - setSchedules((current) => current.map((item) => ( - item.schedule_id === updated.schedule_id - ? { ...updated, nodes: [], edges: [] } - : item - ))); - onNotify({ tone: "success", message: "调度配置已保存" }); - } catch (error) { - const localDraft = applyPositionDrafts(updated); - setSchedule(localDraft); - await handleError(error, "调度配置保存失败"); - } finally { - setBusy(null); - } - }; - - const runCronPreview = async (): Promise => { - if (scheduleForm.triggerType !== "cron") return; - if (busy) return; - setBusy("cron-preview"); - try { - const result = await api.previewCron({ - cron_expression: scheduleForm.cronExpression.trim(), - timezone: scheduleForm.timezone.trim(), - count: 5, - }); - setCronResult(result); - onNotify({ tone: "success", message: "Cron 表达式校验通过" }); - } catch (error) { - setCronResult(null); - await handleError(error, "Cron 预览失败"); - } finally { - setBusy(null); - } - }; - - const runNow = async (): Promise => { - if (!schedule || busy) return; - if (positionDraftCount > 0) { - onNotify({ - tone: "info", - message: "还有节点位置未保存,请先点击'保存配置'再运行", - }); - return; - } - if (!schedule.dag_validation.valid || schedule.nodes.length === 0) { - onNotify({ - tone: "error", - message: "当前调度必须包含有效的非空 DAG 才能运行", - }); - return; - } - setBusy("run-now"); - try { - const created = await api.runScheduleNow(schedule.schedule_id); - setRuns((current) => [ - created, - ...current.filter((item) => item.run_id !== created.run_id), - ].slice(0, 20)); - setSchedule((current) => ( - current ? { ...current, last_run_at: created.queued_at } : current - )); - setSchedules((current) => current.map((item) => ( - item.schedule_id === schedule.schedule_id - ? { ...item, last_run_at: created.queued_at } - : item - ))); - onNotify({ - tone: "success", - message: `运行 ${created.run_id.slice(-8)} 已进入队列`, - }); - } catch (error) { - await handleError(error, "立即运行失败"); - } finally { - setBusy(null); - } - }; - - const addArtifactAt = async ( - artifact: ScheduleArtifact, - positionX: number, - positionY: number, - ): Promise => { - if (!schedule) { - onNotify({ tone: "info", message: "请先新建或选择一个调度方案" }); - return; - } - const nodeKey = artifactNodeKey(artifact, schedule); - const updated = await withMutation( - "add-node", - () => api.createScheduleNode(schedule.schedule_id, { - workflow_version: schedule.workflow_version, - node_key: nodeKey, - node_name: artifact.script_name, - versions_id: artifact.versions_id, - timeout_seconds: 600, - retry_count: 0, - retry_interval_sec: 5, - position_x: Math.max(20, Math.round(positionX)), - position_y: Math.max(20, Math.round(positionY)), - arguments_json: {}, - env_refs_json: {}, - }), - `${artifact.script_name} 已加入画布`, - ); - if (updated) { - const created = updated.nodes.find((item) => item.node_key === nodeKey); - setSelectedNodeId(created?.node_id ?? null); - } + void useSchedulesStore.getState().addSchedule(newScheduleName); }; const onCanvasDrop = (event: DragEvent): void => { @@ -801,7 +201,7 @@ export default function SchedulePage({ + canvasRef.current.scrollLeft - NODE_WIDTH / 2; const positionY = event.clientY - rect.top + canvasRef.current.scrollTop - NODE_HEIGHT / 2; - void addArtifactAt(artifact, positionX, positionY); + void useSchedulesStore.getState().addArtifactAt(artifact, positionX, positionY); }; const startNodeDrag = ( @@ -860,12 +260,12 @@ export default function SchedulePage({ const current = useSchedulesStore.getState().schedule; const node = current?.nodes.find((item) => item.node_id === drag.nodeId); if (!current || !node) return; - const position = { - position_x: Math.round(node.position_x), - position_y: Math.round(node.position_y), - }; - useSchedulesStore.getState().setPositionDraft(node.node_id, position); - setSchedule((value) => { + const position = { + position_x: Math.round(node.position_x), + position_y: Math.round(node.position_y), + }; + useSchedulesStore.getState().setPositionDraft(node.node_id, position); + setSchedule((value) => { if (!value) return value; return { ...value, @@ -876,124 +276,6 @@ export default function SchedulePage({ }); }; - const connectTo = async (targetNodeId: string): Promise => { - if (!schedule || !linkSourceId || busy) return; - if (linkSourceId === targetNodeId) { - setLinkSourceId(null); - onNotify({ tone: "info", message: "已取消连线" }); - return; - } - const sourceId = linkSourceId; - setLinkSourceId(null); - await withMutation( - "create-edge", - () => api.createScheduleEdge(schedule.schedule_id, { - workflow_version: schedule.workflow_version, - source_node_id: sourceId, - target_node_id: targetNodeId, - }), - "节点连线已创建", - ); - }; - - const saveNode = async (): Promise => { - if (!schedule || !selectedNode) return; - try { - const timeoutSeconds = Number(nodeForm.timeoutSeconds); - const retryCount = Number(nodeForm.retryCount); - const retryIntervalSec = Number(nodeForm.retryIntervalSec); - if (!nodeForm.nodeName.trim()) throw new Error("节点名称不能为空"); - if (!Number.isInteger(timeoutSeconds) || timeoutSeconds < 1) { - throw new Error("超时时间必须是正整数"); - } - if (!Number.isInteger(retryCount) || retryCount < 0) { - throw new Error("重试次数必须是非负整数"); - } - if (!Number.isInteger(retryIntervalSec) || retryIntervalSec < 0) { - throw new Error("重试间隔必须是非负整数"); - } - const argumentsJson = parseObject(nodeForm.argumentsJson, "运行参数"); - const rawEnv = parseObject(nodeForm.envRefsJson, "环境引用"); - const envRefsJson = Object.fromEntries( - Object.entries(rawEnv).map(([key, value]) => { - if (typeof value !== "string") { - throw new Error("环境引用的值必须是字符串"); - } - return [key, value]; - }), - ); - await withMutation( - "save-node", - () => api.updateScheduleNode(schedule.schedule_id, selectedNode.node_id, { - workflow_version: schedule.workflow_version, - node_name: nodeForm.nodeName.trim(), - timeout_seconds: timeoutSeconds, - retry_count: retryCount, - retry_interval_sec: retryIntervalSec, - arguments_json: argumentsJson, - env_refs_json: envRefsJson, - }), - "节点配置已保存", - ); - } catch (error) { - await handleError(error, "节点配置保存失败"); - } - }; - - const removeNode = async (target?: ScheduleNode): Promise => { - const node = target ?? selectedNode; - if (!schedule || !node || busy) return; - setContextMenu(null); - if (!window.confirm(`确定删除节点"${node.node_name}"吗?`)) return; - const updated = await withMutation( - "delete-node", - () => api.deleteScheduleNode( - schedule.schedule_id, - node.node_id, - schedule.workflow_version, - ), - "节点已删除", - ); - if (updated && selectedNodeId === node.node_id) setSelectedNodeId(null); - }; - - const removeEdge = async (target?: ScheduleEdge): Promise => { - const edge = target ?? selectedEdge; - if (!schedule || !edge || busy) return; - setContextMenu(null); - const updated = await withMutation( - "delete-edge", - () => api.deleteScheduleEdge( - schedule.schedule_id, - edge.edge_id, - schedule.workflow_version, - ), - "连线已删除", - ); - if (updated && selectedEdgeId === edge.edge_id) setSelectedEdgeId(null); - }; - - const checkDag = async (): Promise => { - if (!schedule || busy) return; - setBusy("validate"); - try { - const result = await api.validateSchedule(schedule.schedule_id); - setSchedule((current) => current - ? { ...current, dag_validation: result } - : current); - onNotify({ - tone: result.valid ? "success" : "error", - message: result.valid - ? `DAG 校验通过,共 ${result.node_count} 个节点` - : result.errors.map((item) => item.message).join(";"), - }); - } catch (error) { - await handleError(error, "DAG 校验失败"); - } finally { - setBusy(null); - } - }; - const filteredSchedules = useMemo(() => { const keyword = scheduleKeyword.trim().toLowerCase(); return keyword @@ -1027,21 +309,21 @@ export default function SchedulePage({
- @@ -1059,7 +341,7 @@ export default function SchedulePage({ ? "请先保存节点位置" : "立即执行当前已保存的稳定版本 DAG" } - onClick={() => void runNow()} + onClick={() => useSchedulesStore.getState().runNow()} > 立即运行 @@ -1067,7 +349,7 @@ export default function SchedulePage({ className="schedule-action--danger" type="button" disabled={!schedule || Boolean(busy)} - onClick={() => void removeSchedule(schedule ?? undefined)} + onClick={() => useSchedulesStore.getState().removeSchedule(schedule ?? undefined)} > 删除 @@ -1081,15 +363,15 @@ export default function SchedulePage({ selectedScheduleId={schedule?.schedule_id ?? null} keyword={scheduleKeyword} loading={loading} - onSelect={chooseSchedule} + onSelect={(id) => useSchedulesStore.getState().chooseSchedule(id)} onContextMenu={(event, item) => openContextMenu(event as ReactMouseEvent, item ? { kind: "schedule", schedule: item } : { kind: "schedule-list" })} onKeywordChange={setScheduleKeyword} /> void addArtifactAt(artifact, x, y)} - onDoubleClick={(artifact) => void addArtifactAt(artifact, 90 + (schedule?.nodes.length ?? 0) * 245, 130)} + onDrop={(artifact, x, y) => void useSchedulesStore.getState().addArtifactAt(artifact, x, y)} + onDoubleClick={(artifact) => void useSchedulesStore.getState().addArtifactAt(artifact, 90 + (schedule?.nodes.length ?? 0) * 245, 130)} onContextMenu={(event, artifact) => openContextMenu(event as ReactMouseEvent, { kind: "artifact", artifact })} onKeywordChange={setArtifactKeyword} /> @@ -1201,7 +483,7 @@ export default function SchedulePage({ onPointerDown={(event) => event.stopPropagation()} onClick={(event) => { event.stopPropagation(); - void connectTo(node.node_id); + void useSchedulesStore.getState().connectTo(node.node_id); }} />
@@ -1240,9 +522,11 @@ export default function SchedulePage({ onPointerDown={(event) => event.stopPropagation()} onClick={(event) => { event.stopPropagation(); - setLinkSourceId((current) => ( - current === node.node_id ? null : node.node_id - )); + setLinkSourceId( + useSchedulesStore.getState().linkSourceId === node.node_id + ? null + : node.node_id, + ); setSelectedNodeId(node.node_id); setSelectedEdgeId(null); }} @@ -1255,7 +539,7 @@ export default function SchedulePage({ 还没有选中调度方案

点击"新建"创建一个调度,然后拖入稳定版本脚本。

-
@@ -1275,7 +559,7 @@ export default function SchedulePage({ {schedule?.nodes.length ?? 0} 个节点 {schedule?.edges.length ?? 0} 条连线 {selectedEdge && ( - )} @@ -1290,8 +574,8 @@ export default function SchedulePage({ form={nodeForm} busy={Boolean(busy)} onChange={setNodeForm} - onSave={() => void saveNode()} - onDelete={() => void removeNode(selectedNode)} + onSave={() => useSchedulesStore.getState().saveNode(selectedNode)} + onDelete={() => useSchedulesStore.getState().removeNode(selectedNode)} /> ) : ( void runCronPreview()} - onSave={() => void saveSchedule()} + onPreview={() => useSchedulesStore.getState().runCronPreview()} + onSave={() => useSchedulesStore.getState().saveSchedule()} /> )}
@@ -1311,7 +595,7 @@ export default function SchedulePage({ loading={runsLoading} disabled={!schedule || Boolean(busy)} onRefresh={() => { - if (schedule) void refreshRuns(schedule.schedule_id, true); + if (schedule) void useSchedulesStore.getState().refreshRuns(schedule.schedule_id, true); }} /> @@ -1329,7 +613,7 @@ export default function SchedulePage({ className="schedule-context-menu__action" type="button" role="menuitem" - onClick={openCreateScheduleDialog} + onClick={() => useSchedulesStore.getState().openCreateDialog()} > 新建调度方案 @@ -1341,7 +625,7 @@ export default function SchedulePage({ className="schedule-context-menu__action" type="button" role="menuitem" - onClick={openCreateScheduleDialog} + onClick={() => useSchedulesStore.getState().openCreateDialog()} > 新建调度方案 @@ -1350,7 +634,7 @@ export default function SchedulePage({ className="schedule-context-menu__action" type="button" role="menuitem" - onClick={() => void renameSchedule(contextMenu.schedule)} + onClick={() => useSchedulesStore.getState().renameSchedule(contextMenu.schedule)} > 改名 @@ -1358,7 +642,7 @@ export default function SchedulePage({ -
void addSchedule(event)}> +