From b0b967d8d4052790caff63c7bcc8637eb1c56b12 Mon Sep 17 00:00:00 2001 From: Erik Jan de Wit Date: Wed, 6 Mar 2024 17:06:13 +0100 Subject: [PATCH] migrated user forms to ui-shared (#27593) * migrated user forms to ui-shared Signed-off-by: Erik Jan de Wit * review comments Signed-off-by: Erik Jan de Wit * fixed test Signed-off-by: Erik Jan de Wit --------- Signed-off-by: Erik Jan de Wit --- .../admin-ui/manage/users/CredentialsPage.ts | 16 +- .../tabs/IdentityProviderLinksTab.ts | 4 +- .../admin/messages/messages_en.properties | 1 + js/apps/admin-ui/src/user/UserForm.tsx | 446 ++++++++---------- js/apps/admin-ui/src/user/UserIdPModal.tsx | 85 ++-- .../user/user-credentials/LifespanField.tsx | 37 +- .../RequiredActionMultiSelect.tsx | 85 +--- .../ResetCredentialDialog.tsx | 11 +- .../user-credentials/ResetPasswordDialog.tsx | 48 +- .../ui-shared/src/controls/TextControl.tsx | 2 + 10 files changed, 284 insertions(+), 451 deletions(-) diff --git a/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/users/CredentialsPage.ts b/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/users/CredentialsPage.ts index 43e34dedd6..5710875b9b 100644 --- a/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/users/CredentialsPage.ts +++ b/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/users/CredentialsPage.ts @@ -6,16 +6,16 @@ export default class CredentialsPage { readonly #setPasswordBtn = "confirm"; readonly #credentialResetModal = "credential-reset-modal"; readonly #resetModalActionsToggleBtn = - "[data-testid=credential-reset-modal] #actions-actions"; + "[data-testid=credential-reset-modal] #actions"; readonly #passwordField = "passwordField"; readonly #passwordConfirmationField = "passwordConfirmationField"; readonly #resetActions = [ - "VERIFY_EMAIL-option", - "UPDATE_PROFILE-option", - "CONFIGURE_TOTP-option", - "UPDATE_PASSWORD-option", - "TERMS_AND_CONDITIONS-option", + "Verify Email", + "Update Profile", + "Configure OTP", + "Update Password", + "Terms and Conditions", ]; readonly #confirmationButton = "confirm"; readonly #editLabelBtn = "editUserLabelBtn"; @@ -57,7 +57,9 @@ export default class CredentialsPage { } clickResetModalAction(index: number) { - cy.findByTestId(this.#resetActions[index]).click(); + cy.get("[data-testid=credential-reset-modal] .pf-c-select__menu") + .contains(this.#resetActions[index]) + .click(); return this; } diff --git a/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/users/user_details/tabs/IdentityProviderLinksTab.ts b/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/users/user_details/tabs/IdentityProviderLinksTab.ts index 52e1a1ae59..354a203013 100644 --- a/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/users/user_details/tabs/IdentityProviderLinksTab.ts +++ b/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/users/user_details/tabs/IdentityProviderLinksTab.ts @@ -9,8 +9,8 @@ export default class IdentityProviderLinksTab { #availableProvidersSection = ".kc-available-idps"; #linkAccountBtn = ".pf-c-button.pf-m-link"; #linkAccountModalIdentityProviderInput = "idpNameInput"; - #linkAccountModalUserIdInput = "userIdInput"; - #linkAccountModalUsernameInput = "usernameInput"; + #linkAccountModalUserIdInput = "userId"; + #linkAccountModalUsernameInput = "userName"; public clickLinkAccount(idpName: string) { cy.get(this.#availableProvidersSection + " tr") diff --git a/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties b/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties index 01727d83fe..4c1bd57f2d 100644 --- a/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties +++ b/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties @@ -2853,6 +2853,7 @@ dropNonexistingGroupsDuringSync=Drop non-existing groups during sync clientAssertionSigningAlgHelp=Signature algorithm to create JWT assertion as client authentication. In the case of JWT signed with private key or JWT signed with client secret, it is required. If no algorithm is specified, the following algorithm is adapted. RS256 is adapted in the case of JWT signed with private key. HS256 is adapted in the case of JWT signed with client secret. jwtX509HeadersEnabledHelp=If enabled, the x5t (X.509 Certificate SHA-1 Thumbprint) header will be added to the JWT to reference the certificate used to sign it. Otherwise, the kid (Key ID) header will be used instead. addProvider_other=Add {{provider}} providers +resetAction=Reset action cibaExpiresIn=Expires In dynamicScopeFormatHelp=This is the regular expression that the system will use to extract the scope name and variable. updateTranslationError=Error updating translation. diff --git a/js/apps/admin-ui/src/user/UserForm.tsx b/js/apps/admin-ui/src/user/UserForm.tsx index 8e570e922d..1a6af99347 100644 --- a/js/apps/admin-ui/src/user/UserForm.tsx +++ b/js/apps/admin-ui/src/user/UserForm.tsx @@ -13,13 +13,18 @@ import { Switch, } from "@patternfly/react-core"; import { TFunction } from "i18next"; -import { useState, useEffect } from "react"; -import { Controller, UseFormReturn } from "react-hook-form"; +import { useEffect, useState } from "react"; +import { Controller, FormProvider, UseFormReturn } from "react-hook-form"; import { useTranslation } from "react-i18next"; import { Link } from "react-router-dom"; -import { HelpItem, UserProfileFields } from "ui-shared"; - +import { + HelpItem, + SwitchControl, + TextControl, + UserProfileFields, +} from "ui-shared"; import { adminClient } from "../admin-client"; +import { DefaultSwitchControl } from "../components/SwitchControl"; import { useAlerts } from "../components/alert/Alerts"; import { FormAccess } from "../components/form/FormAccess"; import { GroupPickerDialog } from "../components/group/GroupPickerDialog"; @@ -68,7 +73,6 @@ export const UserForm = ({ const { handleSubmit, - register, setValue, watch, control, @@ -133,284 +137,214 @@ export const UserForm = ({ fineGrainedAccess={user?.access?.manage} className="pf-u-mt-lg" > - {open && ( - { - user?.id ? addGroups(groups || []) : addChips(groups || []); - setOpen(false); - }} - onClose={() => setOpen(false)} - filterGroups={selectedGroups} + + {open && ( + { + user?.id ? addGroups(groups || []) : addChips(groups || []); + setOpen(false); + }} + onClose={() => setOpen(false)} + filterGroups={selectedGroups} + /> + )} + {user?.id && ( + <> + + + + + + + + )} + - )} - {user?.id && ( - <> - - - - - - - - )} - - {(user?.federationLink || user?.origin) && canViewFederationLink && ( - - } - > - - - )} - {userProfileMetadata ? ( - <> + {(user?.federationLink || user?.origin) && canViewFederationLink && ( } > - ( - field.onChange(value)} - isChecked={field.value} - label={t("yes")} - labelOff={t("no")} - /> - )} - /> + - - t(key as string, params as any)) as TFunction - } - /> - - ) : ( - <> - {!realm.registrationEmailAsUsername && ( - - + + + t(key as string, params as any)) as TFunction + } + /> + + ) : ( + <> + {!realm.registrationEmailAsUsername && ( + - - )} - - - + + + + + )} + {isBruteForceProtected && ( } + > + { + unLockUser(); + setLocked(value); + }} + isChecked={locked} + isDisabled={!locked} + label={t("on")} + labelOff={t("off")} + /> + + )} + {!user?.id && ( + + } > ( - field.onChange(value)} - isChecked={field.value} - label={t("yes")} - labelOff={t("no")} - /> + render={() => ( + + + {selectedGroups.map((currentChip) => ( + deleteItem(currentChip.name!)} + > + {currentChip.path} + + ))} + + + )} /> - - - - - - - - )} - {isBruteForceProtected && ( - - } - > - { - unLockUser(); - setLocked(value); - }} - isChecked={locked} - isDisabled={!locked} - label={t("on")} - labelOff={t("off")} - /> - - )} - {!user?.id && ( - } - > - ( - - - {selectedGroups.map((currentChip) => ( - deleteItem(currentChip.name!)} - > - {currentChip.path} - - ))} - - - - )} - /> - - )} + )} - - - - + + + + + ); }; diff --git a/js/apps/admin-ui/src/user/UserIdPModal.tsx b/js/apps/admin-ui/src/user/UserIdPModal.tsx index 7faff16fe6..93f3b6c3bd 100644 --- a/js/apps/admin-ui/src/user/UserIdPModal.tsx +++ b/js/apps/admin-ui/src/user/UserIdPModal.tsx @@ -7,12 +7,11 @@ import { FormGroup, Modal, ModalVariant, - ValidatedOptions, } from "@patternfly/react-core"; import { capitalize } from "lodash-es"; -import { useForm } from "react-hook-form"; +import { FormProvider, useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; - +import { TextControl } from "ui-shared"; import { adminClient } from "../admin-client"; import { useAlerts } from "../components/alert/Alerts"; import { KeycloakTextInput } from "../components/keycloak-text-input/KeycloakTextInput"; @@ -32,13 +31,13 @@ export const UserIdpModal = ({ }: UserIdpModalProps) => { const { t } = useTranslation(); const { addAlert, addError } = useAlerts(); - const { - register, - handleSubmit, - formState: { isValid, errors }, - } = useForm({ + const form = useForm({ mode: "onChange", }); + const { + handleSubmit, + formState: { isValid }, + } = form; const onSubmit = async ( federatedIdentity: FederatedIdentityRepresentation, @@ -87,55 +86,33 @@ export const UserIdpModal = ({ isOpen >
- - - - - + + + + - - - - +
); diff --git a/js/apps/admin-ui/src/user/user-credentials/LifespanField.tsx b/js/apps/admin-ui/src/user/user-credentials/LifespanField.tsx index b6e7ca090e..cc42de1b8f 100644 --- a/js/apps/admin-ui/src/user/user-credentials/LifespanField.tsx +++ b/js/apps/admin-ui/src/user/user-credentials/LifespanField.tsx @@ -1,37 +1,20 @@ -import { FormGroup } from "@patternfly/react-core"; -import { Controller, useFormContext } from "react-hook-form"; import { useTranslation } from "react-i18next"; - -import { HelpItem } from "ui-shared"; -import { TimeSelector } from "../../components/time-selector/TimeSelector"; +import { TimeSelectorControl } from "../../components/time-selector/TimeSelectorControl"; import { credResetFormDefaultValues } from "./ResetCredentialDialog"; export const LifespanField = () => { const { t } = useTranslation(); - const { control } = useFormContext(); return ( - - } - > - ( - - )} - /> - + labelIcon={t("lifespanHelp")} + units={["minute", "hour", "day"]} + menuAppendTo="parent" + controller={{ + defaultValue: credResetFormDefaultValues.lifespan, + }} + /> ); }; diff --git a/js/apps/admin-ui/src/user/user-credentials/RequiredActionMultiSelect.tsx b/js/apps/admin-ui/src/user/user-credentials/RequiredActionMultiSelect.tsx index 7b991f5119..17d92c6286 100644 --- a/js/apps/admin-ui/src/user/user-credentials/RequiredActionMultiSelect.tsx +++ b/js/apps/admin-ui/src/user/user-credentials/RequiredActionMultiSelect.tsx @@ -1,20 +1,9 @@ import type RequiredActionProviderRepresentation from "@keycloak/keycloak-admin-client/lib/defs/requiredActionProviderRepresentation"; -import { - FormGroup, - Select, - SelectOption, - SelectVariant, -} from "@patternfly/react-core"; +import { SelectVariant } from "@patternfly/react-core"; import { useState } from "react"; -import { - Control, - Controller, - FieldPathByValue, - FieldValues, - PathValue, -} from "react-hook-form"; +import { FieldPathByValue, FieldValues } from "react-hook-form"; import { useTranslation } from "react-i18next"; -import { HelpItem } from "ui-shared"; +import { SelectControl } from "ui-shared"; import { adminClient } from "../../admin-client"; import { useFetch } from "../../utils/useFetch"; @@ -23,7 +12,6 @@ export type RequiredActionMultiSelectProps< T extends FieldValues, P extends FieldPathByValue, > = { - control: Control; name: P; label: string; help: string; @@ -33,13 +21,11 @@ export const RequiredActionMultiSelect = < T extends FieldValues, P extends FieldPathByValue, >({ - control, name, label, help, }: RequiredActionMultiSelectProps) => { const { t } = useTranslation(); - const [open, setOpen] = useState(false); const [requiredActions, setRequiredActions] = useState< RequiredActionProviderRepresentation[] >([]); @@ -56,54 +42,23 @@ export const RequiredActionMultiSelect = < ); return ( - } - fieldId="actions" - > - } - control={control} - render={({ field }) => ( - - )} - /> - + labelIcon={t(help)} + controller={{ defaultValue: [] }} + maxHeight={375} + variant={SelectVariant.typeaheadMulti} + chipGroupProps={{ + numChips: 3, + }} + placeholderText={t("requiredActionPlaceholder")} + menuAppendTo="parent" + typeAheadAriaLabel={t("resetAction")} + options={requiredActions.map(({ alias, name }) => ({ + key: alias!, + value: name || alias!, + }))} + /> ); }; diff --git a/js/apps/admin-ui/src/user/user-credentials/ResetCredentialDialog.tsx b/js/apps/admin-ui/src/user/user-credentials/ResetCredentialDialog.tsx index a898bf2874..ebc3422d32 100644 --- a/js/apps/admin-ui/src/user/user-credentials/ResetCredentialDialog.tsx +++ b/js/apps/admin-ui/src/user/user-credentials/ResetCredentialDialog.tsx @@ -82,13 +82,12 @@ export const ResetCredentialDialog = ({ isHorizontal data-testid="credential-reset-modal" > - + diff --git a/js/apps/admin-ui/src/user/user-credentials/ResetPasswordDialog.tsx b/js/apps/admin-ui/src/user/user-credentials/ResetPasswordDialog.tsx index 1326cc2055..02262f7581 100644 --- a/js/apps/admin-ui/src/user/user-credentials/ResetPasswordDialog.tsx +++ b/js/apps/admin-ui/src/user/user-credentials/ResetPasswordDialog.tsx @@ -1,18 +1,16 @@ -import type UserRepresentation from "@keycloak/keycloak-admin-client/lib/defs/userRepresentation"; import { RequiredActionAlias } from "@keycloak/keycloak-admin-client/lib/defs/requiredActionProviderRepresentation"; +import type UserRepresentation from "@keycloak/keycloak-admin-client/lib/defs/userRepresentation"; import { AlertVariant, ButtonVariant, Form, FormGroup, - Switch, ValidatedOptions, } from "@patternfly/react-core"; -import { Controller, useForm } from "react-hook-form"; +import { FormProvider, useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; -import { HelpItem } from "ui-shared"; - import { adminClient } from "../../admin-client"; +import { DefaultSwitchControl } from "../../components/SwitchControl"; import { useAlerts } from "../../components/alert/Alerts"; import { ConfirmDialogModal, @@ -49,18 +47,18 @@ export const ResetPasswordDialog = ({ onClose, }: ResetPasswordDialogProps) => { const { t } = useTranslation(); + const form = useForm({ + defaultValues: credFormDefaultValues, + mode: "onChange", + }); const { register, - control, formState: { isValid, errors }, watch, handleSubmit, clearErrors, setError, - } = useForm({ - defaultValues: credFormDefaultValues, - mode: "onChange", - }); + } = form; const [confirm, toggle] = useToggle(true); const password = watch("password", ""); @@ -201,32 +199,14 @@ export const ResetPasswordDialog = ({ })} /> - - } - fieldId="kc-temporaryPassword" - > - + ( - - )} + label={t("temporaryPassword")} + labelIcon={t("temporaryPasswordHelpText")} + defaultValue="true" /> - + diff --git a/js/libs/ui-shared/src/controls/TextControl.tsx b/js/libs/ui-shared/src/controls/TextControl.tsx index 2f9560a7f2..8911b63590 100644 --- a/js/libs/ui-shared/src/controls/TextControl.tsx +++ b/js/libs/ui-shared/src/controls/TextControl.tsx @@ -18,6 +18,7 @@ export type TextControlProps< label: string; labelIcon?: string; isDisabled?: boolean; + helperText?: string; }; export const TextControl = < @@ -42,6 +43,7 @@ export const TextControl = < labelIcon={labelIcon} isRequired={required} error={fieldState.error} + helperText={props.helperText} >