diff --git a/package-lock.json b/package-lock.json index 59f883f0..c9ba9dea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", - "@gridsuite/commons-ui": "0.131.0", + "@gridsuite/commons-ui": "0.132.0", "@hookform/resolvers": "^4.1.3", "@mui/icons-material": "^5.18.0", "@mui/lab": "5.0.0-alpha.175", @@ -3268,9 +3268,9 @@ } }, "node_modules/@gridsuite/commons-ui": { - "version": "0.131.0", - "resolved": "https://registry.npmjs.org/@gridsuite/commons-ui/-/commons-ui-0.131.0.tgz", - "integrity": "sha512-sf6me8Z8Nkhb6n47QymBX63r3nMTNI/d1XJ/RmH+xqkjH6cyOXkF6zGxYk12cJgnT96NTplEfIlmCKiSIVU1AA==", + "version": "0.132.0", + "resolved": "https://registry.npmjs.org/@gridsuite/commons-ui/-/commons-ui-0.132.0.tgz", + "integrity": "sha512-QPjt/YIUiruivInMoBm3cnGf/n0/oSRleaqUM3L7QWqWqtLI88ZXkHnvu0yKHProW7I+peUCTMtVqMm54rntXQ==", "license": "MPL-2.0", "dependencies": { "@ag-grid-community/locale": "^33.3.2", diff --git a/package.json b/package.json index f8d1e913..4d11eb66 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "dependencies": { "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", - "@gridsuite/commons-ui": "0.131.0", + "@gridsuite/commons-ui": "0.132.0", "@hookform/resolvers": "^4.1.3", "@mui/icons-material": "^5.18.0", "@mui/lab": "5.0.0-alpha.175", diff --git a/src/components/dialogs/contingency-list/contingency-list-utils.ts b/src/components/dialogs/contingency-list/contingency-list-utils.ts index 59b010b8..b74b7af4 100644 --- a/src/components/dialogs/contingency-list/contingency-list-utils.ts +++ b/src/components/dialogs/contingency-list/contingency-list-utils.ts @@ -16,7 +16,12 @@ import { import type { SetRequired } from 'type-fest'; import { prepareContingencyListForBackend } from '../contingency-list-helper'; import { ContingencyListType } from '../../../utils/elementType'; -import { FilterAttributes } from '../../../utils/contingency-list.type'; +import { + ContingencyFieldConstants, + FilterAttributes, + FilterBasedContingencyList, + FilterElement, +} from '../../../utils/contingency-list.type'; export interface Identifier { type: 'ID_BASED'; @@ -72,13 +77,25 @@ export const getCriteriaBasedFormDataFromFetchedElement = (response: any, name: ...getCriteriaBasedFormData(response), }); -export const getFilterBasedFormDataFromFetchedElement = (response: any, name: string, description: string) => ({ +export const getFilterBasedFormDataFromFetchedElement = ( + response: FilterBasedContingencyList, + name: string, + description: string +) => ({ [FieldConstants.NAME]: name, [FieldConstants.DESCRIPTION]: description, [FieldConstants.CONTINGENCY_LIST_TYPE]: ContingencyListType.FILTERS.id, [FieldConstants.FILTERS]: response.filters.map((filter: FilterAttributes) => { - return { id: filter.id, name: filter.name, specificMetadata: { equipmentType: filter.equipmentType } }; + return { + id: filter.id, + name: filter.name ?? '', + specificMetadata: { equipmentType: filter.equipmentType ?? '' }, + }; }), + [ContingencyFieldConstants.SUB_EQUIPMENT_TYPES_BY_FILTER]: response.selectedEquipmentTypesByFilter.map((value) => ({ + [ContingencyFieldConstants.FILTER_ID]: value.filterId, + [ContingencyFieldConstants.SUB_EQUIPMENT_TYPES]: value.equipmentTypes, + })), }); export const getExplicitNamingFormDataFromFetchedElement = (response: any, name: string, description: string) => { @@ -141,3 +158,10 @@ export const SUPPORTED_CONTINGENCY_LIST_EQUIPMENTS = Object.fromEntries( ([key]) => !excludedEquipmentTypes.includes(key as EquipmentType) ) ); + +export function isSubstationOrVoltageLevelFilter(filter: FilterElement) { + return ( + filter.specificMetadata.equipmentType === EquipmentType.SUBSTATION || + filter.specificMetadata.equipmentType === EquipmentType.VOLTAGE_LEVEL + ); +} diff --git a/src/components/dialogs/contingency-list/filter-based/contingency-list-filter-based-dialog.tsx b/src/components/dialogs/contingency-list/filter-based/contingency-list-filter-based-dialog.tsx index a4ecaac5..0151fd28 100644 --- a/src/components/dialogs/contingency-list/filter-based/contingency-list-filter-based-dialog.tsx +++ b/src/components/dialogs/contingency-list/filter-based/contingency-list-filter-based-dialog.tsx @@ -9,7 +9,6 @@ import { CustomMuiDialog, FieldConstants, MAX_CHAR_DESCRIPTION, - TreeViewFinderNodeProps, useSnackMessage, yupConfig as yup, } from '@gridsuite/commons-ui'; @@ -17,8 +16,14 @@ import { useForm } from 'react-hook-form'; import { yupResolver } from '@hookform/resolvers/yup'; import { useSelector } from 'react-redux'; import { useCallback, useEffect, useState } from 'react'; -import { UUID } from 'crypto'; +import { UUID } from 'node:crypto'; import { ObjectSchema } from 'yup'; +import { + ContingencyFieldConstants, + FilterBasedContingencyList, + FilterElement, + FilterSubEquipments, +} from '../../../../utils/contingency-list.type'; import ContingencyListFilterBasedForm from './contingency-list-filter-based-form'; import { AppState } from '../../../../redux/types'; import { @@ -29,24 +34,34 @@ import { import { handleNotAllowedError } from '../../../utils/rest-errors'; import { ContingencyListType } from '../../../../utils/elementType'; import { getFilterBasedFormDataFromFetchedElement } from '../contingency-list-utils'; -import { FilterBasedContingencyList } from '../../../../utils/contingency-list.type'; const schema: ObjectSchema = yup.object().shape({ [FieldConstants.NAME]: yup.string().required(), [FieldConstants.DESCRIPTION]: yup.string().max(MAX_CHAR_DESCRIPTION), [FieldConstants.FILTERS]: yup.array().required(), + [ContingencyFieldConstants.SUB_EQUIPMENT_TYPES_BY_FILTER]: yup + .array() + .required() + .of( + yup.object().shape({ + [ContingencyFieldConstants.FILTER_ID]: yup.string().required(), + [ContingencyFieldConstants.SUB_EQUIPMENT_TYPES]: yup.array().required().of(yup.string().required()), + }) + ), }); export interface ContingencyListFilterBasedFormData { [FieldConstants.NAME]: string; [FieldConstants.DESCRIPTION]?: string; - [FieldConstants.FILTERS]: TreeViewFinderNodeProps[]; + [FieldConstants.FILTERS]: FilterElement[]; + [ContingencyFieldConstants.SUB_EQUIPMENT_TYPES_BY_FILTER]: FilterSubEquipments[]; } const getContingencyListEmptyFormData = (name = '') => ({ [FieldConstants.NAME]: name, [FieldConstants.DESCRIPTION]: '', [FieldConstants.FILTERS]: [], + [ContingencyFieldConstants.SUB_EQUIPMENT_TYPES_BY_FILTER]: [], }); const emptyFormData = (name?: string) => getContingencyListEmptyFormData(name); @@ -111,11 +126,17 @@ export default function FilterBasedContingencyListDialog({ const onSubmit = useCallback( (data: ContingencyListFilterBasedFormData) => { const filterBaseContingencyList: FilterBasedContingencyList = { - filters: data[FieldConstants.FILTERS]?.map((item: TreeViewFinderNodeProps) => { + filters: data[FieldConstants.FILTERS]?.map((filter) => { return { - id: item.id, + id: filter.id, }; }), + selectedEquipmentTypesByFilter: data[ContingencyFieldConstants.SUB_EQUIPMENT_TYPES_BY_FILTER]?.map( + (filterSubEquipments) => ({ + filterId: filterSubEquipments[ContingencyFieldConstants.FILTER_ID], + equipmentTypes: filterSubEquipments[ContingencyFieldConstants.SUB_EQUIPMENT_TYPES], + }) + ), }; if (id) { @@ -170,9 +191,14 @@ export default function FilterBasedContingencyListDialog({ onSave={onSubmit} formSchema={schema} formMethods={methods} - unscrollableFullHeight disabledSave={Boolean(!!nameError || isValidating)} isDataFetching={isFetching} + sx={{ + '.MuiDialog-paper': { + minWidth: '60vw', + height: '95vh', + }, + }} > diff --git a/src/components/dialogs/contingency-list/filter-based/contingency-list-filter-based-form.tsx b/src/components/dialogs/contingency-list/filter-based/contingency-list-filter-based-form.tsx index c023596d..831bce67 100644 --- a/src/components/dialogs/contingency-list/filter-based/contingency-list-filter-based-form.tsx +++ b/src/components/dialogs/contingency-list/filter-based/contingency-list-filter-based-form.tsx @@ -5,45 +5,54 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import { - CustomAGGrid, + ArrayAction, + CONTINGENCY_LIST_EQUIPMENTS, DescriptionField, - DirectoryItemSelector, DirectoryItemsInput, ElementType, EquipmentType, FieldConstants, - getFilterEquipmentTypeLabel, - SeparatorCellRenderer, - TreeViewFinderNodeProps, + FormEquipment, UniqueNameInput, - unscrollableDialogStyles, - useSnackMessage, } from '@gridsuite/commons-ui'; -import { Box, Button, Grid, Typography } from '@mui/material'; +import { + Checkbox, + Divider, + Grid, + Paper, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, +} from '@mui/material'; import { useSelector } from 'react-redux'; -import { FormattedMessage, useIntl } from 'react-intl'; -import { useCallback, useEffect, useState } from 'react'; -import { FolderOutlined } from '@mui/icons-material'; -import { ColDef } from 'ag-grid-community'; -import { blue, brown, cyan, green, indigo, lightBlue, lightGreen, lime, red, teal } from '@mui/material/colors'; -import { useWatch } from 'react-hook-form'; -import { UUID } from 'crypto'; -import { AppState } from '../../../../redux/types'; -import { getIdentifiablesFromFitlers } from '../../../../utils/rest-api'; +import { FormattedMessage } from 'react-intl'; +import { useCallback, useState } from 'react'; import { - FilterAttributes, - FilteredIdentifiables, - IdentifiableAttributes, -} from '../../../../utils/contingency-list.type'; - -const separator = '/'; -const defaultDef: ColDef = { - flex: 1, - resizable: false, - sortable: false, -}; + blue, + brown, + cyan, + green, + indigo, + lightBlue, + lightGreen, + lime, + orange, + pink, + red, + teal, +} from '@mui/material/colors'; +import { useFormContext, useWatch } from 'react-hook-form'; +import { AppState } from '../../../../redux/types'; +import { ContingencyFieldConstants, FilterElement, FilterSubEquipments } from '../../../../utils/contingency-list.type'; +import { FilterBasedContingencyListVisualizationPanel } from './filter-based-contingency-list-visualization-panel'; +import { isSubstationOrVoltageLevelFilter } from '../contingency-list-utils'; const equipmentTypes: string[] = [ + EquipmentType.SUBSTATION, + EquipmentType.VOLTAGE_LEVEL, EquipmentType.LINE, EquipmentType.TWO_WINDINGS_TRANSFORMER, EquipmentType.THREE_WINDINGS_TRANSFORMER, @@ -57,6 +66,8 @@ const equipmentTypes: string[] = [ ]; const equipmentColorsMap: Map = new Map([ + [EquipmentType.SUBSTATION, pink[700]], + [EquipmentType.VOLTAGE_LEVEL, orange[700]], [EquipmentType.LINE, indigo[700]], [EquipmentType.TWO_WINDINGS_TRANSFORMER, blue[700]], [EquipmentType.THREE_WINDINGS_TRANSFORMER, lightBlue[700]], @@ -70,117 +81,112 @@ const equipmentColorsMap: Map = new Map([ ]); export default function ContingencyListFilterBasedForm() { + const { setValue, getValues } = useFormContext(); const activeDirectory = useSelector((state: AppState) => state.activeDirectory); - const [selectedStudy, setSelectedStudy] = useState(''); - const [selectedStudyId, setSelectedStudyId] = useState(); - const [selectedFolder, setSelectedFolder] = useState(''); - const [isOpen, setIsOpen] = useState(false); - const [isFetching, setIsFetching] = useState(false); - const [rowsData, setRowsData] = useState([]); + const [selectedFilterId, setSelectedFilterId] = useState(null); + const [isDataOutdated, setIsDataOutdated] = useState(false); - const { snackError } = useSnackMessage(); - const filters = useWatch({ name: FieldConstants.FILTERS }); + const filters: FilterElement[] = useWatch({ name: FieldConstants.FILTERS }) as unknown as FilterElement[]; + const substationAndVLFilters = filters.filter(isSubstationOrVoltageLevelFilter); + const filtersSubEquipments: FilterSubEquipments[] = useWatch({ + name: ContingencyFieldConstants.SUB_EQUIPMENT_TYPES_BY_FILTER, + }) as unknown as FilterSubEquipments[]; - const intl = useIntl(); + const handleFilterRowClick = useCallback((clickedFilterId: string, currentFilterId: string | null) => { + if (clickedFilterId !== currentFilterId) { + setSelectedFilterId(clickedFilterId); + } + }, []); - const getTranslatedEquipmentType = useCallback( - (type: string | undefined) => { - const equipmentType = getFilterEquipmentTypeLabel(type); - return equipmentType ? intl.formatMessage({ id: equipmentType }) : ''; + const filterEquipmentTypes = + selectedFilterId && + filtersSubEquipments?.find((value) => value[ContingencyFieldConstants.FILTER_ID] === selectedFilterId)?.[ + ContingencyFieldConstants.SUB_EQUIPMENT_TYPES + ]; + + const handleEquipmentRowClick = useCallback( + (equipmentType: string, isEquipmentSelected: boolean, selectedFilterIdP: string | null) => { + const currentSubEquipmentsByFilters: FilterSubEquipments[] = getValues( + ContingencyFieldConstants.SUB_EQUIPMENT_TYPES_BY_FILTER + ); + setValue( + ContingencyFieldConstants.SUB_EQUIPMENT_TYPES_BY_FILTER, + currentSubEquipmentsByFilters.map((value) => { + if (value[ContingencyFieldConstants.FILTER_ID] === selectedFilterIdP) { + // we either add or remove the clicked equipment from the list of subEquipments + // depending on if the equipment is already selected or not + const updatedSubEquipments = isEquipmentSelected + ? value[ContingencyFieldConstants.SUB_EQUIPMENT_TYPES].filter( + (subEquipment: string) => subEquipment !== equipmentType + ) + : [...value[ContingencyFieldConstants.SUB_EQUIPMENT_TYPES], equipmentType]; + + return { + ...value, + [ContingencyFieldConstants.SUB_EQUIPMENT_TYPES]: updatedSubEquipments, + }; + } + // for the other filters, we just return the value as is + return value; + }), + { shouldDirty: true } + ); + setIsDataOutdated(true); }, - [intl] + [setValue, getValues] ); - const colDef: ColDef[] = [ - { - headerName: intl.formatMessage({ - id: FieldConstants.EQUIPMENT_ID, - }), - field: FieldConstants.ID, - cellRenderer: ({ data }: { data: IdentifiableAttributes }) => { - if (data.id === 'SEPARATOR' && data.type === '') { - return SeparatorCellRenderer({ - value: intl.formatMessage({ id: 'missingFromStudy' }), - }); + const handleFilterOnChange = useCallback( + (_currentFilters: any, action?: ArrayAction, filter?: FilterElement) => { + if (!action || !filter) { + console.error('Action or filter is missing in handleFilterOnChange'); + return; + } + if (isSubstationOrVoltageLevelFilter(filter)) { + const currentSubEquipmentsByFilters = getValues( + ContingencyFieldConstants.SUB_EQUIPMENT_TYPES_BY_FILTER + ); + if (action === ArrayAction.ADD) { + const newFilterSubEquipments = { + [ContingencyFieldConstants.FILTER_ID]: filter.id, + [ContingencyFieldConstants.SUB_EQUIPMENT_TYPES]: [], + }; + setValue(ContingencyFieldConstants.SUB_EQUIPMENT_TYPES_BY_FILTER, [ + ...currentSubEquipmentsByFilters, + newFilterSubEquipments, + ]); + } else if (action === ArrayAction.REMOVE) { + setValue( + ContingencyFieldConstants.SUB_EQUIPMENT_TYPES_BY_FILTER, + currentSubEquipmentsByFilters.filter( + (value: FilterSubEquipments) => value[ContingencyFieldConstants.FILTER_ID] !== filter.id + ) + ); } - return data.id; - }, - }, - { - headerName: intl.formatMessage({ - id: FieldConstants.TYPE, - }), - field: FieldConstants.TYPE, - }, - ]; - - useEffect(() => { - if (filters?.length && selectedStudyId) { - setIsFetching(true); - getIdentifiablesFromFitlers( - selectedStudyId, - filters.map((filter: FilterAttributes) => filter.id) - ) - .then((response: FilteredIdentifiables) => { - const SEPARATOR_TYPE = 'SEPARATOR'; - const attributes: IdentifiableAttributes[] = [ - ...response.equipmentIds.map((element: IdentifiableAttributes) => ({ - id: element.id, - type: getTranslatedEquipmentType(element?.type), - })), - ...(response.notFoundIds?.length > 0 - ? [ - { id: SEPARATOR_TYPE, type: '' }, - ...response.notFoundIds.map((element: IdentifiableAttributes) => ({ - id: element.id, - type: getTranslatedEquipmentType(element?.type), - })), - ] - : []), - ]; - setRowsData(attributes); - }) - .catch((error) => - snackError({ - messageTxt: error.message, - headerId: 'cannotComputeContingencyList', - }) - ) - .finally(() => setIsFetching(false)); - } - }, [filters, getTranslatedEquipmentType, selectedStudyId, snackError]); - - const onNodeChanged = useCallback((nodes: TreeViewFinderNodeProps[]) => { - if (nodes.length > 0) { - if (nodes[0].parents && nodes[0].parents.length > 0) { - setSelectedFolder(nodes[0].parents.map((entry) => entry.name).join(separator)); } - setSelectedStudy(nodes[0].name); - setSelectedStudyId(nodes[0].id); - } - setIsOpen(false); - }, []); + setIsDataOutdated(true); + }, + [setValue, getValues] + ); return ( - <> - - - - - - - - + + + + + + + - - - + + + - - - - - {selectedStudy.length > 0 ? ( - - {selectedFolder ? selectedFolder + separator + selectedStudy : selectedStudy} - - ) : ( - - )} - - - - - - - - - + + {substationAndVLFilters.length > 0 && ( + <> + + + + + + + + + + + + + + + + {substationAndVLFilters.map((filterRow) => ( + handleFilterRowClick(filterRow.id, selectedFilterId)} + selected={filterRow.id === selectedFilterId} + > + + {filterRow.name} + + + ))} + +
+
+
+ + + + + + + + + + + + {filterEquipmentTypes && + Object.values(CONTINGENCY_LIST_EQUIPMENTS).map( + (equipmentRow: FormEquipment) => { + const isEquipmentSelected = filterEquipmentTypes.includes( + equipmentRow.id + ); + return ( + + handleEquipmentRowClick( + equipmentRow.id, + isEquipmentSelected, + selectedFilterId + ) + } + selected={isEquipmentSelected} + > + + + + + + ); + } + )} + +
+
+
+
+ + )} + + + + + + ); } diff --git a/src/components/dialogs/contingency-list/filter-based/data-table-overlay.tsx b/src/components/dialogs/contingency-list/filter-based/data-table-overlay.tsx new file mode 100644 index 00000000..fcb13b28 --- /dev/null +++ b/src/components/dialogs/contingency-list/filter-based/data-table-overlay.tsx @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2025, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import { FormattedMessage } from 'react-intl'; +import { Button, ButtonProps, CircularProgress } from '@mui/material'; +import { Refresh } from '@mui/icons-material'; + +export function DataTableOverlay({ loading, ...buttonProps }: Readonly<{ loading: boolean } & ButtonProps>) { + return loading ? ( + + ) : ( + + ); +} diff --git a/src/components/dialogs/contingency-list/filter-based/filter-based-contingency-list-visualization-panel.tsx b/src/components/dialogs/contingency-list/filter-based/filter-based-contingency-list-visualization-panel.tsx new file mode 100644 index 00000000..7908b779 --- /dev/null +++ b/src/components/dialogs/contingency-list/filter-based/filter-based-contingency-list-visualization-panel.tsx @@ -0,0 +1,263 @@ +/** + * Copyright (c) 2025, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import { UUID } from 'node:crypto'; +import { useCallback, useRef, useState } from 'react'; +import { FormattedMessage, useIntl } from 'react-intl'; +import { AgGridReact } from 'ag-grid-react'; +import { ColDef, GetRowIdParams } from 'ag-grid-community'; +import { useFormContext } from 'react-hook-form'; +import { + CustomAGGrid, + DirectoryItemSelector, + ElementType, + FieldConstants, + getFilterEquipmentTypeLabel, + SeparatorCellRenderer, + TreeViewFinderNodeProps, + useSnackMessage, +} from '@gridsuite/commons-ui'; +import { Alert, Button, Grid, Typography } from '@mui/material'; +import { FolderOutlined } from '@mui/icons-material'; +import { DataTableOverlay } from './data-table-overlay'; +import { + ContingencyFieldConstants, + FilteredIdentifiables, + FilterElement, + FilterSubEquipments, + IdentifiableAttributes, +} from '../../../../utils/contingency-list.type'; +import { getIdentifiablesFromFilters } from '../../../../utils/rest-api'; + +const separator = '/'; +const SEPARATOR_TYPE = 'SEPARATOR'; + +const defaultDef: ColDef = { + resizable: false, + sortable: false, +}; + +interface RowData { + id: string; + type?: string; +} + +const getRowId = (params: GetRowIdParams) => { + return params.data.id; +}; + +export type FilterBasedContingencyListVisualizationPanelProps = { + isDataOutdated: boolean; + setIsDataOutdated: (value: boolean) => void; +}; + +export function FilterBasedContingencyListVisualizationPanel( + props: Readonly +) { + const { isDataOutdated, setIsDataOutdated } = props; + const gridRef = useRef(null); + const intl = useIntl(); + const { snackError } = useSnackMessage(); + const { getValues } = useFormContext(); + const [selectedStudy, setSelectedStudy] = useState(''); + const [selectedStudyId, setSelectedStudyId] = useState(); + const [selectedFolder, setSelectedFolder] = useState(''); + const [isOpen, setIsOpen] = useState(false); + const [isFetching, setIsFetching] = useState(false); + const [rowsData, setRowsData] = useState([]); + + const getTranslatedEquipmentType = useCallback( + (type: string | undefined) => { + const equipmentType = getFilterEquipmentTypeLabel(type); + return equipmentType ? intl.formatMessage({ id: equipmentType }) : ''; + }, + [intl] + ); + + const colDef: ColDef[] = [ + { + headerName: intl.formatMessage({ + id: FieldConstants.EQUIPMENT_ID, + }), + field: FieldConstants.ID, + cellRenderer: ({ data }: { data: IdentifiableAttributes }) => { + return data.id; + }, + flex: 3, + }, + { + headerName: intl.formatMessage({ + id: FieldConstants.TYPE, + }), + field: FieldConstants.TYPE, + flex: 2, + }, + ]; + + const updateRowData = useCallback( + (studyId: UUID | undefined) => { + if (studyId) { + const currentFilters: FilterElement[] = getValues(FieldConstants.FILTERS); + const currentSubEquipments: FilterSubEquipments[] = getValues( + ContingencyFieldConstants.SUB_EQUIPMENT_TYPES_BY_FILTER + ); + const filtersWithSubEquipments = { + filters: currentFilters.map((value) => ({ id: value.id })), + selectedEquipmentTypesByFilter: currentSubEquipments.map((value) => ({ + filterId: value.filterId, + equipmentTypes: value.subEquipmentTypes, + })), + }; + setIsFetching(true); + getIdentifiablesFromFilters(studyId, filtersWithSubEquipments) + .then((response: FilteredIdentifiables) => { + const attributes: IdentifiableAttributes[] = [ + ...response.equipmentIds.map((element: IdentifiableAttributes) => ({ + id: element.id, + type: getTranslatedEquipmentType(element?.type), + })), + ...(response.notFoundIds?.length > 0 + ? [ + { id: SEPARATOR_TYPE, type: '' }, + ...response.notFoundIds.map((element: IdentifiableAttributes) => ({ + id: element.id, + type: getTranslatedEquipmentType(element?.type), + })), + ] + : []), + ]; + setRowsData(attributes); + }) + .catch((error) => + snackError({ + messageTxt: error.message, + headerId: 'cannotComputeContingencyList', + }) + ) + .finally(() => { + setIsFetching(false); + setIsDataOutdated(false); + }); + } + }, + [getTranslatedEquipmentType, snackError, getValues, setIsDataOutdated] + ); + + const onNodeChanged = useCallback( + (nodes: TreeViewFinderNodeProps[]) => { + if (nodes.length > 0) { + if (nodes[0].parents && nodes[0].parents.length > 0) { + setSelectedFolder(nodes[0].parents.map((entry) => entry.name).join(separator)); + } + setSelectedStudy(nodes[0].name); + setSelectedStudyId(nodes[0].id); + const currentFilters: FilterElement[] = getValues(FieldConstants.FILTERS); + if (currentFilters.length > 0) updateRowData(nodes[0].id); + } + setIsOpen(false); + }, + [updateRowData, getValues] + ); + + const scrollOnRowById = useCallback(() => { + if (gridRef.current?.api) { + const { api } = gridRef.current; + const rowNode = api.getRowNode(SEPARATOR_TYPE); + + if (rowNode) { + api.ensureNodeVisible(rowNode, 'top'); + } + } + }, []); + + const hasMissingFromStudy = rowsData.some((row) => row.id === 'SEPARATOR' && row.type === ''); + + const studyName = selectedFolder ? selectedFolder + separator + selectedStudy : selectedStudy; + + return ( + + {/* ugly width fix for the grid layout */} + + + + + + + + + + + {selectedStudy.length > 0 ? ( + + {studyName} + + ) : ( + + )} + + + + + + + {hasMissingFromStudy && ( + + + + )} + + params.rowNode.data.id === 'SEPARATOR'} + fullWidthCellRenderer={() => { + return SeparatorCellRenderer({ + children: intl.formatMessage({ id: 'missingFromStudy' }), + sx: { + paddingLeft: 3, + }, + }); + }} + loading={isFetching || (selectedStudy?.length > 0 && isDataOutdated)} + loadingOverlayComponent={DataTableOverlay} + loadingOverlayComponentParams={{ + onClick: () => { + updateRowData(selectedStudyId); + }, + size: 'large', + loading: isFetching, + }} + /> + + + ); +} diff --git a/src/translations/en.json b/src/translations/en.json index 3944cc4d..f0a0c5ed 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -91,6 +91,8 @@ "contingencyList.explicitNaming": "Explicit naming", "contingencyList.criteriaBasedOrExplicitNaming": "Explicit naming or criteria based", "contingencyList.filterBased": "Filter based", + "contingencyList.filterBased.filtersTableColumn": "Voltage level / substation filters", + "contingencyList.filterBased.subEquipmentsTableColumn": "Equipment types", "distributionKey": "Distribution key", "CSVFileCommentContingencyList1": "# Use \"|\" as a separator if a contingency contains several equipments", "CSVFileCommentContingencyList2": "# ID_line1,Line 1", @@ -195,5 +197,9 @@ "read&Write": "Read / Write", "allUsers": "Anyone", "restricted": "Restricted", - "selectGroups": "Select one or more groups" + "selectGroups": "Select one or more groups", + "visualization": "Visualization", + "missingEquipmentsFromStudy": "Equipments missing from study", + "equipmentTypesByFilters": "Contingencies per equipment types in voltage levels & substations", + "reload": "Reload" } diff --git a/src/translations/fr.json b/src/translations/fr.json index fe050289..f7be8a70 100644 --- a/src/translations/fr.json +++ b/src/translations/fr.json @@ -90,6 +90,8 @@ "contingencyList.explicitNaming": "Par nommage", "contingencyList.criteriaBasedOrExplicitNaming": "Par nommage ou par critères", "contingencyList.filterBased": "Par filtres", + "contingencyList.filterBased.filtersTableColumn": "Filtres de postes / sites", + "contingencyList.filterBased.subEquipmentsTableColumn": "Types d'ouvrages", "distributionKey": "Clé de répartition", "CSVFileCommentContingencyList1": "# Si un aléa est associé à plusieurs ouvrages utiliser la barre verticale \"|\" comme séparateur", "CSVFileCommentContingencyList2": "# ID_ligne1;Ligne 1", @@ -194,5 +196,9 @@ "read&Write": "Lecture / Écriture", "allUsers": "Tout le monde", "restricted": "Restreint", - "selectGroups": "Sélectionner un ou plusieurs groupes" + "selectGroups": "Sélectionner un ou plusieurs groupes", + "visualization": "Visualisation", + "missingEquipmentsFromStudy": "Ouvrages absents de l'étude", + "equipmentTypesByFilters": "Aléas par types d'ouvrage dans les postes et sites", + "reload": "Rafraîchir" } diff --git a/src/utils/contingency-list.type.ts b/src/utils/contingency-list.type.ts index 4b9b7ea8..1eed8bca 100644 --- a/src/utils/contingency-list.type.ts +++ b/src/utils/contingency-list.type.ts @@ -4,7 +4,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { UUID } from 'crypto'; +import { UUID } from 'node:crypto'; // reproduce partially dto from Filter-server export interface FilterAttributes { @@ -27,4 +27,30 @@ export interface FilteredIdentifiables { // type taken from actions-server export interface FilterBasedContingencyList { filters: FilterAttributes[]; + selectedEquipmentTypesByFilter: Array<{ + filterId: string; + equipmentTypes: string[]; + }>; +} + +// type taken from filter-server, for now it's the same as FilterBasedContingencyList +export type FiltersWithEquipmentTypes = FilterBasedContingencyList; + +export interface FilterElement { + id: UUID; + name: string; + specificMetadata: { + equipmentType: string; + }; +} + +export enum ContingencyFieldConstants { + SUB_EQUIPMENT_TYPES_BY_FILTER = 'subEquipmentTypesByFilter', + FILTER_ID = 'filterId', + SUB_EQUIPMENT_TYPES = 'subEquipmentTypes', +} + +export interface FilterSubEquipments { + [ContingencyFieldConstants.FILTER_ID]: string; + [ContingencyFieldConstants.SUB_EQUIPMENT_TYPES]: string[]; } diff --git a/src/utils/rest-api.ts b/src/utils/rest-api.ts index 111990e1..e690aad1 100644 --- a/src/utils/rest-api.ts +++ b/src/utils/rest-api.ts @@ -31,7 +31,7 @@ import { AppState } from '../redux/types'; import { PrepareContingencyListForBackend } from '../components/dialogs/contingency-list-helper'; import { UsersIdentities } from './user-identities.type'; import { HTTP_OK } from './UIconstants'; -import { FilterBasedContingencyList, FilteredIdentifiables } from './contingency-list.type'; +import { FilterBasedContingencyList, FilteredIdentifiables, FiltersWithEquipmentTypes } from './contingency-list.type'; const PREFIX_USER_ADMIN_SERVER_QUERIES = `${import.meta.env.VITE_API_GATEWAY}/user-admin`; const PREFIX_EXPLORE_SERVER_QUERIES = `${import.meta.env.VITE_API_GATEWAY}/explore`; @@ -575,15 +575,16 @@ export function getContingencyList(type: string, id: string) { }); } -export function getIdentifiablesFromFitlers(studyUuid: UUID, filters: UUID[]): Promise { +export function getIdentifiablesFromFilters( + studyUuid: UUID, + filters: FiltersWithEquipmentTypes +): Promise { console.info('get identifiables resulting from application of filters list on study root network'); - const filtersListsQueryParams = getRequestParamFromList('filtersUuid', filters); - const urlSearchParams = new URLSearchParams(filtersListsQueryParams); - - return backendFetchJson(`${PREFIX_STUDY_QUERIES}/v1/studies/${studyUuid}/filters/elements?${urlSearchParams}`, { - method: 'get', + return backendFetchJson(`${PREFIX_STUDY_QUERIES}/v1/studies/${studyUuid}/filters/elements`, { + method: 'post', headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(filters), }); }