From c7e5e7c9d06458827bcd3d19b329f6fc6bd5cf14 Mon Sep 17 00:00:00 2001 From: David BRAQUART Date: Thu, 24 Apr 2025 19:03:37 +0200 Subject: [PATCH 01/18] clean code and use simple click to edit profile Signed-off-by: David BRAQUART --- src/pages/groups/add-group-dialog.tsx | 4 +- .../modification/linked-path-display.tsx | 15 ++++---- .../profile-modification-dialog.tsx | 38 +++++-------------- src/pages/profiles/profiles-page.tsx | 6 +-- src/pages/profiles/profiles-table.tsx | 14 ++----- 5 files changed, 25 insertions(+), 52 deletions(-) diff --git a/src/pages/groups/add-group-dialog.tsx b/src/pages/groups/add-group-dialog.tsx index a633b61..559c8fa 100644 --- a/src/pages/groups/add-group-dialog.tsx +++ b/src/pages/groups/add-group-dialog.tsx @@ -18,7 +18,7 @@ import { } from '@mui/material'; import { FormattedMessage } from 'react-intl'; import { Controller, useForm } from 'react-hook-form'; -import { ManageAccounts } from '@mui/icons-material'; +import { Groups } from '@mui/icons-material'; import { GroupInfos, UserAdminSrv } from '../../services'; import { useSnackMessage } from '@gridsuite/commons-ui'; import { GridTableRef } from '../../components/Grid'; @@ -99,7 +99,7 @@ const AddGroupDialog: FunctionComponent = (props) => { InputProps={{ startAdornment: ( - + ), }} diff --git a/src/pages/profiles/modification/linked-path-display.tsx b/src/pages/profiles/modification/linked-path-display.tsx index c2cd944..1fab9fc 100644 --- a/src/pages/profiles/modification/linked-path-display.tsx +++ b/src/pages/profiles/modification/linked-path-display.tsx @@ -30,14 +30,13 @@ const LinkedPathDisplay: FunctionComponent = (props) => id: props.nameKey, }) + ' : ' + - (props.value - ? props.value - : intl.formatMessage({ - id: - props.linkValidity === false - ? 'linked.path.display.invalidLink' - : 'linked.path.display.noLink', - }))} + (props.value ?? + intl.formatMessage({ + id: + props.linkValidity === false + ? 'linked.path.display.invalidLink' + : 'linked.path.display.noLink', + }))} ); }; diff --git a/src/pages/profiles/modification/profile-modification-dialog.tsx b/src/pages/profiles/modification/profile-modification-dialog.tsx index 63c6646..26c41bb 100644 --- a/src/pages/profiles/modification/profile-modification-dialog.tsx +++ b/src/pages/profiles/modification/profile-modification-dialog.tsx @@ -21,18 +21,10 @@ import yup from '../../../utils/yup-config'; import { yupResolver } from '@hookform/resolvers/yup'; import { useForm } from 'react-hook-form'; import { FunctionComponent, useCallback, useEffect, useMemo, useState } from 'react'; -import { CustomMuiDialog, useSnackMessage } from '@gridsuite/commons-ui'; +import { CustomMuiDialog, FetchStatus, useSnackMessage } from '@gridsuite/commons-ui'; import { UserAdminSrv, UserProfile } from '../../../services'; import { UUID } from 'crypto'; -// TODO remove FetchStatus when exported in commons-ui (available soon) -enum FetchStatus { - IDLE = 'IDLE', - FETCHING = 'FETCHING', - FETCH_SUCCESS = 'FETCH_SUCCESS', - FETCH_ERROR = 'FETCH_ERROR', -} - export interface ProfileModificationDialogProps { profileId: UUID | undefined; open: boolean; @@ -47,7 +39,7 @@ const ProfileModificationDialog: FunctionComponent { const { snackError } = useSnackMessage(); - const [dataFetchStatus, setDataFetchStatus] = useState(FetchStatus.IDLE); + const [dataFetchStatus, setDataFetchStatus] = useState(FetchStatus.IDLE); const formSchema = yup .object() @@ -115,27 +107,15 @@ const ProfileModificationDialog: FunctionComponent { diff --git a/src/pages/profiles/profiles-page.tsx b/src/pages/profiles/profiles-page.tsx index 6f6f95c..512fdfe 100644 --- a/src/pages/profiles/profiles-page.tsx +++ b/src/pages/profiles/profiles-page.tsx @@ -9,7 +9,7 @@ import { FunctionComponent, useCallback, useRef, useState } from 'react'; import { Grid } from '@mui/material'; import { GridTableRef } from '../../components/Grid'; import { UserProfile } from '../../services'; -import { RowDoubleClickedEvent } from 'ag-grid-community'; +import { RowClickedEvent } from 'ag-grid-community'; import ProfileModificationDialog from './modification/profile-modification-dialog'; import { UUID } from 'crypto'; import ProfilesTable from './profiles-table'; @@ -33,7 +33,7 @@ const ProfilesPage: FunctionComponent = () => { handleCloseProfileModificationDialog(); }, [gridContext, handleCloseProfileModificationDialog]); - const onRowDoubleClicked = useCallback((event: RowDoubleClickedEvent) => { + const onRowClicked = useCallback((event: RowClickedEvent) => { if (event.data) { setEditingProfileId(event.data.id); setOpenProfileModificationDialog(true); @@ -51,7 +51,7 @@ const ProfilesPage: FunctionComponent = () => { /> diff --git a/src/pages/profiles/profiles-table.tsx b/src/pages/profiles/profiles-table.tsx index 53db02c..0c03b04 100644 --- a/src/pages/profiles/profiles-table.tsx +++ b/src/pages/profiles/profiles-table.tsx @@ -10,13 +10,7 @@ import { useIntl } from 'react-intl'; import { Cancel, CheckCircle, ManageAccounts, RadioButtonUnchecked } from '@mui/icons-material'; import { GridButton, GridButtonDelete, GridTable, GridTableRef } from '../../components/Grid'; import { UserAdminSrv, UserProfile } from '../../services'; -import { - ColDef, - GetRowIdParams, - RowDoubleClickedEvent, - SelectionChangedEvent, - TextFilterParams, -} from 'ag-grid-community'; +import { ColDef, GetRowIdParams, RowClickedEvent, SelectionChangedEvent, TextFilterParams } from 'ag-grid-community'; import { useSnackMessage } from '@gridsuite/commons-ui'; import DeleteConfirmationDialog from '../common/delete-confirmation-dialog'; @@ -31,7 +25,7 @@ const defaultColDef: ColDef = { export interface ProfilesTableProps { gridRef: RefObject>; - onRowDoubleClicked: (event: RowDoubleClickedEvent) => void; + onRowClicked: (event: RowClickedEvent) => void; setOpenAddProfileDialog: (open: boolean) => void; } @@ -43,7 +37,7 @@ const ProfilesTable: FunctionComponent = (props) => { const [showDeletionDialog, setShowDeletionDialog] = useState(false); function getRowId(params: GetRowIdParams): string { - return params.data.id ? params.data.id : ''; + return params.data.id ?? ''; } const onSelectionChanged = useCallback( @@ -139,7 +133,7 @@ const ProfilesTable: FunctionComponent = (props) => { headerCheckbox: true, hideDisabledCheckboxes: false, }} - onRowDoubleClicked={props.onRowDoubleClicked} + onRowClicked={props.onRowClicked} onSelectionChanged={onSelectionChanged} > Date: Sun, 27 Apr 2025 21:41:30 +0200 Subject: [PATCH 02/18] refacto user page, missing groups in edit dialog Signed-off-by: David BRAQUART --- src/pages/common/table-config.ts | 25 ++ src/pages/groups/GroupsPage.tsx | 10 +- .../profile-modification-form.tsx | 2 +- src/pages/profiles/profiles-table.tsx | 18 +- src/pages/users/UsersPage.tsx | 354 ------------------ src/pages/users/add-user-dialog.tsx | 124 ++++++ src/pages/users/index.ts | 2 +- .../modification/user-modification-dialog.tsx | 120 ++++++ .../modification/user-modification-form.tsx | 57 +++ src/pages/users/users-page.tsx | 57 +++ src/pages/users/users-table.tsx | 217 +++++++++++ src/translations/en.json | 1 - src/translations/fr.json | 1 - 13 files changed, 605 insertions(+), 383 deletions(-) create mode 100644 src/pages/common/table-config.ts delete mode 100644 src/pages/users/UsersPage.tsx create mode 100644 src/pages/users/add-user-dialog.tsx create mode 100644 src/pages/users/modification/user-modification-dialog.tsx create mode 100644 src/pages/users/modification/user-modification-form.tsx create mode 100644 src/pages/users/users-page.tsx create mode 100644 src/pages/users/users-table.tsx diff --git a/src/pages/common/table-config.ts b/src/pages/common/table-config.ts new file mode 100644 index 0000000..e7c8f4f --- /dev/null +++ b/src/pages/common/table-config.ts @@ -0,0 +1,25 @@ +/** + * 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 { ColDef, RowSelectionOptions } from 'ag-grid-community'; + +export const defaultColDef: ColDef = { + editable: false, + resizable: true, + minWidth: 50, + cellRenderer: 'agAnimateSlideCellRenderer', + rowDrag: false, + sortable: true, +}; + +export const defaultRowSelection: RowSelectionOptions = { + mode: 'multiRow', + enableClickSelection: false, + checkboxes: true, + headerCheckbox: true, + hideDisabledCheckboxes: false, +}; diff --git a/src/pages/groups/GroupsPage.tsx b/src/pages/groups/GroupsPage.tsx index a91d10d..25f2a5f 100644 --- a/src/pages/groups/GroupsPage.tsx +++ b/src/pages/groups/GroupsPage.tsx @@ -17,17 +17,9 @@ import DeleteConfirmationDialog from '../common/delete-confirmation-dialog'; import MultiSelectEditorComponent from '../common/multi-select-editor-component'; import MultiChipsRendererComponent from '../common/multi-chips-renderer-component'; import { UUID } from 'crypto'; +import { defaultColDef } from '../common/table-config'; import AddGroupDialog from './add-group-dialog'; -const defaultColDef: ColDef = { - editable: false, - resizable: true, - minWidth: 50, - cellRenderer: 'agAnimateSlideCellRenderer', - rowDrag: false, - sortable: true, -}; - function getRowId(params: GetRowIdParams): string { return params.data.name; } diff --git a/src/pages/profiles/modification/profile-modification-form.tsx b/src/pages/profiles/modification/profile-modification-form.tsx index 6bcea24..185f88f 100644 --- a/src/pages/profiles/modification/profile-modification-form.tsx +++ b/src/pages/profiles/modification/profile-modification-form.tsx @@ -27,7 +27,7 @@ const ProfileModificationForm: FunctionComponent = () => { return ( - +

