feat(ui): complete drainage workbench style migration
This commit is contained in:
@@ -1,21 +1,19 @@
|
||||
import type { UIMessage } from "ai";
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
import type {
|
||||
AgentChatMessage,
|
||||
AgentPermissionReply,
|
||||
AgentPermissionStatus,
|
||||
AgentQuestionRequest,
|
||||
AgentStreamRenderState
|
||||
} from "@/features/agent";
|
||||
import type { AgentSessionStreamEvent } from "@/features/agent/api/client";
|
||||
import {
|
||||
applyPermissionResponse,
|
||||
applyQuestionResponse,
|
||||
toTodoUpdate,
|
||||
upsertPermission,
|
||||
upsertProgress,
|
||||
upsertQuestion
|
||||
} from "@/features/agent/session-state";
|
||||
upsertQuestion,
|
||||
type AgentChatMessage,
|
||||
type AgentPermissionReply,
|
||||
type AgentPermissionStatus,
|
||||
type AgentQuestionRequest,
|
||||
type AgentSessionStreamEvent,
|
||||
type AgentStreamRenderState
|
||||
} from "@/features/agent";
|
||||
|
||||
type AgentUiDataParts = {
|
||||
progress: Record<string, unknown>;
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
type KeyboardEvent as ReactKeyboardEvent,
|
||||
type PointerEvent as ReactPointerEvent
|
||||
} from "react";
|
||||
import {
|
||||
WORKBENCH_LAYOUT,
|
||||
clampAgentPanelWidth,
|
||||
getAgentPanelDefaultWidth,
|
||||
getAgentPanelMaxWidth
|
||||
} from "../layout/workbench-layout";
|
||||
|
||||
export function useAgentPanelResize(viewportWidth: number, conditionExpanded = false) {
|
||||
const panelRef = useRef<HTMLDivElement | null>(null);
|
||||
const resizeLeftRef = useRef(0);
|
||||
const cleanupRef = useRef<(() => void) | null>(null);
|
||||
const widthRef = useRef<number | null>(null);
|
||||
const [width, setWidth] = useState<number | null>(null);
|
||||
const [committedWidth, setCommittedWidth] = useState<number | null>(null);
|
||||
const [resizing, setResizing] = useState(false);
|
||||
|
||||
useEffect(() => () => cleanupRef.current?.(), []);
|
||||
|
||||
useEffect(() => {
|
||||
if (widthRef.current === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextWidth = clampAgentPanelWidth(widthRef.current, viewportWidth, conditionExpanded);
|
||||
widthRef.current = nextWidth;
|
||||
setWidth(nextWidth);
|
||||
setCommittedWidth(nextWidth);
|
||||
}, [conditionExpanded, viewportWidth]);
|
||||
|
||||
const resize = useCallback(
|
||||
(clientX: number) => {
|
||||
const nextWidth = clampAgentPanelWidth(
|
||||
clientX - resizeLeftRef.current,
|
||||
window.innerWidth,
|
||||
conditionExpanded
|
||||
);
|
||||
widthRef.current = nextWidth;
|
||||
setWidth(nextWidth);
|
||||
},
|
||||
[conditionExpanded]
|
||||
);
|
||||
|
||||
const handlePointerDown = useCallback(
|
||||
(event: ReactPointerEvent<HTMLDivElement>) => {
|
||||
if (event.button !== 0 || !panelRef.current) return;
|
||||
|
||||
event.preventDefault();
|
||||
resizeLeftRef.current = panelRef.current.getBoundingClientRect().left;
|
||||
setResizing(true);
|
||||
resize(event.clientX);
|
||||
|
||||
cleanupRef.current?.();
|
||||
const handlePointerMove = (pointerEvent: PointerEvent) => {
|
||||
pointerEvent.preventDefault();
|
||||
resize(pointerEvent.clientX);
|
||||
};
|
||||
const stopResizing = () => {
|
||||
cleanupRef.current?.();
|
||||
setResizing(false);
|
||||
setCommittedWidth(widthRef.current);
|
||||
};
|
||||
const cleanup = () => {
|
||||
window.removeEventListener("pointermove", handlePointerMove);
|
||||
window.removeEventListener("pointerup", stopResizing);
|
||||
window.removeEventListener("pointercancel", stopResizing);
|
||||
cleanupRef.current = null;
|
||||
};
|
||||
|
||||
cleanupRef.current = cleanup;
|
||||
window.addEventListener("pointermove", handlePointerMove);
|
||||
window.addEventListener("pointerup", stopResizing);
|
||||
window.addEventListener("pointercancel", stopResizing);
|
||||
},
|
||||
[resize]
|
||||
);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(event: ReactKeyboardEvent<HTMLDivElement>) => {
|
||||
const currentWidth =
|
||||
panelRef.current?.getBoundingClientRect().width ?? WORKBENCH_LAYOUT.desktop.agentWidth;
|
||||
let nextWidth = currentWidth;
|
||||
|
||||
if (event.key === "ArrowLeft") {
|
||||
nextWidth -= 16;
|
||||
} else if (event.key === "ArrowRight") {
|
||||
nextWidth += 16;
|
||||
} else if (event.key === "Home") {
|
||||
nextWidth = getAgentPanelDefaultWidth(window.innerWidth, conditionExpanded);
|
||||
} else if (event.key === "End") {
|
||||
nextWidth = getAgentPanelMaxWidth(window.innerWidth, conditionExpanded);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
const clampedWidth = clampAgentPanelWidth(nextWidth, window.innerWidth, conditionExpanded);
|
||||
widthRef.current = clampedWidth;
|
||||
setWidth(clampedWidth);
|
||||
setCommittedWidth(clampedWidth);
|
||||
},
|
||||
[conditionExpanded]
|
||||
);
|
||||
|
||||
return {
|
||||
committedWidth,
|
||||
handleKeyDown,
|
||||
handlePointerDown,
|
||||
panelRef,
|
||||
resizing,
|
||||
width
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import type { Map as MapLibreMap, MapLayerMouseEvent } from "maplibre-gl";
|
||||
import type { RefObject } from "react";
|
||||
import { useEffect } from "react";
|
||||
@@ -11,7 +9,6 @@ import {
|
||||
} from "../map/feature-adapter";
|
||||
import { INTERACTIVE_HIT_LAYER_IDS } from "../map/layers";
|
||||
import { SOURCE_LAYERS, type WaterNetworkSourceId } from "../map/sources";
|
||||
import { SCADA_HIT_LAYER_ID } from "../map/scada";
|
||||
|
||||
export type MapFeatureInteractionState = {
|
||||
source: WaterNetworkSourceId;
|
||||
@@ -107,7 +104,7 @@ export function useMapInteractions({
|
||||
if (feature) onSelectFeature(toDetailFeature(feature));
|
||||
};
|
||||
|
||||
const hitLayerIds = [SCADA_HIT_LAYER_ID, ...INTERACTIVE_HIT_LAYER_IDS];
|
||||
const hitLayerIds = [...INTERACTIVE_HIT_LAYER_IDS];
|
||||
map.on("mousemove", hitLayerIds, handleMouseMove);
|
||||
map.on("mouseleave", hitLayerIds, handleMouseLeave);
|
||||
map.on("click", hitLayerIds, handleClick);
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import type { Map as MapLibreMap } from "maplibre-gl";
|
||||
import type { RefObject } from "react";
|
||||
import { useEffect } from "react";
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useChat } from "@ai-sdk/react";
|
||||
import { DefaultChatTransport } from "ai";
|
||||
@@ -7,34 +5,30 @@ import useSWR from "swr";
|
||||
import useSWRImmutable from "swr/immutable";
|
||||
import type { PersonaState } from "@/shared/ai-elements/persona";
|
||||
import { env } from "@/shared/config/env";
|
||||
import { showMapNotice } from "@/features/map/core/components/notice-actions";
|
||||
import type {
|
||||
AgentApprovalMode,
|
||||
AgentChatMessage,
|
||||
AgentModelOption,
|
||||
AgentPermissionReply,
|
||||
AgentPermissionRequest,
|
||||
AgentQuestionRequest,
|
||||
AgentStreamRenderState
|
||||
} from "@/features/agent";
|
||||
import {
|
||||
createAgentApiClient,
|
||||
type AgentSessionStreamEvent
|
||||
} from "@/features/agent/api/client";
|
||||
import { showMapNotice } from "@/features/map/core";
|
||||
import {
|
||||
FrontendActionExecutor,
|
||||
agentBootstrapKey,
|
||||
agentSessionsKey,
|
||||
createAgentApiClient,
|
||||
fetchAgentBootstrap,
|
||||
fetchAgentSessions
|
||||
} from "@/features/agent/api/swr";
|
||||
import {
|
||||
fetchAgentSessions,
|
||||
hasPendingAgentInput,
|
||||
isUiEnvelopeAllowed,
|
||||
parseUiEnvelopePayload,
|
||||
type AgentApprovalMode,
|
||||
type AgentChatMessage,
|
||||
type AgentConnectionStatus,
|
||||
type AgentModelOption,
|
||||
type AgentPermissionReply,
|
||||
type AgentPermissionRequest,
|
||||
type AgentQuestionRequest,
|
||||
type AgentSessionStreamEvent,
|
||||
type AgentStreamRenderState,
|
||||
type FrontendActionRequest,
|
||||
type UIEnvelopePayload,
|
||||
type UIRegistry
|
||||
} from "@/features/agent/ui-envelope";
|
||||
import type { FrontendActionRequest } from "@/features/agent/frontend-action";
|
||||
import { FrontendActionExecutor } from "@/features/agent/frontend-action/executor";
|
||||
} from "@/features/agent";
|
||||
import {
|
||||
applySessionStreamEvent,
|
||||
createCompletedStreamRenderState,
|
||||
@@ -53,8 +47,6 @@ import {
|
||||
|
||||
const AGENT_PANEL_COLLAPSE_MS = 180;
|
||||
|
||||
type AgentRuntimeAvailability = "connecting" | "connected" | "unavailable" | "failed";
|
||||
|
||||
type UseWorkbenchAgentOptions = {
|
||||
onUiEnvelope: (payload: UIEnvelopePayload, sessionId: string) => Promise<void> | void;
|
||||
onFrontendAction: (request: FrontendActionRequest, signal: AbortSignal) => Promise<unknown>;
|
||||
@@ -79,7 +71,8 @@ export function useWorkbenchAgent({ onUiEnvelope, onFrontendAction }: UseWorkben
|
||||
const [registry, setRegistry] = useState<UIRegistry | null>(null);
|
||||
const [sessionTitle, setSessionTitle] = useState("新对话");
|
||||
const [statusLabel, setStatusLabel] = useState("Agent 后端连接中");
|
||||
const [runtimeAvailability, setRuntimeAvailability] = useState<AgentRuntimeAvailability>("connecting");
|
||||
const [connectionStatus, setConnectionStatus] =
|
||||
useState<AgentConnectionStatus>("connecting");
|
||||
const [resumedStreaming, setResumedStreaming] = useState(false);
|
||||
const [modelOptions, setModelOptions] = useState<AgentModelOption[]>([]);
|
||||
const [selectedModel, setSelectedModel] = useState("");
|
||||
@@ -148,6 +141,7 @@ export function useWorkbenchAgent({ onUiEnvelope, onFrontendAction }: UseWorkben
|
||||
|
||||
const handleDataPart = useCallback(
|
||||
(part: AgentDataPart) => {
|
||||
setConnectionStatus("online");
|
||||
const eventSessionId = getDataString(part.data, "session_id");
|
||||
if (eventSessionId && sessionIdRef.current && eventSessionId !== sessionIdRef.current) {
|
||||
return;
|
||||
@@ -234,7 +228,7 @@ export function useWorkbenchAgent({ onUiEnvelope, onFrontendAction }: UseWorkben
|
||||
onData: (part) => handleDataPart(part as AgentDataPart),
|
||||
onError: (error) => {
|
||||
markLastAssistantStreamDone(chatRef.current.messages, setStreamRenderState);
|
||||
setRuntimeAvailability("failed");
|
||||
setConnectionStatus("offline");
|
||||
setStatusLabel("Agent 后端请求失败");
|
||||
showMapNotice({
|
||||
id: "agent-stream-status",
|
||||
@@ -245,6 +239,7 @@ export function useWorkbenchAgent({ onUiEnvelope, onFrontendAction }: UseWorkben
|
||||
},
|
||||
onFinish: () => {
|
||||
markLastAssistantStreamDone(chatRef.current.messages, setStreamRenderState);
|
||||
setConnectionStatus("online");
|
||||
setStatusLabel("Agent 分析完成");
|
||||
}
|
||||
});
|
||||
@@ -311,7 +306,7 @@ export function useWorkbenchAgent({ onUiEnvelope, onFrontendAction }: UseWorkben
|
||||
setModelOptions(backendConnection.models);
|
||||
setSelectedModel((current) => current || backendConnection.defaultModel || backendConnection.models[0]?.id || "");
|
||||
if (chatRef.current.status === "ready") {
|
||||
setRuntimeAvailability("connected");
|
||||
setConnectionStatus("online");
|
||||
setStatusLabel("准备就绪");
|
||||
}
|
||||
}, [backendConnection]);
|
||||
@@ -321,7 +316,7 @@ export function useWorkbenchAgent({ onUiEnvelope, onFrontendAction }: UseWorkben
|
||||
return;
|
||||
}
|
||||
|
||||
setRuntimeAvailability("unavailable");
|
||||
setConnectionStatus("offline");
|
||||
setStatusLabel("Agent 后端不可用");
|
||||
}, [backendConnectionError]);
|
||||
|
||||
@@ -379,10 +374,9 @@ export function useWorkbenchAgent({ onUiEnvelope, onFrontendAction }: UseWorkben
|
||||
const personaState = useMemo(
|
||||
() => deriveAgentPersonaState(messages, {
|
||||
chatStatus: chat.status,
|
||||
runtimeAvailability,
|
||||
statusLabel
|
||||
connectionStatus
|
||||
}),
|
||||
[chat.status, messages, runtimeAvailability, statusLabel]
|
||||
[chat.status, connectionStatus, messages]
|
||||
);
|
||||
|
||||
const refreshSessionHistory = useCallback(async () => {
|
||||
@@ -486,7 +480,7 @@ export function useWorkbenchAgent({ onUiEnvelope, onFrontendAction }: UseWorkben
|
||||
})
|
||||
.catch((error) => {
|
||||
if (!controller.signal.aborted) {
|
||||
setRuntimeAvailability("failed");
|
||||
setConnectionStatus("offline");
|
||||
setStatusLabel("Agent 会话流恢复失败");
|
||||
showMapNotice({
|
||||
id: "agent-stream-status",
|
||||
@@ -597,7 +591,7 @@ export function useWorkbenchAgent({ onUiEnvelope, onFrontendAction }: UseWorkben
|
||||
clearSessionId();
|
||||
setSessionTitle("新对话");
|
||||
setStatusLabel(nextStatusLabel);
|
||||
setRuntimeAvailability("connected");
|
||||
setConnectionStatus("online");
|
||||
setPermissionOverrides({});
|
||||
setQuestionOverrides({});
|
||||
setStreamRenderState({});
|
||||
@@ -801,6 +795,7 @@ export function useWorkbenchAgent({ onUiEnvelope, onFrontendAction }: UseWorkben
|
||||
modelOptions,
|
||||
openMobilePanel,
|
||||
closeMobilePanel,
|
||||
connectionStatus,
|
||||
panelCollapsing,
|
||||
panelOpen,
|
||||
personaState,
|
||||
@@ -832,23 +827,17 @@ function deriveAgentPersonaState(
|
||||
messages: AgentChatMessage[],
|
||||
{
|
||||
chatStatus,
|
||||
runtimeAvailability,
|
||||
statusLabel
|
||||
connectionStatus
|
||||
}: {
|
||||
chatStatus: string;
|
||||
runtimeAvailability: AgentRuntimeAvailability;
|
||||
statusLabel: string;
|
||||
connectionStatus: AgentConnectionStatus;
|
||||
}
|
||||
): PersonaState {
|
||||
if (
|
||||
runtimeAvailability === "unavailable" ||
|
||||
runtimeAvailability === "failed" ||
|
||||
/失败|不可用|错误|降级/.test(statusLabel)
|
||||
) {
|
||||
if (connectionStatus === "offline") {
|
||||
return "asleep";
|
||||
}
|
||||
|
||||
if (hasPendingOperatorInput(messages)) {
|
||||
if (hasPendingAgentInput(messages)) {
|
||||
return "listening";
|
||||
}
|
||||
|
||||
@@ -856,7 +845,7 @@ function deriveAgentPersonaState(
|
||||
chatStatus === "submitted" ||
|
||||
chatStatus === "streaming" ||
|
||||
hasRunningSessionWork(messages) ||
|
||||
runtimeAvailability === "connecting"
|
||||
connectionStatus === "connecting"
|
||||
) {
|
||||
return "thinking";
|
||||
}
|
||||
@@ -864,14 +853,6 @@ function deriveAgentPersonaState(
|
||||
return "idle";
|
||||
}
|
||||
|
||||
function hasPendingOperatorInput(messages: AgentChatMessage[]) {
|
||||
return messages.some(
|
||||
(message) =>
|
||||
message.permissions?.some((permission) => permission.status === "pending" || permission.status === "error") ||
|
||||
message.questions?.some((question) => question.status === "pending" || question.status === "error")
|
||||
);
|
||||
}
|
||||
|
||||
function hasRunningSessionWork(messages: AgentChatMessage[]) {
|
||||
const latestProgress = messages.flatMap((message) => message.progress ?? []).at(-1);
|
||||
const latestTodo = messages.flatMap((message) => message.todos?.todos ?? []).at(-1);
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import type { Map as MapLibreMap, MapMouseEvent } from "maplibre-gl";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState, type RefObject } from "react";
|
||||
import {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import type { Map as MapLibreMap } from "maplibre-gl";
|
||||
import { useEffect, useRef, useSyncExternalStore, type RefObject } from "react";
|
||||
import { getResponsiveWorkbenchPadding } from "../map/camera";
|
||||
@@ -9,15 +7,31 @@ export function useWorkbenchMapController({
|
||||
mapRef,
|
||||
mapReady,
|
||||
leftPanelOpen,
|
||||
rightPanelOpen
|
||||
rightPanelOpen,
|
||||
conditionPanelExpanded = false,
|
||||
agentPanelWidth
|
||||
}: {
|
||||
mapRef: RefObject<MapLibreMap | null>;
|
||||
mapReady: boolean;
|
||||
leftPanelOpen: boolean;
|
||||
rightPanelOpen: boolean;
|
||||
conditionPanelExpanded?: boolean;
|
||||
agentPanelWidth?: number;
|
||||
}) {
|
||||
const valuesRef = useRef({ mapReady, leftPanelOpen, rightPanelOpen });
|
||||
valuesRef.current = { mapReady, leftPanelOpen, rightPanelOpen };
|
||||
const valuesRef = useRef({
|
||||
mapReady,
|
||||
leftPanelOpen,
|
||||
rightPanelOpen,
|
||||
conditionPanelExpanded,
|
||||
agentPanelWidth
|
||||
});
|
||||
valuesRef.current = {
|
||||
mapReady,
|
||||
leftPanelOpen,
|
||||
rightPanelOpen,
|
||||
conditionPanelExpanded,
|
||||
agentPanelWidth
|
||||
};
|
||||
const controllerRef = useRef<WorkbenchMapController | null>(null);
|
||||
if (!controllerRef.current) {
|
||||
controllerRef.current = new WorkbenchMapController({
|
||||
@@ -26,14 +40,22 @@ export function useWorkbenchMapController({
|
||||
getPadding: () => {
|
||||
const map = mapRef.current;
|
||||
return map
|
||||
? getResponsiveWorkbenchPadding(map, valuesRef.current.leftPanelOpen, valuesRef.current.rightPanelOpen)
|
||||
? getResponsiveWorkbenchPadding(
|
||||
map,
|
||||
valuesRef.current.leftPanelOpen,
|
||||
valuesRef.current.rightPanelOpen
|
||||
)
|
||||
: { top: 48, right: 48, bottom: 48, left: 48 };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const controller = controllerRef.current;
|
||||
const state = useSyncExternalStore(controller.subscribe, controller.getSnapshot, controller.getSnapshot);
|
||||
const state = useSyncExternalStore(
|
||||
controller.subscribe,
|
||||
controller.getSnapshot,
|
||||
controller.getSnapshot
|
||||
);
|
||||
useEffect(() => () => controller.destroy(), [controller]);
|
||||
return { controller, state };
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import "maplibre-gl/dist/maplibre-gl.css";
|
||||
|
||||
import maplibregl, { type Map as MapLibreMap, type MapSourceDataEvent } from "maplibre-gl";
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import type { Map as MapLibreMap, MapGeoJSONFeature, MapMouseEvent } from "maplibre-gl";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState, type RefObject } from "react";
|
||||
import {
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
formatHeaderDataTime,
|
||||
getMsUntilNextHeaderDataTimeTick
|
||||
} from "./use-workbench-runtime-data";
|
||||
|
||||
describe("workbench runtime data", () => {
|
||||
it("snaps the header time to the current five-minute interval", () => {
|
||||
expect(formatHeaderDataTime(new Date(2026, 6, 22, 15, 43, 31))).toBe("2026-07-22 15:40");
|
||||
});
|
||||
|
||||
it("schedules the next header refresh on the next interval boundary", () => {
|
||||
expect(getMsUntilNextHeaderDataTimeTick(new Date(2026, 6, 22, 15, 43, 30))).toBe(90_000);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,71 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
SCHEDULED_CONDITION_REFRESH_INTERVAL_MS,
|
||||
createOperationalScheduledConditions
|
||||
} from "../data/scheduled-conditions";
|
||||
import type { ScheduledConditionItem } from "../types";
|
||||
|
||||
const HEADER_DATA_TIME_STEP_MINUTES = 5;
|
||||
const INITIAL_HEADER_DATA_TIME = "同步中";
|
||||
|
||||
export function formatHeaderDataTime(date: Date) {
|
||||
const snappedMinutes =
|
||||
Math.floor(date.getMinutes() / HEADER_DATA_TIME_STEP_MINUTES) * HEADER_DATA_TIME_STEP_MINUTES;
|
||||
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");
|
||||
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}`;
|
||||
}
|
||||
|
||||
export function getMsUntilNextHeaderDataTimeTick(date: Date) {
|
||||
const nextTick = new Date(date);
|
||||
const nextMinutes =
|
||||
Math.floor(date.getMinutes() / HEADER_DATA_TIME_STEP_MINUTES) * HEADER_DATA_TIME_STEP_MINUTES +
|
||||
HEADER_DATA_TIME_STEP_MINUTES;
|
||||
|
||||
nextTick.setMinutes(nextMinutes, 0, 0);
|
||||
return Math.max(nextTick.getTime() - date.getTime(), 1_000);
|
||||
}
|
||||
|
||||
export function useWorkbenchRuntimeData() {
|
||||
const [headerDataTime, setHeaderDataTime] = useState(INITIAL_HEADER_DATA_TIME);
|
||||
const [scheduledConditions, setScheduledConditions] = useState<ScheduledConditionItem[]>([]);
|
||||
const [scheduledConditionsLoading, setScheduledConditionsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
let timeoutId: number;
|
||||
|
||||
const refreshHeaderDataTime = () => {
|
||||
const now = new Date();
|
||||
setHeaderDataTime(formatHeaderDataTime(now));
|
||||
timeoutId = window.setTimeout(refreshHeaderDataTime, getMsUntilNextHeaderDataTimeTick(now));
|
||||
};
|
||||
|
||||
refreshHeaderDataTime();
|
||||
return () => window.clearTimeout(timeoutId);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const refreshScheduledConditions = () => {
|
||||
setScheduledConditions(createOperationalScheduledConditions(new Date()));
|
||||
setScheduledConditionsLoading(false);
|
||||
};
|
||||
|
||||
refreshScheduledConditions();
|
||||
const intervalId = window.setInterval(
|
||||
refreshScheduledConditions,
|
||||
SCHEDULED_CONDITION_REFRESH_INTERVAL_MS
|
||||
);
|
||||
|
||||
return () => window.clearInterval(intervalId);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
headerDataTime,
|
||||
scheduledConditions,
|
||||
scheduledConditionsLoading
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user