From 371416a35cdb4aa59866f9e49135da00d4abff7a Mon Sep 17 00:00:00 2001 From: Carla Martinez Date: Fri, 4 Aug 2023 17:01:47 +0200 Subject: [PATCH] Functionality of Selectors The Select[1] components need to be adapted to take data from the metadata. This will be applied for the fields 'Radius proxy configuration' and 'External IdP configuration'. [1]- http://v4-archive.patternfly.org/v4/components/select Signed-off-by: Carla Martinez --- src/components/Form/IpaSelect.tsx | 34 +++++ src/components/UserSettings.tsx | 11 +- .../UsersSections/UsersAccountSettings.tsx | 140 ++++++++++-------- src/hooks/useUserSettingsData.tsx | 98 +++++++++++- src/pages/ActiveUsers/ActiveUsersTabs.tsx | 2 + src/services/rpc.ts | 38 ++++- src/utils/datatypes/globalDataTypes.ts | 6 + src/utils/ipaObjectUtils.ts | 73 ++++++++- 8 files changed, 333 insertions(+), 69 deletions(-) create mode 100644 src/components/Form/IpaSelect.tsx diff --git a/src/components/Form/IpaSelect.tsx b/src/components/Form/IpaSelect.tsx new file mode 100644 index 000000000..978c12f98 --- /dev/null +++ b/src/components/Form/IpaSelect.tsx @@ -0,0 +1,34 @@ +import React from "react"; +// PatternFly +import { Select, SelectOption, SelectVariant } from "@patternfly/react-core"; +// Utils +import { + IPAParamDefinitionSelect, + getParamPropertiesSelect, +} from "src/utils/ipaObjectUtils"; + +const IpaSelect = (props: IPAParamDefinitionSelect) => { + const { required, readOnly, value } = getParamPropertiesSelect(props); + + return ( + + ); +}; + +export default IpaSelect; diff --git a/src/components/UserSettings.tsx b/src/components/UserSettings.tsx index 64bd74623..0a3070644 100644 --- a/src/components/UserSettings.tsx +++ b/src/components/UserSettings.tsx @@ -16,7 +16,12 @@ import { // Icons import OutlinedQuestionCircleIcon from "@patternfly/react-icons/dist/esm/icons/outlined-question-circle-icon"; // Data types -import { Metadata, User } from "src/utils/datatypes/globalDataTypes"; +import { + IDPServer, + Metadata, + RadiusServer, + User, +} from "src/utils/datatypes/globalDataTypes"; // Layouts import ToolbarLayout from "src/components/layouts/ToolbarLayout"; import TitleLayout from "src/components/layouts/TitleLayout"; @@ -53,6 +58,8 @@ export interface PropsToUserSettings { isDataLoading?: boolean; modifiedValues: () => Partial; onResetValues: () => void; + radiusProxyData?: RadiusServer[]; + idpData?: IDPServer[]; from: "active-users" | "stage-users" | "preserved-users"; } @@ -251,6 +258,8 @@ const UserSettings = (props: PropsToUserSettings) => { onUserChange={props.onUserChange} metadata={props.metadata} onRefresh={props.onRefresh} + radiusProxyConf={props.radiusProxyData || []} + idpConf={props.idpData || []} /> ; onUserChange: (element: Partial) => void; metadata: Metadata; onRefresh: () => void; + radiusProxyConf: RadiusServer[]; + idpConf: IDPServer[]; } // Generic data to pass to the Textbox adder @@ -204,6 +209,59 @@ const UsersAccountSettings = (props: PropsToUsersAccountSettings) => { , ]; + // Dropdown 'Radius proxy configuration' + const [isRadiusConfOpen, setIsRadiusConfOpen] = useState(false); + const [radiusConfSelected, setRadiusConfSelected] = useState( + ipaObject.ipatokenradiusconfiglink + ); + const [radiusConfOptions, setRadiusConfOptions] = useState([]); + const radiusConfOnToggle = (isOpen: boolean) => { + setIsRadiusConfOpen(isOpen); + }; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const radiusConfOnSelect = (selection: any) => { + setRadiusConfSelected(selection.target.textContent as string); + updateIpaObject( + selection.target.textContent as string, + "ipatokenradiusconfiglink" + ); + setIsRadiusConfOpen(false); + }; + + useEffect(() => { + const radiusProxyList: string[] = []; + props.radiusProxyConf.map((item) => { + const itemString = item.cn.toString(); + radiusProxyList.push(itemString); + }); + setRadiusConfOptions(radiusProxyList); + }, [props.radiusProxyConf]); + + // Dropdown 'External IdP configuration' + const [isIdpConfOpen, setIsIdpConfOpen] = useState(false); + const [idpConfSelected, setIdpConfSelected] = useState(""); + const [idpConfOptions, setIdpConfOptions] = useState([]); + + const idpConfOnToggle = (isOpen: boolean) => { + setIsIdpConfOpen(isOpen); + }; + + const idpConfOnSelect = (selection: any) => { + setIdpConfSelected(selection.target.textContent as string); + updateIpaObject(selection.target.textContent as string, "ipaidpconfiglink"); + setIsIdpConfOpen(false); + }; + + useEffect(() => { + const idpList: string[] = []; + props.idpConf.map((item) => { + const itemString = item.cn.toString(); + idpList.push(itemString); + }); + setIdpConfOptions(idpList); + }, [props.idpConf]); + // TODO: This state variables should update the user data via the IPA API (`user_mod`) const [userLogin] = useState(props.user.uid); const [password] = useState(""); @@ -560,38 +618,6 @@ const UsersAccountSettings = (props: PropsToUsersAccountSettings) => { /> ); - // Dropdown 'Radius proxy configuration' - const [isRadiusConfOpen, setIsRadiusConfOpen] = useState(false); - const [radiusConfSelected, setRadiusConfSelected] = useState(""); - const radiusConfOptions = [ - { value: "Option 1", disabled: false }, - { value: "Option 2", disabled: false }, - { value: "Option 3", disabled: false }, - ]; - const radiusConfOnToggle = (isOpen: boolean) => { - setIsRadiusConfOpen(isOpen); - }; - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const radiusConfOnSelect = (selection: any) => { - setRadiusConfSelected(selection.target.textContent); - setIsRadiusConfOpen(false); - }; - - // Dropdown 'External IdP configuration' - const [isIdpConfOpen, setIsIdpConfOpen] = useState(false); - const [idpConfSelected, setIdpConfSelected] = useState(""); - const [idpConfOptions] = useState([]); - - const idpConfOnToggle = (isOpen: boolean) => { - setIsIdpConfOpen(isOpen); - }; - - const idpConfOnSelect = (selection: any) => { - setIdpConfSelected(selection.target.textContent); - setIsIdpConfOpen(false); - }; - // Messages for the popover const certificateMappingDataMessage = () => (
@@ -854,26 +880,18 @@ const UsersAccountSettings = (props: PropsToUsersAccountSettings) => { label="Radius proxy configuration" fieldId="radius-proxy-configuration" > - + ipaObject={ipaObject} + objectName="user" + metadata={props.metadata} + value={radiusConfSelected} + /> { label="External IdP configuration" fieldId="external-idp-configuration" > - + ipaObject={ipaObject} + objectName="user" + metadata={props.metadata} + value={idpConfSelected} + /> ; refetch: () => void; modifiedValues: () => Partial; + radiusServer: RadiusServer[]; + idpServer: IDPServer[]; }; const useUserSettingsData = (userId: string): UserSettingsData => { @@ -31,28 +40,60 @@ const useUserSettingsData = (userId: string): UserSettingsData => { const metadata = metadataQuery.data || {}; const metadataLoading = metadataQuery.isLoading; + // [API call] User const userFullDataQuery = useGetUsersFullDataQuery(userId); const userFullData = userFullDataQuery.data; const isFullDataLoading = userFullDataQuery.isLoading; + // [API call] RADIUS proxy server + const radiusProxyQuery = useGetRadiusProxyQuery(); + const radiusProxyData = radiusProxyQuery.data; + const isRadiusProxyLoading = radiusProxyQuery.isLoading; + + // [API call] RADIUS proxy server + const idpQuery = useGetIdpServerQuery(); + const idpData = idpQuery.data; + const isIdpLoading = idpQuery.isLoading; + const [modified, setModified] = useState(false); // Data displayed and modified by the user const [user, setUser] = useState>({}); + const [radiusServer, setRadiusServer] = useState([]); + const [idpServer, setIdpServer] = useState([]); + useEffect(() => { if (userFullData && !userFullDataQuery.isFetching) { setUser({ ...userFullData.user }); } }, [userFullData, userFullDataQuery.isFetching]); + useEffect(() => { + if (radiusProxyData && !radiusProxyQuery.isFetching) { + setRadiusServer({ ...radiusProxyData }); + } + }, [radiusProxyData, radiusProxyQuery.isFetching]); + + useEffect(() => { + if (idpData && !idpQuery.isFetching) { + setIdpServer({ ...idpData }); + } + }, [idpData, idpQuery.isFetching]); + const settingsData = { - isLoading: metadataLoading || isFullDataLoading, + isLoading: + metadataLoading || + isFullDataLoading || + isRadiusProxyLoading || + isIdpLoading, isFetching: userFullDataQuery.isFetching, modified, setModified, metadata, user, setUser, + radiusServer, + idpServer, refetch: userFullDataQuery.refetch, } as UserSettingsData; @@ -61,8 +102,12 @@ const useUserSettingsData = (userId: string): UserSettingsData => { settingsData.pwPolicyData = userFullData.pwPolicy; settingsData.krbtPolicyData = userFullData.krbtPolicy; settingsData.certData = userFullData.cert; + settingsData.radiusServer = radiusProxyData || []; + settingsData.idpServer = idpData || []; } else { settingsData.originalUser = {}; + settingsData.radiusServer = []; + settingsData.idpServer = []; } const getModifiedValues = (): Partial => { @@ -80,6 +125,7 @@ const useUserSettingsData = (userId: string): UserSettingsData => { }; settingsData.modifiedValues = getModifiedValues; + // Detect any change in 'originalUser' and 'user' objects useEffect(() => { if (!userFullData || !userFullData.user) { return; @@ -102,6 +148,54 @@ const useUserSettingsData = (userId: string): UserSettingsData => { setModified(modified); }, [user, userFullData]); + // Detect any change for 'radiusServer' + useEffect(() => { + if (!radiusProxyData) { + return; + } + + let modified = false; + + for (const [key, value] of Object.entries(radiusProxyData)) { + if (Array.isArray(value)) { + if (JSON.stringify(radiusProxyData[key]) !== JSON.stringify(value)) { + modified = true; + break; + } + } else { + if (radiusProxyData[key] !== value) { + modified = true; + break; + } + } + } + setModified(modified); + }, [radiusServer, radiusProxyData]); + + // Detect any change for 'idpServer' + useEffect(() => { + if (!idpData) { + return; + } + + let modified = false; + + for (const [key, value] of Object.entries(idpData)) { + if (Array.isArray(value)) { + if (JSON.stringify(idpData[key]) !== JSON.stringify(value)) { + modified = true; + break; + } + } else { + if (idpData[key] !== value) { + modified = true; + break; + } + } + } + setModified(modified); + }, [idpServer, idpData]); + const onResetValues = () => { setModified(false); }; diff --git a/src/pages/ActiveUsers/ActiveUsersTabs.tsx b/src/pages/ActiveUsers/ActiveUsersTabs.tsx index 3de02e019..e950443b7 100644 --- a/src/pages/ActiveUsers/ActiveUsersTabs.tsx +++ b/src/pages/ActiveUsers/ActiveUsersTabs.tsx @@ -99,6 +99,8 @@ const ActiveUsersTabs = () => { isModified={userSettingsData.modified} onResetValues={userSettingsData.resetValues} modifiedValues={userSettingsData.modifiedValues} + radiusProxyData={userSettingsData.radiusServer} + idpData={userSettingsData.idpServer} from="active-users" /> diff --git a/src/services/rpc.ts b/src/services/rpc.ts index 1c2b9532d..93d9b7254 100644 --- a/src/services/rpc.ts +++ b/src/services/rpc.ts @@ -8,7 +8,12 @@ import { } from "@reduxjs/toolkit/query/react"; // Utils import { API_VERSION_BACKUP } from "src/utils/utils"; -import { Metadata, User } from "src/utils/datatypes/globalDataTypes"; +import { + IDPServer, + Metadata, + RadiusServer, + User, +} from "src/utils/datatypes/globalDataTypes"; import { apiToUser } from "src/utils/userUtils"; export type UserFullData = { @@ -148,7 +153,12 @@ export const getBatchCommand = (commandData: Command[], apiVersion: string) => { export const api = createApi({ reducerPath: "api", baseQuery: fetchBaseQuery({ baseUrl: "/" }), // TODO: Global settings! - tagTypes: ["ObjectMetadata", "FullUserData"], + tagTypes: [ + "ObjectMetadata", + "FullUserData", + "RadiusServerData", + "IdpServerData", + ], endpoints: (build) => ({ simpleCommand: build.query({ query: (payloadData: Command) => getCommand(payloadData), @@ -326,6 +336,28 @@ export const api = createApi({ }); }, }), + getRadiusProxy: build.query({ + query: () => { + return getCommand({ + method: "radiusproxy_find", + params: [[null], { version: API_VERSION_BACKUP }], + }); + }, + transformResponse: (response: FindRPCResponse): RadiusServer[] => + response.result.result as unknown as RadiusServer[], + providesTags: ["RadiusServerData"], + }), + getIdpServer: build.query({ + query: () => { + return getCommand({ + method: "idp_find", + params: [[null], { version: API_VERSION_BACKUP }], + }); + }, + transformResponse: (response: FindRPCResponse): IDPServer[] => + response.result.result as unknown as IDPServer[], + providesTags: ["IdpServerData"], + }), }), }); @@ -340,4 +372,6 @@ export const { useSaveUserMutation, useRemovePrincipalAliasMutation, useAddPrincipalAliasMutation, + useGetRadiusProxyQuery, + useGetIdpServerQuery, } = api; diff --git a/src/utils/datatypes/globalDataTypes.ts b/src/utils/datatypes/globalDataTypes.ts index e3a50c932..7226531ee 100644 --- a/src/utils/datatypes/globalDataTypes.ts +++ b/src/utils/datatypes/globalDataTypes.ts @@ -198,3 +198,9 @@ export interface ParamMetadata { sortorder: number; type: string; } + +export interface RadiusServer { + ipatokenradiusserver: string; + cn: string; + dn: string; +} diff --git a/src/utils/ipaObjectUtils.ts b/src/utils/ipaObjectUtils.ts index 1a12cc5e2..9451fbba6 100644 --- a/src/utils/ipaObjectUtils.ts +++ b/src/utils/ipaObjectUtils.ts @@ -1,5 +1,7 @@ import { Metadata, ParamMetadata } from "src/utils/datatypes/globalDataTypes"; import { isSimpleValue } from "./userUtils"; +// PatternFly +import { SelectOptionObject } from "@patternfly/react-core"; export type BasicType = string | number | boolean | null | undefined | []; @@ -52,6 +54,31 @@ export interface IPAParamDefinitionCheckbox { className?: string; // } +export interface IPAParamDefinitionSelect { + id?: string; + value?: string; + name: string; + ipaObject?: Record; + objectName: string; + metadata: Metadata; + propertyName?: string; + alwaysWritable?: boolean; + readOnly?: boolean; + required?: boolean; + //--- + variant?: "single" | "checkbox" | "typeahead" | "typeaheadmulti"; + elementsOptions: string[]; + onToggle: (isOpen: boolean) => void; + onSelect?: ( + event: React.MouseEvent | React.ChangeEvent, + value: string | SelectOptionObject, + isPlaceholder?: boolean + ) => void; + selections?: string | SelectOptionObject | (string | SelectOptionObject)[]; + isOpen: boolean; + ariaLabelledBy?: string; +} + export interface ParamProperties { writable: boolean; required: boolean; @@ -82,6 +109,14 @@ export interface ParamPropertiesCheckbox { className: string; // } +export interface ParamPropertiesSelect { + writable: boolean; + required: boolean; + readOnly: boolean; + value: BasicType; + paramMetadata: ParamMetadata; +} + function getParamMetadata( metadata: Metadata, objectName: string, @@ -226,7 +261,6 @@ export function getParamProperties( }; } -// TEST export function getParamPropertiesWithIndex( parDef: IPAParamDefinitionWithIndex ): ParamPropertiesWithIndex { @@ -361,6 +395,43 @@ export function getParamPropertiesCheckBox( }; } +export function getParamPropertiesSelect( + parDef: IPAParamDefinitionSelect +): ParamPropertiesSelect { + const propName = parDef.propertyName || parDef.name; + const paramMetadata = getParamMetadata( + parDef.metadata, + parDef.objectName, + propName + ); + if (!paramMetadata) { + return { + writable: false, + required: false, + readOnly: true, + value: "", + paramMetadata: {} as ParamMetadata, + }; + } + const writable = isWritable( + paramMetadata, + parDef.ipaObject, + parDef.alwaysWritable + ); + const required = isRequired(parDef, paramMetadata, writable); + const readOnly = parDef.readOnly === undefined ? !writable : parDef.readOnly; + const value = getValue(parDef.ipaObject, propName)?.toString(); + + return { + writable, + required, + readOnly, + value, + // onChange, + paramMetadata, + }; +} + export function convertToString(value: BasicType): string { if (value === null || value === undefined) { return "";