Files
next-tjwater-frontend/src/features/workbench/hooks/use-scada-event-atmosphere.ts
T

68 lines
3.6 KiB
TypeScript

import { useCallback, useEffect, useMemo, useState } from "react";
import type { OperationalTaskSummary } from "../api/operational-task-client";
import type { WorkbenchAlert } from "../types";
export type ScadaEventTask = Pick<OperationalTaskSummary,
"task_id" | "project_id" | "task_type" | "workflow" | "lifecycle_status" | "source" | "title" | "created_at">;
export type ScadaAlertAtmosphereState = {
id: string;
projectId: string | null;
alert: WorkbenchAlert | null;
preview: boolean;
count?: number;
};
const storageKey = (project: string | null) => `tjwater.scada-visual-dismissed@1:${project}`;
function readDismissed(project: string | null): string[] {
try {
const value: unknown = JSON.parse(sessionStorage.getItem(storageKey(project)) ?? "[]");
return Array.isArray(value) ? value.filter((id): id is string => typeof id === "string") : [];
} catch { return []; }
}
/** Visual state follows event completion, independently of sensor recovery or run outcomes. */
export function useScadaEventAtmosphere(projectId: string | null, alerts: WorkbenchAlert[] | null,
tasks: readonly ScadaEventTask[] | null) {
const storedDismissed = useMemo(() => readDismissed(projectId), [projectId]);
const [dismissed, setDismissed] = useState<{ project: string | null; ids: string[] } | null>(null);
const muted = dismissed?.project === projectId ? dismissed.ids : storedDismissed;
const [preview, setPreview] = useState<ScadaAlertAtmosphereState | null>(null);
useEffect(() => setPreview(null), [projectId]);
const candidates = new Map<string, WorkbenchAlert>();
const currentTasks = tasks?.filter((task) => task.project_id === projectId) ?? [];
const ended = new Set(currentTasks.filter((task) => ["completed", "cancelled"].includes(task.lifecycle_status))
.map((task) => `operational-task:${task.task_id}`));
for (const alert of alerts ?? []) {
if (alert.source && "kind" in alert.source && alert.source.kind === "scada_replay"
&& alert.source.ruleId === "scada_diagnosis" && alert.conditionId && !ended.has(alert.conditionId)) {
candidates.set(alert.conditionId, alert);
}
}
for (const task of currentTasks) {
const id = `operational-task:${task.task_id}`;
if (task.task_type !== "event_investigation" || !task.source.replay_context
|| !["scada_anomaly", "burst_report"].includes(task.workflow) || ended.has(id) || candidates.has(id)) continue;
candidates.set(id, { id, conditionId: id, title: task.title,
description: "监测事件持续处理中", occurredAt: task.created_at, time: "" });
}
const ids = [...candidates.keys()].filter((id) => !muted.includes(id)).sort();
const realKey = ids.join("|");
useEffect(() => { if (realKey) setPreview(null); }, [realKey]);
const atmosphere: ScadaAlertAtmosphereState | null = ids.length ? {
id: realKey, projectId, alert: candidates.get(ids[0])!, preview: false, count: ids.length
} : preview?.projectId === projectId ? preview : null;
const previewAtmosphere = useCallback(() => {
if (realKey) return;
setPreview({ id: `preview:${crypto.randomUUID()}`, projectId, alert: null, preview: true });
}, [projectId, realKey]);
const endAtmospherePreview = useCallback(() => setPreview(null), []);
const dismissAtmosphere = (id: string) => {
if (id !== atmosphere?.id) return;
setPreview(null);
if (atmosphere.preview) return;
const next = [...new Set([...muted, ...ids])];
setDismissed({ project: projectId, ids: next });
try { sessionStorage.setItem(storageKey(projectId), JSON.stringify(next)); } catch { /* Retain the current page preference. */ }
};
return { atmosphere, previewAtmosphere, endAtmospherePreview, dismissAtmosphere };
}