Files
model-platform/frontend/app/features/schedules/state/runsSlice.ts
T

204 lines
6.5 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.
// Runs slice: per-schedule run summaries, run detail expansion, artifact downloads.
// Cross-slice via `get()` for `schedule.schedule_id` consistency checks (refreshRuns)
// and `busy` (listSlice).
import type { ScheduleRunDetail, ScheduleRunSummary } from "../../../services/api";
import type { StateCreator } from "zustand";
import { handleError, requireApi, setApiOnline } from "./helpers";
import type { SchedulesStore } from "./useSchedulesStore";
// ---- State ----
export type RunsSliceState = {
runs: ScheduleRunSummary[];
runsLoading: boolean;
expandedRunId: string | null;
runDetails: Record<string, ScheduleRunDetail>;
detailLoadingId: string | null;
detailErrors: Record<string, string>;
artifactBusyKey: string | null;
artifactErrors: Record<string, string>;
};
// ---- Actions ----
export type RunsSliceActions = {
// pure setters
setRuns: (
value: ScheduleRunSummary[]
| ((current: ScheduleRunSummary[]) => ScheduleRunSummary[]),
) => void;
setRunsLoading: (loading: boolean) => void;
setRunDetails: (
updater: (current: Record<string, ScheduleRunDetail>) => Record<string, ScheduleRunDetail>,
) => void;
setDetailLoadingId: (id: string | null) => void;
setDetailErrors: (
updater: (current: Record<string, string>) => Record<string, string>,
) => void;
setArtifactBusyKey: (key: string | null) => void;
setArtifactErrors: (
updater: (current: Record<string, string>) => Record<string, string>,
) => void;
setExpandedRunId: (
id: string | null | ((current: string | null) => string | null),
) => void;
// mutations / loaders
refreshRuns: (scheduleId: string, showLoading?: boolean) => Promise<void>;
toggleRun: (runId: string) => void;
downloadResult: (runId: string, nodeRunId: string) => Promise<void>;
downloadLog: (runId: string, nodeRunId: string) => Promise<void>;
};
// `SchedulesStore` is the combined state+actions of all slices, so `get()`
// returns a fully typed snapshot — no `any` propagation.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const createRunsSlice: StateCreator<SchedulesStore, [], [], RunsSliceState & RunsSliceActions> = (set, get) => ({
// ---- initial state ----
runs: [],
runsLoading: false,
expandedRunId: null,
runDetails: {},
detailLoadingId: null,
detailErrors: {},
artifactBusyKey: null,
artifactErrors: {},
// ---- pure setters ----
setRuns: (value) =>
set((s: any) => ({
runs:
typeof value === "function"
? (value as (current: ScheduleRunSummary[]) => ScheduleRunSummary[])(
s.runs,
)
: value,
})),
setRunsLoading: (loading) => set({ runsLoading: loading }),
setRunDetails: (updater) =>
set((s: any) => ({ runDetails: updater(s.runDetails) })),
setDetailLoadingId: (id) => set({ detailLoadingId: id }),
setDetailErrors: (updater) =>
set((s: any) => ({ detailErrors: updater(s.detailErrors) })),
setArtifactBusyKey: (key) => set({ artifactBusyKey: key }),
setArtifactErrors: (updater) =>
set((s: any) => ({ artifactErrors: updater(s.artifactErrors) })),
setExpandedRunId: (id) =>
set((s: any) => ({
expandedRunId:
typeof id === "function"
? (id as (current: string | null) => string | null)(s.expandedRunId)
: id,
})),
// ---- mutations / loaders ----
refreshRuns: async (scheduleId, showLoading = false) => {
const api = requireApi();
if (showLoading) set({ runsLoading: true });
try {
const items = await api.listScheduleRuns({ scheduleId, limit: 20 });
if (get().schedule?.schedule_id === scheduleId) {
set({ runs: items });
}
setApiOnline(true);
} catch (error) {
if (showLoading) await handleError(get, error, "运行记录加载失败");
} finally {
// 如果用户已切换到另一个调度方案,不能让旧请求结束时覆盖新方案的
// 加载状态;新方案会由自己的请求负责关闭 loading。
if (showLoading && get().schedule?.schedule_id === scheduleId) {
set({ runsLoading: false });
}
}
},
toggleRun: (runId) => {
const expanded = get().expandedRunId;
if (expanded === runId) {
set({ expandedRunId: null });
} else {
set({ expandedRunId: runId });
}
},
downloadResult: async (runId, nodeRunId) => {
const api = requireApi();
const busyKey = `${nodeRunId}:result`;
set({
artifactBusyKey: busyKey,
artifactErrors: (() => {
const next = { ...get().artifactErrors };
delete next[nodeRunId];
return next;
})(),
});
try {
const artifacts = await api.getScheduleNodeRunArtifacts(runId, nodeRunId);
if (!artifacts.result) throw new Error("本次节点执行没有可下载结果");
const link = document.createElement("a");
link.href = artifacts.result.url;
link.download = artifacts.result.file_name;
document.body.appendChild(link);
link.click();
link.remove();
} catch (error) {
set((s: any) => ({
artifactErrors: {
...s.artifactErrors,
[nodeRunId]: error instanceof Error ? error.message : "结果下载失败",
},
}));
} finally {
set({ artifactBusyKey: null });
}
},
downloadLog: async (runId, nodeRunId) => {
const api = requireApi();
const busyKey = `${nodeRunId}:log-download`;
set({
artifactBusyKey: busyKey,
artifactErrors: (() => {
const next = { ...get().artifactErrors };
delete next[nodeRunId];
return next;
})(),
});
try {
const artifacts = await api.getScheduleNodeRunArtifacts(runId, nodeRunId);
if (!artifacts.log) throw new Error("本次节点执行没有可下载日志");
const response = await fetch(artifacts.log.url, {
credentials: "same-origin",
cache: "no-store",
});
if (!response.ok) {
throw new Error(`日志下载失败(HTTP ${response.status}`);
}
const objectUrl = URL.createObjectURL(await response.blob());
const link = document.createElement("a");
link.href = objectUrl;
link.download = artifacts.log.file_name;
document.body.appendChild(link);
link.click();
link.remove();
URL.revokeObjectURL(objectUrl);
} catch (error) {
set((s: any) => ({
artifactErrors: {
...s.artifactErrors,
[nodeRunId]:
error instanceof Error ? error.message : "日志下载失败",
},
}));
} finally {
set({ artifactBusyKey: null });
}
},
});