From be7534b8bfec67c7381ff1715c66ab2c9eae100b Mon Sep 17 00:00:00 2001 From: Carla Martinez Date: Fri, 16 Feb 2024 09:22:49 +0100 Subject: [PATCH 1/7] 'MemberOfAddModal' to 'MemberOfAddModalOld' The functionality of `MemberOfAddModal` is going to change but there are some components that depends on the current functionality. That's why it needs to be renamed. Signed-off-by: Carla Martinez --- .../MemberOf/MemberOfAddModalOld.tsx | 245 ++++++++++++++++++ src/pages/ActiveUsers/UserMemberOf.tsx | 2 +- src/pages/Hosts/HostsMemberOf.tsx | 2 +- src/pages/Services/ServicesMemberOf.tsx | 2 +- 4 files changed, 248 insertions(+), 3 deletions(-) create mode 100644 src/components/MemberOf/MemberOfAddModalOld.tsx diff --git a/src/components/MemberOf/MemberOfAddModalOld.tsx b/src/components/MemberOf/MemberOfAddModalOld.tsx new file mode 100644 index 00000000..8a878ed8 --- /dev/null +++ b/src/components/MemberOf/MemberOfAddModalOld.tsx @@ -0,0 +1,245 @@ +import React, { ReactNode, useEffect, useState } from "react"; +// PatternFly +import { Button, DualListSelector } from "@patternfly/react-core"; +// Modals +import ModalWithFormLayout from "src/components/layouts/ModalWithFormLayout"; +// Data types +import { + UserGroup, + Netgroup, + Roles, + HBACRules, + SudoRules, + HostGroup, +} from "src/utils/datatypes/globalDataTypes"; + +interface ModalData { + showModal: boolean; + handleModalToggle: () => void; +} + +interface TabData { + tabName: string; + userName: string; // TODO: Change to a more generalistic name +} + +export interface PropsToAdd { + modalData: ModalData; + availableData: + | UserGroup[] + | Netgroup[] + | Roles[] + | HBACRules[] + | SudoRules[] + | HostGroup[]; + groupRepository: unknown[]; + updateGroupRepository: ( + args: + | UserGroup[] + | Netgroup[] + | Roles[] + | HBACRules[] + | SudoRules[] + | HostGroup[] + ) => void; + updateAvOptionsList: (args: unknown[]) => void; + tabData: TabData; +} + +// Although tabs data types habe been already defined, it is not possible to access to all +// its variables. Just the mandatory ones ('name' and 'description') are accessible at this point. +// To display all the possible data types for all the tabs (and not only the mandatory ones) +// an extra interface 'MemberOfElement' will be defined. This will be called when assigning +// a new group instead of refering to each type (UserGroup | Netgroup | Roles | HBACRules | +// SudoRules | HostGroup). +interface MemberOfElement { + hostGroup?: string; + name: string; + gid?: string; + status?: string; + description: string; +} + +const MemberOfAddModal = (props: PropsToAdd) => { + // Dual list data + const data = props.availableData.map((d) => d.name); + + // Dual list selector + const [availableOptions, setAvailableOptions] = useState(data); + const [chosenOptions, setChosenOptions] = useState([]); + + const listChange = ( + newAvailableOptions: ReactNode[], + newChosenOptions: ReactNode[] + ) => { + setAvailableOptions(newAvailableOptions.sort()); + setChosenOptions(newChosenOptions.sort()); + props.updateAvOptionsList(newAvailableOptions.sort()); + }; + + const fields = [ + { + id: "dual-list-selector", + pfComponent: ( + listChange(newAvailableOptions, newChosenOptions)} + id="basicSelectorWithSearch" + /> + ), + }, + ]; + + // When clean data, set to original values + const cleanData = () => { + setAvailableOptions(data); + setChosenOptions([]); + }; + + // Clean fields and close modal (To prevent data persistence when reopen modal) + const cleanAndCloseModal = () => { + cleanData(); + props.modalData.handleModalToggle(); + }; + + // Buttons are disabled until the user fills the required fields + const [buttonDisabled, setButtonDisabled] = useState(true); + useEffect(() => { + if (chosenOptions.length > 0) { + setButtonDisabled(false); + } else { + setButtonDisabled(true); + } + }, [chosenOptions]); + + // Get all info from a chosen option + const getInfoFromGroupData = (option: unknown) => { + return props.availableData.find((d) => option === d.name); + }; + + // Add group option + const onClickAddGroupHandler = () => { + chosenOptions.map((opt) => { + const optionData: MemberOfElement | undefined = getInfoFromGroupData(opt); + if (optionData !== undefined) { + // User groups + if (props.tabData.tabName === "User groups") { + props.groupRepository.push({ + name: optionData.name !== undefined && optionData.name, + description: + optionData.description !== undefined && optionData.description, + gid: optionData.gid !== undefined && optionData.gid, + status: optionData.status !== undefined && optionData.status, + } as UserGroup); + // Send updated data to table + props.updateGroupRepository(props.groupRepository as UserGroup[]); + } + // Netgroups + if (props.tabData.tabName === "Netgroups") { + props.groupRepository.push({ + name: optionData.name !== undefined && optionData.name, + description: + optionData.description !== undefined && optionData.description, + gid: optionData.gid !== undefined && optionData.gid, + status: optionData.status !== undefined && optionData.status, + } as Netgroup); + // Send updated data to table + props.updateGroupRepository(props.groupRepository as Netgroup[]); + } + // Roles + if (props.tabData.tabName === "Roles") { + props.groupRepository.push({ + name: optionData.name !== undefined && optionData.name, + description: + optionData.description !== undefined && optionData.description, + gid: optionData.gid !== undefined && optionData.gid, + status: optionData.status !== undefined && optionData.status, + } as Roles); + // Send updated data to table + props.updateGroupRepository(props.groupRepository as Roles[]); + } + // HBAC rules + if (props.tabData.tabName === "HBAC rules") { + props.groupRepository.push({ + name: optionData.name !== undefined && optionData.name, + description: + optionData.description !== undefined && optionData.description, + gid: optionData.gid !== undefined && optionData.gid, + status: optionData.status !== undefined && optionData.status, + } as HBACRules); + // Send updated data to table + props.updateGroupRepository(props.groupRepository as HBACRules[]); + } + // Sudo rules + if (props.tabData.tabName === "Sudo rules") { + props.groupRepository.push({ + name: optionData.name !== undefined && optionData.name, + description: + optionData.description !== undefined && optionData.description, + gid: optionData.gid !== undefined && optionData.gid, + status: optionData.status !== undefined && optionData.status, + } as SudoRules); + // Send updated data to table + props.updateGroupRepository(props.groupRepository as SudoRules[]); + } + // Host groups + if (props.tabData.tabName === "Host groups") { + props.groupRepository.push({ + name: optionData.name !== undefined && optionData.name, + description: + optionData.description !== undefined && optionData.description, + } as HostGroup); + // Send updated data to table + props.updateGroupRepository(props.groupRepository as HostGroup[]); + } + } + }); + // Clean chosen options and close modal + setChosenOptions([]); + props.modalData.handleModalToggle(); + }; + + // Buttons that will be shown at the end of the form + const modalActions = [ + , + , + ]; + + // Render 'MemberOfaddModal' + return ( + + ); +}; + +export default MemberOfAddModal; diff --git a/src/pages/ActiveUsers/UserMemberOf.tsx b/src/pages/ActiveUsers/UserMemberOf.tsx index ba5562e8..0d2ced6c 100644 --- a/src/pages/ActiveUsers/UserMemberOf.tsx +++ b/src/pages/ActiveUsers/UserMemberOf.tsx @@ -32,7 +32,7 @@ import { sudoRulesInitialData, } from "src/utils/data/GroupRepositories"; // Modals -import MemberOfAddModal from "src/components/MemberOf/MemberOfAddModal"; +import MemberOfAddModal from "src/components/MemberOf/MemberOfAddModalOld"; import MemberOfDeleteModal from "src/components/MemberOf/MemberOfDeleteModal"; // Wrappers import MemberOfUserGroups from "src/components/MemberOf/MemberOfUserGroups"; diff --git a/src/pages/Hosts/HostsMemberOf.tsx b/src/pages/Hosts/HostsMemberOf.tsx index d2138548..968e0629 100644 --- a/src/pages/Hosts/HostsMemberOf.tsx +++ b/src/pages/Hosts/HostsMemberOf.tsx @@ -35,7 +35,7 @@ import { hostsSudoRulesInitialData, } from "src/utils/data/GroupRepositories"; // Modals -import MemberOfAddModal from "src/components/MemberOf/MemberOfAddModal"; +import MemberOfAddModal from "src/components/MemberOf/MemberOfAddModalOld"; import MemberOfDeleteModal from "src/components/MemberOf/MemberOfDeleteModal"; interface PropsToHostsMemberOf { diff --git a/src/pages/Services/ServicesMemberOf.tsx b/src/pages/Services/ServicesMemberOf.tsx index 53d328a7..bd4bdcba 100644 --- a/src/pages/Services/ServicesMemberOf.tsx +++ b/src/pages/Services/ServicesMemberOf.tsx @@ -21,7 +21,7 @@ import { useAppSelector } from "src/store/hooks"; // Repositories import { servicesRolesInitialData } from "src/utils/data/GroupRepositories"; // Modals -import MemberOfAddModal from "src/components/MemberOf/MemberOfAddModal"; +import MemberOfAddModal from "src/components/MemberOf/MemberOfAddModalOld"; import MemberOfDeleteModal from "src/components/MemberOf/MemberOfDeleteModal"; interface PropsToServicesMemberOf { From 4aded0eb821b00ca144c1c57409087adba9f987c Mon Sep 17 00:00:00 2001 From: Carla Martinez Date: Fri, 16 Feb 2024 09:35:19 +0100 Subject: [PATCH 2/7] 'MemberOfDeleteModal' to 'MemberOfDeleteModalOld' Same as the `MemberOfAddModal`. Signed-off-by: Carla Martinez --- .../MemberOf/MemberOfDeleteModalOld.tsx | 148 ++++++++++++++++++ src/pages/ActiveUsers/UserMemberOf.tsx | 2 +- src/pages/Hosts/HostsMemberOf.tsx | 2 +- src/pages/Services/ServicesMemberOf.tsx | 2 +- 4 files changed, 151 insertions(+), 3 deletions(-) create mode 100644 src/components/MemberOf/MemberOfDeleteModalOld.tsx diff --git a/src/components/MemberOf/MemberOfDeleteModalOld.tsx b/src/components/MemberOf/MemberOfDeleteModalOld.tsx new file mode 100644 index 00000000..2f8c9e3c --- /dev/null +++ b/src/components/MemberOf/MemberOfDeleteModalOld.tsx @@ -0,0 +1,148 @@ +import React, { useState } from "react"; +// PatternFly +import { + TextContent, + Text, + TextVariants, + Button, +} from "@patternfly/react-core"; +// Modals +import ModalWithFormLayout from "src/components/layouts/ModalWithFormLayout"; +// Tables +import MemberOfDeletedGroupsTable from "src/components/MemberOf/MemberOfDeletedGroupsTable"; + +// Although tabs data types habe been already defined, it is not possible to access to all +// its variables. Just the mandatory ones ('name' and 'description') are accessible at this point. +// To display all the possible data types for all the tabs (and not only the mandatory ones) +// an extra interface 'MemberOfElement' will be defined. This will be called in the 'PropsToTable' +// interface instead of each type (UserGroup | Netgroup | Roles | HBACRules | SudoRules). +interface MemberOfElement { + name: string; + gid?: string; + status?: string; + description: string; +} + +interface ModalData { + showModal: boolean; + handleModalToggle: () => void; +} + +interface ButtonData { + changeIsDeleteButtonDisabled: (updatedDeleteButton: boolean) => void; + updateIsDeletion: (option: boolean) => void; +} + +interface TabData { + tabName: string; + activeTabKey: number; +} + +interface PropsToDelete { + modalData: ModalData; + tabData: TabData; + groupNamesToDelete: string[]; + groupRepository: MemberOfElement[]; + updateGroupRepository: (args: MemberOfElement[]) => void; + buttonData: ButtonData; +} + +const MemberOfDeleteModal = (props: PropsToDelete) => { + // Given a single group name, obtain full info to be sent and shown on the deletion table + const getGroupInfoByName = (groupName: string) => { + const res = props.groupRepository.filter( + (group) => group.name === groupName + ); + return res[0]; + }; + + const getListOfGroupsToDelete = () => { + const groupsToDelete: MemberOfElement[] = []; + props.groupNamesToDelete.map((groupName) => + groupsToDelete.push(getGroupInfoByName(groupName)) + ); + return groupsToDelete; + }; + + // Groups to delete list + const [groupsToDelete] = useState(getListOfGroupsToDelete); + + // List of fields + const fields = [ + { + id: "question-text", + pfComponent: ( + + + Are you sure you want to remove the selected entries from the list? + + + ), + }, + { + id: "deleted-users-table", + pfComponent: ( + + ), + }, + ]; + + // Close modal + const closeModal = () => { + props.modalData.handleModalToggle(); + }; + + // Delete groups + const deleteGroups = () => { + // Define function that will be reused to delete the selected entries + let generalUpdatedGroupList = props.groupRepository; + props.groupNamesToDelete.map((groupName) => { + const updatedGroupList = generalUpdatedGroupList.filter( + (grp) => grp.name !== groupName + ); + // If not empty, replace groupList by new array + if (updatedGroupList) { + generalUpdatedGroupList = updatedGroupList; + } + }); + props.updateGroupRepository(generalUpdatedGroupList); + props.buttonData.changeIsDeleteButtonDisabled(true); + props.buttonData.updateIsDeletion(true); + closeModal(); + }; + + // Set the Modal and Action buttons for 'Delete' option + const modalActionsDelete: JSX.Element[] = [ + , + , + ]; + + // Render 'MemberOfDeleteModal' + return ( + + ); +}; + +export default MemberOfDeleteModal; diff --git a/src/pages/ActiveUsers/UserMemberOf.tsx b/src/pages/ActiveUsers/UserMemberOf.tsx index 0d2ced6c..fcd716b2 100644 --- a/src/pages/ActiveUsers/UserMemberOf.tsx +++ b/src/pages/ActiveUsers/UserMemberOf.tsx @@ -33,7 +33,7 @@ import { } from "src/utils/data/GroupRepositories"; // Modals import MemberOfAddModal from "src/components/MemberOf/MemberOfAddModalOld"; -import MemberOfDeleteModal from "src/components/MemberOf/MemberOfDeleteModal"; +import MemberOfDeleteModal from "src/components/MemberOf/MemberOfDeleteModalOld"; // Wrappers import MemberOfUserGroups from "src/components/MemberOf/MemberOfUserGroups"; diff --git a/src/pages/Hosts/HostsMemberOf.tsx b/src/pages/Hosts/HostsMemberOf.tsx index 968e0629..9a94442a 100644 --- a/src/pages/Hosts/HostsMemberOf.tsx +++ b/src/pages/Hosts/HostsMemberOf.tsx @@ -36,7 +36,7 @@ import { } from "src/utils/data/GroupRepositories"; // Modals import MemberOfAddModal from "src/components/MemberOf/MemberOfAddModalOld"; -import MemberOfDeleteModal from "src/components/MemberOf/MemberOfDeleteModal"; +import MemberOfDeleteModal from "src/components/MemberOf/MemberOfDeleteModalOld"; interface PropsToHostsMemberOf { host: Host; diff --git a/src/pages/Services/ServicesMemberOf.tsx b/src/pages/Services/ServicesMemberOf.tsx index bd4bdcba..78e878c8 100644 --- a/src/pages/Services/ServicesMemberOf.tsx +++ b/src/pages/Services/ServicesMemberOf.tsx @@ -22,7 +22,7 @@ import { useAppSelector } from "src/store/hooks"; import { servicesRolesInitialData } from "src/utils/data/GroupRepositories"; // Modals import MemberOfAddModal from "src/components/MemberOf/MemberOfAddModalOld"; -import MemberOfDeleteModal from "src/components/MemberOf/MemberOfDeleteModal"; +import MemberOfDeleteModal from "src/components/MemberOf/MemberOfDeleteModalOld"; interface PropsToServicesMemberOf { service: Service; From 5abf4bbde7ff88149eab4318c0205f86b1f6d357 Mon Sep 17 00:00:00 2001 From: Carla Martinez Date: Fri, 16 Feb 2024 13:20:54 +0100 Subject: [PATCH 3/7] Adapt 'Add' modal to user groups The new 'Add' modal should be adapted to the User groups data. Signed-off-by: Carla Martinez --- src/components/MemberOf/MemberOfAddModal.tsx | 245 ------------------ .../MemberOf/MemberOfAddModalUserGroups.tsx | 141 ++++++++++ .../MemberOf/MemberOfUserGroups.tsx | 77 ++++-- src/pages/ActiveUsers/UserMemberOf.tsx | 38 +-- 4 files changed, 210 insertions(+), 291 deletions(-) delete mode 100644 src/components/MemberOf/MemberOfAddModal.tsx create mode 100644 src/components/MemberOf/MemberOfAddModalUserGroups.tsx diff --git a/src/components/MemberOf/MemberOfAddModal.tsx b/src/components/MemberOf/MemberOfAddModal.tsx deleted file mode 100644 index 8a878ed8..00000000 --- a/src/components/MemberOf/MemberOfAddModal.tsx +++ /dev/null @@ -1,245 +0,0 @@ -import React, { ReactNode, useEffect, useState } from "react"; -// PatternFly -import { Button, DualListSelector } from "@patternfly/react-core"; -// Modals -import ModalWithFormLayout from "src/components/layouts/ModalWithFormLayout"; -// Data types -import { - UserGroup, - Netgroup, - Roles, - HBACRules, - SudoRules, - HostGroup, -} from "src/utils/datatypes/globalDataTypes"; - -interface ModalData { - showModal: boolean; - handleModalToggle: () => void; -} - -interface TabData { - tabName: string; - userName: string; // TODO: Change to a more generalistic name -} - -export interface PropsToAdd { - modalData: ModalData; - availableData: - | UserGroup[] - | Netgroup[] - | Roles[] - | HBACRules[] - | SudoRules[] - | HostGroup[]; - groupRepository: unknown[]; - updateGroupRepository: ( - args: - | UserGroup[] - | Netgroup[] - | Roles[] - | HBACRules[] - | SudoRules[] - | HostGroup[] - ) => void; - updateAvOptionsList: (args: unknown[]) => void; - tabData: TabData; -} - -// Although tabs data types habe been already defined, it is not possible to access to all -// its variables. Just the mandatory ones ('name' and 'description') are accessible at this point. -// To display all the possible data types for all the tabs (and not only the mandatory ones) -// an extra interface 'MemberOfElement' will be defined. This will be called when assigning -// a new group instead of refering to each type (UserGroup | Netgroup | Roles | HBACRules | -// SudoRules | HostGroup). -interface MemberOfElement { - hostGroup?: string; - name: string; - gid?: string; - status?: string; - description: string; -} - -const MemberOfAddModal = (props: PropsToAdd) => { - // Dual list data - const data = props.availableData.map((d) => d.name); - - // Dual list selector - const [availableOptions, setAvailableOptions] = useState(data); - const [chosenOptions, setChosenOptions] = useState([]); - - const listChange = ( - newAvailableOptions: ReactNode[], - newChosenOptions: ReactNode[] - ) => { - setAvailableOptions(newAvailableOptions.sort()); - setChosenOptions(newChosenOptions.sort()); - props.updateAvOptionsList(newAvailableOptions.sort()); - }; - - const fields = [ - { - id: "dual-list-selector", - pfComponent: ( - listChange(newAvailableOptions, newChosenOptions)} - id="basicSelectorWithSearch" - /> - ), - }, - ]; - - // When clean data, set to original values - const cleanData = () => { - setAvailableOptions(data); - setChosenOptions([]); - }; - - // Clean fields and close modal (To prevent data persistence when reopen modal) - const cleanAndCloseModal = () => { - cleanData(); - props.modalData.handleModalToggle(); - }; - - // Buttons are disabled until the user fills the required fields - const [buttonDisabled, setButtonDisabled] = useState(true); - useEffect(() => { - if (chosenOptions.length > 0) { - setButtonDisabled(false); - } else { - setButtonDisabled(true); - } - }, [chosenOptions]); - - // Get all info from a chosen option - const getInfoFromGroupData = (option: unknown) => { - return props.availableData.find((d) => option === d.name); - }; - - // Add group option - const onClickAddGroupHandler = () => { - chosenOptions.map((opt) => { - const optionData: MemberOfElement | undefined = getInfoFromGroupData(opt); - if (optionData !== undefined) { - // User groups - if (props.tabData.tabName === "User groups") { - props.groupRepository.push({ - name: optionData.name !== undefined && optionData.name, - description: - optionData.description !== undefined && optionData.description, - gid: optionData.gid !== undefined && optionData.gid, - status: optionData.status !== undefined && optionData.status, - } as UserGroup); - // Send updated data to table - props.updateGroupRepository(props.groupRepository as UserGroup[]); - } - // Netgroups - if (props.tabData.tabName === "Netgroups") { - props.groupRepository.push({ - name: optionData.name !== undefined && optionData.name, - description: - optionData.description !== undefined && optionData.description, - gid: optionData.gid !== undefined && optionData.gid, - status: optionData.status !== undefined && optionData.status, - } as Netgroup); - // Send updated data to table - props.updateGroupRepository(props.groupRepository as Netgroup[]); - } - // Roles - if (props.tabData.tabName === "Roles") { - props.groupRepository.push({ - name: optionData.name !== undefined && optionData.name, - description: - optionData.description !== undefined && optionData.description, - gid: optionData.gid !== undefined && optionData.gid, - status: optionData.status !== undefined && optionData.status, - } as Roles); - // Send updated data to table - props.updateGroupRepository(props.groupRepository as Roles[]); - } - // HBAC rules - if (props.tabData.tabName === "HBAC rules") { - props.groupRepository.push({ - name: optionData.name !== undefined && optionData.name, - description: - optionData.description !== undefined && optionData.description, - gid: optionData.gid !== undefined && optionData.gid, - status: optionData.status !== undefined && optionData.status, - } as HBACRules); - // Send updated data to table - props.updateGroupRepository(props.groupRepository as HBACRules[]); - } - // Sudo rules - if (props.tabData.tabName === "Sudo rules") { - props.groupRepository.push({ - name: optionData.name !== undefined && optionData.name, - description: - optionData.description !== undefined && optionData.description, - gid: optionData.gid !== undefined && optionData.gid, - status: optionData.status !== undefined && optionData.status, - } as SudoRules); - // Send updated data to table - props.updateGroupRepository(props.groupRepository as SudoRules[]); - } - // Host groups - if (props.tabData.tabName === "Host groups") { - props.groupRepository.push({ - name: optionData.name !== undefined && optionData.name, - description: - optionData.description !== undefined && optionData.description, - } as HostGroup); - // Send updated data to table - props.updateGroupRepository(props.groupRepository as HostGroup[]); - } - } - }); - // Clean chosen options and close modal - setChosenOptions([]); - props.modalData.handleModalToggle(); - }; - - // Buttons that will be shown at the end of the form - const modalActions = [ - , - , - ]; - - // Render 'MemberOfaddModal' - return ( - - ); -}; - -export default MemberOfAddModal; diff --git a/src/components/MemberOf/MemberOfAddModalUserGroups.tsx b/src/components/MemberOf/MemberOfAddModalUserGroups.tsx new file mode 100644 index 00000000..b5e171d0 --- /dev/null +++ b/src/components/MemberOf/MemberOfAddModalUserGroups.tsx @@ -0,0 +1,141 @@ +import React, { ReactNode, useEffect, useState } from "react"; +// PatternFly +import { + Button, + DualListSelector, + Form, + FormGroup, + Modal, +} from "@patternfly/react-core"; +// Data types +import { UserGroup } from "src/utils/datatypes/globalDataTypes"; + +export interface PropsToAdd { + showModal: boolean; + onCloseModal: () => void; + availableData: UserGroup[]; + groupRepository: unknown[]; + updateGroupRepository: (newList: UserGroup[]) => void; +} + +const MemberOfAddModal = (props: PropsToAdd) => { + // Dual list data + const data = props.availableData.map((d) => d.name); + + // Dual list selector + const [availableOptions, setAvailableOptions] = useState(data); + const [chosenOptions, setChosenOptions] = useState([]); + + const listChange = ( + newAvailableOptions: ReactNode[], + newChosenOptions: ReactNode[] + ) => { + setAvailableOptions(newAvailableOptions.sort()); + setChosenOptions(newChosenOptions.sort()); + }; + + const fields = [ + { + id: "dual-list-selector", + name: "Available options", + pfComponent: ( + listChange(newAvailableOptions, newChosenOptions)} + id="basicSelectorWithSearch" + /> + ), + }, + ]; + + // When clean data, set to original values + const cleanData = () => { + setAvailableOptions(data); + setChosenOptions([]); + }; + + // Clean fields and close modal (To prevent data persistence when reopen modal) + const cleanAndCloseModal = () => { + cleanData(); + props.onCloseModal(); + }; + + // Buttons are disabled until the user fills the required fields + const [buttonDisabled, setButtonDisabled] = useState(true); + useEffect(() => { + if (chosenOptions.length > 0) { + setButtonDisabled(false); + } else { + setButtonDisabled(true); + } + }, [chosenOptions]); + + // Get all info from a chosen option + const getInfoFromGroupData = (option: unknown) => { + return props.availableData.find((d) => option === d.name); + }; + + // Add group option + const onClickAddGroupHandler = () => { + chosenOptions.map((opt) => { + const optionData: UserGroup | undefined = getInfoFromGroupData(opt); + if (optionData !== undefined) { + props.groupRepository.push({ + name: optionData.name !== undefined && optionData.name, + description: + optionData.description !== undefined && optionData.description, + gid: optionData.gid !== undefined && optionData.gid, + } as UserGroup); + // Send updated data to table + props.updateGroupRepository(props.groupRepository as UserGroup[]); + } + }); + // Clean chosen options and close modal + setChosenOptions([]); + props.onCloseModal(); + }; + + // Buttons that will be shown at the end of the form + const modalActions = [ + , + , + ]; + + return ( + +
+ {fields.map((field) => ( + + {field.pfComponent} + + ))} +
+
+ ); +}; + +export default MemberOfAddModal; diff --git a/src/components/MemberOf/MemberOfUserGroups.tsx b/src/components/MemberOf/MemberOfUserGroups.tsx index 9b5ea582..5aebce8e 100644 --- a/src/components/MemberOf/MemberOfUserGroups.tsx +++ b/src/components/MemberOf/MemberOfUserGroups.tsx @@ -1,19 +1,16 @@ import React from "react"; -// Repositories -import { userGroupsInitialData } from "src/utils/data/GroupRepositories"; +// PatternFly +import { Pagination, PaginationVariant } from "@patternfly/react-core"; // Data types import { UserGroup } from "src/utils/datatypes/globalDataTypes"; +// Redux +import { useAppSelector } from "src/store/hooks"; // Components import MemberOfToolbarUserGroups, { MembershipDirection, } from "./MemberOfToolbar"; import MemberOfUserGroupsTable from "./MemberOfTableUserGroups"; -import { Pagination, PaginationVariant } from "@patternfly/react-core"; - -interface MemberOfUserGroupsProps { - showAddModal: () => void; - showDeleteModal: () => void; -} +import MemberOfAddModal from "./MemberOfAddModalUserGroups"; function paginate(array: Type[], page: number, perPage: number): Type[] { const startIdx = (page - 1) * perPage; @@ -21,7 +18,35 @@ function paginate(array: Type[], page: number, perPage: number): Type[] { return array.slice(startIdx, endIdx); } +interface TypeWithName { + name: string; +} + +// Filter functions to compare the available data with the data that +// the user is already member of. This is done to prevent duplicates +// (e.g: adding the same element twice). +function filterUserGroupsData( + list1: Array, + list2: Array +): Type[] { + // User groups + return list1.filter((item) => { + return !list2.some((itm) => { + return item.name === itm.name; + }); + }); +} + +interface MemberOfUserGroupsProps { + usersGroupsFromUser: UserGroup[]; + updateUsersGroupsFromUser: (newList: UserGroup[]) => void; +} + const MemberOfUserGroups = (props: MemberOfUserGroupsProps) => { + const userGroupsFullList = useAppSelector( + (state) => state.usergroups.userGroupList + ); + const [groupsNamesSelected, setGroupsNamesSelected] = React.useState< string[] >([]); @@ -31,17 +56,22 @@ const MemberOfUserGroups = (props: MemberOfUserGroupsProps) => { const [searchValue, setSearchValue] = React.useState(""); - const [usersGroupsFromUser] = React.useState( - userGroupsInitialData - ); - const [membershipDirection, setMembershipDirection] = React.useState("direct"); + const [showAddModal, setShowAddModal] = React.useState(false); + const [showDeleteModal, setShowDeleteModal] = React.useState(false); + // Computed "states" const someItemSelected = groupsNamesSelected.length > 0; - const shownUserGroups = paginate(usersGroupsFromUser, page, perPage); - const showTableRows = usersGroupsFromUser.length > 0; + const shownUserGroups = paginate(props.usersGroupsFromUser, page, perPage); + const showTableRows = props.usersGroupsFromUser.length > 0; + + // Available data to be added as member of + const userGroupsFilteredData: UserGroup[] = filterUserGroupsData( + userGroupsFullList, + props.usersGroupsFromUser + ); return ( <> @@ -50,14 +80,14 @@ const MemberOfUserGroups = (props: MemberOfUserGroupsProps) => { onSearchTextChange={setSearchValue} refreshButtonEnabled={true} deleteButtonEnabled={someItemSelected} - onDeleteButtonClick={props.showDeleteModal} + onDeleteButtonClick={() => setShowDeleteModal(true)} addButtonEnabled={true} - onAddButtonClick={props.showAddModal} + onAddButtonClick={() => setShowAddModal(true)} membershipDirectionEnabled={true} membershipDirection={membershipDirection} onMembershipDirectionChange={setMembershipDirection} helpIconEnabled={true} - totalItems={usersGroupsFromUser.length} + totalItems={props.usersGroupsFromUser.length} perPage={perPage} page={page} onPerPageChange={setPerPage} @@ -71,7 +101,7 @@ const MemberOfUserGroups = (props: MemberOfUserGroupsProps) => { /> { onSetPage={(_e, page) => setPage(page)} onPerPageSelect={(_e, perPage) => setPerPage(perPage)} /> + {showAddModal && ( + setShowAddModal(false)} + availableData={userGroupsFilteredData} + groupRepository={props.usersGroupsFromUser} + updateGroupRepository={(newList: UserGroup[]) => + props.updateUsersGroupsFromUser(newList) + } + /> + )} ); }; diff --git a/src/pages/ActiveUsers/UserMemberOf.tsx b/src/pages/ActiveUsers/UserMemberOf.tsx index fcd716b2..def78050 100644 --- a/src/pages/ActiveUsers/UserMemberOf.tsx +++ b/src/pages/ActiveUsers/UserMemberOf.tsx @@ -43,18 +43,12 @@ interface PropsToUserMemberOf { const UserMemberOf = (props: PropsToUserMemberOf) => { // Retrieve each group list from Redux: - let userGroupsList = useAppSelector( - (state) => state.usergroups.userGroupList - ); let netgroupsList = useAppSelector((state) => state.netgroups.netgroupList); let rolesList = useAppSelector((state) => state.roles.roleList); let hbacRulesList = useAppSelector((state) => state.hbacrules.hbacRulesList); let sudoRulesList = useAppSelector((state) => state.sudorules.sudoRulesList); // Alter the available options list to keep the state of the recently added / removed items - const updateUserGroupsList = (newAvOptionsList: unknown[]) => { - userGroupsList = newAvOptionsList as UserGroup[]; - }; const updateNetgroupsList = (newAvOptionsList: unknown[]) => { netgroupsList = newAvOptionsList as Netgroup[]; }; @@ -69,7 +63,9 @@ const UserMemberOf = (props: PropsToUserMemberOf) => { }; // List of default dummy data (for each tab option) - const [userGroupsRepository] = useState(userGroupsInitialData); + const [userGroupsRepository, setUserGroupsRepository] = useState( + userGroupsInitialData + ); const [netgroupsRepository, setNetgroupsRepository] = useState(netgroupsInitialData); const [rolesRepository, setRolesRepository] = useState(rolesInitialData); @@ -88,14 +84,6 @@ const UserMemberOf = (props: PropsToUserMemberOf) => { // Filter functions to compare the available data with the data that // the user is already member of. This is done to prevent duplicates // (e.g: adding the same element twice). - const filterUserGroupsData = () => { - // User groups - return userGroupsList.filter((item) => { - return !userGroupsRepository.some((itm) => { - return item.name === itm.name; - }); - }); - }; const filterNetgroupsData = () => { // Netgroups return netgroupsList.filter((item) => { @@ -130,7 +118,6 @@ const UserMemberOf = (props: PropsToUserMemberOf) => { }; // Available data to be added as member of - const userGroupsFilteredData: UserGroup[] = filterUserGroupsData(); const netgroupsFilteredData: Netgroup[] = filterNetgroupsData(); const rolesFilteredData: Roles[] = filterRolesData(); const hbacRulesFilteredData: HBACRules[] = filterHbacRulesData(); @@ -155,12 +142,7 @@ const UserMemberOf = (props: PropsToUserMemberOf) => { // - The slice of data to show (considering the pagination) // - Number of items for a specific list const updateGroupRepository = ( - groupRepository: - | UserGroup[] - | Netgroup[] - | Roles[] - | HBACRules[] - | SudoRules[] + groupRepository: Netgroup[] | Roles[] | HBACRules[] | SudoRules[] ) => { switch (tabName) { case "Netgroups": @@ -316,7 +298,7 @@ const UserMemberOf = (props: PropsToUserMemberOf) => { const onClickAddHandler = () => { setShowAddModal(true); }; - const onModalToggle = () => { + const onAddModalToggle = () => { setShowAddModal(!showAddModal); }; @@ -397,7 +379,7 @@ const UserMemberOf = (props: PropsToUserMemberOf) => { // - MemberOfAddModal const addModalData = { showModal: showAddModal, - handleModalToggle: onModalToggle, + handleModalToggle: onAddModalToggle, }; const tabData = { @@ -450,8 +432,8 @@ const UserMemberOf = (props: PropsToUserMemberOf) => { } > { - {tabName === "User groups" && ( + {/* {tabName === "User groups" && ( <> {showAddModal && ( { /> )} - )} + )} */} {tabName === "Netgroups" && ( <> {showAddModal && ( From 31e2bf4baf9fbe4feeed74a0fd01e24d61fcea38 Mon Sep 17 00:00:00 2001 From: Carla Martinez Date: Fri, 16 Feb 2024 14:49:15 +0100 Subject: [PATCH 4/7] Adapt 'Delete' modal to user groups The 'Delete' modal functionality must be adapted to work with the User groups data. Signed-off-by: Carla Martinez --- .../MemberOf/MemberOfDeleteModal.tsx | 148 ------------------ .../MemberOfDeleteModalUserGroups.tsx | 115 ++++++++++++++ .../MemberOf/MemberOfUserGroups.tsx | 12 ++ src/pages/ActiveUsers/UserMemberOf.tsx | 24 --- 4 files changed, 127 insertions(+), 172 deletions(-) delete mode 100644 src/components/MemberOf/MemberOfDeleteModal.tsx create mode 100644 src/components/MemberOf/MemberOfDeleteModalUserGroups.tsx diff --git a/src/components/MemberOf/MemberOfDeleteModal.tsx b/src/components/MemberOf/MemberOfDeleteModal.tsx deleted file mode 100644 index 2f8c9e3c..00000000 --- a/src/components/MemberOf/MemberOfDeleteModal.tsx +++ /dev/null @@ -1,148 +0,0 @@ -import React, { useState } from "react"; -// PatternFly -import { - TextContent, - Text, - TextVariants, - Button, -} from "@patternfly/react-core"; -// Modals -import ModalWithFormLayout from "src/components/layouts/ModalWithFormLayout"; -// Tables -import MemberOfDeletedGroupsTable from "src/components/MemberOf/MemberOfDeletedGroupsTable"; - -// Although tabs data types habe been already defined, it is not possible to access to all -// its variables. Just the mandatory ones ('name' and 'description') are accessible at this point. -// To display all the possible data types for all the tabs (and not only the mandatory ones) -// an extra interface 'MemberOfElement' will be defined. This will be called in the 'PropsToTable' -// interface instead of each type (UserGroup | Netgroup | Roles | HBACRules | SudoRules). -interface MemberOfElement { - name: string; - gid?: string; - status?: string; - description: string; -} - -interface ModalData { - showModal: boolean; - handleModalToggle: () => void; -} - -interface ButtonData { - changeIsDeleteButtonDisabled: (updatedDeleteButton: boolean) => void; - updateIsDeletion: (option: boolean) => void; -} - -interface TabData { - tabName: string; - activeTabKey: number; -} - -interface PropsToDelete { - modalData: ModalData; - tabData: TabData; - groupNamesToDelete: string[]; - groupRepository: MemberOfElement[]; - updateGroupRepository: (args: MemberOfElement[]) => void; - buttonData: ButtonData; -} - -const MemberOfDeleteModal = (props: PropsToDelete) => { - // Given a single group name, obtain full info to be sent and shown on the deletion table - const getGroupInfoByName = (groupName: string) => { - const res = props.groupRepository.filter( - (group) => group.name === groupName - ); - return res[0]; - }; - - const getListOfGroupsToDelete = () => { - const groupsToDelete: MemberOfElement[] = []; - props.groupNamesToDelete.map((groupName) => - groupsToDelete.push(getGroupInfoByName(groupName)) - ); - return groupsToDelete; - }; - - // Groups to delete list - const [groupsToDelete] = useState(getListOfGroupsToDelete); - - // List of fields - const fields = [ - { - id: "question-text", - pfComponent: ( - - - Are you sure you want to remove the selected entries from the list? - - - ), - }, - { - id: "deleted-users-table", - pfComponent: ( - - ), - }, - ]; - - // Close modal - const closeModal = () => { - props.modalData.handleModalToggle(); - }; - - // Delete groups - const deleteGroups = () => { - // Define function that will be reused to delete the selected entries - let generalUpdatedGroupList = props.groupRepository; - props.groupNamesToDelete.map((groupName) => { - const updatedGroupList = generalUpdatedGroupList.filter( - (grp) => grp.name !== groupName - ); - // If not empty, replace groupList by new array - if (updatedGroupList) { - generalUpdatedGroupList = updatedGroupList; - } - }); - props.updateGroupRepository(generalUpdatedGroupList); - props.buttonData.changeIsDeleteButtonDisabled(true); - props.buttonData.updateIsDeletion(true); - closeModal(); - }; - - // Set the Modal and Action buttons for 'Delete' option - const modalActionsDelete: JSX.Element[] = [ - , - , - ]; - - // Render 'MemberOfDeleteModal' - return ( - - ); -}; - -export default MemberOfDeleteModal; diff --git a/src/components/MemberOf/MemberOfDeleteModalUserGroups.tsx b/src/components/MemberOf/MemberOfDeleteModalUserGroups.tsx new file mode 100644 index 00000000..7e983c20 --- /dev/null +++ b/src/components/MemberOf/MemberOfDeleteModalUserGroups.tsx @@ -0,0 +1,115 @@ +import React from "react"; +// PatternFly +import { + TextContent, + Text, + TextVariants, + Button, + Modal, + Form, + FormGroup, +} from "@patternfly/react-core"; +// Tables +import MemberOfDeletedGroupsTable from "src/components/MemberOf/MemberOfDeletedGroupsTable"; +// Data types +import { UserGroup } from "src/utils/datatypes/globalDataTypes"; + +interface PropsToDelete { + showModal: boolean; + onCloseModal: () => void; + tabName: string; + groupNamesToDelete: string[]; + groupRepository: UserGroup[]; + updateGroupRepository: (args: UserGroup[]) => void; + updateGroupNamesToDelete: (args: string[]) => void; +} + +const MemberOfDeleteModal = (props: PropsToDelete) => { + // Given a single group name, obtain full info to be sent and shown on the deletion table + const getGroupInfoByName = (groupName: string) => { + const res = props.groupRepository.filter( + (group) => group.name === groupName + ); + return res[0]; + }; + + // Obtain full info of groups to delete + const getListOfGroupsToDelete = () => { + const groupsToDelete: UserGroup[] = []; + props.groupNamesToDelete.map((groupName) => + groupsToDelete.push(getGroupInfoByName(groupName)) + ); + return groupsToDelete; + }; + + // Groups to delete list + const groupsToDelete: UserGroup[] = getListOfGroupsToDelete(); + + // Delete groups + const deleteGroups = () => { + let generalUpdatedGroupList = props.groupRepository; + props.groupNamesToDelete.map((groupName) => { + const updatedGroupList = generalUpdatedGroupList.filter( + (grp) => grp.name !== groupName + ); + // If not empty, replace groupList by new array + if (updatedGroupList) { + generalUpdatedGroupList = updatedGroupList; + } + }); + props.updateGroupRepository(generalUpdatedGroupList); + props.updateGroupNamesToDelete([]); + props.onCloseModal(); + }; + + // Modal actions + const modalActionsDelete: JSX.Element[] = [ + , + , + ]; + + return ( + +
+ + + + Are you sure you want to remove the selected entries from the + list? + + + + + + +
+
+ ); +}; + +export default MemberOfDeleteModal; diff --git a/src/components/MemberOf/MemberOfUserGroups.tsx b/src/components/MemberOf/MemberOfUserGroups.tsx index 5aebce8e..b8dc3182 100644 --- a/src/components/MemberOf/MemberOfUserGroups.tsx +++ b/src/components/MemberOf/MemberOfUserGroups.tsx @@ -11,6 +11,7 @@ import MemberOfToolbarUserGroups, { } from "./MemberOfToolbar"; import MemberOfUserGroupsTable from "./MemberOfTableUserGroups"; import MemberOfAddModal from "./MemberOfAddModalUserGroups"; +import MemberOfDeleteModal from "./MemberOfDeleteModalUserGroups"; function paginate(array: Type[], page: number, perPage: number): Type[] { const startIdx = (page - 1) * perPage; @@ -120,6 +121,17 @@ const MemberOfUserGroups = (props: MemberOfUserGroupsProps) => { } /> )} + {showDeleteModal && someItemSelected && ( + setShowDeleteModal(false)} + tabName="User groups" + groupNamesToDelete={groupsNamesSelected} + updateGroupNamesToDelete={setGroupsNamesSelected} + groupRepository={props.usersGroupsFromUser} + updateGroupRepository={props.updateUsersGroupsFromUser} + /> + )} ); }; diff --git a/src/pages/ActiveUsers/UserMemberOf.tsx b/src/pages/ActiveUsers/UserMemberOf.tsx index def78050..5c16b242 100644 --- a/src/pages/ActiveUsers/UserMemberOf.tsx +++ b/src/pages/ActiveUsers/UserMemberOf.tsx @@ -566,30 +566,6 @@ const UserMemberOf = (props: PropsToUserMemberOf) => { - {/* {tabName === "User groups" && ( - <> - {showAddModal && ( - - )} - {showDeleteModal && groupsNamesSelected.length !== 0 && ( - - )} - - )} */} {tabName === "Netgroups" && ( <> {showAddModal && ( From 0cea82c620751ede83d50dccff6bd3f87ab0feef Mon Sep 17 00:00:00 2001 From: Carla Martinez Date: Fri, 16 Feb 2024 17:20:33 +0100 Subject: [PATCH 5/7] Prepare endpoint, hook, and data Previous preparation of some elements is needed: - The `User` data type needs the following parameters that will be managed in the 'Is a member of' section: - `memberof_group` - `memberof_netgroup` - `memberof_role` - `memberof_hbacrule` - `memberof_sudorule` - `memberof_subid` - The `useGettingGroupsQuery` endpoint wrapper to make the API call - The original `UserGroup` data type has been replaced by the `UserGroupOld` one. All the files that use that type have been adapted - The new `UserGroup` type will contain the new data structure. - The `useUserMemberOfData` hook to retrieve and parse the data - The `normalizeString` helper function to normalize string LDAP values Signed-off-by: Carla Martinez --- .../MemberOf/MemberOfAddModalOld.tsx | 12 ++-- .../MemberOf/MemberOfAddModalUserGroups.tsx | 12 ++-- .../MemberOf/MemberOfDeleteModalOld.tsx | 2 +- .../MemberOfDeleteModalUserGroups.tsx | 10 +-- .../MemberOf/MemberOfTableUserGroups.tsx | 6 +- .../MemberOf/MemberOfToolbarOld.tsx | 8 +-- .../MemberOf/MemberOfUserGroups.tsx | 10 +-- src/hooks/useUserMemberOfData.tsx | 67 +++++++++++++++++++ src/pages/ActiveUsers/UserMemberOf.tsx | 4 +- src/services/rpc.ts | 22 +++++- src/store/Identity/userGroups-slice.ts | 4 +- src/utils/data/GroupRepositories.ts | 4 +- src/utils/datatypes/globalDataTypes.ts | 15 ++++- src/utils/userUtils.tsx | 4 ++ src/utils/utils.tsx | 19 ++++++ 15 files changed, 160 insertions(+), 39 deletions(-) create mode 100644 src/hooks/useUserMemberOfData.tsx diff --git a/src/components/MemberOf/MemberOfAddModalOld.tsx b/src/components/MemberOf/MemberOfAddModalOld.tsx index 8a878ed8..05c88e57 100644 --- a/src/components/MemberOf/MemberOfAddModalOld.tsx +++ b/src/components/MemberOf/MemberOfAddModalOld.tsx @@ -5,7 +5,7 @@ import { Button, DualListSelector } from "@patternfly/react-core"; import ModalWithFormLayout from "src/components/layouts/ModalWithFormLayout"; // Data types import { - UserGroup, + UserGroupOld, Netgroup, Roles, HBACRules, @@ -26,7 +26,7 @@ interface TabData { export interface PropsToAdd { modalData: ModalData; availableData: - | UserGroup[] + | UserGroupOld[] | Netgroup[] | Roles[] | HBACRules[] @@ -35,7 +35,7 @@ export interface PropsToAdd { groupRepository: unknown[]; updateGroupRepository: ( args: - | UserGroup[] + | UserGroupOld[] | Netgroup[] | Roles[] | HBACRules[] @@ -50,7 +50,7 @@ export interface PropsToAdd { // its variables. Just the mandatory ones ('name' and 'description') are accessible at this point. // To display all the possible data types for all the tabs (and not only the mandatory ones) // an extra interface 'MemberOfElement' will be defined. This will be called when assigning -// a new group instead of refering to each type (UserGroup | Netgroup | Roles | HBACRules | +// a new group instead of refering to each type (UserGroupOld | Netgroup | Roles | HBACRules | // SudoRules | HostGroup). interface MemberOfElement { hostGroup?: string; @@ -136,9 +136,9 @@ const MemberOfAddModal = (props: PropsToAdd) => { optionData.description !== undefined && optionData.description, gid: optionData.gid !== undefined && optionData.gid, status: optionData.status !== undefined && optionData.status, - } as UserGroup); + } as UserGroupOld); // Send updated data to table - props.updateGroupRepository(props.groupRepository as UserGroup[]); + props.updateGroupRepository(props.groupRepository as UserGroupOld[]); } // Netgroups if (props.tabData.tabName === "Netgroups") { diff --git a/src/components/MemberOf/MemberOfAddModalUserGroups.tsx b/src/components/MemberOf/MemberOfAddModalUserGroups.tsx index b5e171d0..6b85c9bf 100644 --- a/src/components/MemberOf/MemberOfAddModalUserGroups.tsx +++ b/src/components/MemberOf/MemberOfAddModalUserGroups.tsx @@ -8,14 +8,14 @@ import { Modal, } from "@patternfly/react-core"; // Data types -import { UserGroup } from "src/utils/datatypes/globalDataTypes"; +import { UserGroupOld } from "src/utils/datatypes/globalDataTypes"; export interface PropsToAdd { showModal: boolean; onCloseModal: () => void; - availableData: UserGroup[]; + availableData: UserGroupOld[]; groupRepository: unknown[]; - updateGroupRepository: (newList: UserGroup[]) => void; + updateGroupRepository: (newList: UserGroupOld[]) => void; } const MemberOfAddModal = (props: PropsToAdd) => { @@ -84,16 +84,16 @@ const MemberOfAddModal = (props: PropsToAdd) => { // Add group option const onClickAddGroupHandler = () => { chosenOptions.map((opt) => { - const optionData: UserGroup | undefined = getInfoFromGroupData(opt); + const optionData: UserGroupOld | undefined = getInfoFromGroupData(opt); if (optionData !== undefined) { props.groupRepository.push({ name: optionData.name !== undefined && optionData.name, description: optionData.description !== undefined && optionData.description, gid: optionData.gid !== undefined && optionData.gid, - } as UserGroup); + } as UserGroupOld); // Send updated data to table - props.updateGroupRepository(props.groupRepository as UserGroup[]); + props.updateGroupRepository(props.groupRepository as UserGroupOld[]); } }); // Clean chosen options and close modal diff --git a/src/components/MemberOf/MemberOfDeleteModalOld.tsx b/src/components/MemberOf/MemberOfDeleteModalOld.tsx index 2f8c9e3c..ea47fc77 100644 --- a/src/components/MemberOf/MemberOfDeleteModalOld.tsx +++ b/src/components/MemberOf/MemberOfDeleteModalOld.tsx @@ -15,7 +15,7 @@ import MemberOfDeletedGroupsTable from "src/components/MemberOf/MemberOfDeletedG // its variables. Just the mandatory ones ('name' and 'description') are accessible at this point. // To display all the possible data types for all the tabs (and not only the mandatory ones) // an extra interface 'MemberOfElement' will be defined. This will be called in the 'PropsToTable' -// interface instead of each type (UserGroup | Netgroup | Roles | HBACRules | SudoRules). +// interface instead of each type (UserGroupOld | Netgroup | Roles | HBACRules | SudoRules). interface MemberOfElement { name: string; gid?: string; diff --git a/src/components/MemberOf/MemberOfDeleteModalUserGroups.tsx b/src/components/MemberOf/MemberOfDeleteModalUserGroups.tsx index 7e983c20..1eb7e95c 100644 --- a/src/components/MemberOf/MemberOfDeleteModalUserGroups.tsx +++ b/src/components/MemberOf/MemberOfDeleteModalUserGroups.tsx @@ -12,15 +12,15 @@ import { // Tables import MemberOfDeletedGroupsTable from "src/components/MemberOf/MemberOfDeletedGroupsTable"; // Data types -import { UserGroup } from "src/utils/datatypes/globalDataTypes"; +import { UserGroupOld } from "src/utils/datatypes/globalDataTypes"; interface PropsToDelete { showModal: boolean; onCloseModal: () => void; tabName: string; groupNamesToDelete: string[]; - groupRepository: UserGroup[]; - updateGroupRepository: (args: UserGroup[]) => void; + groupRepository: UserGroupOld[]; + updateGroupRepository: (args: UserGroupOld[]) => void; updateGroupNamesToDelete: (args: string[]) => void; } @@ -35,7 +35,7 @@ const MemberOfDeleteModal = (props: PropsToDelete) => { // Obtain full info of groups to delete const getListOfGroupsToDelete = () => { - const groupsToDelete: UserGroup[] = []; + const groupsToDelete: UserGroupOld[] = []; props.groupNamesToDelete.map((groupName) => groupsToDelete.push(getGroupInfoByName(groupName)) ); @@ -43,7 +43,7 @@ const MemberOfDeleteModal = (props: PropsToDelete) => { }; // Groups to delete list - const groupsToDelete: UserGroup[] = getListOfGroupsToDelete(); + const groupsToDelete: UserGroupOld[] = getListOfGroupsToDelete(); // Delete groups const deleteGroups = () => { diff --git a/src/components/MemberOf/MemberOfTableUserGroups.tsx b/src/components/MemberOf/MemberOfTableUserGroups.tsx index f084cc3a..636a8ee3 100644 --- a/src/components/MemberOf/MemberOfTableUserGroups.tsx +++ b/src/components/MemberOf/MemberOfTableUserGroups.tsx @@ -2,13 +2,13 @@ import React from "react"; // PatternFly import { Table, Tr, Th, Td, Thead, Tbody } from "@patternfly/react-table"; // Data types -import { UserGroup } from "src/utils/datatypes/globalDataTypes"; +import { UserGroupOld } from "src/utils/datatypes/globalDataTypes"; // Components import SkeletonOnTableLayout from "../layouts/Skeleton/SkeletonOnTableLayout"; import EmptyBodyTable from "../tables/EmptyBodyTable"; export interface MemberOfUserGroupsTableProps { - userGroups: UserGroup[]; + userGroups: UserGroupOld[]; checkedItems: string[]; onCheckItemsChange: (checkedItems: string[]) => void; showTableRows: boolean; @@ -16,7 +16,7 @@ export interface MemberOfUserGroupsTableProps { // Body const UserGroupsTableBody = (props: { - userGroups: UserGroup[]; + userGroups: UserGroupOld[]; checkedItems: string[]; onCheckboxChange: (checked: boolean, groupName: string) => void; }) => { diff --git a/src/components/MemberOf/MemberOfToolbarOld.tsx b/src/components/MemberOf/MemberOfToolbarOld.tsx index 9bff5c9a..aec02dd8 100644 --- a/src/components/MemberOf/MemberOfToolbarOld.tsx +++ b/src/components/MemberOf/MemberOfToolbarOld.tsx @@ -21,7 +21,7 @@ import ToolbarLayout, { } from "src/components/layouts/ToolbarLayout"; // Data types import { - UserGroup, + UserGroupOld, Netgroup, Roles, HBACRules, @@ -57,7 +57,7 @@ interface ButtonData { interface SettersData { changeMemberGroupsList: ( arg: - | UserGroup[] + | UserGroupOld[] | Netgroup[] | Roles[] | HBACRules[] @@ -74,14 +74,14 @@ interface SearchValueData { export interface PropsToToolbar { pageRepo: - | UserGroup[] + | UserGroupOld[] | Netgroup[] | Roles[] | HBACRules[] | SudoRules[] | HostGroup[]; shownItems: - | UserGroup[] + | UserGroupOld[] | Netgroup[] | Roles[] | HBACRules[] diff --git a/src/components/MemberOf/MemberOfUserGroups.tsx b/src/components/MemberOf/MemberOfUserGroups.tsx index b8dc3182..6b3b4719 100644 --- a/src/components/MemberOf/MemberOfUserGroups.tsx +++ b/src/components/MemberOf/MemberOfUserGroups.tsx @@ -2,7 +2,7 @@ import React from "react"; // PatternFly import { Pagination, PaginationVariant } from "@patternfly/react-core"; // Data types -import { UserGroup } from "src/utils/datatypes/globalDataTypes"; +import { UserGroupOld } from "src/utils/datatypes/globalDataTypes"; // Redux import { useAppSelector } from "src/store/hooks"; // Components @@ -39,8 +39,8 @@ function filterUserGroupsData( } interface MemberOfUserGroupsProps { - usersGroupsFromUser: UserGroup[]; - updateUsersGroupsFromUser: (newList: UserGroup[]) => void; + usersGroupsFromUser: UserGroupOld[]; + updateUsersGroupsFromUser: (newList: UserGroupOld[]) => void; } const MemberOfUserGroups = (props: MemberOfUserGroupsProps) => { @@ -69,7 +69,7 @@ const MemberOfUserGroups = (props: MemberOfUserGroupsProps) => { const showTableRows = props.usersGroupsFromUser.length > 0; // Available data to be added as member of - const userGroupsFilteredData: UserGroup[] = filterUserGroupsData( + const userGroupsFilteredData: UserGroupOld[] = filterUserGroupsData( userGroupsFullList, props.usersGroupsFromUser ); @@ -116,7 +116,7 @@ const MemberOfUserGroups = (props: MemberOfUserGroupsProps) => { onCloseModal={() => setShowAddModal(false)} availableData={userGroupsFilteredData} groupRepository={props.usersGroupsFromUser} - updateGroupRepository={(newList: UserGroup[]) => + updateGroupRepository={(newList: UserGroupOld[]) => props.updateUsersGroupsFromUser(newList) } /> diff --git a/src/hooks/useUserMemberOfData.tsx b/src/hooks/useUserMemberOfData.tsx new file mode 100644 index 00000000..8fcaf628 --- /dev/null +++ b/src/hooks/useUserMemberOfData.tsx @@ -0,0 +1,67 @@ +// RPC +import React from "react"; +import { BatchRPCResponse, useGettingGroupsQuery } from "src/services/rpc"; +// Data types +import { UserGroup } from "src/utils/datatypes/globalDataTypes"; +// Utils +import { API_VERSION_BACKUP, normalizeString } from "src/utils/utils"; + +type MemberOfData = { + isLoading: boolean; + isFetching: boolean; + refetch: () => void; + userGroupsFullList: UserGroup[]; +}; + +const useUserMemberOfData = ({ firstUserIdx, lastUserIdx }): MemberOfData => { + // [API call] User groups + // TODO: Normalize data to prevent array of arrays + const userGroupsQuery = useGettingGroupsQuery({ + searchValue: "", + sizeLimit: 0, + apiVersion: API_VERSION_BACKUP, + startIdx: firstUserIdx, + stopIdx: lastUserIdx, + }); + + const [userGroupsFullList, setUserGroupsFullList] = React.useState< + UserGroup[] + >([]); + const userGroupsData = userGroupsQuery.data || {}; + const isUserGroupsLoading = userGroupsQuery.isLoading; + + React.useEffect(() => { + if (userGroupsData !== undefined && !userGroupsQuery.isFetching) { + const dataParsed = userGroupsData as BatchRPCResponse; + const count = dataParsed.result.count; + const results = dataParsed.result.results; + + const userGroupsTempList: UserGroup[] = []; + + for (let i = 0; i < count; i++) { + userGroupsTempList.push({ + cn: normalizeString(results[i].result.cn), + gidnumber: normalizeString(results[i].result.gidnumber), + description: normalizeString(results[i].result.description), + dn: results[i].result.dn, + }); + } + setUserGroupsFullList(userGroupsTempList); + } + }, [userGroupsData, userGroupsQuery.isFetching]); + + // [API call] Refresh + const refetch = () => { + userGroupsQuery.refetch(); + }; + + // Return data + return { + isFetching: userGroupsQuery.isFetching, + isLoading: isUserGroupsLoading, + refetch, + userGroupsFullList, + } as MemberOfData; +}; + +export { useUserMemberOfData }; diff --git a/src/pages/ActiveUsers/UserMemberOf.tsx b/src/pages/ActiveUsers/UserMemberOf.tsx index 5c16b242..60428093 100644 --- a/src/pages/ActiveUsers/UserMemberOf.tsx +++ b/src/pages/ActiveUsers/UserMemberOf.tsx @@ -13,7 +13,7 @@ import MemberOfToolbar from "src/components/MemberOf/MemberOfToolbarOld"; import MemberOfTable from "src/components/MemberOf/MemberOfTable"; // Data types import { - UserGroup, + UserGroupOld, Netgroup, Roles, HBACRules, @@ -226,7 +226,7 @@ const UserMemberOf = (props: PropsToUserMemberOf) => { // Update pagination const changeMemberGroupsList = ( - value: UserGroup[] | Netgroup[] | Roles[] | HBACRules[] | SudoRules[] + value: UserGroupOld[] | Netgroup[] | Roles[] | HBACRules[] | SudoRules[] ) => { switch (activeTabKey) { case 1: diff --git a/src/services/rpc.ts b/src/services/rpc.ts index 4954d431..7d954156 100644 --- a/src/services/rpc.ts +++ b/src/services/rpc.ts @@ -22,6 +22,7 @@ import { UIDType, User, Service, + cnType, } from "../utils/datatypes/globalDataTypes"; import { apiToHost } from "../utils/hostUtils"; import { apiToUser } from "../utils/userUtils"; @@ -141,7 +142,7 @@ export interface GenericPayload { stopIdx: number; objName?: string; objAttr?: string; - entryType?: "user" | "stage" | "preserved" | "host" | "service"; + entryType?: "user" | "stage" | "preserved" | "host" | "service" | "group"; } export interface HostAddPayload { @@ -711,6 +712,8 @@ export const api = createApi({ id = idResponseData.result.result[i] as servicesType; } else if (objName === "user" || objName === "stageuser") { id = idResponseData.result.result[i] as UIDType; + } else if (objName === "group") { + id = idResponseData.result.result[i] as cnType; } else { // Unknown, should never happen return { @@ -1089,6 +1092,16 @@ export const api = createApi({ }); }, }), + getUserByUid: build.query({ + query: (uid) => { + return getCommand({ + method: "user_show", + params: [[uid], { version: API_VERSION_BACKUP }], + }); + }, + transformResponse: (response: FindRPCResponse): User => + response.result.result as unknown as User, + }), }), }); @@ -1132,6 +1145,12 @@ export const useGettingServicesQuery = (payloadData) => { payloadData["objAttr"] = "krbprincipalname"; return useGettingGenericQuery(payloadData); }; +// Groups +export const useGettingGroupsQuery = (payloadData) => { + payloadData["objName"] = "group"; + payloadData["objAttr"] = "cn"; + return useGettingGenericQuery(payloadData); +}; // Full search wrappers export const useGetUsersFullQuery = (userId: string) => { @@ -1198,4 +1217,5 @@ export const { useGetGenericListQuery, useRemoveServicesMutation, useSearchEntriesMutation, + useGetUserByUidQuery, } = api; diff --git a/src/store/Identity/userGroups-slice.ts b/src/store/Identity/userGroups-slice.ts index d51f786c..92e2bb7a 100644 --- a/src/store/Identity/userGroups-slice.ts +++ b/src/store/Identity/userGroups-slice.ts @@ -2,10 +2,10 @@ import { createSlice } from "@reduxjs/toolkit"; import type { RootState } from "../store"; import userGroupsJson from "./userGroups.json"; // Data type -import { UserGroup } from "src/utils/datatypes/globalDataTypes"; +import { UserGroupOld } from "src/utils/datatypes/globalDataTypes"; interface UserGroupState { - userGroupList: UserGroup[]; + userGroupList: UserGroupOld[]; } const initialState: UserGroupState = { diff --git a/src/utils/data/GroupRepositories.ts b/src/utils/data/GroupRepositories.ts index f9d4ed8c..b1d17792 100644 --- a/src/utils/data/GroupRepositories.ts +++ b/src/utils/data/GroupRepositories.ts @@ -8,7 +8,7 @@ */ import { - UserGroup, + UserGroupOld, Netgroup, Roles, HBACRules, @@ -18,7 +18,7 @@ import { // USERS // 'User groups' initial data -export let userGroupsInitialData: UserGroup[] = [ +export let userGroupsInitialData: UserGroupOld[] = [ { name: "Initial admins", gid: "12345678", diff --git a/src/utils/datatypes/globalDataTypes.ts b/src/utils/datatypes/globalDataTypes.ts index 136e71ba..4453b0f3 100644 --- a/src/utils/datatypes/globalDataTypes.ts +++ b/src/utils/datatypes/globalDataTypes.ts @@ -67,7 +67,11 @@ export interface User { ipanthomedirectorydrive: string; // 'Member of' data memberof_group: string[]; // multivalue - memberof_subid?: string[]; // multivalue + memberof_netgroup: string[]; // multivalue + memberof_role: string[]; // multivalue + memberof_hbacrule: string[]; // multivalue + memberof_sudorule: string[]; // multivalue + memberof_subid: string[]; // multivalue // 'Managed by' data mepmanagedentry: string[]; // other @@ -123,12 +127,19 @@ export interface KrbPolicy { usercertificatebinary: string[]; } -export interface UserGroup { +export interface UserGroupOld { name: string; gid: string; description: string; } +export interface UserGroup { + cn: string; + gidnumber: string; + description: string; + dn: string; +} + export interface Netgroup { name: string; description: string; diff --git a/src/utils/userUtils.tsx b/src/utils/userUtils.tsx index d2d9f20d..d307671e 100644 --- a/src/utils/userUtils.tsx +++ b/src/utils/userUtils.tsx @@ -161,6 +161,10 @@ export function createEmptyUser(): User { ipanthomedirectorydrive: "", // 'Member of' data memberof_group: [], + memberof_netgroup: [], + memberof_role: [], + memberof_hbacrule: [], + memberof_sudorule: [], memberof_subid: [], // 'Managed by' data mepmanagedentry: [], diff --git a/src/utils/utils.tsx b/src/utils/utils.tsx index 212a288a..872b9d58 100644 --- a/src/utils/utils.tsx +++ b/src/utils/utils.tsx @@ -295,3 +295,22 @@ export const isValidIpAddress = (ipAddress: string) => { return regexIPv4.test(ipAddress); } }; + +// Normalize string LDAP values (string[] --> string) +export const normalizeString = (entry: string[] | string) => { + let newValue = ""; + if (entry !== undefined) { + newValue = entry[0]; + } + return newValue; +}; + +// Some values in a table might not have a specific value defined +// (i.e. empty string ""). This is not allowed by the table component. +// Therefore, this function will return "-" instead of "". +export const parseEmptyString = (str: string) => { + if (str === "") { + return "-"; + } + return str; +}; From 8e354dcad975d138b23628e024dc5d5eda97dda7 Mon Sep 17 00:00:00 2001 From: Carla Martinez Date: Mon, 19 Feb 2024 20:06:26 +0100 Subject: [PATCH 6/7] Sync 'user groups' with real data The dummy data must be replaced by the data from the API for the 'User groups' only. Signed-off-by: Carla Martinez --- .../MemberOf/MemberOfAddModalUserGroups.tsx | 24 +++-- .../MemberOfDeleteModalUserGroups.tsx | 16 ++- .../MemberOf/MemberOfDeletedGroupsTable.tsx | 38 +++---- .../MemberOf/MemberOfTableUserGroups.tsx | 16 +-- .../MemberOf/MemberOfUserGroups.tsx | 100 ++++++++++++------ src/pages/ActiveUsers/UserMemberOf.tsx | 55 +++++++--- 6 files changed, 155 insertions(+), 94 deletions(-) diff --git a/src/components/MemberOf/MemberOfAddModalUserGroups.tsx b/src/components/MemberOf/MemberOfAddModalUserGroups.tsx index 6b85c9bf..22ca14e7 100644 --- a/src/components/MemberOf/MemberOfAddModalUserGroups.tsx +++ b/src/components/MemberOf/MemberOfAddModalUserGroups.tsx @@ -8,19 +8,19 @@ import { Modal, } from "@patternfly/react-core"; // Data types -import { UserGroupOld } from "src/utils/datatypes/globalDataTypes"; +import { UserGroup } from "src/utils/datatypes/globalDataTypes"; export interface PropsToAdd { showModal: boolean; onCloseModal: () => void; - availableData: UserGroupOld[]; + availableData: UserGroup[]; groupRepository: unknown[]; - updateGroupRepository: (newList: UserGroupOld[]) => void; + updateGroupRepository: (newList: UserGroup[]) => void; } const MemberOfAddModal = (props: PropsToAdd) => { // Dual list data - const data = props.availableData.map((d) => d.name); + const data = props.availableData.map((d) => d.cn); // Dual list selector const [availableOptions, setAvailableOptions] = useState(data); @@ -78,22 +78,24 @@ const MemberOfAddModal = (props: PropsToAdd) => { // Get all info from a chosen option const getInfoFromGroupData = (option: unknown) => { - return props.availableData.find((d) => option === d.name); + return props.availableData.find((d) => option === d.cn); }; // Add group option const onClickAddGroupHandler = () => { + const groupRepository = [...props.groupRepository]; chosenOptions.map((opt) => { - const optionData: UserGroupOld | undefined = getInfoFromGroupData(opt); + const optionData: UserGroup | undefined = getInfoFromGroupData(opt); if (optionData !== undefined) { - props.groupRepository.push({ - name: optionData.name !== undefined && optionData.name, + groupRepository.push({ + cn: optionData.cn !== undefined && optionData.cn, description: optionData.description !== undefined && optionData.description, - gid: optionData.gid !== undefined && optionData.gid, - } as UserGroupOld); + gidnumber: optionData.gidnumber !== undefined && optionData.gidnumber, + dn: optionData.dn !== undefined && optionData.dn, + } as UserGroup); // Send updated data to table - props.updateGroupRepository(props.groupRepository as UserGroupOld[]); + props.updateGroupRepository(groupRepository as UserGroup[]); } }); // Clean chosen options and close modal diff --git a/src/components/MemberOf/MemberOfDeleteModalUserGroups.tsx b/src/components/MemberOf/MemberOfDeleteModalUserGroups.tsx index 1eb7e95c..773b88a6 100644 --- a/src/components/MemberOf/MemberOfDeleteModalUserGroups.tsx +++ b/src/components/MemberOf/MemberOfDeleteModalUserGroups.tsx @@ -12,30 +12,28 @@ import { // Tables import MemberOfDeletedGroupsTable from "src/components/MemberOf/MemberOfDeletedGroupsTable"; // Data types -import { UserGroupOld } from "src/utils/datatypes/globalDataTypes"; +import { UserGroup } from "src/utils/datatypes/globalDataTypes"; interface PropsToDelete { showModal: boolean; onCloseModal: () => void; tabName: string; groupNamesToDelete: string[]; - groupRepository: UserGroupOld[]; - updateGroupRepository: (args: UserGroupOld[]) => void; + groupRepository: UserGroup[]; + updateGroupRepository: (args: UserGroup[]) => void; updateGroupNamesToDelete: (args: string[]) => void; } const MemberOfDeleteModal = (props: PropsToDelete) => { // Given a single group name, obtain full info to be sent and shown on the deletion table const getGroupInfoByName = (groupName: string) => { - const res = props.groupRepository.filter( - (group) => group.name === groupName - ); + const res = props.groupRepository.filter((group) => group.cn === groupName); return res[0]; }; // Obtain full info of groups to delete const getListOfGroupsToDelete = () => { - const groupsToDelete: UserGroupOld[] = []; + const groupsToDelete: UserGroup[] = []; props.groupNamesToDelete.map((groupName) => groupsToDelete.push(getGroupInfoByName(groupName)) ); @@ -43,14 +41,14 @@ const MemberOfDeleteModal = (props: PropsToDelete) => { }; // Groups to delete list - const groupsToDelete: UserGroupOld[] = getListOfGroupsToDelete(); + const groupsToDelete: UserGroup[] = getListOfGroupsToDelete(); // Delete groups const deleteGroups = () => { let generalUpdatedGroupList = props.groupRepository; props.groupNamesToDelete.map((groupName) => { const updatedGroupList = generalUpdatedGroupList.filter( - (grp) => grp.name !== groupName + (grp) => grp.cn !== groupName ); // If not empty, replace groupList by new array if (updatedGroupList) { diff --git a/src/components/MemberOf/MemberOfDeletedGroupsTable.tsx b/src/components/MemberOf/MemberOfDeletedGroupsTable.tsx index 94b0a9b5..7627ef1e 100644 --- a/src/components/MemberOf/MemberOfDeletedGroupsTable.tsx +++ b/src/components/MemberOf/MemberOfDeletedGroupsTable.tsx @@ -3,19 +3,14 @@ import React, { useState } from "react"; import { Td, Th, Tr } from "@patternfly/react-table"; // Layout import TableLayout from "src/components/layouts/TableLayout"; - -// Although tabs data types habe been already defined, it is not possible to access to all -// its variables. Just the mandatory ones ('name' and 'description') are accessible at this point. -// To display all the possible data types for all the tabs (and not only the mandatory ones) -// an extra interface 'MemberOfElement' will be defined. This will be assigned in the -// 'PropsToDeleteOnTable' interface instead of each type (UserGroup | Netgroup | Roles -// | HBACRules | SudoRules). -interface MemberOfElement { - name: string; - gid?: string; - status?: string; - description: string; -} +// Data types +import { + UserGroup, + Netgroup, + Roles, + HBACRules, + SudoRules, +} from "src/utils/datatypes/globalDataTypes"; // Interface for Column names // All variables are defined as optional as it won't need to be explicitely defined when @@ -28,7 +23,12 @@ interface ColumnNames { } interface PropsToDeleteOnTable { - groupsToDelete: MemberOfElement[]; + groupsToDelete: + | UserGroup[] + | Netgroup[] + | Roles[] + | HBACRules[] + | SudoRules[]; tabName: string; } @@ -85,10 +85,12 @@ const MemberOfDeletedGroupsTable = (props: PropsToDeleteOnTable) => { ); const body = props.groupsToDelete.map((group) => ( - - {group.name && {group.name}} - {group.gid && {group.gid}} - {group.status && {group.status}} + + {group.cn && {group.cn}} + {group.gidnumber && ( + {group.gidnumber} + )} + {group.dn && {group.dn}} {group.description && ( {group.description} )} diff --git a/src/components/MemberOf/MemberOfTableUserGroups.tsx b/src/components/MemberOf/MemberOfTableUserGroups.tsx index 636a8ee3..84f5a74e 100644 --- a/src/components/MemberOf/MemberOfTableUserGroups.tsx +++ b/src/components/MemberOf/MemberOfTableUserGroups.tsx @@ -2,13 +2,15 @@ import React from "react"; // PatternFly import { Table, Tr, Th, Td, Thead, Tbody } from "@patternfly/react-table"; // Data types -import { UserGroupOld } from "src/utils/datatypes/globalDataTypes"; +import { UserGroup } from "src/utils/datatypes/globalDataTypes"; // Components import SkeletonOnTableLayout from "../layouts/Skeleton/SkeletonOnTableLayout"; import EmptyBodyTable from "../tables/EmptyBodyTable"; +// Utils +import { parseEmptyString } from "src/utils/utils"; export interface MemberOfUserGroupsTableProps { - userGroups: UserGroupOld[]; + userGroups: UserGroup[]; checkedItems: string[]; onCheckItemsChange: (checkedItems: string[]) => void; showTableRows: boolean; @@ -16,7 +18,7 @@ export interface MemberOfUserGroupsTableProps { // Body const UserGroupsTableBody = (props: { - userGroups: UserGroupOld[]; + userGroups: UserGroup[]; checkedItems: string[]; onCheckboxChange: (checked: boolean, groupName: string) => void; }) => { @@ -29,12 +31,12 @@ const UserGroupsTableBody = (props: { select={{ rowIndex: index, onSelect: (_e, isSelected) => - props.onCheckboxChange(isSelected, userGroup.name), - isSelected: props.checkedItems.includes(userGroup.name), + props.onCheckboxChange(isSelected, userGroup.cn), + isSelected: props.checkedItems.includes(userGroup.cn), }} /> - {userGroup.name} - {userGroup.gid} + {userGroup.cn} + {parseEmptyString(userGroup.gidnumber)} {userGroup.description} ))} diff --git a/src/components/MemberOf/MemberOfUserGroups.tsx b/src/components/MemberOf/MemberOfUserGroups.tsx index 6b3b4719..c027c263 100644 --- a/src/components/MemberOf/MemberOfUserGroups.tsx +++ b/src/components/MemberOf/MemberOfUserGroups.tsx @@ -2,9 +2,7 @@ import React from "react"; // PatternFly import { Pagination, PaginationVariant } from "@patternfly/react-core"; // Data types -import { UserGroupOld } from "src/utils/datatypes/globalDataTypes"; -// Redux -import { useAppSelector } from "src/store/hooks"; +import { User, UserGroup } from "src/utils/datatypes/globalDataTypes"; // Components import MemberOfToolbarUserGroups, { MembershipDirection, @@ -12,6 +10,8 @@ import MemberOfToolbarUserGroups, { import MemberOfUserGroupsTable from "./MemberOfTableUserGroups"; import MemberOfAddModal from "./MemberOfAddModalUserGroups"; import MemberOfDeleteModal from "./MemberOfDeleteModalUserGroups"; +// Hooks +import { useUserMemberOfData } from "src/hooks/useUserMemberOfData"; function paginate(array: Type[], page: number, perPage: number): Type[] { const startIdx = (page - 1) * perPage; @@ -20,7 +20,7 @@ function paginate(array: Type[], page: number, perPage: number): Type[] { } interface TypeWithName { - name: string; + cn: string; } // Filter functions to compare the available data with the data that @@ -30,31 +30,61 @@ function filterUserGroupsData( list1: Array, list2: Array ): Type[] { - // User groups return list1.filter((item) => { return !list2.some((itm) => { - return item.name === itm.name; + return item.cn === itm.cn; }); }); } interface MemberOfUserGroupsProps { - usersGroupsFromUser: UserGroupOld[]; - updateUsersGroupsFromUser: (newList: UserGroupOld[]) => void; + user: Partial; + page: number; + setPage: (page: number) => void; + perPage: number; + setPerPage: (perPage: number) => void; } const MemberOfUserGroups = (props: MemberOfUserGroupsProps) => { - const userGroupsFullList = useAppSelector( - (state) => state.usergroups.userGroupList - ); + // 'User groups' assigned to user + const [userGroupsFromUser, setUserGroupsFromUser] = React.useState< + UserGroup[] + >([]); + + const firstUserIdx = (props.page - 1) * props.perPage; + const lastUserIdx = props.page * props.perPage; + + // API call: full list of 'User groups' available + const fullUserGroupsQuery = useUserMemberOfData({ + firstUserIdx, + lastUserIdx, + }); + + const userGroupsFullList = fullUserGroupsQuery.userGroupsFullList; + + // Get full data of the 'User groups' assigned to user + React.useEffect(() => { + if (!fullUserGroupsQuery.isFetching && userGroupsFullList) { + const userGroupsParsed: UserGroup[] = []; + props.user.memberof_group?.map((group) => { + userGroupsFullList.map((g) => { + if (g.cn === group) { + userGroupsParsed.push(g); + } + }); + }); + if ( + JSON.stringify(userGroupsFromUser) !== JSON.stringify(userGroupsParsed) + ) { + setUserGroupsFromUser(userGroupsParsed); + } + } + }, [fullUserGroupsQuery]); const [groupsNamesSelected, setGroupsNamesSelected] = React.useState< string[] >([]); - const [page, setPage] = React.useState(1); - const [perPage, setPerPage] = React.useState(10); - const [searchValue, setSearchValue] = React.useState(""); const [membershipDirection, setMembershipDirection] = @@ -65,13 +95,17 @@ const MemberOfUserGroups = (props: MemberOfUserGroupsProps) => { // Computed "states" const someItemSelected = groupsNamesSelected.length > 0; - const shownUserGroups = paginate(props.usersGroupsFromUser, page, perPage); - const showTableRows = props.usersGroupsFromUser.length > 0; + const shownUserGroups = paginate( + userGroupsFromUser, + props.page, + props.perPage + ); + const showTableRows = userGroupsFromUser.length > 0; // Available data to be added as member of - const userGroupsFilteredData: UserGroupOld[] = filterUserGroupsData( + const userGroupsFilteredData: UserGroup[] = filterUserGroupsData( userGroupsFullList, - props.usersGroupsFromUser + userGroupsFromUser ); return ( @@ -88,11 +122,11 @@ const MemberOfUserGroups = (props: MemberOfUserGroupsProps) => { membershipDirection={membershipDirection} onMembershipDirectionChange={setMembershipDirection} helpIconEnabled={true} - totalItems={props.usersGroupsFromUser.length} - perPage={perPage} - page={page} - onPerPageChange={setPerPage} - onPageChange={setPage} + totalItems={userGroupsFromUser.length} + perPage={props.perPage} + page={props.page} + onPerPageChange={props.setPerPage} + onPageChange={props.setPage} /> { /> setPage(page)} - onPerPageSelect={(_e, perPage) => setPerPage(perPage)} + onSetPage={(_e, page) => props.setPage(page)} + onPerPageSelect={(_e, perPage) => props.setPerPage(perPage)} /> {showAddModal && ( setShowAddModal(false)} availableData={userGroupsFilteredData} - groupRepository={props.usersGroupsFromUser} - updateGroupRepository={(newList: UserGroupOld[]) => - props.updateUsersGroupsFromUser(newList) - } + groupRepository={userGroupsFromUser} + updateGroupRepository={setUserGroupsFromUser} /> )} {showDeleteModal && someItemSelected && ( @@ -128,8 +160,8 @@ const MemberOfUserGroups = (props: MemberOfUserGroupsProps) => { tabName="User groups" groupNamesToDelete={groupsNamesSelected} updateGroupNamesToDelete={setGroupsNamesSelected} - groupRepository={props.usersGroupsFromUser} - updateGroupRepository={props.updateUsersGroupsFromUser} + groupRepository={userGroupsFromUser} + updateGroupRepository={setUserGroupsFromUser} /> )} diff --git a/src/pages/ActiveUsers/UserMemberOf.tsx b/src/pages/ActiveUsers/UserMemberOf.tsx index 60428093..98dae60e 100644 --- a/src/pages/ActiveUsers/UserMemberOf.tsx +++ b/src/pages/ActiveUsers/UserMemberOf.tsx @@ -13,7 +13,6 @@ import MemberOfToolbar from "src/components/MemberOf/MemberOfToolbarOld"; import MemberOfTable from "src/components/MemberOf/MemberOfTable"; // Data types import { - UserGroupOld, Netgroup, Roles, HBACRules, @@ -22,10 +21,8 @@ import { } from "src/utils/datatypes/globalDataTypes"; // Redux import { useAppSelector } from "src/store/hooks"; - // Repositories import { - userGroupsInitialData, netgroupsInitialData, rolesInitialData, hbacRulesInitialData, @@ -36,6 +33,10 @@ import MemberOfAddModal from "src/components/MemberOf/MemberOfAddModalOld"; import MemberOfDeleteModal from "src/components/MemberOf/MemberOfDeleteModalOld"; // Wrappers import MemberOfUserGroups from "src/components/MemberOf/MemberOfUserGroups"; +// RPC +import { useGetUserByUidQuery } from "src/services/rpc"; +// Utils +import { normalizeString } from "src/utils/utils"; interface PropsToUserMemberOf { user: User; @@ -43,6 +44,7 @@ interface PropsToUserMemberOf { const UserMemberOf = (props: PropsToUserMemberOf) => { // Retrieve each group list from Redux: + // TODO: Remove this when all data is taken from the C.L. let netgroupsList = useAppSelector((state) => state.netgroups.netgroupList); let rolesList = useAppSelector((state) => state.roles.roleList); let hbacRulesList = useAppSelector((state) => state.hbacrules.hbacRulesList); @@ -62,10 +64,34 @@ const UserMemberOf = (props: PropsToUserMemberOf) => { sudoRulesList = newAvOptionsList as SudoRules[]; }; + // Page indexes + const [page, setPage] = React.useState(1); + const [perPage, setPerPage] = React.useState(10); + + // User's full data + const userQuery = useGetUserByUidQuery(normalizeString(props.user.uid)); + + const userData = userQuery.data || {}; + + const [user, setUser] = React.useState>({}); + + React.useEffect(() => { + if (!userQuery.isFetching && userData) { + setUser({ ...userData }); + } + }, [userData, userQuery.isFetching]); + + // 'User groups' length to show in tab badge + const [userGroupsLength, setUserGroupLength] = React.useState(0); + + React.useEffect(() => { + if (user && user.memberof_group) { + setUserGroupLength(user.memberof_group.length); + } + }, [user]); + // List of default dummy data (for each tab option) - const [userGroupsRepository, setUserGroupsRepository] = useState( - userGroupsInitialData - ); + // TODO: Remove when all data is adapted to the C.L. const [netgroupsRepository, setNetgroupsRepository] = useState(netgroupsInitialData); const [rolesRepository, setRolesRepository] = useState(rolesInitialData); @@ -84,6 +110,7 @@ const UserMemberOf = (props: PropsToUserMemberOf) => { // Filter functions to compare the available data with the data that // the user is already member of. This is done to prevent duplicates // (e.g: adding the same element twice). + // TODO: Remove this when all tab are set into wrappers const filterNetgroupsData = () => { // Netgroups return netgroupsList.filter((item) => { @@ -205,11 +232,6 @@ const UserMemberOf = (props: PropsToUserMemberOf) => { setActiveTabKey(tabIndex as number); }; - // -- Pagination - // TODO: Remove this when all tabs are adapted to its own wrapper - const [page, setPage] = useState(1); - const [perPage, setPerPage] = useState(10); - // Member groups displayed on the first page const [shownNetgroupsList, setShownNetgroupsList] = useState( netgroupsRepository.slice(0, perPage) @@ -226,7 +248,7 @@ const UserMemberOf = (props: PropsToUserMemberOf) => { // Update pagination const changeMemberGroupsList = ( - value: UserGroupOld[] | Netgroup[] | Roles[] | HBACRules[] | SudoRules[] + value: Netgroup[] | Roles[] | HBACRules[] | SudoRules[] ) => { switch (activeTabKey) { case 1: @@ -426,14 +448,17 @@ const UserMemberOf = (props: PropsToUserMemberOf) => { User groups{" "} - {userGroupsRepository.length} + {userGroupsLength} } > Date: Mon, 19 Feb 2024 20:20:34 +0100 Subject: [PATCH 7/7] Remove unnecesary redux files The Redux functionality won't be used in this context, so it can be safely removed. Signed-off-by: Carla Martinez --- src/store/Identity/userGroups-slice.ts | 23 ----------------------- src/store/Identity/userGroups.json | 22 ---------------------- src/store/store.ts | 2 -- 3 files changed, 47 deletions(-) delete mode 100644 src/store/Identity/userGroups-slice.ts delete mode 100644 src/store/Identity/userGroups.json diff --git a/src/store/Identity/userGroups-slice.ts b/src/store/Identity/userGroups-slice.ts deleted file mode 100644 index 92e2bb7a..00000000 --- a/src/store/Identity/userGroups-slice.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { createSlice } from "@reduxjs/toolkit"; -import type { RootState } from "../store"; -import userGroupsJson from "./userGroups.json"; -// Data type -import { UserGroupOld } from "src/utils/datatypes/globalDataTypes"; - -interface UserGroupState { - userGroupList: UserGroupOld[]; -} - -const initialState: UserGroupState = { - userGroupList: userGroupsJson, -}; - -const userGroupsSlice = createSlice({ - name: "usergroups", - initialState, - reducers: {}, -}); - -export const selectUserGroups = (state: RootState) => - state.usergroups.userGroupList; -export default userGroupsSlice.reducer; diff --git a/src/store/Identity/userGroups.json b/src/store/Identity/userGroups.json deleted file mode 100644 index a88099d2..00000000 --- a/src/store/Identity/userGroups.json +++ /dev/null @@ -1,22 +0,0 @@ -[ - { - "name": "admins", - "gid": "613800000", - "description": "Account administrators group" - }, - { - "name": "editors", - "gid": "613800002", - "description": "Limited admins who can edit other users" - }, - { - "name": "employees", - "gid": "613800005", - "description": "Test Employees" - }, - { - "name": "group123", - "gid": "613800008", - "description": "The best group ever" - } -] diff --git a/src/store/store.ts b/src/store/store.ts index 2d6ae967..a6586e0d 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -3,7 +3,6 @@ import { setupListeners } from "@reduxjs/toolkit/query"; import globalReducer from "./Global/global-slice"; import activeUsersReducer from "./Identity/activeUsers-slice"; import netgroupsReducer from "./Identity/netgroups-slice"; -import userGroupsReducer from "./Identity/userGroups-slice"; import rolesReducer from "./IPA server/roles-slice"; import hbacRulesReducer from "./Policy/hbacRules-slice"; import sudoRulesReducer from "./Policy/sudoRules-slice"; @@ -20,7 +19,6 @@ const store = configureStore({ global: globalReducer, activeUsers: activeUsersReducer, netgroups: netgroupsReducer, - usergroups: userGroupsReducer, roles: rolesReducer, hbacrules: hbacRulesReducer, sudorules: sudoRulesReducer,