refactor: useSchedulesStore.ts

This commit is contained in:
tao.chen
2026-08-25 10:48:29 +08:00
parent 80c92f7fd4
commit 2ed8d7b2cc
16 changed files with 1581 additions and 1259 deletions
@@ -0,0 +1,204 @@
// 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 });
}
},
});