diff --git a/src/components/MemberOf/MemberOfTableUserGroups.tsx b/src/components/MemberOf/MemberOfTableUserGroups.tsx index 8dd18a20..22c20fe6 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[]; showCheckboxColumn: boolean; checkedItems: string[]; onCheckboxChange: (checked: boolean, groupName: string) => void; @@ -31,13 +33,13 @@ 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 7bb25cb8..75a50193 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, { AvailableItems } from "./MemberOfAddModal"; import MemberOfDeleteModal from "./MemberOfDeleteModal"; +// Hooks +import { useUserMemberOfData } from "src/hooks/useUserMemberOfData"; function paginate(array: Type[], page: number, perPage: number): Type[] { const startIdx = (page - 1) * perPage; @@ -19,43 +19,76 @@ function paginate(array: Type[], page: number, perPage: number): Type[] { return array.slice(startIdx, endIdx); } -interface TypeWithName { - name: string; +interface TypeWithCN { + 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( +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 { - uid: string; - usersGroupsFromUser: UserGroupOld[]; - updateUsersGroupsFromUser: (newList: UserGroupOld[]) => void; + user: Partial; } const MemberOfUserGroups = (props: MemberOfUserGroupsProps) => { - const userGroupsFullList = useAppSelector( - (state) => state.usergroups.userGroupList - ); - - const [groupsNamesSelected, setGroupsNamesSelected] = React.useState< - string[] + // 'User groups' assigned to user + const [userGroupsFromUser, setUserGroupsFromUser] = React.useState< + UserGroup[] >([]); + // Page indexes const [page, setPage] = React.useState(1); const [perPage, setPerPage] = React.useState(10); + const firstUserIdx = (page - 1) * perPage; + const lastUserIdx = page * perPage; + + const uid = props.user.uid; + + // API call: full list of 'User groups' available + const fullUserGroupsQuery = useUserMemberOfData({ + uid, + 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 [searchValue, setSearchValue] = React.useState(""); const [membershipDirection, setMembershipDirection] = @@ -66,13 +99,13 @@ 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, page, 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 ); // Parse availableItems to AvailableItems type @@ -80,8 +113,8 @@ const MemberOfUserGroups = (props: MemberOfUserGroupsProps) => { const avItems: AvailableItems[] = []; userGroupsFilteredData.map((item) => { avItems.push({ - key: item.name, - title: item.name, + key: item.cn, + title: item.cn, }); }); return avItems; @@ -93,18 +126,18 @@ const MemberOfUserGroups = (props: MemberOfUserGroupsProps) => { const onAddUserGroup = (items: AvailableItems[]) => { const newItems = items.map((item) => item.key); const newGroups = userGroupsFullList.filter((group) => - newItems.includes(group.name) + newItems.includes(group.cn) ); - const updatedGroups = props.usersGroupsFromUser.concat(newGroups); - props.updateUsersGroupsFromUser(updatedGroups); + const updatedGroups = userGroupsFromUser.concat(newGroups); + setUserGroupsFromUser(updatedGroups); }; // 'Delete' function const onDeleteUserGroup = () => { - const updatedGroups = props.usersGroupsFromUser.filter( - (group) => !groupsNamesSelected.includes(group.name) + const updatedGroups = userGroupsFromUser.filter( + (group) => !groupsNamesSelected.includes(group.cn) ); - props.updateUsersGroupsFromUser(updatedGroups); + setUserGroupsFromUser(updatedGroups); }; return ( @@ -121,7 +154,7 @@ const MemberOfUserGroups = (props: MemberOfUserGroupsProps) => { membershipDirection={membershipDirection} onMembershipDirectionChange={setMembershipDirection} helpIconEnabled={true} - totalItems={props.usersGroupsFromUser.length} + totalItems={userGroupsFromUser.length} perPage={perPage} page={page} onPerPageChange={setPerPage} @@ -135,7 +168,7 @@ const MemberOfUserGroups = (props: MemberOfUserGroupsProps) => { /> { availableItems={availableUserGroupsItems} onAdd={onAddUserGroup} onSearchTextChange={setSearchValue} - title={"Add '" + props.uid + "' into User groups"} + title={"Add '" + props.user.uid + "' into User groups"} ariaLabel="Add user of user group modal" /> )} @@ -163,9 +196,9 @@ const MemberOfUserGroups = (props: MemberOfUserGroupsProps) => { > - groupsNamesSelected.includes(group.name) - ) as UserGroupOld[] + userGroupsFromUser.filter((group) => + groupsNamesSelected.includes(group.cn) + ) as UserGroup[] } showTableRows /> diff --git a/src/pages/ActiveUsers/UserMemberOf.tsx b/src/pages/ActiveUsers/UserMemberOf.tsx index a9f3c3af..2a570057 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, @@ -25,7 +24,6 @@ import { useAppSelector } from "src/store/hooks"; // Repositories import { - userGroupsInitialData, netgroupsInitialData, rolesInitialData, hbacRulesInitialData, @@ -36,6 +34,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 { convertToString } from "src/utils/ipaObjectUtils"; interface PropsToUserMemberOf { user: User; @@ -43,6 +45,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 +65,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(convertToString(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 +111,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 +233,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 +249,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,16 +449,12 @@ const UserMemberOf = (props: PropsToUserMemberOf) => { User groups{" "} - {userGroupsRepository.length} + {userGroupsLength} } > - + { return regexIPv4.test(ipAddress); } }; + +/** + * 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; +};