refactor: useSchedulesStore.ts
This commit is contained in:
@@ -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 });
|
||||
}
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user