68 lines
3.6 KiB
TypeScript
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 };
|
|
}
|