Repository: freeipa/freeipa-webui
Pull request #684: IdP references > 'Settings' page and kebab
Author: login: carma12
Labels: name: ack
description: Pull Request approved, it can be merged
Created: 2025-03-26T09:31:48Z | Merged: 2025-03-31T14:01:39Z

The 'Settings' page should display and manage the available information of a given Identity Provider.
In order to track the changes on the `Secret` field, a new Ipa component `IpaPasswordInput` has been created.

Also, the kebab option (located next to the 'Save' button) should contain the 'Reset password' option to reset the password of the current IdP entry.

## Summary by Sourcery

Add a 'Settings' page for Identity Providers to manage their information, including a new password input component and a reset password option. Enhance IdP-related endpoints with new queries and mutations for better data management.

New Features:
- Introduce a new 'Settings' page for managing Identity Provider (IdP) information, including a new component 'IpaPasswordInput' for handling password inputs.
- Add a 'Reset password' option in the kebab menu next to the 'Save' button for resetting the password of the current IdP entry.

Enhancements:
- Enhance the IdP-related endpoints by adding new queries and mutations such as 'useIdpShowQuery' and 'useIdpModMutation' for showing and modifying IdP data.


Diff:
diff --git a/src/components/Form/IpaPasswordInput.tsx b/src/components/Form/IpaPasswordInput.tsx
new file mode 100644
index 000000000..f19a6c296
--- /dev/null
+++ b/src/components/Form/IpaPasswordInput.tsx
@@ -0,0 +1,51 @@
+import React from "react";
+// PatternFly
+import { TextInput } from "@patternfly/react-core";
+// Utils
+import {
+  IPAParamDefinition,
+  getParamProperties,
+  convertToString,
+} from "src/utils/ipaObjectUtils";
+import { HIDDEN_PASSWORD } from "src/utils/utils";
+
+const IpaPasswordInput = (props: IPAParamDefinition) => {
+  const { required, readOnly, value, onChange } = getParamProperties(props);
+
+  const [textInputValue, setTextInputValue] = React.useState<string>(
+    convertToString(value)
+  );
+
+  // Parse the value to be shown in the input field
+  // - Some passwords are encoded, so those must be shown as hidden
+  // - Undefined passwords must be shown as empty
+  React.useEffect(() => {
+    console.log("value", value);
+    console.log("typeof value", typeof value);
+    if (typeof value === "object") {
+      setTextInputValue(HIDDEN_PASSWORD);
+    } else if (typeof value === undefined) {
+      setTextInputValue("");
+    } else {
+      setTextInputValue(convertToString(value));
+    }
+  }, [value]);
+
+  return (
+    <TextInput
+      type="password"
+      id={props.name}
+      name={props.name}
+      value={textInputValue}
+      onChange={(_event, value) => {
+        setTextInputValue(value);
+        onChange(value);
+      }}
+      aria-label={props.ariaLabel !== undefined ? props.ariaLabel : props.name}
+      isRequired={required}
+      readOnlyVariant={readOnly ? "plain" : undefined}
+    />
+  );
+};
+
+export default IpaPasswordInput;
diff --git a/src/components/ResetIdpPassword.tsx b/src/components/ResetIdpPassword.tsx
new file mode 100644
index 000000000..b4b68b303
--- /dev/null
+++ b/src/components/ResetIdpPassword.tsx
@@ -0,0 +1,209 @@
+import React from "react";
+// PatternFly
+import {
+  Button,
+  HelperText,
+  HelperTextItem,
+  ValidatedOptions,
+} from "@patternfly/react-core";
+// Data types
+import {
+  IDPServer,
+  PasswordValidationType,
+} from "src/utils/datatypes/globalDataTypes";
+// Modals
+import ModalWithFormLayout from "src/components/layouts/ModalWithFormLayout";
+// Components
+import PasswordInput from "src/components/layouts/PasswordInput";
+// RPC
+import { IdpModPayload, useIdpModMutation } from "src/services/rpcIdp";
+// Hooks
+import useAlerts from "src/hooks/useAlerts";
+
+interface PropsToResetIdpPassword {
+  idpId: string;
+  isOpen: boolean;
+  onClose: () => void;
+  onIdpRefChange: (idpRef: Partial<IDPServer>) => void;
+  onRefresh: () => void;
+}
+
+const ResetIdpPassword = (props: PropsToResetIdpPassword) => {
+  // Alerts to show in the UI
+  const alerts = useAlerts();
+
+  // RPC
+  const [resetPassword] = useIdpModMutation();
+
+  // States
+  const [newPassword, setNewPassword] = React.useState("");
+  const [verifyPassword, setVerifyPassword] = React.useState("");
+  const [passwordHidden, setPasswordHidden] = React.useState(true);
+  const [verifyPasswordHidden, setVerifyPasswordHidden] = React.useState(true);
+
+  // Verify password
+  const [passwordValidationResult, setPasswordValidationResult] =
+    React.useState<PasswordValidationType>({
+      isError: false,
+      message: "",
+      pfError: ValidatedOptions.default,
+    });
+
+  // Reset the verify password field
+  const resetVerifyPassword = () => {
+    setPasswordValidationResult({
+      isError: false,
+      message: "",
+      pfError: ValidatedOptions.default,
+    });
+  };
+
+  // Checks that the passwords are the same
+  const validatePasswords = () => {
+    if (newPassword !== verifyPassword) {
+      setPasswordValidationResult({
+        isError: true,
+        message: "Passwords must match",
+        pfError: ValidatedOptions.error,
+      });
+      return true; // is error
+    }
+    resetVerifyPassword();
+    return false;
+  };
+
+  // Fields
+  const fields = [
+    {
+      id: "reset-password-new-password",
+      name: "New Password",
+      pfComponent: (
+        <PasswordInput
+          id="modal-form-reset-password-new-password"
+          name="password"
+          value={newPassword}
+          aria-label="new password text input"
+          onFocus={resetVerifyPassword}
+          onChange={setNewPassword}
+          onRevealHandler={setPasswordHidden}
+          passwordHidden={passwordHidden}
+        />
+      ),
+    },
+    {
+      id: "reset-password-verify-password",
+      name: "Verify password",
+      pfComponent: (
+        <>
+          <PasswordInput
+            id="modal-form-reset-password-verify-password"
+            name="password2"
+            value={verifyPassword}
+            aria-label="verify password text input"
+            onFocus={resetVerifyPassword}
+            onChange={setVerifyPassword}
+            onRevealHandler={setVerifyPasswordHidden}
+            passwordHidden={verifyPasswordHidden}
+            validated={passwordValidationResult.pfError}
+          />
+          <HelperText>
+            <HelperTextItem variant="error">
+              {passwordValidationResult.message}
+            </HelperTextItem>
+          </HelperText>
+        </>
+      ),
+    },
+  ];
+
+  // Verify the passwords are the same when we update a password value
+  React.useEffect(() => {
+    validatePasswords();
+  }, [newPassword, verifyPassword]);
+
+  // Reset fields and close modal
+  const resetFieldsAndCloseModal = () => {
+    // Reset fields
+    setNewPassword("");
+    setVerifyPassword("");
+    setPasswordHidden(true);
+    setVerifyPasswordHidden(true);
+    // Close modal
+    props.onClose();
+  };
+
+  // API call to reset password
+  const onResetPassword = () => {
+    const payload: IdpModPayload = {
+      idpId: props.idpId,
+      ipaidpclientsecret: newPassword,
+    };
+
+    resetPassword(payload).then((response) => {
+      if ("data" in response) {
+        const data = response.data;
+        if (data.error) {
+          alerts.addAlert("error", (data.error as Error).message, "danger");
+        }
+        if (data.result) {
+          props.onIdpRefChange(data.result.result);
+          alerts.addAlert(
+            "success",
+            "Identity Provider password successfully updated",
+            "success"
+          );
+          // Refresh the data
+          props.onRefresh();
+          // Reset values and close
+          resetFieldsAndCloseModal();
+        }
+      }
+    });
+  };
+
+  const actions = [
+    <Button
+      key={"reset-password"}
+      variant="primary"
+      onClick={onResetPassword}
+      isDisabled={
+        passwordValidationResult.isError ||
+        newPassword === "" ||
+        verifyPassword === ""
+      }
+    >
+      Reset password
+    </Button>,
+    <Button
+      key={"cancel-reset-password"}
+      variant="link"
+      onClick={resetFieldsAndCloseModal}
+    >
+      Cancel
+    </Button>,
+  ];
+
+  // Return component
+  return (
+    <>
+      <alerts.ManagedAlerts />
+      <ModalWithFormLayout
+        variantType="small"
+        modalPosition="top"
+        title="Reset password"
+        description={
+          "Reset the password for the selected Identity Provider '" +
+          props.idpId +
+          "'"
+        }
+        formId="reset-password-form"
+        fields={fields}
+        show={props.isOpen}
+        onClose={resetFieldsAndCloseModal}
+        actions={actions}
+      />
+    </>
+  );
+};
+
+export default ResetIdpPassword;
diff --git a/src/components/modals/IdpReferences/AddModal.tsx b/src/components/modals/IdpReferences/AddModal.tsx
index a6f8b101c..7a558dabb 100644
--- a/src/components/modals/IdpReferences/AddModal.tsx
+++ b/src/components/modals/IdpReferences/AddModal.tsx
@@ -127,8 +127,6 @@ const AddModal = (props: PropsToAddModal) => {
     setJwksUri("");
     setScope("");
     setExtIdpUidAttr("");
-    // Selector
-    setSelectedProvider("Keycloak or Red Hat SSO");
   };
 
   // on change radio functions
