Skip to content

Commit eddb22f

Browse files
authored
Feat add diagrams Grid Layout in dev mode (#2872)
Signed-off-by: sBouzols <[email protected]>
1 parent 8cd3e2e commit eddb22f

File tree

9 files changed

+434
-13
lines changed

9 files changed

+434
-13
lines changed

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
"react": "^18.3.1",
3737
"react-csv-downloader": "^3.3.0",
3838
"react-dom": "^18.3.1",
39-
"react-grid-layout": "^1.5.0",
39+
"react-grid-layout": "^1.5.1",
4040
"react-hook-form": "^7.54.2",
4141
"react-intl": "^7.1.6",
4242
"react-papaparse": "^4.4.0",
Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
/**
2+
* Copyright (c) 2025, RTE (http://www.rte-france.com)
3+
* This Source Code Form is subject to the terms of the Mozilla Public
4+
* License, v. 2.0. If a copy of the MPL was not distributed with this
5+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
6+
*/
7+
8+
import { useCallback, useState } from 'react';
9+
import { Layout, Layouts, Responsive, WidthProvider } from 'react-grid-layout';
10+
import { useDiagramModel } from './hooks/use-diagram-model';
11+
import { Diagram, DiagramParams, DiagramType } from './diagram.type';
12+
import { Box, darken, IconButton, Theme, useTheme } from '@mui/material';
13+
import { EquipmentInfos, EquipmentType, OverflowableText } from '@gridsuite/commons-ui';
14+
import CloseIcon from '@mui/icons-material/Close';
15+
import LibraryAddOutlinedIcon from '@mui/icons-material/LibraryAddOutlined';
16+
import { UUID } from 'crypto';
17+
import { TopBarEquipmentSearchDialog } from 'components/top-bar-equipment-seach-dialog/top-bar-equipment-search-dialog';
18+
import SingleLineDiagramContent from './singleLineDiagram/single-line-diagram-content';
19+
import NetworkAreaDiagramContent from './networkAreaDiagram/network-area-diagram-content';
20+
import { DiagramMetadata, SLDMetadata } from '@powsybl/network-viewer';
21+
import { DiagramAdditionalMetadata } from './diagram-common';
22+
import { useParameterState } from 'components/dialogs/parameters/use-parameters-state';
23+
import { PARAM_DEVELOPER_MODE } from 'utils/config-params';
24+
const ResponsiveGridLayout = WidthProvider(Responsive);
25+
26+
// Diagram types to manage here
27+
const diagramTypes = [
28+
DiagramType.VOLTAGE_LEVEL,
29+
DiagramType.SUBSTATION,
30+
DiagramType.NETWORK_AREA_DIAGRAM,
31+
DiagramType.NAD_FROM_CONFIG,
32+
];
33+
34+
const styles = {
35+
window: {
36+
display: 'flex',
37+
flexDirection: 'column',
38+
},
39+
diagramContainer: (theme: Theme) => ({
40+
flexGrow: 1,
41+
overflow: 'hidden',
42+
position: 'relative',
43+
backgroundColor:
44+
theme.palette.mode === 'light'
45+
? theme.palette.background.paper
46+
: theme.networkModificationPanel.backgroundColor,
47+
}),
48+
header: (theme: Theme) => ({
49+
padding: theme.spacing(0.5),
50+
display: 'flex',
51+
alignItems: 'center',
52+
backgroundColor: theme.palette.background.default,
53+
borderBottom: 'solid 1px',
54+
borderBottomColor: theme.palette.mode === 'light' ? theme.palette.action.selected : 'transparent',
55+
}),
56+
};
57+
58+
const DEFAULT_WIDTH = 1;
59+
const DEFAULT_HEIGHT = 2;
60+
61+
const initialLayouts = {
62+
// ResponsiveGridLayout will attempt to interpolate the rest of breakpoints based on this one
63+
lg: [
64+
{
65+
i: 'Adder',
66+
x: 0,
67+
y: 0,
68+
w: 1,
69+
h: 1,
70+
},
71+
],
72+
};
73+
74+
interface DiagramGridLayoutProps {
75+
studyUuid: UUID;
76+
showInSpreadsheet: (equipment: { equipmentId: string | null; equipmentType: EquipmentType | null }) => void;
77+
visible: boolean;
78+
}
79+
80+
function DiagramGridLayout({ studyUuid, showInSpreadsheet, visible }: Readonly<DiagramGridLayoutProps>) {
81+
const theme = useTheme();
82+
const [enableDeveloperMode] = useParameterState(PARAM_DEVELOPER_MODE);
83+
const [layouts, setLayouts] = useState<Layouts>(initialLayouts);
84+
const [isDialogSearchOpen, setIsDialogSearchOpen] = useState(false);
85+
86+
const onAddDiagram = (diagram: Diagram) => {
87+
setLayouts((old_layouts) => {
88+
const new_lg_layouts = old_layouts.lg.filter((layout) => layout.i !== 'Adder');
89+
const layoutItem: Layout = {
90+
i: diagram.diagramUuid,
91+
x: Infinity,
92+
y: 0,
93+
w: DEFAULT_WIDTH,
94+
h: DEFAULT_HEIGHT,
95+
minH: DEFAULT_HEIGHT,
96+
minW: DEFAULT_WIDTH,
97+
};
98+
new_lg_layouts.push(layoutItem);
99+
return { lg: new_lg_layouts };
100+
});
101+
};
102+
const { diagrams, removeDiagram, createDiagram } = useDiagramModel({
103+
diagramTypes: enableDeveloperMode ? diagramTypes : [],
104+
onAddDiagram,
105+
});
106+
107+
const onRemoveItem = useCallback(
108+
(diagramUuid: UUID) => {
109+
setLayouts((old_layouts) => {
110+
const new_lg_layouts = old_layouts.lg.filter((layout: Layout) => layout.i !== diagramUuid);
111+
if (new_lg_layouts.length === 0) {
112+
return initialLayouts;
113+
}
114+
return { lg: new_lg_layouts };
115+
});
116+
removeDiagram(diagramUuid);
117+
},
118+
[removeDiagram]
119+
);
120+
121+
const showVoltageLevelDiagram = useCallback(
122+
(element: EquipmentInfos) => {
123+
if (element.type === EquipmentType.VOLTAGE_LEVEL) {
124+
const diagram: DiagramParams = {
125+
type: DiagramType.VOLTAGE_LEVEL,
126+
voltageLevelId: element.voltageLevelId ?? '',
127+
};
128+
createDiagram(diagram);
129+
} else if (element.type === EquipmentType.SUBSTATION) {
130+
const diagram: DiagramParams = {
131+
type: DiagramType.SUBSTATION,
132+
substationId: element.id,
133+
};
134+
createDiagram(diagram);
135+
}
136+
},
137+
[createDiagram]
138+
);
139+
const renderDiagramAdder = useCallback(() => {
140+
if (Object.values(diagrams).length > 0) {
141+
return;
142+
}
143+
return (
144+
<div key={'Adder'} style={{ display: 'flex', flexDirection: 'column' }}>
145+
<Box sx={styles.header}>
146+
<OverflowableText
147+
className="react-grid-dragHandle"
148+
sx={{ flexGrow: '1' }}
149+
text={'Add a new diagram'}
150+
/>
151+
</Box>
152+
<Box
153+
sx={{
154+
display: 'flex',
155+
flexDirection: 'column',
156+
flexGrow: 1,
157+
alignItems: 'center',
158+
justifyContent: 'center',
159+
}}
160+
>
161+
<IconButton
162+
onClick={(e) => {
163+
setIsDialogSearchOpen(true);
164+
}}
165+
>
166+
<LibraryAddOutlinedIcon />
167+
</IconButton>
168+
</Box>
169+
</div>
170+
);
171+
}, [diagrams]);
172+
173+
// This function is called by the diagram's contents, when they get their sizes from the backend.
174+
const setDiagramSize = useCallback((diagramId: UUID, diagramType: DiagramType, width: number, height: number) => {
175+
console.log('TODO setDiagramSize', diagramId, diagramType, width, height);
176+
// TODO adapt the layout w and h cnsidering those values
177+
}, []);
178+
179+
const renderDiagrams = useCallback(() => {
180+
if (Object.values(diagrams).length === 0) {
181+
return;
182+
}
183+
return Object.values(diagrams).map((diagram) => {
184+
if (!diagram) {
185+
return null;
186+
}
187+
return (
188+
<Box key={diagram.diagramUuid} sx={styles.window}>
189+
<Box sx={styles.header}>
190+
<OverflowableText
191+
className="react-grid-dragHandle"
192+
sx={{ flexGrow: '1' }}
193+
text={diagram.name}
194+
/>
195+
<IconButton
196+
size={'small'}
197+
onClick={(e) => {
198+
onRemoveItem(diagram.diagramUuid);
199+
e.stopPropagation();
200+
}}
201+
>
202+
<CloseIcon fontSize="small" />
203+
</IconButton>
204+
</Box>
205+
<Box sx={styles.diagramContainer}>
206+
{(diagram.type === DiagramType.VOLTAGE_LEVEL || diagram.type === DiagramType.SUBSTATION) && (
207+
<SingleLineDiagramContent
208+
showInSpreadsheet={showInSpreadsheet}
209+
studyUuid={studyUuid}
210+
diagramId={diagram.diagramUuid}
211+
svg={diagram.svg?.svg ?? undefined}
212+
svgType={diagram.type}
213+
svgMetadata={(diagram.svg?.metadata as SLDMetadata) ?? undefined}
214+
loadingState={false} // TODO
215+
diagramSizeSetter={setDiagramSize}
216+
visible={visible}
217+
/>
218+
)}
219+
{(diagram.type === DiagramType.NETWORK_AREA_DIAGRAM ||
220+
diagram.type === DiagramType.NAD_FROM_CONFIG) && (
221+
<NetworkAreaDiagramContent
222+
diagramId={diagram.diagramUuid}
223+
svg={diagram.svg?.svg ?? undefined}
224+
svgType={diagram.type}
225+
svgMetadata={(diagram.svg?.metadata as DiagramMetadata) ?? undefined}
226+
svgScalingFactor={
227+
(diagram.svg?.additionalMetadata as DiagramAdditionalMetadata)?.scalingFactor ??
228+
undefined
229+
}
230+
svgVoltageLevels={
231+
(diagram.svg?.additionalMetadata as DiagramAdditionalMetadata)?.voltageLevels
232+
.map((vl) => vl.id)
233+
.filter((vlId) => vlId !== undefined) as string[]
234+
}
235+
loadingState={false} // TODO
236+
diagramSizeSetter={setDiagramSize}
237+
visible={visible}
238+
/>
239+
)}
240+
</Box>
241+
</Box>
242+
);
243+
});
244+
}, [diagrams, onRemoveItem, setDiagramSize, showInSpreadsheet, studyUuid, visible]);
245+
246+
return (
247+
<>
248+
<ResponsiveGridLayout
249+
className="layout"
250+
breakpoints={{ lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }}
251+
cols={{ lg: 4, md: 2, sm: 2, xs: 1, xxs: 1 }}
252+
compactType={'horizontal'}
253+
onLayoutChange={(currentLayout, allLayouts) => setLayouts(allLayouts)}
254+
layouts={layouts}
255+
style={{
256+
backgroundColor:
257+
theme.palette.mode === 'light'
258+
? darken(theme.palette.background.paper, 0.1)
259+
: theme.reactflow.backgroundColor,
260+
flexGrow: 1,
261+
overflow: 'auto',
262+
}}
263+
draggableHandle=".react-grid-dragHandle"
264+
onDragStart={(layout, oldItem, newItem, placeholder, e, element) => {
265+
if (e.target) {
266+
(e.target as HTMLElement).style.cursor = 'grabbing';
267+
}
268+
}}
269+
onDragStop={(layout, oldItem, newItem, placeholder, e, element) => {
270+
if (e.target) {
271+
(e.target as HTMLElement).style.cursor = 'default';
272+
}
273+
}}
274+
>
275+
{renderDiagramAdder()}
276+
{renderDiagrams()}
277+
</ResponsiveGridLayout>
278+
<TopBarEquipmentSearchDialog
279+
showVoltageLevelDiagram={showVoltageLevelDiagram}
280+
isDialogSearchOpen={isDialogSearchOpen}
281+
setIsDialogSearchOpen={setIsDialogSearchOpen}
282+
disableEventSearch
283+
/>
284+
</>
285+
);
286+
}
287+
288+
export default DiagramGridLayout;

0 commit comments

Comments
 (0)