refactor(workbench): remove redundant notification state
This commit is contained in:
@@ -71,11 +71,8 @@ export function useOperationalTasks(
|
|||||||
const acknowledgeEventsThrough = sync.acknowledgeEventsThrough;
|
const acknowledgeEventsThrough = sync.acknowledgeEventsThrough;
|
||||||
const [tasks, setTasks] = useState<OperationalTask[]>([]);
|
const [tasks, setTasks] = useState<OperationalTask[]>([]);
|
||||||
const [notifications, setNotifications] = useState<OperationalTaskNotification[]>([]);
|
const [notifications, setNotifications] = useState<OperationalTaskNotification[]>([]);
|
||||||
const [available, setAvailable] = useState(false);
|
|
||||||
const activatedRef = useRef(false);
|
|
||||||
const tasksRef = useRef<OperationalTask[]>([]);
|
const tasksRef = useRef<OperationalTask[]>([]);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [lastSyncedAt, setLastSyncedAt] = useState<string | null>(null);
|
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [refreshGeneration, setRefreshGeneration] = useState(0);
|
const [refreshGeneration, setRefreshGeneration] = useState(0);
|
||||||
const forceRefreshRef = useRef(false);
|
const forceRefreshRef = useRef(false);
|
||||||
@@ -111,13 +108,9 @@ export function useOperationalTasks(
|
|||||||
detailResetGenerationRef.current = sync.detailResetGeneration;
|
detailResetGenerationRef.current = sync.detailResetGeneration;
|
||||||
setTasks(nextTasks);
|
setTasks(nextTasks);
|
||||||
setNotifications(selectHeaderNotifications(sync.snapshot?.notifications ?? []));
|
setNotifications(selectHeaderNotifications(sync.snapshot?.notifications ?? []));
|
||||||
setAvailable(true);
|
|
||||||
activatedRef.current = true;
|
|
||||||
setError(null);
|
setError(null);
|
||||||
setLastSyncedAt(new Date().toISOString());
|
|
||||||
} catch (nextError) {
|
} catch (nextError) {
|
||||||
if (controller.signal.aborted) return;
|
if (controller.signal.aborted) return;
|
||||||
if (!activatedRef.current) setAvailable(false);
|
|
||||||
setError(nextError instanceof Error ? nextError.message : "工况任务服务暂时不可用。");
|
setError(nextError instanceof Error ? nextError.message : "工况任务服务暂时不可用。");
|
||||||
} finally {
|
} finally {
|
||||||
if (!controller.signal.aborted) {
|
if (!controller.signal.aborted) {
|
||||||
@@ -159,7 +152,6 @@ export function useOperationalTasks(
|
|||||||
...tasksRef.current.filter((task) => !changed.has(task.task_id))
|
...tasksRef.current.filter((task) => !changed.has(task.task_id))
|
||||||
];
|
];
|
||||||
setTasks(tasksRef.current);
|
setTasks(tasksRef.current);
|
||||||
setLastSyncedAt(new Date().toISOString());
|
|
||||||
}
|
}
|
||||||
acknowledgeEventsThrough(lastSequence);
|
acknowledgeEventsThrough(lastSequence);
|
||||||
}).catch((nextError) => {
|
}).catch((nextError) => {
|
||||||
@@ -189,11 +181,8 @@ export function useOperationalTasks(
|
|||||||
}, getAccessToken);
|
}, getAccessToken);
|
||||||
tasksRef.current = [task, ...tasksRef.current.filter((item) => item.task_id !== task.task_id)];
|
tasksRef.current = [task, ...tasksRef.current.filter((item) => item.task_id !== task.task_id)];
|
||||||
setTasks(tasksRef.current);
|
setTasks(tasksRef.current);
|
||||||
activatedRef.current = true;
|
|
||||||
setAvailable(true);
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setError(null);
|
setError(null);
|
||||||
setLastSyncedAt(new Date().toISOString());
|
|
||||||
return task;
|
return task;
|
||||||
}, [getAccessToken]);
|
}, [getAccessToken]);
|
||||||
|
|
||||||
@@ -300,10 +289,7 @@ export function useOperationalTasks(
|
|||||||
fetchOperationalTaskEvidenceSnapshot(taskId, evidenceId, getAccessToken), [getAccessToken]);
|
fetchOperationalTaskEvidenceSnapshot(taskId, evidenceId, getAccessToken), [getAccessToken]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
available,
|
|
||||||
error: error ?? sync.error,
|
error: error ?? sync.error,
|
||||||
syncStatus: sync.status,
|
|
||||||
lastSyncedAt,
|
|
||||||
loading,
|
loading,
|
||||||
tasks,
|
tasks,
|
||||||
conditions,
|
conditions,
|
||||||
|
|||||||
@@ -107,10 +107,15 @@ import type {
|
|||||||
WorkbenchUser
|
WorkbenchUser
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { getScheduledConditionIdFromAlertId } from "./utils/scheduled-condition-alerts";
|
import { getScheduledConditionIdFromAlertId } from "./utils/scheduled-condition-alerts";
|
||||||
import { getOperationalNotificationRoute } from "./utils/operational-notification-routing";
|
import { getOperationalNotificationSeverity } from "./utils/operational-notification-routing";
|
||||||
import { createAlertQueueConversationPrompt } from "./utils/scheduled-condition-prompts";
|
import { createAlertQueueConversationPrompt } from "./utils/scheduled-condition-prompts";
|
||||||
|
|
||||||
const WORKBENCH_LAYOUT_CSS_VARIABLES = getWorkbenchLayoutCssVariables();
|
const WORKBENCH_LAYOUT_CSS_VARIABLES = getWorkbenchLayoutCssVariables();
|
||||||
|
const ALERT_TIME_FORMATTER = new Intl.DateTimeFormat("zh-CN", {
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
hour12: false
|
||||||
|
});
|
||||||
|
|
||||||
export function MapWorkbenchPage({
|
export function MapWorkbenchPage({
|
||||||
user = WORKBENCH_USER,
|
user = WORKBENCH_USER,
|
||||||
@@ -212,28 +217,26 @@ export function MapWorkbenchPage({
|
|||||||
});
|
});
|
||||||
const scheduledConditions = useMemo(() => [
|
const scheduledConditions = useMemo(() => [
|
||||||
...runtimeScheduledConditions,
|
...runtimeScheduledConditions,
|
||||||
...(operationalTasks.available
|
...operationalTasks.conditions.filter((condition) =>
|
||||||
? operationalTasks.conditions.filter((condition) =>
|
|
||||||
condition.operationalTask?.task_type === "event_investigation"
|
condition.operationalTask?.task_type === "event_investigation"
|
||||||
|| condition.operationalTask?.task_type === "external_work_order"
|
|| condition.operationalTask?.task_type === "external_work_order"
|
||||||
)
|
)
|
||||||
: [])
|
], [operationalTasks.conditions, runtimeScheduledConditions]);
|
||||||
], [operationalTasks.available, operationalTasks.conditions, runtimeScheduledConditions]);
|
|
||||||
const workbenchAlerts = useMemo<WorkbenchAlert[]>(() => {
|
const workbenchAlerts = useMemo<WorkbenchAlert[]>(() => {
|
||||||
const taskNotifications = operationalTasks.notifications.flatMap((notification) => {
|
const taskNotifications = operationalTasks.notifications.flatMap((notification) => {
|
||||||
const route = getOperationalNotificationRoute(notification.kind);
|
const severity = getOperationalNotificationSeverity(notification.kind);
|
||||||
return route ? [{
|
return severity ? [{
|
||||||
id: `operational-task-notification:${notification.notification_id}`,
|
id: `operational-task-notification:${notification.notification_id}`,
|
||||||
conditionId: operationalTaskConditionId(notification.task_id),
|
conditionId: operationalTaskConditionId(notification.task_id),
|
||||||
title: notification.title,
|
title: notification.title,
|
||||||
description: notification.description,
|
description: notification.description,
|
||||||
severity: route.severity,
|
severity,
|
||||||
occurredAt: notification.created_at,
|
occurredAt: notification.created_at,
|
||||||
time: new Intl.DateTimeFormat("zh-CN", { hour: "2-digit", minute: "2-digit", hour12: false }).format(new Date(notification.created_at))
|
time: ALERT_TIME_FORMATTER.format(new Date(notification.created_at))
|
||||||
}] : [];
|
}] : [];
|
||||||
});
|
});
|
||||||
return operationalTasks.available ? [...taskNotifications, ...runtimeWorkbenchAlerts] : runtimeWorkbenchAlerts;
|
return [...taskNotifications, ...runtimeWorkbenchAlerts];
|
||||||
}, [operationalTasks.available, operationalTasks.notifications, runtimeWorkbenchAlerts]);
|
}, [operationalTasks.notifications, runtimeWorkbenchAlerts]);
|
||||||
const collapseAgentPanel = agent.collapsePanel;
|
const collapseAgentPanel = agent.collapsePanel;
|
||||||
const expandAgentPanel = agent.expandPanel;
|
const expandAgentPanel = agent.expandPanel;
|
||||||
const clearActiveTool = useCallback(() => {
|
const clearActiveTool = useCallback(() => {
|
||||||
|
|||||||
@@ -1,31 +1,22 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { getOperationalNotificationRoute } from "./operational-notification-routing";
|
import { getOperationalNotificationSeverity } from "./operational-notification-routing";
|
||||||
|
|
||||||
describe("getOperationalNotificationRoute", () => {
|
describe("getOperationalNotificationSeverity", () => {
|
||||||
it.each(["task_created", "report_updated", "task_completed"] as const)(
|
it.each(["task_created", "report_updated", "task_completed"] as const)(
|
||||||
"keeps normal lifecycle notification %s out of exception handling",
|
"keeps normal lifecycle notification %s out of exception handling",
|
||||||
(kind) => expect(getOperationalNotificationRoute(kind)).toBeNull()
|
(kind) => expect(getOperationalNotificationSeverity(kind)).toBeNull()
|
||||||
);
|
);
|
||||||
|
|
||||||
it("routes failures, approvals, human input, and external coordination", () => {
|
it("maps actionable notifications to their displayed severity", () => {
|
||||||
expect(getOperationalNotificationRoute("task_failed")).toEqual({
|
expect(getOperationalNotificationSeverity("task_failed")).toBe("critical");
|
||||||
category: "task_exception",
|
expect(getOperationalNotificationSeverity("run_delivery_degraded")).toBe("critical");
|
||||||
severity: "critical"
|
expect(getOperationalNotificationSeverity("access_approval_pending")).toBe("warning");
|
||||||
});
|
expect(getOperationalNotificationSeverity("action_approval_pending")).toBe("warning");
|
||||||
expect(getOperationalNotificationRoute("access_approval_pending")?.category)
|
expect(getOperationalNotificationSeverity("human_input_required")).toBe("warning");
|
||||||
.toBe("human_approval");
|
expect(getOperationalNotificationSeverity("external_coordination_failed")).toBe("critical");
|
||||||
expect(getOperationalNotificationRoute("action_approval_pending")?.category)
|
|
||||||
.toBe("human_approval");
|
|
||||||
expect(getOperationalNotificationRoute("human_input_required")?.category)
|
|
||||||
.toBe("human_input");
|
|
||||||
expect(getOperationalNotificationRoute("external_coordination_failed")).toEqual({
|
|
||||||
category: "external_coordination",
|
|
||||||
severity: "critical"
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps legacy action notifications visible during the database rollout", () => {
|
it("keeps legacy action notifications visible during the database rollout", () => {
|
||||||
expect(getOperationalNotificationRoute("action_pending")?.category)
|
expect(getOperationalNotificationSeverity("action_pending")).toBe("warning");
|
||||||
.toBe("human_approval");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,33 +1,19 @@
|
|||||||
import type { OperationalTaskNotification } from "../api/operational-task-client";
|
import type { OperationalTaskNotification } from "../api/operational-task-client";
|
||||||
|
|
||||||
export type OperationalNotificationCategory =
|
const NOTIFICATION_SEVERITIES: Partial<
|
||||||
| "task_exception"
|
Record<OperationalTaskNotification["kind"], "warning" | "critical">
|
||||||
| "human_approval"
|
|
||||||
| "human_input"
|
|
||||||
| "external_coordination";
|
|
||||||
|
|
||||||
export type OperationalNotificationRoute = {
|
|
||||||
category: OperationalNotificationCategory;
|
|
||||||
severity: "warning" | "critical";
|
|
||||||
};
|
|
||||||
|
|
||||||
const NOTIFICATION_ROUTES: Partial<
|
|
||||||
Record<OperationalTaskNotification["kind"], OperationalNotificationRoute>
|
|
||||||
> = {
|
> = {
|
||||||
task_failed: { category: "task_exception", severity: "critical" },
|
task_failed: "critical",
|
||||||
run_delivery_degraded: { category: "task_exception", severity: "critical" },
|
run_delivery_degraded: "critical",
|
||||||
action_pending: { category: "human_approval", severity: "warning" },
|
action_pending: "warning",
|
||||||
access_approval_pending: { category: "human_approval", severity: "warning" },
|
access_approval_pending: "warning",
|
||||||
action_approval_pending: { category: "human_approval", severity: "warning" },
|
action_approval_pending: "warning",
|
||||||
human_input_required: { category: "human_input", severity: "warning" },
|
human_input_required: "warning",
|
||||||
external_coordination_failed: {
|
external_coordination_failed: "critical"
|
||||||
category: "external_coordination",
|
|
||||||
severity: "critical"
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getOperationalNotificationRoute(
|
export function getOperationalNotificationSeverity(
|
||||||
kind: OperationalTaskNotification["kind"]
|
kind: OperationalTaskNotification["kind"]
|
||||||
): OperationalNotificationRoute | null {
|
): "warning" | "critical" | null {
|
||||||
return NOTIFICATION_ROUTES[kind] ?? null;
|
return NOTIFICATION_SEVERITIES[kind] ?? null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -428,8 +428,7 @@ test("后端没有调查任务时显示空态,分类标签按压不缩放", as
|
|||||||
await page.goto("/");
|
await page.goto("/");
|
||||||
await page.getByRole("button", { name: "工况任务", exact: true }).press("Enter");
|
await page.getByRole("button", { name: "工况任务", exact: true }).press("Enter");
|
||||||
const conditionPanel = page.getByRole("region", { name: "工况任务", exact: true });
|
const conditionPanel = page.getByRole("region", { name: "工况任务", exact: true });
|
||||||
const expandButton = conditionPanel.getByRole("button", { name: "展开工况任务" });
|
await conditionPanel.getByRole("button", { name: "展开工况任务" }).click();
|
||||||
if (await expandButton.isVisible()) await expandButton.click();
|
|
||||||
await conditionPanel.getByRole("button", { name: /调查任务/ }).click();
|
await conditionPanel.getByRole("button", { name: /调查任务/ }).click();
|
||||||
await expect(conditionPanel.getByText("暂无任务", { exact: true })).toBeVisible();
|
await expect(conditionPanel.getByText("暂无任务", { exact: true })).toBeVisible();
|
||||||
await expect(conditionPanel.getByText("新任务进入当前分类后会出现在这里。", { exact: true })).toBeVisible();
|
await expect(conditionPanel.getByText("新任务进入当前分类后会出现在这里。", { exact: true })).toBeVisible();
|
||||||
|
|||||||
Reference in New Issue
Block a user