fix(api): align frontend with REST contracts

This commit is contained in:
2026-07-30 20:38:52 +08:00
parent b57e58ff87
commit 782363cfb6
65 changed files with 99362 additions and 318 deletions
+2 -2
View File
@@ -640,7 +640,7 @@ export const SystemAdminPanel = () => {
const loadInitialState = async () => {
try {
const adminResponse = await apiFetch(`${config.BACKEND_URL}/api/v1/admin/me`, {
const adminResponse = await apiFetch(`${config.BACKEND_URL}/api/v1/admin/users/me`, {
projectHeaderMode: "omit",
skipAuthRedirect: true,
});
@@ -947,7 +947,7 @@ export const SystemAdminPanel = () => {
const form = databaseForms[role];
const body = form.dsn.trim() ? { dsn: form.dsn.trim() } : {};
const response = await apiFetch(
`${config.BACKEND_URL}/api/v1/admin/projects/${projectId.trim()}/databases/${role}/health`,
`${config.BACKEND_URL}/api/v1/admin/projects/${projectId.trim()}/databases/${role}/health-checks`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
+1 -1
View File
@@ -107,7 +107,7 @@ const defaultFilters: AuditFilters = {
end_time: "",
};
const AUDIT_LOGS_PATH = "/api/v1/audit/logs";
const AUDIT_LOGS_PATH = "/api/v1/audit-logs";
const statusFilterOptions: Array<{ value: AuditStatusFilter; label: string }> = [
{ value: "all", label: "全部状态" },
+2 -2
View File
@@ -84,7 +84,7 @@ describe("chatStorage backend session operations", () => {
sessionId: "session-1",
messages: [{ id: "message-1", role: "user", content: "查压力" }],
});
expect(String(apiFetch.mock.calls[0][0])).toContain("/session/session-1");
expect(String(apiFetch.mock.calls[0][0])).toContain("/sessions/session-1");
expect(apiFetch.mock.calls[0][1]).toMatchObject({ method: "GET" });
});
@@ -98,7 +98,7 @@ describe("chatStorage backend session operations", () => {
isTitleManuallyEdited: true,
});
expect(String(apiFetch.mock.calls[0][0])).toContain("/session/session-1/title");
expect(String(apiFetch.mock.calls[0][0])).toContain("/sessions/session-1");
expect(apiFetch.mock.calls[0][1]).toMatchObject({ method: "PATCH" });
expect(JSON.parse(String(apiFetch.mock.calls[0][1]?.body))).toEqual({
title: "新标题",
+4 -4
View File
@@ -46,7 +46,7 @@ const toMillis = (value: string | number | undefined) =>
const normalizeTitle = (value?: string) => value?.trim() || "新对话";
const fetchBackendChatSessions = async (): Promise<ChatSessionSummary[]> => {
const response = await apiFetch(`${config.AGENT_URL}/api/v1/agent/chat/sessions`, {
const response = await apiFetch(`${config.AGENT_URL}/api/v1/agent/sessions`, {
method: "GET",
projectHeaderMode: "include",
skipAuthRedirect: true,
@@ -72,7 +72,7 @@ const fetchBackendChatSessions = async (): Promise<ChatSessionSummary[]> => {
const fetchBackendChatSession = async (sessionId: string): Promise<LoadedChatState> => {
const response = await apiFetch(
`${config.AGENT_URL}/api/v1/agent/chat/session/${encodeURIComponent(sessionId)}`,
`${config.AGENT_URL}/api/v1/agent/sessions/${encodeURIComponent(sessionId)}`,
{
method: "GET",
projectHeaderMode: "include",
@@ -110,7 +110,7 @@ const updateBackendChatSessionTitle = async (
isTitleManuallyEdited?: boolean,
) => {
const response = await apiFetch(
`${config.AGENT_URL}/api/v1/agent/chat/session/${encodeURIComponent(sessionId)}/title`,
`${config.AGENT_URL}/api/v1/agent/sessions/${encodeURIComponent(sessionId)}`,
{
method: "PATCH",
headers: {
@@ -131,7 +131,7 @@ const updateBackendChatSessionTitle = async (
const deleteBackendChatSession = async (sessionId: string) => {
const response = await apiFetch(
`${config.AGENT_URL}/api/v1/agent/chat/session/${encodeURIComponent(sessionId)}`,
`${config.AGENT_URL}/api/v1/agent/sessions/${encodeURIComponent(sessionId)}`,
{
method: "DELETE",
projectHeaderMode: "include",
@@ -11,8 +11,7 @@ describe("burst detection request", () => {
const state = createBurstDetectionAnalysisParametersState();
state.schemeName = " latest-case ";
expect(buildBurstDetectionRequest(state, "fengyang")).toEqual({
network: "fengyang",
expect(buildBurstDetectionRequest(state)).toEqual({
scheme_name: "latest-case",
sampling_interval_minutes: 15,
});
@@ -33,10 +32,8 @@ describe("burst detection request", () => {
samplingIntervalMinutes: 30,
samplingIntervalSource: "manual",
},
"fengyang",
),
).toEqual({
network: "fengyang",
scheme_name: "history-case",
sampling_interval_minutes: 30,
target_time: targetTime.toISOString(),
@@ -92,9 +92,7 @@ export const resolvePressureSamplingInterval = (items: ScadaInfoItem[]) => {
export const buildBurstDetectionRequest = (
parameters: BurstDetectionAnalysisParametersState,
network: string,
) => ({
network,
scheme_name: parameters.schemeName.trim(),
sampling_interval_minutes: parameters.samplingIntervalMinutes,
...(parameters.detectionMode === "historical" && parameters.targetTime
@@ -129,7 +127,7 @@ const AnalysisParameters: React.FC<Props> = ({
let active = true;
setFrequencyLoading(true);
api
.get("/api/v1/getallscadainfo/", { params: { network: NETWORK_NAME } })
.get("/api/v1/scada-info")
.then((response) => {
if (!active) return;
const interval = resolvePressureSamplingInterval(
@@ -186,8 +184,8 @@ const AnalysisParameters: React.FC<Props> = ({
try {
const response = await api.post(
"/api/v1/burst-detection/detect/",
buildBurstDetectionRequest(parametersState, NETWORK_NAME),
"/api/v1/burst-detections",
buildBurstDetectionRequest(parametersState),
);
onResult(response.data as BurstDetectionResult);
open?.({
@@ -131,7 +131,6 @@ const SchemeQuery: React.FC<Props> = ({
setLoading(true);
try {
const params: Record<string, string> = {
network: NETWORK_NAME,
scheme_type: "burst_detection",
};
if (!queryAll && queryDate) {
@@ -162,7 +161,7 @@ const SchemeQuery: React.FC<Props> = ({
try {
const response = await api.get(
`/api/v1/schemes/${encodeURIComponent(schemeName)}`,
{ params: { network: NETWORK_NAME, scheme_type: "burst_detection" } },
{ params: { scheme_type: "burst_detection" } },
);
const schemeRecord = response.data as BurstDetectionSchemeRecord & {
result_payload?: BurstDetectionResult;
@@ -125,7 +125,7 @@ const AnalysisParameters: React.FC<Props> = ({
setSchemeLoading(true);
try {
const response = await api.get(`${config.BACKEND_URL}/api/v1/schemes`, {
params: { network: NETWORK_NAME, scheme_type: "burst_analysis" },
params: { scheme_type: "burst_analysis" },
});
const burstSchemes = (response.data as SchemeItem[]).filter(
(scheme) => scheme.scheme_type === "burst_analysis",
@@ -222,9 +222,8 @@ const AnalysisParameters: React.FC<Props> = ({
: undefined;
const response = await api.post(
`${config.BACKEND_URL}/api/v1/burst-location/locate/`,
`${config.BACKEND_URL}/api/v1/burst-locations`,
{
network: NETWORK_NAME,
data_source: dataSource,
scheme_name: schemeName.trim() || undefined,
burst_leakage: toM3s(burstLeakage, FLOW_DISPLAY_UNIT),
@@ -228,7 +228,6 @@ const SchemeQuery: React.FC<Props> = ({
setLoading(true);
try {
const params: Record<string, string> = {
network: NETWORK_NAME,
scheme_type: "burst_location",
};
if (!queryAll && queryDate) {
@@ -238,7 +237,7 @@ const SchemeQuery: React.FC<Props> = ({
const [response, simulationResponse] = await Promise.all([
api.get(`${config.BACKEND_URL}/api/v1/schemes`, { params }),
api.get(`${config.BACKEND_URL}/api/v1/schemes`, {
params: { network: NETWORK_NAME, scheme_type: "burst_analysis" },
params: { scheme_type: "burst_analysis" },
}),
]);
const nextSchemes = response.data as BurstSchemeRecord[];
@@ -281,7 +280,7 @@ const SchemeQuery: React.FC<Props> = ({
try {
const response = await api.get(
`${config.BACKEND_URL}/api/v1/schemes/${encodeURIComponent(schemeName)}`,
{ params: { network: NETWORK_NAME, scheme_type: "burst_location" } },
{ params: { scheme_type: "burst_location" } },
);
const schemeRecord = response.data as BurstSchemeRecord & {
result_payload?: BurstLocationResult;
@@ -322,16 +322,15 @@ const AnalysisParameters: React.FC<AnalysisParametersProps> = ({
: "";
const modify_total_duration = duration;
const params = {
network: network,
modify_pattern_start_time: modify_pattern_start_time,
burst_ID: burst_ID,
burst_id: burst_ID,
burst_size: burst_size,
modify_total_duration: modify_total_duration,
scheme_name: schemeName,
};
try {
await api.get(`${config.BACKEND_URL}/api/v1/burst-analysis`, {
await api.post(`${config.BACKEND_URL}/api/v1/burst-analyses`, undefined, {
params,
paramsSerializer: {
indexes: null, // 移除数组索引,即由 burst_ID[] 变为 burst_ID
@@ -29,9 +29,12 @@ import {
PrintOutlined,
} from "@mui/icons-material";
import { NETWORK_NAME } from "@config/config";
import { queryFeaturesByIds } from "@/utils/mapQueryService";
import {
clearPipeDiameterCache,
getCachedPipeDiameters,
getPipeDiameterDisplay,
getPipeDiameterQueryKey,
loadPipeDiameters,
type PipeDiameterMap,
} from "./schemePipeDiameters";
import { SchemeRecord, ValveIsolationResult } from "./types";
@@ -54,66 +57,7 @@ const REPORT_BLUE = "#0b4f87";
const REPORT_INK = "#172435";
const REPORT_MUTED = "#5f6f80";
const REPORT_LINE = "#d8e0e8";
const diameterCache = new Map<string, PipeDiameterMap>();
const diameterRequestCache = new Map<string, Promise<PipeDiameterMap>>();
export const clearAnalysisReportDiameterCache = () => {
diameterCache.clear();
diameterRequestCache.clear();
};
const loadPipeDiameters = async (
pipeIds: string[],
queryKey: string,
): Promise<PipeDiameterMap> => {
const cachedDiameters = diameterCache.get(queryKey);
if (cachedDiameters) {
return cachedDiameters;
}
const cachedRequest = diameterRequestCache.get(queryKey);
if (cachedRequest) {
return cachedRequest;
}
const request = (async () => {
let features = await queryFeaturesByIds(pipeIds, "geo_pipes_mat");
const foundIds = new Set(
features.map((feature) => String(feature.getProperties().id)),
);
const missingIds = pipeIds.filter((pipeId) => !foundIds.has(pipeId));
if (missingIds.length) {
features = [
...features,
...(await queryFeaturesByIds(missingIds, "geo_pipes")),
];
}
const diameters: PipeDiameterMap = Object.fromEntries(
pipeIds.map((pipeId) => [pipeId, null]),
);
features.forEach((feature) => {
const properties = feature.getProperties();
const pipeId = String(properties.id);
const diameter = Number(properties.diameter);
if (pipeIds.includes(pipeId)) {
diameters[pipeId] = Number.isFinite(diameter) ? diameter : null;
}
});
diameterCache.set(queryKey, diameters);
return diameters;
})();
diameterRequestCache.set(queryKey, request);
try {
return await request;
} finally {
if (diameterRequestCache.get(queryKey) === request) {
diameterRequestCache.delete(queryKey);
}
}
};
export const clearAnalysisReportDiameterCache = clearPipeDiameterCache;
const formatDateTime = (value: Date | string | null | undefined) => {
if (!value) return "未记录";
@@ -411,8 +355,8 @@ const AnalysisReport: React.FC<AnalysisReportProps> = ({
() => scheme?.schemeDetail?.burst_ID ?? [],
[scheme],
);
const diameterQueryKey = pipeIds.join("\u0000");
const cachedDiameters = diameterCache.get(diameterQueryKey);
const diameterQueryKey = getPipeDiameterQueryKey(pipeIds);
const cachedDiameters = getCachedPipeDiameters(pipeIds);
const [diameterState, setDiameterState] = useState<{
queryKey: string | null;
values: PipeDiameterMap;
@@ -435,13 +379,13 @@ const AnalysisReport: React.FC<AnalysisReportProps> = ({
if (!pipeIds.length) {
return;
}
if (diameterCache.has(diameterQueryKey)) {
if (getCachedPipeDiameters(pipeIds)) {
return;
}
let cancelled = false;
loadPipeDiameters(pipeIds, diameterQueryKey)
loadPipeDiameters(pipeIds)
.then((nextDiameters) => {
if (!cancelled) {
setDiameterState({
@@ -53,7 +53,9 @@ import { toLonLat } from "ol/proj";
import Timeline from "@components/olmap/core/Controls/Timeline";
import { SchemaItem, SchemeRecord } from "./types";
import {
getCachedPipeDiameters,
getPipeDiameterDisplay,
loadPipeDiameters,
type PipeDiameterMap,
} from "./schemePipeDiameters";
import { useSchemeCreatorName } from "@components/olmap/core/useSchemeCreatorName";
@@ -160,7 +162,6 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
setLoading(true);
try {
const params: Record<string, string> = {
network,
scheme_type: SCHEME_TYPE,
};
if (!queryAll && queryDate) {
@@ -251,40 +252,23 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
return;
}
const cachedDiameters = getCachedPipeDiameters(pipeIds);
if (cachedDiameters) {
setPipeDiametersByScheme((previous) => ({
...previous,
[expandedId]: cachedDiameters,
}));
return;
}
let cancelled = false;
setLoadingDiameterByScheme((previous) => ({
...previous,
[expandedId]: true,
}));
const loadPipeDiameters = async () => {
let features = await queryFeaturesByIds(pipeIds, "geo_pipes_mat");
const foundPipeIds = new Set(
features.map((feature) => String(feature.getProperties().id)),
);
const missingPipeIds = pipeIds.filter(
(pipeId) => !foundPipeIds.has(pipeId),
);
if (missingPipeIds.length > 0) {
const fallbackFeatures = await queryFeaturesByIds(
missingPipeIds,
"geo_pipes",
);
features = [...features, ...fallbackFeatures];
}
const nextDiameters: PipeDiameterMap = Object.fromEntries(
pipeIds.map((pipeId) => [pipeId, null]),
);
features.forEach((feature) => {
const properties = feature.getProperties();
const pipeId = String(properties.id);
const diameter = Number(properties.diameter);
nextDiameters[pipeId] = Number.isFinite(diameter) ? diameter : null;
});
const applyPipeDiameters = async () => {
const nextDiameters = await loadPipeDiameters(pipeIds);
if (cancelled) {
return;
}
@@ -299,7 +283,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
}));
};
loadPipeDiameters().catch((error) => {
applyPipeDiameters().catch((error) => {
console.error("查询管径失败:", error);
if (!cancelled) {
setLoadingDiameterByScheme((previous) => ({
@@ -361,14 +361,14 @@ const ValveIsolation: React.FC<ValveIsolationProps> = ({
}
try {
const params: any = {
network: NETWORK_NAME,
accident_element: ids,
};
if (disabled.length > 0) {
params.disabled_valves = disabled;
}
const response = await api.get<ValveIsolationResult>(
`${config.BACKEND_URL}/api/v1/valve-isolation-analysis`,
const response = await api.post<ValveIsolationResult>(
`${config.BACKEND_URL}/api/v1/valve-isolation-analyses`,
undefined,
{
params,
paramsSerializer: {
@@ -1,4 +1,48 @@
import { getPipeDiameterDisplay } from "./schemePipeDiameters";
import { queryFeaturesByIds } from "@/utils/mapQueryService";
import {
clearPipeDiameterCache,
getPipeDiameterDisplay,
loadPipeDiameters,
} from "./schemePipeDiameters";
jest.mock("@/utils/mapQueryService", () => ({
queryFeaturesByIds: jest.fn(),
}));
const feature = (id: string, diameter: number) => ({
getProperties: () => ({ id, diameter }),
});
afterEach(() => {
clearPipeDiameterCache();
jest.clearAllMocks();
});
describe("loadPipeDiameters", () => {
it("shares an in-flight WFS request and reuses its resolved result", async () => {
let resolveFeatures: ((features: ReturnType<typeof feature>[]) => void) | undefined;
const pendingFeatures = new Promise<ReturnType<typeof feature>[]>((resolve) => {
resolveFeatures = resolve;
});
(queryFeaturesByIds as jest.Mock).mockImplementation(
async () => pendingFeatures,
);
const firstRequest = loadPipeDiameters(["P-1"]);
const duplicateRequest = loadPipeDiameters(["P-1"]);
expect(queryFeaturesByIds).toHaveBeenCalledTimes(1);
resolveFeatures?.([feature("P-1", 315)]);
await expect(firstRequest).resolves.toEqual({ "P-1": 315 });
await expect(duplicateRequest).resolves.toEqual({ "P-1": 315 });
await expect(loadPipeDiameters(["P-1"])).resolves.toEqual({
"P-1": 315,
});
expect(queryFeaturesByIds).toHaveBeenCalledTimes(1);
});
});
describe("getPipeDiameterDisplay", () => {
it("shows the actual diameter for one burst pipe", () => {
@@ -1,5 +1,94 @@
import { queryFeaturesByIds } from "@/utils/mapQueryService";
import config from "@config/config";
export type PipeDiameterMap = Record<string, number | null | undefined>;
const diameterCache = new Map<string, PipeDiameterMap>();
const diameterRequestCache = new Map<string, Promise<PipeDiameterMap>>();
const normalizePipeIds = (pipeIds: string[]) =>
[...new Set(pipeIds.map((pipeId) => String(pipeId).trim()).filter(Boolean))];
export const getPipeDiameterQueryKey = (pipeIds: string[]) =>
[
config.MAP_URL,
config.MAP_WORKSPACE,
...normalizePipeIds(pipeIds).sort(),
].join("\u0000");
export const getCachedPipeDiameters = (pipeIds: string[]) =>
diameterCache.get(getPipeDiameterQueryKey(pipeIds));
export const clearPipeDiameterCache = () => {
diameterCache.clear();
diameterRequestCache.clear();
};
export const loadPipeDiameters = async (
pipeIds: string[],
): Promise<PipeDiameterMap> => {
const normalizedPipeIds = normalizePipeIds(pipeIds);
if (normalizedPipeIds.length === 0) {
return {};
}
const queryKey = getPipeDiameterQueryKey(normalizedPipeIds);
const cachedDiameters = diameterCache.get(queryKey);
if (cachedDiameters) {
return cachedDiameters;
}
const cachedRequest = diameterRequestCache.get(queryKey);
if (cachedRequest) {
return cachedRequest;
}
const request = (async () => {
let features = await queryFeaturesByIds(
normalizedPipeIds,
"geo_pipes_mat",
);
const foundPipeIds = new Set(
features.map((feature) => String(feature.getProperties().id)),
);
const missingPipeIds = normalizedPipeIds.filter(
(pipeId) => !foundPipeIds.has(pipeId),
);
if (missingPipeIds.length > 0) {
const fallbackFeatures = await queryFeaturesByIds(
missingPipeIds,
"geo_pipes",
);
features = [...features, ...fallbackFeatures];
}
const diameters: PipeDiameterMap = Object.fromEntries(
normalizedPipeIds.map((pipeId) => [pipeId, null]),
);
features.forEach((feature) => {
const properties = feature.getProperties();
const pipeId = String(properties.id);
const diameter = Number(properties.diameter);
if (pipeId in diameters) {
diameters[pipeId] = Number.isFinite(diameter) ? diameter : null;
}
});
diameterCache.set(queryKey, diameters);
return diameters;
})();
diameterRequestCache.set(queryKey, request);
try {
return await request;
} finally {
if (diameterRequestCache.get(queryKey) === request) {
diameterRequestCache.delete(queryKey);
}
}
};
export const getPipeDiameterDisplay = (
pipeIds: string[] | undefined,
diameters: PipeDiameterMap | undefined,
@@ -228,7 +228,6 @@ const AnalysisParameters: React.FC<AnalysisParametersProps> = ({
console.log("默认设置 pattern 为 CONSTANT");
}
const params = {
network,
start_time: start_time,
source: sourceNode,
concentration,
@@ -237,7 +236,7 @@ const AnalysisParameters: React.FC<AnalysisParametersProps> = ({
scheme_name: schemeName,
};
await api.get(`${config.BACKEND_URL}/api/v1/contaminant-simulation`, {
await api.post(`${config.BACKEND_URL}/api/v1/contaminant-simulations`, undefined, {
params,
});
@@ -223,7 +223,6 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
setLoading(true);
try {
const params: Record<string, string> = {
network,
scheme_type: SCHEME_TYPE,
};
if (!queryAll && queryDate) {
@@ -18,7 +18,7 @@ import dayjs, { Dayjs } from "dayjs";
import "dayjs/locale/zh-cn";
import { useNotification } from "@refinedev/core";
import { api } from "@/lib/api";
import { NETWORK_NAME, config } from "@config/config";
import { config } from "@config/config";
import { useControllableObjectState } from "@components/olmap/core/useControllableState";
import { LeakageResultDetail } from "./types";
import { FLOW_DISPLAY_UNIT, toM3s } from "@utils/units";
@@ -108,9 +108,8 @@ const AnalysisParameters: React.FC<Props> = ({
});
try {
const response = await api.post(
`${config.BACKEND_URL}/api/v1/leakage/identify/`,
`${config.BACKEND_URL}/api/v1/leakage-identifications`,
{
network: NETWORK_NAME,
scheme_name: schemeName.trim(),
dma_count: dmaCount,
scada_start: startTime.toISOString(),
@@ -85,7 +85,6 @@ const SchemeQuery: React.FC<Props> = ({
setLoading(true);
try {
const params: Record<string, string> = {
network: NETWORK_NAME,
scheme_type: "dma_leak_identification",
};
if (!queryAll && queryDate) {
@@ -129,7 +128,6 @@ const SchemeQuery: React.FC<Props> = ({
`${config.BACKEND_URL}/api/v1/schemes/${encodeURIComponent(schemeName)}`,
{
params: {
network: NETWORK_NAME,
scheme_type: "dma_leak_identification",
},
},
@@ -307,11 +307,10 @@ const AnalysisParameters: React.FC<AnalysisParametersProps> = ({
const params = {
scheme_name: schemeName,
network: NETWORK_NAME,
start_time: formattedTime,
valves: valves.map(v => v.id),
valves_k: valves.map(v => v.k),
drainage_node_ID: drainageNode,
drainage_node_id: drainageNode,
flush_flow: flushFlow,
duration: duration
};
@@ -320,7 +319,7 @@ const AnalysisParameters: React.FC<AnalysisParametersProps> = ({
// but axios usually handles array as valves[]=1&valves[]=2
// FastAPI default expects repeated query params.
const response = await api.get(`${config.BACKEND_URL}/api/v1/flushing-analysis`, {
const response = await api.post(`${config.BACKEND_URL}/api/v1/flushing-analyses`, undefined, {
params,
// Ensure arrays are sent as repeated keys: valves=1&valves=2
paramsSerializer: {
@@ -269,7 +269,6 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
setLoading(true);
try {
const params: Record<string, string> = {
network,
scheme_type: SCHEME_TYPE,
};
if (!queryAll && queryDate) {
@@ -392,7 +392,7 @@ const Timeline: React.FC<TimelineProps> = ({
});
try {
const response = await apiFetch(
`${config.BACKEND_URL}/api/v1/composite/pipeline-health-prediction?query_time=${query_time}&network_name=${NETWORK_NAME}`,
`${config.BACKEND_URL}/api/v1/pipeline-health-predictions?query_time=${query_time}`,
);
if (response.ok) {
@@ -135,7 +135,7 @@ const MonitoringPlaceOptimizationPanel: React.FC<
const handleOpenScheme = async (schemeId: number) => {
setLoadingScheme(true);
try {
const scheme = await getSensorPlacementScheme(NETWORK_NAME, schemeId);
const scheme = await getSensorPlacementScheme(schemeId);
setActiveScheme(scheme);
setLoadingScheme(false);
setPendingOpenSchemeId(scheme.id);
@@ -90,7 +90,6 @@ const OptimizationParameters: React.FC<OptimizationParametersProps> = ({
try {
const created = await optimizeSensorPlacement({
network,
scheme_name: schemeName,
sensor_type: "pressure",
method: method as "sensitivity" | "kmeans",
@@ -0,0 +1,176 @@
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
import SchemeEditor from "./SchemeEditor";
import type { SensorPlacementScheme } from "./types";
const mockOpen = jest.fn();
let mockSingleClickHandler: ((event: unknown) => void) | undefined;
const mockMap = {
addLayer: jest.fn(),
removeLayer: jest.fn(),
on: jest.fn((eventName: string, handler: (event: unknown) => void) => {
if (eventName === "singleclick") {
mockSingleClickHandler = handler;
}
}),
un: jest.fn(),
forEachFeatureAtPixel: jest.fn(() => undefined),
getView: jest.fn(() => ({
animate: jest.fn(),
getZoom: jest.fn(() => 16),
getResolution: jest.fn(() => 1),
})),
};
jest.mock("@refinedev/core", () => ({
useNotification: () => ({ open: mockOpen }),
}));
jest.mock("@components/olmap/core/MapComponent", () => ({
useMap: () => mockMap,
}));
jest.mock("@mui/x-data-grid", () => ({
DataGrid: () => <div data-testid="scheme-grid" />,
GridToolbar: () => null,
}));
jest.mock("@mui/x-data-grid/locales", () => ({
zhCN: {
components: {
MuiDataGrid: {
defaultProps: {
localeText: {},
},
},
},
},
}));
jest.mock("ol/Feature", () => ({
__esModule: true,
default: class {
private values: Record<string, unknown>;
constructor(values: Record<string, unknown>) {
this.values = values;
}
get(key: string) {
return this.values[key];
}
setId() {}
},
}));
jest.mock("ol/geom/Point", () => ({
__esModule: true,
default: class {
constructor(_coordinates: number[]) {}
},
}));
jest.mock("ol/source/Vector", () => ({
__esModule: true,
default: class {
clear() {}
addFeature() {}
},
}));
jest.mock("ol/layer/Vector", () => ({
__esModule: true,
default: class {
private source: { clear: () => void; addFeature: () => void };
constructor(options: {
source: { clear: () => void; addFeature: () => void };
}) {
this.source = options.source;
}
getSource() {
return this.source;
}
setVisible() {}
},
}));
jest.mock("ol/style", () => ({
Circle: class {},
Fill: class {},
Stroke: class {},
Style: class {},
Text: class {},
}));
jest.mock("ol/proj", () => ({
fromLonLat: (coordinates: number[]) => coordinates,
toLonLat: (coordinates: number[]) => coordinates,
}));
jest.mock("@/utils/mapQueryService", () => ({
handleMapClickSelectFeatures: jest.fn(),
}));
jest.mock("./SchemeDrawingDialog", () => ({
__esModule: true,
default: () => null,
}));
jest.mock("./schemeApi", () => ({
exportSensorPlacementExcel: jest.fn(),
overwriteSensorPlacementScheme: jest.fn(),
}));
const scheme: SensorPlacementScheme = {
id: 1,
scheme_name: "测试方案",
sensor_number: 1,
min_diameter: 300,
username: "operator",
create_time: "2026-07-30T08:00:00+08:00",
sensor_location: ["J1"],
sensor_points: [
{
node_id: "J1",
project_x: 10,
project_y: 20,
map_x: 13500010,
map_y: 3600020,
longitude: 121,
latitude: 31,
elevation: 5,
},
],
can_edit: true,
};
describe("SchemeEditor notifications", () => {
beforeEach(() => {
jest.clearAllMocks();
mockSingleClickHandler = undefined;
});
it("uses the same error notification contract as scheme query when replace starts outside an existing sensor", async () => {
render(<SchemeEditor scheme={scheme} network="fengyang" />);
fireEvent.click(screen.getByRole("button", { name: "替换" }));
await waitFor(() => expect(mockSingleClickHandler).toBeDefined());
act(() => {
mockSingleClickHandler?.({
pixel: [0, 0],
coordinate: [13500000, 3600000],
stopPropagation: jest.fn(),
});
});
expect(mockOpen).toHaveBeenCalledWith({
type: "error",
message: "请先点击要替换的监测点",
});
});
});
@@ -28,7 +28,8 @@ import {
EditLocationAlt as ReplaceIcon,
LocationOn as LocateIcon,
Map as MapIcon,
Redo as ResetIcon,
Redo as RedoIcon,
RestartAlt as ResetIcon,
Save as SaveIcon,
Undo as UndoIcon,
} from "@mui/icons-material";
@@ -55,6 +56,7 @@ import {
createSchemeEditorState,
deleteSensorPoint,
isSchemeDirty,
redoSchemeEdit,
replaceSensorPoint,
resetSchemeEdit,
summarizeChanges,
@@ -230,8 +232,8 @@ const SchemeEditor: React.FC<SchemeEditorProps> = ({
x: number;
y: number;
elevation: number;
}>(`${config.BACKEND_URL}/api/v1/getjunctionproperties/`, {
params: { network, junction: nodeId },
}>(`${config.BACKEND_URL}/api/v1/junctions/properties`, {
params: { junction: nodeId },
});
if (!response.data?.id) return null;
const [longitude, latitude] = toLonLat([mapX, mapY]);
@@ -249,7 +251,7 @@ const SchemeEditor: React.FC<SchemeEditorProps> = ({
return null;
}
},
[map, network],
[map],
);
useEffect(() => {
@@ -275,7 +277,7 @@ const SchemeEditor: React.FC<SchemeEditorProps> = ({
if (mode === "delete") {
if (!markerNodeId) {
open?.({ type: "progress", message: "请点击要删除的监测点" });
open?.({ type: "error", message: "请点击要删除的监测点" });
return;
}
setEditor((current) => deleteSensorPoint(current, markerNodeId));
@@ -285,7 +287,7 @@ const SchemeEditor: React.FC<SchemeEditorProps> = ({
if (mode === "replace" && !replaceSourceId) {
if (!markerNodeId) {
open?.({ type: "progress", message: "请先点击要替换的监测点" });
open?.({ type: "error", message: "请先点击要替换的监测点" });
return;
}
setReplaceSourceId(markerNodeId);
@@ -295,7 +297,7 @@ const SchemeEditor: React.FC<SchemeEditorProps> = ({
const candidate = await resolveJunction(event);
if (!candidate) {
open?.({ type: "progress", message: "请选择有效的管网节点" });
open?.({ type: "error", message: "请选择有效的管网节点" });
return;
}
@@ -304,7 +306,7 @@ const SchemeEditor: React.FC<SchemeEditorProps> = ({
(point) => point.node_id === candidate.node_id,
);
if (exists) {
open?.({ type: "progress", message: "该节点已在当前方案中" });
open?.({ type: "error", message: "该节点已在当前方案中" });
return;
}
setEditor((current) => addSensorPoint(current, candidate));
@@ -316,7 +318,7 @@ const SchemeEditor: React.FC<SchemeEditorProps> = ({
point.node_id !== replaceSourceId,
);
if (duplicate) {
open?.({ type: "progress", message: "目标节点已在当前方案中" });
open?.({ type: "error", message: "目标节点已在当前方案中" });
return;
}
setEditor((current) =>
@@ -379,7 +381,6 @@ const SchemeEditor: React.FC<SchemeEditorProps> = ({
setSaving(true);
try {
const updated = await overwriteSensorPlacementScheme(
network,
scheme.id,
editor.baseline.map((point) => point.node_id),
editor.points.map((point) => point.node_id),
@@ -410,7 +411,6 @@ const SchemeEditor: React.FC<SchemeEditorProps> = ({
setExporting(true);
try {
const blob = await exportSensorPlacementExcel(
network,
scheme.id,
editor.points.map((point) => point.node_id),
editor.statuses,
@@ -693,6 +693,19 @@ const SchemeEditor: React.FC<SchemeEditorProps> = ({
</IconButton>
</span>
</Tooltip>
<Tooltip title="重做下一步">
<span>
<IconButton
aria-label="重做下一步"
size="small"
disabled={!editor.future.length}
onClick={() => setEditor((current) => redoSchemeEdit(current))}
sx={{ width: 40, height: 40 }}
>
<RedoIcon fontSize="small" />
</IconButton>
</span>
</Tooltip>
<Tooltip title="重置为服务器版本">
<span>
<IconButton
@@ -177,7 +177,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
setLoading(true);
try {
const response = await api.get(
`${config.BACKEND_URL}/api/v1/sensor-placement-schemes?network=${network}`,
`${config.BACKEND_URL}/api/v1/sensor-placement-schemes`,
);
let filteredResults = response.data;
@@ -6,7 +6,6 @@ import type {
} from "./types";
export interface OptimizeSchemeInput {
network: string;
scheme_name: string;
sensor_type: "pressure";
method: "sensitivity" | "kmeans";
@@ -18,25 +17,22 @@ export const optimizeSensorPlacement = async (
input: OptimizeSchemeInput,
): Promise<SensorPlacementScheme> => {
const response = await api.post<SensorPlacementScheme>(
`${config.BACKEND_URL}/api/v1/sensor-placement-schemes/optimize`,
`${config.BACKEND_URL}/api/v1/sensor-placement-optimization-runs`,
input,
);
return response.data;
};
export const getSensorPlacementScheme = async (
network: string,
schemeId: number,
): Promise<SensorPlacementScheme> => {
const response = await api.get<SensorPlacementScheme>(
`${config.BACKEND_URL}/api/v1/sensor-placement-schemes/${schemeId}`,
{ params: { network } },
);
return response.data;
};
export const overwriteSensorPlacementScheme = async (
network: string,
schemeId: number,
expectedSensorLocation: string[],
sensorLocation: string[],
@@ -47,13 +43,11 @@ export const overwriteSensorPlacementScheme = async (
expected_sensor_location: expectedSensorLocation,
sensor_location: sensorLocation,
},
{ params: { network } },
);
return response.data;
};
export const exportSensorPlacementExcel = async (
network: string,
schemeId: number,
sensorLocation: string[],
adjustmentStatus: Record<string, AdjustmentStatus>,
@@ -65,7 +59,6 @@ export const exportSensorPlacementExcel = async (
adjustment_status: adjustmentStatus,
},
{
params: { network },
responseType: "blob",
},
);
@@ -3,6 +3,7 @@ import {
createSchemeEditorState,
deleteSensorPoint,
isSchemeDirty,
redoSchemeEdit,
replaceSensorPoint,
resetSchemeEdit,
summarizeChanges,
@@ -44,6 +45,24 @@ describe("scheme editor", () => {
expect(undoSchemeEdit(added).points).toEqual(initial.points);
});
it("can redo an undone edit and clears redo history after a new edit", () => {
const initial = createSchemeEditorState(scheme);
const added = addSensorPoint(initial, point("J3"));
const undone = undoSchemeEdit(added);
const redone = redoSchemeEdit(undone);
expect(redone.points.map((item) => item.node_id)).toEqual([
"J1",
"J2",
"J3",
]);
expect(redone.history).toHaveLength(1);
expect(redone.future).toEqual([]);
const editedAfterUndo = replaceSensorPoint(undone, "J1", point("J4"));
expect(editedAfterUndo.future).toEqual([]);
});
it("replaces a node without changing row order", () => {
const initial = createSchemeEditorState(scheme);
const replaced = replaceSensorPoint(initial, "J1", point("J3"));
@@ -80,5 +99,6 @@ describe("scheme editor", () => {
expect(reset.points.map((item) => item.node_id)).toEqual(["J1", "J2"]);
expect(isSchemeDirty(reset)).toBe(false);
expect(reset.history).toEqual([]);
expect(reset.future).toEqual([]);
});
});
@@ -13,6 +13,7 @@ export interface SchemeEditorSnapshot {
export interface SchemeEditorState extends SchemeEditorSnapshot {
baseline: SensorPoint[];
history: SchemeEditorSnapshot[];
future: SchemeEditorSnapshot[];
}
const clonePoints = (points: SensorPoint[]) => points.map((point) => ({ ...point }));
@@ -30,6 +31,7 @@ export const createSchemeEditorState = (
points: clonePoints(scheme.sensor_points),
statuses: currentStatuses(scheme.sensor_points),
history: [],
future: [],
});
const snapshot = (state: SchemeEditorState): SchemeEditorSnapshot => ({
@@ -46,6 +48,7 @@ const withHistory = (
points,
statuses,
history: [...state.history, snapshot(state)],
future: [],
});
export const addSensorPoint = (
@@ -114,6 +117,19 @@ export const undoSchemeEdit = (state: SchemeEditorState): SchemeEditorState => {
points: clonePoints(previous.points),
statuses: { ...previous.statuses },
history: state.history.slice(0, -1),
future: [...state.future, snapshot(state)],
};
};
export const redoSchemeEdit = (state: SchemeEditorState): SchemeEditorState => {
const next = state.future[state.future.length - 1];
if (!next) return state;
return {
...state,
points: clonePoints(next.points),
statuses: { ...next.statuses },
history: [...state.history, snapshot(state)],
future: state.future.slice(0, -1),
};
};
@@ -122,6 +138,7 @@ export const resetSchemeEdit = (state: SchemeEditorState): SchemeEditorState =>
points: clonePoints(state.baseline),
statuses: currentStatuses(state.baseline),
history: [],
future: [],
});
export const isSchemeDirty = (state: SchemeEditorState) => {
@@ -105,11 +105,11 @@ const fetchFromBackend = async (
const start_time = dayjs(range.from).toISOString();
const end_time = dayjs(range.to).toISOString();
// 清洗数据接口
const cleaningDataUrl = `${config.BACKEND_URL}/api/v1/scada/by-ids-field-time-range?device_ids=${device_ids}&field=cleaned_value&start_time=${start_time}&end_time=${end_time}`;
const cleaningDataUrl = `${config.BACKEND_URL}/api/v1/timeseries/scada-readings/fields?device_ids=${device_ids}&field=cleaned_value&start_time=${start_time}&end_time=${end_time}`;
// 原始数据
const rawDataUrl = `${config.BACKEND_URL}/api/v1/scada/by-ids-field-time-range?device_ids=${device_ids}&field=monitored_value&start_time=${start_time}&end_time=${end_time}`;
const rawDataUrl = `${config.BACKEND_URL}/api/v1/timeseries/scada-readings/fields?device_ids=${device_ids}&field=monitored_value&start_time=${start_time}&end_time=${end_time}`;
// 模拟数据接口
const simulationDataUrl = `${config.BACKEND_URL}/api/v1/composite/scada-simulation?device_ids=${device_ids}&start_time=${start_time}&end_time=${end_time}`;
const simulationDataUrl = `${config.BACKEND_URL}/api/v1/timeseries/views/scada-simulations?device_ids=${device_ids}&start_time=${start_time}&end_time=${end_time}`;
try {
// 优先查询清洗数据和模拟数据
const [cleaningRes, simulationRes] = await Promise.all([
@@ -351,11 +351,11 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
const end_time = dayjs(range.to).toISOString();
// 清洗数据接口
const cleaningDataUrl = `${config.BACKEND_URL}/api/v1/scada/by-ids-field-time-range?device_ids=${device_ids}&field=cleaned_value&start_time=${start_time}&end_time=${end_time}`;
const cleaningDataUrl = `${config.BACKEND_URL}/api/v1/timeseries/scada-readings/fields?device_ids=${device_ids}&field=cleaned_value&start_time=${start_time}&end_time=${end_time}`;
// 原始数据
const rawDataUrl = `${config.BACKEND_URL}/api/v1/scada/by-ids-field-time-range?device_ids=${device_ids}&field=monitored_value&start_time=${start_time}&end_time=${end_time}`;
const rawDataUrl = `${config.BACKEND_URL}/api/v1/timeseries/scada-readings/fields?device_ids=${device_ids}&field=monitored_value&start_time=${start_time}&end_time=${end_time}`;
// 模拟数据接口
const simulationDataUrl = `${config.BACKEND_URL}/api/v1/composite/scada-simulation?device_ids=${device_ids}&start_time=${start_time}&end_time=${end_time}`;
const simulationDataUrl = `${config.BACKEND_URL}/api/v1/timeseries/views/scada-simulations?device_ids=${device_ids}&start_time=${start_time}&end_time=${end_time}`;
try {
const [cleanRes, rawRes, simRes] = await Promise.all([
apiFetch(cleaningDataUrl)
@@ -530,7 +530,7 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
const response = await api.post(
`${
config.BACKEND_URL
}/api/v1/composite/clean-scada?device_ids=${deviceIds.join(
}/api/v1/timeseries/scada-cleaning-runs?device_ids=${deviceIds.join(
",",
)}&start_time=${startTime}&end_time=${endTime}`,
);
@@ -624,7 +624,7 @@ const SCADADeviceList: React.FC<SCADADeviceListProps> = ({
// 调用后端清洗接口
const response = await api.post(
`${config.BACKEND_URL}/api/v1/composite/clean-scada?device_ids=all&start_time=${startTime}&end_time=${endTime}`,
`${config.BACKEND_URL}/api/v1/timeseries/scada-cleaning-runs?device_ids=all&start_time=${startTime}&end_time=${endTime}`,
);
// 处理成功响应
@@ -108,13 +108,13 @@ const fetchFromBackend = async (
.join(",");
// 监测值数据接口(use_cleaned=false
const rawDataUrl = `${config.BACKEND_URL}/api/v1/composite/element-scada?element_id=${feature_ids}&start_time=${start_time}&end_time=${end_time}&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/composite/element-scada?element_id=${feature_ids}&start_time=${start_time}&end_time=${end_time}&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/composite/element-simulation?feature_infos=${feature_infos}&start_time=${start_time}&end_time=${end_time}`;
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/composite/element-simulation?feature_infos=${feature_infos}&start_time=${start_time}&end_time=${end_time}&scheme_type=${scheme_type}&scheme_name=${scheme_name}`;
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}`;
try {
if (type === "none") {
@@ -241,11 +241,11 @@ const Timeline: React.FC<TimelineProps> = ({
nodePromise =
sourceType === "scheme" && schemeName
? apiFetch(
`${config.BACKEND_URL}/api/v1/scheme/query/by-scheme-time-property?scheme_type=${schemeType}&scheme_name=${schemeName}&query_time=${query_time}&type=node&property=${junctionProperties}`,
`${config.BACKEND_URL}/api/v1/timeseries/schemes/records?scheme_type=${schemeType}&scheme_name=${schemeName}&query_time=${query_time}&type=node&property=${junctionProperties}`,
{ signal },
)
: apiFetch(
`${config.BACKEND_URL}/api/v1/realtime/query/by-time-property?query_time=${query_time}&type=node&property=${junctionProperties}`,
`${config.BACKEND_URL}/api/v1/timeseries/realtime/records?query_time=${query_time}&type=node&property=${junctionProperties}`,
{ signal },
);
requests.push(nodePromise);
@@ -270,11 +270,11 @@ const Timeline: React.FC<TimelineProps> = ({
linkPromise =
sourceType === "scheme" && schemeName
? apiFetch(
`${config.BACKEND_URL}/api/v1/scheme/query/by-scheme-time-property?scheme_type=${schemeType}&scheme_name=${schemeName}&query_time=${query_time}&type=link&property=${normalizedPipeProperties}`,
`${config.BACKEND_URL}/api/v1/timeseries/schemes/records?scheme_type=${schemeType}&scheme_name=${schemeName}&query_time=${query_time}&type=link&property=${normalizedPipeProperties}`,
{ signal },
)
: apiFetch(
`${config.BACKEND_URL}/api/v1/realtime/query/by-time-property?query_time=${query_time}&type=link&property=${normalizedPipeProperties}`,
`${config.BACKEND_URL}/api/v1/timeseries/realtime/records?query_time=${query_time}&type=link&property=${normalizedPipeProperties}`,
{ signal },
);
requests.push(linkPromise);
@@ -717,13 +717,12 @@ const Timeline: React.FC<TimelineProps> = ({
});
try {
const body = {
name: NETWORK_NAME,
start_time: dayjs(calculationDateTime).format("YYYY-MM-DDTHH:mm:ssZ"),
duration: calculatedInterval,
};
const response = await apiFetch(
`${config.BACKEND_URL}/api/v1/simulations/run-by-date`,
`${config.BACKEND_URL}/api/v1/simulation-runs`,
{
method: "POST",
headers: {
+9 -14
View File
@@ -431,11 +431,10 @@ const Toolbar: React.FC<ToolbarProps> = ({
try {
const params = new URLSearchParams({
network: networkName,
link: selectedValveId,
});
const response = await apiFetch(
`${config.BACKEND_URL}/api/v1/getstatus/?${params.toString()}`,
`${config.BACKEND_URL}/api/v1/status?${params.toString()}`,
);
if (!response.ok) {
@@ -484,11 +483,10 @@ const Toolbar: React.FC<ToolbarProps> = ({
try {
const params = new URLSearchParams({
network: networkName,
valve: selectedValveId,
});
const response = await apiFetch(
`${config.BACKEND_URL}/api/v1/getvalveproperties/?${params.toString()}`,
`${config.BACKEND_URL}/api/v1/valves/properties?${params.toString()}`,
);
if (!response.ok) {
@@ -534,13 +532,12 @@ const Toolbar: React.FC<ToolbarProps> = ({
setIsValveStatusSaving(true);
try {
const params = new URLSearchParams({
network: networkName,
link: selectedValveId,
});
const response = await apiFetch(
`${config.BACKEND_URL}/api/v1/setstatus/?${params.toString()}`,
`${config.BACKEND_URL}/api/v1/status-properties?${params.toString()}`,
{
method: "POST",
method: "PATCH",
headers: {
"Content-Type": "application/json",
},
@@ -567,7 +564,7 @@ const Toolbar: React.FC<ToolbarProps> = ({
setIsValveStatusSaving(false);
}
},
[networkName, open, selectedValveId],
[open, selectedValveId],
);
const handleValveSettingSave = useCallback(
@@ -600,13 +597,12 @@ const Toolbar: React.FC<ToolbarProps> = ({
setIsValveSettingSaving(true);
try {
const params = new URLSearchParams({
network: networkName,
valve: selectedValveId,
});
const response = await apiFetch(
`${config.BACKEND_URL}/api/v1/setvalveproperties/?${params.toString()}`,
`${config.BACKEND_URL}/api/v1/valves/properties?${params.toString()}`,
{
method: "POST",
method: "PATCH",
headers: {
"Content-Type": "application/json",
},
@@ -637,7 +633,6 @@ const Toolbar: React.FC<ToolbarProps> = ({
}
},
[
networkName,
open,
selectedValveId,
selectedValveType,
@@ -679,12 +674,12 @@ const Toolbar: React.FC<ToolbarProps> = ({
if (queryType === "scheme") {
response = await apiFetch(
// `${config.BACKEND_URL}/queryschemesimulationrecordsbyidtime/?scheme_name=${schemeName}&id=${id}&querytime=${querytime}&type=${type}`
`${config.BACKEND_URL}/api/v1/scheme/query/by-id-time?scheme_type=${schemeType}&scheme_name=${schemeName}&id=${id}&type=${type}&query_time=${querytime}`,
`${config.BACKEND_URL}/api/v1/timeseries/schemes/simulation-results?scheme_type=${schemeType}&scheme_name=${schemeName}&id=${id}&type=${type}&query_time=${querytime}`,
);
} else {
response = await apiFetch(
// `${config.BACKEND_URL}/querysimulationrecordsbyidtime/?id=${id}&querytime=${querytime}&type=${type}`
`${config.BACKEND_URL}/api/v1/realtime/query/by-id-time?id=${id}&type=${type}&query_time=${querytime}`,
`${config.BACKEND_URL}/api/v1/timeseries/realtime/simulation-results?id=${id}&type=${type}&query_time=${querytime}`,
);
}
if (!response.ok) {
@@ -32,7 +32,7 @@ describe("useTimelineTimeConfig", () => {
});
expect(result.current.durationMinutes).toBe(1440);
expect(String(apiFetch.mock.calls[0][0])).toContain(
"/api/v1/gettimeproperties/?network=test-network",
"/api/v1/network-options/time",
);
});
@@ -68,9 +68,7 @@ export const useTimelineTimeConfig = (): TimelineTimeConfig => {
const fetchTimeProperties = async () => {
try {
const response = await apiFetch(
`${config.BACKEND_URL}/api/v1/gettimeproperties/?network=${encodeURIComponent(
networkName,
)}`,
`${config.BACKEND_URL}/api/v1/network-options/time`,
);
if (!response.ok) {
throw new Error(`Failed to fetch time properties: ${response.status}`);
@@ -164,7 +164,7 @@ export const useToolbarChatActions = ({
? `?session_id=${encodeURIComponent(action.sessionId)}`
: "";
const response = await apiFetch(
`${config.AGENT_URL}/api/v1/agent/chat/render-ref/${encodeURIComponent(action.renderRef)}${query}`,
`${config.AGENT_URL}/api/v1/agent/render-references/${encodeURIComponent(action.renderRef)}${query}`,
{
method: "GET",
projectHeaderMode: "include",
+1 -1
View File
@@ -62,7 +62,7 @@ export const ProjectSelector: React.FC<ProjectSelectorProps> = ({
setLoadError(null);
try {
const response = await apiFetch(
`${config.BACKEND_URL}/api/v1/meta/projects`,
`${config.BACKEND_URL}/api/v1/projects`,
{ projectHeaderMode: "omit" },
);
if (!response.ok) {