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 #setPasswordBtn = "confirm";
readonly #credentialResetModal = "credential-reset-modal"; readonly #credentialResetModal = "credential-reset-modal";
readonly #resetModalActionsToggleBtn = readonly #resetModalActionsToggleBtn =
"[data-testid=credential-reset-modal] #actions-actions"; "[data-testid=credential-reset-modal] #actions";
readonly #passwordField = "passwordField"; readonly #passwordField = "passwordField";
readonly #passwordConfirmationField = "passwordConfirmationField"; readonly #passwordConfirmationField = "passwordConfirmationField";
readonly #resetActions = [ readonly #resetActions = [
"VERIFY_EMAIL-option", "Verify Email",
"UPDATE_PROFILE-option", "Update Profile",
"CONFIGURE_TOTP-option", "Configure OTP",
"UPDATE_PASSWORD-option", "Update Password",
"TERMS_AND_CONDITIONS-option", "Terms and Conditions",
]; ];
readonly #confirmationButton = "confirm"; readonly #confirmationButton = "confirm";
readonly #editLabelBtn = "editUserLabelBtn"; readonly #editLabelBtn = "editUserLabelBtn";
@ -57,7 +57,9 @@ export default class CredentialsPage {
} }
clickResetModalAction(index: number) { 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; return this;
} }

View file

