From 856bece0e94106f74f9b7f92f9587bbb34412a41 Mon Sep 17 00:00:00 2001 From: Carla Martinez Date: Mon, 26 Feb 2024 16:26:16 +0100 Subject: [PATCH 1/4] Replace old 'UserGroup' by a new one The deprecated `UserGroup` data type should be renamed and the components that consumes it adapted to the new name. At the same time, a new `UserGroup` type should take the new values and be ready to be used. ```ts export interface UserGroup { cn: string; gidnumber: string; description: string; dn: string; } ``` Signed-off-by: Carla Martinez --- src/components/MemberOf/MemberOfAddModalOld.tsx | 12 ++++++------ src/components/MemberOf/MemberOfDeleteModalOld.tsx | 2 +- .../MemberOf/MemberOfDeletedGroupsTable.tsx | 2 +- src/components/MemberOf/MemberOfTable.tsx | 2 +- src/components/MemberOf/MemberOfTableUserGroups.tsx | 6 +++--- src/components/MemberOf/MemberOfToolbarOld.tsx | 8 ++++---- src/components/MemberOf/MemberOfUserGroups.tsx | 10 +++++----- src/pages/ActiveUsers/UserMemberOf.tsx | 4 ++-- src/store/Identity/userGroups-slice.ts | 4 ++-- src/utils/data/GroupRepositories.ts | 4 ++-- src/utils/datatypes/globalDataTypes.ts | 9 ++++++++- 11 files changed, 35 insertions(+), 28 deletions(-) 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/MemberOfDeleteModalOld.tsx b/src/components/MemberOf/MemberOfDeleteModalOld.tsx index 2272fbd7..5b41b4eb 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/MemberOfDeletedGroupsTable.tsx b/src/components/MemberOf/MemberOfDeletedGroupsTable.tsx index fb4cb72c..9c3a92c5 100644 --- a/src/components/MemberOf/MemberOfDeletedGroupsTable.tsx +++ b/src/components/MemberOf/MemberOfDeletedGroupsTable.tsx @@ -8,7 +8,7 @@ import TableLayout from "src/components/layouts/TableLayout"; // 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 +// 'PropsToDeleteOnTable' interface instead of each type (UserGroupOld | Netgroup | Roles // | HBACRules | SudoRules). interface MemberOfElement { name: string; diff --git a/src/components/MemberOf/MemberOfTable.tsx b/src/components/MemberOf/MemberOfTable.tsx index 6689978d..02b26346 100644 --- a/src/components/MemberOf/MemberOfTable.tsx +++ b/src/components/MemberOf/MemberOfTable.tsx @@ -27,7 +27,7 @@ interface ColumnNames { // 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 | HostGroup). +// interface instead of each type (UserGroupOld | Netgroup | Roles | HBACRules | SudoRules | HostGroup). interface MemberOfElement { name: string; gid?: string; diff --git a/src/components/MemberOf/MemberOfTableUserGroups.tsx b/src/components/MemberOf/MemberOfTableUserGroups.tsx index 8154585e..8dd18a20 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[]; showCheckboxColumn: boolean; 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 646a29ed..7bb25cb8 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 @@ -40,8 +40,8 @@ function filterUserGroupsData( interface MemberOfUserGroupsProps { uid: string; - usersGroupsFromUser: UserGroup[]; - updateUsersGroupsFromUser: (newList: UserGroup[]) => void; + usersGroupsFromUser: UserGroupOld[]; + updateUsersGroupsFromUser: (newList: UserGroupOld[]) => void; } const MemberOfUserGroups = (props: MemberOfUserGroupsProps) => { @@ -70,7 +70,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 ); @@ -165,7 +165,7 @@ const MemberOfUserGroups = (props: MemberOfUserGroupsProps) => { userGroups={ props.usersGroupsFromUser.filter((group) => groupsNamesSelected.includes(group.name) - ) as UserGroup[] + ) as UserGroupOld[] } showTableRows /> diff --git a/src/pages/ActiveUsers/UserMemberOf.tsx b/src/pages/ActiveUsers/UserMemberOf.tsx index 4f4468f6..a9f3c3af 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/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..ddd3cb1a 100644 --- a/src/utils/datatypes/globalDataTypes.ts +++ b/src/utils/datatypes/globalDataTypes.ts @@ -123,12 +123,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; From c1ded2a5283e61c90c85eaddfd0cd6520ce8f41c Mon Sep 17 00:00:00 2001 From: Carla Martinez Date: Mon, 26 Feb 2024 16:39:07 +0100 Subject: [PATCH 2/4] Add new User variables The `User` data type needs the following parameters to manage the 'Is a 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 ``` Signed-off-by: Carla Martinez --- src/utils/datatypes/globalDataTypes.ts | 6 +++++- src/utils/userUtils.tsx | 4 ++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/utils/datatypes/globalDataTypes.ts b/src/utils/datatypes/globalDataTypes.ts index ddd3cb1a..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 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: [], From f6f368b052e13aa4a4a3b5b2ce8cfeada580fde3 Mon Sep 17 00:00:00 2001 From: Carla Martinez Date: Tue, 27 Feb 2024 10:35:59 +0100 Subject: [PATCH 3/4] Create user groups endpoint and custom hook New endpoint to retrieve the list of uids and user groups should be created. The latter will be used in a custom hook that would retrieve all needed info related to the 'Is a member of' section. Signed-off-by: Carla Martinez TEMP: usergroups endpoint and c.hook Signed-off-by: Carla Martinez --- src/hooks/useUserMemberOfData.tsx | 66 +++++++++++++++++++++++++++++++ src/services/rpc.ts | 34 +++++++++++++++- src/utils/groupUtils.tsx | 37 +++++++++++++++++ 3 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 src/hooks/useUserMemberOfData.tsx create mode 100644 src/utils/groupUtils.tsx diff --git a/src/hooks/useUserMemberOfData.tsx b/src/hooks/useUserMemberOfData.tsx new file mode 100644 index 00000000..d877d1c9 --- /dev/null +++ b/src/hooks/useUserMemberOfData.tsx @@ -0,0 +1,66 @@ +// RPC +import React from "react"; +import { BatchRPCResponse, useGettingGroupsQuery } from "src/services/rpc"; +// Data types +import { UserGroup } from "src/utils/datatypes/globalDataTypes"; +import { apiToGroup } from "src/utils/groupUtils"; +// Utils +import { API_VERSION_BACKUP } from "src/utils/utils"; + +type MemberOfData = { + isLoading: boolean; + isFetching: boolean; + refetch: () => void; + userGroupsFullList: UserGroup[]; +}; + +const useUserMemberOfData = ({ + uid, + firstUserIdx, + lastUserIdx, +}): MemberOfData => { + // [API call] User groups + // TODO: Normalize data to prevent array of arrays + const userGroupsQuery = useGettingGroupsQuery({ + user: uid, + 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(apiToGroup(results[i].result)); + } + 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/services/rpc.ts b/src/services/rpc.ts index 4954d431..bf6cd777 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"; @@ -137,11 +138,13 @@ export interface GenericPayload { searchValue: string; sizeLimit: number; apiVersion: string; + user?: string; + no_user?: string; startIdx: number; stopIdx: number; objName?: string; objAttr?: string; - entryType?: "user" | "stage" | "preserved" | "host" | "service"; + entryType?: "user" | "stage" | "preserved" | "host" | "service" | "group"; } export interface HostAddPayload { @@ -649,6 +652,8 @@ export const api = createApi({ searchValue, sizeLimit, apiVersion, + user, + no_user, startIdx, stopIdx, objAttr, @@ -682,6 +687,14 @@ export const api = createApi({ version: apiVersion, }; + if (objName === "group") { + if (user !== undefined) { + params["user"] = user; + } else if (no_user !== undefined) { + params["no_user"] = no_user; + } + } + if (objName === "preserved") { params["preserved"] = true; objName = "user"; @@ -711,6 +724,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 +1104,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 => + apiToUser(response.result.result), + }), }), }); @@ -1132,6 +1157,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 +1229,5 @@ export const { useGetGenericListQuery, useRemoveServicesMutation, useSearchEntriesMutation, + useGetUserByUidQuery, } = api; diff --git a/src/utils/groupUtils.tsx b/src/utils/groupUtils.tsx new file mode 100644 index 00000000..e0814c47 --- /dev/null +++ b/src/utils/groupUtils.tsx @@ -0,0 +1,37 @@ +// Data types +import { UserGroup } from "src/utils/datatypes/globalDataTypes"; +// Utils +import { convertApiObj } from "./ipaObjectUtils"; + +const simpleValues = new Set(["cn", "gidnumber", "description", "dn"]); +const dateValues = new Set([]); + +export function apiToGroup(apiRecord: Record): UserGroup { + const converted = convertApiObj( + apiRecord, + simpleValues, + dateValues + ) as Partial; + return partialGroupToGroup(converted) as UserGroup; +} + +export function partialGroupToGroup( + partialGroup: Partial +): UserGroup { + return { + ...createEmptyGroup(), + ...partialGroup, + }; +} + +// Get empty User object initialized with default values +export function createEmptyGroup(): UserGroup { + const group: UserGroup = { + cn: "", + gidnumber: "", + description: "", + dn: "", + }; + + return group; +} From 1f763ad2675098b603e7ff339dabe36e3b6b454c Mon Sep 17 00:00:00 2001 From: Carla Martinez Date: Tue, 27 Feb 2024 13:00:17 +0100 Subject: [PATCH 4/4] Replace dummy data by data from the API The dummy data from the table must be replaced by the data from the API server. Signed-off-by: Carla Martinez --- .../MemberOf/MemberOfTableUserGroups.tsx | 16 +-- .../MemberOf/MemberOfUserGroups.tsx | 101 ++++++++++++------ src/pages/ActiveUsers/UserMemberOf.tsx | 53 ++++++--- src/utils/utils.tsx | 13 +++ 4 files changed, 125 insertions(+), 58 deletions(-) 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; +};