diff --git a/src/pages/profiles/profiles-table.tsx b/src/pages/profiles/profiles-table.tsx index 0c03b04..ddf098c 100644 --- a/src/pages/profiles/profiles-table.tsx +++ b/src/pages/profiles/profiles-table.tsx @@ -13,15 +13,7 @@ import { UserAdminSrv, UserProfile } from '../../services'; import { ColDef, GetRowIdParams, RowClickedEvent, SelectionChangedEvent, TextFilterParams } from 'ag-grid-community'; import { useSnackMessage } from '@gridsuite/commons-ui'; import DeleteConfirmationDialog from '../common/delete-confirmation-dialog'; - -const defaultColDef: ColDef = { - editable: false, - resizable: true, - minWidth: 50, - cellRenderer: 'agAnimateSlideCellRenderer', - rowDrag: false, - sortable: true, -}; +import { defaultColDef, defaultRowSelection } from '../common/table-config'; export interface ProfilesTableProps { gridRef: RefObject>; @@ -126,13 +118,7 @@ const ProfilesTable: FunctionComponent = (props) => { defaultColDef={defaultColDef} gridId="table-profiles" getRowId={getRowId} - rowSelection={{ - mode: 'multiRow', - enableClickSelection: false, - checkboxes: true, - headerCheckbox: true, - hideDisabledCheckboxes: false, - }} + rowSelection={defaultRowSelection} onRowClicked={props.onRowClicked} onSelectionChanged={onSelectionChanged} > diff --git a/src/pages/users/UsersPage.tsx b/src/pages/users/UsersPage.tsx deleted file mode 100644 index 8f1ad35..0000000 --- a/src/pages/users/UsersPage.tsx +++ /dev/null @@ -1,354 +0,0 @@ -/* - * Copyright (c) 2024, 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 { FunctionComponent, useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { FormattedMessage, useIntl } from 'react-intl'; -import { - Button, - Dialog, - DialogActions, - DialogContent, - DialogContentText, - DialogTitle, - Grid, - InputAdornment, - TextField, -} from '@mui/material'; -import { AccountCircle, PersonAdd } from '@mui/icons-material'; -import { GridButton, GridButtonDelete, GridTable, GridTableRef } from '../../components/Grid'; -import { GroupInfos, UpdateUserInfos, UserAdminSrv, UserInfos, UserProfile } from '../../services'; -import { useSnackMessage } from '@gridsuite/commons-ui'; -import { Controller, SubmitHandler, useForm } from 'react-hook-form'; -import { - CellEditingStoppedEvent, - ColDef, - GetRowIdParams, - ICellEditorParams, - ICheckboxCellRendererParams, - SelectionChangedEvent, - TextFilterParams, -} from 'ag-grid-community'; -import PaperForm from '../common/paper-form'; -import DeleteConfirmationDialog from '../common/delete-confirmation-dialog'; -import MultiSelectEditorComponent from '../common/multi-select-editor-component'; -import MultiChipsRendererComponent from '../common/multi-chips-renderer-component'; - -const defaultColDef: ColDef = { - editable: false, - resizable: true, - minWidth: 50, - cellRenderer: 'agAnimateSlideCellRenderer', //'agAnimateShowChangeCellRenderer' - rowDrag: false, - sortable: true, -}; - -function getRowId(params: GetRowIdParams): string { - return params.data.sub; -} - -const UsersPage: FunctionComponent = () => { - const intl = useIntl(); - const { snackError } = useSnackMessage(); - const gridRef = useRef>(null); - const gridContext = gridRef.current?.context; - const [profileNameOptions, setprofileNameOptions] = useState([]); - const [groupsOptions, setGroupsOptions] = useState([]); - - useEffect(() => { - // fetch available profiles - UserAdminSrv.fetchProfilesWithoutValidityCheck() - .then((allProfiles: UserProfile[]) => { - let profiles: string[] = [intl.formatMessage({ id: 'users.table.profile.none' })]; - allProfiles?.forEach((p) => profiles.push(p.name)); - setprofileNameOptions(profiles); - }) - .catch((error) => - snackError({ - messageTxt: error.message, - headerId: 'users.table.error.profiles', - }) - ); - - // fetch available groups - UserAdminSrv.fetchGroups() - .then((allGroups: GroupInfos[]) => { - let groups: string[] = []; - allGroups?.forEach((g) => groups.push(g.name)); - setGroupsOptions(groups); - }) - .catch((error) => - snackError({ - messageTxt: error.message, - headerId: 'users.table.error.groups', - }) - ); - }, [intl, snackError]); - - const updateUserCallback = useCallback( - (sub: string, profileName: string | undefined, isAdmin: boolean | undefined, groups: string[]) => { - const newData: UpdateUserInfos = { - sub: sub, - profileName: profileName, - isAdmin: isAdmin, - groups: groups, - }; - UserAdminSrv.udpateUser(newData) - .catch((error) => - snackError({ - messageTxt: error.message, - headerId: 'users.table.error.update', - }) - ) - .then(() => gridContext?.refresh?.()); - }, - [gridContext, snackError] - ); - - const columns = useMemo( - (): ColDef[] => [ - { - field: 'sub', - cellDataType: 'text', - flex: 3, - lockVisible: true, - filter: true, - headerName: intl.formatMessage({ id: 'users.table.id' }), - headerTooltip: intl.formatMessage({ - id: 'users.table.id.description', - }), - filterParams: { - caseSensitive: false, - trimInput: true, - } as TextFilterParams, - initialSort: 'asc', - }, - { - field: 'profileName', - cellDataType: 'text', - flex: 1, - filter: true, - headerName: intl.formatMessage({ - id: 'users.table.profileName', - }), - headerTooltip: intl.formatMessage({ - id: 'users.table.profileName.description', - }), - filterParams: { - caseSensitive: false, - trimInput: true, - } as TextFilterParams, - editable: true, - cellEditor: 'agSelectCellEditor', - cellEditorParams: () => { - return { - values: profileNameOptions, - valueListMaxHeight: 400, - valueListMaxWidth: 300, - }; - }, - }, - { - field: 'isAdmin', - cellDataType: 'boolean', - //detected as cellRenderer: 'agCheckboxCellRenderer', - cellRendererParams: { - disabled: true, - } as ICheckboxCellRendererParams, - flex: 1, - headerName: intl.formatMessage({ - id: 'users.table.isAdmin', - }), - headerTooltip: intl.formatMessage({ - id: 'users.table.isAdmin.description', - }), - filter: true, - }, - { - field: 'groups', - cellDataType: 'text', - flex: 1, - filter: true, - headerName: intl.formatMessage({ - id: 'users.table.groups', - }), - headerTooltip: intl.formatMessage({ - id: 'users.table.groups.description', - }), - filterParams: { - caseSensitive: false, - trimInput: true, - } as TextFilterParams, - editable: true, - cellRenderer: MultiChipsRendererComponent, - cellEditor: MultiSelectEditorComponent, - cellEditorParams: (params: ICellEditorParams) => ({ - options: groupsOptions, - setValue: (values: string[]) => { - if (params.data?.sub) { - updateUserCallback(params.data.sub, params.data.profileName, params.data.isAdmin, values); - } - }, - }), - }, - ], - [intl, profileNameOptions, groupsOptions, updateUserCallback] - ); - - const [rowsSelection, setRowsSelection] = useState([]); - const deleteUsers = useCallback((): Promise | undefined => { - let subs = rowsSelection.map((user) => user.sub); - return UserAdminSrv.deleteUsers(subs) - .catch((error) => - snackError({ - messageTxt: error.message, - headerId: 'users.table.error.delete', - }) - ) - .then(() => gridContext?.refresh?.()); - }, [gridContext, rowsSelection, snackError]); - const deleteUsersDisabled = useMemo(() => rowsSelection.length <= 0, [rowsSelection.length]); - - const addUser = useCallback( - (id: string) => { - UserAdminSrv.addUser(id) - .catch((error) => - snackError({ - headerId: 'users.table.error.add', - headerValues: { - user: id, - }, - }) - ) - .then(() => gridContext?.refresh?.()); - }, - [gridContext, snackError] - ); - const { handleSubmit, control, reset, clearErrors } = useForm<{ - user: string; - }>({ - defaultValues: { user: '' }, //need default not undefined value for html input, else react error at runtime - }); - const [open, setOpen] = useState(false); - const [showDeletionDialog, setShowDeletionDialog] = useState(false); - const handleClose = () => { - setOpen(false); - reset(); - clearErrors(); - }; - const onSubmit: SubmitHandler<{ user: string }> = (data) => { - addUser(data.user.trim()); - handleClose(); - }; - const onSubmitForm = handleSubmit(onSubmit); - - const handleCellEditingStopped = useCallback( - (event: CellEditingStoppedEvent) => { - if (event.valueChanged && event.data) { - updateUserCallback(event.data.sub, event.data.profileName, event.data.isAdmin, event.data.groups); - } else { - gridContext?.refresh?.(); - } - }, - [gridContext, updateUserCallback] - ); - - return ( - - - - ref={gridRef} - dataLoader={UserAdminSrv.fetchUsers} - columnDefs={columns} - defaultColDef={defaultColDef} - stopEditingWhenCellsLoseFocus={true} - onCellEditingStopped={handleCellEditingStopped} - gridId="table-users" - getRowId={getRowId} - rowSelection={{ - mode: 'multiRow', - enableClickSelection: false, - checkboxes: true, - headerCheckbox: true, - hideDisabledCheckboxes: false, - }} - onSelectionChanged={useCallback( - (event: SelectionChangedEvent) => - setRowsSelection(event.api.getSelectedRows() ?? []), - [] - )} - > - } - color="primary" - onClick={useCallback(() => setOpen(true), [])} - /> - setShowDeletionDialog(true)} disabled={deleteUsersDisabled} /> - - } - > - - - - - - - - ( - } - type="text" - fullWidth - variant="standard" - inputMode="text" - InputProps={{ - startAdornment: ( - - - - ), - }} - error={fieldState?.invalid} - helperText={fieldState?.error?.message} - /> - )} - /> - - - - - - - - user.sub)} - deleteFunc={deleteUsers} - /> - - - ); -}; -export default UsersPage; diff --git a/src/pages/users/add-user-dialog.tsx b/src/pages/users/add-user-dialog.tsx new file mode 100644 index 0000000..08e8bf1 --- /dev/null +++ b/src/pages/users/add-user-dialog.tsx @@ -0,0 +1,124 @@ +/** + * 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 { FunctionComponent, RefObject, useCallback } from 'react'; +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, + InputAdornment, + TextField, +} from '@mui/material'; +import { FormattedMessage } from 'react-intl'; +import { Controller, useForm } from 'react-hook-form'; +import { AccountCircle } from '@mui/icons-material'; +import { UserAdminSrv, UserInfos } from '../../services'; +import { useSnackMessage } from '@gridsuite/commons-ui'; +import { GridTableRef } from '../../components/Grid'; +import PaperForm from '../common/paper-form'; + +export interface AddUserDialogProps { + gridRef: RefObject>; + open: boolean; + setOpen: (open: boolean) => void; +} + +const AddUserDialog: FunctionComponent = (props) => { + const { snackError } = useSnackMessage(); + + const { handleSubmit, control, reset, clearErrors } = useForm<{ + name: string; + }>({ + defaultValues: { name: '' }, //need default not undefined value for html input, else react error at runtime + }); + + const addUser = useCallback( + (id: string) => { + UserAdminSrv.addUser(id) + .catch((error) => + snackError({ + headerId: 'users.table.error.add', + headerValues: { + user: id, + }, + }) + ) + .then(() => props.gridRef?.current?.context?.refresh?.()); + }, + [props.gridRef, snackError] + ); + + const handleClose = useCallback(() => { + props.setOpen(false); + reset(); + clearErrors(); + }, [clearErrors, props, reset]); + + const onSubmit = useCallback( + (data: { name: string }) => { + addUser(data.name.trim()); + handleClose(); + }, + [addUser, handleClose] + ); + + return ( + } + > + + + + + + + + ( + } + type="text" + fullWidth + variant="standard" + inputMode="text" + InputProps={{ + startAdornment: ( + + + + ), + }} + error={fieldState?.invalid} + helperText={fieldState?.error?.message} + /> + )} + /> + + + + + + + ); +}; +export default AddUserDialog; diff --git a/src/pages/users/index.ts b/src/pages/users/index.ts index 3da2b99..e678457 100644 --- a/src/pages/users/index.ts +++ b/src/pages/users/index.ts @@ -5,4 +5,4 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -export { default as Users } from './UsersPage'; +export { default as Users } from './users-page'; diff --git a/src/pages/users/modification/user-modification-dialog.tsx b/src/pages/users/modification/user-modification-dialog.tsx new file mode 100644 index 0000000..746cfbc --- /dev/null +++ b/src/pages/users/modification/user-modification-dialog.tsx @@ -0,0 +1,120 @@ +/* + * 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 ProfileModificationForm, { + USER_NAME, + USER_PROFILE_NAME, + UserModificationFormType, + UserModificationSchema, +} from './user-modification-form'; +import { yupResolver } from '@hookform/resolvers/yup'; +import { useForm } from 'react-hook-form'; +import { FunctionComponent, useCallback, useEffect, useMemo, useState } from 'react'; +import { CustomMuiDialog, FetchStatus, useSnackMessage } from '@gridsuite/commons-ui'; +import { UpdateUserInfos, UserAdminSrv, UserInfos, UserProfile } from '../../../services'; + +interface UserModificationDialogProps { + userInfos: UserInfos | undefined; + open: boolean; + onClose: () => void; + onUpdate: () => void; +} + +const UserModificationDialog: FunctionComponent = ({ + userInfos, + open, + onClose, + onUpdate, +}) => { + const { snackError } = useSnackMessage(); + const formMethods = useForm({ + resolver: yupResolver(UserModificationSchema), + }); + const { reset } = formMethods; + const [profileOptions, setprofileOptions] = useState([]); + const [dataFetchStatus, setDataFetchStatus] = useState(FetchStatus.IDLE); + + useEffect(() => { + // fetch available profiles + if (userInfos && open) { + setDataFetchStatus(FetchStatus.FETCHING); + UserAdminSrv.fetchProfilesWithoutValidityCheck() + .then((allProfiles: UserProfile[]) => { + setDataFetchStatus(FetchStatus.FETCH_SUCCESS); + setprofileOptions( + allProfiles.map((p) => p.name).sort((a: string, b: string) => a.localeCompare(b)) + ); + }) + .catch((error) => { + setDataFetchStatus(FetchStatus.FETCH_ERROR); + snackError({ + messageTxt: error.message, + headerId: 'users.table.error.profiles', + }); + }); + } + }, [open, snackError, userInfos]); + + useEffect(() => { + if (userInfos && open) { + reset({ + [USER_NAME]: userInfos.sub, + [USER_PROFILE_NAME]: userInfos.profileName, + }); + } + }, [userInfos, open, reset]); + + const onDialogClose = useCallback(() => { + setDataFetchStatus(FetchStatus.IDLE); + onClose(); + }, [onClose]); + + const onSubmit = useCallback( + (userFormData: UserModificationFormType) => { + if (userInfos) { + console.log('DBG DBR', userFormData, userInfos); + const newData: UpdateUserInfos = { + sub: userInfos.sub, // sub cannot be changed, it is a PK in database + isAdmin: userInfos.isAdmin, // cannot be changed for now + profileName: userFormData.profileName ?? undefined, + groups: [], + }; + UserAdminSrv.udpateUser(newData) + .catch((error) => + snackError({ + messageTxt: error.message, + headerId: 'users.table.error.update', + }) + ) + .then(() => { + onUpdate(); + }); + } + }, + [onUpdate, snackError, userInfos] + ); + + const isDataReady = useMemo(() => dataFetchStatus === FetchStatus.FETCH_SUCCESS, [dataFetchStatus]); + const isDataFetching = useMemo(() => dataFetchStatus === FetchStatus.FETCHING, [dataFetchStatus]); + + return ( + + {isDataReady && } + + ); +}; + +export default UserModificationDialog; diff --git a/src/pages/users/modification/user-modification-form.tsx b/src/pages/users/modification/user-modification-form.tsx new file mode 100644 index 0000000..bc0a13b --- /dev/null +++ b/src/pages/users/modification/user-modification-form.tsx @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2024, 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 { AutocompleteInput, TextInput } from '@gridsuite/commons-ui'; +import Grid from '@mui/material/Grid'; +import React, { FunctionComponent } from 'react'; +import yup from '../../../utils/yup-config'; + +export const USER_NAME = 'sub'; +export const USER_PROFILE_NAME = 'profileName'; + +export const UserModificationSchema = yup + .object() + .shape({ + [USER_NAME]: yup.string().trim().required('nameEmpty'), + [USER_PROFILE_NAME]: yup.string().nullable(), + }) + .required(); + +export type UserModificationFormType = yup.InferType; + +interface UserModificationFormProps { + profileOptions: string[]; +} + +const UserModificationForm: FunctionComponent = ({ profileOptions }) => { + return ( + + + + + + + + + ); +}; + +export default UserModificationForm; diff --git a/src/pages/users/users-page.tsx b/src/pages/users/users-page.tsx new file mode 100644 index 0000000..8d6294b --- /dev/null +++ b/src/pages/users/users-page.tsx @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2024, 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 { FunctionComponent, useCallback, useRef, useState } from 'react'; +import { Grid } from '@mui/material'; +import { GridTableRef } from '../../components/Grid'; +import { UserInfos } from '../../services'; +import { RowClickedEvent } from 'ag-grid-community'; +import UserModificationDialog from './modification/user-modification-dialog'; +import UsersTable from './users-table'; +import AddUserDialog from './add-user-dialog'; + +const UsersPage: FunctionComponent = () => { + const gridRef = useRef>(null); + const gridContext = gridRef.current?.context; + const [openUserModificationDialog, setOpenUserModificationDialog] = useState(false); + const [editingUser, setEditingUser] = useState(); + + const [openAddUserDialog, setOpenAddUserDialog] = useState(false); + + const handleCloseUserModificationDialog = useCallback(() => { + setOpenUserModificationDialog(false); + setEditingUser(undefined); + }, []); + + const handleUpdateUserModificationDialog = useCallback(() => { + gridContext?.refresh?.(); + handleCloseUserModificationDialog(); + }, [gridContext, handleCloseUserModificationDialog]); + + const onRowClicked = useCallback((event: RowClickedEvent) => { + if (event.data) { + setEditingUser(event.data); + setOpenUserModificationDialog(true); + } + }, []); + + return ( + + + + + + + + ); +}; +export default UsersPage; diff --git a/src/pages/users/users-table.tsx b/src/pages/users/users-table.tsx new file mode 100644 index 0000000..5ba0083 --- /dev/null +++ b/src/pages/users/users-table.tsx @@ -0,0 +1,217 @@ +/** + * Copyright (c) 2024, 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 { FunctionComponent, RefObject, useCallback, useEffect, useMemo, useState } from 'react'; +import { useIntl } from 'react-intl'; +import { PersonAdd } from '@mui/icons-material'; +import { GridButton, GridButtonDelete, GridTable, GridTableRef } from '../../components/Grid'; +import { GroupInfos, UpdateUserInfos, UserAdminSrv, UserInfos } from '../../services'; +import { + ColDef, + GetRowIdParams, + ICellEditorParams, + ICheckboxCellRendererParams, + RowClickedEvent, + SelectionChangedEvent, + TextFilterParams, +} from 'ag-grid-community'; +import { useSnackMessage } from '@gridsuite/commons-ui'; +import DeleteConfirmationDialog from '../common/delete-confirmation-dialog'; +import { defaultColDef, defaultRowSelection } from '../common/table-config'; +import MultiChipsRendererComponent from '../common/multi-chips-renderer-component'; +import MultiSelectEditorComponent from '../common/multi-select-editor-component'; + +export interface UsersTableProps { + gridRef: RefObject>; + onRowClicked: (event: RowClickedEvent) => void; + setOpenAddUserDialog: (open: boolean) => void; +} + +const UsersTable: FunctionComponent = (props) => { + const intl = useIntl(); + const { snackError } = useSnackMessage(); + + const [rowsSelection, setRowsSelection] = useState([]); + const [showDeletionDialog, setShowDeletionDialog] = useState(false); + const [groupsOptions, setGroupsOptions] = useState([]); + + useEffect(() => { + // fetch available groups + UserAdminSrv.fetchGroups() + .then((allGroups: GroupInfos[]) => { + let groups: string[] = []; + allGroups?.forEach((g) => groups.push(g.name)); + setGroupsOptions(groups); + }) + .catch((error) => + snackError({ + messageTxt: error.message, + headerId: 'users.table.error.groups', + }) + ); + }, [intl, snackError]); + + function getRowId(params: GetRowIdParams): string { + return params.data.sub ?? ''; + } + + const onSelectionChanged = useCallback( + (event: SelectionChangedEvent) => setRowsSelection(event.api.getSelectedRows() ?? []), + [setRowsSelection] + ); + + const onAddButton = useCallback(() => props.setOpenAddUserDialog(true), [props]); + + const deleteUsers = useCallback(() => { + let subs = rowsSelection.map((user) => user.sub); + return UserAdminSrv.deleteUsers(subs) + .catch((error) => + snackError({ + messageTxt: error.message, + headerId: 'users.table.error.delete', + }) + ) + .then(() => props.gridRef?.current?.context?.refresh?.()); + }, [props.gridRef, rowsSelection, snackError]); + + const deleteUsersDisabled = useMemo(() => rowsSelection.length <= 0, [rowsSelection.length]); + + const updateUserCallback = useCallback( + (sub: string, profileName: string | undefined, isAdmin: boolean | undefined, groups: string[]) => { + const newData: UpdateUserInfos = { + sub: sub, + profileName: profileName, + isAdmin: isAdmin, + groups: groups, + }; + UserAdminSrv.udpateUser(newData) + .catch((error) => + snackError({ + messageTxt: error.message, + headerId: 'users.table.error.update', + }) + ) + .then(() => props.gridRef?.current?.context?.refresh?.()); + }, + [props.gridRef, snackError] + ); + + const columns = useMemo( + (): ColDef[] => [ + { + field: 'sub', + cellDataType: 'text', + flex: 3, + lockVisible: true, + filter: true, + headerName: intl.formatMessage({ id: 'users.table.id' }), + headerTooltip: intl.formatMessage({ + id: 'users.table.id.description', + }), + filterParams: { + caseSensitive: false, + trimInput: true, + } as TextFilterParams, + initialSort: 'asc', + }, + { + field: 'profileName', + cellDataType: 'text', + flex: 1, + filter: true, + headerName: intl.formatMessage({ + id: 'users.table.profileName', + }), + headerTooltip: intl.formatMessage({ + id: 'users.table.profileName.description', + }), + filterParams: { + caseSensitive: false, + trimInput: true, + } as TextFilterParams, + }, + { + field: 'isAdmin', + cellDataType: 'boolean', + //detected as cellRenderer: 'agCheckboxCellRenderer', + cellRendererParams: { + disabled: true, + } as ICheckboxCellRendererParams, + flex: 1, + headerName: intl.formatMessage({ + id: 'users.table.isAdmin', + }), + headerTooltip: intl.formatMessage({ + id: 'users.table.isAdmin.description', + }), + filter: true, + }, + { + field: 'groups', + cellDataType: 'text', + flex: 1, + filter: true, + headerName: intl.formatMessage({ + id: 'users.table.groups', + }), + headerTooltip: intl.formatMessage({ + id: 'users.table.groups.description', + }), + filterParams: { + caseSensitive: false, + trimInput: true, + } as TextFilterParams, + editable: true, + cellRenderer: MultiChipsRendererComponent, + cellEditor: MultiSelectEditorComponent, + cellEditorParams: (params: ICellEditorParams) => ({ + options: groupsOptions, + setValue: (values: string[]) => { + if (params.data?.sub) { + updateUserCallback(params.data.sub, params.data.profileName, params.data.isAdmin, values); + } + }, + }), + }, + ], + [groupsOptions, intl, updateUserCallback] + ); + + return ( + <> + + ref={props.gridRef} + dataLoader={UserAdminSrv.fetchUsers} + columnDefs={columns} + defaultColDef={defaultColDef} + gridId="table-users" + getRowId={getRowId} + rowSelection={defaultRowSelection} + onRowClicked={props.onRowClicked} + onSelectionChanged={onSelectionChanged} + > + } + color="primary" + onClick={onAddButton} + /> + setShowDeletionDialog(true)} disabled={deleteUsersDisabled} /> + + + user.sub)} + deleteFunc={deleteUsers} + /> + + ); +}; +export default UsersTable; diff --git a/src/translations/en.json b/src/translations/en.json index bd1a00e..16d3a02 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -39,7 +39,6 @@ "users.table.error.add": "Error while adding user {user} : A user with the same name already exists.", "users.table.toolbar.add": "Add user", "users.table.error.update": "Error while modifying user", - "users.table.profile.none": "", "users.table.error.profiles": "Error while fetching profiles", "users.table.error.groups": "Error while fetching groups", "users.table.groups": "Groups", diff --git a/src/translations/fr.json b/src/translations/fr.json index 80ca69f..a9d0a23 100644 --- a/src/translations/fr.json +++ b/src/translations/fr.json @@ -41,7 +41,6 @@ "users.table.error.update": "Erreur pendant la modification de l'utilisateur", "users.table.toolbar.add.label": "Ajouter un utilisateur", "users.table.toolbar.add": "Ajouter utilisateur", - "users.table.profile.none": "", "users.table.error.profiles": "Erreur pendant la récupération des profils", "users.table.error.groups": "Erreur pendant la récupération des groupes", "users.table.groups": "Groupes", From bc72bedc3c24222a66ecb00af9bfdd1add18022a Mon Sep 17 00:00:00 2001 From: David BRAQUART Date: Mon, 28 Apr 2025 10:44:02 +0200 Subject: [PATCH 03/18] refacto group page, missing users in edit dialog Signed-off-by: David BRAQUART --- src/pages/groups/groups-page.tsx | 61 +++++++ .../{GroupsPage.tsx => groups-table.tsx} | 167 +++++++++--------- src/pages/groups/index.ts | 2 +- .../group-modification-dialog.tsx | 116 ++++++++++++ .../modification/group-modification-form.tsx | 38 ++++ .../modification/user-modification-dialog.tsx | 7 +- src/translations/en.json | 2 + src/translations/fr.json | 2 + 8 files changed, 309 insertions(+), 86 deletions(-) create mode 100644 src/pages/groups/groups-page.tsx rename src/pages/groups/{GroupsPage.tsx => groups-table.tsx} (59%) create mode 100644 src/pages/groups/modification/group-modification-dialog.tsx create mode 100644 src/pages/groups/modification/group-modification-form.tsx diff --git a/src/pages/groups/groups-page.tsx b/src/pages/groups/groups-page.tsx new file mode 100644 index 0000000..c3e3ba0 --- /dev/null +++ b/src/pages/groups/groups-page.tsx @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2024, 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 { FunctionComponent, useCallback, useRef, useState } from 'react'; +import { Grid } from '@mui/material'; +import { GridTableRef } from '../../components/Grid'; +import { GroupInfos } from '../../services'; +import { RowClickedEvent } from 'ag-grid-community'; +import GroupModificationDialog from './modification/group-modification-dialog'; +import GroupsTable from './groups-table'; +import AddGroupDialog from './add-group-dialog'; + +const GroupsPage: FunctionComponent = () => { + const gridRef = useRef>(null); + const gridContext = gridRef.current?.context; + const [openGroupModificationDialog, setOpenGroupModificationDialog] = useState(false); + const [editingGroup, setEditingGroup] = useState(); + + const [openAddGroupDialog, setOpenAddGroupDialog] = useState(false); + + const handleCloseGroupModificationDialog = useCallback(() => { + setOpenGroupModificationDialog(false); + setEditingGroup(undefined); + }, []); + + const handleUpdateGroupModificationDialog = useCallback(() => { + gridContext?.refresh?.(); + handleCloseGroupModificationDialog(); + }, [gridContext, handleCloseGroupModificationDialog]); + + const onRowClicked = useCallback((event: RowClickedEvent) => { + if (event.data) { + setEditingGroup(event.data); + setOpenGroupModificationDialog(true); + } + }, []); + + return ( + + + + + + + + ); +}; +export default GroupsPage; diff --git a/src/pages/groups/GroupsPage.tsx b/src/pages/groups/groups-table.tsx similarity index 59% rename from src/pages/groups/GroupsPage.tsx rename to src/pages/groups/groups-table.tsx index 25f2a5f..f8f184b 100644 --- a/src/pages/groups/GroupsPage.tsx +++ b/src/pages/groups/groups-table.tsx @@ -1,36 +1,43 @@ -/* - * Copyright (c) 2025, RTE (http://www.rte-france.com) +/** + * Copyright (c) 2024, 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 { FunctionComponent, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { FunctionComponent, RefObject, useCallback, useEffect, useMemo, useState } from 'react'; import { useIntl } from 'react-intl'; -import { Grid } from '@mui/material'; import { GroupAdd } from '@mui/icons-material'; import { GridButton, GridButtonDelete, GridTable, GridTableRef } from '../../components/Grid'; -import { UserAdminSrv, GroupInfos, UserInfos, UpdateGroupInfos } from '../../services'; +import { GroupInfos, UpdateGroupInfos, UserAdminSrv, UserInfos } from '../../services'; +import { + ColDef, + GetRowIdParams, + ICellEditorParams, + RowClickedEvent, + SelectionChangedEvent, + TextFilterParams, +} from 'ag-grid-community'; import { useSnackMessage } from '@gridsuite/commons-ui'; -import { ColDef, GetRowIdParams, SelectionChangedEvent, TextFilterParams, ICellEditorParams } from 'ag-grid-community'; import DeleteConfirmationDialog from '../common/delete-confirmation-dialog'; -import MultiSelectEditorComponent from '../common/multi-select-editor-component'; +import { defaultColDef, defaultRowSelection } from '../common/table-config'; import MultiChipsRendererComponent from '../common/multi-chips-renderer-component'; +import MultiSelectEditorComponent from '../common/multi-select-editor-component'; import { UUID } from 'crypto'; -import { defaultColDef } from '../common/table-config'; -import AddGroupDialog from './add-group-dialog'; -function getRowId(params: GetRowIdParams): string { - return params.data.name; +export interface GroupsTableProps { + gridRef: RefObject>; + onRowClicked: (event: RowClickedEvent) => void; + setOpenAddGroupDialog: (open: boolean) => void; } -const GroupsPage: FunctionComponent = () => { +const GroupsTable: FunctionComponent = (props) => { const intl = useIntl(); const { snackError } = useSnackMessage(); - const gridRef = useRef>(null); - const gridContext = gridRef.current?.context; + + const [rowsSelection, setRowsSelection] = useState([]); + const [showDeletionDialog, setShowDeletionDialog] = useState(false); const [usersOptions, setUsersOptions] = useState([]); - const [openAddGroupDialog, setOpenAddGroupDialog] = useState(false); useEffect(() => { UserAdminSrv.fetchUsers() @@ -46,12 +53,43 @@ const GroupsPage: FunctionComponent = () => { ); }, [snackError]); + function getRowId(params: GetRowIdParams): string { + return params.data.name; + } + + const onSelectionChanged = useCallback( + (event: SelectionChangedEvent) => setRowsSelection(event.api.getSelectedRows() ?? []), + [setRowsSelection] + ); + + const onAddButton = useCallback(() => props.setOpenAddGroupDialog(true), [props]); + + const deleteGroups = useCallback((): Promise | undefined => { + let groupNames = rowsSelection.map((group) => group.name); + return UserAdminSrv.deleteGroups(groupNames) + .catch((error) => { + if (error.status === 422) { + snackError({ + headerId: 'groups.table.integrity.error.delete', + }); + } else { + snackError({ + messageTxt: error.message, + headerId: 'groups.table.error.delete', + }); + } + }) + .then(() => props.gridRef?.current?.context?.refresh?.()); + }, [props.gridRef, rowsSelection, snackError]); + + const deleteGroupsDisabled = useMemo(() => rowsSelection.length <= 0, [rowsSelection.length]); + const updateGroupCallback = useCallback( (id: UUID, name: string, users: string[]) => { const newData: UpdateGroupInfos = { id: id, name: name, - users: users, + users: [], }; UserAdminSrv.udpateGroup(newData) .catch((error) => @@ -60,9 +98,9 @@ const GroupsPage: FunctionComponent = () => { headerId: 'groups.table.error.update', }) ) - .then(() => gridContext?.refresh?.()); + .then(() => props.gridRef?.current?.context?.refresh?.()); }, - [gridContext, snackError] + [props.gridRef, snackError] ); const columns = useMemo( @@ -114,70 +152,37 @@ const GroupsPage: FunctionComponent = () => { [intl, usersOptions, updateGroupCallback] ); - const [rowsSelection, setRowsSelection] = useState([]); - const deleteGroups = useCallback((): Promise | undefined => { - let groupNames = rowsSelection.map((group) => group.name); - return UserAdminSrv.deleteGroups(groupNames) - .catch((error) => { - if (error.status === 422) { - snackError({ - headerId: 'groups.table.integrity.error.delete', - }); - } else { - snackError({ - messageTxt: error.message, - headerId: 'groups.table.error.delete', - }); - } - }) - .then(() => gridContext?.refresh?.()); - }, [gridContext, rowsSelection, snackError]); - const deleteGroupsDisabled = useMemo(() => rowsSelection.length <= 0, [rowsSelection.length]); - const [showDeletionDialog, setShowDeletionDialog] = useState(false); - return ( - - - - ref={gridRef} - dataLoader={UserAdminSrv.fetchGroups} - columnDefs={columns} - defaultColDef={defaultColDef} - stopEditingWhenCellsLoseFocus={true} - gridId="table-groups" - getRowId={getRowId} - rowSelection={{ - mode: 'multiRow', - enableClickSelection: false, - checkboxes: true, - headerCheckbox: true, - hideDisabledCheckboxes: false, - }} - onSelectionChanged={useCallback( - (event: SelectionChangedEvent) => - setRowsSelection(event.api.getSelectedRows() ?? []), - [] - )} - > - } - color="primary" - onClick={useCallback(() => setOpenAddGroupDialog(true), [])} - /> - setShowDeletionDialog(true)} disabled={deleteGroupsDisabled} /> - - - group.name)} - deleteFunc={deleteGroups} + <> + + ref={props.gridRef} + dataLoader={UserAdminSrv.fetchGroups} + columnDefs={columns} + defaultColDef={defaultColDef} + gridId="table-groups" + getRowId={getRowId} + rowSelection={defaultRowSelection} + onRowClicked={props.onRowClicked} + onSelectionChanged={onSelectionChanged} + > + } + color="primary" + onClick={onAddButton} /> - - + setShowDeletionDialog(true)} disabled={deleteGroupsDisabled} /> + + + user.name)} + deleteFunc={deleteGroups} + /> + ); }; -export default GroupsPage; +export default GroupsTable; diff --git a/src/pages/groups/index.ts b/src/pages/groups/index.ts index 5281001..0dde7c2 100644 --- a/src/pages/groups/index.ts +++ b/src/pages/groups/index.ts @@ -5,4 +5,4 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -export { default as Groups } from './GroupsPage'; +export { default as Groups } from './groups-page'; diff --git a/src/pages/groups/modification/group-modification-dialog.tsx b/src/pages/groups/modification/group-modification-dialog.tsx new file mode 100644 index 0000000..54e13de --- /dev/null +++ b/src/pages/groups/modification/group-modification-dialog.tsx @@ -0,0 +1,116 @@ +/* + * 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 ProfileModificationForm, { + GROUP_NAME, + GroupModificationFormType, + GroupModificationSchema, +} from './group-modification-form'; +import { yupResolver } from '@hookform/resolvers/yup'; +import { useForm } from 'react-hook-form'; +import { FunctionComponent, useCallback, useEffect, useMemo, useState } from 'react'; +import { CustomMuiDialog, FetchStatus, useSnackMessage } from '@gridsuite/commons-ui'; +import { GroupInfos, UpdateGroupInfos, UserAdminSrv, UserInfos } from '../../../services'; + +interface GroupModificationDialogProps { + groupInfos: GroupInfos | undefined; + open: boolean; + onClose: () => void; + onUpdate: () => void; +} + +const GroupModificationDialog: FunctionComponent = ({ + groupInfos, + open, + onClose, + onUpdate, +}) => { + const { snackError } = useSnackMessage(); + const formMethods = useForm({ + resolver: yupResolver(GroupModificationSchema), + }); + const { reset } = formMethods; + const [userOptions, setUserOptions] = useState([]); + const [dataFetchStatus, setDataFetchStatus] = useState(FetchStatus.IDLE); + + useEffect(() => { + // fetch available profiles + if (groupInfos && open) { + setDataFetchStatus(FetchStatus.FETCHING); + UserAdminSrv.fetchUsers() + .then((allUsers: UserInfos[]) => { + setDataFetchStatus(FetchStatus.FETCH_SUCCESS); + setUserOptions( + allUsers?.map((p) => p.sub).sort((a: string, b: string) => a.localeCompare(b)) || [] + ); + }) + .catch((error) => { + setDataFetchStatus(FetchStatus.FETCH_ERROR); + snackError({ + messageTxt: error.message, + headerId: 'groups.table.error.users', + }); + }); + } + }, [open, snackError, groupInfos]); + + useEffect(() => { + if (groupInfos && open) { + reset({ + [GROUP_NAME]: groupInfos.name, + }); + } + }, [groupInfos, open, reset]); + + const onDialogClose = useCallback(() => { + setDataFetchStatus(FetchStatus.IDLE); + onClose(); + }, [onClose]); + + const onSubmit = useCallback( + (groupFormData: GroupModificationFormType) => { + if (groupInfos?.id) { + const newData: UpdateGroupInfos = { + id: groupInfos.id, + name: groupFormData.name, + users: [], + }; + UserAdminSrv.udpateGroup(newData) + .catch((error) => + snackError({ + messageTxt: error.message, + headerId: 'groups.table.error.update', + }) + ) + .then(() => { + onUpdate(); + }); + } + }, + [onUpdate, snackError, groupInfos] + ); + + const isDataReady = useMemo(() => dataFetchStatus === FetchStatus.FETCH_SUCCESS, [dataFetchStatus]); + const isDataFetching = useMemo(() => dataFetchStatus === FetchStatus.FETCHING, [dataFetchStatus]); + + return ( + + {isDataReady && } + + ); +}; + +export default GroupModificationDialog; diff --git a/src/pages/groups/modification/group-modification-form.tsx b/src/pages/groups/modification/group-modification-form.tsx new file mode 100644 index 0000000..80f72b0 --- /dev/null +++ b/src/pages/groups/modification/group-modification-form.tsx @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2024, 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 { TextInput } from '@gridsuite/commons-ui'; +import Grid from '@mui/material/Grid'; +import React, { FunctionComponent } from 'react'; +import yup from '../../../utils/yup-config'; + +export const GROUP_NAME = 'name'; + +export const GroupModificationSchema = yup + .object() + .shape({ + [GROUP_NAME]: yup.string().trim().required('nameEmpty'), + }) + .required(); + +export type GroupModificationFormType = yup.InferType; + +interface GroupModificationFormProps { + usersOptions: string[]; +} + +const GroupModificationForm: FunctionComponent = ({ usersOptions }) => { + return ( + + + + + + ); +}; + +export default GroupModificationForm; diff --git a/src/pages/users/modification/user-modification-dialog.tsx b/src/pages/users/modification/user-modification-dialog.tsx index 746cfbc..3bf4b5d 100644 --- a/src/pages/users/modification/user-modification-dialog.tsx +++ b/src/pages/users/modification/user-modification-dialog.tsx @@ -35,7 +35,7 @@ const UserModificationDialog: FunctionComponent = ( resolver: yupResolver(UserModificationSchema), }); const { reset } = formMethods; - const [profileOptions, setprofileOptions] = useState([]); + const [profileOptions, setProfileOptions] = useState([]); const [dataFetchStatus, setDataFetchStatus] = useState(FetchStatus.IDLE); useEffect(() => { @@ -45,7 +45,7 @@ const UserModificationDialog: FunctionComponent = ( UserAdminSrv.fetchProfilesWithoutValidityCheck() .then((allProfiles: UserProfile[]) => { setDataFetchStatus(FetchStatus.FETCH_SUCCESS); - setprofileOptions( + setProfileOptions( allProfiles.map((p) => p.name).sort((a: string, b: string) => a.localeCompare(b)) ); }) @@ -76,7 +76,6 @@ const UserModificationDialog: FunctionComponent = ( const onSubmit = useCallback( (userFormData: UserModificationFormType) => { if (userInfos) { - console.log('DBG DBR', userFormData, userInfos); const newData: UpdateUserInfos = { sub: userInfos.sub, // sub cannot be changed, it is a PK in database isAdmin: userInfos.isAdmin, // cannot be changed for now @@ -108,7 +107,7 @@ const UserModificationDialog: FunctionComponent = ( onSave={onSubmit} formSchema={UserModificationSchema} formMethods={formMethods} - titleId={'profiles.form.modification.title'} + titleId={'users.form.modification.title'} removeOptional={true} isDataFetching={isDataFetching} > diff --git a/src/translations/en.json b/src/translations/en.json index 16d3a02..86db9ac 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -47,6 +47,7 @@ "users.form.title": "Add a user", "users.form.content": "Please fill in new user data.", "users.form.field.username.label": "User ID", + "users.form.modification.title": "Edit user", "form.delete.dialog.user": "user", "form.delete.dialog.profile": "profile", @@ -102,6 +103,7 @@ "groups.form.title": "Add a group", "groups.form.content": "Please fill in new group data.", "groups.form.field.group.label": "Group ID", + "groups.form.modification.title": "Edit group", "linked.path.display.noLink": "no configuration selected.", "linked.path.display.invalidLink": "invalid configurations link." diff --git a/src/translations/fr.json b/src/translations/fr.json index a9d0a23..73cf165 100644 --- a/src/translations/fr.json +++ b/src/translations/fr.json @@ -48,6 +48,7 @@ "users.form.title": "Ajouter un utilisateur", "users.form.content": "Veuillez renseigner les informations de l'utilisateur.", "users.form.field.username.label": "ID utilisateur", + "users.form.modification.title": "Modifier utilisateur", "form.delete.dialog.user": "utilisateur", "form.delete.dialog.profile": "profil", @@ -103,6 +104,7 @@ "groups.form.title": "Ajouter un groupe", "groups.form.content": "Veuillez renseigner les informations du groupe.", "groups.form.field.group.label": "ID groupe", + "groups.form.modification.title": "Modifier groupe", "linked.path.display.noLink": "pas de configuration selectionnée.", "linked.path.display.invalidLink": "lien vers configurations invalide." From a00e272565ed36cf69a9cb960cc7e69394ab31f2 Mon Sep 17 00:00:00 2001 From: David BRAQUART Date: Mon, 28 Apr 2025 14:51:31 +0200 Subject: [PATCH 04/18] user dialog with group table, with style ok Signed-off-by: David BRAQUART --- src/pages/common/table-selection.tsx | 105 ++++++++++++++++++ .../group-modification-dialog.tsx | 1 - .../modification/user-modification-dialog.tsx | 70 ++++++++++-- .../modification/user-modification-form.tsx | 23 +++- src/pages/users/users-page.tsx | 28 +++-- src/services/user-admin.ts | 2 +- 6 files changed, 204 insertions(+), 25 deletions(-) create mode 100644 src/pages/common/table-selection.tsx diff --git a/src/pages/common/table-selection.tsx b/src/pages/common/table-selection.tsx new file mode 100644 index 0000000..4bdb5ac --- /dev/null +++ b/src/pages/common/table-selection.tsx @@ -0,0 +1,105 @@ +/** + * Copyright (c) 2024, 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 { FunctionComponent, useCallback, useMemo, useRef, useState } from 'react'; +import { FormattedMessage, useIntl } from 'react-intl'; +import { CustomAGGrid } from '@gridsuite/commons-ui'; +import { Grid, Typography } from '@mui/material'; +import { AgGridReact } from 'ag-grid-react'; +import { ColDef, GetRowIdParams, GridReadyEvent, RowSelectionOptions } from 'ag-grid-community'; +import { defaultColDef } from './table-config'; + +export interface TableSelectionProps { + itemNameTranslationKey: string; + tableItems: string[]; + tableSelectedItems?: string[]; + onSelectionChanged: (selectedItems: string[]) => void; +} + +const TableSelection: FunctionComponent = (props) => { + const intl = useIntl(); + const [selectedRowsLength, setSelectedRowsLength] = useState(0); + const gridRef = useRef(null); + + const handleEquipmentSelectionChanged = useCallback(() => { + const selectedRows = gridRef.current?.api.getSelectedRows(); + if (selectedRows == null) { + setSelectedRowsLength(0); + props.onSelectionChanged([]); + } else { + setSelectedRowsLength(selectedRows.length); + props.onSelectionChanged(selectedRows.map((r) => r.id)); + } + }, [props]); + + const rowData = useMemo(() => { + return props.tableItems.map((str) => ({ id: str })); + }, [props.tableItems]); + + const rowSelection: RowSelectionOptions = { + mode: 'multiRow', + enableClickSelection: false, + checkboxes: true, + headerCheckbox: true, + hideDisabledCheckboxes: false, + }; + + const columnDefs = useMemo( + (): ColDef[] => [ + { + field: 'id', + filter: true, + sortable: true, + minWidth: 80, + headerName: intl.formatMessage({ + id: props.itemNameTranslationKey, + }), + }, + ], + [intl, props.itemNameTranslationKey] + ); + + function getRowId(params: GetRowIdParams): string { + return params.data.id; + } + + const onGridReady = useCallback( + ({ api }: GridReadyEvent) => { + api?.forEachNode((n) => { + if (props.tableSelectedItems !== undefined && n.id && props.tableSelectedItems.includes(n.id)) { + n.setSelected(true); + } + }); + }, + [props.tableSelectedItems] + ); + + return ( + + + + + {` (${selectedRowsLength} / ${rowData?.length ?? 0})`} + + + + + + + ); +}; +export default TableSelection; diff --git a/src/pages/groups/modification/group-modification-dialog.tsx b/src/pages/groups/modification/group-modification-dialog.tsx index 54e13de..553913d 100644 --- a/src/pages/groups/modification/group-modification-dialog.tsx +++ b/src/pages/groups/modification/group-modification-dialog.tsx @@ -38,7 +38,6 @@ const GroupModificationDialog: FunctionComponent = const [dataFetchStatus, setDataFetchStatus] = useState(FetchStatus.IDLE); useEffect(() => { - // fetch available profiles if (groupInfos && open) { setDataFetchStatus(FetchStatus.FETCHING); UserAdminSrv.fetchUsers() diff --git a/src/pages/users/modification/user-modification-dialog.tsx b/src/pages/users/modification/user-modification-dialog.tsx index 3bf4b5d..f64de55 100644 --- a/src/pages/users/modification/user-modification-dialog.tsx +++ b/src/pages/users/modification/user-modification-dialog.tsx @@ -5,9 +5,10 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import ProfileModificationForm, { +import UserModificationForm, { USER_NAME, USER_PROFILE_NAME, + USER_SELECTED_GROUPS, UserModificationFormType, UserModificationSchema, } from './user-modification-form'; @@ -15,7 +16,7 @@ import { yupResolver } from '@hookform/resolvers/yup'; import { useForm } from 'react-hook-form'; import { FunctionComponent, useCallback, useEffect, useMemo, useState } from 'react'; import { CustomMuiDialog, FetchStatus, useSnackMessage } from '@gridsuite/commons-ui'; -import { UpdateUserInfos, UserAdminSrv, UserInfos, UserProfile } from '../../../services'; +import { GroupInfos, UpdateUserInfos, UserAdminSrv, UserInfos, UserProfile } from '../../../services'; interface UserModificationDialogProps { userInfos: UserInfos | undefined; @@ -34,36 +35,64 @@ const UserModificationDialog: FunctionComponent = ( const formMethods = useForm({ resolver: yupResolver(UserModificationSchema), }); - const { reset } = formMethods; + const { reset, setValue } = formMethods; const [profileOptions, setProfileOptions] = useState([]); + const [groupOptions, setGroupOptions] = useState([]); + const [selectedGroups, setSelectedGroups] = useState([]); const [dataFetchStatus, setDataFetchStatus] = useState(FetchStatus.IDLE); useEffect(() => { - // fetch available profiles if (userInfos && open) { + setSelectedGroups(userInfos.groups); + // fetch profile & groups setDataFetchStatus(FetchStatus.FETCHING); - UserAdminSrv.fetchProfilesWithoutValidityCheck() + let fetcherPromises: Promise[] = []; + const profilePromise = UserAdminSrv.fetchProfilesWithoutValidityCheck(); + const groupPromise = UserAdminSrv.fetchGroups(); + fetcherPromises.push(profilePromise); + fetcherPromises.push(groupPromise); + + profilePromise .then((allProfiles: UserProfile[]) => { - setDataFetchStatus(FetchStatus.FETCH_SUCCESS); setProfileOptions( allProfiles.map((p) => p.name).sort((a: string, b: string) => a.localeCompare(b)) ); }) .catch((error) => { - setDataFetchStatus(FetchStatus.FETCH_ERROR); snackError({ messageTxt: error.message, headerId: 'users.table.error.profiles', }); }); + + groupPromise + .then((allGroups: GroupInfos[]) => { + setGroupOptions(allGroups.map((g) => g.name).sort((a: string, b: string) => a.localeCompare(b))); + }) + .catch((error) => { + snackError({ + messageTxt: error.message, + headerId: 'users.table.error.groups', + }); + }); + + Promise.all(fetcherPromises) + .then(() => { + setDataFetchStatus(FetchStatus.FETCH_SUCCESS); + }) + .catch(() => { + setDataFetchStatus(FetchStatus.FETCH_ERROR); + }); } }, [open, snackError, userInfos]); useEffect(() => { if (userInfos && open) { + const sortedGroups = Array.from(userInfos.groups).sort((a, b) => a.localeCompare(b)); reset({ [USER_NAME]: userInfos.sub, [USER_PROFILE_NAME]: userInfos.profileName, + [USER_SELECTED_GROUPS]: JSON.stringify(sortedGroups), // only used to dirty the form }); } }, [userInfos, open, reset]); @@ -73,6 +102,19 @@ const UserModificationDialog: FunctionComponent = ( onClose(); }, [onClose]); + const onSelectionChanged = useCallback( + (selectedItems: string[]) => { + if (userInfos) { + setSelectedGroups(selectedItems); + selectedItems.sort((a, b) => a.localeCompare(b)); + setValue(USER_SELECTED_GROUPS, JSON.stringify(selectedItems), { + shouldDirty: true, + }); + } + }, + [setValue, userInfos] + ); + const onSubmit = useCallback( (userFormData: UserModificationFormType) => { if (userInfos) { @@ -80,7 +122,7 @@ const UserModificationDialog: FunctionComponent = ( sub: userInfos.sub, // sub cannot be changed, it is a PK in database isAdmin: userInfos.isAdmin, // cannot be changed for now profileName: userFormData.profileName ?? undefined, - groups: [], + groups: selectedGroups, }; UserAdminSrv.udpateUser(newData) .catch((error) => @@ -94,7 +136,7 @@ const UserModificationDialog: FunctionComponent = ( }); } }, - [onUpdate, snackError, userInfos] + [onUpdate, selectedGroups, snackError, userInfos] ); const isDataReady = useMemo(() => dataFetchStatus === FetchStatus.FETCH_SUCCESS, [dataFetchStatus]); @@ -110,8 +152,16 @@ const UserModificationDialog: FunctionComponent = ( titleId={'users.form.modification.title'} removeOptional={true} isDataFetching={isDataFetching} + unscrollableFullHeight > - {isDataReady && } + {isDataReady && ( + + )} ); }; diff --git a/src/pages/users/modification/user-modification-form.tsx b/src/pages/users/modification/user-modification-form.tsx index bc0a13b..fc33ae2 100644 --- a/src/pages/users/modification/user-modification-form.tsx +++ b/src/pages/users/modification/user-modification-form.tsx @@ -9,15 +9,18 @@ import { AutocompleteInput, TextInput } from '@gridsuite/commons-ui'; import Grid from '@mui/material/Grid'; import React, { FunctionComponent } from 'react'; import yup from '../../../utils/yup-config'; +import TableSelection from '../../common/table-selection'; export const USER_NAME = 'sub'; export const USER_PROFILE_NAME = 'profileName'; +export const USER_SELECTED_GROUPS = 'groups'; export const UserModificationSchema = yup .object() .shape({ [USER_NAME]: yup.string().trim().required('nameEmpty'), [USER_PROFILE_NAME]: yup.string().nullable(), + [USER_SELECTED_GROUPS]: yup.string().nullable(), }) .required(); @@ -25,11 +28,19 @@ export type UserModificationFormType = yup.InferType void; } -const UserModificationForm: FunctionComponent = ({ profileOptions }) => { +const UserModificationForm: FunctionComponent = ({ + profileOptions, + groupOptions, + selectedGroups, + onSelectionChanged, +}) => { return ( - + = ({ pr options={profileOptions} /> + + + ); }; diff --git a/src/pages/users/users-page.tsx b/src/pages/users/users-page.tsx index 8d6294b..f52d9e0 100644 --- a/src/pages/users/users-page.tsx +++ b/src/pages/users/users-page.tsx @@ -40,18 +40,24 @@ const UsersPage: FunctionComponent = () => { }, []); return ( - - - - - + <> + + + + - + + + ); }; export default UsersPage; diff --git a/src/services/user-admin.ts b/src/services/user-admin.ts index fcd2636..6937b85 100644 --- a/src/services/user-admin.ts +++ b/src/services/user-admin.ts @@ -42,7 +42,7 @@ export type UserInfos = { sub: string; profileName: string; isAdmin: boolean; - groups: GroupInfos[]; + groups: string[]; }; export function fetchUsers(): Promise { From 8e9b404aeaf8520cad7192facfdda2817cf1fe0d Mon Sep 17 00:00:00 2001 From: David BRAQUART Date: Tue, 29 Apr 2025 18:50:40 +0200 Subject: [PATCH 05/18] group dialog with user table Signed-off-by: David BRAQUART --- src/pages/common/table-selection.tsx | 22 ++----- src/pages/groups/groups-page.tsx | 32 +++++----- src/pages/groups/groups-table.tsx | 61 ++----------------- .../group-modification-dialog.tsx | 44 +++++++++---- .../modification/group-modification-form.tsx | 21 ++++++- .../modification/user-modification-dialog.tsx | 39 +++++------- .../modification/user-modification-form.tsx | 2 +- src/services/user-admin.ts | 2 +- 8 files changed, 94 insertions(+), 129 deletions(-) diff --git a/src/pages/common/table-selection.tsx b/src/pages/common/table-selection.tsx index 4bdb5ac..531707f 100644 --- a/src/pages/common/table-selection.tsx +++ b/src/pages/common/table-selection.tsx @@ -6,12 +6,12 @@ */ import { FunctionComponent, useCallback, useMemo, useRef, useState } from 'react'; -import { FormattedMessage, useIntl } from 'react-intl'; +import { FormattedMessage } from 'react-intl'; import { CustomAGGrid } from '@gridsuite/commons-ui'; import { Grid, Typography } from '@mui/material'; import { AgGridReact } from 'ag-grid-react'; -import { ColDef, GetRowIdParams, GridReadyEvent, RowSelectionOptions } from 'ag-grid-community'; -import { defaultColDef } from './table-config'; +import { ColDef, GetRowIdParams, GridReadyEvent } from 'ag-grid-community'; +import { defaultColDef, defaultRowSelection } from './table-config'; export interface TableSelectionProps { itemNameTranslationKey: string; @@ -21,7 +21,6 @@ export interface TableSelectionProps { } const TableSelection: FunctionComponent = (props) => { - const intl = useIntl(); const [selectedRowsLength, setSelectedRowsLength] = useState(0); const gridRef = useRef(null); @@ -40,14 +39,6 @@ const TableSelection: FunctionComponent = (props) => { return props.tableItems.map((str) => ({ id: str })); }, [props.tableItems]); - const rowSelection: RowSelectionOptions = { - mode: 'multiRow', - enableClickSelection: false, - checkboxes: true, - headerCheckbox: true, - hideDisabledCheckboxes: false, - }; - const columnDefs = useMemo( (): ColDef[] => [ { @@ -55,12 +46,9 @@ const TableSelection: FunctionComponent = (props) => { filter: true, sortable: true, minWidth: 80, - headerName: intl.formatMessage({ - id: props.itemNameTranslationKey, - }), }, ], - [intl, props.itemNameTranslationKey] + [] ); function getRowId(params: GetRowIdParams): string { @@ -93,8 +81,8 @@ const TableSelection: FunctionComponent = (props) => { rowData={rowData} columnDefs={columnDefs} defaultColDef={defaultColDef} + rowSelection={defaultRowSelection} getRowId={getRowId} - rowSelection={rowSelection} onSelectionChanged={handleEquipmentSelectionChanged} onGridReady={onGridReady} /> diff --git a/src/pages/groups/groups-page.tsx b/src/pages/groups/groups-page.tsx index c3e3ba0..0006f61 100644 --- a/src/pages/groups/groups-page.tsx +++ b/src/pages/groups/groups-page.tsx @@ -40,22 +40,24 @@ const GroupsPage: FunctionComponent = () => { }, []); return ( - - - - - + <> + + + + - + + + ); }; export default GroupsPage; diff --git a/src/pages/groups/groups-table.tsx b/src/pages/groups/groups-table.tsx index f8f184b..1f5c9d9 100644 --- a/src/pages/groups/groups-table.tsx +++ b/src/pages/groups/groups-table.tsx @@ -5,25 +5,16 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { FunctionComponent, RefObject, useCallback, useEffect, useMemo, useState } from 'react'; +import { FunctionComponent, RefObject, useCallback, useMemo, useState } from 'react'; import { useIntl } from 'react-intl'; import { GroupAdd } from '@mui/icons-material'; import { GridButton, GridButtonDelete, GridTable, GridTableRef } from '../../components/Grid'; -import { GroupInfos, UpdateGroupInfos, UserAdminSrv, UserInfos } from '../../services'; -import { - ColDef, - GetRowIdParams, - ICellEditorParams, - RowClickedEvent, - SelectionChangedEvent, - TextFilterParams, -} from 'ag-grid-community'; +import { GroupInfos, UserAdminSrv, UserInfos } from '../../services'; +import { ColDef, GetRowIdParams, RowClickedEvent, SelectionChangedEvent, TextFilterParams } from 'ag-grid-community'; import { useSnackMessage } from '@gridsuite/commons-ui'; import DeleteConfirmationDialog from '../common/delete-confirmation-dialog'; import { defaultColDef, defaultRowSelection } from '../common/table-config'; import MultiChipsRendererComponent from '../common/multi-chips-renderer-component'; -import MultiSelectEditorComponent from '../common/multi-select-editor-component'; -import { UUID } from 'crypto'; export interface GroupsTableProps { gridRef: RefObject>; @@ -37,21 +28,6 @@ const GroupsTable: FunctionComponent = (props) => { const [rowsSelection, setRowsSelection] = useState([]); const [showDeletionDialog, setShowDeletionDialog] = useState(false); - const [usersOptions, setUsersOptions] = useState([]); - - useEffect(() => { - UserAdminSrv.fetchUsers() - .then((allUsers: UserInfos[]) => { - const users = allUsers?.map((u) => u.sub) || []; - setUsersOptions(users); - }) - .catch((error) => - snackError({ - messageTxt: error.message, - headerId: 'groups.table.error.users', - }) - ); - }, [snackError]); function getRowId(params: GetRowIdParams): string { return params.data.name; @@ -84,25 +60,6 @@ const GroupsTable: FunctionComponent = (props) => { const deleteGroupsDisabled = useMemo(() => rowsSelection.length <= 0, [rowsSelection.length]); - const updateGroupCallback = useCallback( - (id: UUID, name: string, users: string[]) => { - const newData: UpdateGroupInfos = { - id: id, - name: name, - users: [], - }; - UserAdminSrv.udpateGroup(newData) - .catch((error) => - snackError({ - messageTxt: error.message, - headerId: 'groups.table.error.update', - }) - ) - .then(() => props.gridRef?.current?.context?.refresh?.()); - }, - [props.gridRef, snackError] - ); - const columns = useMemo( (): ColDef[] => [ { @@ -136,20 +93,10 @@ const GroupsTable: FunctionComponent = (props) => { caseSensitive: false, trimInput: true, } as TextFilterParams, - editable: true, cellRenderer: MultiChipsRendererComponent, - cellEditor: MultiSelectEditorComponent, - cellEditorParams: (params: ICellEditorParams) => ({ - options: usersOptions, - setValue: (values: string[]) => { - if (params.data?.id) { - updateGroupCallback(params.data.id, params.data.name, values); - } - }, - }), }, ], - [intl, usersOptions, updateGroupCallback] + [intl] ); return ( diff --git a/src/pages/groups/modification/group-modification-dialog.tsx b/src/pages/groups/modification/group-modification-dialog.tsx index 553913d..7fbc4d4 100644 --- a/src/pages/groups/modification/group-modification-dialog.tsx +++ b/src/pages/groups/modification/group-modification-dialog.tsx @@ -5,10 +5,11 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import ProfileModificationForm, { +import GroupModificationForm, { GROUP_NAME, GroupModificationFormType, GroupModificationSchema, + SELECTED_USERS, } from './group-modification-form'; import { yupResolver } from '@hookform/resolvers/yup'; import { useForm } from 'react-hook-form'; @@ -33,12 +34,21 @@ const GroupModificationDialog: FunctionComponent = const formMethods = useForm({ resolver: yupResolver(GroupModificationSchema), }); - const { reset } = formMethods; + const { reset, setValue } = formMethods; const [userOptions, setUserOptions] = useState([]); + const [selectedUsers, setSelectedUsers] = useState([]); const [dataFetchStatus, setDataFetchStatus] = useState(FetchStatus.IDLE); useEffect(() => { if (groupInfos && open) { + const sortedUsers = Array.from(groupInfos.users).sort((a, b) => a.localeCompare(b)); + reset({ + [GROUP_NAME]: groupInfos.name, + [SELECTED_USERS]: JSON.stringify(sortedUsers), // only used to dirty the form + }); + setSelectedUsers(groupInfos.users); + + // fetch all users setDataFetchStatus(FetchStatus.FETCHING); UserAdminSrv.fetchUsers() .then((allUsers: UserInfos[]) => { @@ -55,15 +65,18 @@ const GroupModificationDialog: FunctionComponent = }); }); } - }, [open, snackError, groupInfos]); + }, [open, snackError, groupInfos, reset]); - useEffect(() => { - if (groupInfos && open) { - reset({ - [GROUP_NAME]: groupInfos.name, + const onSelectionChanged = useCallback( + (selectedItems: string[]) => { + setSelectedUsers(selectedItems); + selectedItems.sort((a, b) => a.localeCompare(b)); + setValue(SELECTED_USERS, JSON.stringify(selectedItems), { + shouldDirty: true, }); - } - }, [groupInfos, open, reset]); + }, + [setValue] + ); const onDialogClose = useCallback(() => { setDataFetchStatus(FetchStatus.IDLE); @@ -76,7 +89,7 @@ const GroupModificationDialog: FunctionComponent = const newData: UpdateGroupInfos = { id: groupInfos.id, name: groupFormData.name, - users: [], + users: selectedUsers, }; UserAdminSrv.udpateGroup(newData) .catch((error) => @@ -90,7 +103,7 @@ const GroupModificationDialog: FunctionComponent = }); } }, - [onUpdate, snackError, groupInfos] + [groupInfos?.id, selectedUsers, snackError, onUpdate] ); const isDataReady = useMemo(() => dataFetchStatus === FetchStatus.FETCH_SUCCESS, [dataFetchStatus]); @@ -106,8 +119,15 @@ const GroupModificationDialog: FunctionComponent = titleId={'groups.form.modification.title'} removeOptional={true} isDataFetching={isDataFetching} + unscrollableFullHeight > - {isDataReady && } + {isDataReady && ( + + )} ); }; diff --git a/src/pages/groups/modification/group-modification-form.tsx b/src/pages/groups/modification/group-modification-form.tsx index 80f72b0..1eff460 100644 --- a/src/pages/groups/modification/group-modification-form.tsx +++ b/src/pages/groups/modification/group-modification-form.tsx @@ -9,13 +9,16 @@ import { TextInput } from '@gridsuite/commons-ui'; import Grid from '@mui/material/Grid'; import React, { FunctionComponent } from 'react'; import yup from '../../../utils/yup-config'; +import TableSelection from 'pages/common/table-selection'; export const GROUP_NAME = 'name'; +export const SELECTED_USERS = 'users'; export const GroupModificationSchema = yup .object() .shape({ [GROUP_NAME]: yup.string().trim().required('nameEmpty'), + [SELECTED_USERS]: yup.string().nullable(), }) .required(); @@ -23,14 +26,28 @@ export type GroupModificationFormType = yup.InferType void; } -const GroupModificationForm: FunctionComponent = ({ usersOptions }) => { +const GroupModificationForm: FunctionComponent = ({ + usersOptions, + selectedUsers, + onSelectionChanged, +}) => { return ( - + + + + ); }; diff --git a/src/pages/users/modification/user-modification-dialog.tsx b/src/pages/users/modification/user-modification-dialog.tsx index f64de55..5c43d79 100644 --- a/src/pages/users/modification/user-modification-dialog.tsx +++ b/src/pages/users/modification/user-modification-dialog.tsx @@ -43,14 +43,18 @@ const UserModificationDialog: FunctionComponent = ( useEffect(() => { if (userInfos && open) { + const sortedGroups = Array.from(userInfos.groups).sort((a, b) => a.localeCompare(b)); + reset({ + [USER_NAME]: userInfos.sub, + [USER_PROFILE_NAME]: userInfos.profileName, + [USER_SELECTED_GROUPS]: JSON.stringify(sortedGroups), // only used to dirty the form + }); setSelectedGroups(userInfos.groups); + // fetch profile & groups setDataFetchStatus(FetchStatus.FETCHING); - let fetcherPromises: Promise[] = []; const profilePromise = UserAdminSrv.fetchProfilesWithoutValidityCheck(); const groupPromise = UserAdminSrv.fetchGroups(); - fetcherPromises.push(profilePromise); - fetcherPromises.push(groupPromise); profilePromise .then((allProfiles: UserProfile[]) => { @@ -76,7 +80,7 @@ const UserModificationDialog: FunctionComponent = ( }); }); - Promise.all(fetcherPromises) + Promise.all([profilePromise, groupPromise]) .then(() => { setDataFetchStatus(FetchStatus.FETCH_SUCCESS); }) @@ -84,18 +88,7 @@ const UserModificationDialog: FunctionComponent = ( setDataFetchStatus(FetchStatus.FETCH_ERROR); }); } - }, [open, snackError, userInfos]); - - useEffect(() => { - if (userInfos && open) { - const sortedGroups = Array.from(userInfos.groups).sort((a, b) => a.localeCompare(b)); - reset({ - [USER_NAME]: userInfos.sub, - [USER_PROFILE_NAME]: userInfos.profileName, - [USER_SELECTED_GROUPS]: JSON.stringify(sortedGroups), // only used to dirty the form - }); - } - }, [userInfos, open, reset]); + }, [open, reset, snackError, userInfos]); const onDialogClose = useCallback(() => { setDataFetchStatus(FetchStatus.IDLE); @@ -104,15 +97,13 @@ const UserModificationDialog: FunctionComponent = ( const onSelectionChanged = useCallback( (selectedItems: string[]) => { - if (userInfos) { - setSelectedGroups(selectedItems); - selectedItems.sort((a, b) => a.localeCompare(b)); - setValue(USER_SELECTED_GROUPS, JSON.stringify(selectedItems), { - shouldDirty: true, - }); - } + setSelectedGroups(selectedItems); + selectedItems.sort((a, b) => a.localeCompare(b)); + setValue(USER_SELECTED_GROUPS, JSON.stringify(selectedItems), { + shouldDirty: true, + }); }, - [setValue, userInfos] + [setValue] ); const onSubmit = useCallback( diff --git a/src/pages/users/modification/user-modification-form.tsx b/src/pages/users/modification/user-modification-form.tsx index fc33ae2..28ee3e8 100644 --- a/src/pages/users/modification/user-modification-form.tsx +++ b/src/pages/users/modification/user-modification-form.tsx @@ -63,7 +63,7 @@ const UserModificationForm: FunctionComponent = ({ { export type GroupInfos = { id?: UUID; name: string; - users: UserInfos[]; + users: string[]; }; export function fetchGroups(): Promise { From 4880091d56850de255cf7aab32a0989ebcaae5df Mon Sep 17 00:00:00 2001 From: David BRAQUART Date: Wed, 30 Apr 2025 16:03:50 +0200 Subject: [PATCH 06/18] display users and groups as text Signed-off-by: David BRAQUART --- .../common/multi-chips-renderer-component.tsx | 26 ----- .../common/multi-select-editor-component.tsx | 52 --------- src/pages/common/table-selection.tsx | 1 + src/pages/groups/groups-table.tsx | 29 ++++- src/pages/profiles/profiles-page.tsx | 12 +- src/pages/profiles/profiles-table.tsx | 15 ++- src/pages/users/users-table.tsx | 108 ++++++------------ 7 files changed, 74 insertions(+), 169 deletions(-) delete mode 100644 src/pages/common/multi-chips-renderer-component.tsx delete mode 100644 src/pages/common/multi-select-editor-component.tsx diff --git a/src/pages/common/multi-chips-renderer-component.tsx b/src/pages/common/multi-chips-renderer-component.tsx deleted file mode 100644 index 8ace597..0000000 --- a/src/pages/common/multi-chips-renderer-component.tsx +++ /dev/null @@ -1,26 +0,0 @@ -/** - * 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 { Chip, Grid } from '@mui/material'; - -export interface MultiChipCellRendererProps { - value: string[]; -} - -const MultiChipsRendererComponent = (props: MultiChipCellRendererProps) => { - const values: string[] = props.value || []; - - return ( - - {values.map((val: string) => ( - - ))} - - ); -}; - -export default MultiChipsRendererComponent; diff --git a/src/pages/common/multi-select-editor-component.tsx b/src/pages/common/multi-select-editor-component.tsx deleted file mode 100644 index 2a050ae..0000000 --- a/src/pages/common/multi-select-editor-component.tsx +++ /dev/null @@ -1,52 +0,0 @@ -/** - * 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 { useState } from 'react'; -import { Autocomplete } from '@mui/lab'; -import { Chip, TextField } from '@mui/material'; - -export interface MultiSelectCellEditorProps { - value: string[]; - options: string[]; - setValue: (value: string[]) => void; -} - -const MultiSelectEditorComponent = (props: MultiSelectCellEditorProps) => { - const [selectedValues, setSelectedValues] = useState(props.value || []); - - const handleDelete = (label: string) => { - let newValues = selectedValues.filter((val) => val !== label); - setSelectedValues(newValues); - props.setValue(newValues); - }; - - return ( - { - setSelectedValues(newValue); - props.setValue(newValue); - }} - renderInput={(params) => } - renderTags={(val: string[]) => - val.map((label: string) => ( - handleDelete(label)} - /> - )) - } - /> - ); -}; - -export default MultiSelectEditorComponent; diff --git a/src/pages/common/table-selection.tsx b/src/pages/common/table-selection.tsx index 531707f..e5aa3e4 100644 --- a/src/pages/common/table-selection.tsx +++ b/src/pages/common/table-selection.tsx @@ -46,6 +46,7 @@ const TableSelection: FunctionComponent = (props) => { filter: true, sortable: true, minWidth: 80, + tooltipField: 'id', }, ], [] diff --git a/src/pages/groups/groups-table.tsx b/src/pages/groups/groups-table.tsx index 1f5c9d9..02f926f 100644 --- a/src/pages/groups/groups-table.tsx +++ b/src/pages/groups/groups-table.tsx @@ -10,11 +10,17 @@ import { useIntl } from 'react-intl'; import { GroupAdd } from '@mui/icons-material'; import { GridButton, GridButtonDelete, GridTable, GridTableRef } from '../../components/Grid'; import { GroupInfos, UserAdminSrv, UserInfos } from '../../services'; -import { ColDef, GetRowIdParams, RowClickedEvent, SelectionChangedEvent, TextFilterParams } from 'ag-grid-community'; +import { + ColDef, + GetRowIdParams, + ITooltipParams, + RowClickedEvent, + SelectionChangedEvent, + TextFilterParams +} from 'ag-grid-community'; import { useSnackMessage } from '@gridsuite/commons-ui'; import DeleteConfirmationDialog from '../common/delete-confirmation-dialog'; import { defaultColDef, defaultRowSelection } from '../common/table-config'; -import MultiChipsRendererComponent from '../common/multi-chips-renderer-component'; export interface GroupsTableProps { gridRef: RefObject>; @@ -65,7 +71,7 @@ const GroupsTable: FunctionComponent = (props) => { { field: 'name', cellDataType: 'text', - flex: 3, + flex: 2, lockVisible: true, filter: true, headerName: intl.formatMessage({ id: 'groups.table.id' }), @@ -81,7 +87,7 @@ const GroupsTable: FunctionComponent = (props) => { { field: 'users', cellDataType: 'text', - flex: 1, + flex: 3, filter: true, headerName: intl.formatMessage({ id: 'groups.table.users', @@ -93,7 +99,19 @@ const GroupsTable: FunctionComponent = (props) => { caseSensitive: false, trimInput: true, } as TextFilterParams, - cellRenderer: MultiChipsRendererComponent, + tooltipValueGetter: (p: ITooltipParams) => { + const items = p.value as string[]; + if (items == null || items.length === 0) { + return ''; + } + let userWord = intl.formatMessage({ + id: 'form.delete.dialog.user', + }); + if (items.length > 1) { + userWord = userWord.concat('s'); + } + return `${items.length} ${userWord}`; + }, }, ], [intl] @@ -106,6 +124,7 @@ const GroupsTable: FunctionComponent = (props) => { dataLoader={UserAdminSrv.fetchGroups} columnDefs={columns} defaultColDef={defaultColDef} + tooltipShowDelay={1000} gridId="table-groups" getRowId={getRowId} rowSelection={defaultRowSelection} diff --git a/src/pages/profiles/profiles-page.tsx b/src/pages/profiles/profiles-page.tsx index 512fdfe..ffac52d 100644 --- a/src/pages/profiles/profiles-page.tsx +++ b/src/pages/profiles/profiles-page.tsx @@ -43,18 +43,18 @@ const ProfilesPage: FunctionComponent = () => { return ( - + ); diff --git a/src/pages/profiles/profiles-table.tsx b/src/pages/profiles/profiles-table.tsx index ddf098c..6f1dfd4 100644 --- a/src/pages/profiles/profiles-table.tsx +++ b/src/pages/profiles/profiles-table.tsx @@ -75,7 +75,6 @@ const ProfilesTable: FunctionComponent = (props) => { caseSensitive: false, trimInput: true, } as TextFilterParams, - editable: false, }, { field: 'allLinksValid', @@ -85,13 +84,13 @@ const ProfilesTable: FunctionComponent = (props) => { alignItems: 'center', }), cellRenderer: (params: any) => { - return params.value == null ? ( - - ) : params.value ? ( - - ) : ( - - ); + if (params.value == null) { + return ; + } else if (params.value) { + return ; + } else { + return ; + } }, flex: 1, headerName: intl.formatMessage({ diff --git a/src/pages/users/users-table.tsx b/src/pages/users/users-table.tsx index 5ba0083..359d474 100644 --- a/src/pages/users/users-table.tsx +++ b/src/pages/users/users-table.tsx @@ -5,16 +5,16 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { FunctionComponent, RefObject, useCallback, useEffect, useMemo, useState } from 'react'; +import { FunctionComponent, RefObject, useCallback, useMemo, useState } from 'react'; import { useIntl } from 'react-intl'; import { PersonAdd } from '@mui/icons-material'; import { GridButton, GridButtonDelete, GridTable, GridTableRef } from '../../components/Grid'; -import { GroupInfos, UpdateUserInfos, UserAdminSrv, UserInfos } from '../../services'; +import { GroupInfos, UserAdminSrv, UserInfos } from '../../services'; import { ColDef, GetRowIdParams, - ICellEditorParams, ICheckboxCellRendererParams, + ITooltipParams, RowClickedEvent, SelectionChangedEvent, TextFilterParams, @@ -22,8 +22,6 @@ import { import { useSnackMessage } from '@gridsuite/commons-ui'; import DeleteConfirmationDialog from '../common/delete-confirmation-dialog'; import { defaultColDef, defaultRowSelection } from '../common/table-config'; -import MultiChipsRendererComponent from '../common/multi-chips-renderer-component'; -import MultiSelectEditorComponent from '../common/multi-select-editor-component'; export interface UsersTableProps { gridRef: RefObject>; @@ -37,23 +35,6 @@ const UsersTable: FunctionComponent = (props) => { const [rowsSelection, setRowsSelection] = useState([]); const [showDeletionDialog, setShowDeletionDialog] = useState(false); - const [groupsOptions, setGroupsOptions] = useState([]); - - useEffect(() => { - // fetch available groups - UserAdminSrv.fetchGroups() - .then((allGroups: GroupInfos[]) => { - let groups: string[] = []; - allGroups?.forEach((g) => groups.push(g.name)); - setGroupsOptions(groups); - }) - .catch((error) => - snackError({ - messageTxt: error.message, - headerId: 'users.table.error.groups', - }) - ); - }, [intl, snackError]); function getRowId(params: GetRowIdParams): string { return params.data.sub ?? ''; @@ -80,32 +61,12 @@ const UsersTable: FunctionComponent = (props) => { const deleteUsersDisabled = useMemo(() => rowsSelection.length <= 0, [rowsSelection.length]); - const updateUserCallback = useCallback( - (sub: string, profileName: string | undefined, isAdmin: boolean | undefined, groups: string[]) => { - const newData: UpdateUserInfos = { - sub: sub, - profileName: profileName, - isAdmin: isAdmin, - groups: groups, - }; - UserAdminSrv.udpateUser(newData) - .catch((error) => - snackError({ - messageTxt: error.message, - headerId: 'users.table.error.update', - }) - ) - .then(() => props.gridRef?.current?.context?.refresh?.()); - }, - [props.gridRef, snackError] - ); - const columns = useMemo( (): ColDef[] => [ { field: 'sub', cellDataType: 'text', - flex: 3, + flex: 2, lockVisible: true, filter: true, headerName: intl.formatMessage({ id: 'users.table.id' }), @@ -121,7 +82,7 @@ const UsersTable: FunctionComponent = (props) => { { field: 'profileName', cellDataType: 'text', - flex: 1, + flex: 2, filter: true, headerName: intl.formatMessage({ id: 'users.table.profileName', @@ -134,26 +95,10 @@ const UsersTable: FunctionComponent = (props) => { trimInput: true, } as TextFilterParams, }, - { - field: 'isAdmin', - cellDataType: 'boolean', - //detected as cellRenderer: 'agCheckboxCellRenderer', - cellRendererParams: { - disabled: true, - } as ICheckboxCellRendererParams, - flex: 1, - headerName: intl.formatMessage({ - id: 'users.table.isAdmin', - }), - headerTooltip: intl.formatMessage({ - id: 'users.table.isAdmin.description', - }), - filter: true, - }, { field: 'groups', cellDataType: 'text', - flex: 1, + flex: 3, filter: true, headerName: intl.formatMessage({ id: 'users.table.groups', @@ -165,20 +110,38 @@ const UsersTable: FunctionComponent = (props) => { caseSensitive: false, trimInput: true, } as TextFilterParams, - editable: true, - cellRenderer: MultiChipsRendererComponent, - cellEditor: MultiSelectEditorComponent, - cellEditorParams: (params: ICellEditorParams) => ({ - options: groupsOptions, - setValue: (values: string[]) => { - if (params.data?.sub) { - updateUserCallback(params.data.sub, params.data.profileName, params.data.isAdmin, values); - } - }, + tooltipValueGetter: (p: ITooltipParams) => { + const items = p.value as string[]; + if (items == null || items.length === 0) { + return ''; + } + let groupWord = intl.formatMessage({ + id: 'form.delete.dialog.group', + }); + if (items.length > 1) { + groupWord = groupWord.concat('s'); + } + return `${items.length} ${groupWord}`; + }, + }, + { + field: 'isAdmin', + cellDataType: 'boolean', + //detected as cellRenderer: 'agCheckboxCellRenderer', + cellRendererParams: { + disabled: true, + } as ICheckboxCellRendererParams, + flex: 1, + headerName: intl.formatMessage({ + id: 'users.table.isAdmin', }), + headerTooltip: intl.formatMessage({ + id: 'users.table.isAdmin.description', + }), + filter: true, }, ], - [groupsOptions, intl, updateUserCallback] + [intl] ); return ( @@ -193,6 +156,7 @@ const UsersTable: FunctionComponent = (props) => { rowSelection={defaultRowSelection} onRowClicked={props.onRowClicked} onSelectionChanged={onSelectionChanged} + tooltipShowDelay={1000} > Date: Wed, 30 Apr 2025 16:18:58 +0200 Subject: [PATCH 07/18] clean code Signed-off-by: David BRAQUART --- src/pages/common/table-config.ts | 1 - src/pages/common/table-selection.tsx | 6 ++-- src/pages/groups/groups-table.tsx | 2 +- .../modification/group-modification-form.tsx | 4 +-- src/pages/profiles/profiles-page.tsx | 32 ++++++++++--------- .../modification/user-modification-dialog.tsx | 4 +-- .../modification/user-modification-form.tsx | 2 +- 7 files changed, 26 insertions(+), 25 deletions(-) diff --git a/src/pages/common/table-config.ts b/src/pages/common/table-config.ts index e7c8f4f..92703ac 100644 --- a/src/pages/common/table-config.ts +++ b/src/pages/common/table-config.ts @@ -11,7 +11,6 @@ export const defaultColDef: ColDef = { editable: false, resizable: true, minWidth: 50, - cellRenderer: 'agAnimateSlideCellRenderer', rowDrag: false, sortable: true, }; diff --git a/src/pages/common/table-selection.tsx b/src/pages/common/table-selection.tsx index e5aa3e4..ac1829e 100644 --- a/src/pages/common/table-selection.tsx +++ b/src/pages/common/table-selection.tsx @@ -14,7 +14,7 @@ import { ColDef, GetRowIdParams, GridReadyEvent } from 'ag-grid-community'; import { defaultColDef, defaultRowSelection } from './table-config'; export interface TableSelectionProps { - itemNameTranslationKey: string; + itemName: string; tableItems: string[]; tableSelectedItems?: string[]; onSelectionChanged: (selectedItems: string[]) => void; @@ -45,8 +45,8 @@ const TableSelection: FunctionComponent = (props) => { field: 'id', filter: true, sortable: true, - minWidth: 80, tooltipField: 'id', + flex: 1, }, ], [] @@ -71,7 +71,7 @@ const TableSelection: FunctionComponent = (props) => { - + {` (${selectedRowsLength} / ${rowData?.length ?? 0})`} diff --git a/src/pages/groups/groups-table.tsx b/src/pages/groups/groups-table.tsx index 02f926f..2ac3429 100644 --- a/src/pages/groups/groups-table.tsx +++ b/src/pages/groups/groups-table.tsx @@ -16,7 +16,7 @@ import { ITooltipParams, RowClickedEvent, SelectionChangedEvent, - TextFilterParams + TextFilterParams, } from 'ag-grid-community'; import { useSnackMessage } from '@gridsuite/commons-ui'; import DeleteConfirmationDialog from '../common/delete-confirmation-dialog'; diff --git a/src/pages/groups/modification/group-modification-form.tsx b/src/pages/groups/modification/group-modification-form.tsx index 1eff460..a44c112 100644 --- a/src/pages/groups/modification/group-modification-form.tsx +++ b/src/pages/groups/modification/group-modification-form.tsx @@ -40,9 +40,9 @@ const GroupModificationForm: FunctionComponent = ({ - + { }, []); return ( - - - - - + <> + + + + - + + + ); }; export default ProfilesPage; diff --git a/src/pages/users/modification/user-modification-dialog.tsx b/src/pages/users/modification/user-modification-dialog.tsx index 5c43d79..6a8c022 100644 --- a/src/pages/users/modification/user-modification-dialog.tsx +++ b/src/pages/users/modification/user-modification-dialog.tsx @@ -110,8 +110,8 @@ const UserModificationDialog: FunctionComponent = ( (userFormData: UserModificationFormType) => { if (userInfos) { const newData: UpdateUserInfos = { - sub: userInfos.sub, // sub cannot be changed, it is a PK in database - isAdmin: userInfos.isAdmin, // cannot be changed for now + sub: userInfos.sub, // can't be changed + isAdmin: userInfos.isAdmin, // can't be changed profileName: userFormData.profileName ?? undefined, groups: selectedGroups, }; diff --git a/src/pages/users/modification/user-modification-form.tsx b/src/pages/users/modification/user-modification-form.tsx index 28ee3e8..9aaec3a 100644 --- a/src/pages/users/modification/user-modification-form.tsx +++ b/src/pages/users/modification/user-modification-form.tsx @@ -63,7 +63,7 @@ const UserModificationForm: FunctionComponent = ({ Date: Wed, 30 Apr 2025 17:17:39 +0200 Subject: [PATCH 08/18] add validity tooltype Signed-off-by: David BRAQUART --- src/pages/profiles/profiles-table.tsx | 24 +++++++++++++++++++++++- src/translations/en.json | 5 ++++- src/translations/fr.json | 3 +++ 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/pages/profiles/profiles-table.tsx b/src/pages/profiles/profiles-table.tsx index 6f1dfd4..f071b8e 100644 --- a/src/pages/profiles/profiles-table.tsx +++ b/src/pages/profiles/profiles-table.tsx @@ -10,7 +10,14 @@ import { useIntl } from 'react-intl'; import { Cancel, CheckCircle, ManageAccounts, RadioButtonUnchecked } from '@mui/icons-material'; import { GridButton, GridButtonDelete, GridTable, GridTableRef } from '../../components/Grid'; import { UserAdminSrv, UserProfile } from '../../services'; -import { ColDef, GetRowIdParams, RowClickedEvent, SelectionChangedEvent, TextFilterParams } from 'ag-grid-community'; +import { + ColDef, + GetRowIdParams, + ITooltipParams, + RowClickedEvent, + SelectionChangedEvent, + TextFilterParams +} from 'ag-grid-community'; import { useSnackMessage } from '@gridsuite/commons-ui'; import DeleteConfirmationDialog from '../common/delete-confirmation-dialog'; import { defaultColDef, defaultRowSelection } from '../common/table-config'; @@ -92,6 +99,21 @@ const ProfilesTable: FunctionComponent = (props) => { return ; } }, + tooltipValueGetter: (p: ITooltipParams) => { + if (p.value == null) { + return intl.formatMessage({ + id: 'profiles.table.validity.tooltip.none', + }); + } else if (p.value) { + return intl.formatMessage({ + id: 'profiles.table.validity.tooltip.ok', + }); + } else { + return intl.formatMessage({ + id: 'profiles.table.validity.tooltip.ko', + }); + } + }, flex: 1, headerName: intl.formatMessage({ id: 'profiles.table.validity', diff --git a/src/translations/en.json b/src/translations/en.json index 86db9ac..91a58a1 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -66,6 +66,9 @@ "profiles.table.id.description": "Profile name", "profiles.table.validity": "Configurations links validity", "profiles.table.validity.description": "Are all configurations links valid ?", + "profiles.table.validity.tooltip.none": "No configuration link defined", + "profiles.table.validity.tooltip.ok": "All configurations links are valid", + "profiles.table.validity.tooltip.ko": "At least one configuration link is invalid", "profiles.table.error.add": "Error while adding profile {profile} : A profile with the same name already exists.", "profiles.table.error.delete": "Error while deleting profile", "profiles.table.integrity.error.delete": "Error while deleting profile : a profile is still referenced by users", @@ -89,7 +92,7 @@ "profiles.form.modification.readError": "Error while reading the profile", "profiles.form.modification.updateError": "Error while updating the profile", - "groups.table.id": "ID", + "groups.table.id": "Name", "groups.table.id.description": "Group identifier", "groups.table.users": "Users", "groups.table.users.description": "The group's users", diff --git a/src/translations/fr.json b/src/translations/fr.json index 73cf165..d51f619 100644 --- a/src/translations/fr.json +++ b/src/translations/fr.json @@ -67,6 +67,9 @@ "profiles.table.id.description": "Nom du profil", "profiles.table.validity": "Validité des liens vers les configurations", "profiles.table.validity.description": "Tous les liens vers les configurations sont-ils valides ?", + "profiles.table.validity.tooltip.none": "Aucun lien vers une configuration n'est défini", + "profiles.table.validity.tooltip.ok": "Tous les liens vers les configurations sont valides", + "profiles.table.validity.tooltip.ko": "Au moins un lien vers les configurations est invalide", "profiles.table.error.add": "Erreur pendant l'ajout du profil {profile} : Un profil du même nom existe déjà.", "profiles.table.error.delete": "Erreur pendant la suppression de profil", "profiles.table.integrity.error.delete": "Erreur pendant la suppression de profil: un profil est toujours référencé par des utilisateurs", From 9b844a2c9c0ac94906003fe0e3ba4933df5e6e7a Mon Sep 17 00:00:00 2001 From: David BRAQUART Date: Wed, 30 Apr 2025 17:41:48 +0200 Subject: [PATCH 09/18] clean types Signed-off-by: David BRAQUART --- src/pages/common/table-selection.tsx | 2 +- .../group-modification-dialog.tsx | 4 +-- .../modification/group-modification-form.tsx | 2 +- src/pages/profiles/profiles-table.tsx | 2 +- .../modification/user-modification-dialog.tsx | 6 ++--- src/services/user-admin.ts | 27 ++++--------------- 6 files changed, 13 insertions(+), 30 deletions(-) diff --git a/src/pages/common/table-selection.tsx b/src/pages/common/table-selection.tsx index ac1829e..c6ee6d9 100644 --- a/src/pages/common/table-selection.tsx +++ b/src/pages/common/table-selection.tsx @@ -1,5 +1,5 @@ /** - * Copyright (c) 2024, RTE (http://www.rte-france.com) + * 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/. diff --git a/src/pages/groups/modification/group-modification-dialog.tsx b/src/pages/groups/modification/group-modification-dialog.tsx index 7fbc4d4..89fd2ab 100644 --- a/src/pages/groups/modification/group-modification-dialog.tsx +++ b/src/pages/groups/modification/group-modification-dialog.tsx @@ -15,7 +15,7 @@ import { yupResolver } from '@hookform/resolvers/yup'; import { useForm } from 'react-hook-form'; import { FunctionComponent, useCallback, useEffect, useMemo, useState } from 'react'; import { CustomMuiDialog, FetchStatus, useSnackMessage } from '@gridsuite/commons-ui'; -import { GroupInfos, UpdateGroupInfos, UserAdminSrv, UserInfos } from '../../../services'; +import { GroupInfos, UserAdminSrv, UserInfos } from '../../../services'; interface GroupModificationDialogProps { groupInfos: GroupInfos | undefined; @@ -86,7 +86,7 @@ const GroupModificationDialog: FunctionComponent = const onSubmit = useCallback( (groupFormData: GroupModificationFormType) => { if (groupInfos?.id) { - const newData: UpdateGroupInfos = { + const newData: GroupInfos = { id: groupInfos.id, name: groupFormData.name, users: selectedUsers, diff --git a/src/pages/groups/modification/group-modification-form.tsx b/src/pages/groups/modification/group-modification-form.tsx index a44c112..be994f4 100644 --- a/src/pages/groups/modification/group-modification-form.tsx +++ b/src/pages/groups/modification/group-modification-form.tsx @@ -9,7 +9,7 @@ import { TextInput } from '@gridsuite/commons-ui'; import Grid from '@mui/material/Grid'; import React, { FunctionComponent } from 'react'; import yup from '../../../utils/yup-config'; -import TableSelection from 'pages/common/table-selection'; +import TableSelection from '../../common/table-selection'; export const GROUP_NAME = 'name'; export const SELECTED_USERS = 'users'; diff --git a/src/pages/profiles/profiles-table.tsx b/src/pages/profiles/profiles-table.tsx index f071b8e..9e19e04 100644 --- a/src/pages/profiles/profiles-table.tsx +++ b/src/pages/profiles/profiles-table.tsx @@ -16,7 +16,7 @@ import { ITooltipParams, RowClickedEvent, SelectionChangedEvent, - TextFilterParams + TextFilterParams, } from 'ag-grid-community'; import { useSnackMessage } from '@gridsuite/commons-ui'; import DeleteConfirmationDialog from '../common/delete-confirmation-dialog'; diff --git a/src/pages/users/modification/user-modification-dialog.tsx b/src/pages/users/modification/user-modification-dialog.tsx index 6a8c022..157b0d2 100644 --- a/src/pages/users/modification/user-modification-dialog.tsx +++ b/src/pages/users/modification/user-modification-dialog.tsx @@ -16,7 +16,7 @@ import { yupResolver } from '@hookform/resolvers/yup'; import { useForm } from 'react-hook-form'; import { FunctionComponent, useCallback, useEffect, useMemo, useState } from 'react'; import { CustomMuiDialog, FetchStatus, useSnackMessage } from '@gridsuite/commons-ui'; -import { GroupInfos, UpdateUserInfos, UserAdminSrv, UserInfos, UserProfile } from '../../../services'; +import { GroupInfos, UserAdminSrv, UserInfos, UserProfile } from '../../../services'; interface UserModificationDialogProps { userInfos: UserInfos | undefined; @@ -109,13 +109,13 @@ const UserModificationDialog: FunctionComponent = ( const onSubmit = useCallback( (userFormData: UserModificationFormType) => { if (userInfos) { - const newData: UpdateUserInfos = { + const newData: UserInfos = { sub: userInfos.sub, // can't be changed isAdmin: userInfos.isAdmin, // can't be changed profileName: userFormData.profileName ?? undefined, groups: selectedGroups, }; - UserAdminSrv.udpateUser(newData) + UserAdminSrv.updateUser(newData) .catch((error) => snackError({ messageTxt: error.message, diff --git a/src/services/user-admin.ts b/src/services/user-admin.ts index 4bc0e81..96aa056 100644 --- a/src/services/user-admin.ts +++ b/src/services/user-admin.ts @@ -7,15 +7,11 @@ import { User } from 'oidc-client'; import { backendFetch, backendFetchJson, getRestBase } from '../utils/api-rest'; -import { extractUserSub, getToken, getUser } from '../utils/api'; +import { extractUserSub, getToken } from '../utils/api'; import { UUID } from 'crypto'; const USER_ADMIN_URL = `${getRestBase()}/user-admin/v1`; -export function getUserSub(): Promise { - return extractUserSub(getUser()); -} - /* * fetchValidateUser is call from commons-ui AuthServices to validate user infos before setting state.user! */ @@ -40,7 +36,7 @@ export function fetchValidateUser(user: User): Promise { export type UserInfos = { sub: string; - profileName: string; + profileName?: string; isAdmin: boolean; groups: string[]; }; @@ -59,14 +55,7 @@ export function fetchUsers(): Promise { }) as Promise; } -export type UpdateUserInfos = { - sub: string; - profileName?: string; - isAdmin?: boolean; - groups: string[]; -}; - -export function udpateUser(userInfos: UpdateUserInfos) { +export function updateUser(userInfos: UserInfos) { console.debug(`Updating a user...`); return backendFetch(`${USER_ADMIN_URL}/users/${userInfos.sub}`, { @@ -216,7 +205,7 @@ export function deleteProfiles(names: string[]): Promise { } export type GroupInfos = { - id?: UUID; + id: UUID; name: string; users: string[]; }; @@ -235,13 +224,7 @@ export function fetchGroups(): Promise { }) as Promise; } -export type UpdateGroupInfos = { - id: UUID; - name: string; - users: string[]; -}; - -export function udpateGroup(groupInfos: UpdateGroupInfos) { +export function udpateGroup(groupInfos: GroupInfos) { console.debug(`Updating a group...`); return backendFetch(`${USER_ADMIN_URL}/groups/${groupInfos.id}`, { From b9aff282d745b4177133839305bc58c89f99db61 Mon Sep 17 00:00:00 2001 From: David BRAQUART Date: Mon, 5 May 2025 16:18:15 +0200 Subject: [PATCH 10/18] rev remarks Signed-off-by: David BRAQUART --- src/pages/groups/groups-page.tsx | 2 +- src/pages/groups/groups-table.tsx | 2 +- src/pages/groups/modification/group-modification-form.tsx | 2 +- src/pages/users/modification/user-modification-form.tsx | 2 +- src/pages/users/users-page.tsx | 2 +- src/pages/users/users-table.tsx | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/pages/groups/groups-page.tsx b/src/pages/groups/groups-page.tsx index 0006f61..57234b8 100644 --- a/src/pages/groups/groups-page.tsx +++ b/src/pages/groups/groups-page.tsx @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024, RTE (http://www.rte-france.com) + * 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/. diff --git a/src/pages/groups/groups-table.tsx b/src/pages/groups/groups-table.tsx index 2ac3429..1e7d448 100644 --- a/src/pages/groups/groups-table.tsx +++ b/src/pages/groups/groups-table.tsx @@ -1,5 +1,5 @@ /** - * Copyright (c) 2024, RTE (http://www.rte-france.com) + * 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/. diff --git a/src/pages/groups/modification/group-modification-form.tsx b/src/pages/groups/modification/group-modification-form.tsx index be994f4..8ef95db 100644 --- a/src/pages/groups/modification/group-modification-form.tsx +++ b/src/pages/groups/modification/group-modification-form.tsx @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024, RTE (http://www.rte-france.com) + * 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/. diff --git a/src/pages/users/modification/user-modification-form.tsx b/src/pages/users/modification/user-modification-form.tsx index 9aaec3a..ce036dc 100644 --- a/src/pages/users/modification/user-modification-form.tsx +++ b/src/pages/users/modification/user-modification-form.tsx @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024, RTE (http://www.rte-france.com) + * 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/. diff --git a/src/pages/users/users-page.tsx b/src/pages/users/users-page.tsx index f52d9e0..5f7e494 100644 --- a/src/pages/users/users-page.tsx +++ b/src/pages/users/users-page.tsx @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024, RTE (http://www.rte-france.com) + * 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/. diff --git a/src/pages/users/users-table.tsx b/src/pages/users/users-table.tsx index 359d474..88b0799 100644 --- a/src/pages/users/users-table.tsx +++ b/src/pages/users/users-table.tsx @@ -1,5 +1,5 @@ /** - * Copyright (c) 2024, RTE (http://www.rte-france.com) + * 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/. From 4dd45b5e790b41b1ee02e981ad13048bd00d52f9 Mon Sep 17 00:00:00 2001 From: David BRAQUART Date: Tue, 6 May 2025 13:39:34 +0200 Subject: [PATCH 11/18] use chips in table display Signed-off-by: David BRAQUART --- src/pages/common/multi-chip-cell-renderer.tsx | 58 +++++++++++++++++++ src/pages/groups/groups-table.tsx | 3 + src/pages/users/users-table.tsx | 8 ++- 3 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 src/pages/common/multi-chip-cell-renderer.tsx diff --git a/src/pages/common/multi-chip-cell-renderer.tsx b/src/pages/common/multi-chip-cell-renderer.tsx new file mode 100644 index 0000000..2b214d2 --- /dev/null +++ b/src/pages/common/multi-chip-cell-renderer.tsx @@ -0,0 +1,58 @@ +/** + * 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 { Chip, Grid } from '@mui/material'; +import { mergeSx } from '@gridsuite/commons-ui'; + +const CHIP_LIMIT_NUMBER: number = 5; + +const chipStyles = { + default: { + marginTop: '16px', + marginLeft: '8px', + maxWidth: '50%', + }, + withCounter: { + '&.MuiChip-root': { + fontWeight: 'bold', + }, + }, +}; + +export interface MultiChipCellRendererProps { + value: string[]; +} + +const MultiChipCellRenderer = (props: MultiChipCellRendererProps) => { + const values: string[] = props.value || []; + + const customChip = (label: string, index: number, chipsNumber: number) => { + if (index < CHIP_LIMIT_NUMBER) { + return ; + } else if (index === CHIP_LIMIT_NUMBER) { + return ( + + ); + } + return undefined; + }; + + return ( + + {values.map((label: string, index: number) => { + return customChip(label, index, values.length); + })} + + ); +}; + +export default MultiChipCellRenderer; diff --git a/src/pages/groups/groups-table.tsx b/src/pages/groups/groups-table.tsx index 1e7d448..6bfa50d 100644 --- a/src/pages/groups/groups-table.tsx +++ b/src/pages/groups/groups-table.tsx @@ -21,6 +21,7 @@ import { import { useSnackMessage } from '@gridsuite/commons-ui'; import DeleteConfirmationDialog from '../common/delete-confirmation-dialog'; import { defaultColDef, defaultRowSelection } from '../common/table-config'; +import MultiChipCellRenderer from '../common/multi-chip-cell-renderer'; export interface GroupsTableProps { gridRef: RefObject>; @@ -83,6 +84,7 @@ const GroupsTable: FunctionComponent = (props) => { trimInput: true, } as TextFilterParams, initialSort: 'asc', + tooltipField: 'name', }, { field: 'users', @@ -99,6 +101,7 @@ const GroupsTable: FunctionComponent = (props) => { caseSensitive: false, trimInput: true, } as TextFilterParams, + cellRenderer: MultiChipCellRenderer, tooltipValueGetter: (p: ITooltipParams) => { const items = p.value as string[]; if (items == null || items.length === 0) { diff --git a/src/pages/users/users-table.tsx b/src/pages/users/users-table.tsx index 88b0799..3cb5536 100644 --- a/src/pages/users/users-table.tsx +++ b/src/pages/users/users-table.tsx @@ -22,6 +22,7 @@ import { import { useSnackMessage } from '@gridsuite/commons-ui'; import DeleteConfirmationDialog from '../common/delete-confirmation-dialog'; import { defaultColDef, defaultRowSelection } from '../common/table-config'; +import MultiChipCellRenderer from 'pages/common/multi-chip-cell-renderer'; export interface UsersTableProps { gridRef: RefObject>; @@ -66,7 +67,7 @@ const UsersTable: FunctionComponent = (props) => { { field: 'sub', cellDataType: 'text', - flex: 2, + flex: 1, lockVisible: true, filter: true, headerName: intl.formatMessage({ id: 'users.table.id' }), @@ -78,6 +79,7 @@ const UsersTable: FunctionComponent = (props) => { trimInput: true, } as TextFilterParams, initialSort: 'asc', + tooltipField: 'sub', }, { field: 'profileName', @@ -94,11 +96,12 @@ const UsersTable: FunctionComponent = (props) => { caseSensitive: false, trimInput: true, } as TextFilterParams, + tooltipField: 'profileName', }, { field: 'groups', cellDataType: 'text', - flex: 3, + flex: 4, filter: true, headerName: intl.formatMessage({ id: 'users.table.groups', @@ -110,6 +113,7 @@ const UsersTable: FunctionComponent = (props) => { caseSensitive: false, trimInput: true, } as TextFilterParams, + cellRenderer: MultiChipCellRenderer, tooltipValueGetter: (p: ITooltipParams) => { const items = p.value as string[]; if (items == null || items.length === 0) { From 7156e68b2700356a64f66955a027d8f7f3849d57 Mon Sep 17 00:00:00 2001 From: David BRAQUART Date: Tue, 6 May 2025 13:40:18 +0200 Subject: [PATCH 12/18] use normal and bold to display links Signed-off-by: David BRAQUART --- src/pages/profiles/modification/linked-path-display.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/profiles/modification/linked-path-display.tsx b/src/pages/profiles/modification/linked-path-display.tsx index 1fab9fc..6d3327d 100644 --- a/src/pages/profiles/modification/linked-path-display.tsx +++ b/src/pages/profiles/modification/linked-path-display.tsx @@ -22,7 +22,7 @@ const LinkedPathDisplay: FunctionComponent = (props) => return ( From 18366149fbf63d14e3c92823a51a893ef7a54d79 Mon Sep 17 00:00:00 2001 From: David BRAQUART Date: Tue, 6 May 2025 13:53:22 +0200 Subject: [PATCH 13/18] use separate componant for validity (sonar issue) Signed-off-by: David BRAQUART --- src/pages/profiles/profiles-table.tsx | 14 +++------- src/pages/profiles/validity-cell-renderer.tsx | 26 +++++++++++++++++++ 2 files changed, 30 insertions(+), 10 deletions(-) create mode 100644 src/pages/profiles/validity-cell-renderer.tsx diff --git a/src/pages/profiles/profiles-table.tsx b/src/pages/profiles/profiles-table.tsx index 9e19e04..e2dd66f 100644 --- a/src/pages/profiles/profiles-table.tsx +++ b/src/pages/profiles/profiles-table.tsx @@ -7,7 +7,7 @@ import { FunctionComponent, RefObject, useCallback, useMemo, useState } from 'react'; import { useIntl } from 'react-intl'; -import { Cancel, CheckCircle, ManageAccounts, RadioButtonUnchecked } from '@mui/icons-material'; +import { ManageAccounts } from '@mui/icons-material'; import { GridButton, GridButtonDelete, GridTable, GridTableRef } from '../../components/Grid'; import { UserAdminSrv, UserProfile } from '../../services'; import { @@ -21,6 +21,7 @@ import { import { useSnackMessage } from '@gridsuite/commons-ui'; import DeleteConfirmationDialog from '../common/delete-confirmation-dialog'; import { defaultColDef, defaultRowSelection } from '../common/table-config'; +import ValidityCellRenderer from './validity-cell-renderer'; export interface ProfilesTableProps { gridRef: RefObject>; @@ -82,6 +83,7 @@ const ProfilesTable: FunctionComponent = (props) => { caseSensitive: false, trimInput: true, } as TextFilterParams, + tooltipField: 'name', }, { field: 'allLinksValid', @@ -90,15 +92,7 @@ const ProfilesTable: FunctionComponent = (props) => { display: 'flex', alignItems: 'center', }), - cellRenderer: (params: any) => { - if (params.value == null) { - return ; - } else if (params.value) { - return ; - } else { - return ; - } - }, + cellRenderer: ValidityCellRenderer, tooltipValueGetter: (p: ITooltipParams) => { if (p.value == null) { return intl.formatMessage({ diff --git a/src/pages/profiles/validity-cell-renderer.tsx b/src/pages/profiles/validity-cell-renderer.tsx new file mode 100644 index 0000000..712e39d --- /dev/null +++ b/src/pages/profiles/validity-cell-renderer.tsx @@ -0,0 +1,26 @@ +/** + * 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 { Grid } from '@mui/material'; +import { Cancel, CheckCircle, RadioButtonUnchecked } from '@mui/icons-material'; +import { ICellRendererParams } from 'ag-grid-community'; + +export const ValidityCellRenderer = (props: ICellRendererParams) => { + const renderIcon = (valid: boolean | undefined | null) => { + if (valid == null) { + return ; + } else if (valid) { + return ; + } else { + return ; + } + }; + + return {renderIcon(props.value)}; +}; + +export default ValidityCellRenderer; From 14f17f87ff19f11c395a9b4d67a099fe57ae4741 Mon Sep 17 00:00:00 2001 From: David BRAQUART Date: Tue, 6 May 2025 14:37:17 +0200 Subject: [PATCH 14/18] fix margin and refacto Signed-off-by: David BRAQUART --- .../profile-modification-form.tsx | 69 +++++++------------ 1 file changed, 25 insertions(+), 44 deletions(-) diff --git a/src/pages/profiles/modification/profile-modification-form.tsx b/src/pages/profiles/modification/profile-modification-form.tsx index 185f88f..69f817d 100644 --- a/src/pages/profiles/modification/profile-modification-form.tsx +++ b/src/pages/profiles/modification/profile-modification-form.tsx @@ -7,7 +7,7 @@ import { ElementType, IntegerInput, TextInput } from '@gridsuite/commons-ui'; import Grid from '@mui/material/Grid'; -import ConfigurationSelection from './configuration-selection'; +import ConfigurationSelection, { ConfigSelectionProps } from './configuration-selection'; import { FormattedMessage } from 'react-intl'; import React, { FunctionComponent } from 'react'; @@ -23,6 +23,19 @@ export const NETWORK_VISUALIZATION_PARAMETERS_ID = 'networkVisualizationParamete export const USER_QUOTA_CASE_NB = 'userQuotaCaseNb'; export const USER_QUOTA_BUILD_NB = 'userQuotaBuildNb'; +const configList: ConfigSelectionProps[] = [ + { selectionFormId: LOADFLOW_PARAM_ID, elementType: ElementType.LOADFLOW_PARAMETERS }, + { selectionFormId: SECURITY_ANALYSIS_PARAM_ID, elementType: ElementType.SECURITY_ANALYSIS_PARAMETERS }, + { selectionFormId: SENSITIVITY_ANALYSIS_PARAM_ID, elementType: ElementType.SENSITIVITY_PARAMETERS }, + { selectionFormId: SHORTCIRCUIT_PARAM_ID, elementType: ElementType.SHORT_CIRCUIT_PARAMETERS }, + { selectionFormId: VOLTAGE_INIT_PARAM_ID, elementType: ElementType.VOLTAGE_INIT_PARAMETERS }, + { selectionFormId: SPREADSHEET_CONFIG_COLLECTION_ID, elementType: ElementType.SPREADSHEET_CONFIG_COLLECTION }, + { + selectionFormId: NETWORK_VISUALIZATION_PARAMETERS_ID, + elementType: ElementType.NETWORK_VISUALIZATIONS_PARAMETERS, + }, +]; + const ProfileModificationForm: FunctionComponent = () => { return ( @@ -34,54 +47,22 @@ const ProfileModificationForm: FunctionComponent = () => {

- - - - - - - - - - - - - - - - - - - - - + {configList.map((config) => { + return ( + + + + ); + })}

- + Date: Tue, 6 May 2025 15:07:44 +0200 Subject: [PATCH 15/18] fix ci import issue Signed-off-by: David BRAQUART --- src/pages/users/users-table.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/users/users-table.tsx b/src/pages/users/users-table.tsx index 3cb5536..58f2561 100644 --- a/src/pages/users/users-table.tsx +++ b/src/pages/users/users-table.tsx @@ -22,7 +22,7 @@ import { import { useSnackMessage } from '@gridsuite/commons-ui'; import DeleteConfirmationDialog from '../common/delete-confirmation-dialog'; import { defaultColDef, defaultRowSelection } from '../common/table-config'; -import MultiChipCellRenderer from 'pages/common/multi-chip-cell-renderer'; +import MultiChipCellRenderer from '../common/multi-chip-cell-renderer'; export interface UsersTableProps { gridRef: RefObject>; From 0ac12bb8bbe9dbe18df2ebe6c29fd0c67f0c85c9 Mon Sep 17 00:00:00 2001 From: David BRAQUART Date: Tue, 6 May 2025 15:40:41 +0200 Subject: [PATCH 16/18] fix new sonar issue Signed-off-by: David BRAQUART --- src/pages/profiles/validity-cell-renderer.tsx | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/src/pages/profiles/validity-cell-renderer.tsx b/src/pages/profiles/validity-cell-renderer.tsx index 712e39d..da04102 100644 --- a/src/pages/profiles/validity-cell-renderer.tsx +++ b/src/pages/profiles/validity-cell-renderer.tsx @@ -10,17 +10,13 @@ import { Cancel, CheckCircle, RadioButtonUnchecked } from '@mui/icons-material'; import { ICellRendererParams } from 'ag-grid-community'; export const ValidityCellRenderer = (props: ICellRendererParams) => { - const renderIcon = (valid: boolean | undefined | null) => { - if (valid == null) { - return ; - } else if (valid) { - return ; - } else { - return ; - } - }; - - return {renderIcon(props.value)}; + return ( + + {props.value == null && } + {props.value === true && } + {props.value === false && } + + ); }; export default ValidityCellRenderer; From f5708a7d67c5e2acd8ded2a7e2d303ceb8c0381d Mon Sep 17 00:00:00 2001 From: Ayoub LABIDI Date: Wed, 7 May 2025 14:18:31 +0200 Subject: [PATCH 17/18] dynamically adjust chip limit Signed-off-by: Ayoub LABIDI --- src/components/Grid/GridTable.tsx | 2 +- src/pages/common/multi-chip-cell-renderer.tsx | 74 ++++++++++++++----- src/pages/groups/groups-table.tsx | 23 +----- src/pages/users/users-table.tsx | 15 +--- 4 files changed, 59 insertions(+), 55 deletions(-) diff --git a/src/components/Grid/GridTable.tsx b/src/components/Grid/GridTable.tsx index b9bc634..6b9b54a 100644 --- a/src/components/Grid/GridTable.tsx +++ b/src/components/Grid/GridTable.tsx @@ -76,7 +76,7 @@ export const GridTable: GridTableWithRef = forwardRef(function AgGridToolbar + { const values: string[] = props.value || []; + const containerRef = useRef(null); + const [chipLimit, setChipLimit] = useState(5); + + useEffect(() => { + const updateChipLimit = () => { + if (!containerRef.current) { + return; + } + const zoomLevel = window.devicePixelRatio; + const adjustedContainerWidth = containerRef.current.clientWidth / zoomLevel; + const maxChips = Math.max(1, Math.floor(adjustedContainerWidth / (maxChipWidth + counterChipWidth))); + setChipLimit(maxChips); + }; + + updateChipLimit(); + const resizeObserver = new ResizeObserver(updateChipLimit); + if (containerRef.current) { + resizeObserver.observe(containerRef.current); + } + return () => resizeObserver.disconnect(); + }, [values.length]); const customChip = (label: string, index: number, chipsNumber: number) => { - if (index < CHIP_LIMIT_NUMBER) { - return ; - } else if (index === CHIP_LIMIT_NUMBER) { + if (index < chipLimit) { + return ( + + + + ); + } else if (index === chipLimit) { + const hiddenLabels = values.slice(chipLimit); + const tooltipContent = ( + <> + {hiddenLabels.map((hiddenLabel) => ( +
{'- ' + hiddenLabel}
+ ))} + + ); + return ( - + + + ); } - return undefined; + return null; }; return ( - - {values.map((label: string, index: number) => { - return customChip(label, index, values.length); - })} + + {values.map((label: string, index: number) => customChip(label, index, values.length))} ); }; diff --git a/src/pages/groups/groups-table.tsx b/src/pages/groups/groups-table.tsx index 6bfa50d..16cca42 100644 --- a/src/pages/groups/groups-table.tsx +++ b/src/pages/groups/groups-table.tsx @@ -10,14 +10,7 @@ import { useIntl } from 'react-intl'; import { GroupAdd } from '@mui/icons-material'; import { GridButton, GridButtonDelete, GridTable, GridTableRef } from '../../components/Grid'; import { GroupInfos, UserAdminSrv, UserInfos } from '../../services'; -import { - ColDef, - GetRowIdParams, - ITooltipParams, - RowClickedEvent, - SelectionChangedEvent, - TextFilterParams, -} from 'ag-grid-community'; +import { ColDef, GetRowIdParams, RowClickedEvent, SelectionChangedEvent, TextFilterParams } from 'ag-grid-community'; import { useSnackMessage } from '@gridsuite/commons-ui'; import DeleteConfirmationDialog from '../common/delete-confirmation-dialog'; import { defaultColDef, defaultRowSelection } from '../common/table-config'; @@ -88,6 +81,7 @@ const GroupsTable: FunctionComponent = (props) => { }, { field: 'users', + minWidth: 200, cellDataType: 'text', flex: 3, filter: true, @@ -102,19 +96,6 @@ const GroupsTable: FunctionComponent = (props) => { trimInput: true, } as TextFilterParams, cellRenderer: MultiChipCellRenderer, - tooltipValueGetter: (p: ITooltipParams) => { - const items = p.value as string[]; - if (items == null || items.length === 0) { - return ''; - } - let userWord = intl.formatMessage({ - id: 'form.delete.dialog.user', - }); - if (items.length > 1) { - userWord = userWord.concat('s'); - } - return `${items.length} ${userWord}`; - }, }, ], [intl] diff --git a/src/pages/users/users-table.tsx b/src/pages/users/users-table.tsx index 58f2561..08663d2 100644 --- a/src/pages/users/users-table.tsx +++ b/src/pages/users/users-table.tsx @@ -14,7 +14,6 @@ import { ColDef, GetRowIdParams, ICheckboxCellRendererParams, - ITooltipParams, RowClickedEvent, SelectionChangedEvent, TextFilterParams, @@ -100,6 +99,7 @@ const UsersTable: FunctionComponent = (props) => { }, { field: 'groups', + minWidth: 200, cellDataType: 'text', flex: 4, filter: true, @@ -114,19 +114,6 @@ const UsersTable: FunctionComponent = (props) => { trimInput: true, } as TextFilterParams, cellRenderer: MultiChipCellRenderer, - tooltipValueGetter: (p: ITooltipParams) => { - const items = p.value as string[]; - if (items == null || items.length === 0) { - return ''; - } - let groupWord = intl.formatMessage({ - id: 'form.delete.dialog.group', - }); - if (items.length > 1) { - groupWord = groupWord.concat('s'); - } - return `${items.length} ${groupWord}`; - }, }, { field: 'isAdmin', From 21894d9deb3889ea5511dc10a102d78fae072026 Mon Sep 17 00:00:00 2001 From: David BRAQUART Date: Wed, 7 May 2025 15:18:54 +0200 Subject: [PATCH 18/18] sort the chips in the cell renderer Signed-off-by: David BRAQUART --- src/pages/common/multi-chip-cell-renderer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/common/multi-chip-cell-renderer.tsx b/src/pages/common/multi-chip-cell-renderer.tsx index 08cc7f6..e1bef68 100644 --- a/src/pages/common/multi-chip-cell-renderer.tsx +++ b/src/pages/common/multi-chip-cell-renderer.tsx @@ -30,7 +30,7 @@ export interface MultiChipCellRendererProps { } const MultiChipCellRenderer = (props: MultiChipCellRendererProps) => { - const values: string[] = props.value || []; + const values: string[] = props.value ? [...props.value].sort((a: string, b: string) => a.localeCompare(b)) : []; const containerRef = useRef(null); const [chipLimit, setChipLimit] = useState(5);