@ -9,8 +9,8 @@ export default class IdentityProviderLinksTab {
#availableProvidersSection = ".kc-available-idps"; #availableProvidersSection = ".kc-available-idps";
#linkAccountBtn = ".pf-c-button.pf-m-link"; #linkAccountBtn = ".pf-c-button.pf-m-link";
#linkAccountModalIdentityProviderInput = "idpNameInput"; #linkAccountModalIdentityProviderInput = "idpNameInput";
#linkAccountModalUserIdInput = "userIdInput"; #linkAccountModalUserIdInput = "userId";
#linkAccountModalUsernameInput = "usernameInput"; #linkAccountModalUsernameInput = "userName";
public clickLinkAccount(idpName: string) { public clickLinkAccount(idpName: string) {
cy.get(this.#availableProvidersSection + " tr") 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. 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. 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 addProvider_other=Add {{provider}} providers
resetAction=Reset action
cibaExpiresIn=Expires In cibaExpiresIn=Expires In
dynamicScopeFormatHelp=This is the regular expression that the system will use to extract the scope name and variable. dynamicScopeFormatHelp=This is the regular expression that the system will use to extract the scope name and variable.
updateTranslationError=Error updating translation. updateTranslationError=Error updating translation.

View file

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

View file

@ -7,12 +7,11 @@ import {
FormGroup, FormGroup,
Modal, Modal,
ModalVariant, ModalVariant,
ValidatedOptions,
} from "@patternfly/react-core"; } from "@patternfly/react-core";
import { capitalize } from "lodash-es"; import { capitalize } from "lodash-es";
import { useForm } from "react-hook-form"; import { FormProvider, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { TextControl } from "ui-shared";
import { adminClient } from "../admin-client"; import { adminClient } from "../admin-client";
import { useAlerts } from "../components/alert/Alerts"; import { useAlerts } from "../components/alert/Alerts";
import { KeycloakTextInput } from "../components/keycloak-text-input/KeycloakTextInput"; import { KeycloakTextInput } from "../components/keycloak-text-input/KeycloakTextInput";
@ -32,13 +31,13 @@ export const UserIdpModal = ({
}: UserIdpModalProps) => { }: UserIdpModalProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { addAlert, addError } = useAlerts(); const { addAlert, addError } = useAlerts();
const { const form = useForm<FederatedIdentityRepresentation>({
register,
handleSubmit,
formState: { isValid, errors },
} = useForm<FederatedIdentityRepresentation>({
mode: "onChange", mode: "onChange",
}); });
const {
handleSubmit,
formState: { isValid },
} = form;
const onSubmit = async ( const onSubmit = async (
federatedIdentity: FederatedIdentityRepresentation, federatedIdentity: FederatedIdentityRepresentation,
@ -87,55 +86,33 @@ export const UserIdpModal = ({
isOpen isOpen
> >
<Form id="group-form" onSubmit={handleSubmit(onSubmit)}> <Form id="group-form" onSubmit={handleSubmit(onSubmit)}>
<FormProvider {...form}>
<FormGroup label={t("identityProvider")} fieldId="identityProvider"> <FormGroup label={t("identityProvider")} fieldId="identityProvider">
<KeycloakTextInput <KeycloakTextInput
id="identityProvider" id="identityProvider"
data-testid="idpNameInput" data-testid="idpNameInput"
value={capitalize(federatedId)} value={capitalize(federatedId)}
isReadOnly readOnly
/> />
</FormGroup> </FormGroup>
<FormGroup <TextControl
name="userId"
label={t("userID")} label={t("userID")}
fieldId="userID"
helperText={t("userIdHelperText")} 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 autoFocus
{...register("userId", { required: true })} rules={{
required: t("required"),
}}
/> />
</FormGroup> <TextControl
<FormGroup name="userName"
label={t("username")} label={t("username")}
fieldId="username"
helperText={t("usernameHelperText")} helperText={t("usernameHelperText")}
helperTextInvalid={t("required")} rules={{
validated={ required: t("required"),
errors.userName ? ValidatedOptions.error : ValidatedOptions.default }}
}
isRequired
>
<KeycloakTextInput
id="username"
data-testid="usernameInput"
validated={
errors.userName
? ValidatedOptions.error
: ValidatedOptions.default
}
{...register("userName", { required: true })}
/> />
</FormGroup> </FormProvider>
</Form> </Form>
</Modal> </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 { useTranslation } from "react-i18next";
import { TimeSelectorControl } from "../../components/time-selector/TimeSelectorControl";
import { HelpItem } from "ui-shared";
import { TimeSelector } from "../../components/time-selector/TimeSelector";
import { credResetFormDefaultValues } from "./ResetCredentialDialog"; import { credResetFormDefaultValues } from "./ResetCredentialDialog";
export const LifespanField = () => { export const LifespanField = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { control } = useFormContext();
return ( return (
<FormGroup <TimeSelectorControl
fieldId="lifespan"
label={t("lifespan")}
isStack
labelIcon={
<HelpItem helpText={t("lifespanHelp")} fieldLabelId="lifespan" />
}
>
<Controller
name="lifespan" name="lifespan"
defaultValue={credResetFormDefaultValues.lifespan} label={t("lifespan")}
control={control} labelIcon={t("lifespanHelp")}
render={({ field }) => (
<TimeSelector
value={field.value}
units={["minute", "hour", "day"]} units={["minute", "hour", "day"]}
onChange={field.onChange}
menuAppendTo="parent" 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 type RequiredActionProviderRepresentation from "@keycloak/keycloak-admin-client/lib/defs/requiredActionProviderRepresentation";
import { import { SelectVariant } from "@patternfly/react-core";
FormGroup,
Select,
SelectOption,
SelectVariant,
} from "@patternfly/react-core";
import { useState } from "react"; import { useState } from "react";
import { import { FieldPathByValue, FieldValues } from "react-hook-form";
Control,
Controller,
FieldPathByValue,
FieldValues,
PathValue,
} from "react-hook-form";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { HelpItem } from "ui-shared"; import { SelectControl } from "ui-shared";
import { adminClient } from "../../admin-client"; import { adminClient } from "../../admin-client";
import { useFetch } from "../../utils/useFetch"; import { useFetch } from "../../utils/useFetch";
@ -23,7 +12,6 @@ export type RequiredActionMultiSelectProps<
T extends FieldValues, T extends FieldValues,
P extends FieldPathByValue<T, string[] | undefined>, P extends FieldPathByValue<T, string[] | undefined>,
> = { > = {
control: Control<T>;
name: P; name: P;
label: string; label: string;
help: string; help: string;
@ -33,13 +21,11 @@ export const RequiredActionMultiSelect = <
T extends FieldValues, T extends FieldValues,
P extends FieldPathByValue<T, string[] | undefined>, P extends FieldPathByValue<T, string[] | undefined>,
>({ >({
control,
name, name,
label, label,
help, help,
}: RequiredActionMultiSelectProps<T, P>) => { }: RequiredActionMultiSelectProps<T, P>) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [open, setOpen] = useState(false);
const [requiredActions, setRequiredActions] = useState< const [requiredActions, setRequiredActions] = useState<
RequiredActionProviderRepresentation[] RequiredActionProviderRepresentation[]
>([]); >([]);
@ -56,54 +42,23 @@ export const RequiredActionMultiSelect = <
); );
return ( return (
<FormGroup <SelectControl
label={t(label)}
labelIcon={<HelpItem helpText={t(help)} fieldLabelId="resetAction" />}
fieldId="actions"
>
<Controller
name={name} name={name}
defaultValue={[] as PathValue<T, P>} label={t(label)}
control={control} labelIcon={t(help)}
render={({ field }) => ( controller={{ defaultValue: [] }}
<Select
maxHeight={375} maxHeight={375}
toggleId={`${name}-actions`}
variant={SelectVariant.typeaheadMulti} variant={SelectVariant.typeaheadMulti}
chipGroupProps={{ chipGroupProps={{
numChips: 3, numChips: 3,
}} }}
placeholderText={t("requiredActionPlaceholder")} placeholderText={t("requiredActionPlaceholder")}
menuAppendTo="parent" 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")} typeAheadAriaLabel={t("resetAction")}
> options={requiredActions.map(({ alias, name }) => ({
{requiredActions.map(({ alias, name }) => ( key: alias!,
<SelectOption value: name || alias!,
key={alias} }))}
value={alias}
data-testid={`${alias}-option`}
>
{name}
</SelectOption>
))}
</Select>
)}
/> />
</FormGroup>
); );
}; };

View file

@ -82,13 +82,12 @@ export const ResetCredentialDialog = ({
isHorizontal isHorizontal
data-testid="credential-reset-modal" data-testid="credential-reset-modal"
> >
<FormProvider {...form}>
<RequiredActionMultiSelect <RequiredActionMultiSelect
control={control}
name="actions" name="actions"
label="resetAction" label="resetAction"
help="resetActions" help="resetActions"
/> />
<FormProvider {...form}>
<LifespanField /> <LifespanField />
</FormProvider> </FormProvider>
</Form> </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 { RequiredActionAlias } from "@keycloak/keycloak-admin-client/lib/defs/requiredActionProviderRepresentation";
import type UserRepresentation from "@keycloak/keycloak-admin-client/lib/defs/userRepresentation";
import { import {
AlertVariant, AlertVariant,
ButtonVariant, ButtonVariant,
Form, Form,
FormGroup, FormGroup,
Switch,
ValidatedOptions, ValidatedOptions,
} from "@patternfly/react-core"; } from "@patternfly/react-core";
import { Controller, useForm } from "react-hook-form"; import { FormProvider, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { HelpItem } from "ui-shared";
import { adminClient } from "../../admin-client"; import { adminClient } from "../../admin-client";
import { DefaultSwitchControl } from "../../components/SwitchControl";
import { useAlerts } from "../../components/alert/Alerts"; import { useAlerts } from "../../components/alert/Alerts";
import { import {
ConfirmDialogModal, ConfirmDialogModal,
@ -49,18 +47,18 @@ export const ResetPasswordDialog = ({
onClose, onClose,
}: ResetPasswordDialogProps) => { }: ResetPasswordDialogProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const form = useForm<CredentialsForm>({
defaultValues: credFormDefaultValues,
mode: "onChange",
});
const { const {
register, register,
control,
formState: { isValid, errors }, formState: { isValid, errors },
watch, watch,
handleSubmit, handleSubmit,
clearErrors, clearErrors,
setError, setError,
} = useForm<CredentialsForm>({ } = form;
defaultValues: credFormDefaultValues,
mode: "onChange",
});
const [confirm, toggle] = useToggle(true); const [confirm, toggle] = useToggle(true);
const password = watch("password", ""); const password = watch("password", "");
@ -201,32 +199,14 @@ export const ResetPasswordDialog = ({
})} })}
/> />
</FormGroup> </FormGroup>
<FormGroup <FormProvider {...form}>
label={t("temporaryPassword")} <DefaultSwitchControl
labelIcon={
<HelpItem
helpText={t("temporaryPasswordHelpText")}
fieldLabelId="temporaryPassword"
/>
}
fieldId="kc-temporaryPassword"
>
<Controller
name="temporaryPassword" name="temporaryPassword"
defaultValue={true} label={t("temporaryPassword")}
control={control} labelIcon={t("temporaryPasswordHelpText")}
render={({ field }) => ( defaultValue="true"
<Switch
className="kc-temporaryPassword"
onChange={field.onChange}
isChecked={field.value}
label={t("on")}
labelOff={t("off")}
aria-label={t("temporaryPassword")}
/> />
)} </FormProvider>
/>
</FormGroup>
</Form> </Form>
</ConfirmDialogModal> </ConfirmDialogModal>
</> </>

View file

@ -18,6 +18,7 @@ export type TextControlProps<
label: string; label: string;
labelIcon?: string; labelIcon?: string;
isDisabled?: boolean; isDisabled?: boolean;
helperText?: string;
}; };
export const TextControl = < export const TextControl = <
@ -42,6 +43,7 @@ export const TextControl = <
labelIcon={labelIcon} labelIcon={labelIcon}
isRequired={required} isRequired={required}
error={fieldState.error} error={fieldState.error}
helperText={props.helperText}
> >
<KeycloakTextInput <KeycloakTextInput
isRequired={required} isRequired={required}