feat(sensor): 完善监测点结果编辑与图层切换

This commit is contained in:
2026-08-03 19:00:16 +08:00
parent 5592c27386
commit 5d9c40e454
12 changed files with 463 additions and 127 deletions
+1 -1
View File
@@ -7,7 +7,7 @@
},
"server": {
"file": "server-v1.openapi.json",
"sha256": "9cd5b962e9556ec227c52d0dc7d4ef4af562dcea86e16877c923c37de0f4f704"
"sha256": "b003a57c9b8c1a041b644363ef58afb8bfcede1fe076ce48a190d2a1ac915e3f"
}
}
}
+121
View File
@@ -2598,6 +2598,18 @@
"title": "Map Y",
"type": "number"
},
"max_pipe_diameter": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
],
"description": "节点关联管道的最大管径,单位:毫米",
"title": "Max Pipe Diameter"
},
"node_id": {
"title": "Node Id",
"type": "string"
@@ -2613,6 +2625,7 @@
},
"required": [
"node_id",
"max_pipe_diameter",
"project_x",
"project_y",
"map_x",
@@ -35546,6 +35559,114 @@
]
}
},
"/api/v1/sensor-placement-candidates/{node_id}": {
"get": {
"operationId": "get_sensor_placement_candidates_node_id",
"parameters": [
{
"in": "path",
"name": "node_id",
"required": true,
"schema": {
"maxLength": 32,
"minLength": 1,
"title": "Node Id",
"type": "string"
}
},
{
"in": "header",
"name": "X-Project-Id",
"required": true,
"schema": {
"title": "X-Project-Id",
"type": "string"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SensorPointResponse"
}
}
},
"description": "Successful Response"
},
"401": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
}
},
"description": "Authentication required"
},
"403": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
}
},
"description": "Insufficient permission"
},
"404": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
}
},
"description": "Resource not found"
},
"409": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
}
},
"description": "Resource conflict"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
}
},
"description": "Validation error"
},
"503": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
}
},
"description": "Dependency unavailable"
}
},
"security": [
{
"OAuth2PasswordBearer": []
}
],
"summary": "获取监测点候选节点详情",
"tags": [
"Sensor Placement"
]
}
},
"/api/v1/sensor-placement-optimization-runs": {
"post": {
"operationId": "post_sensor_placement_optimization_runs",
@@ -1,5 +1,6 @@
import { act, fireEvent, render, screen } from "@testing-library/react";
import MonitoringPlaceOptimizationPanel, {
getMonitoringLayerVisibility,
getTabIndicatorSx,
getTabIndicatorTransform,
} from "./MonitoringPlaceOptimizationPanel";
@@ -7,6 +8,7 @@ import { getSensorPlacementScheme } from "./schemeApi";
import type { SensorPlacementScheme } from "./types";
const mockSchemeEditorRender = jest.fn();
const mockSchemeQueryRender = jest.fn();
jest.mock("@refinedev/core", () => ({
useNotification: () => ({ open: jest.fn() }),
@@ -20,9 +22,16 @@ jest.mock("./OptimizationParameters", () => ({
jest.mock("./SchemeQuery", () => ({
__esModule: true,
default: ({ onEdit }: { onEdit: (schemeId: number) => void }) => (
<button onClick={() => onEdit(7)}></button>
),
default: ({
onEdit,
active,
}: {
onEdit: (schemeId: number) => void;
active?: boolean;
}) => {
mockSchemeQueryRender(active);
return <button onClick={() => onEdit(7)}></button>;
},
createMonitoringSchemeQueryState: () => ({}),
}));
@@ -66,6 +75,7 @@ const scheme: SensorPlacementScheme = {
describe("MonitoringPlaceOptimizationPanel", () => {
beforeEach(() => {
mockSchemeEditorRender.mockClear();
mockSchemeQueryRender.mockClear();
});
it("applies equal-width positioning to the rendered tab indicator", () => {
@@ -90,6 +100,21 @@ describe("MonitoringPlaceOptimizationPanel", () => {
expect(indicator).toHaveStyle({ transform: "translateX(200%)" });
});
it("keeps query and editor layers mutually exclusive by active tab", () => {
expect(getMonitoringLayerVisibility(0)).toEqual({
editor: false,
query: false,
});
expect(getMonitoringLayerVisibility(1)).toEqual({
editor: true,
query: false,
});
expect(getMonitoringLayerVisibility(2)).toEqual({
editor: false,
query: true,
});
});
it("prepares a queried scheme before switching to the result editor", async () => {
let resolveScheme: (value: SensorPlacementScheme) => void = () => {};
mockGetSensorPlacementScheme.mockReturnValue(
@@ -119,5 +144,23 @@ describe("MonitoringPlaceOptimizationPanel", () => {
false,
true,
]);
expect(mockSchemeQueryRender.mock.calls.at(-1)?.[0]).toBe(false);
});
it("keeps the active result layer visible when the panel is collapsed", async () => {
mockGetSensorPlacementScheme.mockResolvedValue(scheme);
render(<MonitoringPlaceOptimizationPanel />);
fireEvent.click(screen.getByRole("tab", { name: /方案查询/ }));
fireEvent.click(screen.getByRole("button", { name: "打开测试方案" }));
await screen.findByTestId("scheme-editor");
const collapseButton = screen
.getByTestId("ChevronRightIcon")
.closest("button");
expect(collapseButton).not.toBeNull();
fireEvent.click(collapseButton!);
expect(mockSchemeEditorRender.mock.calls.at(-1)?.[0]).toBe(true);
});
});
@@ -69,6 +69,11 @@ export const getTabIndicatorSx = (tabIndex: number) => ({
},
});
export const getMonitoringLayerVisibility = (tabIndex: number) => ({
editor: tabIndex === 1,
query: tabIndex === 2,
});
interface PreparedSchemeEditorProps
extends React.ComponentProps<typeof SchemeEditor> {
onReady?: () => void;
@@ -126,6 +131,7 @@ const MonitoringPlaceOptimizationPanel: React.FC<
};
const drawerWidth = currentTab === 1 ? 820 : 520;
const layerVisibility = getMonitoringLayerVisibility(currentTab);
const handleSchemeEditorReady = useCallback(() => {
setCurrentTab(1);
@@ -326,7 +332,7 @@ const MonitoringPlaceOptimizationPanel: React.FC<
<PreparedSchemeEditor
scheme={activeScheme}
network={NETWORK_NAME}
active={isOpen && currentTab === 1}
active={layerVisibility.editor}
onSaved={handleSchemeSaved}
onReady={
pendingOpenSchemeId === activeScheme.id
@@ -346,6 +352,7 @@ const MonitoringPlaceOptimizationPanel: React.FC<
<TabPanel value={currentTab} index={2}>
<SchemeQuery
schemes={schemes}
active={layerVisibility.query}
onSchemesChange={setSchemes}
state={queryState}
onStateChange={setQueryState}
@@ -1,9 +1,16 @@
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
import SchemeEditor from "./SchemeEditor";
import { getSensorPlacementCandidate } from "./schemeApi";
import type { SensorPlacementScheme } from "./types";
import { handleMapClickSelectFeatures } from "@/utils/mapQueryService";
const mockOpen = jest.fn();
let mockSingleClickHandler: ((event: unknown) => void) | undefined;
let mockGridColumns: Array<{
field: string;
headerName?: string;
valueFormatter?: (value: unknown) => string;
}> = [];
const mockMap = {
addLayer: jest.fn(),
@@ -32,19 +39,23 @@ jest.mock("@components/olmap/core/MapComponent", () => ({
jest.mock("@mui/x-data-grid", () => ({
DataGrid: (props: {
columns: typeof mockGridColumns;
density?: string;
initialState?: { density?: string };
rowHeight?: number;
columnHeaderHeight?: number;
}) => (
<div
data-testid="scheme-grid"
data-density={props.density ?? "uncontrolled"}
data-initial-density={props.initialState?.density ?? ""}
data-row-height={props.rowHeight ?? "automatic"}
data-column-header-height={props.columnHeaderHeight ?? "automatic"}
/>
),
}) => {
mockGridColumns = props.columns;
return (
<div
data-testid="scheme-grid"
data-density={props.density ?? "uncontrolled"}
data-initial-density={props.initialState?.density ?? ""}
data-row-height={props.rowHeight ?? "automatic"}
data-column-header-height={props.columnHeaderHeight ?? "automatic"}
/>
);
},
GridToolbar: () => null,
}));
@@ -119,11 +130,6 @@ jest.mock("ol/style", () => ({
Text: class {},
}));
jest.mock("ol/proj", () => ({
fromLonLat: (coordinates: number[]) => coordinates,
toLonLat: (coordinates: number[]) => coordinates,
}));
jest.mock("@/utils/mapQueryService", () => ({
handleMapClickSelectFeatures: jest.fn(),
}));
@@ -135,6 +141,7 @@ jest.mock("./SchemeDrawingDialog", () => ({
jest.mock("./schemeApi", () => ({
exportSensorPlacementExcel: jest.fn(),
getSensorPlacementCandidate: jest.fn(),
overwriteSensorPlacementScheme: jest.fn(),
}));
@@ -149,6 +156,7 @@ const scheme: SensorPlacementScheme = {
sensor_points: [
{
node_id: "J1",
max_pipe_diameter: 400,
project_x: 10,
project_y: 20,
map_x: 13500010,
@@ -161,10 +169,18 @@ const scheme: SensorPlacementScheme = {
can_edit: true,
};
const mockGetSensorPlacementCandidate = jest.mocked(
getSensorPlacementCandidate,
);
const mockHandleMapClickSelectFeatures = jest.mocked(
handleMapClickSelectFeatures,
);
describe("SchemeEditor notifications", () => {
beforeEach(() => {
jest.clearAllMocks();
mockSingleClickHandler = undefined;
mockGridColumns = [];
});
it("uses the same error notification contract as scheme query when replace starts outside an existing sensor", async () => {
@@ -187,6 +203,38 @@ describe("SchemeEditor notifications", () => {
});
});
it("loads authoritative point details when adding a map-selected node", async () => {
mockHandleMapClickSelectFeatures.mockResolvedValue({
get: (key: string) => (key === "id" ? "J2" : undefined),
getId: () => "J2",
} as never);
mockGetSensorPlacementCandidate.mockResolvedValue({
node_id: "J2",
max_pipe_diameter: 500,
project_x: 30,
project_y: 40,
map_x: 13500030,
map_y: 3600040,
longitude: 121.1,
latitude: 31.1,
elevation: 6,
});
render(<SchemeEditor scheme={scheme} network="fengyang" />);
fireEvent.click(screen.getByRole("button", { name: "添加" }));
await waitFor(() => expect(mockSingleClickHandler).toBeDefined());
await act(async () => {
mockSingleClickHandler?.({
pixel: [0, 0],
coordinate: [13500030, 3600040],
stopPropagation: jest.fn(),
});
await Promise.resolve();
});
expect(mockGetSensorPlacementCandidate).toHaveBeenCalledWith("J2");
});
it("lets the Data Grid density selector control row sizing", () => {
render(<SchemeEditor scheme={scheme} network="fengyang" />);
@@ -207,4 +255,22 @@ describe("SchemeEditor notifications", () => {
"automatic",
);
});
it("places actions and status first and displays the selected pipe diameter", () => {
render(<SchemeEditor scheme={scheme} network="fengyang" />);
expect(mockGridColumns.slice(0, 5).map((column) => column.field)).toEqual([
"actions",
"adjustment_status",
"sequence",
"node_id",
"max_pipe_diameter",
]);
const diameterColumn = mockGridColumns.find(
(column) => column.field === "max_pipe_diameter",
);
expect(diameterColumn?.headerName).toBe("最大管径 (mm)");
expect(diameterColumn?.valueFormatter?.(400)).toBe("400");
expect(diameterColumn?.valueFormatter?.(null)).toBe("无");
});
});
@@ -45,10 +45,7 @@ import Point from "ol/geom/Point";
import VectorLayer from "ol/layer/Vector";
import VectorSource from "ol/source/Vector";
import { Circle, Fill, Stroke, Style, Text } from "ol/style";
import { fromLonLat, toLonLat } from "ol/proj";
import { useNotification } from "@refinedev/core";
import { api } from "@/lib/api";
import { config } from "@/config/config";
import { useMap } from "@components/olmap/core/MapComponent";
import { handleMapClickSelectFeatures } from "@/utils/mapQueryService";
import {
@@ -65,6 +62,7 @@ import {
} from "./schemeEditor";
import {
exportSensorPlacementExcel,
getSensorPlacementCandidate,
overwriteSensorPlacementScheme,
} from "./schemeApi";
import SchemeDrawingDialog from "./SchemeDrawingDialog";
@@ -147,6 +145,8 @@ const SchemeEditor: React.FC<SchemeEditorProps> = ({
const [saveDialogOpen, setSaveDialogOpen] = useState(false);
const [drawingOpen, setDrawingOpen] = useState(false);
const markerLayerRef = useRef<VectorLayer<VectorSource> | null>(null);
const activeRef = useRef(active);
activeRef.current = active;
useEffect(() => {
setEditor(createSchemeEditorState(scheme));
@@ -171,6 +171,7 @@ const SchemeEditor: React.FC<SchemeEditorProps> = ({
},
zIndex: 120,
});
layer.setVisible(activeRef.current);
markerLayerRef.current = layer;
map.addLayer(layer);
return () => {
@@ -206,47 +207,8 @@ const SchemeEditor: React.FC<SchemeEditorProps> = ({
const feature = await handleMapClickSelectFeatures(event, map);
const nodeId = String(feature?.get("id") ?? feature?.getId() ?? "").trim();
if (!nodeId) return null;
const featureGeometry = feature?.getGeometry();
const featureCoordinate =
featureGeometry instanceof Point
? featureGeometry.getCoordinates()
: event.coordinate;
const projectedFeatureCoordinate =
Math.abs(featureCoordinate[0]) <= 180 &&
Math.abs(featureCoordinate[1]) <= 90
? fromLonLat(featureCoordinate)
: featureCoordinate;
const resolution = map.getView().getResolution() ?? 1;
const isAlignedWithMap =
Math.hypot(
projectedFeatureCoordinate[0] - event.coordinate[0],
projectedFeatureCoordinate[1] - event.coordinate[1],
) <=
resolution * 20;
const [mapX, mapY] = isAlignedWithMap
? projectedFeatureCoordinate
: event.coordinate;
try {
const response = await api.get<{
id: string;
x: number;
y: number;
elevation: number;
}>(`${config.BACKEND_URL}/api/v1/junctions/properties`, {
params: { junction: nodeId },
});
if (!response.data?.id) return null;
const [longitude, latitude] = toLonLat([mapX, mapY]);
return {
node_id: String(response.data.id),
project_x: Number(response.data.x),
project_y: Number(response.data.y),
map_x: mapX,
map_y: mapY,
longitude,
latitude,
elevation: Number(response.data.elevation),
};
return await getSensorPlacementCandidate(nodeId);
} catch {
return null;
}
@@ -436,6 +398,70 @@ const SchemeEditor: React.FC<SchemeEditorProps> = ({
const columns = useMemo<GridColDef<SensorPointRow>[]>(
() => [
{
field: "actions",
headerName: "操作",
width: scheme.can_edit ? 138 : 54,
sortable: false,
filterable: false,
renderCell: ({ row }) => (
<Stack direction="row" spacing={0.25} sx={{ alignItems: "center" }}>
<Tooltip title="地图定位">
<IconButton
aria-label={`定位节点 ${row.node_id}`}
size="small"
onClick={() => locateRow(row)}
sx={{ width: 40, height: 40 }}
>
<LocateIcon fontSize="small" />
</IconButton>
</Tooltip>
{scheme.can_edit && (
<>
<Tooltip title="替换节点">
<IconButton
aria-label={`替换节点 ${row.node_id}`}
size="small"
onClick={() => activateMode("replace", row.node_id)}
sx={{ width: 40, height: 40 }}
>
<ReplaceIcon fontSize="small" />
</IconButton>
</Tooltip>
<Tooltip title="删除节点">
<span>
<IconButton
aria-label={`删除节点 ${row.node_id}`}
size="small"
onClick={() => handleDelete(row.node_id)}
disabled={rows.length <= 1}
sx={{ width: 40, height: 40 }}
>
<DeleteIcon fontSize="small" />
</IconButton>
</span>
</Tooltip>
</>
)}
</Stack>
),
},
{
field: "adjustment_status",
headerName: "调整状态",
width: 108,
renderCell: ({ value }) => {
const status = value as AdjustmentStatus;
return (
<Chip
label={STATUS_LABELS[status]}
color={STATUS_COLORS[status]}
variant="outlined"
size="small"
/>
);
},
},
{
field: "sequence",
headerName: "序号",
@@ -445,6 +471,21 @@ const SchemeEditor: React.FC<SchemeEditorProps> = ({
sortable: false,
},
{ field: "node_id", headerName: "节点 ID", minWidth: 120, flex: 0.8 },
{
field: "max_pipe_diameter",
headerName: "最大管径 (mm)",
description: "节点关联管道中的最大管径",
minWidth: 132,
flex: 0.75,
align: "right",
headerAlign: "right",
valueFormatter: (value) => {
if (value == null || !Number.isFinite(Number(value))) return "无";
return Number(value).toLocaleString("zh-CN", {
maximumFractionDigits: 3,
});
},
},
{
field: "longitude",
headerName: "经度",
@@ -507,70 +548,6 @@ const SchemeEditor: React.FC<SchemeEditorProps> = ({
headerAlign: "right",
valueFormatter: (value) => Number(value).toFixed(3),
},
{
field: "adjustment_status",
headerName: "调整状态",
width: 108,
renderCell: ({ value }) => {
const status = value as AdjustmentStatus;
return (
<Chip
label={STATUS_LABELS[status]}
color={STATUS_COLORS[status]}
variant="outlined"
size="small"
/>
);
},
},
{
field: "actions",
headerName: "操作",
width: scheme.can_edit ? 138 : 54,
sortable: false,
filterable: false,
renderCell: ({ row }) => (
<Stack direction="row" spacing={0.25} sx={{ alignItems: "center" }}>
<Tooltip title="地图定位">
<IconButton
aria-label={`定位节点 ${row.node_id}`}
size="small"
onClick={() => locateRow(row)}
sx={{ width: 40, height: 40 }}
>
<LocateIcon fontSize="small" />
</IconButton>
</Tooltip>
{scheme.can_edit && (
<>
<Tooltip title="替换节点">
<IconButton
aria-label={`替换节点 ${row.node_id}`}
size="small"
onClick={() => activateMode("replace", row.node_id)}
sx={{ width: 40, height: 40 }}
>
<ReplaceIcon fontSize="small" />
</IconButton>
</Tooltip>
<Tooltip title="删除节点">
<span>
<IconButton
aria-label={`删除节点 ${row.node_id}`}
size="small"
onClick={() => handleDelete(row.node_id)}
disabled={rows.length <= 1}
sx={{ width: 40, height: 40 }}
>
<DeleteIcon fontSize="small" />
</IconButton>
</span>
</Tooltip>
</>
)}
</Stack>
),
},
],
[activateMode, handleDelete, locateRow, rows.length, scheme.can_edit],
);
@@ -1,6 +1,6 @@
"use client";
import React, { useEffect, useMemo, useState } from "react";
import React, { useEffect, useMemo, useRef, useState } from "react";
import {
Box,
Button,
@@ -53,6 +53,7 @@ interface SchemaItem {
interface SchemeQueryProps {
schemes?: SchemeRecord[];
active?: boolean;
onSchemesChange?: (schemes: SchemeRecord[]) => void;
onEdit?: (id: number) => void;
network?: string;
@@ -77,6 +78,7 @@ export const createMonitoringSchemeQueryState =
const SchemeQuery: React.FC<SchemeQueryProps> = ({
schemes: externalSchemes,
active = true,
onSchemesChange,
onEdit,
network = NETWORK_NAME,
@@ -98,6 +100,8 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
const [highlightLayer, setHighlightLayer] =
useState<VectorLayer<VectorSource> | null>(null);
const activeRef = useRef(active);
activeRef.current = active;
const [highlightFeatures, setHighlightFeatures] = useState<Feature[]>([]);
// 使用外部提供的 schemes 或内部状态
const schemes =
@@ -142,6 +146,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
queryable: false,
},
});
highlightLayer.setVisible(activeRef.current);
map.addLayer(highlightLayer);
setHighlightLayer(highlightLayer);
@@ -151,6 +156,10 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
};
}, [map]);
useEffect(() => {
highlightLayer?.setVisible(active);
}, [active, highlightLayer]);
// 高亮要素的函数
useEffect(() => {
if (!highlightLayer) {
@@ -107,6 +107,7 @@ const point = (
map_y: number,
): SensorPointRow => ({
node_id,
max_pipe_diameter: 300,
sequence,
map_x,
map_y,
@@ -3,6 +3,7 @@ import { config } from "@/config/config";
import type {
AdjustmentStatus,
SensorPlacementScheme,
SensorPoint,
} from "./types";
export interface OptimizeSchemeInput {
@@ -32,6 +33,15 @@ export const getSensorPlacementScheme = async (
return response.data;
};
export const getSensorPlacementCandidate = async (
nodeId: string,
): Promise<SensorPoint> => {
const response = await api.get<SensorPoint>(
`${config.BACKEND_URL}/api/v1/sensor-placement-candidates/${encodeURIComponent(nodeId)}`,
);
return response.data;
};
export const overwriteSensorPlacementScheme = async (
schemeId: number,
expectedSensorLocation: string[],
@@ -13,6 +13,7 @@ import type { SensorPlacementScheme, SensorPoint } from "./types";
const point = (node_id: string): SensorPoint => ({
node_id,
max_pipe_diameter: 300,
project_x: Number(node_id.slice(1)) * 10,
project_y: Number(node_id.slice(1)) * 20,
map_x: 13500000 + Number(node_id.slice(1)) * 10,
@@ -2,6 +2,7 @@ export type AdjustmentStatus = "current" | "original" | "added" | "replaced";
export interface SensorPoint {
node_id: string;
max_pipe_diameter: number | null;
project_x: number;
project_y: number;
map_x: number;
+100
View File
@@ -4833,6 +4833,23 @@ export interface paths {
patch?: never;
trace?: never;
};
"/api/v1/sensor-placement-candidates/{node_id}": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** 获取监测点候选节点详情 */
get: operations["get_sensor_placement_candidates_node_id"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/v1/sensor-placement-optimization-runs": {
parameters: {
query?: never;
@@ -8153,6 +8170,11 @@ export interface components {
map_x: number;
/** Map Y */
map_y: number;
/**
* Max Pipe Diameter
* @description
*/
max_pipe_diameter: number | null;
/** Node Id */
node_id: string;
/** Project X */
@@ -31651,6 +31673,84 @@ export interface operations {
};
};
};
get_sensor_placement_candidates_node_id: {
parameters: {
query?: never;
header: {
"X-Project-Id": string;
};
path: {
node_id: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["SensorPointResponse"];
};
};
/** @description Authentication required */
401: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ProblemDetails"];
};
};
/** @description Insufficient permission */
403: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ProblemDetails"];
};
};
/** @description Resource not found */
404: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ProblemDetails"];
};
};
/** @description Resource conflict */
409: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ProblemDetails"];
};
};
/** @description Validation error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ProblemDetails"];
};
};
/** @description Dependency unavailable */
503: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ProblemDetails"];
};
};
};
};
post_sensor_placement_optimization_runs: {
parameters: {
query?: never;