Migrate authenticate settings and policy detail to react-hook-form v7 (#3984)

This commit is contained in:
Erik Jan de Wit 2023-01-16 12:34:18 +01:00 committed by GitHub
parent 498034ee2e
commit e0246c70d4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 250 additions and 99 deletions

View file

@ -3,7 +3,7 @@ import { ActionGroup, ActionGroupProps, Button } from "@patternfly/react-core";
type SaveResetProps = ActionGroupProps & {
name: string;
save: () => void;
save?: () => void;
reset: () => void;
isActive?: boolean;
};
@ -18,7 +18,12 @@ export const SaveReset = ({
const { t } = useTranslation("common");
return (
<ActionGroup {...rest}>
<Button isDisabled={!isActive} data-testid={name + "Save"} onClick={save}>
<Button
isDisabled={!isActive}
data-testid={name + "Save"}
onClick={save}
type={save ? "button" : "submit"}
>
{t("save")}
</Button>
<Button

View file

@ -10,7 +10,7 @@ import {
Switch,
} from "@patternfly/react-core";
import { useState } from "react";
import { Controller, FormProvider, useForm } from "react-hook-form-v7";
import { Controller, FormProvider, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation";
@ -213,7 +213,7 @@ export const AuthorizationEvaluate = ({ client }: Props) => {
control={control}
defaultValue={[]}
rules={{ validate: (value) => (value || "").length > 0 }}
render={({ field }) => (
render={(field) => (
<Select
placeholderText={t("selectARole")}
variant={SelectVariant.typeaheadMulti}
@ -314,7 +314,8 @@ export const AuthorizationEvaluate = ({ client }: Props) => {
<KeycloakTextInput
id="alias"
data-testid="alias"
{...register("alias", { required: true })}
name="alias"
ref={register({ required: true })}
/>
</FormGroup>
<FormGroup
@ -331,7 +332,7 @@ export const AuthorizationEvaluate = ({ client }: Props) => {
name="authScopes"
defaultValue={[]}
control={control}
render={({ field }) => (
render={(field) => (
<Select
toggleId="authScopes"
onToggle={setScopesDropdownOpen}

View file

@ -1,5 +1,5 @@
import { FormGroup, Radio } from "@patternfly/react-core";
import { Controller, useFormContext } from "react-hook-form";
import { Controller, useFormContext } from "react-hook-form-v7";
import { useTranslation } from "react-i18next";
import { HelpItem } from "../../components/help-enabler/HelpItem";
@ -35,7 +35,7 @@ export const DecisionStrategySelect = ({
data-testid="decisionStrategy"
defaultValue={DECISION_STRATEGY[0]}
control={control}
render={(field) => (
render={({ field }) => (
<>
{(isLimited
? DECISION_STRATEGY.slice(0, 2)

View file

@ -1,6 +1,6 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { Controller, FormProvider, useForm } from "react-hook-form";
import { Controller, FormProvider, useForm } from "react-hook-form-v7";
import {
AlertVariant,
Button,
@ -20,7 +20,7 @@ import { SaveReset } from "../advanced/SaveReset";
import { ImportDialog } from "./ImportDialog";
import useToggle from "../../utils/useToggle";
import { useAlerts } from "../../components/alert/Alerts";
import { DecisionStrategySelect } from "./DecisionStragegySelect";
import { DecisionStrategySelect } from "./DecisionStrategySelect";
const POLICY_ENFORCEMENT_MODES = [
"ENFORCING",
@ -28,12 +28,17 @@ const POLICY_ENFORCEMENT_MODES = [
"DISABLED",
] as const;
export type FormFields = Omit<
ResourceServerRepresentation,
"scopes" | "resources"
>;
export const AuthorizationSettings = ({ clientId }: { clientId: string }) => {
const { t } = useTranslation("clients");
const [resource, setResource] = useState<ResourceServerRepresentation>();
const [importDialog, toggleImportDialog] = useToggle();
const form = useForm<ResourceServerRepresentation>({});
const form = useForm<FormFields>({});
const { control, reset, handleSubmit } = form;
const { adminClient } = useAdminClient();
@ -58,7 +63,7 @@ export const AuthorizationSettings = ({ clientId }: { clientId: string }) => {
}
};
const save = async (resource: ResourceServerRepresentation) => {
const onSubmit = async (resource: ResourceServerRepresentation) => {
try {
await adminClient.clients.updateResourceServer(
{ id: clientId },
@ -82,7 +87,11 @@ export const AuthorizationSettings = ({ clientId }: { clientId: string }) => {
closeDialog={toggleImportDialog}
/>
)}
<FormAccess role="view-clients" isHorizontal>
<FormAccess
role="view-clients"
isHorizontal
onSubmit={handleSubmit(onSubmit)}
>
<FormGroup
label={t("import")}
fieldId="import"
@ -114,16 +123,16 @@ export const AuthorizationSettings = ({ clientId }: { clientId: string }) => {
data-testid="policyEnforcementMode"
defaultValue={POLICY_ENFORCEMENT_MODES[0]}
control={control}
render={({ onChange, value }) => (
render={({ field }) => (
<>
{POLICY_ENFORCEMENT_MODES.map((mode) => (
<Radio
id={mode}
key={mode}
data-testid={mode}
isChecked={value === mode}
isChecked={field.value === mode}
name="policyEnforcementMode"
onChange={() => onChange(mode)}
onChange={() => field.onChange(mode)}
label={t(`policyEnforcementModes.${mode}`)}
className="pf-u-mb-md"
/>
@ -151,13 +160,13 @@ export const AuthorizationSettings = ({ clientId }: { clientId: string }) => {
data-testid="allowRemoteResourceManagement"
defaultValue={false}
control={control}
render={({ onChange, value }) => (
render={({ field }) => (
<Switch
id="allowRemoteResourceManagement"
label={t("common:on")}
labelOff={t("common:off")}
isChecked={value}
onChange={onChange}
isChecked={field.value}
onChange={field.onChange}
aria-label={t("allowRemoteResourceManagement")}
/>
)}
@ -165,7 +174,6 @@ export const AuthorizationSettings = ({ clientId }: { clientId: string }) => {
</FormGroup>
<SaveReset
name="authenticationSettings"
save={() => handleSubmit(save)()}
reset={() => reset(resource)}
isActive
/>

View file

@ -4,7 +4,7 @@ import { useTranslation } from "react-i18next";
import { HelpItem } from "../../../components/help-enabler/HelpItem";
import { useParams } from "../../../utils/useParams";
import type { PolicyDetailsParams } from "../../routes/PolicyDetails";
import { DecisionStrategySelect } from "../DecisionStragegySelect";
import { DecisionStrategySelect } from "../DecisionStrategySelect";
import { ResourcesPolicySelect } from "../ResourcesPolicySelect";
export const Aggregate = () => {

View file

@ -1,5 +1,5 @@
import { useState } from "react";
import { Controller, useFormContext } from "react-hook-form";
import { Controller, useFormContext } from "react-hook-form-v7";
import {
SelectOption,
FormGroup,
@ -84,23 +84,25 @@ export const Client = () => {
rules={{
validate: (value) => value.length > 0,
}}
render={({ onChange, value }) => (
render={({ field }) => (
<Select
toggleId="clients"
variant={SelectVariant.typeaheadMulti}
onToggle={(open) => setOpen(open)}
isOpen={open}
selections={value}
selections={field.value}
onFilter={(_, value) => {
setSearch(value);
return convert(clients);
}}
onSelect={(_, v) => {
const option = v.toString();
if (value.includes(option)) {
onChange(value.filter((item: string) => item !== option));
if (field.value.includes(option)) {
field.onChange(
field.value.filter((item: string) => item !== option)
);
} else {
onChange([...value, option]);
field.onChange([...field.value, option]);
}
setOpen(false);
}}

View file

@ -1,6 +1,6 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useFormContext, Controller } from "react-hook-form";
import { useFormContext, Controller } from "react-hook-form-v7";
import { FormGroup, Button, Checkbox } from "@patternfly/react-core";
import { MinusCircleIcon } from "@patternfly/react-icons";
import {
@ -76,13 +76,15 @@ export const ClientScope = () => {
validate: (value: RequiredIdValue[]) =>
value.filter((c) => c.id).length > 0,
}}
render={({ onChange, value }) => (
render={({ field }) => (
<>
{open && (
<AddScopeDialog
clientScopes={scopes.filter(
(scope) =>
!value.map((c: RequiredIdValue) => c.id).includes(scope.id!)
!field.value
.map((c: RequiredIdValue) => c.id)
.includes(scope.id!)
)}
isClientScopesConditionType
open={open}
@ -92,8 +94,8 @@ export const ClientScope = () => {
...selectedScopes,
...scopes.map((s) => s.scope),
]);
onChange([
...value,
field.onChange([
...field.value,
...scopes
.map((scope) => scope.scope)
.map((item) => ({ id: item.id!, required: false })),
@ -128,16 +130,16 @@ export const ClientScope = () => {
<Td>{scope.name}</Td>
<Td>
<Controller
name={`clientScopes[${index}].required`}
name={`clientScopes.${index}.required`}
defaultValue={false}
control={control}
render={({ onChange, value }) => (
render={({ field }) => (
<Checkbox
id="required"
data-testid="standard"
name="required"
isChecked={value}
onChange={onChange}
isChecked={field.value}
onChange={field.onChange}
/>
)}
/>

View file

@ -1,6 +1,6 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useFormContext, Controller } from "react-hook-form";
import { useFormContext, Controller } from "react-hook-form-v7";
import { MinusCircleIcon } from "@patternfly/react-icons";
import { FormGroup, Button, Checkbox } from "@patternfly/react-core";
import {
@ -18,6 +18,11 @@ import { useAdminClient, useFetch } from "../../../context/auth/AdminClient";
import { GroupPickerDialog } from "../../../components/group/GroupPickerDialog";
import { KeycloakTextInput } from "../../../components/keycloak-text-input/KeycloakTextInput";
type GroupForm = {
groups?: GroupValue[];
groupsClaim: string;
};
export type GroupValue = {
id: string;
extendChildren: boolean;
@ -31,9 +36,7 @@ export const Group = () => {
getValues,
setValue,
formState: { errors },
} = useFormContext<{
groups?: GroupValue[];
}>();
} = useFormContext<GroupForm>();
const values = getValues("groups");
const [open, setOpen] = useState(false);
@ -73,9 +76,8 @@ export const Group = () => {
<KeycloakTextInput
type="text"
id="groupsClaim"
name="groupsClaim"
data-testid="groupsClaim"
ref={register}
{...register("groupsClaim")}
/>
</FormGroup>
<FormGroup
@ -96,10 +98,10 @@ export const Group = () => {
control={control}
defaultValue={[]}
rules={{
validate: (value: GroupValue[]) =>
value.filter(({ id }) => id).length > 0,
validate: (value?: GroupValue[]) =>
value && value.filter(({ id }) => id).length > 0,
}}
render={({ onChange, value }) => (
render={({ field }) => (
<>
{open && (
<GroupPickerDialog
@ -109,8 +111,8 @@ export const Group = () => {
ok: "common:add",
}}
onConfirm={(groups) => {
onChange([
...value,
field.onChange([
...(field.value || []),
...(groups || []).map(({ id }) => ({ id })),
]);
setSelectedGroups([...selectedGroups, ...(groups || [])]);
@ -149,16 +151,16 @@ export const Group = () => {
<Td>{group.path}</Td>
<Td>
<Controller
name={`groups[${index}].extendChildren`}
name={`groups.${index}.extendChildren`}
defaultValue={false}
control={control}
render={({ onChange, value }) => (
render={({ field }) => (
<Checkbox
id="extendChildren"
data-testid="standard"
name="extendChildren"
isChecked={value}
onChange={onChange}
isChecked={field.value}
onChange={field.onChange}
isDisabled={group.subGroups?.length === 0}
/>
)}

View file

@ -1,5 +1,5 @@
import { useTranslation } from "react-i18next";
import { Controller, useFormContext } from "react-hook-form";
import { Controller, useFormContext } from "react-hook-form-v7";
import { FormGroup } from "@patternfly/react-core";
import { CodeEditor, Language } from "@patternfly/react-code-editor";
@ -25,13 +25,12 @@ export const JavaScript = () => {
name="code"
defaultValue=""
control={control}
render={({ onChange, value }) => (
render={({ field }) => (
<CodeEditor
id="code"
data-testid="code"
type="text"
onChange={onChange}
code={value}
onChange={field.onChange}
code={field.value}
height="600px"
language={Language.javascript}
/>

View file

@ -1,5 +1,5 @@
import { useTranslation } from "react-i18next";
import { Controller, useFormContext } from "react-hook-form";
import { Controller, useFormContext } from "react-hook-form-v7";
import { FormGroup, Radio } from "@patternfly/react-core";
import { HelpItem } from "../../../components/help-enabler/HelpItem";
@ -24,16 +24,16 @@ export const LogicSelector = () => {
data-testid="logic"
defaultValue={LOGIC_TYPES[0]}
control={control}
render={({ onChange, value }) => (
render={({ field }) => (
<>
{LOGIC_TYPES.map((type) => (
<Radio
id={type}
key={type}
data-testid={type}
isChecked={value === type}
isChecked={field.value === type}
name="logic"
onChange={() => onChange(type)}
onChange={() => field.onChange(type)}
label={t(`logicType.${type.toLowerCase()}`)}
className="pf-u-mb-md"
/>

View file

@ -1,5 +1,5 @@
import { useTranslation } from "react-i18next";
import { useFormContext } from "react-hook-form";
import { useFormContext } from "react-hook-form-v7";
import { FormGroup, ValidatedOptions } from "@patternfly/react-core";
import { HelpItem } from "../../../components/help-enabler/HelpItem";
@ -35,11 +35,9 @@ export const NameDescription = ({ prefix }: NameDescriptionProps) => {
}
>
<KeycloakTextInput
type="text"
id="kc-name"
name="name"
data-testid="name"
ref={register({ required: true })}
{...register("name", { required: true })}
validated={
errors.name ? ValidatedOptions.error : ValidatedOptions.default
}
@ -60,15 +58,13 @@ export const NameDescription = ({ prefix }: NameDescriptionProps) => {
helperTextInvalid={errors.description?.message}
>
<KeycloakTextArea
ref={register({
{...register("description", {
maxLength: {
value: 255,
message: t("common:maxLength", { length: 255 }),
},
})}
type="text"
id="kc-description"
name="description"
data-testid="description"
validated={
errors.description

View file

@ -8,7 +8,7 @@ import {
PageSection,
} from "@patternfly/react-core";
import { FunctionComponent, useState } from "react";
import { FormProvider, useForm } from "react-hook-form";
import { FormProvider, useForm } from "react-hook-form-v7";
import { useTranslation } from "react-i18next";
import { Link, useNavigate } from "react-router-dom-v5-compat";
@ -105,7 +105,7 @@ export default function PolicyDetails() {
[]
);
const save = async (policy: Policy) => {
const onSubmit = async (policy: Policy) => {
// remove entries that only have the boolean set and no id
policy.groups = policy.groups?.filter((g) => g.id);
policy.clientScopes = policy.clientScopes?.filter((c) => c.id);
@ -194,7 +194,7 @@ export default function PolicyDetails() {
<PageSection variant="light">
<FormAccess
isHorizontal
onSubmit={handleSubmit(save)}
onSubmit={handleSubmit(onSubmit)}
role="view-clients"
>
<FormProvider {...form}>

View file

@ -1,5 +1,5 @@
import { useTranslation } from "react-i18next";
import { useFormContext } from "react-hook-form";
import { useFormContext } from "react-hook-form-v7";
import { FormGroup } from "@patternfly/react-core";
import { HelpItem } from "../../../components/help-enabler/HelpItem";
@ -28,11 +28,9 @@ export const Regex = () => {
}
>
<KeycloakTextInput
type="text"
id="targetClaim"
name="targetClaim"
data-testid="targetClaim"
ref={register({ required: true })}
{...register("targetClaim", { required: true })}
validated={errors.targetClaim ? "error" : "default"}
/>
</FormGroup>
@ -50,10 +48,8 @@ export const Regex = () => {
helperTextInvalid={t("common:required")}
>
<KeycloakTextInput
ref={register({ required: true })}
type="text"
{...register("pattern", { required: true })}
id="pattern"
name="pattern"
data-testid="regexPattern"
validated={errors.pattern ? "error" : "default"}
/>

View file

@ -1,6 +1,6 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useFormContext, Controller } from "react-hook-form";
import { useFormContext, Controller } from "react-hook-form-v7";
import { FormGroup, Button, Checkbox } from "@patternfly/react-core";
import { MinusCircleIcon } from "@patternfly/react-icons";
import {
@ -77,18 +77,18 @@ export const Role = () => {
control={control}
defaultValue={[]}
rules={{
validate: (value: RequiredIdValue[]) =>
value.filter((c) => c.id).length > 0,
validate: (value?: RequiredIdValue[]) =>
value && value.filter((c) => c.id).length > 0,
}}
render={({ onChange, value }) => (
render={({ field }) => (
<>
{open && (
<AddRoleMappingModal
id="role"
type="roles"
onAssign={(rows) => {
onChange([
...value,
field.onChange([
...(field.value || []),
...rows.map((row) => ({ id: row.role.id })),
]);
setSelectedRoles([...selectedRoles, ...rows]);
@ -129,16 +129,16 @@ export const Role = () => {
</Td>
<Td>
<Controller
name={`roles[${index}].required`}
name={`roles.${index}.required`}
defaultValue={false}
control={control}
render={({ onChange, value }) => (
render={({ field }) => (
<Checkbox
id="required"
data-testid="standard"
name="required"
isChecked={value}
onChange={onChange}
isChecked={field.value}
onChange={field.onChange}
/>
)}
/>

View file

@ -1,6 +1,6 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { Controller, useFormContext } from "react-hook-form";
import { Controller, useFormContext } from "react-hook-form-v7";
import {
DatePicker,
Flex,
@ -59,15 +59,20 @@ const DateTime = ({ name }: { name: string }) => {
defaultValue=""
control={control}
rules={{ required: true }}
render={({ onChange, value }) => {
const dateTime = value.match(DATE_TIME_FORMAT) || ["", "", "0", "00"];
render={({ field }) => {
const dateTime = field.value.match(DATE_TIME_FORMAT) || [
"",
"",
"0",
"00",
];
return (
<Split hasGutter id={name}>
<SplitItem>
<DatePicker
value={dateTime[1]}
onChange={(_, date) => {
onChange(parseDate(value, date));
field.onChange(parseDate(field.value, date));
}}
/>
</SplitItem>
@ -75,7 +80,7 @@ const DateTime = ({ name }: { name: string }) => {
<TimePicker
time={`${dateTime[2]}:${dateTime[3]}`}
onChange={(_, hour, minute) =>
onChange(parseTime(value, hour, minute))
field.onChange(parseTime(field.value, hour, minute))
}
is24Hour
/>
@ -102,17 +107,17 @@ const NumberControl = ({ name, min, max }: NumberControlProps) => {
name={name}
defaultValue=""
control={control}
render={({ onChange, value }) => (
render={({ field }) => (
<NumberInput
id={name}
value={value}
value={field.value}
min={min}
max={max}
onPlus={() => onChange(Number(value) + 1)}
onMinus={() => onChange(Number(value) - 1)}
onPlus={() => field.onChange(Number(field.value) + 1)}
onMinus={() => field.onChange(Number(field.value) - 1)}
onChange={(event) => {
const newValue = Number(event.currentTarget.value);
onChange(setValue(!isNaN(newValue) ? newValue : 0));
field.onChange(setValue(!isNaN(newValue) ? newValue : 0));
}}
/>
)}
@ -149,7 +154,10 @@ const FromTo = ({ name, ...rest }: NumberControlProps) => {
export const Time = () => {
const { t } = useTranslation("clients");
const { getValues, errors } = useFormContext();
const {
getValues,
formState: { errors },
} = useFormContext();
const [repeat, setRepeat] = useState(getValues("month"));
return (
<>

View file

@ -1,4 +1,4 @@
import { UserSelect } from "../../../components/users/UserSelect";
import { UserSelect } from "../../../components/users/hook-form-v7/UserSelect";
export const User = () => (
<UserSelect

View file

@ -0,0 +1,132 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { Controller, useFormContext } from "react-hook-form-v7";
import {
SelectOption,
FormGroup,
Select,
SelectVariant,
} from "@patternfly/react-core";
import type UserRepresentation from "@keycloak/keycloak-admin-client/lib/defs/userRepresentation";
import type { UserQuery } from "@keycloak/keycloak-admin-client/lib/resources/users";
import type { ComponentProps } from "../../dynamic/components";
import { useAdminClient, useFetch } from "../../../context/auth/AdminClient";
import { HelpItem } from "../../help-enabler/HelpItem";
import useToggle from "../../../utils/useToggle";
type UserSelectProps = ComponentProps & {
variant?: SelectVariant;
isRequired?: boolean;
};
export const UserSelect = ({
name,
label,
helpText,
defaultValue,
isRequired,
variant = SelectVariant.typeaheadMulti,
}: UserSelectProps) => {
const { t } = useTranslation("clients");
const {
control,
getValues,
formState: { errors },
} = useFormContext();
const values: string[] | undefined = getValues(name!);
const [open, toggleOpen] = useToggle();
const [users, setUsers] = useState<(UserRepresentation | undefined)[]>([]);
const [search, setSearch] = useState("");
const { adminClient } = useAdminClient();
useFetch(
() => {
const params: UserQuery = {
max: 20,
};
if (search) {
params.username = search;
}
if (values?.length && !search) {
return Promise.all(
values.map((id: string) => adminClient.users.findOne({ id }))
);
}
return adminClient.users.find(params);
},
setUsers,
[search]
);
const convert = (clients: (UserRepresentation | undefined)[]) =>
clients
.filter((c) => c !== undefined)
.map((option) => (
<SelectOption
key={option!.id}
value={option!.id}
selected={values?.includes(option!.id!)}
>
{option!.username}
</SelectOption>
));
return (
<FormGroup
label={t(label!)}
isRequired={isRequired}
labelIcon={
<HelpItem helpText={helpText!} fieldLabelId={`clients:${label}`} />
}
fieldId={name!}
validated={errors[name!] ? "error" : "default"}
helperTextInvalid={t("common:required")}
>
<Controller
name={name!}
defaultValue={defaultValue}
control={control}
rules={
isRequired && variant === SelectVariant.typeaheadMulti
? { validate: (value) => value.length > 0 }
: isRequired
? { required: true }
: {}
}
render={({ field }) => (
<Select
toggleId={name!}
variant={variant}
placeholderText={t("selectAUser")}
onToggle={toggleOpen}
isOpen={open}
selections={field.value}
onFilter={(_, value) => {
setSearch(value);
return convert(users);
}}
onSelect={(_, v) => {
const option = v.toString();
if (field.value.includes(option)) {
field.onChange(
field.value.filter((item: string) => item !== option)
);
} else {
field.onChange([...field.value, option]);
}
toggleOpen();
}}
aria-label={t(name!)}
>
{convert(users)}
</Select>
)}
/>
</FormGroup>
);
};