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:
parent
39299eeb38
commit
b0b967d8d4
10 changed files with 284 additions and 451 deletions
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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,284 +137,214 @@ export const UserForm = ({
|
||||||
fineGrainedAccess={user?.access?.manage}
|
fineGrainedAccess={user?.access?.manage}
|
||||||
className="pf-u-mt-lg"
|
className="pf-u-mt-lg"
|
||||||
>
|
>
|
||||||
{open && (
|
<FormProvider {...form}>
|
||||||
<GroupPickerDialog
|
{open && (
|
||||||
type="selectMany"
|
<GroupPickerDialog
|
||||||
text={{
|
type="selectMany"
|
||||||
title: "selectGroups",
|
text={{
|
||||||
ok: "join",
|
title: "selectGroups",
|
||||||
}}
|
ok: "join",
|
||||||
canBrowse={isManager}
|
}}
|
||||||
onConfirm={(groups) => {
|
canBrowse={isManager}
|
||||||
user?.id ? addGroups(groups || []) : addChips(groups || []);
|
onConfirm={(groups) => {
|
||||||
setOpen(false);
|
user?.id ? addGroups(groups || []) : addChips(groups || []);
|
||||||
}}
|
setOpen(false);
|
||||||
onClose={() => setOpen(false)}
|
}}
|
||||||
filterGroups={selectedGroups}
|
onClose={() => setOpen(false)}
|
||||||
|
filterGroups={selectedGroups}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{user?.id && (
|
||||||
|
<>
|
||||||
|
<FormGroup label={t("id")} fieldId="kc-id" isRequired>
|
||||||
|
<KeycloakTextInput
|
||||||
|
id={user.id}
|
||||||
|
aria-label={t("userID")}
|
||||||
|
value={user.id}
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup
|
||||||
|
label={t("createdAt")}
|
||||||
|
fieldId="kc-created-at"
|
||||||
|
isRequired
|
||||||
|
>
|
||||||
|
<KeycloakTextInput
|
||||||
|
value={formatDate(new Date(user.createdTimestamp!))}
|
||||||
|
id="kc-created-at"
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<RequiredActionMultiSelect
|
||||||
|
name="requiredActions"
|
||||||
|
label="requiredUserActions"
|
||||||
|
help="requiredUserActionsHelp"
|
||||||
/>
|
/>
|
||||||
)}
|
{(user?.federationLink || user?.origin) && canViewFederationLink && (
|
||||||
{user?.id && (
|
|
||||||
<>
|
|
||||||
<FormGroup label={t("id")} fieldId="kc-id" isRequired>
|
|
||||||
<KeycloakTextInput
|
|
||||||
id={user.id}
|
|
||||||
aria-label={t("userID")}
|
|
||||||
value={user.id}
|
|
||||||
type="text"
|
|
||||||
isReadOnly
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
<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
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<RequiredActionMultiSelect
|
|
||||||
control={control}
|
|
||||||
name="requiredActions"
|
|
||||||
label="requiredUserActions"
|
|
||||||
help="requiredUserActionsHelp"
|
|
||||||
/>
|
|
||||||
{(user?.federationLink || user?.origin) && canViewFederationLink && (
|
|
||||||
<FormGroup
|
|
||||||
label={t("federationLink")}
|
|
||||||
labelIcon={
|
|
||||||
<HelpItem
|
|
||||||
helpText={t("federationLinkHelp")}
|
|
||||||
fieldLabelId="federationLink"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<FederatedUserLink user={user} />
|
|
||||||
</FormGroup>
|
|
||||||
)}
|
|
||||||
{userProfileMetadata ? (
|
|
||||||
<>
|
|
||||||
<FormGroup
|
<FormGroup
|
||||||
label={t("emailVerified")}
|
label={t("federationLink")}
|
||||||
fieldId="kc-email-verified"
|
|
||||||
helperTextInvalid={t("required")}
|
|
||||||
labelIcon={
|
labelIcon={
|
||||||
<HelpItem
|
<HelpItem
|
||||||
helpText={t("emailVerifiedHelp")}
|
helpText={t("federationLinkHelp")}
|
||||||
fieldLabelId="emailVerified"
|
fieldLabelId="federationLink"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Controller
|
<FederatedUserLink user={user} />
|
||||||
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")}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
<UserProfileFields
|
)}
|
||||||
form={form}
|
{userProfileMetadata ? (
|
||||||
userProfileMetadata={userProfileMetadata}
|
<>
|
||||||
hideReadOnly={!user}
|
<DefaultSwitchControl
|
||||||
supportedLocales={realm.supportedLocales || []}
|
name="emailVerified"
|
||||||
t={
|
label={t("emailVerified")}
|
||||||
((key: unknown, params) =>
|
labelIcon={t("emailVerifiedHelp")}
|
||||||
t(key as string, params as any)) as TFunction
|
/>
|
||||||
}
|
<UserProfileFields
|
||||||
/>
|
form={form}
|
||||||
</>
|
userProfileMetadata={userProfileMetadata}
|
||||||
) : (
|
hideReadOnly={!user}
|
||||||
<>
|
supportedLocales={realm.supportedLocales || []}
|
||||||
{!realm.registrationEmailAsUsername && (
|
t={
|
||||||
<FormGroup
|
((key: unknown, params) =>
|
||||||
label={t("username")}
|
t(key as string, params as any)) as TFunction
|
||||||
fieldId="kc-username"
|
}
|
||||||
isRequired
|
/>
|
||||||
validated={errors.username ? "error" : "default"}
|
</>
|
||||||
helperTextInvalid={t("required")}
|
) : (
|
||||||
>
|
<>
|
||||||
<KeycloakTextInput
|
{!realm.registrationEmailAsUsername && (
|
||||||
id="kc-username"
|
<TextControl
|
||||||
isReadOnly={
|
name="username"
|
||||||
|
label={t("username")}
|
||||||
|
readOnly={
|
||||||
!!user?.id &&
|
!!user?.id &&
|
||||||
!realm.editUsernameAllowed &&
|
!realm.editUsernameAllowed &&
|
||||||
realm.editUsernameAllowed !== undefined
|
realm.editUsernameAllowed !== undefined
|
||||||
}
|
}
|
||||||
{...register("username")}
|
rules={{
|
||||||
|
required: t("required"),
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
)}
|
||||||
)}
|
<TextControl
|
||||||
<FormGroup
|
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
|
||||||
|
name="emailVerified"
|
||||||
|
label={t("emailVerified")}
|
||||||
|
labelIcon={t("emailVerifiedHelp")}
|
||||||
|
labelOn={t("yes")}
|
||||||
|
labelOff={t("no")}
|
||||||
|
/>
|
||||||
|
<TextControl name="firstName" label={t("firstName")} />
|
||||||
|
<TextControl name="lastName" label={t("lastName")} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{isBruteForceProtected && (
|
||||||
<FormGroup
|
<FormGroup
|
||||||
label={t("emailVerified")}
|
label={t("temporaryLocked")}
|
||||||
fieldId="kc-email-verified"
|
fieldId="temporaryLocked"
|
||||||
helperTextInvalid={t("required")}
|
|
||||||
labelIcon={
|
labelIcon={
|
||||||
<HelpItem
|
<HelpItem
|
||||||
helpText={t("emailVerifiedHelp")}
|
helpText={t("temporaryLockedHelp")}
|
||||||
fieldLabelId="emailVerified"
|
fieldLabelId="temporaryLocked"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
>
|
||||||
|
<Switch
|
||||||
|
data-testid="user-locked-switch"
|
||||||
|
id="temporaryLocked"
|
||||||
|
onChange={(value) => {
|
||||||
|
unLockUser();
|
||||||
|
setLocked(value);
|
||||||
|
}}
|
||||||
|
isChecked={locked}
|
||||||
|
isDisabled={!locked}
|
||||||
|
label={t("on")}
|
||||||
|
labelOff={t("off")}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
)}
|
||||||
|
{!user?.id && (
|
||||||
|
<FormGroup
|
||||||
|
label={t("groups")}
|
||||||
|
fieldId="kc-groups"
|
||||||
|
validated={errors.requiredActions ? "error" : "default"}
|
||||||
|
helperTextInvalid={t("required")}
|
||||||
|
labelIcon={
|
||||||
|
<HelpItem helpText={t("groups")} fieldLabelId="groups" />
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Controller
|
<Controller
|
||||||
name="emailVerified"
|
name="groups"
|
||||||
defaultValue={false}
|
defaultValue={[]}
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => (
|
render={() => (
|
||||||
<Switch
|
<InputGroup>
|
||||||
data-testid="email-verified-switch"
|
<ChipGroup categoryName={" "}>
|
||||||
id="kc-user-email-verified"
|
{selectedGroups.map((currentChip) => (
|
||||||
onChange={(value) => field.onChange(value)}
|
<Chip
|
||||||
isChecked={field.value}
|
key={currentChip.id}
|
||||||
label={t("yes")}
|
onClick={() => deleteItem(currentChip.name!)}
|
||||||
labelOff={t("no")}
|
>
|
||||||
/>
|
{currentChip.path}
|
||||||
|
</Chip>
|
||||||
|
))}
|
||||||
|
</ChipGroup>
|
||||||
|
<Button
|
||||||
|
id="kc-join-groups-button"
|
||||||
|
onClick={toggleModal}
|
||||||
|
variant="secondary"
|
||||||
|
data-testid="join-groups-button"
|
||||||
|
>
|
||||||
|
{t("joinGroups")}
|
||||||
|
</Button>
|
||||||
|
</InputGroup>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</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 && (
|
|
||||||
<FormGroup
|
|
||||||
label={t("temporaryLocked")}
|
|
||||||
fieldId="temporaryLocked"
|
|
||||||
labelIcon={
|
|
||||||
<HelpItem
|
|
||||||
helpText={t("temporaryLockedHelp")}
|
|
||||||
fieldLabelId="temporaryLocked"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Switch
|
|
||||||
data-testid="user-locked-switch"
|
|
||||||
id="temporaryLocked"
|
|
||||||
onChange={(value) => {
|
|
||||||
unLockUser();
|
|
||||||
setLocked(value);
|
|
||||||
}}
|
|
||||||
isChecked={locked}
|
|
||||||
isDisabled={!locked}
|
|
||||||
label={t("on")}
|
|
||||||
labelOff={t("off")}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
)}
|
|
||||||
{!user?.id && (
|
|
||||||
<FormGroup
|
|
||||||
label={t("groups")}
|
|
||||||
fieldId="kc-groups"
|
|
||||||
validated={errors.requiredActions ? "error" : "default"}
|
|
||||||
helperTextInvalid={t("required")}
|
|
||||||
labelIcon={<HelpItem helpText={t("groups")} fieldLabelId="groups" />}
|
|
||||||
>
|
|
||||||
<Controller
|
|
||||||
name="groups"
|
|
||||||
defaultValue={[]}
|
|
||||||
control={control}
|
|
||||||
render={() => (
|
|
||||||
<InputGroup>
|
|
||||||
<ChipGroup categoryName={" "}>
|
|
||||||
{selectedGroups.map((currentChip) => (
|
|
||||||
<Chip
|
|
||||||
key={currentChip.id}
|
|
||||||
onClick={() => deleteItem(currentChip.name!)}
|
|
||||||
>
|
|
||||||
{currentChip.path}
|
|
||||||
</Chip>
|
|
||||||
))}
|
|
||||||
</ChipGroup>
|
|
||||||
<Button
|
|
||||||
id="kc-join-groups-button"
|
|
||||||
onClick={toggleModal}
|
|
||||||
variant="secondary"
|
|
||||||
data-testid="join-groups-button"
|
|
||||||
>
|
|
||||||
{t("joinGroups")}
|
|
||||||
</Button>
|
|
||||||
</InputGroup>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<ActionGroup>
|
<ActionGroup>
|
||||||
<Button
|
<Button
|
||||||
data-testid={!user?.id ? "create-user" : "save-user"}
|
data-testid={!user?.id ? "create-user" : "save-user"}
|
||||||
isDisabled={
|
isDisabled={
|
||||||
!user?.id &&
|
!user?.id &&
|
||||||
!watchUsernameInput &&
|
!watchUsernameInput &&
|
||||||
realm.registrationEmailAsUsername === false
|
realm.registrationEmailAsUsername === false
|
||||||
}
|
}
|
||||||
variant="primary"
|
variant="primary"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
{user?.id ? t("save") : t("create")}
|
{user?.id ? t("save") : t("create")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
data-testid="cancel-create-user"
|
data-testid="cancel-create-user"
|
||||||
variant="link"
|
variant="link"
|
||||||
onClick={user?.id ? () => reset(toUserFormFields(user)) : undefined}
|
onClick={user?.id ? () => reset(toUserFormFields(user)) : undefined}
|
||||||
component={
|
component={
|
||||||
!user?.id
|
!user?.id
|
||||||
? (props) => (
|
? (props) => (
|
||||||
<Link {...props} to={toUsers({ realm: realm.realm! })} />
|
<Link {...props} to={toUsers({ realm: realm.realm! })} />
|
||||||
)
|
)
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{user?.id ? t("revert") : t("cancel")}
|
{user?.id ? t("revert") : t("cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
</ActionGroup>
|
</ActionGroup>
|
||||||
|
</FormProvider>
|
||||||
</FormAccess>
|
</FormAccess>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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)}>
|
||||||
<FormGroup label={t("identityProvider")} fieldId="identityProvider">
|
<FormProvider {...form}>
|
||||||
<KeycloakTextInput
|
<FormGroup label={t("identityProvider")} fieldId="identityProvider">
|
||||||
id="identityProvider"
|
<KeycloakTextInput
|
||||||
data-testid="idpNameInput"
|
id="identityProvider"
|
||||||
value={capitalize(federatedId)}
|
data-testid="idpNameInput"
|
||||||
isReadOnly
|
value={capitalize(federatedId)}
|
||||||
/>
|
readOnly
|
||||||
</FormGroup>
|
/>
|
||||||
<FormGroup
|
</FormGroup>
|
||||||
label={t("userID")}
|
<TextControl
|
||||||
fieldId="userID"
|
name="userId"
|
||||||
helperText={t("userIdHelperText")}
|
label={t("userID")}
|
||||||
helperTextInvalid={t("required")}
|
helperText={t("userIdHelperText")}
|
||||||
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")}
|
rules={{
|
||||||
helperTextInvalid={t("required")}
|
required: 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 })}
|
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormProvider>
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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"
|
name="lifespan"
|
||||||
label={t("lifespan")}
|
label={t("lifespan")}
|
||||||
isStack
|
labelIcon={t("lifespanHelp")}
|
||||||
labelIcon={
|
units={["minute", "hour", "day"]}
|
||||||
<HelpItem helpText={t("lifespanHelp")} fieldLabelId="lifespan" />
|
menuAppendTo="parent"
|
||||||
}
|
controller={{
|
||||||
>
|
defaultValue: credResetFormDefaultValues.lifespan,
|
||||||
<Controller
|
}}
|
||||||
name="lifespan"
|
/>
|
||||||
defaultValue={credResetFormDefaultValues.lifespan}
|
|
||||||
control={control}
|
|
||||||
render={({ field }) => (
|
|
||||||
<TimeSelector
|
|
||||||
value={field.value}
|
|
||||||
units={["minute", "hour", "day"]}
|
|
||||||
onChange={field.onChange}
|
|
||||||
menuAppendTo="parent"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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
|
||||||
|
name={name}
|
||||||
label={t(label)}
|
label={t(label)}
|
||||||
labelIcon={<HelpItem helpText={t(help)} fieldLabelId="resetAction" />}
|
labelIcon={t(help)}
|
||||||
fieldId="actions"
|
controller={{ defaultValue: [] }}
|
||||||
>
|
maxHeight={375}
|
||||||
<Controller
|
variant={SelectVariant.typeaheadMulti}
|
||||||
name={name}
|
chipGroupProps={{
|
||||||
defaultValue={[] as PathValue<T, P>}
|
numChips: 3,
|
||||||
control={control}
|
}}
|
||||||
render={({ field }) => (
|
placeholderText={t("requiredActionPlaceholder")}
|
||||||
<Select
|
menuAppendTo="parent"
|
||||||
maxHeight={375}
|
typeAheadAriaLabel={t("resetAction")}
|
||||||
toggleId={`${name}-actions`}
|
options={requiredActions.map(({ alias, name }) => ({
|
||||||
variant={SelectVariant.typeaheadMulti}
|
key: alias!,
|
||||||
chipGroupProps={{
|
value: name || alias!,
|
||||||
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>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -82,13 +82,12 @@ export const ResetCredentialDialog = ({
|
||||||
isHorizontal
|
isHorizontal
|
||||||
data-testid="credential-reset-modal"
|
data-testid="credential-reset-modal"
|
||||||
>
|
>
|
||||||
<RequiredActionMultiSelect
|
|
||||||
control={control}
|
|
||||||
name="actions"
|
|
||||||
label="resetAction"
|
|
||||||
help="resetActions"
|
|
||||||
/>
|
|
||||||
<FormProvider {...form}>
|
<FormProvider {...form}>
|
||||||
|
<RequiredActionMultiSelect
|
||||||
|
name="actions"
|
||||||
|
label="resetAction"
|
||||||
|
help="resetActions"
|
||||||
|
/>
|
||||||
<LifespanField />
|
<LifespanField />
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
|
@ -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")}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormProvider>
|
||||||
</Form>
|
</Form>
|
||||||
</ConfirmDialogModal>
|
</ConfirmDialogModal>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -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}
|
||||||
|
|
Loading…
Reference in a new issue