@@ -154,6 +152,11 @@ const AddModal = (props: PropsToAddModal) => {
       ipaidpclientid: clientId,
     };
 
+    // Check if secret is defined
+    if (secret !== "" && verifySecret !== "" && secret === verifySecret) {
+      payload.ipaidpclientsecret = secret;
+    }
+
     // Define payload based on the radio buttons and selected provider
     if (isPreDefinedChecked) {
       if (selectedProvider === "Keycloak or Red Hat SSO") {
@@ -186,7 +189,6 @@ const AddModal = (props: PropsToAddModal) => {
       }
     } else if (isCustomChecked) {
       const customData: CustomIdpAddPayload = {
-        // cn: idpRefName,
         ipaidpclientid: clientId,
         ipaidpauthendpoint: authUri,
         ipaidpdevauthendpoint: devAuthUri,
@@ -624,7 +626,11 @@ const AddModal = (props: PropsToAddModal) => {
     <Button
       key="add-new"
       variant="secondary"
-      isDisabled={isAddButtonSpinning || areMandatoryFieldsEmpty}
+      isDisabled={
+        isAddButtonSpinning ||
+        areMandatoryFieldsEmpty ||
+        secret !== verifySecret
+      }
       form="add-modal-form"
       onClick={() => {
         onAdd(false);
@@ -635,7 +641,11 @@ const AddModal = (props: PropsToAddModal) => {
     <Button
       key="add-new-again"
       variant="secondary"
-      isDisabled={isAddAnotherButtonSpinning || areMandatoryFieldsEmpty}
+      isDisabled={
+        isAddAnotherButtonSpinning ||
+        areMandatoryFieldsEmpty ||
+        secret !== verifySecret
+      }
       form="add-again-modal-form"
       onClick={() => {
         onAdd(true);
diff --git a/src/hooks/useIdpRefSettingsData.tsx b/src/hooks/useIdpRefSettingsData.tsx
new file mode 100644
index 000000000..a7122c4f0
--- /dev/null
+++ b/src/hooks/useIdpRefSettingsData.tsx
@@ -0,0 +1,111 @@
+import React from "react";
+// RPC
+import { useGetObjectMetadataQuery } from "src/services/rpc";
+import { useIdpShowQuery } from "src/services/rpcIdp";
+// Data types
+import { IDPServer, Metadata } from "src/utils/datatypes/globalDataTypes";
+
+type IdpRefSettingsData = {
+  isLoading: boolean;
+  isFetching: boolean;
+  modified: boolean;
+  setModified: (value: boolean) => void;
+  resetValues: () => void;
+  metadata: Metadata;
+  originalIdpRef: Partial<IDPServer>;
+  idpRef: Partial<IDPServer>;
+  setIdpRef: (fqdn: Partial<IDPServer>) => void;
+  refetch: () => void;
+  modifiedValues: () => Partial<IDPServer>;
+};
+
+const useIdpRefSettingsData = (idpRefId: string): IdpRefSettingsData => {
+  // [API call] Metadata
+  const metadataQuery = useGetObjectMetadataQuery();
+  const metadata = metadataQuery.data || {};
+  const metadataLoading = metadataQuery.isLoading;
+
+  // [API call] IdP Reference
+  const idpRefDetails = useIdpShowQuery(idpRefId);
+  const idpRefData = idpRefDetails.data;
+  const isIdpRefDataLoading = idpRefDetails.isLoading;
+
+  const [modified, setModified] = React.useState(false);
+
+  // Data displayed and modified by the user
+  const [idpRef, setIdpRef] = React.useState<Partial<IDPServer>>({});
+
+  React.useEffect(() => {
+    if (idpRefData && !idpRefDetails.isFetching) {
+      setIdpRef({ ...idpRefData });
+    }
+  }, [idpRefData, idpRefDetails.isFetching]);
+
+  const settings: IdpRefSettingsData = {
+    isLoading: metadataLoading || isIdpRefDataLoading,
+    isFetching: idpRefDetails.isFetching,
+    modified,
+    setModified,
+    metadata,
+    // eslint-disable-next-line @typescript-eslint/no-empty-function
+    resetValues: () => {},
+    originalIdpRef: idpRef,
+    setIdpRef,
+    refetch: idpRefDetails.refetch,
+    idpRef,
+    modifiedValues: () => idpRef,
+  };
+
+  const getModifiedValues = (): Partial<IDPServer> => {
+    if (!idpRefData) {
+      return {};
+    }
+
+    const modifiedValues = {};
+    for (const [key, value] of Object.entries(idpRef)) {
+      if (Array.isArray(value)) {
+        // Using 'JSON.stringify' when comparing arrays (to prevent data type false positives)
+        if (JSON.stringify(idpRef[key]) !== JSON.stringify(value)) {
+          modifiedValues[key] = value;
+        }
+      } else if (idpRefData[key] !== value) {
+        modifiedValues[key] = value;
+      }
+    }
+    return modifiedValues;
+  };
+  settings.modifiedValues = getModifiedValues;
+
+  // Detect any change in 'originalIdpRef' and 'idpRef' objects
+  React.useEffect(() => {
+    if (!idpRefData) {
+      return;
+    }
+    let modified = false;
+    for (const [key, value] of Object.entries(idpRef)) {
+      if (Array.isArray(value)) {
+        // Using 'JSON.stringify' when comparing arrays (to prevent data type false positives)
+        if (JSON.stringify(idpRefData[key]) !== JSON.stringify(value)) {
+          modified = true;
+          break;
+        }
+      } else {
+        if (idpRefData[key] !== value) {
+          modified = true;
+          break;
+        }
+      }
+    }
+    setModified(modified);
+  }, [idpRef, idpRefData]);
+
+  // Reset values
+  const onResetValues = () => {
+    setModified(false);
+  };
+  settings.resetValues = onResetValues;
+
+  return settings;
+};
+
+export { useIdpRefSettingsData };
diff --git a/src/navigation/AppRoutes.tsx b/src/navigation/AppRoutes.tsx
index c65850299..b78951c7c 100644
--- a/src/navigation/AppRoutes.tsx
+++ b/src/navigation/AppRoutes.tsx
@@ -56,6 +56,7 @@ import SubIdsStatistics from "src/pages/SubordinateIDs/SubIdsStatistics";
 import SubIdsTabs from "src/pages/SubordinateIDs/SubIdsTabs";
 import PasswordPoliciesTabs from "src/pages/PasswordPolicies/PasswordPoliciesTabs";
 import IdpReferences from "src/pages/IdPReferences/IdpReferences";
+import IdpReferencesTabs from "src/pages/IdPReferences/IdpReferencesTabs";
 
 // Renders routes (React)
 export const AppRoutes = ({ isInitialDataLoaded }): React.ReactElement => {
@@ -431,6 +432,12 @@ export const AppRoutes = ({ isInitialDataLoaded }): React.ReactElement => {
               </Route>
               <Route path="identity-provider-references">
                 <Route path="" element={<IdpReferences />} />
+                <Route path=":cn">
+                  <Route
+                    path=""
+                    element={<IdpReferencesTabs section="settings" />}
+                  />
+                </Route>
               </Route>
               <Route path="configuration" element={<Configuration />} />
               {/* Redirect to Active users page if user is logged in and navigates to the root page */}
diff --git a/src/pages/IdPReferences/IdpReferences.tsx b/src/pages/IdPReferences/IdpReferences.tsx
index afcabe6f5..95473db75 100644
--- a/src/pages/IdPReferences/IdpReferences.tsx
+++ b/src/pages/IdPReferences/IdpReferences.tsx
@@ -456,7 +456,7 @@ const IdpReferences = () => {
                   hasCheckboxes={true}
                   pathname="identity-provider-references"
                   showTableRows={showTableRows}
-                  showLink={false}
+                  showLink={true}
                   elementsData={{
                     isElementSelectable: isIdpServerSelectable,
                     selectedElements,
diff --git a/src/pages/IdPReferences/IdpReferencesSettings.tsx b/src/pages/IdPReferences/IdpReferencesSettings.tsx
new file mode 100644
index 000000000..4e7ac3845
--- /dev/null
+++ b/src/pages/IdPReferences/IdpReferencesSettings.tsx
@@ -0,0 +1,403 @@
+import React from "react";
+// PatternFly
+import {
+  DropdownItem,
+  Flex,
+  FlexItem,
+  Form,
+  FormGroup,
+  JumpLinks,
+  JumpLinksItem,
+  Sidebar,
+  SidebarContent,
+  SidebarPanel,
+} from "@patternfly/react-core";
+// Data types
+import { IDPServer, Metadata } from "src/utils/datatypes/globalDataTypes";
+// Hooks
+import useUpdateRoute from "src/hooks/useUpdateRoute";
+import useAlerts from "src/hooks/useAlerts";
+// Utils
+import { asRecord } from "src/utils/subIdUtils";
+// Icons
+import { OutlinedQuestionCircleIcon } from "@patternfly/react-icons";
+// RPC
+import { IdpModPayload, useIdpModMutation } from "src/services/rpcIdp";
+// Components
+import IpaTextInput from "src/components/Form/IpaTextInput/IpaTextInput";
+import TabLayout from "src/components/layouts/TabLayout";
+import SecondaryButton from "src/components/layouts/SecondaryButton";
+import HelpTextWithIconLayout from "src/components/layouts/HelpTextWithIconLayout";
+import IpaTextContent from "src/components/Form/IpaTextContent/IpaTextContent";
+import TitleLayout from "src/components/layouts/TitleLayout";
+import IpaPasswordInput from "src/components/Form/IpaPasswordInput";
+import KebabLayout from "src/components/layouts/KebabLayout";
+import ResetIdpPassword from "src/components/ResetIdpPassword";
+
+interface PropsToIdpRefSettings {
+  idpRef: Partial<IDPServer>;
+  originalIdpRef: Partial<IDPServer>;
+  metadata: Metadata;
+  onIdpRefChange: (idpRef: Partial<IDPServer>) => void;
+  onRefresh: () => void;
+  isModified: boolean;
+  isDataLoading?: boolean;
+  modifiedValues: () => Partial<IDPServer>;
+  onResetValues: () => void;
+  pathname: string;
+}
+
+const IdpRefSettings = (props: PropsToIdpRefSettings) => {
+  // Alerts to show in the UI
+  const alerts = useAlerts();
+
+  // Update current route data to Redux and highlight the current page in the Nav bar
+  useUpdateRoute({ pathname: props.pathname });
+
+  // States
+  const [isDataLoading, setIsDataLoading] = React.useState(false);
+
+  // Get 'ipaObject' and 'recordOnChange' to use in 'IpaTextInput'
+  const { ipaObject, recordOnChange } = asRecord(
+    props.idpRef,
+    props.onIdpRefChange
+  );
+
+  // API calls
+  const [saveIdpRef] = useIdpModMutation();
+
+  // 'Revert' handler method
+  const onRevert = () => {
+    props.onIdpRefChange(props.originalIdpRef);
+    props.onRefresh();
+    alerts.addAlert(
+      "revert-success",
+      "Identity Provider data reverted",
+      "success"
+    );
+  };
+
+  // Helper method to build the payload based on values
+  const buildPayload = (
+    modifiedValues: Partial<IDPServer>,
+    keyArray: string[]
+  ): IdpModPayload => {
+    const payload: IdpModPayload = {
+      idpId: props.idpRef.cn?.toString() as string,
+    };
+
+    keyArray.forEach((key) => {
+      // Modified values are either the value (in string) or an array ([]) when set to empty string
+      if (modifiedValues[key] !== undefined) {
+        if (modifiedValues[key] === "") {
+          payload[key] = [];
+        } else {
+          payload[key] = modifiedValues[key].toString();
+        }
+      }
+    });
+    return payload;
+  };
+
+  // on Save handler method
+  const onSave = () => {
+    setIsDataLoading(true);
+    const modifiedValues = props.modifiedValues();
+
+    // Generate payload
+    const payload = buildPayload(modifiedValues, [
+      "ipaidpclientid",
+      "ipaidpclientsecret",
+      "ipaidpscope",
+      "ipaidpsub",
+      "ipaidpauthendpoint",
+      "ipaidpdevauthendpoint",
+      "ipaidptokenendpoint",
+      "ipaidpuserinfoendpoint",
+      "ipaidpkeysendpoint",
+      "ipaidpissuerurl",
+    ]);
+
+    saveIdpRef(payload).then((response) => {
+      if ("data" in response) {
+        const data = response.data;
+        if (data.error) {
+          alerts.addAlert("error", (data.error as Error).message, "danger");
+        }
+        if (data.result) {
+          props.onIdpRefChange(data.result.result);
+          alerts.addAlert(
+            "success",
+            "Identity Provider '" + props.idpRef.cn + "' updated",
+            "success"
+          );
+          // Reset values. Disable 'revert' and 'save' buttons
+          props.onResetValues();
+        }
+      }
+      setIsDataLoading(false);
+    });
+  };
+
+  // 'Reset password' option
+  const [isResetPasswordModalOpen, setIsResetPasswordModalOpen] =
+    React.useState(false);
+
+  // Kebab
+  const [isKebabOpen, setIsKebabOpen] = React.useState(false);
+
+  const kebabItems = [
+    <DropdownItem
+      key="reset password"
+      onClick={() => setIsResetPasswordModalOpen(true)}
+    >
+      Reset password
+    </DropdownItem>,
+  ];
+
+  // Toolbar
+  const toolbarFields = [
+    {
+      key: 0,
+      element: (
+        <SecondaryButton onClickHandler={props.onRefresh}>
+          Refresh
+        </SecondaryButton>
+      ),
+    },
+    {
+      key: 1,
+      element: (
+        <SecondaryButton
+          isDisabled={!props.isModified || isDataLoading}
+          onClickHandler={onRevert}
+        >
+          Revert
+        </SecondaryButton>
+      ),
+    },
+    {
+      key: 2,
+      element: (
+        <SecondaryButton
+          isDisabled={!props.isModified || isDataLoading}
+          onClickHandler={onSave}
+        >
+          Save
+        </SecondaryButton>
+      ),
+    },
+    {
+      key: 3,
+      element: (
+        <KebabLayout
+          direction={"up"}
+          onDropdownSelect={() => setIsKebabOpen(!isKebabOpen)}
+          onKebabToggle={() => setIsKebabOpen(!isKebabOpen)}
+          idKebab="toggle-action-buttons"
+          isKebabOpen={isKebabOpen}
+          dropdownItems={kebabItems}
+        />
+      ),
+    },
+  ];
+
+  // Render component
+  return (
+    <>
+      <TabLayout id="settings-page" toolbarItems={toolbarFields}>
+        <alerts.ManagedAlerts />
+        <Sidebar isPanelRight>
+          <SidebarPanel variant="sticky">
+            <HelpTextWithIconLayout
+              textContent="Help"
+              icon={
+                <OutlinedQuestionCircleIcon className="pf-v5-u-primary-color-100 pf-v5-u-mr-sm" />
+              }
+            />
+            <JumpLinks
+              isVertical
+              label="Jump to section"
+              scrollableSelector="#idp-reference-page"
+              offset={220} // for masthead
+              expandable={{ default: "expandable", md: "nonExpandable" }}
+              className="pf-v5-u-mt-md"
+            >
+              <JumpLinksItem key={0} href="#oauth-client-settings">
+                OAuth 2.0 client details
+              </JumpLinksItem>
+              <JumpLinksItem key={1} href="#idp-details">
+                Identity provider details
+              </JumpLinksItem>
+            </JumpLinks>
+          </SidebarPanel>
+          <SidebarContent className="pf-v5-u-mr-xl">
+            <TitleLayout
+              key={0}
+              id="oauth-client-settings"
+              text="OAuth 2.0 client details"
+              headingLevel="h2"
+              className="pf-v5-u-mt-lg pf-v5-u-mb-md"
+            />
+            <Flex direction={{ default: "column", lg: "row" }}>
+              <FlexItem flex={{ default: "flex_1" }}>
+                <Form className="pf-v5-u-mb-lg">
+                  <FormGroup
+                    label="Identity Provider reference name"
+                    fieldId="cn"
+                  >
+                    <IpaTextContent
+                      name={"cn"}
+                      ariaLabel={"Identity Provider reference name"}
+                      ipaObject={ipaObject}
+                      objectName="idp"
+                      metadata={props.metadata}
+                    />
+                  </FormGroup>
+                  <FormGroup
+                    label="Client identifier"
+                    fieldId="ipaidpclientid"
+                    isRequired
+                  >
+                    <IpaTextInput
+                      name={"ipaidpclientid"}
+                      ariaLabel={"Client identifier"}
+                      ipaObject={ipaObject}
+                      onChange={recordOnChange}
+                      objectName="idp"
+                      metadata={props.metadata}
+                    />
+                  </FormGroup>
+                  <FormGroup label="Secret" fieldId="ipaidpclientsecret">
+                    <IpaPasswordInput
+                      name={"ipaidpclientsecret"}
+                      ariaLabel={"Secret"}
+                      ipaObject={ipaObject}
+                      onChange={recordOnChange}
+                      objectName="idp"
+                      metadata={props.metadata}
+                    />
+                  </FormGroup>
+                </Form>
+              </FlexItem>
+            </Flex>
+            <TitleLayout
+              key={1}
+              headingLevel="h2"
+              id="idp-details"
+              text="Identity provider details"
+              className="pf-v5-u-mt-lg pf-v5-u-mb-md"
+            />
+            <Flex direction={{ default: "column", lg: "row" }}>
+              <FlexItem flex={{ default: "flex_1" }}>
+                <Form className="pf-v5-u-mb-lg">
+                  <FormGroup label="Scope" fieldId="ipaidpscope">
+                    <IpaTextInput
+                      name={"ipaidpscope"}
+                      ariaLabel={"Scope"}
+                      ipaObject={ipaObject}
+                      onChange={recordOnChange}
+                      objectName="idp"
+                      metadata={props.metadata}
+                    />
+                  </FormGroup>
+                  <FormGroup
+                    label="External IdP user identifier attribute"
+                    fieldId="ipaidpsub"
+                  >
+                    <IpaTextInput
+                      name={"ipaidpsub"}
+                      ariaLabel={"External IdP user identifier attribute"}
+                      ipaObject={ipaObject}
+                      onChange={recordOnChange}
+                      objectName="idp"
+                      metadata={props.metadata}
+                    />
+                  </FormGroup>
+                  <FormGroup
+                    label="Authorization URI"
+                    fieldId="ipaidpauthendpoint"
+                  >
+                    <IpaTextInput
+                      name={"ipaidpauthendpoint"}
+                      ariaLabel={"Authorization URI"}
+                      ipaObject={ipaObject}
+                      onChange={recordOnChange}
+                      objectName="idp"
+                      metadata={props.metadata}
+                    />
+                  </FormGroup>
+                  <FormGroup
+                    label="Device authorization URI"
+                    fieldId="ipaidpdevauthendpoint"
+                  >
+                    <IpaTextInput
+                      name={"ipaidpdevauthendpoint"}
+                      ariaLabel={"Device authorization URI"}
+                      ipaObject={ipaObject}
+                      onChange={recordOnChange}
+                      objectName="idp"
+                      metadata={props.metadata}
+                    />
+                  </FormGroup>
+                  <FormGroup label="Token URI" fieldId="ipaidptokenendpoint">
+                    <IpaTextInput
+                      name={"ipaidptokenendpoint"}
+                      ariaLabel={"Token URI"}
+                      ipaObject={ipaObject}
+                      onChange={recordOnChange}
+                      objectName="idp"
+                      metadata={props.metadata}
+                    />
+                  </FormGroup>
+                  <FormGroup
+                    label="User info URI"
+                    fieldId="ipaidpuserinfoendpoint"
+                  >
+                    <IpaTextInput
+                      name={"ipaidpuserinfoendpoint"}
+                      ariaLabel={"User info URI"}
+                      ipaObject={ipaObject}
+                      onChange={recordOnChange}
+                      objectName="idp"
+                      metadata={props.metadata}
+                    />
+                  </FormGroup>
+                  <FormGroup label="JWKS URI" fieldId="ipaidpkeysendpoint">
+                    <IpaTextInput
+                      name={"ipaidpkeysendpoint"}
+                      ariaLabel={"JWKS URI"}
+                      ipaObject={ipaObject}
+                      onChange={recordOnChange}
+                      objectName="idp"
+                      metadata={props.metadata}
+                    />
+                  </FormGroup>
+                  <FormGroup label="OIDC URL" fieldId="ipaidpissuerurl">
+                    <IpaTextInput
+                      name={"ipaidpissuerurl"}
+                      ariaLabel={"OIDC URL"}
+                      ipaObject={ipaObject}
+                      onChange={recordOnChange}
+                      objectName="idp"
+                      metadata={props.metadata}
+                    />
+                  </FormGroup>
+                </Form>
+              </FlexItem>
+            </Flex>
+          </SidebarContent>
+        </Sidebar>
+        <ResetIdpPassword
+          idpId={props.idpRef.cn as string}
+          isOpen={isResetPasswordModalOpen}
+          onClose={() => setIsResetPasswordModalOpen(false)}
+          onIdpRefChange={props.onIdpRefChange}
+          onRefresh={props.onRefresh}
+        />
+      </TabLayout>
+    </>
+  );
+};
+
+export default IdpRefSettings;
diff --git a/src/pages/IdPReferences/IdpReferencesTabs.tsx b/src/pages/IdPReferences/IdpReferencesTabs.tsx
new file mode 100644
index 000000000..b310a4aa2
--- /dev/null
+++ b/src/pages/IdPReferences/IdpReferencesTabs.tsx
@@ -0,0 +1,140 @@
+import React from "react";
+// PatternFly
+import {
+  PageSection,
+  PageSectionVariants,
+  Tabs,
+  Tab,
+  TabTitleText,
+} from "@patternfly/react-core";
+// React Router DOM
+import { useNavigate, useParams } from "react-router-dom";
+// Navigation
+import { URL_PREFIX } from "src/navigation/NavRoutes";
+import { NotFound } from "src/components/errors/PageErrors";
+// Hooks
+import { useIdpRefSettingsData } from "src/hooks/useIdpRefSettingsData";
+// Components
+import TitleLayout from "src/components/layouts/TitleLayout";
+import DataSpinner from "src/components/layouts/DataSpinner";
+import BreadCrumb, {
+  BreadCrumbItem,
+} from "src/components/layouts/BreadCrumb/BreadCrumb";
+import IdpRefSettings from "./IdpReferencesSettings";
+
+// eslint-disable-next-line react/prop-types
+const IdpReferencesTabs = ({ section }) => {
+  const { cn } = useParams();
+  const navigate = useNavigate();
+  const pathname = "identity-provider-references";
+
+  const [breadcrumbItems, setBreadcrumbItems] = React.useState<
+    BreadCrumbItem[]
+  >([]);
+
+  // States - Identifier of the entity (IdP -> cn)
+  const [id, setId] = React.useState("");
+
+  // Data loaded from the API
+  const idpRefSettingsData = useIdpRefSettingsData(cn as string);
+
+  // Tab
+  const [activeTabKey, setActiveTabKey] = React.useState(section);
+
+  const handleTabClick = (
+    _event: React.MouseEvent<HTMLElement, MouseEvent>,
+    tabIndex: number | string
+  ) => {
+    setActiveTabKey(tabIndex as string);
+
+    if (tabIndex === "settings") {
+      navigate("/" + pathname + "/" + id);
+    }
+  };
+
+  React.useEffect(() => {
+    if (!cn) {
+      // Redirect to the main page
+      navigate(URL_PREFIX + "/" + pathname);
+    } else {
+      setId(cn);
+      // Update breadcrumb route
+      const currentPath: BreadCrumbItem[] = [
+        {
+          name: "Identity provider references",
+          url: URL_PREFIX + "/" + pathname,
+        },
+        {
+          name: cn,
+          url: URL_PREFIX + "/" + pathname + "/" + cn,
+          isActive: true,
+        },
+      ];
+      setBreadcrumbItems(currentPath);
+      setActiveTabKey("settings");
+    }
+  }, [cn]);
+
+  // Handling of the API data
+  if (idpRefSettingsData.isLoading || !idpRefSettingsData.idpRef) {
+    return <DataSpinner />;
+  }
+
+  // Show the 'NotFound' page if the host is not found
+  if (
+    !idpRefSettingsData.isLoading &&
+    Object.keys(idpRefSettingsData.idpRef).length === 0
+  ) {
+    return <NotFound />;
+  }
+
+  // Return component
+  return (
+    <>
+      <PageSection variant={PageSectionVariants.light} className="pf-v5-u-pr-0">
+        <BreadCrumb
+          className="pf-v5-u-mb-md"
+          breadcrumbItems={breadcrumbItems}
+        />
+        <TitleLayout
+          id={id}
+          preText="Identity provider reference:"
+          text={id}
+          headingLevel="h1"
+        />
+      </PageSection>
+      <PageSection type="tabs" variant={PageSectionVariants.light} isFilled>
+        <Tabs
+          activeKey={activeTabKey}
+          onSelect={handleTabClick}
+          variant="light300"
+          isBox
+          className="pf-v5-u-ml-lg"
+          mountOnEnter
+          unmountOnExit
+        >
+          <Tab
+            eventKey={"settings"}
+            name="settings-details"
+            title={<TabTitleText>Settings</TabTitleText>}
+          >
+            <IdpRefSettings
+              idpRef={idpRefSettingsData.idpRef}
+              originalIdpRef={idpRefSettingsData.originalIdpRef}
+              metadata={idpRefSettingsData.metadata}
+              onIdpRefChange={idpRefSettingsData.setIdpRef}
+              onRefresh={idpRefSettingsData.refetch}
+              isModified={idpRefSettingsData.modified}
+              isDataLoading={idpRefSettingsData.isLoading}
+              modifiedValues={idpRefSettingsData.modifiedValues}
+              onResetValues={idpRefSettingsData.resetValues}
+              pathname={pathname}
+            />
+          </Tab>
+        </Tabs>
+      </PageSection>
+    </>
+  );
+};
+
+export default IdpReferencesTabs;
diff --git a/src/services/rpcIdp.ts b/src/services/rpcIdp.ts
index 63b0d9798..a37dbe387 100644
--- a/src/services/rpcIdp.ts
+++ b/src/services/rpcIdp.ts
@@ -7,20 +7,23 @@ import {
   getCommand,
 } from "./rpc";
 // Data types
-import { cnType } from "src/utils/datatypes/globalDataTypes";
+import { cnType, IDPServer } from "src/utils/datatypes/globalDataTypes";
 // Redux
 import { FetchBaseQueryError } from "@reduxjs/toolkit/query";
 import { API_VERSION_BACKUP } from "src/utils/utils";
+import { apiToIdpServer } from "src/utils/ipdServerUtils";
 
 /**
  * IdP-related endpoints: useGetIdpEntriesQuery, useSearchIdpEntriesMutation
- *                        useIdpAddMutation, useIdpDeleteMutation
+ *                        useIdpAddMutation, useIdpDeleteMutation, useIdpShowQuery,
+ *                        useIdpModMutation
  *
  * API commands:
  * - idp_find: https://freeipa.readthedocs.io/en/latest/api/idp_find.html
  * - idp_show: https://freeipa.readthedocs.io/en/latest/api/idp_show.html
  * - idp_add: https://freeipa.readthedocs.io/en/latest/api/idp_add.html
  * - idp_del: https://freeipa.readthedocs.io/en/latest/api/idp_del.html
+ * - idp_mod: https://freeipa.readthedocs.io/en/latest/api/idp_mod.html
  */
 
 export interface IdpFindPayload {
@@ -61,6 +64,7 @@ export interface AddParams {
   // Selector option
   ipaidpprovider?: string;
   // Other optional fields
+  ipaidpclientsecret?: string;
   ipaidpscope?: string;
   ipaidpsub?: string;
   version?: string;
@@ -79,11 +83,26 @@ export interface IdpAddPayload {
   // Custom definition fields
   customFields?: CustomIdpAddPayload;
   // Other optional fields
+  ipaidpclientsecret?: string;
   ipaidpscope?: string;
   ipaidpsub?: string;
   version?: string;
 }
 
+export interface IdpModPayload {
+  idpId: string;
+  ipaidpclientid?: string;
+  ipaidpclientsecret?: string;
+  ipaidpscope?: string;
+  ipaidpsub?: string;
+  ipaidpauthendpoint?: string;
+  ipaidpdevauthendpoint?: string;
+  ipaidptokenendpoint?: string;
+  ipaidpuserinfoendpoint?: string;
+  ipaidpkeysendpoint?: string;
+  ipaidpissuerurl?: string;
+}
+
 const extendedApi = api.injectEndpoints({
   endpoints: (build) => ({
     /**
@@ -249,13 +268,14 @@ const extendedApi = api.injectEndpoints({
     idpAdd: build.mutation<FindRPCResponse, IdpAddPayload>({
       query: (payload) => {
         const payloadReceived: AddParams = {
-          // cn: payload.cn,
           ipaidpclientid: payload.ipaidpclientid,
-          // ipaidpprovider: payload.ipaidpprovider,
           version: payload.version || API_VERSION_BACKUP,
         };
 
         // Add optional fields if not undefined
+        if (payload.ipaidpclientsecret) {
+          payloadReceived.ipaidpclientsecret = payload.ipaidpclientsecret;
+        }
         if (payload.ipaidpscope) {
           payloadReceived.ipaidpscope = payload.ipaidpscope;
         }
@@ -331,6 +351,66 @@ const extendedApi = api.injectEndpoints({
         return getBatchCommand(commands, API_VERSION_BACKUP);
       },
     }),
+    /**
+     * Show a specific Identity Provider (with all its details).
+     * @param string
+     * @returns IDPServer
+     */
+    idpShow: build.query<IDPServer, string>({
+      query: (idpId) => {
+        return getCommand({
+          method: "idp_show",
+          params: [
+            [idpId],
+            { all: true, rights: true, version: API_VERSION_BACKUP },
+          ],
+        });
+      },
+      transformResponse: (response: FindRPCResponse) => {
+        const idpServer = apiToIdpServer(response.result.result);
+        // return response.result.result as unknown as IDPServer;
+        return idpServer;
+      },
+    }),
+    /**
+     * Update a specific IdP data.
+     * @param IdpModPayload
+     * @returns FindRPCResponse
+     */
+    idpMod: build.mutation<FindRPCResponse, IdpModPayload>({
+      query: (payload) => {
+        const params: Record<string, unknown> = {
+          all: true,
+          rights: true,
+          version: API_VERSION_BACKUP,
+        };
+
+        const optionalKeys: Array<keyof Omit<IdpModPayload, "idpId">> = [
+          "ipaidpclientid",
+          "ipaidpclientsecret",
+          "ipaidpscope",
+          "ipaidpsub",
+          "ipaidpauthendpoint",
+          "ipaidpdevauthendpoint",
+          "ipaidptokenendpoint",
+          "ipaidpuserinfoendpoint",
+          "ipaidpkeysendpoint",
+          "ipaidpissuerurl",
+        ];
+
+        optionalKeys.forEach((key) => {
+          const value = payload[key];
+          if (value !== undefined) {
+            params[key] = value.toString();
+          }
+        });
+
+        return getCommand({
+          method: "idp_mod",
+          params: [[payload.idpId], params],
+        });
+      },
+    }),
   }),
   overrideExisting: false,
 });
@@ -340,4 +420,6 @@ export const {
   useSearchIdpEntriesMutation,
   useIdpAddMutation,
   useIdpDeleteMutation,
+  useIdpShowQuery,
+  useIdpModMutation,
 } = extendedApi;
diff --git a/src/utils/datatypes/globalDataTypes.ts b/src/utils/datatypes/globalDataTypes.ts
index 01330b0e3..b905706df 100644
--- a/src/utils/datatypes/globalDataTypes.ts
+++ b/src/utils/datatypes/globalDataTypes.ts
@@ -1,5 +1,7 @@
 /* eslint-disable @typescript-eslint/no-explicit-any */
 
+import { ValidatedOptions } from "@patternfly/react-core";
+
 export interface User {
   [key: string]: unknown; // to fulfill Record<string, unknown> type
   // identity
@@ -702,3 +704,9 @@ export interface KrbTicket {
   aci: string[];
   dn: string;
 }
+
+export interface PasswordValidationType {
+  isError: boolean;
+  message: string;
+  pfError: ValidatedOptions;
+}
diff --git a/src/utils/utils.tsx b/src/utils/utils.tsx
index b63a177dc..48a896aa8 100644
--- a/src/utils/utils.tsx
+++ b/src/utils/utils.tsx
@@ -428,3 +428,8 @@ export function paginate<Type>(
 export function containsAny(array1: unknown[], array2: unknown[]): boolean {
   return array1.some((item) => array2.includes(item));
 }
+
+/**
+ * Returns hidden password string
+ */
+export const HIDDEN_PASSWORD = "********";


Reviews:
user: login: sourcery-ai[bot]
body: Hey @carma12 - I've reviewed your changes - here's some feedback:

**Overall Comments**:
- Consider adding error handling for network failures in the `useIdpModMutation` API calls to improve robustness.
- The `buildPayload` function in `IdpRefSettings` could be optimized by using a loop or a utility function to reduce repetitive code when setting parameters.

<details>
<summary>Here's what I looked at during the review</summary>

- 🟢 **General issues**: all looks good
- 🟢 **Security**: all looks good
- 🟢 **Testing**: all looks good
- 🟡 **Complexity**: 2 issues found
- 🟢 **Documentation**: all looks good
</details>

***

<details>
<summary>Sourcery is free for open source - if you like our reviews please consider sharing them ✨</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https://sourcery.ai)
- [Facebook](https://www.facebook.com/sharer/sharer.php?u=https://sourcery.ai)

</details>

<sub>
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
</sub>
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-26T09:32:52Z
user: login: duzda
body: Apart from notes,

You can add an entry even if secrets do not match, so please fix that, non matching secrets should prevent adding new entry. 

Setting secret does probably nothing, the entry then doesn't show it (it's blank).

Strangely, you can change the secret later, can't view it's contents and you are not asked to fill verify secret.

![image](https://github.com/user-attachments/assets/3fbfe559-b9d7-417d-b5eb-4d7a4d8e9356)
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-28T08:19:04Z
user: login: duzda
body: ![image](https://github.com/user-attachments/assets/a65a1383-1f28-4427-b5d7-972090272e9e)

Secret is still not shown as set even though it's been assigned in the add modal. I think it's not being added to the payload.

Please double check and fix: `src/components/modals/IdpReferences/AddModal.tsx:152`

state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-28T12:01:53Z
user: login: duzda
body: Seems to be fixed, thanks!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T08:14:30Z

Review comments:
diff_hunk: @@ -331,6 +348,78 @@ const extendedApi = api.injectEndpoints({
         return getBatchCommand(commands, API_VERSION_BACKUP);
       },
     }),
+    /**
+     * Show a specific Identity Provider (with all its details).
+     * @param string
+     * @returns IDPServer
+     */
+    idpShow: build.query<IDPServer, string>({
+      query: (idpId) => {
+        return getCommand({
+          method: "idp_show",
+          params: [
+            [idpId],
+            { all: true, rights: true, version: API_VERSION_BACKUP },
+          ],
+        });
+      },
+      transformResponse: (response: FindRPCResponse) => {
+        const idpServer = apiToIdpServer(response.result.result);
+        // return response.result.result as unknown as IDPServer;
+        return idpServer;
+      },
+    }),
+    /**
+     * Update a specific IdP data.
+     * @param IdpModPayload
+     * @returns FindRPCResponse
+     */
+    idpMod: build.mutation<FindRPCResponse, IdpModPayload>({
path: src/services/rpcIdp.ts
user: login: sourcery-ai[bot]
body: **issue (complexity):** Consider iterating over an array of optional keys to reduce repetition in the `idpMod` mutation.

You can reduce the repetition in the `idpMod` mutation by iterating over an array of optional keys. For example:

```ts
idpMod: build.mutation<FindRPCResponse, IdpModPayload>({
  query: (payload) => {
    const params: Record<string, any> = {
      all: true,
      rights: true,
      version: API_VERSION_BACKUP,
    };

    const optionalKeys: Array<keyof Omit<IdpModPayload, "idpId">> = [
      "ipaidpclientid",
      "ipaidpclientsecret",
      "ipaidpscope",
      "ipaidpsub",
      "ipaidpauthendpoint",
      "ipaidpdevauthendpoint",
      "ipaidptokenendpoint",
      "ipaidpuserinfoendpoint",
      "ipaidpkeysendpoint",
      "ipaidpissuerurl",
    ];

    optionalKeys.forEach(key => {
      const value = payload[key];
      if (value !== undefined) {
        params[key] = value.toString();
      }
    });

    return getCommand({
      method: "idp_mod",
      params: [[payload.idpId], params],
    });
  },
}),
```

This approach maintains your existing functionality while reducing code complexity and repetition.
created_at: 2025-03-26T09:32:52Z
author_association: NONE
diff_hunk: @@ -0,0 +1,414 @@
+import React from "react";
+// PatternFly
+import {
+  DropdownItem,
+  Flex,
+  FlexItem,
+  Form,
+  FormGroup,
+  JumpLinks,
+  JumpLinksItem,
+  Sidebar,
+  SidebarContent,
+  SidebarPanel,
+} from "@patternfly/react-core";
+// Data types
+import { IDPServer, Metadata } from "src/utils/datatypes/globalDataTypes";
+// Hooks
+import useUpdateRoute from "src/hooks/useUpdateRoute";
+import useAlerts from "src/hooks/useAlerts";
+// Utils
+import { asRecord } from "src/utils/subIdUtils";
+// Icons
+import { OutlinedQuestionCircleIcon } from "@patternfly/react-icons";
+// RPC
+import { IdpModPayload, useIdpModMutation } from "src/services/rpcIdp";
+// Components
+import IpaTextInput from "src/components/Form/IpaTextInput/IpaTextInput";
+import TabLayout from "src/components/layouts/TabLayout";
+import SecondaryButton from "src/components/layouts/SecondaryButton";
+import HelpTextWithIconLayout from "src/components/layouts/HelpTextWithIconLayout";
+import IpaTextContent from "src/components/Form/IpaTextContent/IpaTextContent";
+import TitleLayout from "src/components/layouts/TitleLayout";
+import IpaPasswordInput from "src/components/Form/IpaPasswordInput";
+import KebabLayout from "src/components/layouts/KebabLayout";
+import ResetIdpPassword from "src/components/ResetIdpPassword";
+
+interface PropsToIdpRefSettings {
+  idpRef: Partial<IDPServer>;
+  originalIdpRef: Partial<IDPServer>;
+  metadata: Metadata;
+  onIdpRefChange: (idpRef: Partial<IDPServer>) => void;
+  onRefresh: () => void;
+  isModified: boolean;
+  isDataLoading?: boolean;
+  modifiedValues: () => Partial<IDPServer>;
+  onResetValues: () => void;
+  pathname: string;
+}
+
+const IdpRefSettings = (props: PropsToIdpRefSettings) => {
path: src/pages/IdPReferences/IdpReferencesSettings.tsx
user: login: sourcery-ai[bot]
body: **issue (complexity):** Consider extracting repeated UI sections and helper functions into subcomponents and utility files.

Consider extracting some of the repeated UI sections and helper functions into focused subcomponents. For example, you could create a dedicated form component for each major section (e.g., OAuth client settings and Identity provider details) and move inline helper functions (like `buildPayload`) into a separate utility file. This would both reduce the size of the main component and make each piece easier to reason about.

**Example: Extracting the OAuth client form:**

```tsx
// OAuthClientForm.tsx
import React from "react";
import { Form, FormGroup } from "@patternfly/react-core";
import IpaTextInput from "src/components/Form/IpaTextInput/IpaTextInput";
import IpaTextContent from "src/components/Form/IpaTextContent/IpaTextContent";

interface OAuthClientFormProps {
  ipaObject: any;
  recordOnChange: (value: any) => void;
  metadata: any;
}
const OAuthClientForm = ({ ipaObject, recordOnChange, metadata }: OAuthClientFormProps) => (
  <Form className="pf-v5-u-mb-lg">
    <FormGroup label="Identity Provider reference name" fieldId="cn">
      <IpaTextContent
        name="cn"
        ariaLabel="Identity Provider reference name"
        ipaObject={ipaObject}
        objectName="idp"
        metadata={metadata}
      />
    </FormGroup>
    <FormGroup label="Client identifier" fieldId="ipaidpclientid" isRequired>
      <IpaTextInput
        name="ipaidpclientid"
        ariaLabel="Client identifier"
        ipaObject={ipaObject}
        onChange={recordOnChange}
        objectName="idp"
        metadata={metadata}
      />
    </FormGroup>
    <FormGroup label="Secret" fieldId="ipaidpclientsecret">
      <IpaTextInput
        // Alternatively, if this is a password input, reference IpaPasswordInput as needed
        name="ipaidpclientsecret"
        ariaLabel="Secret"
        ipaObject={ipaObject}
        onChange={recordOnChange}
        objectName="idp"
        metadata={metadata}
      />
    </FormGroup>
  </Form>
);

export default OAuthClientForm;
```

Then in your main component, you simply import and use it:

```tsx
import OAuthClientForm from "./OAuthClientForm";

// Inside render:
<FlexItem flex={{ default: "flex_1" }}>
  <OAuthClientForm
    ipaObject={ipaObject}
    recordOnChange={recordOnChange}
    metadata={props.metadata}
  />
</FlexItem>
```

**Example: Extracting the helper function:**

Create a utility module (e.g., `payloadUtils.ts`):

```tsx
// payloadUtils.ts
import { IdpModPayload } from "src/services/rpcIdp";
import { Partial } from "src/utils/datatypes/globalDataTypes";

export const buildPayload = (
  modifiedValues: Partial<any>,
  keyArray: string[],
  cn: string
): IdpModPayload => {
  const payload: IdpModPayload = { idpId: cn };
  keyArray.forEach((key) => {
    if (modifiedValues[key] !== undefined) {
      payload[key] = modifiedValues[key] === "" ? [] : modifiedValues[key].toString();
    }
  });
  return payload;
};
```

Then import and use it in your main component:

```tsx
import { buildPayload } from "src/utils/payloadUtils";

// usage in onSave:
const payload = buildPayload(modifiedValues, keyArray, props.idpRef.cn?.toString() as string);
```

These extractions help reduce cognitive load and improve maintainability while keeping functionality intact.
created_at: 2025-03-26T09:32:52Z
author_association: NONE
diff_hunk: @@ -0,0 +1,207 @@
+import React from "react";
+// PatternFly
+import {
+  Button,
+  HelperText,
+  HelperTextItem,
+  ValidatedOptions,
+} from "@patternfly/react-core";
+// Data types
+import { IDPServer } from "src/utils/datatypes/globalDataTypes";
+// Modals
+import ModalWithFormLayout from "src/components/layouts/ModalWithFormLayout";
+// Components
+import PasswordInput from "src/components/layouts/PasswordInput";
+// RPC
+import { IdpModPayload, useIdpModMutation } from "src/services/rpcIdp";
+// Hooks
+import useAlerts from "src/hooks/useAlerts";
+
+interface PropsToResetIdpPassword {
+  idpId: string;
+  isOpen: boolean;
+  onClose: () => void;
+  onIdpRefChange: (idpRef: Partial<IDPServer>) => void;
+  onRefresh: () => void;
+}
+
+const ResetIdpPassword = (props: PropsToResetIdpPassword) => {
+  // Alerts to show in the UI
+  const alerts = useAlerts();
+
+  // RPC
+  const [resetPassword] = useIdpModMutation();
+
+  // States
+  const [newPassword, setNewPassword] = React.useState("");
+  const [verifyPassword, setVerifyPassword] = React.useState("");
+  const [passwordHidden, setPasswordHidden] = React.useState(true);
+  const [verifyPasswordHidden, setVerifyPasswordHidden] = React.useState(true);
+
+  // Verify password
+  const [passwordValidationResult, setPasswordValidationResult] =
+    React.useState({
+      isError: false,
+      message: "",
+      pfError: ValidatedOptions.default,
+    });
path: src/components/ResetIdpPassword.tsx
user: login: duzda
body: Consider extracting the anonymous type to a proper interface

```
React.useState<Type>({
  isError: false,
  message: "",
  pfError: ValidatedOptions.default,
});
```
created_at: 2025-03-28T07:39:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,207 @@
+import React from "react";
+// PatternFly
+import {
+  Button,
+  HelperText,
+  HelperTextItem,
+  ValidatedOptions,
+} from "@patternfly/react-core";
+// Data types
+import { IDPServer } from "src/utils/datatypes/globalDataTypes";
+// Modals
+import ModalWithFormLayout from "src/components/layouts/ModalWithFormLayout";
+// Components
+import PasswordInput from "src/components/layouts/PasswordInput";
+// RPC
+import { IdpModPayload, useIdpModMutation } from "src/services/rpcIdp";
+// Hooks
+import useAlerts from "src/hooks/useAlerts";
+
+interface PropsToResetIdpPassword {
+  idpId: string;
+  isOpen: boolean;
+  onClose: () => void;
+  onIdpRefChange: (idpRef: Partial<IDPServer>) => void;
+  onRefresh: () => void;
+}
+
+const ResetIdpPassword = (props: PropsToResetIdpPassword) => {
+  // Alerts to show in the UI
+  const alerts = useAlerts();
+
+  // RPC
+  const [resetPassword] = useIdpModMutation();
+
+  // States
+  const [newPassword, setNewPassword] = React.useState("");
+  const [verifyPassword, setVerifyPassword] = React.useState("");
+  const [passwordHidden, setPasswordHidden] = React.useState(true);
+  const [verifyPasswordHidden, setVerifyPasswordHidden] = React.useState(true);
+
+  // Verify password
+  const [passwordValidationResult, setPasswordValidationResult] =
+    React.useState({
+      isError: false,
+      message: "",
+      pfError: ValidatedOptions.default,
+    });
+
+  // Reset the verify password field
+  const resetVerifyPassword = () => {
+    setPasswordValidationResult({
+      isError: false,
+      message: "",
+      pfError: ValidatedOptions.default,
+    });
+  };
+
+  // Checks that the passwords are the same
+  const validatePasswords = () => {
+    if (newPassword !== verifyPassword) {
+      const verifyPassVal = {
+        isError: true,
+        message: "Passwords must match",
+        pfError: ValidatedOptions.error,
+      };
+      setPasswordValidationResult(verifyPassVal);
path: src/components/ResetIdpPassword.tsx
user: login: duzda
body: You don't need the variable, I would normally not comment, but it's right after assigning anonymous object, so this stands out.
created_at: 2025-03-28T07:44:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,111 @@
+import React from "react";
+// RPC
+import { useGetObjectMetadataQuery } from "src/services/rpc";
+import { useIdpShowQuery } from "src/services/rpcIdp";
+// Data types
+import { IDPServer, Metadata } from "src/utils/datatypes/globalDataTypes";
+
+type IdpRefSettingsData = {
+  isLoading: boolean;
+  isFetching: boolean;
+  modified: boolean;
+  setModified: (value: boolean) => void;
+  resetValues: () => void;
+  metadata: Metadata;
+  originalIdpRef: Partial<IDPServer>;
+  idpRef: Partial<IDPServer>;
+  setIdpRef: (fqdn: Partial<IDPServer>) => void;
+  refetch: () => void;
+  modifiedValues: () => Partial<IDPServer>;
+};
+
+const useIdpRefSettingsData = (idpRefId: string): IdpRefSettingsData => {
+  // [API call] Metadata
+  const metadataQuery = useGetObjectMetadataQuery();
+  const metadata = metadataQuery.data || {};
+  const metadataLoading = metadataQuery.isLoading;
+
+  // [API call] IdP Reference
+  const idpRefDetails = useIdpShowQuery(idpRefId);
+  const idpRefData = idpRefDetails.data;
+  const isIdpRefDataLoading = idpRefDetails.isLoading;
+
+  const [modified, setModified] = React.useState(false);
+
+  // Data displayed and modified by the user
+  const [idpRef, setIdpRef] = React.useState<Partial<IDPServer>>({});
+
+  React.useEffect(() => {
+    if (idpRefData && !idpRefDetails.isFetching) {
+      setIdpRef({ ...idpRefData });
+    }
+  }, [idpRefData, idpRefDetails.isFetching]);
+
+  const settings = {
+    isLoading: metadataLoading || isIdpRefDataLoading,
+    isFetching: idpRefDetails.isFetching,
+    modified,
+    setModified,
+    metadata,
+    // eslint-disable-next-line @typescript-eslint/no-empty-function
+    resetValues: () => {},
+    originalIdpRef: idpRef,
+    setIdpRef,
+    refetch: idpRefDetails.refetch,
+    idpRef,
+    modifiedValues: () => idpRef,
+  } as IdpRefSettingsData;
path: src/hooks/useIdpRefSettingsData.tsx
user: login: duzda
body: Please use 

```
const settings: IdpRefSettingsData = { ... };
```

Instead of
```
const settings = { ... } as IdpRefSettingsData;
```
created_at: 2025-03-28T07:50:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,414 @@
+import React from "react";
+// PatternFly
+import {
+  DropdownItem,
+  Flex,
+  FlexItem,
+  Form,
+  FormGroup,
+  JumpLinks,
+  JumpLinksItem,
+  Sidebar,
+  SidebarContent,
+  SidebarPanel,
+} from "@patternfly/react-core";
+// Data types
+import { IDPServer, Metadata } from "src/utils/datatypes/globalDataTypes";
+// Hooks
+import useUpdateRoute from "src/hooks/useUpdateRoute";
+import useAlerts from "src/hooks/useAlerts";
+// Utils
+import { asRecord } from "src/utils/subIdUtils";
+// Icons
+import { OutlinedQuestionCircleIcon } from "@patternfly/react-icons";
+// RPC
+import { IdpModPayload, useIdpModMutation } from "src/services/rpcIdp";
+// Components
+import IpaTextInput from "src/components/Form/IpaTextInput/IpaTextInput";
+import TabLayout from "src/components/layouts/TabLayout";
+import SecondaryButton from "src/components/layouts/SecondaryButton";
+import HelpTextWithIconLayout from "src/components/layouts/HelpTextWithIconLayout";
+import IpaTextContent from "src/components/Form/IpaTextContent/IpaTextContent";
+import TitleLayout from "src/components/layouts/TitleLayout";
+import IpaPasswordInput from "src/components/Form/IpaPasswordInput";
+import KebabLayout from "src/components/layouts/KebabLayout";
+import ResetIdpPassword from "src/components/ResetIdpPassword";
+
+interface PropsToIdpRefSettings {
+  idpRef: Partial<IDPServer>;
+  originalIdpRef: Partial<IDPServer>;
+  metadata: Metadata;
+  onIdpRefChange: (idpRef: Partial<IDPServer>) => void;
+  onRefresh: () => void;
+  isModified: boolean;
+  isDataLoading?: boolean;
+  modifiedValues: () => Partial<IDPServer>;
+  onResetValues: () => void;
+  pathname: string;
+}
+
+const IdpRefSettings = (props: PropsToIdpRefSettings) => {
+  // Alerts to show in the UI
+  const alerts = useAlerts();
+
+  // Update current route data to Redux and highlight the current page in the Nav bar
+  useUpdateRoute({ pathname: props.pathname });
+
+  // States
+  const [isDataLoading, setIsDataLoading] = React.useState(false);
+
+  // Get 'ipaObject' and 'recordOnChange' to use in 'IpaTextInput'
+  const { ipaObject, recordOnChange } = asRecord(
+    props.idpRef,
+    props.onIdpRefChange
+  );
+
+  // API calls
+  const [saveIdpRef] = useIdpModMutation();
+
+  // 'Revert' handler method
+  const onRevert = () => {
+    props.onIdpRefChange(props.originalIdpRef);
+    props.onRefresh();
+    alerts.addAlert(
+      "revert-success",
+      "Identity Provider data reverted",
+      "success"
+    );
+  };
+
+  // Helper method to build the payload based on values
+  const buildPayload = (
+    modifiedValues: Partial<IDPServer>,
+    keyArray: string[]
+  ): IdpModPayload => {
+    const payload: IdpModPayload = {
+      idpId: props.idpRef.cn?.toString() as string,
+    };
+
+    keyArray.forEach((key) => {
+      // Modified values are either the value (in string) or an array ([]) when set to empty string
+      if (modifiedValues[key] !== undefined) {
+        if (modifiedValues[key] === "") {
+          payload[key] = [];
+        } else {
+          payload[key] = modifiedValues[key].toString();
+        }
+      }
+    });
+    return payload;
+  };
+
+  // on Save handler method
+  const onSave = () => {
+    setIsDataLoading(true);
+    const modifiedValues = props.modifiedValues();
+
+    // Generate payload
+    const payload = buildPayload(modifiedValues, [
+      "ipaidpclientid",
+      "ipaidpclientsecret",
+      "ipaidpscope",
+      "ipaidpsub",
+      "ipaidpauthendpoint",
+      "ipaidpdevauthendpoint",
+      "ipaidptokenendpoint",
+      "ipaidpuserinfoendpoint",
+      "ipaidpkeysendpoint",
+      "ipaidpissuerurl",
+    ]);
+
+    saveIdpRef(payload).then((response) => {
+      if ("data" in response) {
+        const data = response.data;
+        if (data.error) {
+          alerts.addAlert("error", (data.error as Error).message, "danger");
+        }
+        if (data.result) {
+          props.onIdpRefChange(data.result.result);
+          alerts.addAlert(
+            "success",
+            "Identity Provider '" + props.idpRef.cn + "' updated",
+            "success"
+          );
+          // Reset values. Disable 'revert' and 'save' buttons
+          props.onResetValues();
+        }
+      }
+      setIsDataLoading(false);
+    });
+  };
+
+  // 'Reset password' option
+  const [isResetPasswordModalOpen, setIsResetPasswordModalOpen] =
+    React.useState(false);
+
+  // Kebab
+  const [isKebabOpen, setIsKebabOpen] = React.useState(false);
+
+  const onKebabToggle = () => {
+    setIsKebabOpen(!isKebabOpen);
+  };
+
+  const onKebabSelect = (
+    // eslint-disable-next-line @typescript-eslint/no-unused-vars
+    _event: React.MouseEvent<Element, MouseEvent> | undefined
+  ) => {
+    setIsKebabOpen(!isKebabOpen);
+  };
path: src/pages/IdPReferences/IdpReferencesSettings.tsx
user: login: duzda
body: These two functions are duplicate, in any case, you don't need either of them and instead can just do 
```
<KebabLayout
  direction={"up"}
  onDropdownSelect={() => setIsKebabOpen(!isKebabOpen)}
  onKebabToggle={() => setIsKebabOpen(!isKebabOpen)}
  idKebab="toggle-action-buttons"
  isKebabOpen={isKebabOpen}
  dropdownItems={kebabItems}
/>
```
created_at: 2025-03-28T07:55:12Z
author_association: CONTRIBUTOR

PR comments:
user: login: sourcery-ai[bot]
created_at: 2025-03-26T09:31:53Z
body: <!-- Generated by sourcery-ai[bot]: start review_guide -->

## Reviewer's Guide by Sourcery

This pull request introduces a new 'Settings' page for managing Identity Provider (IdP) information, adds a 'Reset password' option in the kebab menu, and implements a new component for password input handling.

#### Class diagram for the updated IdP components

```mermaid
classDiagram
  class IdpModPayload {
    +string idpId
    +string ipaidpclientid
    +string ipaidpclientsecret
    +string ipaidpscope
    +string ipaidpsub
    +string ipaidpauthendpoint
    +string ipaidpdevauthendpoint
    +string ipaidptokenendpoint
    +string ipaidpuserinfoendpoint
    +string ipaidpkeysendpoint
    +string ipaidpissuerurl
  }
  class IdpRefSettings {
    +Partial<IDPServer> idpRef
    +Partial<IDPServer> originalIdpRef
    +Metadata metadata
    +onIdpRefChange(idpRef: Partial<IDPServer>)
    +onRefresh()
    +isModified: boolean
    +isDataLoading: boolean
    +modifiedValues(): Partial<IDPServer>
    +onResetValues()
    +pathname: string
  }
  class ResetIdpPassword {
    +string idpId
    +boolean isOpen
    +onClose()
    +onIdpRefChange(idpRef: Partial<IDPServer>)
    +onRefresh()
  }
  class IpaPasswordInput {
    +string name
    +string ariaLabel
    +value
    +onChange(value)
  }
  IdpRefSettings --> IdpModPayload
  ResetIdpPassword --> IdpModPayload
  IpaPasswordInput --> IdpRefSettings
```

### File-Level Changes

| Change | Details | Files |
| ------ | ------- | ----- |
| Added new API endpoints and payloads for IdP operations. | <ul><li>Introduced 'idpShow' query to fetch detailed IdP information.</li><li>Added 'idpMod' mutation to update IdP data.</li><li>Defined 'IdpModPayload' interface for updating IdP data.</li></ul> | `src/services/rpcIdp.ts` |
| Updated application routes to include new IdP settings page. | <ul><li>Added 'IdpReferencesTabs' component to handle IdP settings navigation.</li><li>Updated routes to include a path for IdP settings.</li></ul> | `src/navigation/AppRoutes.tsx` |
| Enhanced IdP references page to show links and manage settings. | <ul><li>Enabled links in IdP references table.</li><li>Added new 'IdpReferencesSettings' component for managing IdP settings.</li></ul> | `src/pages/IdPReferences/IdpReferences.tsx`<br/>`src/pages/IdPReferences/IdpReferencesSettings.tsx` |
| Implemented a new component for password input handling. | <ul><li>Created 'IpaPasswordInput' component for secure password input.</li></ul> | `src/components/Form/IpaPasswordInput.tsx` |
| Added functionality to reset IdP passwords. | <ul><li>Implemented 'ResetIdpPassword' component for password reset modal.</li><li>Integrated password reset option in the kebab menu.</li></ul> | `src/components/ResetIdpPassword.tsx`<br/>`src/pages/IdPReferences/IdpReferencesSettings.tsx` |
| Introduced hooks for managing IdP settings data. | <ul><li>Created 'useIdpRefSettingsData' hook to manage IdP settings state and API interactions.</li></ul> | `src/hooks/useIdpRefSettingsData.tsx` |

---

<details>
<summary>Tips and commands</summary>

#### Interacting with Sourcery

- **Trigger a new review:** Comment `@sourcery-ai review` on the pull request.
- **Continue discussions:** Reply directly to Sourcery's review comments.
- **Generate a GitHub issue from a review comment:** Ask Sourcery to create an
  issue from a review comment by replying to it. You can also reply to a
  review comment with `@sourcery-ai issue` to create an issue from it.
- **Generate a pull request title:** Write `@sourcery-ai` anywhere in the pull
  request title to generate a title at any time. You can also comment
  `@sourcery-ai title` on the pull request to (re-)generate the title at any time.
- **Generate a pull request summary:** Write `@sourcery-ai summary` anywhere in
  the pull request body to generate a PR summary at any time exactly where you
  want it. You can also comment `@sourcery-ai summary` on the pull request to
  (re-)generate the summary at any time.
- **Generate reviewer's guide:** Comment `@sourcery-ai guide` on the pull
  request to (re-)generate the reviewer's guide at any time.
- **Resolve all Sourcery comments:** Comment `@sourcery-ai resolve` on the
  pull request to resolve all Sourcery comments. Useful if you've already
  addressed all the comments and don't want to see them anymore.
- **Dismiss all Sourcery reviews:** Comment `@sourcery-ai dismiss` on the pull
  request to dismiss all existing Sourcery reviews. Especially useful if you
  want to start fresh with a new review - don't forget to comment
  `@sourcery-ai review` to trigger a new review!
- **Generate a plan of action for an issue:** Comment `@sourcery-ai plan` on
  an issue to generate a plan of action for it.

#### Customizing Your Experience

Access your [dashboard](https://app.sourcery.ai) to:
- Enable or disable review features such as the Sourcery-generated pull request
  summary, the reviewer's guide, and others.
- Change the review language.
- Add, remove or edit custom review instructions.
- Adjust other review settings.

#### Getting Help

- [Contact our support team](mailto:support@sourcery.ai) for questions or feedback.
- Visit our [documentation](https://docs.sourcery.ai) for detailed guides and information.
- Keep in touch with the Sourcery team by following us on [X/Twitter](https://x.com/SourceryAI), [LinkedIn](https://www.linkedin.com/company/sourcery-ai/) or [GitHub](https://github.com/sourcery-ai).

</details>

<!-- Generated by sourcery-ai[bot]: end review_guide -->
author_association: NONE
user: login: carma12
created_at: 2025-03-26T11:05:22Z
body: I corrected the code based on some sourcery.ai suggestions :) 
author_association: COLLABORATOR
user: login: carma12
created_at: 2025-03-28T10:38:40Z
body: @duzda - The code has been corrected based on your feedback.
author_association: COLLABORATOR
user: login: carma12
created_at: 2025-03-28T12:33:11Z
body: > ![image](https://private-user-images.githubusercontent.com/25201406/428000751-a65a1383-1f28-4427-b5d7-972090272e9e.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDMxNjU0MjEsIm5iZiI6MTc0MzE2NTEyMSwicGF0aCI6Ii8yNTIwMTQwNi80MjgwMDA3NTEtYTY1YTEzODMtMWYyOC00NDI3LWI1ZDctOTcyMDkwMjcyZTllLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAzMjglMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMzI4VDEyMzIwMVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTcwY2FiYTIzYzg5ZTM4NzUzYzY2MmFiMTRiMjU5N2U4NTg4OTU4MGIxNjA2NTMyN2M5YzVhZmM0MTIyYTI4MWMmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.rNStk2i4eKJ1NdwICF4i99HqJXxkzmEHhd-N_ucjkT4)
> 
> Secret is still not shown as set even though it's been assigned in the add modal. I think it's not being added to the payload.
> 
> Please double check and fix: `src/components/modals/IdpReferences/AddModal.tsx:152`

That's quite weird, I thought this was fixed already. Will check...
author_association: COLLABORATOR
user: login: carma12
created_at: 2025-03-28T13:34:31Z
body: @duzda - It seems I was not managing the `ipaidpclientsecret` correctly when creating the IdP reference. I already fix that, but just in case please check it yourself.
author_association: COLLABORATOR