diff --git a/src/components/MemberOf/MemberOfAddModal.tsx b/src/components/MemberOf/MemberOfAddModalOld.tsx similarity index 97% rename from src/components/MemberOf/MemberOfAddModal.tsx rename to src/components/MemberOf/MemberOfAddModalOld.tsx index 8a878ed8..05c88e57 100644 --- a/src/components/MemberOf/MemberOfAddModal.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 new file mode 100644 index 00000000..22ca14e7 --- /dev/null +++ b/src/components/MemberOf/MemberOfAddModalUserGroups.tsx @@ -0,0 +1,143 @@ +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.cn); + + // 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.cn); + }; + + // Add group option + const onClickAddGroupHandler = () => { + const groupRepository = [...props.groupRepository]; + chosenOptions.map((opt) => { + const optionData: UserGroup | undefined = getInfoFromGroupData(opt); + if (optionData !== undefined) { + groupRepository.push({ + cn: optionData.cn !== undefined && optionData.cn, + description: + optionData.description !== undefined && optionData.description, + gidnumber: optionData.gidnumber !== undefined && optionData.gidnumber, + dn: optionData.dn !== undefined && optionData.dn, + } as UserGroup); + // Send updated data to table + props.updateGroupRepository(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/MemberOfDeleteModal.tsx b/src/components/MemberOf/MemberOfDeleteModalOld.tsx similarity index 97% rename from src/components/MemberOf/MemberOfDeleteModal.tsx rename to src/components/MemberOf/MemberOfDeleteModalOld.tsx index 2f8c9e3c..ea47fc77 100644 --- a/src/components/MemberOf/MemberOfDeleteModal.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 new file mode 100644 index 00000000..773b88a6 --- /dev/null +++ b/src/components/MemberOf/MemberOfDeleteModalUserGroups.tsx @@ -0,0 +1,113 @@ +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.cn === 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.cn !== 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/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 f084cc3a..84f5a74e 100644 --- a/src/components/MemberOf/MemberOfTableUserGroups.tsx +++ b/src/components/MemberOf/MemberOfTableUserGroups.tsx @@ -6,6 +6,8 @@ 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: UserGroup[]; @@ -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/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 9b5ea582..c027c263 100644 --- a/src/components/MemberOf/MemberOfUserGroups.tsx +++ b/src/components/MemberOf/MemberOfUserGroups.tsx @@ -1,19 +1,17 @@ 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"; +import { User, UserGroup } from "src/utils/datatypes/globalDataTypes"; // 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"; +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; @@ -21,27 +19,94 @@ function paginate(array: Type[], page: number, perPage: number): Type[] { return array.slice(startIdx, endIdx); } +interface TypeWithName { + cn: 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[] { + return list1.filter((item) => { + return !list2.some((itm) => { + return item.cn === itm.cn; + }); + }); +} + +interface MemberOfUserGroupsProps { + user: Partial; + page: number; + setPage: (page: number) => void; + perPage: number; + setPerPage: (perPage: number) => void; +} + const MemberOfUserGroups = (props: MemberOfUserGroupsProps) => { + // '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 [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( + userGroupsFromUser, + props.page, + props.perPage + ); + const showTableRows = userGroupsFromUser.length > 0; + + // Available data to be added as member of + const userGroupsFilteredData: UserGroup[] = filterUserGroupsData( + userGroupsFullList, + userGroupsFromUser + ); return ( <> @@ -50,18 +115,18 @@ 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} - 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={userGroupsFromUser} + updateGroupRepository={setUserGroupsFromUser} + /> + )} + {showDeleteModal && someItemSelected && ( + setShowDeleteModal(false)} + tabName="User groups" + groupNamesToDelete={groupsNamesSelected} + updateGroupNamesToDelete={setGroupsNamesSelected} + groupRepository={userGroupsFromUser} + updateGroupRepository={setUserGroupsFromUser} + /> + )} ); }; 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 ba5562e8..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 { - UserGroup, Netgroup, Roles, HBACRules, @@ -22,20 +21,22 @@ import { } from "src/utils/datatypes/globalDataTypes"; // Redux import { useAppSelector } from "src/store/hooks"; - // Repositories import { - userGroupsInitialData, netgroupsInitialData, rolesInitialData, hbacRulesInitialData, sudoRulesInitialData, } from "src/utils/data/GroupRepositories"; // Modals -import MemberOfAddModal from "src/components/MemberOf/MemberOfAddModal"; -import MemberOfDeleteModal from "src/components/MemberOf/MemberOfDeleteModal"; +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,18 +44,13 @@ interface PropsToUserMemberOf { const UserMemberOf = (props: PropsToUserMemberOf) => { // Retrieve each group list from Redux: - let userGroupsList = useAppSelector( - (state) => state.usergroups.userGroupList - ); + // 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); 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[]; }; @@ -68,8 +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] = useState(userGroupsInitialData); + // TODO: Remove when all data is adapted to the C.L. const [netgroupsRepository, setNetgroupsRepository] = useState(netgroupsInitialData); const [rolesRepository, setRolesRepository] = useState(rolesInitialData); @@ -88,14 +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). - const filterUserGroupsData = () => { - // User groups - return userGroupsList.filter((item) => { - return !userGroupsRepository.some((itm) => { - return item.name === itm.name; - }); - }); - }; + // TODO: Remove this when all tab are set into wrappers const filterNetgroupsData = () => { // Netgroups return netgroupsList.filter((item) => { @@ -130,7 +145,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 +169,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": @@ -223,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) @@ -244,7 +248,7 @@ const UserMemberOf = (props: PropsToUserMemberOf) => { // Update pagination const changeMemberGroupsList = ( - value: UserGroup[] | Netgroup[] | Roles[] | HBACRules[] | SudoRules[] + value: Netgroup[] | Roles[] | HBACRules[] | SudoRules[] ) => { switch (activeTabKey) { case 1: @@ -316,7 +320,7 @@ const UserMemberOf = (props: PropsToUserMemberOf) => { const onClickAddHandler = () => { setShowAddModal(true); }; - const onModalToggle = () => { + const onAddModalToggle = () => { setShowAddModal(!showAddModal); }; @@ -397,7 +401,7 @@ const UserMemberOf = (props: PropsToUserMemberOf) => { // - MemberOfAddModal const addModalData = { showModal: showAddModal, - handleModalToggle: onModalToggle, + handleModalToggle: onAddModalToggle, }; const tabData = { @@ -444,14 +448,17 @@ const UserMemberOf = (props: PropsToUserMemberOf) => { User groups{" "} - {userGroupsRepository.length} + {userGroupsLength} } > { - {tabName === "User groups" && ( - <> - {showAddModal && ( - - )} - {showDeleteModal && groupsNamesSelected.length !== 0 && ( - - )} - - )} {tabName === "Netgroups" && ( <> {showAddModal && ( diff --git a/src/pages/Hosts/HostsMemberOf.tsx b/src/pages/Hosts/HostsMemberOf.tsx index d2138548..9a94442a 100644 --- a/src/pages/Hosts/HostsMemberOf.tsx +++ b/src/pages/Hosts/HostsMemberOf.tsx @@ -35,8 +35,8 @@ import { hostsSudoRulesInitialData, } from "src/utils/data/GroupRepositories"; // Modals -import MemberOfAddModal from "src/components/MemberOf/MemberOfAddModal"; -import MemberOfDeleteModal from "src/components/MemberOf/MemberOfDeleteModal"; +import MemberOfAddModal from "src/components/MemberOf/MemberOfAddModalOld"; +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 53d328a7..78e878c8 100644 --- a/src/pages/Services/ServicesMemberOf.tsx +++ b/src/pages/Services/ServicesMemberOf.tsx @@ -21,8 +21,8 @@ import { useAppSelector } from "src/store/hooks"; // Repositories import { servicesRolesInitialData } from "src/utils/data/GroupRepositories"; // Modals -import MemberOfAddModal from "src/components/MemberOf/MemberOfAddModal"; -import MemberOfDeleteModal from "src/components/MemberOf/MemberOfDeleteModal"; +import MemberOfAddModal from "src/components/MemberOf/MemberOfAddModalOld"; +import MemberOfDeleteModal from "src/components/MemberOf/MemberOfDeleteModalOld"; interface PropsToServicesMemberOf { service: Service; 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 deleted file mode 100644 index d51f786c..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 { UserGroup } from "src/utils/datatypes/globalDataTypes"; - -interface UserGroupState { - userGroupList: UserGroup[]; -} - -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, 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; +};