fix(api): align frontend with project-scoped backend
Use project-scoped endpoints and run IDs for current project, SCADA metadata, historical time series, migrated DMA results, and sensor placement runs. The regressions recurred because tests mocked pre-interceptor and new-only payload shapes, while history queries relied on implicit global scheme state and unbounded request fan-out. Cover post-interceptor pages, migrated result_rows, explicit run_id routing, multi-element requests, request limits, and cancellation.
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
import { apiFetch } from "@/lib/apiFetch";
|
||||
import { fetchHistoryData } from "./HistoryDataPanel";
|
||||
|
||||
jest.mock("@/lib/apiFetch", () => ({
|
||||
apiFetch: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("@components/olmap/core/MapComponent", () => ({
|
||||
useData: () => null,
|
||||
}));
|
||||
|
||||
const jsonResponse = (payload: unknown, status = 200) =>
|
||||
({
|
||||
ok: status >= 200 && status < 300,
|
||||
status,
|
||||
json: jest.fn().mockResolvedValue(payload),
|
||||
}) as unknown as Response;
|
||||
|
||||
const range = {
|
||||
from: new Date("2026-08-17T09:00:00.000Z"),
|
||||
to: new Date("2026-08-17T10:00:00.000Z"),
|
||||
};
|
||||
|
||||
describe("fetchHistoryData", () => {
|
||||
beforeEach(() => jest.clearAllMocks());
|
||||
|
||||
it("queries SCADA readings once per selected network element", async () => {
|
||||
jest.mocked(apiFetch).mockImplementation(async (input) => {
|
||||
const url = new URL(String(input));
|
||||
const elementId = url.searchParams.get("element_id") ?? "";
|
||||
return jsonResponse({
|
||||
[elementId]: [{ time: range.from.toISOString(), value: 1 }],
|
||||
});
|
||||
});
|
||||
|
||||
await fetchHistoryData(
|
||||
[
|
||||
["J-1", "junction"],
|
||||
["J-2", "junction"],
|
||||
],
|
||||
range,
|
||||
"none",
|
||||
);
|
||||
|
||||
const elementIds = jest
|
||||
.mocked(apiFetch)
|
||||
.mock.calls.map(([input]) =>
|
||||
new URL(String(input)).searchParams.get("element_id"),
|
||||
);
|
||||
expect(elementIds).toEqual(["J-1", "J-2", "J-1", "J-2"]);
|
||||
});
|
||||
|
||||
it("uses the analysis run ID for historical scheme simulation data", async () => {
|
||||
jest.mocked(apiFetch).mockResolvedValue(jsonResponse({ "P-1": [] }));
|
||||
|
||||
await fetchHistoryData(
|
||||
[["P-1", "pipe"]],
|
||||
range,
|
||||
"scheme",
|
||||
"99dd4142-368b-54cb-bfca-d59ee48f6298",
|
||||
);
|
||||
|
||||
const simulationUrls = jest
|
||||
.mocked(apiFetch)
|
||||
.mock.calls.map(([input]) => new URL(String(input)))
|
||||
.filter((url) => url.pathname.endsWith("/element-simulations"));
|
||||
|
||||
expect(simulationUrls).toHaveLength(2);
|
||||
expect(
|
||||
simulationUrls.map((url) => url.searchParams.get("run_id")),
|
||||
).toEqual([null, "99dd4142-368b-54cb-bfca-d59ee48f6298"]);
|
||||
});
|
||||
|
||||
it("rejects oversized element selections before issuing requests", async () => {
|
||||
jest.mocked(apiFetch).mockResolvedValue(jsonResponse({}));
|
||||
const featureInfos = Array.from(
|
||||
{ length: 201 },
|
||||
(_, index) => [`J-${index}`, "junction"] as [string, string],
|
||||
);
|
||||
|
||||
await expect(
|
||||
fetchHistoryData(featureInfos, range, "none"),
|
||||
).rejects.toThrow("历史数据一次最多查询 200 个管网元素");
|
||||
expect(apiFetch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("limits concurrent SCADA requests for multi-element history", async () => {
|
||||
let activeRequests = 0;
|
||||
let peakRequests = 0;
|
||||
jest.mocked(apiFetch).mockImplementation(async (input) => {
|
||||
activeRequests += 1;
|
||||
peakRequests = Math.max(peakRequests, activeRequests);
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
activeRequests -= 1;
|
||||
const elementId = new URL(String(input)).searchParams.get("element_id") ?? "";
|
||||
return jsonResponse({ [elementId]: [] });
|
||||
});
|
||||
|
||||
await fetchHistoryData(
|
||||
Array.from(
|
||||
{ length: 20 },
|
||||
(_, index) => [`J-${index}`, "junction"] as [string, string],
|
||||
),
|
||||
range,
|
||||
"none",
|
||||
);
|
||||
|
||||
expect(peakRequests).toBeLessThanOrEqual(8);
|
||||
});
|
||||
});
|
||||
@@ -53,10 +53,6 @@ export interface SCADADataPanelProps {
|
||||
featureInfos: [string, string][];
|
||||
/** 数据类型: realtime-查询模拟值和监测值, none-仅查询监测值, scheme-查询策略模拟值和监测值 */
|
||||
type?: "realtime" | "scheme" | "none";
|
||||
/** 策略类型 */
|
||||
scheme_type?: string;
|
||||
/** 策略名称 */
|
||||
scheme_name?: string;
|
||||
/** 默认展示的选项卡 */
|
||||
defaultTab?: "chart" | "table";
|
||||
/** Y 轴数值的小数位数 */
|
||||
@@ -65,6 +61,8 @@ export interface SCADADataPanelProps {
|
||||
start_time?: string;
|
||||
/** 外部传入结束时间(ISO8601 字符串),用于初始化并触发查询 */
|
||||
end_time?: string;
|
||||
/** 方案分析运行 ID;方案数据必须显式指定,避免读取到其他页面残留的全局方案。 */
|
||||
runId?: string;
|
||||
/** 关闭面板 */
|
||||
onClose: () => void;
|
||||
}
|
||||
@@ -73,6 +71,10 @@ type PanelTab = "chart" | "table";
|
||||
|
||||
type LoadingState = "idle" | "loading" | "success" | "error";
|
||||
|
||||
const MAX_HISTORY_ELEMENTS = 200;
|
||||
const MAX_HISTORY_ELEMENT_ID_LENGTH = 128;
|
||||
const HISTORY_SCADA_CONCURRENCY = 4;
|
||||
|
||||
const panelHeaderActionSx = {
|
||||
color: "primary.contrastText",
|
||||
backgroundColor: "rgba(255,255,255,0.08)",
|
||||
@@ -81,51 +83,126 @@ const panelHeaderActionSx = {
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 从后端 API 获取 SCADA 数据
|
||||
*/
|
||||
const fetchFromBackend = async (
|
||||
const buildApiUrl = (
|
||||
path: string,
|
||||
params: Record<string, string | boolean>,
|
||||
) => {
|
||||
const searchParams = new URLSearchParams();
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
searchParams.set(key, String(value));
|
||||
});
|
||||
return `${config.BACKEND_URL}${path}?${searchParams.toString()}`;
|
||||
};
|
||||
|
||||
const fetchOptionalJson = async (url: string, signal?: AbortSignal) => {
|
||||
const response = await apiFetch(url, { signal });
|
||||
if (response.status === 404) return null;
|
||||
if (!response.ok) {
|
||||
throw new Error(`历史数据请求失败: HTTP ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
};
|
||||
|
||||
const mapWithConcurrency = async <Input, Output>(
|
||||
items: Input[],
|
||||
limit: number,
|
||||
mapper: (item: Input, index: number) => Promise<Output>,
|
||||
): Promise<Output[]> => {
|
||||
const results = new Array<Output>(items.length);
|
||||
let nextIndex = 0;
|
||||
const workerCount = Math.min(limit, items.length);
|
||||
|
||||
await Promise.all(
|
||||
Array.from({ length: workerCount }, async () => {
|
||||
while (nextIndex < items.length) {
|
||||
const currentIndex = nextIndex;
|
||||
nextIndex += 1;
|
||||
results[currentIndex] = await mapper(items[currentIndex], currentIndex);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
/** 从后端 API 获取管网元素的监测、实时模拟和方案模拟数据。 */
|
||||
export const fetchHistoryData = async (
|
||||
featureInfos: [string, string][],
|
||||
range: { from: Date; to: Date },
|
||||
type: "realtime" | "scheme" | "none",
|
||||
scheme_type?: string,
|
||||
scheme_name?: string
|
||||
schemeRunId?: string,
|
||||
signal?: AbortSignal,
|
||||
): Promise<TimeSeriesPoint[]> => {
|
||||
if (featureInfos.length === 0) {
|
||||
return [];
|
||||
}
|
||||
if (featureInfos.length > MAX_HISTORY_ELEMENTS) {
|
||||
throw new Error(`历史数据一次最多查询 ${MAX_HISTORY_ELEMENTS} 个管网元素`);
|
||||
}
|
||||
if (
|
||||
featureInfos.some(
|
||||
([id]) =>
|
||||
id.trim().length === 0 || id.length > MAX_HISTORY_ELEMENT_ID_LENGTH,
|
||||
)
|
||||
) {
|
||||
throw new Error("历史数据包含无效的管网元素 ID");
|
||||
}
|
||||
|
||||
// 提取设备 ID 列表
|
||||
const featureIds = featureInfos.map(([id]) => id);
|
||||
const uniqueFeatureInfos = Array.from(
|
||||
new Map(featureInfos.map((featureInfo) => [featureInfo[0], featureInfo])).values(),
|
||||
);
|
||||
const featureIds = uniqueFeatureInfos.map(([id]) => id);
|
||||
|
||||
const feature_ids = featureIds.join(",");
|
||||
const start_time = dayjs(range.from).toISOString();
|
||||
const end_time = dayjs(range.to).toISOString();
|
||||
|
||||
// 将 featureInfos 转换为后端期望的格式: id1:type1,id2:type2
|
||||
const feature_infos = featureInfos
|
||||
const feature_infos = uniqueFeatureInfos
|
||||
.map(([id, type]) => `${id}:${type}`)
|
||||
.join(",");
|
||||
|
||||
// 监测值数据接口(use_cleaned=false)
|
||||
const rawDataUrl = `${config.BACKEND_URL}/api/v1/timeseries/views/element-scada-readings?element_id=${feature_ids}&start_time=${start_time}&end_time=${end_time}&use_cleaned=false`;
|
||||
// 清洗数据接口(use_cleaned=true)
|
||||
const cleanedDataUrl = `${config.BACKEND_URL}/api/v1/timeseries/views/element-scada-readings?element_id=${feature_ids}&start_time=${start_time}&end_time=${end_time}&use_cleaned=true`;
|
||||
// 模拟数据接口
|
||||
const simulationDataUrl = `${config.BACKEND_URL}/api/v1/timeseries/views/element-simulations?feature_infos=${feature_infos}&start_time=${start_time}&end_time=${end_time}`;
|
||||
// 策略模拟数据接口
|
||||
const schemeSimulationDataUrl = `${config.BACKEND_URL}/api/v1/timeseries/views/element-simulations?feature_infos=${feature_infos}&start_time=${start_time}&end_time=${end_time}&scheme_type=${scheme_type}&scheme_name=${scheme_name}`;
|
||||
const fetchElementScadaData = async (useCleaned: boolean) => {
|
||||
const results = await mapWithConcurrency(
|
||||
featureIds,
|
||||
HISTORY_SCADA_CONCURRENCY,
|
||||
(elementId) =>
|
||||
fetchOptionalJson(
|
||||
buildApiUrl("/api/v1/timeseries/views/element-scada-readings", {
|
||||
element_id: elementId,
|
||||
start_time,
|
||||
end_time,
|
||||
use_cleaned: useCleaned,
|
||||
}),
|
||||
signal,
|
||||
),
|
||||
);
|
||||
return Object.assign({}, ...results.filter(Boolean));
|
||||
};
|
||||
|
||||
const simulationDataUrl = buildApiUrl(
|
||||
"/api/v1/timeseries/views/element-simulations",
|
||||
{ feature_infos, start_time, end_time },
|
||||
);
|
||||
|
||||
if (type === "scheme" && !schemeRunId) {
|
||||
throw new Error("历史方案缺少分析运行 ID,无法读取方案时序数据");
|
||||
}
|
||||
|
||||
const schemeSimulationDataUrl = schemeRunId
|
||||
? buildApiUrl("/api/v1/timeseries/views/element-simulations", {
|
||||
feature_infos,
|
||||
start_time,
|
||||
end_time,
|
||||
run_id: schemeRunId,
|
||||
})
|
||||
: null;
|
||||
|
||||
try {
|
||||
if (type === "none") {
|
||||
// 查询清洗值和监测值
|
||||
const [cleanedRes, rawRes] = await Promise.all([
|
||||
apiFetch(cleanedDataUrl)
|
||||
.then((r) => (r.ok ? r.json() : null))
|
||||
.catch(() => null),
|
||||
apiFetch(rawDataUrl)
|
||||
.then((r) => (r.ok ? r.json() : null))
|
||||
.catch(() => null),
|
||||
fetchElementScadaData(true),
|
||||
fetchElementScadaData(false),
|
||||
]);
|
||||
|
||||
const cleanedData = transformBackendData(cleanedRes, featureIds);
|
||||
@@ -143,18 +220,10 @@ const fetchFromBackend = async (
|
||||
} else if (type === "scheme") {
|
||||
// 查询策略模拟值、实时模拟值、清洗值和监测值
|
||||
const [cleanedRes, rawRes, simulationRes, schemeSimRes] = await Promise.all([
|
||||
apiFetch(cleanedDataUrl)
|
||||
.then((r) => (r.ok ? r.json() : null))
|
||||
.catch(() => null),
|
||||
apiFetch(rawDataUrl)
|
||||
.then((r) => (r.ok ? r.json() : null))
|
||||
.catch(() => null),
|
||||
apiFetch(simulationDataUrl)
|
||||
.then((r) => (r.ok ? r.json() : null))
|
||||
.catch(() => null),
|
||||
apiFetch(schemeSimulationDataUrl)
|
||||
.then((r) => (r.ok ? r.json() : null))
|
||||
.catch(() => null),
|
||||
fetchElementScadaData(true),
|
||||
fetchElementScadaData(false),
|
||||
fetchOptionalJson(simulationDataUrl, signal),
|
||||
fetchOptionalJson(schemeSimulationDataUrl!, signal),
|
||||
]);
|
||||
|
||||
const cleanedData = transformBackendData(cleanedRes, featureIds);
|
||||
@@ -176,15 +245,9 @@ const fetchFromBackend = async (
|
||||
} else {
|
||||
// realtime: 查询模拟值、清洗值和监测值
|
||||
const [cleanedRes, rawRes, simulationRes] = await Promise.all([
|
||||
apiFetch(cleanedDataUrl)
|
||||
.then((r) => (r.ok ? r.json() : null))
|
||||
.catch(() => null),
|
||||
apiFetch(rawDataUrl)
|
||||
.then((r) => (r.ok ? r.json() : null))
|
||||
.catch(() => null),
|
||||
apiFetch(simulationDataUrl)
|
||||
.then((r) => (r.ok ? r.json() : null))
|
||||
.catch(() => null),
|
||||
fetchElementScadaData(true),
|
||||
fetchElementScadaData(false),
|
||||
fetchOptionalJson(simulationDataUrl, signal),
|
||||
]);
|
||||
|
||||
const cleanedData = transformBackendData(cleanedRes, featureIds);
|
||||
@@ -425,12 +488,11 @@ const emptyStateMessages: Record<
|
||||
const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
|
||||
featureInfos,
|
||||
type = "none",
|
||||
scheme_type = "burst_analysis",
|
||||
scheme_name,
|
||||
defaultTab = "chart",
|
||||
fractionDigits = 2,
|
||||
start_time,
|
||||
end_time,
|
||||
runId,
|
||||
onClose,
|
||||
}) => {
|
||||
// 从 featureInfos 中提取设备 ID 列表
|
||||
@@ -465,6 +527,7 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
|
||||
"raw" | "clean" | "sim" | "all"
|
||||
>(() => (featureInfos.length === 1 ? "all" : "clean"));
|
||||
const draggableRef = useRef<HTMLDivElement>(null);
|
||||
const requestControllerRef = useRef<AbortController | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setActiveTab(defaultTab);
|
||||
@@ -503,6 +566,7 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
|
||||
const handleFetch = useCallback(
|
||||
async (reason: string) => {
|
||||
if (!hasDevices) {
|
||||
requestControllerRef.current?.abort();
|
||||
setTimeSeries([]);
|
||||
setLoadingState("idle");
|
||||
setError(null);
|
||||
@@ -511,26 +575,43 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
|
||||
|
||||
setLoadingState("loading");
|
||||
setError(null);
|
||||
requestControllerRef.current?.abort();
|
||||
const requestController = new AbortController();
|
||||
requestControllerRef.current = requestController;
|
||||
try {
|
||||
const { from: rangeFrom, to: rangeTo } = normalizedRange;
|
||||
const result = await fetchFromBackend(
|
||||
const result = await fetchHistoryData(
|
||||
featureInfos,
|
||||
{
|
||||
from: rangeFrom.toDate(),
|
||||
to: rangeTo.toDate(),
|
||||
},
|
||||
type,
|
||||
scheme_type,
|
||||
scheme_name
|
||||
runId,
|
||||
requestController.signal,
|
||||
);
|
||||
if (requestControllerRef.current !== requestController) return;
|
||||
setTimeSeries(result);
|
||||
setLoadingState("success");
|
||||
} catch (err) {
|
||||
if (
|
||||
requestControllerRef.current !== requestController ||
|
||||
(err instanceof Error && err.name === "AbortError")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
setError(err instanceof Error ? err.message : "未知错误");
|
||||
setLoadingState("error");
|
||||
}
|
||||
},
|
||||
[featureInfos, hasDevices, normalizedRange, type, scheme_type, scheme_name]
|
||||
[featureInfos, hasDevices, normalizedRange, runId, type],
|
||||
);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
requestControllerRef.current?.abort();
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
// 设备变化时自动查询
|
||||
|
||||
@@ -123,6 +123,7 @@ const Toolbar: React.FC<ToolbarProps> = ({
|
||||
const [chatPanelType, setChatPanelType] = useState<
|
||||
"realtime" | "scheme" | "none"
|
||||
>("none");
|
||||
const [chatPanelRunId, setChatPanelRunId] = useState<string | null>(null);
|
||||
const [chatPanelTimeRange, setChatPanelTimeRange] = useState<{
|
||||
startTime?: string;
|
||||
endTime?: string;
|
||||
@@ -142,6 +143,7 @@ const Toolbar: React.FC<ToolbarProps> = ({
|
||||
setHighlightFeatures,
|
||||
setChatPanelFeatureInfos,
|
||||
setChatPanelType,
|
||||
setChatPanelRunId,
|
||||
setChatPanelTimeRange,
|
||||
setShowHistoryPanel,
|
||||
setShowStyleEditor,
|
||||
@@ -349,6 +351,7 @@ const Toolbar: React.FC<ToolbarProps> = ({
|
||||
setHighlightFeatures([]);
|
||||
}
|
||||
setChatPanelFeatureInfos(null);
|
||||
setChatPanelRunId(null);
|
||||
setChatPanelTimeRange(null);
|
||||
break;
|
||||
}
|
||||
@@ -934,12 +937,14 @@ const Toolbar: React.FC<ToolbarProps> = ({
|
||||
<ToolbarHistoryPanel
|
||||
showHistoryPanel={showHistoryPanel}
|
||||
chatPanelType={chatPanelType}
|
||||
chatPanelRunId={chatPanelRunId}
|
||||
chatPanelFeatureInfos={chatPanelFeatureInfos}
|
||||
chatPanelTimeRange={chatPanelTimeRange}
|
||||
highlightFeatures={highlightFeatures}
|
||||
HistoryPanel={HistoryPanel}
|
||||
schemeName={schemeName}
|
||||
queryType={queryType}
|
||||
schemeRunId={schemeRunId}
|
||||
onClose={() => {
|
||||
deactivateTool("history");
|
||||
setActiveTools((prev) => prev.filter((t) => t !== "history"));
|
||||
|
||||
@@ -10,6 +10,7 @@ import HistoryDataPanel from "./HistoryDataPanel";
|
||||
type ToolbarHistoryPanelProps = {
|
||||
showHistoryPanel: boolean;
|
||||
chatPanelType: "realtime" | "scheme" | "none";
|
||||
chatPanelRunId: string | null;
|
||||
chatPanelFeatureInfos: [string, string][] | null;
|
||||
chatPanelTimeRange: {
|
||||
startTime?: string;
|
||||
@@ -19,18 +20,21 @@ type ToolbarHistoryPanelProps = {
|
||||
HistoryPanel?: React.ComponentType<any>;
|
||||
schemeName?: string;
|
||||
queryType?: string;
|
||||
schemeRunId?: string;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
const ToolbarHistoryPanel: React.FC<ToolbarHistoryPanelProps> = ({
|
||||
showHistoryPanel,
|
||||
chatPanelType,
|
||||
chatPanelRunId,
|
||||
chatPanelFeatureInfos,
|
||||
chatPanelTimeRange,
|
||||
highlightFeatures,
|
||||
HistoryPanel,
|
||||
schemeName,
|
||||
queryType,
|
||||
schemeRunId,
|
||||
onClose,
|
||||
}) => {
|
||||
const featureInfos = useMemo(
|
||||
@@ -75,8 +79,6 @@ const ToolbarHistoryPanel: React.FC<ToolbarHistoryPanelProps> = ({
|
||||
return (
|
||||
<HistoryDataPanel
|
||||
featureInfos={featureInfos}
|
||||
scheme_type="burst_analysis"
|
||||
scheme_name={schemeName}
|
||||
type={
|
||||
chatPanelFeatureInfos
|
||||
? chatPanelType
|
||||
@@ -84,6 +86,7 @@ const ToolbarHistoryPanel: React.FC<ToolbarHistoryPanelProps> = ({
|
||||
}
|
||||
start_time={chatPanelTimeRange?.startTime}
|
||||
end_time={chatPanelTimeRange?.endTime}
|
||||
runId={chatPanelFeatureInfos ? chatPanelRunId ?? undefined : schemeRunId}
|
||||
onClose={onClose}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -20,6 +20,7 @@ type UseToolbarChatActionsParams = {
|
||||
setHighlightFeatures: Dispatch<SetStateAction<Feature[]>>;
|
||||
setChatPanelFeatureInfos: Dispatch<SetStateAction<[string, string][] | null>>;
|
||||
setChatPanelType: Dispatch<SetStateAction<"realtime" | "scheme" | "none">>;
|
||||
setChatPanelRunId: Dispatch<SetStateAction<string | null>>;
|
||||
setChatPanelTimeRange: Dispatch<
|
||||
SetStateAction<{ startTime?: string; endTime?: string } | null>
|
||||
>;
|
||||
@@ -37,6 +38,7 @@ export const useToolbarChatActions = ({
|
||||
setHighlightFeatures,
|
||||
setChatPanelFeatureInfos,
|
||||
setChatPanelType,
|
||||
setChatPanelRunId,
|
||||
setChatPanelTimeRange,
|
||||
setShowHistoryPanel,
|
||||
setShowStyleEditor,
|
||||
@@ -126,6 +128,7 @@ export const useToolbarChatActions = ({
|
||||
case "view_history": {
|
||||
setChatPanelFeatureInfos(action.featureInfos);
|
||||
setChatPanelType(action.dataType);
|
||||
setChatPanelRunId(action.runId ?? null);
|
||||
setChatPanelTimeRange({
|
||||
startTime: action.startTime,
|
||||
endTime: action.endTime,
|
||||
@@ -136,6 +139,7 @@ export const useToolbarChatActions = ({
|
||||
case "view_scada": {
|
||||
setChatPanelFeatureInfos(action.featureInfos);
|
||||
setChatPanelType("none");
|
||||
setChatPanelRunId(null);
|
||||
setChatPanelTimeRange({
|
||||
startTime: action.startTime,
|
||||
endTime: action.endTime,
|
||||
@@ -249,6 +253,7 @@ export const useToolbarChatActions = ({
|
||||
setChatPanelFeatureInfos,
|
||||
setChatPanelTimeRange,
|
||||
setChatPanelType,
|
||||
setChatPanelRunId,
|
||||
setHighlightFeatures,
|
||||
setShowHistoryPanel,
|
||||
setShowStyleEditor,
|
||||
|
||||
Reference in New Issue
Block a user