feat(frontend): enforce RBAC and refine burst analysis
This commit is contained in:
@@ -11,7 +11,10 @@ import AnalysisReport, {
|
||||
matchesValveAnalysis,
|
||||
} from "./AnalysisReport";
|
||||
import { SchemeRecord, ValveIsolationResult } from "./types";
|
||||
import { isAllowedAccidentPipe } from "./valveIsolationScope";
|
||||
import {
|
||||
isAllowedAccidentPipe,
|
||||
normalizeValveIsolationResult,
|
||||
} from "./valveIsolationScope";
|
||||
|
||||
jest.mock("@/utils/mapQueryService", () => ({
|
||||
queryFeaturesByIds: jest.fn(),
|
||||
@@ -139,12 +142,69 @@ describe("AnalysisReport", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("shows only the affected count for a non-isolatable result", async () => {
|
||||
const legacyAffectedNodes = Array.from(
|
||||
{ length: 100 },
|
||||
(_, index) => `legacy-node-${index}`,
|
||||
);
|
||||
const nonIsolatableResult = {
|
||||
...valveResult,
|
||||
affected_nodes: legacyAffectedNodes,
|
||||
affected_node_count: 85747,
|
||||
must_close_valves: [],
|
||||
isolatable: false,
|
||||
};
|
||||
|
||||
render(
|
||||
<AnalysisReport
|
||||
scheme={scheme}
|
||||
valveResult={nonIsolatableResult}
|
||||
disabledValves={[]}
|
||||
generatedAt={new Date("2026-07-30T10:00:00+08:00")}
|
||||
/>,
|
||||
);
|
||||
|
||||
const preview = within(
|
||||
screen.getByTestId("burst-analysis-report-preview"),
|
||||
);
|
||||
expect(preview.getByText("85747 个")).toBeInTheDocument();
|
||||
expect(
|
||||
preview.getByText("不可隔离,未生成受影响节点清单。"),
|
||||
).toBeInTheDocument();
|
||||
expect(preview.queryByText("legacy-node-0")).not.toBeInTheDocument();
|
||||
expect(preview.queryByText("legacy-node-99")).not.toBeInTheDocument();
|
||||
await waitFor(() =>
|
||||
expect(preview.getByText("315 mm")).toBeInTheDocument(),
|
||||
);
|
||||
});
|
||||
|
||||
it("limits scheme-bound valve analysis to the scheme accident pipes", () => {
|
||||
expect(isAllowedAccidentPipe("P-1", ["P-1", "P-2"])).toBe(true);
|
||||
expect(isAllowedAccidentPipe("P-99", ["P-1", "P-2"])).toBe(false);
|
||||
expect(isAllowedAccidentPipe("P-99", undefined)).toBe(true);
|
||||
});
|
||||
|
||||
it("normalizes legacy valve results without changing isolatable lists", () => {
|
||||
expect(
|
||||
normalizeValveIsolationResult({
|
||||
...valveResult,
|
||||
affected_nodes: ["J-1", "J-2", "J-3"],
|
||||
must_close_valves: [],
|
||||
isolatable: false,
|
||||
}),
|
||||
).toMatchObject({
|
||||
affected_nodes: [],
|
||||
affected_node_count: 3,
|
||||
isolatable: false,
|
||||
});
|
||||
|
||||
expect(normalizeValveIsolationResult(valveResult)).toMatchObject({
|
||||
affected_nodes: ["J-1", "J-2"],
|
||||
affected_node_count: 2,
|
||||
isolatable: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("prints only after assigning the report print state", async () => {
|
||||
const originalTitle = document.title;
|
||||
const print = jest
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
type PipeDiameterMap,
|
||||
} from "./schemePipeDiameters";
|
||||
import { SchemeRecord, ValveIsolationResult } from "./types";
|
||||
import { getAffectedNodeCount } from "./valveIsolationScope";
|
||||
import PanelEmptyState from "@components/olmap/common/PanelEmptyState";
|
||||
|
||||
interface AnalysisReportProps {
|
||||
@@ -212,6 +213,18 @@ const ReportDocument: React.FC<ReportDocumentProps> = ({
|
||||
? valveResult
|
||||
: null;
|
||||
const duration = scheme.schemeDetail?.modify_total_duration;
|
||||
const valveDetailRows: Array<[string, string[] | undefined]> =
|
||||
matchedValveResult
|
||||
? [
|
||||
["已分析事故管段", matchedValveResult.accident_elements],
|
||||
["必关阀门", matchedValveResult.must_close_valves],
|
||||
["可选阀门", matchedValveResult.optional_valves],
|
||||
["不可用阀门", disabledValves],
|
||||
]
|
||||
: [];
|
||||
if (matchedValveResult?.isolatable) {
|
||||
valveDetailRows.push(["受影响节点", matchedValveResult.affected_nodes]);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
@@ -348,7 +361,7 @@ const ReportDocument: React.FC<ReportDocumentProps> = ({
|
||||
{[
|
||||
["隔离结论", matchedValveResult.isolatable ? "可隔离" : "不可隔离"],
|
||||
["必关阀门", `${matchedValveResult.must_close_valves?.length ?? 0} 个`],
|
||||
["受影响节点", `${matchedValveResult.affected_nodes?.length ?? 0} 个`],
|
||||
["受影响节点", `${getAffectedNodeCount(matchedValveResult)} 个`],
|
||||
].map(([label, value]) => (
|
||||
<Paper
|
||||
key={label}
|
||||
@@ -364,20 +377,19 @@ const ReportDocument: React.FC<ReportDocumentProps> = ({
|
||||
</Paper>
|
||||
))}
|
||||
</Box>
|
||||
{[
|
||||
["已分析事故管段", matchedValveResult.accident_elements],
|
||||
["必关阀门", matchedValveResult.must_close_valves],
|
||||
["可选阀门", matchedValveResult.optional_valves],
|
||||
["不可用阀门", disabledValves],
|
||||
["受影响节点", matchedValveResult.affected_nodes],
|
||||
].map(([label, values]) => (
|
||||
<Box key={label as string} sx={{ breakInside: "avoid" }}>
|
||||
{valveDetailRows.map(([label, values]) => (
|
||||
<Box key={label} sx={{ breakInside: "avoid" }}>
|
||||
<Typography sx={{ mb: 0.75, fontSize: 13, fontWeight: 700 }}>
|
||||
{label as string}
|
||||
{label}
|
||||
</Typography>
|
||||
<IdList values={values as string[]} />
|
||||
<IdList values={values} />
|
||||
</Box>
|
||||
))}
|
||||
{!matchedValveResult.isolatable && (
|
||||
<Alert severity="info" variant="outlined">
|
||||
不可隔离,未生成受影响节点清单。
|
||||
</Alert>
|
||||
)}
|
||||
</Stack>
|
||||
) : (
|
||||
<Alert severity="info" variant="outlined">
|
||||
|
||||
@@ -52,7 +52,11 @@ import {
|
||||
import { Point } from "ol/geom";
|
||||
import { toLonLat } from "ol/proj";
|
||||
import { useControllableObjectState } from "@components/olmap/core/useControllableState";
|
||||
import { isAllowedAccidentPipe } from "./valveIsolationScope";
|
||||
import {
|
||||
getAffectedNodeCount,
|
||||
isAllowedAccidentPipe,
|
||||
normalizeValveIsolationResult,
|
||||
} from "./valveIsolationScope";
|
||||
|
||||
interface ValveIsolationProps {
|
||||
initialPipeIds?: string[];
|
||||
@@ -363,7 +367,7 @@ const ValveIsolation: React.FC<ValveIsolationProps> = ({
|
||||
if (disabled.length > 0) {
|
||||
params.disabled_valves = disabled;
|
||||
}
|
||||
const response = await api.get(
|
||||
const response = await api.get<ValveIsolationResult>(
|
||||
`${config.BACKEND_URL}/api/v1/valve-isolation-analysis`,
|
||||
{
|
||||
params,
|
||||
@@ -372,7 +376,7 @@ const ValveIsolation: React.FC<ValveIsolationProps> = ({
|
||||
},
|
||||
},
|
||||
);
|
||||
setResult(response.data);
|
||||
setResult(normalizeValveIsolationResult(response.data));
|
||||
if (!isExpandSearch) {
|
||||
setActiveStep(1);
|
||||
} else {
|
||||
@@ -711,7 +715,7 @@ const ValveIsolation: React.FC<ValveIsolationProps> = ({
|
||||
{[
|
||||
{ label: "必关阀门", value: result.must_close_valves?.length || 0, color: "red", bgInfo: "from-red-50 to-red-100", textInfo: "text-red-700" },
|
||||
{ label: "可选阀门", value: result.optional_valves?.length || 0, color: "orange", bgInfo: "from-orange-50 to-orange-100", textInfo: "text-orange-700" },
|
||||
{ label: "影响节点", value: result.affected_nodes?.length || 0, color: "blue", bgInfo: "from-blue-50 to-blue-100", textInfo: "text-blue-700" },
|
||||
{ label: "影响节点", value: getAffectedNodeCount(result), color: "blue", bgInfo: "from-blue-50 to-blue-100", textInfo: "text-blue-700" },
|
||||
].map((item, index) => (
|
||||
<Box
|
||||
key={index}
|
||||
@@ -877,8 +881,14 @@ const ValveIsolation: React.FC<ValveIsolationProps> = ({
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{!result.isolatable && (
|
||||
<Alert severity="info" variant="outlined">
|
||||
不可隔离,未生成受影响节点清单。
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* 受影响节点 */}
|
||||
{result.affected_nodes && result.affected_nodes.length > 0 && (
|
||||
{result.isolatable && result.affected_nodes && result.affected_nodes.length > 0 && (
|
||||
<Box>
|
||||
<Box className="flex items-center justify-between mb-2">
|
||||
<Typography variant="caption" className="text-gray-800 font-bold border-l-4 border-blue-500 pl-2">
|
||||
|
||||
@@ -31,6 +31,7 @@ export interface SchemaItem {
|
||||
export interface ValveIsolationResult {
|
||||
accident_elements: string[];
|
||||
affected_nodes: string[];
|
||||
affected_node_count?: number;
|
||||
must_close_valves: string[];
|
||||
optional_valves: string[];
|
||||
isolatable: boolean;
|
||||
|
||||
@@ -1,4 +1,17 @@
|
||||
import type { ValveIsolationResult } from "./types";
|
||||
|
||||
export const isAllowedAccidentPipe = (
|
||||
pipeId: string,
|
||||
allowedPipeIds: string[] | undefined,
|
||||
) => allowedPipeIds === undefined || allowedPipeIds.includes(pipeId);
|
||||
|
||||
export const getAffectedNodeCount = (result: ValveIsolationResult) =>
|
||||
result.affected_node_count ?? result.affected_nodes?.length ?? 0;
|
||||
|
||||
export const normalizeValveIsolationResult = (
|
||||
result: ValveIsolationResult,
|
||||
): ValveIsolationResult => ({
|
||||
...result,
|
||||
affected_nodes: result.isolatable ? result.affected_nodes ?? [] : [],
|
||||
affected_node_count: getAffectedNodeCount(result),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user