diff --git a/src/components/MemberOf/MemberOfAddModalNew.tsx b/src/components/MemberOf/MemberOfAddModalNew.tsx new file mode 100644 index 000000000..322fa5dfd --- /dev/null +++ b/src/components/MemberOf/MemberOfAddModalNew.tsx @@ -0,0 +1,277 @@ +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 { + Netgroup, + Roles, + HBACRules, + SudoRules, + HostGroup, + UserGroupNew, +} from "src/utils/datatypes/globalDataTypes"; +// Hooks +import { AlertObject } from "src/hooks/useAlerts"; +// Utils +import { getNameValue } from "./MemberOfTableNew"; +import { ErrorResult, useGroupAddMemberMutation } from "src/services/rpc"; + +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: + | UserGroupNew[] + | Netgroup[] + | Roles[] + | HBACRules[] + | SudoRules[] + | HostGroup[]; + groupRepository: unknown[]; + updateGroupRepository: ( + args: + | UserGroupNew[] + | Netgroup[] + | Roles[] + | HBACRules[] + | SudoRules[] + | HostGroup[] + ) => void; + updateAvOptionsList: (args: unknown[]) => void; + tabData: TabData; + alerts: AlertObject; +} + +const MemberOfAddModal = (props: PropsToAdd) => { + // Dual list data + const data = props.availableData.map((d) => getNameValue(d)); + + // API call + const [addMemberToUserGroup] = useGroupAddMemberMutation(); + + // 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", + 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) => { + const availableDataCopy = [...props.availableData]; + return availableDataCopy.find((d) => { + const name = getNameValue(d); + return option === name; + }); + }; + + // Add new member to 'User group' + const onAddToUserGroup = (toUid: string, newData: string[]) => { + addMemberToUserGroup([toUid, newData]).then((response) => { + if ("data" in response) { + if (response.data.result) { + // Set alert: success + props.alerts.addAlert( + "add-member-success", + "Added new members to user group '" + toUid + "'", + "success" + ); + // Close modal + props.modalData.handleModalToggle(); + } else if (response.data.error) { + // Set alert: error + const errorMessage = response.data.error as unknown as ErrorResult; + props.alerts.addAlert( + "add-member-error", + errorMessage.message, + "danger" + ); + } + } + }); + }; + + // Add group option + const onClickAddGroupHandler = () => { + // List of items to be added + const newGroupRepoItems: unknown[] = [...props.groupRepository]; + const optionsToAdd: string[] = []; + + chosenOptions.map((opt) => { + let optionData = getInfoFromGroupData(opt); + if (optionData !== undefined) { + // User groups + if (props.tabData.tabName === "User groups") { + optionData = optionData as UserGroupNew; + newGroupRepoItems.push({ + cn: optionData.cn !== undefined && optionData.cn, + description: + optionData.description !== undefined && optionData.description, + gidnumber: + optionData.gidnumber !== undefined && optionData.gidnumber, + }); + optionsToAdd.push(optionData.cn); + } + // Netgroups + if (props.tabData.tabName === "Netgroups") { + optionData = optionData as Netgroup; + props.groupRepository.push({ + name: optionData.name !== undefined && optionData.name, + description: + optionData.description !== undefined && optionData.description, + } as Netgroup); + props.updateGroupRepository(props.groupRepository as Netgroup[]); + } + // Roles + if (props.tabData.tabName === "Roles") { + optionData = optionData as Roles; + props.groupRepository.push({ + name: optionData.name !== undefined && optionData.name, + description: + optionData.description !== undefined && optionData.description, + } as Roles); + props.updateGroupRepository(props.groupRepository as Roles[]); + } + // HBAC rules + if (props.tabData.tabName === "HBAC rules") { + optionData = optionData as HBACRules; + props.groupRepository.push({ + name: optionData.name !== undefined && optionData.name, + description: + optionData.description !== undefined && optionData.description, + } as HBACRules); + props.updateGroupRepository(props.groupRepository as HBACRules[]); + } + // Sudo rules + if (props.tabData.tabName === "Sudo rules") { + optionData = optionData as SudoRules; + props.groupRepository.push({ + name: optionData.name !== undefined && optionData.name, + description: + optionData.description !== undefined && optionData.description, + } as SudoRules); + props.updateGroupRepository(props.groupRepository as SudoRules[]); + } + // Host groups + if (props.tabData.tabName === "Host groups") { + optionData = optionData as HostGroup; + props.groupRepository.push({ + name: optionData.name !== undefined && optionData.name, + description: + optionData.description !== undefined && optionData.description, + } as HostGroup); + props.updateGroupRepository(props.groupRepository as HostGroup[]); + } + } + }); + + // API call + onAddToUserGroup(props.tabData.userName, optionsToAdd); + + // Send updated data to table based on data type (New approach) + switch (props.tabData.tabName) { + case "User groups": + props.updateGroupRepository(newGroupRepoItems as UserGroupNew[]); + break; + // TODO: Add other cases here as they're adapted to the C.L. + } + + // 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/MemberOfTableNew.tsx b/src/components/MemberOf/MemberOfTableNew.tsx index bbcfbb6e3..b0e5a1f4b 100644 --- a/src/components/MemberOf/MemberOfTableNew.tsx +++ b/src/components/MemberOf/MemberOfTableNew.tsx @@ -28,7 +28,7 @@ interface ColumnNames { // 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 | HostGroup). -interface MemberOfElement { +export interface MemberOfElement { cn?: string; name?: string; gid?: string; diff --git a/src/hooks/useAlerts.tsx b/src/hooks/useAlerts.tsx index 7fcbca202..ba902428a 100644 --- a/src/hooks/useAlerts.tsx +++ b/src/hooks/useAlerts.tsx @@ -15,6 +15,15 @@ export interface AlertInfo { variant: AlertVariant; } +// In some cases, it is needed to propagate a specific alert to child components. +// The following object is defined to be refered everytime an Alert is propagated via props. +export interface AlertObject { + addAlert: (name: string, title: string, variant: AlertVariant) => void; + removeAlert: (name: string) => void; + removeAllAlerts: () => void; + ManagedAlerts: () => JSX.Element; +} + export function useAlerts() { const [alerts, setAlerts] = React.useState([]); diff --git a/src/pages/ActiveUsers/UserMemberOf.tsx b/src/pages/ActiveUsers/UserMemberOf.tsx index b9efadac9..c219abd30 100644 --- a/src/pages/ActiveUsers/UserMemberOf.tsx +++ b/src/pages/ActiveUsers/UserMemberOf.tsx @@ -36,14 +36,21 @@ import { // Modals import MemberOfAddModal from "src/components/MemberOf/MemberOfAddModal"; import MemberOfDeleteModal from "src/components/MemberOf/MemberOfDeleteModal"; +import MemberOfAddModalNew from "src/components/MemberOf/MemberOfAddModalNew"; +// RPC import { useGetUserByUidQuery } from "src/services/rpc"; +// Hooks import { useUserMemberOfData } from "src/hooks/useUserMemberOfData"; +import useAlerts from "src/hooks/useAlerts"; interface PropsToUserMemberOf { user: User; } const UserMemberOf = (props: PropsToUserMemberOf) => { + // Alerts to show in the UI + const alerts = useAlerts(); + // Retrieve each group list from Redux: let netgroupsList = useAppSelector((state) => state.netgroups.netgroupList); let rolesList = useAppSelector((state) => state.roles.roleList); @@ -51,6 +58,9 @@ const UserMemberOf = (props: PropsToUserMemberOf) => { 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[]) => { + setUserGroupsFromUser(newAvOptionsList as UserGroupNew[]); + }; const updateNetgroupsList = (newAvOptionsList: unknown[]) => { netgroupsList = newAvOptionsList as Netgroup[]; }; @@ -71,7 +81,7 @@ const UserMemberOf = (props: PropsToUserMemberOf) => { const [user, setUser] = React.useState>({}); - // Member groups associated to user (string[] | UserGroupNew[]) + // Member groups associated to user const [userGroupsFromUser, setUserGroupsFromUser] = React.useState< UserGroupNew[] >([]); @@ -118,6 +128,14 @@ 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 userGroupsFullList.filter((item) => { + return !userGroupsFromUser.some((itm) => { + return item.cn === itm.cn; + }); + }); + }; const filterNetgroupsData = () => { // Netgroups return netgroupsList.filter((item) => { @@ -152,6 +170,7 @@ const UserMemberOf = (props: PropsToUserMemberOf) => { }; // Available data to be added as member of + const userGroupsFilteredData: UserGroupNew[] = filterUserGroupsData(); const netgroupsFilteredData: Netgroup[] = filterNetgroupsData(); const rolesFilteredData: Roles[] = filterRolesData(); const hbacRulesFilteredData: HBACRules[] = filterHbacRulesData(); @@ -557,6 +576,7 @@ const UserMemberOf = (props: PropsToUserMemberOf) => { // Render 'ActiveUsersIsMemberOf' return ( <> + { )} )} */} + {showAddModal && ( + + )} {tabName === "Netgroups" && ( <> {showAddModal && ( diff --git a/src/services/rpc.ts b/src/services/rpc.ts index 69875f610..39efd6e4d 100644 --- a/src/services/rpc.ts +++ b/src/services/rpc.ts @@ -999,6 +999,21 @@ export const api = createApi({ }; }, }), + groupAddMember: build.mutation({ + query: (payload) => { + const toUid = payload[0]; + const listOfMembers = payload[1]; + const membersToAdd: Command[] = []; + listOfMembers.map((member) => { + const payloadItem = { + method: "group_add_member", + params: [[member], {user: toUid}], + } as Command; + membersToAdd.push(payloadItem); + }); + return getBatchCommand(membersToAdd, API_VERSION_BACKUP); + }, + }), }), }); @@ -1080,4 +1095,5 @@ export const { useGetHostsFullDataQuery, useGetUserByUidQuery, useGetUserGroupsQuery, + useGroupAddMemberMutation, } = api;