118 lines
3.3 KiB
TypeScript
118 lines
3.3 KiB
TypeScript
// Logs slice: per-node-run log fetch + display state.
|
||
// toggleLog owns the open/closed toggle and lazily loads content from the
|
||
// signed URL on first open.
|
||
|
||
import type { StateCreator } from "zustand";
|
||
|
||
import { requireApi } from "./helpers";
|
||
import type { LogState } from "./types";
|
||
import type { SchedulesStore } from "./useSchedulesStore";
|
||
|
||
// ---- State ----
|
||
|
||
export type LogsSliceState = {
|
||
openLogNodeRunIds: Set<string>;
|
||
logStates: Record<string, LogState>;
|
||
};
|
||
|
||
// ---- Actions ----
|
||
|
||
export type LogsSliceActions = {
|
||
// pure setters
|
||
setOpenLogNodeRunIds: (
|
||
value: Set<string> | ((current: Set<string>) => Set<string>),
|
||
) => void;
|
||
setLogStates: (
|
||
updater: (current: Record<string, LogState>) => Record<string, LogState>,
|
||
) => void;
|
||
|
||
// mutations
|
||
toggleLog: (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 createLogsSlice: StateCreator<SchedulesStore, [], [], LogsSliceState & LogsSliceActions> = (set, get) => ({
|
||
// ---- initial state ----
|
||
openLogNodeRunIds: new Set(),
|
||
logStates: {},
|
||
|
||
// ---- pure setters ----
|
||
|
||
setOpenLogNodeRunIds: (value) =>
|
||
set((s: any) => ({
|
||
openLogNodeRunIds:
|
||
typeof value === "function"
|
||
? (value as (current: Set<string>) => Set<string>)(
|
||
s.openLogNodeRunIds,
|
||
)
|
||
: value,
|
||
})),
|
||
setLogStates: (updater) =>
|
||
set((s: any) => ({ logStates: updater(s.logStates) })),
|
||
|
||
// ---- mutations ----
|
||
|
||
toggleLog: async (runId, nodeRunId) => {
|
||
const api = requireApi();
|
||
const state = get();
|
||
const open = state.openLogNodeRunIds;
|
||
if (open.has(nodeRunId)) {
|
||
const next = new Set(open);
|
||
next.delete(nodeRunId);
|
||
set({ openLogNodeRunIds: next });
|
||
return;
|
||
}
|
||
const next = new Set(open);
|
||
next.add(nodeRunId);
|
||
set({ openLogNodeRunIds: next });
|
||
if (state.logStates[nodeRunId]?.content) return;
|
||
set((s: any) => ({
|
||
logStates: {
|
||
...s.logStates,
|
||
[nodeRunId]: {
|
||
loading: true,
|
||
content: null,
|
||
fileName: null,
|
||
error: null,
|
||
},
|
||
},
|
||
}));
|
||
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 content = await response.text();
|
||
set((s: any) => ({
|
||
logStates: {
|
||
...s.logStates,
|
||
[nodeRunId]: {
|
||
loading: false,
|
||
content,
|
||
fileName: artifacts.log?.file_name ?? null,
|
||
error: null,
|
||
},
|
||
},
|
||
}));
|
||
} catch (error) {
|
||
set((s: any) => ({
|
||
logStates: {
|
||
...s.logStates,
|
||
[nodeRunId]: {
|
||
loading: false,
|
||
content: null,
|
||
fileName: null,
|
||
error: error instanceof Error ? error.message : "日志读取失败",
|
||
},
|
||
},
|
||
}));
|
||
}
|
||
},
|
||
}); |