|
| 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