Migrate authenticate settings and policy detail to react-hook-form
v7 (#3984)
This commit is contained in:
parent
498034ee2e
commit
e0246c70d4
17 changed files with 250 additions and 99 deletions
|
@ -3,7 +3,7 @@ import { ActionGroup, ActionGroupProps, Button } from "@patternfly/react-core";
|
||||||
|
|
||||||
type SaveResetProps = ActionGroupProps & {
|
type SaveResetProps = ActionGroupProps & {
|
||||||
name: string;
|
name: string;
|
||||||
save: () => void;
|
save?: () => void;
|
||||||
reset: () => void;
|
reset: () => void;
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
};
|
};
|
||||||
|
@ -18,7 +18,12 @@ export const SaveReset = ({
|
||||||
const { t } = useTranslation("common");
|
const { t } = useTranslation("common");
|
||||||
return (
|
return (
|
||||||
<ActionGroup {...rest}>
|
<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")}
|
{t("save")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
|
|
@ -10,7 +10,7 @@ import {
|
||||||
Switch,
|
Switch,
|
||||||
} from "@patternfly/react-core";
|
} from "@patternfly/react-core";
|
||||||
import { useState } from "react";
|
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 { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation";
|
import type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation";
|
||||||
|
@ -213,7 +213,7 @@ export const AuthorizationEvaluate = ({ client }: Props) => {
|
||||||
control={control}
|
control={control}
|
||||||
defaultValue={[]}
|
defaultValue={[]}
|
||||||
rules={{ validate: (value) => (value || "").length > 0 }}
|
rules={{ validate: (value) => (value || "").length > 0 }}
|
||||||
render={({ field }) => (
|
render={(field) => (
|
||||||
<Select
|
<Select
|
||||||
placeholderText={t("selectARole")}
|
placeholderText={t("selectARole")}
|
||||||
variant={SelectVariant.typeaheadMulti}
|
variant={SelectVariant.typeaheadMulti}
|
||||||
|
@ -314,7 +314,8 @@ export const AuthorizationEvaluate = ({ client }: Props) => {
|
||||||
<KeycloakTextInput
|
<KeycloakTextInput
|
||||||
id="alias"
|
id="alias"
|
||||||
data-testid="alias"
|
data-testid="alias"
|
||||||
{...register("alias", { required: true })}
|
name="alias"
|
||||||
|
ref={register({ required: true })}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
<FormGroup
|
<FormGroup
|
||||||
|
@ -331,7 +332,7 @@ export const AuthorizationEvaluate = ({ client }: Props) => {
|
||||||
name="authScopes"
|
name="authScopes"
|
||||||
defaultValue={[]}
|
defaultValue={[]}
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => (
|
render={(field) => (
|
||||||
<Select
|
<Select
|
||||||
toggleId="authScopes"
|
toggleId="authScopes"
|
||||||
onToggle={setScopesDropdownOpen}
|
onToggle={setScopesDropdownOpen}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { FormGroup, Radio } from "@patternfly/react-core";
|
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 { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { HelpItem } from "../../components/help-enabler/HelpItem";
|
import { HelpItem } from "../../components/help-enabler/HelpItem";
|
||||||
|
@ -35,7 +35,7 @@ export const DecisionStrategySelect = ({
|
||||||
data-testid="decisionStrategy"
|
data-testid="decisionStrategy"
|
||||||
defaultValue={DECISION_STRATEGY[0]}
|
defaultValue={DECISION_STRATEGY[0]}
|
||||||
control={control}
|
control={control}
|
||||||
render={(field) => (
|
render={({ field }) => (
|
||||||
<>
|
<>
|
||||||
{(isLimited
|
{(isLimited
|
||||||
? DECISION_STRATEGY.slice(0, 2)
|
? DECISION_STRATEGY.slice(0, 2)
|
|
@ -1,6 +1,6 @@
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Controller, FormProvider, useForm } from "react-hook-form";
|
import { Controller, FormProvider, useForm } from "react-hook-form-v7";
|
||||||
import {
|
import {
|
||||||
AlertVariant,
|
AlertVariant,
|
||||||
Button,
|
Button,
|
||||||
|
@ -20,7 +20,7 @@ import { SaveReset } from "../advanced/SaveReset";
|
||||||
import { ImportDialog } from "./ImportDialog";
|
import { ImportDialog } from "./ImportDialog";
|
||||||
import useToggle from "../../utils/useToggle";
|
import useToggle from "../../utils/useToggle";
|
||||||
import { useAlerts } from "../../components/alert/Alerts";
|
import { useAlerts } from "../../components/alert/Alerts";
|
||||||
import { DecisionStrategySelect } from "./DecisionStragegySelect";
|
import { DecisionStrategySelect } from "./DecisionStrategySelect";
|
||||||
|
|
||||||
const POLICY_ENFORCEMENT_MODES = [
|
const POLICY_ENFORCEMENT_MODES = [
|
||||||
"ENFORCING",
|
"ENFORCING",
|
||||||
|
@ -28,12 +28,17 @@ const POLICY_ENFORCEMENT_MODES = [
|
||||||
"DISABLED",
|
"DISABLED",
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
export type FormFields = Omit<
|
||||||
|
ResourceServerRepresentation,
|
||||||
|
"scopes" | "resources"
|
||||||
|
>;
|
||||||
|
|
||||||
export const AuthorizationSettings = ({ clientId }: { clientId: string }) => {
|
export const AuthorizationSettings = ({ clientId }: { clientId: string }) => {
|
||||||
const { t } = useTranslation("clients");
|
const { t } = useTranslation("clients");
|
||||||
const [resource, setResource] = useState<ResourceServerRepresentation>();
|
const [resource, setResource] = useState<ResourceServerRepresentation>();
|
||||||
const [importDialog, toggleImportDialog] = useToggle();
|
const [importDialog, toggleImportDialog] = useToggle();
|
||||||
|
|
||||||
const form = useForm<ResourceServerRepresentation>({});
|
const form = useForm<FormFields>({});
|
||||||
const { control, reset, handleSubmit } = form;
|
const { control, reset, handleSubmit } = form;
|
||||||
|
|
||||||
const { adminClient } = useAdminClient();
|
const { adminClient } = useAdminClient();
|
||||||
|
@ -58,7 +63,7 @@ export const AuthorizationSettings = ({ clientId }: { clientId: string }) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const save = async (resource: ResourceServerRepresentation) => {
|
const onSubmit = async (resource: ResourceServerRepresentation) => {
|
||||||
try {
|
try {
|
||||||
await adminClient.clients.updateResourceServer(
|
await adminClient.clients.updateResourceServer(
|
||||||
{ id: clientId },
|
{ id: clientId },
|
||||||
|
@ -82,7 +87,11 @@ export const AuthorizationSettings = ({ clientId }: { clientId: string }) => {
|
||||||
closeDialog={toggleImportDialog}
|
closeDialog={toggleImportDialog}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<FormAccess role="view-clients" isHorizontal>
|
<FormAccess
|
||||||
|
role="view-clients"
|
||||||
|
isHorizontal
|
||||||
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
|
>
|
||||||
<FormGroup
|
<FormGroup
|
||||||
label={t("import")}
|
label={t("import")}
|
||||||
fieldId="import"
|
fieldId="import"
|
||||||
|
@ -114,16 +123,16 @@ export const AuthorizationSettings = ({ clientId }: { clientId: string }) => {
|
||||||
data-testid="policyEnforcementMode"
|
data-testid="policyEnforcementMode"
|
||||||
defaultValue={POLICY_ENFORCEMENT_MODES[0]}
|
defaultValue={POLICY_ENFORCEMENT_MODES[0]}
|
||||||
control={control}
|
control={control}
|
||||||
render={({ onChange, value }) => (
|
render={({ field }) => (
|
||||||
<>
|
<>
|
||||||
{POLICY_ENFORCEMENT_MODES.map((mode) => (
|
{POLICY_ENFORCEMENT_MODES.map((mode) => (
|
||||||
<Radio
|
<Radio
|
||||||
id={mode}
|
id={mode}
|
||||||
key={mode}
|
key={mode}
|
||||||
data-testid={mode}
|
data-testid={mode}
|
||||||
isChecked={value === mode}
|
isChecked={field.value === mode}
|
||||||
name="policyEnforcementMode"
|
name="policyEnforcementMode"
|
||||||
onChange={() => onChange(mode)}
|
onChange={() => field.onChange(mode)}
|
||||||
label={t(`policyEnforcementModes.${mode}`)}
|
label={t(`policyEnforcementModes.${mode}`)}
|
||||||
className="pf-u-mb-md"
|
className="pf-u-mb-md"
|
||||||
/>
|
/>
|
||||||
|
@ -151,13 +160,13 @@ export const AuthorizationSettings = ({ clientId }: { clientId: string }) => {
|
||||||
data-testid="allowRemoteResourceManagement"
|
data-testid="allowRemoteResourceManagement"
|
||||||
defaultValue={false}
|
defaultValue={false}
|
||||||
control={control}
|
control={control}
|
||||||
render={({ onChange, value }) => (
|
render={({ field }) => (
|
||||||
<Switch
|
<Switch
|
||||||
id="allowRemoteResourceManagement"
|
id="allowRemoteResourceManagement"
|
||||||
label={t("common:on")}
|
label={t("common:on")}
|
||||||
labelOff={t("common:off")}
|
labelOff={t("common:off")}
|
||||||
isChecked={value}
|
isChecked={field.value}
|
||||||
onChange={onChange}
|
onChange={field.onChange}
|
||||||
aria-label={t("allowRemoteResourceManagement")}
|
aria-label={t("allowRemoteResourceManagement")}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
@ -165,7 +174,6 @@ export const AuthorizationSettings = ({ clientId }: { clientId: string }) => {
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
<SaveReset
|
<SaveReset
|
||||||
name="authenticationSettings"
|
name="authenticationSettings"
|
||||||
save={() => handleSubmit(save)()}
|
|
||||||
reset={() => reset(resource)}
|
reset={() => reset(resource)}
|
||||||
isActive
|
isActive
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { useTranslation } from "react-i18next";
|
||||||
import { HelpItem } from "../../../components/help-enabler/HelpItem";
|
import { HelpItem } from "../../../components/help-enabler/HelpItem";
|
||||||
import { useParams } from "../../../utils/useParams";
|
import { useParams } from "../../../utils/useParams";
|
||||||
import type { PolicyDetailsParams } from "../../routes/PolicyDetails";
|
import type { PolicyDetailsParams } from "../../routes/PolicyDetails";
|
||||||
import { DecisionStrategySelect } from "../DecisionStragegySelect";
|
import { DecisionStrategySelect } from "../DecisionStrategySelect";
|
||||||
import { ResourcesPolicySelect } from "../ResourcesPolicySelect";
|
import { ResourcesPolicySelect } from "../ResourcesPolicySelect";
|
||||||
|
|
||||||
export const Aggregate = () => {
|
export const Aggregate = () => {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Controller, useFormContext } from "react-hook-form";
|
import { Controller, useFormContext } from "react-hook-form-v7";
|
||||||
import {
|
import {
|
||||||
SelectOption,
|
SelectOption,
|
||||||
FormGroup,
|
FormGroup,
|
||||||
|
@ -84,23 +84,25 @@ export const Client = () => {
|
||||||
rules={{
|
rules={{
|
||||||
validate: (value) => value.length > 0,
|
validate: (value) => value.length > 0,
|
||||||
}}
|
}}
|
||||||
render={({ onChange, value }) => (
|
render={({ field }) => (
|
||||||
<Select
|
<Select
|
||||||
toggleId="clients"
|
toggleId="clients"
|
||||||
variant={SelectVariant.typeaheadMulti}
|
variant={SelectVariant.typeaheadMulti}
|
||||||
onToggle={(open) => setOpen(open)}
|
onToggle={(open) => setOpen(open)}
|
||||||
isOpen={open}
|
isOpen={open}
|
||||||
selections={value}
|
selections={field.value}
|
||||||
onFilter={(_, value) => {
|
onFilter={(_, value) => {
|
||||||
setSearch(value);
|
setSearch(value);
|
||||||
return convert(clients);
|
return convert(clients);
|
||||||
}}
|
}}
|
||||||
onSelect={(_, v) => {
|
onSelect={(_, v) => {
|
||||||
const option = v.toString();
|
const option = v.toString();
|
||||||
if (value.includes(option)) {
|
if (field.value.includes(option)) {
|
||||||
onChange(value.filter((item: string) => item !== option));
|
field.onChange(
|
||||||
|
field.value.filter((item: string) => item !== option)
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
onChange([...value, option]);
|
field.onChange([...field.value, option]);
|
||||||
}
|
}
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
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 { FormGroup, Button, Checkbox } from "@patternfly/react-core";
|
||||||
import { MinusCircleIcon } from "@patternfly/react-icons";
|
import { MinusCircleIcon } from "@patternfly/react-icons";
|
||||||
import {
|
import {
|
||||||
|
@ -76,13 +76,15 @@ export const ClientScope = () => {
|
||||||
validate: (value: RequiredIdValue[]) =>
|
validate: (value: RequiredIdValue[]) =>
|
||||||
value.filter((c) => c.id).length > 0,
|
value.filter((c) => c.id).length > 0,
|
||||||
}}
|
}}
|
||||||
render={({ onChange, value }) => (
|
render={({ field }) => (
|
||||||
<>
|
<>
|
||||||
{open && (
|
{open && (
|
||||||
<AddScopeDialog
|
<AddScopeDialog
|
||||||
clientScopes={scopes.filter(
|
clientScopes={scopes.filter(
|
||||||
(scope) =>
|
(scope) =>
|
||||||
!value.map((c: RequiredIdValue) => c.id).includes(scope.id!)
|
!field.value
|
||||||
|
.map((c: RequiredIdValue) => c.id)
|
||||||
|
.includes(scope.id!)
|
||||||
)}
|
)}
|
||||||
isClientScopesConditionType
|
isClientScopesConditionType
|
||||||
open={open}
|
open={open}
|
||||||
|
@ -92,8 +94,8 @@ export const ClientScope = () => {
|
||||||
...selectedScopes,
|
...selectedScopes,
|
||||||
...scopes.map((s) => s.scope),
|
...scopes.map((s) => s.scope),
|
||||||
]);
|
]);
|
||||||
onChange([
|
field.onChange([
|
||||||
...value,
|
...field.value,
|
||||||
...scopes
|
...scopes
|
||||||
.map((scope) => scope.scope)
|
.map((scope) => scope.scope)
|
||||||
.map((item) => ({ id: item.id!, required: false })),
|
.map((item) => ({ id: item.id!, required: false })),
|
||||||
|
@ -128,16 +130,16 @@ export const ClientScope = () => {
|
||||||
<Td>{scope.name}</Td>
|
<Td>{scope.name}</Td>
|
||||||
<Td>
|
<Td>
|
||||||
<Controller
|
<Controller
|
||||||
name={`clientScopes[${index}].required`}
|
name={`clientScopes.${index}.required`}
|
||||||
defaultValue={false}
|
defaultValue={false}
|
||||||
control={control}
|
control={control}
|
||||||
render={({ onChange, value }) => (
|
render={({ field }) => (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
id="required"
|
id="required"
|
||||||
data-testid="standard"
|
data-testid="standard"
|
||||||
name="required"
|
name="required"
|
||||||
isChecked={value}
|
isChecked={field.value}
|
||||||
onChange={onChange}
|
onChange={field.onChange}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
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 { MinusCircleIcon } from "@patternfly/react-icons";
|
||||||
import { FormGroup, Button, Checkbox } from "@patternfly/react-core";
|
import { FormGroup, Button, Checkbox } from "@patternfly/react-core";
|
||||||
import {
|
import {
|
||||||
|
@ -18,6 +18,11 @@ import { useAdminClient, useFetch } from "../../../context/auth/AdminClient";
|
||||||
import { GroupPickerDialog } from "../../../components/group/GroupPickerDialog";
|
import { GroupPickerDialog } from "../../../components/group/GroupPickerDialog";
|
||||||
import { KeycloakTextInput } from "../../../components/keycloak-text-input/KeycloakTextInput";
|
import { KeycloakTextInput } from "../../../components/keycloak-text-input/KeycloakTextInput";
|
||||||
|
|
||||||
|
type GroupForm = {
|
||||||
|
groups?: GroupValue[];
|
||||||
|
groupsClaim: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type GroupValue = {
|
export type GroupValue = {
|
||||||
id: string;
|
id: string;
|
||||||
extendChildren: boolean;
|
extendChildren: boolean;
|
||||||
|
@ -31,9 +36,7 @@ export const Group = () => {
|
||||||
getValues,
|
getValues,
|
||||||
setValue,
|
setValue,
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
} = useFormContext<{
|
} = useFormContext<GroupForm>();
|
||||||
groups?: GroupValue[];
|
|
||||||
}>();
|
|
||||||
const values = getValues("groups");
|
const values = getValues("groups");
|
||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
@ -73,9 +76,8 @@ export const Group = () => {
|
||||||
<KeycloakTextInput
|
<KeycloakTextInput
|
||||||
type="text"
|
type="text"
|
||||||
id="groupsClaim"
|
id="groupsClaim"
|
||||||
name="groupsClaim"
|
|
||||||
data-testid="groupsClaim"
|
data-testid="groupsClaim"
|
||||||
ref={register}
|
{...register("groupsClaim")}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
<FormGroup
|
<FormGroup
|
||||||
|
@ -96,10 +98,10 @@ export const Group = () => {
|
||||||
control={control}
|
control={control}
|
||||||
defaultValue={[]}
|
defaultValue={[]}
|
||||||
rules={{
|
rules={{
|
||||||
validate: (value: GroupValue[]) =>
|
validate: (value?: GroupValue[]) =>
|
||||||
value.filter(({ id }) => id).length > 0,
|
value && value.filter(({ id }) => id).length > 0,
|
||||||
}}
|
}}
|
||||||
render={({ onChange, value }) => (
|
render={({ field }) => (
|
||||||
<>
|
<>
|
||||||
{open && (
|
{open && (
|
||||||
<GroupPickerDialog
|
<GroupPickerDialog
|
||||||
|
@ -109,8 +111,8 @@ export const Group = () => {
|
||||||
ok: "common:add",
|
ok: "common:add",
|
||||||
}}
|
}}
|
||||||
onConfirm={(groups) => {
|
onConfirm={(groups) => {
|
||||||
onChange([
|
field.onChange([
|
||||||
...value,
|
...(field.value || []),
|
||||||
...(groups || []).map(({ id }) => ({ id })),
|
...(groups || []).map(({ id }) => ({ id })),
|
||||||
]);
|
]);
|
||||||
setSelectedGroups([...selectedGroups, ...(groups || [])]);
|
setSelectedGroups([...selectedGroups, ...(groups || [])]);
|
||||||
|
@ -149,16 +151,16 @@ export const Group = () => {
|
||||||
<Td>{group.path}</Td>
|
<Td>{group.path}</Td>
|
||||||
<Td>
|
<Td>
|
||||||
<Controller
|
<Controller
|
||||||
name={`groups[${index}].extendChildren`}
|
name={`groups.${index}.extendChildren`}
|
||||||
defaultValue={false}
|
defaultValue={false}
|
||||||
control={control}
|
control={control}
|
||||||
render={({ onChange, value }) => (
|
render={({ field }) => (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
id="extendChildren"
|
id="extendChildren"
|
||||||
data-testid="standard"
|
data-testid="standard"
|
||||||
name="extendChildren"
|
name="extendChildren"
|
||||||
isChecked={value}
|
isChecked={field.value}
|
||||||
onChange={onChange}
|
onChange={field.onChange}
|
||||||
isDisabled={group.subGroups?.length === 0}
|
isDisabled={group.subGroups?.length === 0}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { useTranslation } from "react-i18next";
|
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 { FormGroup } from "@patternfly/react-core";
|
||||||
import { CodeEditor, Language } from "@patternfly/react-code-editor";
|
import { CodeEditor, Language } from "@patternfly/react-code-editor";
|
||||||
|
|
||||||
|
@ -25,13 +25,12 @@ export const JavaScript = () => {
|
||||||
name="code"
|
name="code"
|
||||||
defaultValue=""
|
defaultValue=""
|
||||||
control={control}
|
control={control}
|
||||||
render={({ onChange, value }) => (
|
render={({ field }) => (
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
id="code"
|
id="code"
|
||||||
data-testid="code"
|
data-testid="code"
|
||||||
type="text"
|
onChange={field.onChange}
|
||||||
onChange={onChange}
|
code={field.value}
|
||||||
code={value}
|
|
||||||
height="600px"
|
height="600px"
|
||||||
language={Language.javascript}
|
language={Language.javascript}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { useTranslation } from "react-i18next";
|
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 { FormGroup, Radio } from "@patternfly/react-core";
|
||||||
|
|
||||||
import { HelpItem } from "../../../components/help-enabler/HelpItem";
|
import { HelpItem } from "../../../components/help-enabler/HelpItem";
|
||||||
|
@ -24,16 +24,16 @@ export const LogicSelector = () => {
|
||||||
data-testid="logic"
|
data-testid="logic"
|
||||||
defaultValue={LOGIC_TYPES[0]}
|
defaultValue={LOGIC_TYPES[0]}
|
||||||
control={control}
|
control={control}
|
||||||
render={({ onChange, value }) => (
|
render={({ field }) => (
|
||||||
<>
|
<>
|
||||||
{LOGIC_TYPES.map((type) => (
|
{LOGIC_TYPES.map((type) => (
|
||||||
<Radio
|
<Radio
|
||||||
id={type}
|
id={type}
|
||||||
key={type}
|
key={type}
|
||||||
data-testid={type}
|
data-testid={type}
|
||||||
isChecked={value === type}
|
isChecked={field.value === type}
|
||||||
name="logic"
|
name="logic"
|
||||||
onChange={() => onChange(type)}
|
onChange={() => field.onChange(type)}
|
||||||
label={t(`logicType.${type.toLowerCase()}`)}
|
label={t(`logicType.${type.toLowerCase()}`)}
|
||||||
className="pf-u-mb-md"
|
className="pf-u-mb-md"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { useTranslation } from "react-i18next";
|
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 { FormGroup, ValidatedOptions } from "@patternfly/react-core";
|
||||||
|
|
||||||
import { HelpItem } from "../../../components/help-enabler/HelpItem";
|
import { HelpItem } from "../../../components/help-enabler/HelpItem";
|
||||||
|
@ -35,11 +35,9 @@ export const NameDescription = ({ prefix }: NameDescriptionProps) => {
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<KeycloakTextInput
|
<KeycloakTextInput
|
||||||
type="text"
|
|
||||||
id="kc-name"
|
id="kc-name"
|
||||||
name="name"
|
|
||||||
data-testid="name"
|
data-testid="name"
|
||||||
ref={register({ required: true })}
|
{...register("name", { required: true })}
|
||||||
validated={
|
validated={
|
||||||
errors.name ? ValidatedOptions.error : ValidatedOptions.default
|
errors.name ? ValidatedOptions.error : ValidatedOptions.default
|
||||||
}
|
}
|
||||||
|
@ -60,15 +58,13 @@ export const NameDescription = ({ prefix }: NameDescriptionProps) => {
|
||||||
helperTextInvalid={errors.description?.message}
|
helperTextInvalid={errors.description?.message}
|
||||||
>
|
>
|
||||||
<KeycloakTextArea
|
<KeycloakTextArea
|
||||||
ref={register({
|
{...register("description", {
|
||||||
maxLength: {
|
maxLength: {
|
||||||
value: 255,
|
value: 255,
|
||||||
message: t("common:maxLength", { length: 255 }),
|
message: t("common:maxLength", { length: 255 }),
|
||||||
},
|
},
|
||||||
})}
|
})}
|
||||||
type="text"
|
|
||||||
id="kc-description"
|
id="kc-description"
|
||||||
name="description"
|
|
||||||
data-testid="description"
|
data-testid="description"
|
||||||
validated={
|
validated={
|
||||||
errors.description
|
errors.description
|
||||||
|
|
|
@ -8,7 +8,7 @@ import {
|
||||||
PageSection,
|
PageSection,
|
||||||
} from "@patternfly/react-core";
|
} from "@patternfly/react-core";
|
||||||
import { FunctionComponent, useState } from "react";
|
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 { useTranslation } from "react-i18next";
|
||||||
import { Link, useNavigate } from "react-router-dom-v5-compat";
|
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
|
// remove entries that only have the boolean set and no id
|
||||||
policy.groups = policy.groups?.filter((g) => g.id);
|
policy.groups = policy.groups?.filter((g) => g.id);
|
||||||
policy.clientScopes = policy.clientScopes?.filter((c) => c.id);
|
policy.clientScopes = policy.clientScopes?.filter((c) => c.id);
|
||||||
|
@ -194,7 +194,7 @@ export default function PolicyDetails() {
|
||||||
<PageSection variant="light">
|
<PageSection variant="light">
|
||||||
<FormAccess
|
<FormAccess
|
||||||
isHorizontal
|
isHorizontal
|
||||||
onSubmit={handleSubmit(save)}
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
role="view-clients"
|
role="view-clients"
|
||||||
>
|
>
|
||||||
<FormProvider {...form}>
|
<FormProvider {...form}>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { useTranslation } from "react-i18next";
|
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 { FormGroup } from "@patternfly/react-core";
|
||||||
|
|
||||||
import { HelpItem } from "../../../components/help-enabler/HelpItem";
|
import { HelpItem } from "../../../components/help-enabler/HelpItem";
|
||||||
|
@ -28,11 +28,9 @@ export const Regex = () => {
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<KeycloakTextInput
|
<KeycloakTextInput
|
||||||
type="text"
|
|
||||||
id="targetClaim"
|
id="targetClaim"
|
||||||
name="targetClaim"
|
|
||||||
data-testid="targetClaim"
|
data-testid="targetClaim"
|
||||||
ref={register({ required: true })}
|
{...register("targetClaim", { required: true })}
|
||||||
validated={errors.targetClaim ? "error" : "default"}
|
validated={errors.targetClaim ? "error" : "default"}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
@ -50,10 +48,8 @@ export const Regex = () => {
|
||||||
helperTextInvalid={t("common:required")}
|
helperTextInvalid={t("common:required")}
|
||||||
>
|
>
|
||||||
<KeycloakTextInput
|
<KeycloakTextInput
|
||||||
ref={register({ required: true })}
|
{...register("pattern", { required: true })}
|
||||||
type="text"
|
|
||||||
id="pattern"
|
id="pattern"
|
||||||
name="pattern"
|
|
||||||
data-testid="regexPattern"
|
data-testid="regexPattern"
|
||||||
validated={errors.pattern ? "error" : "default"}
|
validated={errors.pattern ? "error" : "default"}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
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 { FormGroup, Button, Checkbox } from "@patternfly/react-core";
|
||||||
import { MinusCircleIcon } from "@patternfly/react-icons";
|
import { MinusCircleIcon } from "@patternfly/react-icons";
|
||||||
import {
|
import {
|
||||||
|
@ -77,18 +77,18 @@ export const Role = () => {
|
||||||
control={control}
|
control={control}
|
||||||
defaultValue={[]}
|
defaultValue={[]}
|
||||||
rules={{
|
rules={{
|
||||||
validate: (value: RequiredIdValue[]) =>
|
validate: (value?: RequiredIdValue[]) =>
|
||||||
value.filter((c) => c.id).length > 0,
|
value && value.filter((c) => c.id).length > 0,
|
||||||
}}
|
}}
|
||||||
render={({ onChange, value }) => (
|
render={({ field }) => (
|
||||||
<>
|
<>
|
||||||
{open && (
|
{open && (
|
||||||
<AddRoleMappingModal
|
<AddRoleMappingModal
|
||||||
id="role"
|
id="role"
|
||||||
type="roles"
|
type="roles"
|
||||||
onAssign={(rows) => {
|
onAssign={(rows) => {
|
||||||
onChange([
|
field.onChange([
|
||||||
...value,
|
...(field.value || []),
|
||||||
...rows.map((row) => ({ id: row.role.id })),
|
...rows.map((row) => ({ id: row.role.id })),
|
||||||
]);
|
]);
|
||||||
setSelectedRoles([...selectedRoles, ...rows]);
|
setSelectedRoles([...selectedRoles, ...rows]);
|
||||||
|
@ -129,16 +129,16 @@ export const Role = () => {
|
||||||
</Td>
|
</Td>
|
||||||
<Td>
|
<Td>
|
||||||
<Controller
|
<Controller
|
||||||
name={`roles[${index}].required`}
|
name={`roles.${index}.required`}
|
||||||
defaultValue={false}
|
defaultValue={false}
|
||||||
control={control}
|
control={control}
|
||||||
render={({ onChange, value }) => (
|
render={({ field }) => (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
id="required"
|
id="required"
|
||||||
data-testid="standard"
|
data-testid="standard"
|
||||||
name="required"
|
name="required"
|
||||||
isChecked={value}
|
isChecked={field.value}
|
||||||
onChange={onChange}
|
onChange={field.onChange}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Controller, useFormContext } from "react-hook-form";
|
import { Controller, useFormContext } from "react-hook-form-v7";
|
||||||
import {
|
import {
|
||||||
DatePicker,
|
DatePicker,
|
||||||
Flex,
|
Flex,
|
||||||
|
@ -59,15 +59,20 @@ const DateTime = ({ name }: { name: string }) => {
|
||||||
defaultValue=""
|
defaultValue=""
|
||||||
control={control}
|
control={control}
|
||||||
rules={{ required: true }}
|
rules={{ required: true }}
|
||||||
render={({ onChange, value }) => {
|
render={({ field }) => {
|
||||||
const dateTime = value.match(DATE_TIME_FORMAT) || ["", "", "0", "00"];
|
const dateTime = field.value.match(DATE_TIME_FORMAT) || [
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"0",
|
||||||
|
"00",
|
||||||
|
];
|
||||||
return (
|
return (
|
||||||
<Split hasGutter id={name}>
|
<Split hasGutter id={name}>
|
||||||
<SplitItem>
|
<SplitItem>
|
||||||
<DatePicker
|
<DatePicker
|
||||||
value={dateTime[1]}
|
value={dateTime[1]}
|
||||||
onChange={(_, date) => {
|
onChange={(_, date) => {
|
||||||
onChange(parseDate(value, date));
|
field.onChange(parseDate(field.value, date));
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</SplitItem>
|
</SplitItem>
|
||||||
|
@ -75,7 +80,7 @@ const DateTime = ({ name }: { name: string }) => {
|
||||||
<TimePicker
|
<TimePicker
|
||||||
time={`${dateTime[2]}:${dateTime[3]}`}
|
time={`${dateTime[2]}:${dateTime[3]}`}
|
||||||
onChange={(_, hour, minute) =>
|
onChange={(_, hour, minute) =>
|
||||||
onChange(parseTime(value, hour, minute))
|
field.onChange(parseTime(field.value, hour, minute))
|
||||||
}
|
}
|
||||||
is24Hour
|
is24Hour
|
||||||
/>
|
/>
|
||||||
|
@ -102,17 +107,17 @@ const NumberControl = ({ name, min, max }: NumberControlProps) => {
|
||||||
name={name}
|
name={name}
|
||||||
defaultValue=""
|
defaultValue=""
|
||||||
control={control}
|
control={control}
|
||||||
render={({ onChange, value }) => (
|
render={({ field }) => (
|
||||||
<NumberInput
|
<NumberInput
|
||||||
id={name}
|
id={name}
|
||||||
value={value}
|
value={field.value}
|
||||||
min={min}
|
min={min}
|
||||||
max={max}
|
max={max}
|
||||||
onPlus={() => onChange(Number(value) + 1)}
|
onPlus={() => field.onChange(Number(field.value) + 1)}
|
||||||
onMinus={() => onChange(Number(value) - 1)}
|
onMinus={() => field.onChange(Number(field.value) - 1)}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
const newValue = Number(event.currentTarget.value);
|
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 = () => {
|
export const Time = () => {
|
||||||
const { t } = useTranslation("clients");
|
const { t } = useTranslation("clients");
|
||||||
const { getValues, errors } = useFormContext();
|
const {
|
||||||
|
getValues,
|
||||||
|
formState: { errors },
|
||||||
|
} = useFormContext();
|
||||||
const [repeat, setRepeat] = useState(getValues("month"));
|
const [repeat, setRepeat] = useState(getValues("month"));
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { UserSelect } from "../../../components/users/UserSelect";
|
import { UserSelect } from "../../../components/users/hook-form-v7/UserSelect";
|
||||||
|
|
||||||
export const User = () => (
|
export const User = () => (
|
||||||
<UserSelect
|
<UserSelect
|
||||||
|
|
132
apps/admin-ui/src/components/users/hook-form-v7/UserSelect.tsx
Normal file
132
apps/admin-ui/src/components/users/hook-form-v7/UserSelect.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
Loading…
Reference in a new issue