migrated user forms to ui-shared (#27593)

* migrated user forms to ui-shared

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* review comments

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* fixed test

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

---------

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
This commit is contained in:
Erik Jan de Wit 2024-03-06 17:06:13 +01:00 committed by GitHub
parent 39299eeb38
commit b0b967d8d4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 284 additions and 451 deletions

View file

@ -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;
}

View file

@ -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")

View file

@ -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.

View file

@ -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,6 +137,7 @@ export const UserForm = ({
fineGrainedAccess={user?.access?.manage}
className="pf-u-mt-lg"
>
<FormProvider {...form}>
{open && (
<GroupPickerDialog
type="selectMany"
@ -156,24 +161,23 @@ export const UserForm = ({
id={user.id}
aria-label={t("userID")}
value={user.id}
type="text"
isReadOnly
readOnly
/>
</FormGroup>
<FormGroup label={t("createdAt")} fieldId="kc-created-at" isRequired>
<FormGroup
label={t("createdAt")}
fieldId="kc-created-at"
isRequired
>
<KeycloakTextInput
value={formatDate(new Date(user.createdTimestamp!))}
type="text"
id="kc-created-at"
aria-label={t("createdAt")}
name="createdTimestamp"
isReadOnly
readOnly
/>
</FormGroup>
</>
)}
<RequiredActionMultiSelect
control={control}
name="requiredActions"
label="requiredUserActions"
help="requiredUserActionsHelp"
@ -193,33 +197,11 @@ export const UserForm = ({
)}
{userProfileMetadata ? (
<>
<FormGroup
label={t("emailVerified")}
fieldId="kc-email-verified"
helperTextInvalid={t("required")}
labelIcon={
<HelpItem
helpText={t("emailVerifiedHelp")}
fieldLabelId="emailVerified"
/>
}
>
<Controller
<DefaultSwitchControl
name="emailVerified"
defaultValue={false}
control={control}
render={({ field }) => (
<Switch
data-testid="email-verified-switch"
id="kc-user-email-verified"
onChange={(value) => field.onChange(value)}
isChecked={field.value}
label={t("yes")}
labelOff={t("no")}
label={t("emailVerified")}
labelIcon={t("emailVerifiedHelp")}
/>
)}
/>
</FormGroup>
<UserProfileFields
form={form}
userProfileMetadata={userProfileMetadata}
@ -234,90 +216,39 @@ export const UserForm = ({
) : (
<>
{!realm.registrationEmailAsUsername && (
<FormGroup
<TextControl
name="username"
label={t("username")}
fieldId="kc-username"
isRequired
validated={errors.username ? "error" : "default"}
helperTextInvalid={t("required")}
>
<KeycloakTextInput
id="kc-username"
isReadOnly={
readOnly={
!!user?.id &&
!realm.editUsernameAllowed &&
realm.editUsernameAllowed !== undefined
}
{...register("username")}
rules={{
required: t("required"),
}}
/>
</FormGroup>
)}
<FormGroup
<TextControl
name="email"
label={t("email")}
fieldId="kc-email"
validated={errors.email ? "error" : "default"}
helperTextInvalid={t("emailInvalid")}
>
<KeycloakTextInput
type="email"
id="kc-email"
data-testid="email-input"
{...register("email", {
pattern: emailRegexPattern,
})}
rules={{
pattern: {
value: emailRegexPattern,
message: t("emailInvalid"),
},
}}
/>
</FormGroup>
<FormGroup
label={t("emailVerified")}
fieldId="kc-email-verified"
helperTextInvalid={t("required")}
labelIcon={
<HelpItem
helpText={t("emailVerifiedHelp")}
fieldLabelId="emailVerified"
/>
}
>
<Controller
<SwitchControl
name="emailVerified"
defaultValue={false}
control={control}
render={({ field }) => (
<Switch
data-testid="email-verified-switch"
id="kc-user-email-verified"
onChange={(value) => field.onChange(value)}
isChecked={field.value}
label={t("yes")}
label={t("emailVerified")}
labelIcon={t("emailVerifiedHelp")}
labelOn={t("yes")}
labelOff={t("no")}
/>
)}
/>
</FormGroup>
<FormGroup
label={t("firstName")}
fieldId="kc-firstName"
validated={errors.firstName ? "error" : "default"}
helperTextInvalid={t("required")}
>
<KeycloakTextInput
data-testid="firstName-input"
id="kc-firstName"
{...register("firstName")}
/>
</FormGroup>
<FormGroup
label={t("lastName")}
fieldId="kc-lastName"
validated={errors.lastName ? "error" : "default"}
>
<KeycloakTextInput
data-testid="lastName-input"
id="kc-lastName"
aria-label={t("lastName")}
{...register("lastName")}
/>
</FormGroup>
<TextControl name="firstName" label={t("firstName")} />
<TextControl name="lastName" label={t("lastName")} />
</>
)}
{isBruteForceProtected && (
@ -351,7 +282,9 @@ export const UserForm = ({
fieldId="kc-groups"
validated={errors.requiredActions ? "error" : "default"}
helperTextInvalid={t("required")}
labelIcon={<HelpItem helpText={t("groups")} fieldLabelId="groups" />}
labelIcon={
<HelpItem helpText={t("groups")} fieldLabelId="groups" />
}
>
<Controller
name="groups"
@ -411,6 +344,7 @@ export const UserForm = ({
{user?.id ? t("revert") : t("cancel")}
</Button>
</ActionGroup>
</FormProvider>
</FormAccess>
);
};

View file

@ -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<FederatedIdentityRepresentation>({
const form = useForm<FederatedIdentityRepresentation>({
mode: "onChange",
});
const {
handleSubmit,
formState: { isValid },
} = form;
const onSubmit = async (
federatedIdentity: FederatedIdentityRepresentation,
@ -87,55 +86,33 @@ export const UserIdpModal = ({
isOpen
>
<Form id="group-form" onSubmit={handleSubmit(onSubmit)}>
<FormProvider {...form}>
<FormGroup label={t("identityProvider")} fieldId="identityProvider">
<KeycloakTextInput
id="identityProvider"
data-testid="idpNameInput"
value={capitalize(federatedId)}
isReadOnly
readOnly
/>
</FormGroup>
<FormGroup
<TextControl
name="userId"
label={t("userID")}
fieldId="userID"
helperText={t("userIdHelperText")}
helperTextInvalid={t("required")}
validated={
errors.userId ? ValidatedOptions.error : ValidatedOptions.default
}
isRequired
>
<KeycloakTextInput
id="userID"
data-testid="userIdInput"
validated={
errors.userId ? ValidatedOptions.error : ValidatedOptions.default
}
autoFocus
{...register("userId", { required: true })}
rules={{
required: t("required"),
}}
/>
</FormGroup>
<FormGroup
<TextControl
name="userName"
label={t("username")}
fieldId="username"
helperText={t("usernameHelperText")}
helperTextInvalid={t("required")}
validated={
errors.userName ? ValidatedOptions.error : ValidatedOptions.default
}
isRequired
>
<KeycloakTextInput
id="username"
data-testid="usernameInput"
validated={
errors.userName
? ValidatedOptions.error
: ValidatedOptions.default
}
{...register("userName", { required: true })}
rules={{
required: t("required"),
}}
/>
</FormGroup>
</FormProvider>
</Form>
</Modal>
);

View file

@ -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 (
<FormGroup
fieldId="lifespan"
label={t("lifespan")}
isStack
labelIcon={
<HelpItem helpText={t("lifespanHelp")} fieldLabelId="lifespan" />
}
>
<Controller
<TimeSelectorControl
name="lifespan"
defaultValue={credResetFormDefaultValues.lifespan}
control={control}
render={({ field }) => (
<TimeSelector
value={field.value}
label={t("lifespan")}
labelIcon={t("lifespanHelp")}
units={["minute", "hour", "day"]}
onChange={field.onChange}
menuAppendTo="parent"
controller={{
defaultValue: credResetFormDefaultValues.lifespan,
}}
/>
)}
/>
</FormGroup>
);
};

View file

@ -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<T, string[] | undefined>,
> = {
control: Control<T>;
name: P;
label: string;
help: string;
@ -33,13 +21,11 @@ export const RequiredActionMultiSelect = <
T extends FieldValues,
P extends FieldPathByValue<T, string[] | undefined>,
>({
control,
name,
label,
help,
}: RequiredActionMultiSelectProps<T, P>) => {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const [requiredActions, setRequiredActions] = useState<
RequiredActionProviderRepresentation[]
>([]);
@ -56,54 +42,23 @@ export const RequiredActionMultiSelect = <
);
return (
<FormGroup
label={t(label)}
labelIcon={<HelpItem helpText={t(help)} fieldLabelId="resetAction" />}
fieldId="actions"
>
<Controller
<SelectControl
name={name}
defaultValue={[] as PathValue<T, P>}
control={control}
render={({ field }) => (
<Select
label={t(label)}
labelIcon={t(help)}
controller={{ defaultValue: [] }}
maxHeight={375}
toggleId={`${name}-actions`}
variant={SelectVariant.typeaheadMulti}
chipGroupProps={{
numChips: 3,
}}
placeholderText={t("requiredActionPlaceholder")}
menuAppendTo="parent"
onToggle={(open) => setOpen(open)}
isOpen={open}
selections={field.value as string[]}
onSelect={(_, selectedValue) => {
const value: string[] = field.value;
field.onChange(
value.find((item) => item === selectedValue)
? value.filter((item) => item !== selectedValue)
: [...value, selectedValue],
);
}}
onClear={(event) => {
event.stopPropagation();
field.onChange([]);
}}
typeAheadAriaLabel={t("resetAction")}
>
{requiredActions.map(({ alias, name }) => (
<SelectOption
key={alias}
value={alias}
data-testid={`${alias}-option`}
>
{name}
</SelectOption>
))}
</Select>
)}
options={requiredActions.map(({ alias, name }) => ({
key: alias!,
value: name || alias!,
}))}
/>
</FormGroup>
);
};

View file

@ -82,13 +82,12 @@ export const ResetCredentialDialog = ({
isHorizontal
data-testid="credential-reset-modal"
>
<FormProvider {...form}>
<RequiredActionMultiSelect
control={control}
name="actions"
label="resetAction"
help="resetActions"
/>
<FormProvider {...form}>
<LifespanField />
</FormProvider>
</Form>

View file

@ -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<CredentialsForm>({
defaultValues: credFormDefaultValues,
mode: "onChange",
});
const {
register,
control,
formState: { isValid, errors },
watch,
handleSubmit,
clearErrors,
setError,
} = useForm<CredentialsForm>({
defaultValues: credFormDefaultValues,
mode: "onChange",
});
} = form;
const [confirm, toggle] = useToggle(true);
const password = watch("password", "");
@ -201,32 +199,14 @@ export const ResetPasswordDialog = ({
})}
/>
</FormGroup>
<FormGroup
label={t("temporaryPassword")}
labelIcon={
<HelpItem
helpText={t("temporaryPasswordHelpText")}
fieldLabelId="temporaryPassword"
/>
}
fieldId="kc-temporaryPassword"
>
<Controller
<FormProvider {...form}>
<DefaultSwitchControl
name="temporaryPassword"
defaultValue={true}
control={control}
render={({ field }) => (
<Switch
className="kc-temporaryPassword"
onChange={field.onChange}
isChecked={field.value}
label={t("on")}
labelOff={t("off")}
aria-label={t("temporaryPassword")}
label={t("temporaryPassword")}
labelIcon={t("temporaryPasswordHelpText")}
defaultValue="true"
/>
)}
/>
</FormGroup>
</FormProvider>
</Form>
</ConfirmDialogModal>
</>

View file

@ -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}
>
<KeycloakTextInput
isRequired={required}