feat(workbench): integrate runtime data and map tooling
This commit is contained in:
@@ -1,23 +1,60 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import type { AccessTokenProvider } from "@/shared/auth/keycloak-auth";
|
||||
import { env } from "@/shared/config/env";
|
||||
import {
|
||||
fetchWorkbenchRuntimeSnapshot,
|
||||
type WorkbenchRuntimeSnapshot
|
||||
} from "../api/workbench-runtime-client";
|
||||
import { fetchRealtimePipeFlows } from "../api/realtime-flow-client";
|
||||
import {
|
||||
SCHEDULED_CONDITION_REFRESH_INTERVAL_MS,
|
||||
createOperationalScheduledConditions
|
||||
} from "../data/scheduled-conditions";
|
||||
import { WORKBENCH_SCENARIOS } from "../data/workbench-session";
|
||||
import type { ScheduledConditionItem } from "../types";
|
||||
import { createScheduledConditionAlerts } from "../utils/scheduled-condition-alerts";
|
||||
|
||||
const HEADER_DATA_TIME_STEP_MINUTES = 5;
|
||||
const INITIAL_HEADER_DATA_TIME = "同步中";
|
||||
const BACKEND_RUNTIME_REFRESH_INTERVAL_MS = 15_000;
|
||||
|
||||
export function formatHeaderDataTime(date: Date) {
|
||||
const snappedMinutes =
|
||||
Math.floor(date.getMinutes() / HEADER_DATA_TIME_STEP_MINUTES) * HEADER_DATA_TIME_STEP_MINUTES;
|
||||
export function resolveWorkbenchOperationalDate(
|
||||
backendSnapshot: WorkbenchRuntimeSnapshot | null,
|
||||
options: {
|
||||
now?: Date;
|
||||
runtimeMode?: "auto" | "demo" | "history" | "backend";
|
||||
} = {}
|
||||
) {
|
||||
const now = options.now ?? new Date();
|
||||
if (
|
||||
!["demo", "history"].includes(options.runtimeMode ?? "auto")
|
||||
|| !backendSnapshot
|
||||
) return now;
|
||||
|
||||
const timestamps = backendSnapshot.workOrders
|
||||
.map((workOrder) => Date.parse(workOrder.scheduledAt))
|
||||
.filter(Number.isFinite);
|
||||
const historicalTimestamps = timestamps.filter((timestamp) => timestamp <= now.getTime());
|
||||
const latestTimestamp = historicalTimestamps.length > 0
|
||||
? Math.max(...historicalTimestamps)
|
||||
: timestamps.length > 0
|
||||
? Math.min(...timestamps)
|
||||
: null;
|
||||
|
||||
return latestTimestamp === null ? now : new Date(latestTimestamp);
|
||||
}
|
||||
|
||||
export function formatHeaderDataTime(date: Date, snapToStep = true) {
|
||||
const minutes = snapToStep
|
||||
? Math.floor(date.getMinutes() / HEADER_DATA_TIME_STEP_MINUTES) * HEADER_DATA_TIME_STEP_MINUTES
|
||||
: date.getMinutes();
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(date.getDate()).padStart(2, "0");
|
||||
const hours = String(date.getHours()).padStart(2, "0");
|
||||
const minutes = String(snappedMinutes).padStart(2, "0");
|
||||
const formattedMinutes = String(minutes).padStart(2, "0");
|
||||
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}`;
|
||||
return `${year}-${month}-${day} ${hours}:${formattedMinutes}`;
|
||||
}
|
||||
|
||||
export function getMsUntilNextHeaderDataTimeTick(date: Date) {
|
||||
@@ -30,9 +67,74 @@ export function getMsUntilNextHeaderDataTimeTick(date: Date) {
|
||||
return Math.max(nextTick.getTime() - date.getTime(), 1_000);
|
||||
}
|
||||
|
||||
export function useWorkbenchRuntimeData() {
|
||||
export function resolveWorkbenchRuntimeData(
|
||||
mockConditions: ScheduledConditionItem[],
|
||||
backendSnapshot: WorkbenchRuntimeSnapshot | null,
|
||||
options: {
|
||||
operationalDate?: Date;
|
||||
runtimeMode?: "auto" | "demo" | "history" | "backend";
|
||||
} = {}
|
||||
) {
|
||||
const operationalDate = options.operationalDate ?? new Date();
|
||||
const fallbackConditions = mockConditions.filter(
|
||||
(condition) => condition.kind === "condition"
|
||||
&& isSameLocalOperationalDate(condition.scheduledAt, operationalDate)
|
||||
);
|
||||
const backendWorkOrders = (backendSnapshot?.workOrders ?? []).filter(
|
||||
(workOrder) => isSameLocalOperationalDate(workOrder.scheduledAt, operationalDate)
|
||||
);
|
||||
const backendWorkOrdersConfigured = Boolean(
|
||||
backendSnapshot && backendSnapshot.workOrderAvailability !== "not_configured"
|
||||
);
|
||||
const resolvedWorkOrders = backendWorkOrdersConfigured ? backendWorkOrders : [];
|
||||
const backendAlertsConfigured = Boolean(
|
||||
backendSnapshot
|
||||
&& backendSnapshot.alertAvailability !== "not_configured"
|
||||
);
|
||||
const useBackendScenarios = Boolean(
|
||||
backendSnapshot
|
||||
&& backendSnapshot.scenarioAvailability !== "not_configured"
|
||||
&& backendSnapshot.scenarios.length > 0
|
||||
);
|
||||
const useBackendArtifact = Boolean(
|
||||
backendSnapshot
|
||||
&& backendSnapshot.artifactAvailability !== "not_configured"
|
||||
&& backendSnapshot.latestAnalysisArtifact
|
||||
);
|
||||
|
||||
return {
|
||||
scheduledConditions: [...fallbackConditions, ...resolvedWorkOrders],
|
||||
workbenchAlerts: backendAlertsConfigured
|
||||
? backendSnapshot!.alerts
|
||||
: createScheduledConditionAlerts(fallbackConditions),
|
||||
alertDataSource: backendAlertsConfigured ? "backend" as const : "mock" as const,
|
||||
runtimeDataSource: "mock" as const,
|
||||
scenarios: useBackendScenarios ? backendSnapshot!.scenarios : WORKBENCH_SCENARIOS,
|
||||
scenarioDataSource: useBackendScenarios ? "analysis_runs" as const : "mock" as const,
|
||||
latestAnalysisArtifact: useBackendArtifact
|
||||
? backendSnapshot!.latestAnalysisArtifact
|
||||
: null,
|
||||
simulationOverlay: useBackendArtifact ? backendSnapshot!.simulationOverlay : null,
|
||||
artifactDataSource: useBackendArtifact ? "analysis_runs" as const : "mock" as const,
|
||||
workOrderDataSource: backendWorkOrdersConfigured ? "backend" as const : "none" as const
|
||||
};
|
||||
}
|
||||
|
||||
function isSameLocalOperationalDate(value: string, operationalDate: Date) {
|
||||
const candidate = new Date(value);
|
||||
if (Number.isNaN(candidate.getTime()) || Number.isNaN(operationalDate.getTime())) return false;
|
||||
return candidate.getFullYear() === operationalDate.getFullYear()
|
||||
&& candidate.getMonth() === operationalDate.getMonth()
|
||||
&& candidate.getDate() === operationalDate.getDate();
|
||||
}
|
||||
|
||||
export function useWorkbenchRuntimeData(getAccessToken?: AccessTokenProvider) {
|
||||
const [headerDataTime, setHeaderDataTime] = useState(INITIAL_HEADER_DATA_TIME);
|
||||
const [scheduledConditions, setScheduledConditions] = useState<ScheduledConditionItem[]>([]);
|
||||
const [mockConditions, setMockConditions] = useState<ScheduledConditionItem[]>([]);
|
||||
const [backendSnapshot, setBackendSnapshot] = useState<WorkbenchRuntimeSnapshot | null>(null);
|
||||
const [realtimePipeFlows, setRealtimePipeFlows] = useState<Awaited<
|
||||
ReturnType<typeof fetchRealtimePipeFlows>
|
||||
> | null>(null);
|
||||
const [scheduledConditionsLoading, setScheduledConditionsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -50,7 +152,10 @@ export function useWorkbenchRuntimeData() {
|
||||
|
||||
useEffect(() => {
|
||||
const refreshScheduledConditions = () => {
|
||||
setScheduledConditions(createOperationalScheduledConditions(new Date()));
|
||||
const operationalDate = resolveWorkbenchOperationalDate(backendSnapshot, {
|
||||
runtimeMode: env.TJWATER_WORKBENCH_RUNTIME_MODE
|
||||
});
|
||||
setMockConditions(createOperationalScheduledConditions(operationalDate));
|
||||
setScheduledConditionsLoading(false);
|
||||
};
|
||||
|
||||
@@ -61,11 +166,114 @@ export function useWorkbenchRuntimeData() {
|
||||
);
|
||||
|
||||
return () => window.clearInterval(intervalId);
|
||||
}, []);
|
||||
}, [backendSnapshot]);
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
let timeoutId: number | undefined;
|
||||
|
||||
const refreshBackendSnapshot = async () => {
|
||||
try {
|
||||
const snapshot = await fetchWorkbenchRuntimeSnapshot(getAccessToken, controller.signal);
|
||||
if (!controller.signal.aborted) {
|
||||
setBackendSnapshot(snapshot);
|
||||
}
|
||||
} catch {
|
||||
if (!controller.signal.aborted) setBackendSnapshot(null);
|
||||
} finally {
|
||||
if (!controller.signal.aborted) {
|
||||
timeoutId = window.setTimeout(
|
||||
refreshBackendSnapshot,
|
||||
BACKEND_RUNTIME_REFRESH_INTERVAL_MS
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
void refreshBackendSnapshot();
|
||||
return () => {
|
||||
controller.abort();
|
||||
if (timeoutId !== undefined) window.clearTimeout(timeoutId);
|
||||
};
|
||||
}, [getAccessToken]);
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
let timeoutId: number | undefined;
|
||||
|
||||
const refreshRealtimePipeFlows = async () => {
|
||||
try {
|
||||
const snapshot = await fetchRealtimePipeFlows(getAccessToken, controller.signal);
|
||||
if (!controller.signal.aborted) {
|
||||
setRealtimePipeFlows(snapshot.records.length > 0 ? snapshot : null);
|
||||
}
|
||||
} catch {
|
||||
if (!controller.signal.aborted) setRealtimePipeFlows(null);
|
||||
} finally {
|
||||
if (!controller.signal.aborted) {
|
||||
timeoutId = window.setTimeout(
|
||||
refreshRealtimePipeFlows,
|
||||
BACKEND_RUNTIME_REFRESH_INTERVAL_MS
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
void refreshRealtimePipeFlows();
|
||||
return () => {
|
||||
controller.abort();
|
||||
if (timeoutId !== undefined) window.clearTimeout(timeoutId);
|
||||
};
|
||||
}, [getAccessToken]);
|
||||
|
||||
const operationalDate = resolveWorkbenchOperationalDate(backendSnapshot, {
|
||||
runtimeMode: env.TJWATER_WORKBENCH_RUNTIME_MODE
|
||||
});
|
||||
const resolvedRuntimeData = resolveWorkbenchRuntimeData(mockConditions, backendSnapshot, {
|
||||
operationalDate,
|
||||
runtimeMode: env.TJWATER_WORKBENCH_RUNTIME_MODE
|
||||
});
|
||||
const backendDataTime = realtimePipeFlows?.observedAt;
|
||||
const backendRuntimeDataTime = backendSnapshot?.generatedAt;
|
||||
const demoMode = env.TJWATER_WORKBENCH_RUNTIME_MODE === "demo";
|
||||
const historyMode = env.TJWATER_WORKBENCH_RUNTIME_MODE === "history";
|
||||
const backendRuntimeActive = resolvedRuntimeData.runtimeDataSource !== "mock";
|
||||
const backendWorkOrdersActive = resolvedRuntimeData.workOrderDataSource === "backend";
|
||||
const backendHistoryActive = historyMode && (backendRuntimeActive || backendWorkOrdersActive);
|
||||
|
||||
return {
|
||||
headerDataTime,
|
||||
scheduledConditions,
|
||||
scheduledConditionsLoading
|
||||
headerDataTime: backendHistoryActive
|
||||
? `历史 ${formatHeaderDataTime(operationalDate, false)}`
|
||||
: demoMode || historyMode
|
||||
? `演示 ${formatHeaderDataTime(operationalDate, false)}`
|
||||
: backendDataTime
|
||||
? formatHeaderDataTime(new Date(backendDataTime), false)
|
||||
: backendRuntimeActive && backendRuntimeDataTime
|
||||
? formatHeaderDataTime(new Date(backendRuntimeDataTime), false)
|
||||
: headerDataTime === INITIAL_HEADER_DATA_TIME
|
||||
? headerDataTime
|
||||
: `演示 ${headerDataTime}`,
|
||||
headerDataSource: backendHistoryActive
|
||||
? "history" as const
|
||||
: demoMode || historyMode
|
||||
? "demo" as const
|
||||
: backendDataTime
|
||||
? "realtime" as const
|
||||
: backendRuntimeActive
|
||||
? "backend" as const
|
||||
: "mock" as const,
|
||||
operationalDate: formatLocalDateKey(operationalDate),
|
||||
operationalCursorTime: operationalDate.toISOString(),
|
||||
scheduledConditionsLoading,
|
||||
realtimePipeFlows,
|
||||
flowDataSource: realtimePipeFlows ? "realtime" as const : "mock" as const,
|
||||
...resolvedRuntimeData
|
||||
};
|
||||
}
|
||||
|
||||
function formatLocalDateKey(value: Date) {
|
||||
const year = value.getFullYear();
|
||||
const month = String(value.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(value.getDate()).padStart(2, "0");
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user