Use react-hook-form v7 for client section (#3919)

This commit is contained in:
Erik Jan de Wit 2022-12-02 09:54:30 -05:00 committed by GitHub
parent 8dde92e2fd
commit 997881a7a1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 832 additions and 598 deletions

View file

@ -55,7 +55,7 @@ export const ExecutionConfigModal = ({
} = form;
const setupForm = (config?: AuthenticatorConfigRepresentation) => {
convertToFormValues(config, setValue);
convertToFormValues(config || {}, setValue);
};
useFetch(

View file

@ -1,6 +1,7 @@
import { useTranslation } from "react-i18next";
import { useFormContext } from "react-hook-form";
import { AlertVariant, PageSection, Text } from "@patternfly/react-core";
import type { TFunction } from "i18next";
import { useFormContext } from "react-hook-form-v7";
import { useTranslation } from "react-i18next";
import type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation";
import type GlobalRequestResult from "@keycloak/keycloak-admin-client/lib/defs/globalRequestResult";
@ -10,13 +11,12 @@ import { ScrollForm } from "../components/scroll-form/ScrollForm";
import { convertAttributeNameToForm, toUpperCase } from "../util";
import { AdvancedSettings } from "./advanced/AdvancedSettings";
import { AuthenticationOverrides } from "./advanced/AuthenticationOverrides";
import { ClusteringPanel } from "./advanced/ClusteringPanel";
import { FineGrainOpenIdConnect } from "./advanced/FineGrainOpenIdConnect";
import { FineGrainSamlEndpointConfig } from "./advanced/FineGrainSamlEndpointConfig";
import { OpenIdConnectCompatibilityModes } from "./advanced/OpenIdConnectCompatibilityModes";
import type { SaveOptions } from "./ClientDetails";
import type { TFunction } from "i18next";
import { RevocationPanel } from "./advanced/RevocationPanel";
import { ClusteringPanel } from "./advanced/ClusteringPanel";
import type { FormFields, SaveOptions } from "./ClientDetails";
export const parseResult = (
result: GlobalRequestResult,
@ -55,7 +55,7 @@ export const AdvancedTab = ({ save, client }: AdvancedProps) => {
const { t } = useTranslation("clients");
const openIdConnect = "openid-connect";
const { setValue, control } = useFormContext();
const { setValue } = useFormContext();
const {
publicClient,
attributes,
@ -66,7 +66,7 @@ export const AdvancedTab = ({ save, client }: AdvancedProps) => {
const resetFields = (names: string[]) => {
for (const name of names) {
setValue(
convertAttributeNameToForm(`attributes.${name}`),
convertAttributeNameToForm<FormFields>(`attributes.${name}`),
attributes?.[name] || ""
);
}
@ -128,7 +128,6 @@ export const AdvancedTab = ({ save, client }: AdvancedProps) => {
{t("clients-help:openIdConnectCompatibilityModes")}
</Text>
<OpenIdConnectCompatibilityModes
control={control}
save={() => save()}
reset={() =>
resetFields(["exclude.session.state.from.auth.response"])
@ -146,7 +145,6 @@ export const AdvancedTab = ({ save, client }: AdvancedProps) => {
{t("clients-help:fineGrainSamlEndpointConfig")}
</Text>
<FineGrainSamlEndpointConfig
control={control}
save={() => save()}
reset={() =>
resetFields([
@ -178,7 +176,6 @@ export const AdvancedTab = ({ save, client }: AdvancedProps) => {
</Text>
<AdvancedSettings
protocol={protocol}
control={control}
save={() => save()}
reset={() => {
resetFields([
@ -201,7 +198,6 @@ export const AdvancedTab = ({ save, client }: AdvancedProps) => {
</Text>
<AuthenticationOverrides
protocol={protocol}
control={control}
save={() => save()}
reset={() => {
setValue(

View file

@ -1,12 +1,12 @@
import { Controller, useFormContext } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { FormGroup, Switch, ValidatedOptions } from "@patternfly/react-core";
import { Controller, useFormContext } from "react-hook-form-v7";
import { useTranslation } from "react-i18next";
import type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation";
import { HelpItem } from "../components/help-enabler/HelpItem";
import { FormAccess } from "../components/form-access/FormAccess";
import { KeycloakTextInput } from "../components/keycloak-text-input/KeycloakTextInput";
import { HelpItem } from "../components/help-enabler/HelpItem";
import { KeycloakTextArea } from "../components/keycloak-text-area/KeycloakTextArea";
import { KeycloakTextInput } from "../components/keycloak-text-input/KeycloakTextInput";
import { FormFields } from "./ClientDetails";
type ClientDescriptionProps = {
protocol?: string;
@ -21,7 +21,7 @@ export const ClientDescription = ({
register,
control,
formState: { errors },
} = useFormContext<ClientRepresentation>();
} = useFormContext<FormFields>();
return (
<FormAccess role="manage-clients" fineGrainedAccess={configure} unWrap>
<FormGroup
@ -37,11 +37,9 @@ export const ClientDescription = ({
isRequired
>
<KeycloakTextInput
ref={register({ required: true })}
type="text"
{...register("clientId", { required: true })}
id="kc-client-id"
data-testid="kc-client-id"
name="clientId"
validated={
errors.clientId ? ValidatedOptions.error : ValidatedOptions.default
}
@ -54,12 +52,7 @@ export const ClientDescription = ({
label={t("common:name")}
fieldId="kc-name"
>
<KeycloakTextInput
ref={register()}
type="text"
id="kc-name"
name="name"
/>
<KeycloakTextInput {...register("name")} id="kc-name" />
</FormGroup>
<FormGroup
labelIcon={
@ -76,15 +69,13 @@ export const ClientDescription = ({
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"
validated={
errors.description
? ValidatedOptions.error
@ -107,13 +98,13 @@ export const ClientDescription = ({
name="alwaysDisplayInConsole"
defaultValue={false}
control={control}
render={({ onChange, value }) => (
render={({ field }) => (
<Switch
id="kc-always-display-in-console-switch"
label={t("common:on")}
labelOff={t("common:off")}
isChecked={value}
onChange={onChange}
isChecked={field.value}
onChange={field.onChange}
aria-label={t("alwaysDisplayInConsole")}
/>
)}

View file

@ -1,3 +1,4 @@
import type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation";
import {
AlertVariant,
ButtonVariant,
@ -10,10 +11,14 @@ import {
Tooltip,
} from "@patternfly/react-core";
import { InfoCircleIcon } from "@patternfly/react-icons";
import type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation";
import { cloneDeep, sortBy } from "lodash-es";
import { useMemo, useState } from "react";
import { Controller, FormProvider, useForm, useWatch } from "react-hook-form";
import {
Controller,
FormProvider,
useForm,
useWatch,
} from "react-hook-form-v7";
import { useTranslation } from "react-i18next";
import { useHistory, useParams } from "react-router-dom";
import { useNavigate } from "react-router-dom-v5-compat";
@ -23,13 +28,21 @@ import {
useConfirmDialog,
} from "../components/confirm-dialog/ConfirmDialog";
import { DownloadDialog } from "../components/download-dialog/DownloadDialog";
import type { KeyValueType } from "../components/key-value-form/key-value-convert";
import { KeycloakSpinner } from "../components/keycloak-spinner/KeycloakSpinner";
import { PermissionsTab } from "../components/permission-tab/PermissionTab";
import {
routableTab,
RoutableTabs,
} from "../components/routable-tabs/RoutableTabs";
import {
ViewHeader,
ViewHeaderBadge,
} from "../components/view-header/ViewHeader";
import { KeycloakSpinner } from "../components/keycloak-spinner/KeycloakSpinner";
import { useAccess } from "../context/access/Access";
import { useAdminClient, useFetch } from "../context/auth/AdminClient";
import { useRealm } from "../context/realm-context/RealmContext";
import { useServerInfo } from "../context/server-info/ServerInfoProvider";
import { RolesList } from "../realm-roles/RolesList";
import {
convertAttributeNameToForm,
@ -39,41 +52,33 @@ import {
} from "../util";
import useToggle from "../utils/useToggle";
import { AdvancedTab } from "./AdvancedTab";
import { ClientSettings } from "./ClientSettings";
import { ClientSessions } from "./ClientSessions";
import { Credentials } from "./credentials/Credentials";
import { Keys } from "./keys/Keys";
import { ClientParams, ClientTab, toClient } from "./routes/Client";
import { toClients } from "./routes/Clients";
import { ClientScopes } from "./scopes/ClientScopes";
import { EvaluateScopes } from "./scopes/EvaluateScopes";
import { ServiceAccount } from "./service-account/ServiceAccount";
import { isRealmClient, getProtocolName } from "./utils";
import { SamlKeys } from "./keys/SamlKeys";
import { AuthorizationSettings } from "./authorization/Settings";
import { AuthorizationEvaluate } from "./authorization/AuthorizationEvaluate";
import { AuthorizationExport } from "./authorization/AuthorizationExport";
import { AuthorizationPermissions } from "./authorization/Permissions";
import { AuthorizationPolicies } from "./authorization/Policies";
import { AuthorizationResources } from "./authorization/Resources";
import { AuthorizationScopes } from "./authorization/Scopes";
import { AuthorizationPolicies } from "./authorization/Policies";
import { AuthorizationPermissions } from "./authorization/Permissions";
import { AuthorizationEvaluate } from "./authorization/AuthorizationEvaluate";
import {
routableTab,
RoutableTabs,
} from "../components/routable-tabs/RoutableTabs";
import { AuthorizationSettings } from "./authorization/Settings";
import { ClientSessions } from "./ClientSessions";
import { ClientSettings } from "./ClientSettings";
import { Credentials } from "./credentials/Credentials";
import { Keys } from "./keys/Keys";
import { SamlKeys } from "./keys/SamlKeys";
import {
AuthorizationTab,
toAuthorizationTab,
} from "./routes/AuthenticationTab";
import { ClientParams, ClientTab, toClient } from "./routes/Client";
import { toClients } from "./routes/Clients";
import { toClientScopesTab } from "./routes/ClientScopeTab";
import { AuthorizationExport } from "./authorization/AuthorizationExport";
import { useServerInfo } from "../context/server-info/ServerInfoProvider";
import { PermissionsTab } from "../components/permission-tab/PermissionTab";
import type { KeyValueType } from "../components/key-value-form/key-value-convert";
import { useAccess } from "../context/access/Access";
import { ClientScopes } from "./scopes/ClientScopes";
import { EvaluateScopes } from "./scopes/EvaluateScopes";
import { ServiceAccount } from "./service-account/ServiceAccount";
import { getProtocolName, isRealmClient } from "./utils";
type ClientDetailHeaderProps = {
onChange: (value: boolean) => void;
value: boolean;
value: boolean | undefined;
save: () => void;
client: ClientRepresentation;
toggleDownloadDialog: () => void;
@ -178,6 +183,11 @@ export type SaveOptions = {
messageKey?: string;
};
export type FormFields = Omit<
ClientRepresentation,
"authorizationSettings" | "resources"
>;
export default function ClientDetails() {
const { t } = useTranslation("clients");
const { adminClient } = useAdminClient();
@ -198,7 +208,7 @@ export default function ClientDetails() {
const [downloadDialogOpen, toggleDownloadDialogOpen] = useToggle();
const [changeAuthenticatorOpen, toggleChangeAuthenticatorOpen] = useToggle();
const form = useForm<ClientRepresentation>({ shouldUnregister: false });
const form = useForm<FormFields>({ shouldUnregister: false });
const { clientId } = useParams<ClientParams>();
const [key, setKey] = useState(0);
@ -237,6 +247,7 @@ export default function ClientDetails() {
if (client.attributes?.["acr.loa.map"]) {
form.setValue(
convertAttributeNameToForm("attributes.acr.loa.map"),
// @ts-ignore
Object.entries(JSON.parse(client.attributes["acr.loa.map"])).flatMap(
([key, value]) => ({ key, value })
)
@ -262,46 +273,48 @@ export default function ClientDetails() {
messageKey: "clientSaveSuccess",
}
) => {
if (await form.trigger()) {
if (
!client?.publicClient &&
client?.clientAuthenticatorType !== clientAuthenticatorType &&
!confirmed
) {
toggleChangeAuthenticatorOpen();
return;
}
if (!(await form.trigger())) {
return;
}
const values = convertFormValuesToObject(form.getValues());
if (
!client?.publicClient &&
client?.clientAuthenticatorType !== clientAuthenticatorType &&
!confirmed
) {
toggleChangeAuthenticatorOpen();
return;
}
const submittedClient =
convertFormValuesToObject<ClientRepresentation>(values);
const values = convertFormValuesToObject(form.getValues());
if (submittedClient.attributes?.["acr.loa.map"]) {
submittedClient.attributes["acr.loa.map"] = JSON.stringify(
Object.fromEntries(
(submittedClient.attributes["acr.loa.map"] as KeyValueType[])
.filter(({ key }) => key !== "")
.map(({ key, value }) => [key, value])
)
);
}
const submittedClient =
convertFormValuesToObject<ClientRepresentation>(values);
try {
const newClient: ClientRepresentation = {
...client,
...submittedClient,
};
if (submittedClient.attributes?.["acr.loa.map"]) {
submittedClient.attributes["acr.loa.map"] = JSON.stringify(
Object.fromEntries(
(submittedClient.attributes["acr.loa.map"] as KeyValueType[])
.filter(({ key }) => key !== "")
.map(({ key, value }) => [key, value])
)
);
}
newClient.clientId = newClient.clientId?.trim();
try {
const newClient: ClientRepresentation = {
...client,
...submittedClient,
};
await adminClient.clients.update({ id: clientId }, newClient);
setupForm(newClient);
setClient(newClient);
addAlert(t(messageKey), AlertVariant.success);
} catch (error) {
addError("clients:clientSaveError", error);
}
newClient.clientId = newClient.clientId?.trim();
await adminClient.clients.update({ id: clientId }, newClient);
setupForm(newClient);
setClient(newClient);
addAlert(t(messageKey), AlertVariant.success);
} catch (error) {
addError("clients:clientSaveError", error);
}
};
@ -360,10 +373,10 @@ export default function ClientDetails() {
name="enabled"
control={form.control}
defaultValue={true}
render={({ onChange, value }) => (
render={({ field }) => (
<ClientDetailHeader
value={value}
onChange={onChange}
value={field.value}
onChange={field.onChange}
client={client}
save={save}
toggleDeleteDialog={toggleDeleteDialog}

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 { Form } from "@patternfly/react-core";
import type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation";
@ -11,6 +11,7 @@ import { SamlSignature } from "./add/SamlSignature";
import { AccessSettings } from "./add/AccessSettings";
import { LoginSettingsPanel } from "./add/LoginSettingsPanel";
import { LogoutPanel } from "./add/LogoutPanel";
import { FormFields } from "./ClientDetails";
export type ClientSettingsProps = {
client: ClientRepresentation;
@ -21,7 +22,7 @@ export type ClientSettingsProps = {
export const ClientSettings = (props: ClientSettingsProps) => {
const { t } = useTranslation("clients");
const { watch } = useFormContext<ClientRepresentation>();
const { watch } = useFormContext<FormFields>();
const protocol = watch("protocol");
const { client } = props;

View file

@ -1,18 +1,18 @@
import { useTranslation } from "react-i18next";
import { useFormContext } from "react-hook-form";
import { FormGroup } from "@patternfly/react-core";
import { useFormContext } from "react-hook-form-v7";
import { useTranslation } from "react-i18next";
import type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation";
import type { ClientSettingsProps } from "../ClientSettings";
import { MultiLineInput } from "../../components/multi-line-input/MultiLineInput";
import { FormAccess } from "../../components/form-access/FormAccess";
import { HelpItem } from "../../components/help-enabler/HelpItem";
import { KeycloakTextInput } from "../../components/keycloak-text-input/KeycloakTextInput";
import { SaveReset } from "../advanced/SaveReset";
import environment from "../../environment";
import { useRealm } from "../../context/realm-context/RealmContext";
import { MultiLineInput } from "../../components/multi-line-input/hook-form-v7/MultiLineInput";
import { useAccess } from "../../context/access/Access";
import { useRealm } from "../../context/realm-context/RealmContext";
import environment from "../../environment";
import { convertAttributeNameToForm } from "../../util";
import { SaveReset } from "../advanced/SaveReset";
import { FormFields } from "../ClientDetails";
import type { ClientSettingsProps } from "../ClientSettings";
export const AccessSettings = ({
client,
@ -20,7 +20,7 @@ export const AccessSettings = ({
reset,
}: ClientSettingsProps) => {
const { t } = useTranslation("clients");
const { register, watch } = useFormContext<ClientRepresentation>();
const { register, watch } = useFormContext<FormFields>();
const { realm } = useRealm();
const { hasAccess } = useAccess();
@ -49,12 +49,7 @@ export const AccessSettings = ({
/>
}
>
<KeycloakTextInput
type="text"
id="kc-root-url"
name="rootUrl"
ref={register}
/>
<KeycloakTextInput id="kc-root-url" {...register("rootUrl")} />
</FormGroup>
<FormGroup
label={t("homeURL")}
@ -66,12 +61,7 @@ export const AccessSettings = ({
/>
}
>
<KeycloakTextInput
type="text"
id="kc-home-url"
name="baseUrl"
ref={register}
/>
<KeycloakTextInput id="kc-home-url" {...register("baseUrl")} />
</FormGroup>
<FormGroup
label={t("validRedirectUri")}
@ -127,11 +117,9 @@ export const AccessSettings = ({
}
>
<KeycloakTextInput
type="text"
id="idpInitiatedSsoUrlName"
name="attributes.saml_idp_initiated_sso_url_name"
data-testid="idpInitiatedSsoUrlName"
ref={register}
{...register("attributes.saml_idp_initiated_sso_url_name")}
/>
</FormGroup>
<FormGroup
@ -145,11 +133,9 @@ export const AccessSettings = ({
}
>
<KeycloakTextInput
type="text"
id="idpInitiatedSsoRelayState"
name="attributes.saml_idp_initiated_sso_relay_state"
data-testid="idpInitiatedSsoRelayState"
ref={register}
{...register("attributes.saml_idp_initiated_sso_relay_state")}
/>
</FormGroup>
<FormGroup
@ -163,11 +149,9 @@ export const AccessSettings = ({
}
>
<KeycloakTextInput
type="text"
id="masterSamlProcessingUrl"
name="adminUrl"
data-testid="masterSamlProcessingUrl"
ref={register}
{...register("adminUrl")}
/>
</FormGroup>
</>
@ -203,12 +187,7 @@ export const AccessSettings = ({
/>
}
>
<KeycloakTextInput
type="text"
id="kc-admin-url"
name="adminUrl"
ref={register}
/>
<KeycloakTextInput id="kc-admin-url" {...register("adminUrl")} />
</FormGroup>
)}
{client.bearerOnly && (

View file

@ -1,18 +1,19 @@
import { useTranslation } from "react-i18next";
import { Controller, useFormContext } from "react-hook-form";
import {
FormGroup,
Switch,
Checkbox,
FormGroup,
Grid,
GridItem,
InputGroup,
Switch,
} from "@patternfly/react-core";
import { Controller, useFormContext } from "react-hook-form-v7";
import { useTranslation } from "react-i18next";
import type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation";
import { FormAccess } from "../../components/form-access/FormAccess";
import { HelpItem } from "../../components/help-enabler/HelpItem";
import { convertAttributeNameToForm } from "../../util";
import { FormFields } from "../ClientDetails";
import "./capability-config.css";
@ -26,7 +27,7 @@ export const CapabilityConfig = ({
protocol: type,
}: CapabilityConfigProps) => {
const { t } = useTranslation("clients");
const { control, watch, setValue } = useFormContext<ClientRepresentation>();
const { control, watch, setValue } = useFormContext<FormFields>();
const protocol = type || watch("protocol");
const clientAuthentication = watch("publicClient");
const authorization = watch("authorizationServicesEnabled");
@ -56,21 +57,20 @@ export const CapabilityConfig = ({
name="publicClient"
defaultValue={false}
control={control}
render={({ onChange, value }) => (
render={({ field }) => (
<Switch
data-testid="authentication"
id="kc-authentication-switch"
name="publicClient"
label={t("common:on")}
labelOff={t("common:off")}
isChecked={!value}
isChecked={!field.value}
onChange={(value) => {
onChange(!value);
field.onChange(!value);
if (!value) {
setValue("authorizationServicesEnabled", false);
setValue("serviceAccountsEnabled", false);
setValue(
convertAttributeNameToForm(
convertAttributeNameToForm<FormFields>(
"attributes.oidc.ciba.grant.enabled"
),
false
@ -97,16 +97,15 @@ export const CapabilityConfig = ({
name="authorizationServicesEnabled"
defaultValue={false}
control={control}
render={({ onChange, value }) => (
render={({ field }) => (
<Switch
data-testid="authorization"
id="kc-authorization-switch"
name="authorizationServicesEnabled"
label={t("common:on")}
labelOff={t("common:off")}
isChecked={value && !clientAuthentication}
isChecked={field.value && !clientAuthentication}
onChange={(value) => {
onChange(value);
field.onChange(value);
if (value) {
setValue("serviceAccountsEnabled", true);
}
@ -128,15 +127,14 @@ export const CapabilityConfig = ({
name="standardFlowEnabled"
defaultValue={true}
control={control}
render={({ onChange, value }) => (
render={({ field }) => (
<InputGroup>
<Checkbox
data-testid="standard"
label={t("standardFlow")}
id="kc-flow-standard"
name="standardFlowEnabled"
isChecked={value.toString() === "true"}
onChange={onChange}
isChecked={field.value?.toString() === "true"}
onChange={field.onChange}
/>
<HelpItem
helpText="clients-help:standardFlow"
@ -151,15 +149,14 @@ export const CapabilityConfig = ({
name="directAccessGrantsEnabled"
defaultValue={true}
control={control}
render={({ onChange, value }) => (
render={({ field }) => (
<InputGroup>
<Checkbox
data-testid="direct"
label={t("directAccess")}
id="kc-flow-direct"
name="directAccessGrantsEnabled"
isChecked={value}
onChange={onChange}
isChecked={field.value}
onChange={field.onChange}
/>
<HelpItem
helpText="clients-help:directAccess"
@ -174,15 +171,14 @@ export const CapabilityConfig = ({
name="implicitFlowEnabled"
defaultValue={true}
control={control}
render={({ onChange, value }) => (
render={({ field }) => (
<InputGroup>
<Checkbox
data-testid="implicit"
label={t("implicitFlow")}
id="kc-flow-implicit"
name="implicitFlowEnabled"
isChecked={value.toString() === "true"}
onChange={onChange}
isChecked={field.value?.toString() === "true"}
onChange={field.onChange}
/>
<HelpItem
helpText="clients-help:implicitFlow"
@ -197,18 +193,17 @@ export const CapabilityConfig = ({
name="serviceAccountsEnabled"
defaultValue={false}
control={control}
render={({ onChange, value }) => (
render={({ field }) => (
<InputGroup>
<Checkbox
data-testid="service-account"
label={t("serviceAccount")}
id="kc-flow-service-account"
name="serviceAccountsEnabled"
isChecked={
value.toString() === "true" ||
field.value?.toString() === "true" ||
(clientAuthentication && authorization)
}
onChange={onChange}
onChange={field.onChange}
isDisabled={
(clientAuthentication && !authorization) ||
(!clientAuthentication && authorization)
@ -224,20 +219,20 @@ export const CapabilityConfig = ({
</GridItem>
<GridItem lg={8} sm={6}>
<Controller
name={convertAttributeNameToForm(
"attributes.oauth2.device.authorization.grant.enabled"
)}
name={convertAttributeNameToForm<
Required<ClientRepresentation["attributes"]>
>("attributes.oauth2.device.authorization.grant.enabled")}
defaultValue={false}
control={control}
render={({ onChange, value }) => (
render={({ field }) => (
<InputGroup>
<Checkbox
data-testid="oauth-device-authorization-grant"
label={t("oauthDeviceAuthorizationGrant")}
id="kc-oauth-device-authorization-grant"
name="oauth2.device.authorization.grant.enabled"
isChecked={value.toString() === "true"}
onChange={onChange}
isChecked={field.value.toString() === "true"}
onChange={field.onChange}
/>
<HelpItem
helpText="clients-help:oauthDeviceAuthorizationGrant"
@ -249,20 +244,20 @@ export const CapabilityConfig = ({
</GridItem>
<GridItem lg={8} sm={6}>
<Controller
name={convertAttributeNameToForm(
name={convertAttributeNameToForm<FormFields>(
"attributes.oidc.ciba.grant.enabled"
)}
defaultValue={false}
control={control}
render={({ onChange, value }) => (
render={({ field }) => (
<InputGroup>
<Checkbox
data-testid="oidc-ciba-grant"
label={t("oidcCibaGrant")}
id="kc-oidc-ciba-grant"
name="oidc.ciba.grant.enabled"
isChecked={value.toString() === "true"}
onChange={onChange}
isChecked={field.value.toString() === "true"}
onChange={field.onChange}
isDisabled={clientAuthentication}
/>
<HelpItem
@ -291,17 +286,19 @@ export const CapabilityConfig = ({
hasNoPaddingTop
>
<Controller
name={convertAttributeNameToForm("attributes.saml.encrypt")}
name={convertAttributeNameToForm<FormFields>(
"attributes.saml.encrypt"
)}
control={control}
defaultValue={false}
render={({ onChange, value }) => (
render={({ field }) => (
<Switch
data-testid="encrypt"
id="kc-encrypt"
label={t("common:on")}
labelOff={t("common:off")}
isChecked={value}
onChange={onChange}
isChecked={field.value}
onChange={field.onChange}
aria-label={t("encryptAssertions")}
/>
)}
@ -319,19 +316,19 @@ export const CapabilityConfig = ({
hasNoPaddingTop
>
<Controller
name={convertAttributeNameToForm(
name={convertAttributeNameToForm<FormFields>(
"attributes.saml.client.signature"
)}
control={control}
defaultValue={false}
render={({ onChange, value }) => (
render={({ field }) => (
<Switch
data-testid="client-signature"
id="kc-client-signature"
label={t("common:on")}
labelOff={t("common:off")}
isChecked={value}
onChange={onChange}
isChecked={field.value}
onChange={field.onChange}
aria-label={t("clientSignature")}
/>
)}

View file

@ -1,17 +1,17 @@
import { useState } from "react";
import {
FormGroup,
Select,
SelectVariant,
SelectOption,
SelectVariant,
} from "@patternfly/react-core";
import { useState } from "react";
import { Controller, useFormContext } from "react-hook-form-v7";
import { useTranslation } from "react-i18next";
import { Controller, useFormContext } from "react-hook-form";
import { useLoginProviders } from "../../context/server-info/ServerInfoProvider";
import { ClientDescription } from "../ClientDescription";
import { FormAccess } from "../../components/form-access/FormAccess";
import { HelpItem } from "../../components/help-enabler/HelpItem";
import { useLoginProviders } from "../../context/server-info/ServerInfoProvider";
import { ClientDescription } from "../ClientDescription";
import { getProtocolName } from "../utils";
export const GeneralSettings = () => {
@ -41,22 +41,22 @@ export const GeneralSettings = () => {
name="protocol"
defaultValue=""
control={control}
render={({ onChange, value }) => (
render={({ field }) => (
<Select
id="kc-type"
onToggle={isOpen}
onSelect={(_, value) => {
onChange(value.toString());
field.onChange(value.toString());
isOpen(false);
}}
selections={value}
selections={field.value}
variant={SelectVariant.single}
aria-label={t("selectEncryptionType")}
isOpen={open}
>
{providers.map((option) => (
<SelectOption
selected={option === value}
selected={option === field.value}
key={option}
value={option}
data-testid={`option-${option}`}

View file

@ -1,6 +1,3 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { Controller, useFormContext } from "react-hook-form";
import {
FormGroup,
Select,
@ -8,23 +5,28 @@ import {
SelectVariant,
Switch,
} from "@patternfly/react-core";
import { useState } from "react";
import { Controller, useFormContext } from "react-hook-form-v7";
import { useTranslation } from "react-i18next";
import type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation";
import { FormAccess } from "../../components/form-access/FormAccess";
import { HelpItem } from "../../components/help-enabler/HelpItem";
import { useServerInfo } from "../../context/server-info/ServerInfoProvider";
import { KeycloakTextArea } from "../../components/keycloak-text-area/KeycloakTextArea";
import { useServerInfo } from "../../context/server-info/ServerInfoProvider";
import { convertAttributeNameToForm } from "../../util";
import { FormFields } from "../ClientDetails";
export const LoginSettingsPanel = ({ access }: { access?: boolean }) => {
const { t } = useTranslation("clients");
const { register, control, watch } = useFormContext<ClientRepresentation>();
const { register, control, watch } = useFormContext<FormFields>();
const [loginThemeOpen, setLoginThemeOpen] = useState(false);
const loginThemes = useServerInfo().themes!["login"];
const consentRequired = watch("consentRequired");
const displayOnConsentScreen: string = watch(
convertAttributeNameToForm("attributes.display.on.consent.screen")
convertAttributeNameToForm<FormFields>(
"attributes.display.on.consent.screen"
)
);
return (
@ -43,15 +45,15 @@ export const LoginSettingsPanel = ({ access }: { access?: boolean }) => {
name="attributes.login_theme"
defaultValue=""
control={control}
render={({ onChange, value }) => (
render={({ field }) => (
<Select
toggleId="loginTheme"
onToggle={setLoginThemeOpen}
onSelect={(_, value) => {
onChange(value.toString());
field.onChange(value.toString());
setLoginThemeOpen(false);
}}
selections={value || t("common:choose")}
selections={field.value || t("common:choose")}
variant={SelectVariant.single}
aria-label={t("loginTheme")}
isOpen={loginThemeOpen}
@ -62,7 +64,7 @@ export const LoginSettingsPanel = ({ access }: { access?: boolean }) => {
</SelectOption>,
...loginThemes.map((theme) => (
<SelectOption
selected={theme.name === value}
selected={theme.name === field.value}
key={theme.name}
value={theme.name}
/>
@ -87,13 +89,13 @@ export const LoginSettingsPanel = ({ access }: { access?: boolean }) => {
name="consentRequired"
defaultValue={false}
control={control}
render={({ onChange, value }) => (
render={({ field }) => (
<Switch
id="kc-consent-switch"
label={t("common:on")}
labelOff={t("common:off")}
isChecked={value}
onChange={onChange}
isChecked={field.value}
onChange={field.onChange}
aria-label={t("consentRequired")}
/>
)}
@ -111,18 +113,18 @@ export const LoginSettingsPanel = ({ access }: { access?: boolean }) => {
hasNoPaddingTop
>
<Controller
name={convertAttributeNameToForm(
name={convertAttributeNameToForm<FormFields>(
"attributes.display.on.consent.screen"
)}
defaultValue={false}
control={control}
render={({ onChange, value }) => (
render={({ field }) => (
<Switch
id="kc-display-on-client-switch"
label={t("common:on")}
labelOff={t("common:off")}
isChecked={value === "true"}
onChange={(value) => onChange("" + value)}
isChecked={field.value === "true"}
onChange={(value) => field.onChange("" + value)}
isDisabled={!consentRequired}
aria-label={t("displayOnClient")}
/>
@ -141,8 +143,11 @@ export const LoginSettingsPanel = ({ access }: { access?: boolean }) => {
>
<KeycloakTextArea
id="kc-consent-screen-text"
name={convertAttributeNameToForm("attributes.consent.screen.text")}
ref={register}
{...register(
convertAttributeNameToForm<FormFields>(
"attributes.consent.screen.text"
)
)}
isDisabled={!(consentRequired && displayOnConsentScreen === "true")}
/>
</FormGroup>

View file

@ -1,15 +1,15 @@
import { useTranslation } from "react-i18next";
import { Controller, useFormContext } from "react-hook-form";
import { FormGroup, Switch, ValidatedOptions } from "@patternfly/react-core";
import { Controller, useFormContext } from "react-hook-form-v7";
import { useTranslation } from "react-i18next";
import type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation";
import type { ClientSettingsProps } from "../ClientSettings";
import { FormAccess } from "../../components/form-access/FormAccess";
import { HelpItem } from "../../components/help-enabler/HelpItem";
import { KeycloakTextInput } from "../../components/keycloak-text-input/KeycloakTextInput";
import { useAccess } from "../../context/access/Access";
import { beerify, convertAttributeNameToForm } from "../../util";
import { SaveReset } from "../advanced/SaveReset";
import { convertAttributeNameToForm } from "../../util";
import type { ClientSettingsProps } from "../ClientSettings";
import { FormFields } from "../ClientDetails";
export const LogoutPanel = ({
save,
@ -22,7 +22,7 @@ export const LogoutPanel = ({
control,
watch,
formState: { errors },
} = useFormContext<ClientRepresentation>();
} = useFormContext<FormFields>();
const { hasAccess } = useAccess();
const isManager = hasAccess("manage-clients") || access?.configure;
@ -51,13 +51,13 @@ export const LogoutPanel = ({
name="frontchannelLogout"
defaultValue={true}
control={control}
render={({ onChange, value }) => (
render={({ field }) => (
<Switch
id="kc-frontchannelLogout-switch"
label={t("common:on")}
labelOff={t("common:off")}
isChecked={value}
onChange={onChange}
isChecked={field.value}
onChange={field.onChange}
aria-label={t("frontchannelLogout")}
/>
)}
@ -74,29 +74,30 @@ export const LogoutPanel = ({
/>
}
helperTextInvalid={
errors.attributes?.frontchannel?.logout?.url?.message
errors.attributes?.[beerify("frontchannel.logout.url")]?.message
}
validated={
errors.attributes?.frontchannel?.logout?.url?.message
errors.attributes?.[beerify("frontchannel.logout.url")]?.message
? ValidatedOptions.error
: ValidatedOptions.default
}
>
<KeycloakTextInput
type="text"
id="frontchannelLogoutUrl"
name={convertAttributeNameToForm(
"attributes.frontchannel.logout.url"
{...register(
convertAttributeNameToForm<FormFields>(
"attributes.frontchannel.logout.url"
),
{
validate: (uri) =>
((uri.startsWith("https://") || uri.startsWith("http://")) &&
!uri.includes("*")) ||
uri === "" ||
t("frontchannelUrlInvalid").toString(),
}
)}
ref={register({
validate: (uri) =>
((uri.startsWith("https://") || uri.startsWith("http://")) &&
!uri.includes("*")) ||
uri === "" ||
t("frontchannelUrlInvalid").toString(),
})}
validated={
errors.attributes?.frontchannel?.logout?.url?.message
errors.attributes?.[beerify("frontchannel.logout.url")]?.message
? ValidatedOptions.error
: ValidatedOptions.default
}
@ -115,29 +116,31 @@ export const LogoutPanel = ({
/>
}
helperTextInvalid={
errors.attributes?.backchannel?.logout?.url?.message
errors.attributes?.[beerify("backchannel.logout.url")]?.message
}
validated={
errors.attributes?.backchannel?.logout?.url?.message
errors.attributes?.[beerify("backchannel.logout.url")]?.message
? ValidatedOptions.error
: ValidatedOptions.default
}
>
<KeycloakTextInput
type="text"
id="backchannelLogoutUrl"
name={convertAttributeNameToForm(
"attributes.backchannel.logout.url"
{...register(
convertAttributeNameToForm<FormFields>(
"attributes.backchannel.logout.url"
),
{
validate: (uri) =>
((uri.startsWith("https://") ||
uri.startsWith("http://")) &&
!uri.includes("*")) ||
uri === "" ||
t("backchannelUrlInvalid").toString(),
}
)}
ref={register({
validate: (uri) =>
((uri.startsWith("https://") || uri.startsWith("http://")) &&
!uri.includes("*")) ||
uri === "" ||
t("backchannelUrlInvalid").toString(),
})}
validated={
errors.attributes?.backchannel?.logout?.url?.message
errors.attributes?.[beerify("backchannel.logout.url")]?.message
? ValidatedOptions.error
: ValidatedOptions.default
}
@ -155,18 +158,18 @@ export const LogoutPanel = ({
hasNoPaddingTop
>
<Controller
name={convertAttributeNameToForm(
name={convertAttributeNameToForm<FormFields>(
"attributes.backchannel.logout.session.required"
)}
defaultValue="true"
control={control}
render={({ onChange, value }) => (
render={({ field }) => (
<Switch
id="backchannelLogoutSessionRequired"
label={t("common:on")}
labelOff={t("common:off")}
isChecked={value === "true"}
onChange={(value) => onChange(value.toString())}
isChecked={field.value === "true"}
onChange={(value) => field.onChange(value.toString())}
aria-label={t("backchannelLogoutSessionRequired")}
/>
)}
@ -184,18 +187,18 @@ export const LogoutPanel = ({
hasNoPaddingTop
>
<Controller
name={convertAttributeNameToForm(
name={convertAttributeNameToForm<FormFields>(
"attributes.backchannel.logout.revoke.offline.tokens"
)}
defaultValue="false"
control={control}
render={({ onChange, value }) => (
render={({ field }) => (
<Switch
id="backchannelLogoutRevokeOfflineSessions"
label={t("common:on")}
labelOff={t("common:off")}
isChecked={value === "true"}
onChange={(value) => onChange(value.toString())}
isChecked={field.value === "true"}
onChange={(value) => field.onChange(value.toString())}
aria-label={t("backchannelLogoutRevokeOfflineSessions")}
/>
)}

View file

@ -1,3 +1,4 @@
import type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation";
import {
AlertVariant,
Button,
@ -6,9 +7,8 @@ import {
WizardContextConsumer,
WizardFooter,
} from "@patternfly/react-core";
import type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation";
import { useState } from "react";
import { FormProvider, useForm } from "react-hook-form";
import { FormProvider, useForm } from "react-hook-form-v7";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom-v5-compat";
import { useAlerts } from "../../components/alert/Alerts";
@ -16,6 +16,7 @@ import { ViewHeader } from "../../components/view-header/ViewHeader";
import { useAdminClient } from "../../context/auth/AdminClient";
import { useRealm } from "../../context/realm-context/RealmContext";
import { convertFormValuesToObject } from "../../util";
import { FormFields } from "../ClientDetails";
import { toClient } from "../routes/Client";
import { toClients } from "../routes/Clients";
import { CapabilityConfig } from "./CapabilityConfig";
@ -42,7 +43,7 @@ export default function NewClientForm() {
frontchannelLogout: true,
});
const { addAlert, addError } = useAlerts();
const methods = useForm<ClientRepresentation>({ defaultValues: client });
const methods = useForm<FormFields>({ defaultValues: client });
const protocol = methods.watch("protocol");
const save = async () => {

View file

@ -1,6 +1,3 @@
import { useState } from "react";
import { Controller, useFormContext } from "react-hook-form";
import { useTranslation } from "react-i18next";
import {
FormGroup,
Select,
@ -8,15 +5,27 @@ import {
SelectVariant,
Switch,
} from "@patternfly/react-core";
import { useState } from "react";
import {
Controller,
Path,
PathValue,
useFormContext,
} from "react-hook-form-v7";
import { useTranslation } from "react-i18next";
import type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation";
import { FormAccess } from "../../components/form-access/FormAccess";
import { HelpItem } from "../../components/help-enabler/HelpItem";
import { convertAttributeNameToForm } from "../../util";
import { FormFields } from "../ClientDetails";
export const Toggle = ({ name, label }: { name: string; label: string }) => {
type ToggleProps = {
name: PathValue<FormFields, Path<FormFields>>;
label: string;
};
export const Toggle = ({ name, label }: ToggleProps) => {
const { t } = useTranslation("clients");
const { control } = useFormContext<ClientRepresentation>();
const { control } = useFormContext<FormFields>();
return (
<FormGroup
@ -34,14 +43,14 @@ export const Toggle = ({ name, label }: { name: string; label: string }) => {
name={name}
defaultValue="false"
control={control}
render={({ onChange, value }) => (
render={({ field }) => (
<Switch
id={name!}
data-testid={label}
label={t("common:on")}
labelOff={t("common:off")}
isChecked={value === "true"}
onChange={(value) => onChange(value.toString())}
isChecked={field.value === "true"}
onChange={(value) => field.onChange(value.toString())}
aria-label={t(label)}
/>
)}
@ -52,7 +61,7 @@ export const Toggle = ({ name, label }: { name: string; label: string }) => {
export const SamlConfig = () => {
const { t } = useTranslation("clients");
const { control } = useFormContext<ClientRepresentation>();
const { control } = useFormContext<FormFields>();
const [nameFormatOpen, setNameFormatOpen] = useState(false);
return (
@ -75,22 +84,22 @@ export const SamlConfig = () => {
name="attributes.saml_name_id_format"
defaultValue="username"
control={control}
render={({ onChange, value }) => (
render={({ field }) => (
<Select
toggleId="samlNameIdFormat"
onToggle={setNameFormatOpen}
onSelect={(_, value) => {
onChange(value.toString());
field.onChange(value.toString());
setNameFormatOpen(false);
}}
selections={value}
selections={field.value}
variant={SelectVariant.single}
aria-label={t("nameIdFormat")}
isOpen={nameFormatOpen}
>
{["username", "email", "transient", "persistent"].map((name) => (
<SelectOption
selected={name === value}
selected={name === field.value}
key={name}
value={name}
/>

View file

@ -1,17 +1,17 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { Controller, useFormContext } from "react-hook-form";
import {
FormGroup,
Select,
SelectOption,
SelectVariant,
} from "@patternfly/react-core";
import { useState } from "react";
import { Controller, useFormContext } from "react-hook-form-v7";
import { useTranslation } from "react-i18next";
import type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation";
import { convertAttributeNameToForm } from "../../util";
import { FormAccess } from "../../components/form-access/FormAccess";
import { HelpItem } from "../../components/help-enabler/HelpItem";
import { convertAttributeNameToForm } from "../../util";
import { FormFields } from "../ClientDetails";
import { Toggle } from "./SamlConfig";
const SIGNATURE_ALGORITHMS = [
@ -47,13 +47,15 @@ export const SamlSignature = () => {
const [keyOpen, setKeyOpen] = useState(false);
const [canOpen, setCanOpen] = useState(false);
const { control, watch } = useFormContext<ClientRepresentation>();
const { control, watch } = useFormContext<FormFields>();
const signDocs = watch(
convertAttributeNameToForm("attributes.saml.server.signature")
convertAttributeNameToForm<FormFields>("attributes.saml.server.signature")
);
const signAssertion = watch(
convertAttributeNameToForm("attributes.saml.assertion.signature")
convertAttributeNameToForm<FormFields>(
"attributes.saml.assertion.signature"
)
);
return (
@ -83,28 +85,27 @@ export const SamlSignature = () => {
}
>
<Controller
name={convertAttributeNameToForm(
name={convertAttributeNameToForm<FormFields>(
"attributes.saml.signature.algorithm"
)}
defaultValue={SIGNATURE_ALGORITHMS[0]}
Key
control={control}
render={({ onChange, value }) => (
render={({ field }) => (
<Select
toggleId="signatureAlgorithm"
onToggle={setAlgOpen}
onSelect={(_, value) => {
onChange(value.toString());
field.onChange(value.toString());
setAlgOpen(false);
}}
selections={value}
selections={field.value}
variant={SelectVariant.single}
aria-label={t("signatureAlgorithm")}
isOpen={algOpen}
>
{SIGNATURE_ALGORITHMS.map((algorithm) => (
<SelectOption
selected={algorithm === value}
selected={algorithm === field.value}
key={algorithm}
value={algorithm}
/>
@ -124,27 +125,27 @@ export const SamlSignature = () => {
}
>
<Controller
name={convertAttributeNameToForm(
"attributes.saml.server.signature.keyinfo$xmlSigKeyInfoKeyNameTransformer"
name={convertAttributeNameToForm<FormFields>(
"attributes.saml.server.signature.keyinfo.xmlSigKeyInfoKeyNameTransformer"
)}
defaultValue={KEYNAME_TRANSFORMER[0]}
control={control}
render={({ onChange, value }) => (
render={({ field }) => (
<Select
toggleId="signatureKeyName"
onToggle={setKeyOpen}
onSelect={(_, value) => {
onChange(value.toString());
field.onChange(value.toString());
setKeyOpen(false);
}}
selections={value}
selections={field.value}
variant={SelectVariant.single}
aria-label={t("signatureKeyName")}
isOpen={keyOpen}
>
{KEYNAME_TRANSFORMER.map((key) => (
<SelectOption
selected={key === value}
selected={key === field.value}
key={key}
value={key}
/>
@ -167,16 +168,17 @@ export const SamlSignature = () => {
name="attributes.saml_signature_canonicalization_method"
defaultValue={CANONICALIZATION[0].value}
control={control}
render={({ onChange, value }) => (
render={({ field }) => (
<Select
toggleId="canonicalization"
onToggle={setCanOpen}
onSelect={(_, value) => {
onChange(value.toString());
field.onChange(value.toString());
setCanOpen(false);
}}
selections={
CANONICALIZATION.find((can) => can.value === value)?.name
CANONICALIZATION.find((can) => can.value === field.value)
?.name
}
variant={SelectVariant.single}
aria-label={t("canonicalization")}
@ -184,7 +186,7 @@ export const SamlSignature = () => {
>
{CANONICALIZATION.map((can) => (
<SelectOption
selected={can.value === value}
selected={can.value === field.value}
key={can.name}
value={can.value}
>

View file

@ -1,6 +1,3 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { Control, Controller } from "react-hook-form";
import {
ActionGroup,
Button,
@ -10,17 +7,20 @@ import {
SelectVariant,
Switch,
} from "@patternfly/react-core";
import { useState } from "react";
import { Controller, useFormContext } from "react-hook-form-v7";
import { useTranslation } from "react-i18next";
import { FormAccess } from "../../components/form-access/FormAccess";
import { HelpItem } from "../../components/help-enabler/HelpItem";
import { KeyValueInput } from "../../components/key-value-form/hook-form-v7/KeyValueInput";
import { MultiLineInput } from "../../components/multi-line-input/hook-form-v7/MultiLineInput";
import { TimeSelector } from "../../components/time-selector/TimeSelector";
import { TokenLifespan } from "./TokenLifespan";
import { KeyValueInput } from "../../components/key-value-form/KeyValueInput";
import { MultiLineInput } from "../../components/multi-line-input/MultiLineInput";
import { convertAttributeNameToForm } from "../../util";
import { FormFields } from "../ClientDetails";
import { TokenLifespan } from "./TokenLifespan";
type AdvancedSettingsProps = {
control: Control<Record<string, any>>;
save: () => void;
reset: () => void;
protocol?: string;
@ -28,7 +28,6 @@ type AdvancedSettingsProps = {
};
export const AdvancedSettings = ({
control,
save,
reset,
protocol,
@ -36,6 +35,8 @@ export const AdvancedSettings = ({
}: AdvancedSettingsProps) => {
const { t } = useTranslation("clients");
const [open, setOpen] = useState(false);
const { control, register } = useFormContext();
return (
<FormAccess
role="manage-realm"
@ -54,16 +55,16 @@ export const AdvancedSettings = ({
}
>
<Controller
name={convertAttributeNameToForm(
name={convertAttributeNameToForm<FormFields>(
"attributes.saml.assertion.lifespan"
)}
defaultValue=""
control={control}
render={({ onChange, value }) => (
render={({ field }) => (
<TimeSelector
units={["minute", "day", "hour"]}
value={value}
onChange={onChange}
value={field.value}
onChange={field.onChange}
/>
)}
/>
@ -78,7 +79,6 @@ export const AdvancedSettings = ({
)}
defaultValue=""
units={["minute", "day", "hour"]}
control={control}
/>
<TokenLifespan
@ -88,7 +88,6 @@ export const AdvancedSettings = ({
)}
defaultValue=""
units={["minute", "day", "hour"]}
control={control}
/>
<TokenLifespan
@ -98,7 +97,6 @@ export const AdvancedSettings = ({
)}
defaultValue=""
units={["minute", "day", "hour"]}
control={control}
/>
<TokenLifespan
@ -108,7 +106,6 @@ export const AdvancedSettings = ({
)}
defaultValue=""
units={["minute", "day", "hour"]}
control={control}
/>
<TokenLifespan
@ -118,7 +115,6 @@ export const AdvancedSettings = ({
)}
defaultValue=""
units={["minute", "day", "hour"]}
control={control}
/>
<FormGroup
@ -136,13 +132,13 @@ export const AdvancedSettings = ({
name="attributes.tls-client-certificate-bound-access-tokens"
defaultValue={false}
control={control}
render={({ onChange, value }) => (
render={({ field }) => (
<Switch
id="oAuthMutual-switch"
label={t("common:on")}
labelOff={t("common:off")}
isChecked={value === "true"}
onChange={(value) => onChange("" + value)}
isChecked={field.value === "true"}
onChange={(value) => field.onChange("" + value)}
aria-label={t("oAuthMutual")}
/>
)}
@ -160,22 +156,22 @@ export const AdvancedSettings = ({
}
>
<Controller
name={convertAttributeNameToForm(
name={convertAttributeNameToForm<FormFields>(
"attributes.pkce.code.challenge.method"
)}
defaultValue=""
control={control}
render={({ onChange, value }) => (
render={({ field }) => (
<Select
toggleId="keyForCodeExchange"
variant={SelectVariant.single}
onToggle={setOpen}
isOpen={open}
onSelect={(_, value) => {
onChange(value);
field.onChange(value);
setOpen(false);
}}
selections={[value || t("common:choose")]}
selections={[field.value || t("common:choose")]}
>
{["", "S256", "plain"].map((v) => (
<SelectOption key={v} value={v}>
@ -197,18 +193,18 @@ export const AdvancedSettings = ({
}
>
<Controller
name={convertAttributeNameToForm(
name={convertAttributeNameToForm<FormFields>(
"attributes.require.pushed.authorization.requests"
)}
defaultValue="false"
control={control}
render={({ onChange, value }) => (
render={({ field }) => (
<Switch
id="pushedAuthorizationRequestRequired"
label={t("common:on")}
labelOff={t("common:off")}
isChecked={value === "true"}
onChange={(value) => onChange(value.toString())}
isChecked={field.value === "true"}
onChange={(value) => field.onChange(value.toString())}
aria-label={t("pushedAuthorizationRequestRequired")}
/>
)}
@ -225,7 +221,9 @@ export const AdvancedSettings = ({
}
>
<KeyValueInput
name={convertAttributeNameToForm("attributes.acr.loa.map")}
{...register(
convertAttributeNameToForm("attributes.acr.loa.map")
)}
/>
</FormGroup>
<FormGroup

View file

@ -1,10 +1,11 @@
import { useTranslation } from "react-i18next";
import { useFormContext } from "react-hook-form";
import { FormGroup } from "@patternfly/react-core";
import { useFormContext } from "react-hook-form-v7";
import { useTranslation } from "react-i18next";
import { HelpItem } from "../../components/help-enabler/HelpItem";
import { KeycloakTextInput } from "../../components/keycloak-text-input/KeycloakTextInput";
import { convertAttributeNameToForm } from "../../util";
import { FormFields } from "../ClientDetails";
export const ApplicationUrls = () => {
const { t } = useTranslation("clients");
@ -23,11 +24,11 @@ export const ApplicationUrls = () => {
}
>
<KeycloakTextInput
type="text"
id="logoUrl"
name={convertAttributeNameToForm("attributes.logoUri")}
data-testid="logoUrl"
ref={register}
{...register(
convertAttributeNameToForm<FormFields>("attributes.logoUri")
)}
/>
</FormGroup>
<FormGroup
@ -41,11 +42,11 @@ export const ApplicationUrls = () => {
}
>
<KeycloakTextInput
type="text"
id="policyUrl"
name={convertAttributeNameToForm("attributes.policyUri")}
data-testid="policyUrl"
ref={register}
{...register(
convertAttributeNameToForm<FormFields>("attributes.policyUri")
)}
/>
</FormGroup>
<FormGroup
@ -59,11 +60,11 @@ export const ApplicationUrls = () => {
}
>
<KeycloakTextInput
type="text"
id="termsOfServiceUrl"
name={convertAttributeNameToForm("attributes.tosUri")}
data-testid="termsOfServiceUrl"
ref={register}
{...register(
convertAttributeNameToForm<FormFields>("attributes.tosUri")
)}
/>
</FormGroup>
</>

View file

@ -1,7 +1,3 @@
import { useState } from "react";
import { Control, Controller } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { sortBy } from "lodash-es";
import {
ActionGroup,
Button,
@ -10,13 +6,16 @@ import {
SelectOption,
SelectVariant,
} from "@patternfly/react-core";
import { sortBy } from "lodash-es";
import { useState } from "react";
import { Controller, useFormContext } from "react-hook-form-v7";
import { useTranslation } from "react-i18next";
import { FormAccess } from "../../components/form-access/FormAccess";
import { HelpItem } from "../../components/help-enabler/HelpItem";
import { useFetch, useAdminClient } from "../../context/auth/AdminClient";
import { useAdminClient, useFetch } from "../../context/auth/AdminClient";
type AuthenticationOverridesProps = {
control: Control<Record<string, any>>;
save: () => void;
reset: () => void;
protocol?: string;
@ -25,7 +24,6 @@ type AuthenticationOverridesProps = {
export const AuthenticationOverrides = ({
protocol,
control,
save,
reset,
hasConfigureAccess,
@ -36,6 +34,8 @@ export const AuthenticationOverrides = ({
const [browserFlowOpen, setBrowserFlowOpen] = useState(false);
const [directGrantOpen, setDirectGrantOpen] = useState(false);
const { control } = useFormContext();
useFetch(
() => adminClient.authenticationManagement.getFlows(),
(flows) => {
@ -77,17 +77,17 @@ export const AuthenticationOverrides = ({
name="authenticationFlowBindingOverrides.browser"
defaultValue=""
control={control}
render={({ onChange, value }) => (
render={({ field }) => (
<Select
toggleId="browserFlow"
variant={SelectVariant.single}
onToggle={setBrowserFlowOpen}
isOpen={browserFlowOpen}
onSelect={(_, value) => {
onChange(value);
field.onChange(value);
setBrowserFlowOpen(false);
}}
selections={[value]}
selections={[field.value]}
>
{flows}
</Select>
@ -109,17 +109,17 @@ export const AuthenticationOverrides = ({
name="authenticationFlowBindingOverrides.direct_grant"
defaultValue=""
control={control}
render={({ onChange, value }) => (
render={({ field }) => (
<Select
toggleId="directGrant"
variant={SelectVariant.single}
onToggle={setDirectGrantOpen}
isOpen={directGrantOpen}
onSelect={(_, value) => {
onChange(value);
field.onChange(value);
setDirectGrantOpen(false);
}}
selections={[value]}
selections={[field.value]}
>
{flows}
</Select>

View file

@ -1,6 +1,3 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { Controller, useFormContext } from "react-hook-form";
import {
AlertVariant,
Button,
@ -11,8 +8,10 @@ import {
SplitItem,
ToolbarItem,
} from "@patternfly/react-core";
import { useState } from "react";
import { Controller, useFormContext } from "react-hook-form-v7";
import { useTranslation } from "react-i18next";
import { AdvancedProps, parseResult } from "../AdvancedTab";
import { useAlerts } from "../../components/alert/Alerts";
import { useConfirmDialog } from "../../components/confirm-dialog/ConfirmDialog";
import { FormAccess } from "../../components/form-access/FormAccess";
@ -21,8 +20,9 @@ import { ListEmptyState } from "../../components/list-empty-state/ListEmptyState
import { KeycloakDataTable } from "../../components/table-toolbar/KeycloakDataTable";
import { TimeSelector } from "../../components/time-selector/TimeSelector";
import { useAdminClient } from "../../context/auth/AdminClient";
import { AddHostDialog } from ".././advanced/AddHostDialog";
import useFormatDate, { FORMAT_DATE_AND_TIME } from "../../utils/useFormatDate";
import { AddHostDialog } from ".././advanced/AddHostDialog";
import { AdvancedProps, parseResult } from "../AdvancedTab";
export const ClusteringPanel = ({
save,
@ -98,8 +98,8 @@ export const ClusteringPanel = ({
name="nodeReRegistrationTimeout"
defaultValue=""
control={control}
render={({ onChange, value }) => (
<TimeSelector value={value} onChange={onChange} />
render={({ field }) => (
<TimeSelector value={field.value} onChange={field.onChange} />
)}
/>
</SplitItem>

View file

@ -1,6 +1,3 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { Controller, useFormContext } from "react-hook-form";
import {
ActionGroup,
Button,
@ -9,12 +6,16 @@ import {
SelectOption,
SelectVariant,
} from "@patternfly/react-core";
import { useState } from "react";
import { Controller, useFormContext } from "react-hook-form-v7";
import { useTranslation } from "react-i18next";
import { FormAccess } from "../../components/form-access/FormAccess";
import { HelpItem } from "../../components/help-enabler/HelpItem";
import { MultiLineInput } from "../../components/multi-line-input/hook-form-v7/MultiLineInput";
import { useServerInfo } from "../../context/server-info/ServerInfoProvider";
import { convertAttributeNameToForm, sortProviders } from "../../util";
import { MultiLineInput } from "../../components/multi-line-input/MultiLineInput";
import { FormFields } from "../ClientDetails";
import { ApplicationUrls } from "./ApplicationUrls";
type FineGrainOpenIdConnectProps = {
@ -160,22 +161,22 @@ export const FineGrainOpenIdConnect = ({
}
>
<Controller
name={convertAttributeNameToForm(
name={convertAttributeNameToForm<FormFields>(
"attributes.access.token.signed.response.alg"
)}
defaultValue=""
control={control}
render={({ onChange, value }) => (
render={({ field }) => (
<Select
toggleId="accessTokenSignatureAlgorithm"
variant={SelectVariant.single}
onToggle={setAccessTokenOpen}
isOpen={accessTokenOpen}
onSelect={(_, value) => {
onChange(value);
field.onChange(value);
setAccessTokenOpen(false);
}}
selections={value}
selections={field.value}
>
{keyOptions}
</Select>
@ -193,22 +194,22 @@ export const FineGrainOpenIdConnect = ({
}
>
<Controller
name={convertAttributeNameToForm(
name={convertAttributeNameToForm<FormFields>(
"attributes.id.token.signed.response.alg"
)}
defaultValue=""
control={control}
render={({ onChange, value }) => (
render={({ field }) => (
<Select
toggleId="idTokenSignatureAlgorithm"
variant={SelectVariant.single}
onToggle={setIdTokenOpen}
isOpen={idTokenOpen}
onSelect={(_, value) => {
onChange(value);
field.onChange(value);
setIdTokenOpen(false);
}}
selections={value}
selections={field.value}
>
{keyOptions}
</Select>
@ -226,22 +227,22 @@ export const FineGrainOpenIdConnect = ({
}
>
<Controller
name={convertAttributeNameToForm(
name={convertAttributeNameToForm<FormFields>(
"attributes.id.token.encrypted.response.alg"
)}
defaultValue=""
control={control}
render={({ onChange, value }) => (
render={({ field }) => (
<Select
toggleId="idTokenEncryptionKeyManagementAlgorithm"
variant={SelectVariant.single}
onToggle={setIdTokenKeyManagementOpen}
isOpen={idTokenKeyManagementOpen}
onSelect={(_, value) => {
onChange(value);
field.onChange(value);
setIdTokenKeyManagementOpen(false);
}}
selections={value}
selections={field.value}
>
{cekManagementOptions}
</Select>
@ -259,22 +260,22 @@ export const FineGrainOpenIdConnect = ({
}
>
<Controller
name={convertAttributeNameToForm(
name={convertAttributeNameToForm<FormFields>(
"attributes.id.token.encrypted.response.enc"
)}
defaultValue=""
control={control}
render={({ onChange, value }) => (
render={({ field }) => (
<Select
toggleId="idTokenEncryptionContentEncryptionAlgorithm"
variant={SelectVariant.single}
onToggle={setIdTokenContentOpen}
isOpen={idTokenContentOpen}
onSelect={(_, value) => {
onChange(value);
field.onChange(value);
setIdTokenContentOpen(false);
}}
selections={value}
selections={field.value}
>
{contentOptions}
</Select>
@ -292,22 +293,22 @@ export const FineGrainOpenIdConnect = ({
}
>
<Controller
name={convertAttributeNameToForm(
name={convertAttributeNameToForm<FormFields>(
"attributes.user.info.response.signature.alg"
)}
defaultValue=""
control={control}
render={({ onChange, value }) => (
render={({ field }) => (
<Select
toggleId="userInfoSignedResponseAlgorithm"
variant={SelectVariant.single}
onToggle={setUserInfoSignedResponseOpen}
isOpen={userInfoSignedResponseOpen}
onSelect={(_, value) => {
onChange(value);
field.onChange(value);
setUserInfoSignedResponseOpen(false);
}}
selections={value}
selections={field.value}
>
{signatureOptions}
</Select>
@ -325,22 +326,22 @@ export const FineGrainOpenIdConnect = ({
}
>
<Controller
name={convertAttributeNameToForm(
name={convertAttributeNameToForm<FormFields>(
"attributes.request.object.signature.alg"
)}
defaultValue=""
control={control}
render={({ onChange, value }) => (
render={({ field }) => (
<Select
toggleId="requestObjectSignatureAlgorithm"
variant={SelectVariant.single}
onToggle={setRequestObjectSignatureOpen}
isOpen={requestObjectSignatureOpen}
onSelect={(_, value) => {
onChange(value);
field.onChange(value);
setRequestObjectSignatureOpen(false);
}}
selections={value}
selections={field.value}
>
{requestObjectOptions}
</Select>
@ -358,22 +359,22 @@ export const FineGrainOpenIdConnect = ({
}
>
<Controller
name={convertAttributeNameToForm(
name={convertAttributeNameToForm<FormFields>(
"attributes.request.object.encryption.alg"
)}
defaultValue=""
control={control}
render={({ onChange, value }) => (
render={({ field }) => (
<Select
toggleId="requestObjectEncryption"
variant={SelectVariant.single}
onToggle={setRequestObjectEncryptionOpen}
isOpen={requestObjectEncryptionOpen}
onSelect={(_, value) => {
onChange(value);
field.onChange(value);
setRequestObjectEncryptionOpen(false);
}}
selections={value}
selections={field.value}
>
{requestObjectEncryptionOptions}
</Select>
@ -391,22 +392,22 @@ export const FineGrainOpenIdConnect = ({
}
>
<Controller
name={convertAttributeNameToForm(
name={convertAttributeNameToForm<FormFields>(
"attributes.request.object.encryption.enc"
)}
defaultValue=""
control={control}
render={({ onChange, value }) => (
render={({ field }) => (
<Select
toggleId="requestObjectEncoding"
variant={SelectVariant.single}
onToggle={setRequestObjectEncodingOpen}
isOpen={requestObjectEncodingOpen}
onSelect={(_, value) => {
onChange(value);
field.onChange(value);
setRequestObjectEncodingOpen(false);
}}
selections={value}
selections={field.value}
>
{requestObjectEncodingOptions}
</Select>
@ -424,22 +425,22 @@ export const FineGrainOpenIdConnect = ({
}
>
<Controller
name={convertAttributeNameToForm(
name={convertAttributeNameToForm<FormFields>(
"attributes.request.object.required"
)}
defaultValue=""
control={control}
render={({ onChange, value }) => (
render={({ field }) => (
<Select
toggleId="requestObjectRequired"
variant={SelectVariant.single}
onToggle={setRequestObjectRequiredOpen}
isOpen={requestObjectRequiredOpen}
onSelect={(_, value) => {
onChange(value);
field.onChange(value);
setRequestObjectRequiredOpen(false);
}}
selections={value}
selections={field.value}
>
{requestObjectRequiredOptions}
</Select>
@ -474,22 +475,22 @@ export const FineGrainOpenIdConnect = ({
}
>
<Controller
name={convertAttributeNameToForm(
name={convertAttributeNameToForm<FormFields>(
"attributes.authorization.signed.response.alg"
)}
defaultValue=""
control={control}
render={({ onChange, value }) => (
render={({ field }) => (
<Select
toggleId="authorizationSignedResponseAlg"
variant={SelectVariant.single}
onToggle={setAuthorizationSignedOpen}
isOpen={authorizationSignedOpen}
onSelect={(_, value) => {
onChange(value);
field.onChange(value);
setAuthorizationSignedOpen(false);
}}
selections={value}
selections={field.value}
>
{authorizationSignedResponseOptions}
</Select>
@ -507,22 +508,22 @@ export const FineGrainOpenIdConnect = ({
}
>
<Controller
name={convertAttributeNameToForm(
name={convertAttributeNameToForm<FormFields>(
"attributes.authorization.encrypted.response.alg"
)}
defaultValue=""
control={control}
render={({ onChange, value }) => (
render={({ field }) => (
<Select
toggleId="authorizationEncryptedResponseAlg"
variant={SelectVariant.single}
onToggle={setAuthorizationEncryptedOpen}
isOpen={authorizationEncryptedOpen}
onSelect={(_, value) => {
onChange(value);
field.onChange(value);
setAuthorizationEncryptedOpen(false);
}}
selections={value}
selections={field.value}
>
{cekManagementOptions}
</Select>
@ -540,22 +541,22 @@ export const FineGrainOpenIdConnect = ({
}
>
<Controller
name={convertAttributeNameToForm(
name={convertAttributeNameToForm<FormFields>(
"attributes.authorization.encrypted.response.enc"
)}
defaultValue=""
control={control}
render={({ onChange, value }) => (
render={({ field }) => (
<Select
toggleId="authorizationEncryptedResponseEnc"
variant={SelectVariant.single}
onToggle={setAuthorizationEncryptedResponseOpen}
isOpen={authorizationEncryptedResponseOpen}
onSelect={(_, value) => {
onChange(value);
field.onChange(value);
setAuthorizationEncryptedResponseOpen(false);
}}
selections={value}
selections={field.value}
>
{contentOptions}
</Select>

View file

@ -1,6 +1,6 @@
import { useTranslation } from "react-i18next";
import type { Control } from "react-hook-form";
import { ActionGroup, Button, FormGroup } from "@patternfly/react-core";
import { useFormContext } from "react-hook-form-v7";
import { useTranslation } from "react-i18next";
import { FormAccess } from "../../components/form-access/FormAccess";
import { HelpItem } from "../../components/help-enabler/HelpItem";
@ -8,17 +8,16 @@ import { KeycloakTextInput } from "../../components/keycloak-text-input/Keycloak
import { ApplicationUrls } from "./ApplicationUrls";
type FineGrainSamlEndpointConfigProps = {
control: Control<Record<string, any>>;
save: () => void;
reset: () => void;
};
export const FineGrainSamlEndpointConfig = ({
control: { register },
save,
reset,
}: FineGrainSamlEndpointConfigProps) => {
const { t } = useTranslation("clients");
const { register } = useFormContext();
return (
<FormAccess role="manage-realm" isHorizontal>
<ApplicationUrls />
@ -33,10 +32,8 @@ export const FineGrainSamlEndpointConfig = ({
}
>
<KeycloakTextInput
ref={register()}
type="text"
id="assertionConsumerServicePostBindingURL"
name="attributes.saml_assertion_consumer_url_post"
{...register("attributes.saml_assertion_consumer_url_post")}
/>
</FormGroup>
<FormGroup
@ -50,10 +47,8 @@ export const FineGrainSamlEndpointConfig = ({
}
>
<KeycloakTextInput
ref={register()}
type="text"
id="assertionConsumerServiceRedirectBindingURL"
name="attributes.saml_assertion_consumer_url_redirect"
{...register("attributes.saml_assertion_consumer_url_redirect")}
/>
</FormGroup>
<FormGroup
@ -67,10 +62,8 @@ export const FineGrainSamlEndpointConfig = ({
}
>
<KeycloakTextInput
ref={register()}
type="text"
id="logoutServicePostBindingURL"
name="attributes.saml_single_logout_service_url_post"
{...register("attributes.saml_single_logout_service_url_post")}
/>
</FormGroup>
<FormGroup
@ -84,10 +77,8 @@ export const FineGrainSamlEndpointConfig = ({
}
>
<KeycloakTextInput
ref={register()}
type="text"
id="logoutServiceRedirectBindingURL"
name="attributes.saml_single_logout_service_url_redirect"
{...register("attributes.saml_single_logout_service_url_redirect")}
/>
</FormGroup>
<FormGroup
@ -101,10 +92,8 @@ export const FineGrainSamlEndpointConfig = ({
}
>
<KeycloakTextInput
ref={register()}
type="text"
id="logoutServiceArtifactBindingUrl"
name="attributes.saml_single_logout_service_url_artifact"
{...register("attributes.saml_single_logout_service_url_artifact")}
/>
</FormGroup>
<FormGroup
@ -118,10 +107,8 @@ export const FineGrainSamlEndpointConfig = ({
}
>
<KeycloakTextInput
ref={register()}
type="text"
id="artifactBindingUrl"
name="attributes.saml_artifact_binding_url"
{...register("attributes.saml_artifact_binding_url")}
/>
</FormGroup>
<FormGroup
@ -135,10 +122,8 @@ export const FineGrainSamlEndpointConfig = ({
}
>
<KeycloakTextInput
ref={register()}
type="text"
id="artifactResolutionService"
name="attributes.saml_artifact_resolution_service_url"
{...register("attributes.saml_artifact_resolution_service_url")}
/>
</FormGroup>

View file

@ -1,25 +1,25 @@
import { useTranslation } from "react-i18next";
import { Control, Controller } from "react-hook-form";
import { ActionGroup, Button, FormGroup, Switch } from "@patternfly/react-core";
import { Controller, useFormContext } from "react-hook-form-v7";
import { useTranslation } from "react-i18next";
import { FormAccess } from "../../components/form-access/FormAccess";
import { HelpItem } from "../../components/help-enabler/HelpItem";
import { convertAttributeNameToForm } from "../../util";
import { FormFields } from "../ClientDetails";
type OpenIdConnectCompatibilityModesProps = {
control: Control<Record<string, any>>;
save: () => void;
reset: () => void;
hasConfigureAccess?: boolean;
};
export const OpenIdConnectCompatibilityModes = ({
control,
save,
reset,
hasConfigureAccess,
}: OpenIdConnectCompatibilityModesProps) => {
const { t } = useTranslation("clients");
const { control } = useFormContext();
return (
<FormAccess
role="manage-realm"
@ -38,18 +38,18 @@ export const OpenIdConnectCompatibilityModes = ({
}
>
<Controller
name={convertAttributeNameToForm(
name={convertAttributeNameToForm<FormFields>(
"attributes.exclude.session.state.from.auth.response"
)}
defaultValue=""
control={control}
render={({ onChange, value }) => (
render={({ field }) => (
<Switch
id="excludeSessionStateFromAuthenticationResponse-switch"
label={t("common:on")}
labelOff={t("common:off")}
isChecked={value === "true"}
onChange={(value) => onChange(value.toString())}
isChecked={field.value === "true"}
onChange={(value) => field.onChange(value.toString())}
aria-label={t("excludeSessionStateFromAuthenticationResponse")}
/>
)}
@ -67,16 +67,18 @@ export const OpenIdConnectCompatibilityModes = ({
}
>
<Controller
name={convertAttributeNameToForm("attributes.use.refresh.tokens")}
name={convertAttributeNameToForm<FormFields>(
"attributes.use.refresh.tokens"
)}
defaultValue="true"
control={control}
render={({ onChange, value }) => (
render={({ field }) => (
<Switch
id="useRefreshTokens"
label={t("common:on")}
labelOff={t("common:off")}
isChecked={value === "true"}
onChange={(value) => onChange(value.toString())}
isChecked={field.value === "true"}
onChange={(value) => field.onChange(value.toString())}
aria-label={t("useRefreshTokens")}
/>
)}
@ -94,18 +96,18 @@ export const OpenIdConnectCompatibilityModes = ({
}
>
<Controller
name={convertAttributeNameToForm(
name={convertAttributeNameToForm<FormFields>(
"attributes.client_credentials.use_refresh_token"
)}
defaultValue="false"
control={control}
render={({ onChange, value }) => (
render={({ field }) => (
<Switch
id="useRefreshTokenForClientCredentialsGrant"
label={t("common:on")}
labelOff={t("common:off")}
isChecked={value === "true"}
onChange={(value) => onChange(value.toString())}
isChecked={field.value === "true"}
onChange={(value) => field.onChange(value.toString())}
aria-label={t("useRefreshTokenForClientCredentialsGrant")}
/>
)}
@ -123,18 +125,18 @@ export const OpenIdConnectCompatibilityModes = ({
}
>
<Controller
name={convertAttributeNameToForm(
name={convertAttributeNameToForm<FormFields>(
"attributes.token.response.type.bearer.lower-case"
)}
defaultValue="false"
control={control}
render={({ onChange, value }) => (
render={({ field }) => (
<Switch
id="useLowerCaseBearerType"
label={t("common:on")}
labelOff={t("common:off")}
isChecked={value === "true"}
onChange={(value) => onChange(value.toString())}
isChecked={field.value === "true"}
onChange={(value) => field.onChange(value.toString())}
aria-label={t("useLowerCaseBearerType")}
/>
)}

View file

@ -1,25 +1,25 @@
import { useEffect, useRef } from "react";
import { Link } from "react-router-dom-v5-compat";
import { Trans, useTranslation } from "react-i18next";
import { useFormContext } from "react-hook-form";
import {
ActionGroup,
Button,
FormGroup,
InputGroup,
Button,
ActionGroup,
Tooltip,
Text,
Tooltip,
} from "@patternfly/react-core";
import { useEffect, useRef } from "react";
import { useFormContext } from "react-hook-form-v7";
import { Trans, useTranslation } from "react-i18next";
import { Link } from "react-router-dom-v5-compat";
import { AdvancedProps, parseResult } from "../AdvancedTab";
import { useAlerts } from "../../components/alert/Alerts";
import { FormAccess } from "../../components/form-access/FormAccess";
import { HelpItem } from "../../components/help-enabler/HelpItem";
import { KeycloakTextInput } from "../../components/keycloak-text-input/KeycloakTextInput";
import { useAdminClient } from "../../context/auth/AdminClient";
import { useRealm } from "../../context/realm-context/RealmContext";
import { toClient } from "../routes/Client";
import useFormatDate, { FORMAT_DATE_AND_TIME } from "../../utils/useFormatDate";
import { AdvancedProps, parseResult } from "../AdvancedTab";
import { toClient } from "../routes/Client";
export const RevocationPanel = ({
save,

View file

@ -1,5 +1,5 @@
import { useState } from "react";
import { Control, Controller, FieldValues } from "react-hook-form";
import { Controller, useFormContext } from "react-hook-form-v7";
import { useTranslation } from "react-i18next";
import {
FormGroup,
@ -20,7 +20,6 @@ type TokenLifespanProps = {
id: string;
name: string;
defaultValue: string;
control: Control<FieldValues>;
units?: Unit[];
};
@ -31,7 +30,6 @@ export const TokenLifespan = ({
id,
name,
defaultValue,
control,
units,
}: TokenLifespanProps) => {
const { t } = useTranslation("clients");
@ -41,6 +39,7 @@ export const TokenLifespan = ({
const onFocus = () => setFocused(true);
const onBlur = () => setFocused(false);
const { control } = useFormContext();
const isExpireSet = (value: string | number) =>
(typeof value === "number" && value !== -1) ||
(typeof value === "string" && value !== "" && value !== "-1") ||
@ -61,7 +60,7 @@ export const TokenLifespan = ({
name={name}
defaultValue={defaultValue}
control={control}
render={({ onChange, value }) => (
render={({ field }) => (
<Split hasGutter>
<SplitItem>
<Select
@ -69,21 +68,21 @@ export const TokenLifespan = ({
onToggle={setOpen}
isOpen={open}
onSelect={(_, value) => {
onChange(value);
field.onChange(value);
setOpen(false);
}}
selections={[isExpireSet(value) ? t(expires) : t(never)]}
selections={[isExpireSet(field.value) ? t(expires) : t(never)]}
>
<SelectOption value={-1}>{t(never)}</SelectOption>
<SelectOption value={60}>{t(expires)}</SelectOption>
</Select>
</SplitItem>
<SplitItem>
{isExpireSet(value) && (
{isExpireSet(field.value) && (
<TimeSelector
units={units}
value={value}
onChange={onChange}
value={field.value}
onChange={field.onChange}
onFocus={onFocus}
onBlur={onBlur}
min={1}

View file

@ -1,40 +1,41 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { Controller, FormProvider, useForm } from "react-hook-form";
import {
FormGroup,
Select,
SelectVariant,
SelectOption,
PageSection,
ActionGroup,
Button,
Switch,
ExpandableSection,
FormGroup,
PageSection,
Select,
SelectOption,
SelectVariant,
Switch,
} from "@patternfly/react-core";
import { useState } from "react";
import { Controller, FormProvider, useForm } from "react-hook-form-v7";
import { useTranslation } from "react-i18next";
import type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation";
import type RoleRepresentation from "@keycloak/keycloak-admin-client/lib/defs/roleRepresentation";
import type EvaluationResultRepresentation from "@keycloak/keycloak-admin-client/lib/defs/evaluationResultRepresentation";
import type PolicyEvaluationResponse from "@keycloak/keycloak-admin-client/lib/defs/policyEvaluationResponse";
import type ResourceEvaluation from "@keycloak/keycloak-admin-client/lib/defs/resourceEvaluation";
import type ResourceRepresentation from "@keycloak/keycloak-admin-client/lib/defs/resourceRepresentation";
import type RoleRepresentation from "@keycloak/keycloak-admin-client/lib/defs/roleRepresentation";
import type ScopeRepresentation from "@keycloak/keycloak-admin-client/lib/defs/scopeRepresentation";
import type PolicyEvaluationResponse from "@keycloak/keycloak-admin-client/lib/defs/policyEvaluationResponse";
import type { KeyValueType } from "../../components/key-value-form/key-value-convert";
import { KeycloakTextInput } from "../../components/keycloak-text-input/KeycloakTextInput";
import { ClientSelect } from "../../components/client/ClientSelect";
import { FormAccess } from "../../components/form-access/FormAccess";
import { HelpItem } from "../../components/help-enabler/HelpItem";
import type { KeyValueType } from "../../components/key-value-form/key-value-convert";
import { KeycloakTextInput } from "../../components/keycloak-text-input/KeycloakTextInput";
import { FormPanel } from "../../components/scroll-form/FormPanel";
import { UserSelect } from "../../components/users/UserSelect";
import { useAccess } from "../../context/access/Access";
import { useAdminClient, useFetch } from "../../context/auth/AdminClient";
import { useRealm } from "../../context/realm-context/RealmContext";
import { KeyBasedAttributeInput } from "./KeyBasedAttributeInput";
import { defaultContextAttributes } from "../utils";
import { useAccess } from "../../context/access/Access";
import { ForbiddenSection } from "../../ForbiddenSection";
import { FormFields } from "../ClientDetails";
import { defaultContextAttributes } from "../utils";
import { Results } from "./evaluate/Results";
import { ClientSelect } from "../../components/client/ClientSelect";
import { UserSelect } from "../../components/users/UserSelect";
import { KeyBasedAttributeInput } from "./KeyBasedAttributeInput";
import "./auth-evaluate.css";
@ -46,7 +47,7 @@ interface EvaluateFormInputs
attributes: Record<string, string>[];
};
resources?: Record<string, string>[];
client: ClientRepresentation;
client: FormFields;
user: string[];
}
@ -82,9 +83,8 @@ export const AuthorizationEvaluate = ({ client }: Props) => {
control,
register,
reset,
errors,
trigger,
formState: { isValid },
formState: { isValid, errors },
} = form;
const { t } = useTranslation("clients");
const { adminClient } = useAdminClient();
@ -210,37 +210,37 @@ export const AuthorizationEvaluate = ({ client }: Props) => {
>
<Controller
name="roleIds"
placeholderText={t("selectARole")}
control={control}
defaultValue={[]}
rules={{ validate: (value) => value.length > 0 }}
render={({ onChange, value }) => (
rules={{ validate: (value) => (value || "").length > 0 }}
render={({ field }) => (
<Select
placeholderText={t("selectARole")}
variant={SelectVariant.typeaheadMulti}
toggleId="role"
onToggle={setRoleDropdownOpen}
selections={value}
selections={field.value}
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]);
}
setRoleDropdownOpen(false);
}}
onClear={(event) => {
event.stopPropagation();
onChange([]);
field.onChange([]);
}}
aria-label={t("realmRole")}
isOpen={roleDropdownOpen}
>
{clientRoles.map((role) => (
<SelectOption
selected={role.name === value}
selected={role.name === field.value}
key={role.name}
value={role.name}
/>
@ -308,15 +308,13 @@ export const AuthorizationEvaluate = ({ client }: Props) => {
/>
}
fieldId="client"
validated={form.errors.alias ? "error" : "default"}
validated={errors.alias ? "error" : "default"}
helperTextInvalid={t("common:required")}
>
<KeycloakTextInput
type="text"
id="alias"
name="alias"
data-testid="alias"
ref={register({ required: true })}
{...register("alias", { required: true })}
/>
</FormGroup>
<FormGroup
@ -333,29 +331,31 @@ export const AuthorizationEvaluate = ({ client }: Props) => {
name="authScopes"
defaultValue={[]}
control={control}
render={({ onChange, value }) => (
render={({ field }) => (
<Select
toggleId="authScopes"
onToggle={setScopesDropdownOpen}
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]);
}
setScopesDropdownOpen(false);
}}
selections={value}
selections={field.value}
variant={SelectVariant.typeaheadMulti}
aria-label={t("authScopes")}
isOpen={scopesDropdownOpen}
>
{scopes.map((scope) => (
<SelectOption
selected={scope.name === value}
selected={field.value.includes(scope.name!)}
key={scope.id}
value={scope.name}
/>

View file

@ -1,6 +1,6 @@
import { useTranslation } from "react-i18next";
import { Controller, useFormContext } from "react-hook-form";
import { FormGroup, Radio } from "@patternfly/react-core";
import { Controller, useFormContext } from "react-hook-form";
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={({ onChange, value }) => (
render={(field) => (
<>
{(isLimited
? DECISION_STRATEGY.slice(0, 2)
@ -45,9 +45,9 @@ export const DecisionStrategySelect = ({
id={strategy}
key={strategy}
data-testid={strategy}
isChecked={value === strategy}
isChecked={field.value === strategy}
name="decisionStrategy"
onChange={() => onChange(strategy)}
onChange={() => field.onChange(strategy)}
label={t(`decisionStrategies.${strategy}`)}
className="pf-u-mb-md"
/>

View file

@ -1,7 +1,4 @@
import { useState } from "react";
import { Link, useNavigate } from "react-router-dom-v5-compat";
import { useTranslation } from "react-i18next";
import { FormProvider, useForm } from "react-hook-form";
import { Language } from "@patternfly/react-code-editor";
import {
ActionGroup,
AlertVariant,
@ -9,14 +6,18 @@ import {
FormGroup,
PageSection,
} from "@patternfly/react-core";
import { Language } from "@patternfly/react-code-editor";
import { useState } from "react";
import { FormProvider, useForm } from "react-hook-form-v7";
import { useTranslation } from "react-i18next";
import { Link, useNavigate } from "react-router-dom-v5-compat";
import type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation";
import { useAlerts } from "../../components/alert/Alerts";
import { FormAccess } from "../../components/form-access/FormAccess";
import { ViewHeader } from "../../components/view-header/ViewHeader";
import { FileUploadForm } from "../../components/json-file-upload/FileUploadForm";
import { KeycloakTextInput } from "../../components/keycloak-text-input/KeycloakTextInput";
import { ViewHeader } from "../../components/view-header/ViewHeader";
import { useAdminClient } from "../../context/auth/AdminClient";
import { useRealm } from "../../context/realm-context/RealmContext";
import {
@ -24,12 +25,12 @@ import {
convertFormValuesToObject,
convertToFormValues,
} from "../../util";
import { getAuthorizationHeaders } from "../../utils/getAuthorizationHeaders";
import { CapabilityConfig } from "../add/CapabilityConfig";
import { ClientDescription } from "../ClientDescription";
import { FormFields } from "../ClientDetails";
import { toClient } from "../routes/Client";
import { toClients } from "../routes/Clients";
import { FileUploadForm } from "../../components/json-file-upload/FileUploadForm";
import { getAuthorizationHeaders } from "../../utils/getAuthorizationHeaders";
const isXml = (text: string) => text.startsWith("<");
@ -38,7 +39,7 @@ export default function ImportForm() {
const navigate = useNavigate();
const { adminClient } = useAdminClient();
const { realm } = useRealm();
const form = useForm<ClientRepresentation>({ shouldUnregister: false });
const form = useForm<FormFields>({ shouldUnregister: false });
const { register, handleSubmit, setValue } = form;
const [imported, setImported] = useState<ClientRepresentation>({});
@ -118,11 +119,9 @@ export default function ImportForm() {
<ClientDescription hasConfigureAccess />
<FormGroup label={t("common:type")} fieldId="kc-type">
<KeycloakTextInput
type="text"
id="kc-type"
name="protocol"
isReadOnly
ref={register()}
{...register("protocol")}
/>
</FormGroup>
<CapabilityConfig unWrap={true} />

View file

@ -1,6 +1,3 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { saveAs } from "file-saver";
import {
ActionGroup,
AlertVariant,
@ -15,21 +12,24 @@ import {
Text,
TextContent,
} from "@patternfly/react-core";
import { saveAs } from "file-saver";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation";
import type CertificateRepresentation from "@keycloak/keycloak-admin-client/lib/defs/certificateRepresentation";
import type KeyStoreConfig from "@keycloak/keycloak-admin-client/lib/defs/keystoreConfig";
import { HelpItem } from "../../components/help-enabler/HelpItem";
import { FormAccess } from "../../components/form-access/FormAccess";
import { KeycloakTextInput } from "../../components/keycloak-text-input/KeycloakTextInput";
import { Controller, useFormContext, useWatch } from "react-hook-form";
import { GenerateKeyDialog, getFileExtension } from "./GenerateKeyDialog";
import { useFetch, useAdminClient } from "../../context/auth/AdminClient";
import { Controller, useFormContext, useWatch } from "react-hook-form-v7";
import { useAlerts } from "../../components/alert/Alerts";
import useToggle from "../../utils/useToggle";
import { FormAccess } from "../../components/form-access/FormAccess";
import { HelpItem } from "../../components/help-enabler/HelpItem";
import { KeycloakTextInput } from "../../components/keycloak-text-input/KeycloakTextInput";
import { useAdminClient, useFetch } from "../../context/auth/AdminClient";
import { convertAttributeNameToForm } from "../../util";
import { ImportKeyDialog, ImportFile } from "./ImportKeyDialog";
import useToggle from "../../utils/useToggle";
import { FormFields } from "../ClientDetails";
import { Certificate } from "./Certificate";
import { GenerateKeyDialog, getFileExtension } from "./GenerateKeyDialog";
import { ImportFile, ImportKeyDialog } from "./ImportKeyDialog";
type KeysProps = {
save: () => void;
@ -46,7 +46,7 @@ export const Keys = ({ clientId, save, hasConfigureAccess }: KeysProps) => {
register,
getValues,
formState: { isDirty },
} = useFormContext<ClientRepresentation>();
} = useFormContext<FormFields>();
const { adminClient } = useAdminClient();
const { addAlert, addError } = useAlerts();
@ -149,16 +149,15 @@ export const Keys = ({ clientId, save, hasConfigureAccess }: KeysProps) => {
>
<Controller
name={convertAttributeNameToForm("attributes.use.jwks.url")}
defaultValue="false"
control={control}
render={({ onChange, value }) => (
render={({ field }) => (
<Switch
data-testid="useJwksUrl"
id="useJwksUrl-switch"
label={t("common:on")}
labelOff={t("common:off")}
isChecked={value === "true"}
onChange={(value) => onChange(`${value}`)}
isChecked={field.value === "true"}
onChange={(value) => field.onChange(`${value}`)}
aria-label={t("useJwksUrl")}
/>
)}
@ -182,10 +181,10 @@ export const Keys = ({ clientId, save, hasConfigureAccess }: KeysProps) => {
}
>
<KeycloakTextInput
type="text"
id="jwksUrl"
name={convertAttributeNameToForm("attributes.jwks.url")}
ref={register}
{...register(
convertAttributeNameToForm("attributes.jwks.url")
)}
/>
</FormGroup>
)}

View file

@ -1,35 +1,35 @@
import { Fragment, useState } from "react";
import { saveAs } from "file-saver";
import { useTranslation } from "react-i18next";
import { Controller, useFormContext } from "react-hook-form";
import {
CardBody,
PageSection,
TextContent,
Text,
FormGroup,
Switch,
Card,
Form,
ActionGroup,
Button,
AlertVariant,
Button,
Card,
CardBody,
Form,
FormGroup,
PageSection,
Switch,
Text,
TextContent,
} from "@patternfly/react-core";
import { saveAs } from "file-saver";
import { Fragment, useState } from "react";
import { Controller, useFormContext } from "react-hook-form-v7";
import { useTranslation } from "react-i18next";
import type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation";
import type CertificateRepresentation from "@keycloak/keycloak-admin-client/lib/defs/certificateRepresentation";
import { useAdminClient, useFetch } from "../../context/auth/AdminClient";
import { useAlerts } from "../../components/alert/Alerts";
import { useConfirmDialog } from "../../components/confirm-dialog/ConfirmDialog";
import { FormAccess } from "../../components/form-access/FormAccess";
import { HelpItem } from "../../components/help-enabler/HelpItem";
import { SamlKeysDialog } from "./SamlKeysDialog";
import { FormPanel } from "../../components/scroll-form/FormPanel";
import { Certificate } from "./Certificate";
import { useConfirmDialog } from "../../components/confirm-dialog/ConfirmDialog";
import { useAlerts } from "../../components/alert/Alerts";
import { SamlImportKeyDialog } from "./SamlImportKeyDialog";
import { useAdminClient, useFetch } from "../../context/auth/AdminClient";
import { convertAttributeNameToForm } from "../../util";
import useToggle from "../../utils/useToggle";
import { FormFields } from "../ClientDetails";
import { Certificate } from "./Certificate";
import { ExportSamlKeyDialog } from "./ExportSamlKeyDialog";
import { SamlImportKeyDialog } from "./SamlImportKeyDialog";
import { SamlKeysDialog } from "./SamlKeysDialog";
type SamlKeysProps = {
clientId: string;
@ -70,14 +70,14 @@ const KeySection = ({
onImport,
}: KeySectionProps) => {
const { t } = useTranslation("clients");
const { control, watch } = useFormContext<ClientRepresentation>();
const { control, watch } = useFormContext<FormFields>();
const title = KEYS_MAPPING[attr].title;
const key = KEYS_MAPPING[attr].key;
const name = KEYS_MAPPING[attr].name;
const [showImportDialog, toggleImportDialog] = useToggle();
const section = watch(name);
const section = watch(name as keyof FormFields);
return (
<>
{showImportDialog && (
@ -100,21 +100,21 @@ const KeySection = ({
hasNoPaddingTop
>
<Controller
name={name}
name={name as keyof FormFields}
control={control}
defaultValue="false"
render={({ onChange, value }) => (
render={({ field }) => (
<Switch
data-testid={key}
id={key}
label={t("common:on")}
labelOff={t("common:off")}
isChecked={value === "true"}
isChecked={field.value === "true"}
onChange={(value) => {
const v = value.toString();
if (v === "true") {
onChanged(attr);
onChange(v);
field.onChange(v);
} else {
onGenerate(attr, false);
}

View file

@ -0,0 +1,115 @@
import {
ActionList,
ActionListItem,
Button,
Flex,
FlexItem,
} from "@patternfly/react-core";
import { MinusCircleIcon, PlusCircleIcon } from "@patternfly/react-icons";
import { useEffect } from "react";
import { useFieldArray, useFormContext, useWatch } from "react-hook-form-v7";
import { useTranslation } from "react-i18next";
import { KeycloakTextInput } from "../../keycloak-text-input/KeycloakTextInput";
import { KeyValueType } from "../key-value-convert";
type KeyValueInputProps = {
name: string;
};
export const KeyValueInput = ({ name }: KeyValueInputProps) => {
const { t } = useTranslation("common");
const { control, register } = useFormContext();
const { fields, append, remove } = useFieldArray({
control,
name,
});
const watchFields = useWatch({
control,
name,
defaultValue: [{ key: "", value: "" }],
});
const isValid =
Array.isArray(watchFields) &&
watchFields.every(
({ key, value }: KeyValueType) =>
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
key?.trim().length !== 0 && value?.trim().length !== 0
);
useEffect(() => {
if (!fields.length) {
append({ key: "", value: "" }, { shouldFocus: false });
}
}, [fields]);
return (
<>
<Flex direction={{ default: "column" }}>
<Flex>
<FlexItem
grow={{ default: "grow" }}
spacer={{ default: "spacerNone" }}
>
<strong>{t("key")}</strong>
</FlexItem>
<FlexItem grow={{ default: "grow" }}>
<strong>{t("value")}</strong>
</FlexItem>
</Flex>
{fields.map((attribute, index) => (
<Flex key={attribute.id} data-testid="row">
<FlexItem grow={{ default: "grow" }}>
<KeycloakTextInput
placeholder={t("keyPlaceholder")}
aria-label={t("key")}
defaultValue=""
data-testid={`${name}[${index}].key`}
{...register(`${name}[${index}].key`)}
/>
</FlexItem>
<FlexItem
grow={{ default: "grow" }}
spacer={{ default: "spacerNone" }}
>
<KeycloakTextInput
placeholder={t("valuePlaceholder")}
aria-label={t("value")}
defaultValue=""
data-testid={`${name}[${index}].value`}
{...register(`${name}[${index}].value`)}
/>
</FlexItem>
<FlexItem>
<Button
variant="link"
title={t("removeAttribute")}
isDisabled={watchFields.length === 1}
onClick={() => remove(index)}
data-testid={`${name}[${index}].remove`}
>
<MinusCircleIcon />
</Button>
</FlexItem>
</Flex>
))}
</Flex>
<ActionList>
<ActionListItem>
<Button
data-testid={`${name}-add-row`}
className="pf-u-px-0 pf-u-mt-sm"
variant="link"
icon={<PlusCircleIcon />}
isDisabled={!isValid}
onClick={() => append({ key: "", value: "" })}
>
{t("addAttribute")}
</Button>
</ActionListItem>
</ActionList>
</>
);
};

View file

@ -1,3 +1,5 @@
import { Path, PathValue } from "react-hook-form-v7";
export type KeyValueType = { key: string; value: string };
export function keyValueToArray(attributeArray: KeyValueType[] = []) {
@ -15,10 +17,10 @@ export function keyValueToArray(attributeArray: KeyValueType[] = []) {
return result;
}
export function arrayToKeyValue(attributes: Record<string, string[]> = {}) {
export function arrayToKeyValue<T>(attributes: Record<string, string[]> = {}) {
const result = Object.entries(attributes).flatMap(([key, value]) =>
value.map<KeyValueType>((value) => ({ key, value }))
);
return result.concat({ key: "", value: "" });
return result.concat({ key: "", value: "" }) as PathValue<T, Path<T>>;
}

View file

@ -0,0 +1,124 @@
import {
Button,
ButtonVariant,
InputGroup,
TextInput,
TextInputProps,
} from "@patternfly/react-core";
import { MinusCircleIcon, PlusCircleIcon } from "@patternfly/react-icons";
import { Fragment, useEffect, useState } from "react";
import { useFormContext } from "react-hook-form-v7";
import { useTranslation } from "react-i18next";
function stringToMultiline(value?: string): string[] {
return value ? value.split("##") : [];
}
function toStringValue(formValue: string[]): string {
return formValue.join("##");
}
type IdValue = {
id: number;
value: string;
};
const generateId = () => Math.floor(Math.random() * 1000);
export type MultiLineInputProps = Omit<TextInputProps, "form"> & {
name: string;
addButtonLabel?: string;
isDisabled?: boolean;
defaultValue?: string[];
stringify?: boolean;
};
export const MultiLineInput = ({
name,
addButtonLabel,
isDisabled = false,
defaultValue,
stringify = false,
...rest
}: MultiLineInputProps) => {
const { t } = useTranslation();
const { register, setValue, getValues } = useFormContext();
const [fields, setFields] = useState<IdValue[]>([]);
const remove = (index: number) => {
update([...fields.slice(0, index), ...fields.slice(index + 1)]);
};
const append = () => {
update([...fields, { id: generateId(), value: "" }]);
};
const updateValue = (index: number, value: string) => {
update([
...fields.slice(0, index),
{ ...fields[index], value },
...fields.slice(index + 1),
]);
};
const update = (values: IdValue[]) => {
setFields(values);
const fieldValue = values.flatMap((field) => field.value);
setValue(name, stringify ? toStringValue(fieldValue) : fieldValue);
};
useEffect(() => {
register(name);
let values = stringify
? stringToMultiline(getValues(name))
: getValues(name);
values =
Array.isArray(values) && values.length !== 0
? values
: defaultValue || [""];
setFields(values.map((value: string) => ({ value, id: generateId() })));
}, [register, getValues]);
return (
<>
{fields.map(({ id, value }, index) => (
<Fragment key={id}>
<InputGroup>
<TextInput
data-testid={name + index}
onChange={(value) => updateValue(index, value)}
name={`${name}[${index}].value`}
value={value}
isDisabled={isDisabled}
{...rest}
/>
<Button
variant={ButtonVariant.link}
onClick={() => remove(index)}
tabIndex={-1}
aria-label={t("common:remove")}
isDisabled={fields.length === 1}
>
<MinusCircleIcon />
</Button>
</InputGroup>
{index === fields.length - 1 && (
<Button
variant={ButtonVariant.link}
onClick={append}
tabIndex={-1}
aria-label={t("common:add")}
data-testid="addValue"
isDisabled={!value}
>
<PlusCircleIcon /> {t(addButtonLabel || "common:add")}
</Button>
)}
</Fragment>
))}
</>
);
};

View file

@ -53,7 +53,7 @@ export const EventsTab = ({ realm }: EventsTabProps) => {
const setupForm = (eventConfig?: EventsConfigForm) => {
setEvents(eventConfig);
convertToFormValues(eventConfig, setValue);
convertToFormValues(eventConfig || {}, setValue);
};
const clear = async (type: EventsType) => {

View file

@ -17,6 +17,7 @@ import {
import {
arrayToKeyValue,
keyValueToArray,
KeyValueType,
} from "../components/key-value-form/key-value-convert";
import { useAdminClient } from "../context/auth/AdminClient";
import { useUserProfile } from "../realm-settings/user-profile/UserProfileContext";
@ -34,8 +35,9 @@ export const UserAttributes = ({ user: defaultUser }: UserAttributesProps) => {
const { config } = useUserProfile();
const convertAttributes = () => {
return arrayToKeyValue(user.attributes!).filter(
(a) => !config?.attributes?.some((attribute) => attribute.name === a.key)
return arrayToKeyValue<UserRepresentation>(user.attributes!).filter(
(a: KeyValueType) =>
!config?.attributes?.some((attribute) => attribute.name === a.key)
);
};

View file

@ -1,9 +1,16 @@
import { saveAs } from "file-saver";
import { cloneDeep } from "lodash-es";
import {
FieldValues,
Path,
PathValue,
UseFormSetValue,
} from "react-hook-form-v7";
import { flatten } from "flat";
import type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation";
import type { ProviderRepresentation } from "@keycloak/keycloak-admin-client/lib/defs/serverInfoRepesentation";
import type { IFormatter, IFormatterValueType } from "@patternfly/react-table";
import { saveAs } from "file-saver";
import { flatten } from "flat";
import { cloneDeep } from "lodash-es";
import {
arrayToKeyValue,
@ -82,24 +89,27 @@ const isAttributeArray = (value: any) => {
const isEmpty = (obj: any) => Object.keys(obj).length === 0;
export const convertAttributeNameToForm = <T extends string>(name: T) => {
export function convertAttributeNameToForm<T>(
name: string
): PathValue<T, Path<T>> {
const index = name.indexOf(".");
return `${name.substring(0, index)}.${beerify(
name.substring(index + 1)
)}` as ReplaceString<T, ".", "🍺", { skipFirst: true }>;
};
)}` as PathValue<T, Path<T>>;
}
const beerify = <T extends string>(name: T) =>
export const beerify = <T extends string>(name: T) =>
name.replaceAll(".", "🍺") as ReplaceString<T, ".", "🍺">;
const debeerify = <T extends string>(name: T) =>
name.replaceAll("🍺", ".") as ReplaceString<T, "🍺", ".">;
export const convertToFormValues = (
obj: any,
setValue: (name: string, value: any) => void
) => {
Object.entries(obj).map(([key, value]) => {
export function convertToFormValues<T extends FieldValues>(
obj: FieldValues,
setValue: UseFormSetValue<T>
) {
Object.entries(obj).map((entry) => {
const [key, value] = entry as [Path<T>, any];
if (key === "attributes" && isAttributesObject(value)) {
setValue(key, arrayToKeyValue(value as Record<string, string[]>));
} else if (key === "config" || key === "attributes") {
@ -110,16 +120,16 @@ export const convertToFormValues = (
);
convertedValues.forEach(([k, v]) =>
setValue(`${key}.${beerify(k)}`, v)
setValue(`${key}.${beerify(k)}` as Path<T>, v)
);
} else {
setValue(key, undefined);
setValue(key, undefined as PathValue<T, Path<T>>);
}
} else {
setValue(key, value);
}
});
};
}
export function convertFormValuesToObject<T extends Record<string, any>, G = T>(
obj: T