diff --git a/apps/admin-ui/src/authentication/components/ExecutionConfigModal.tsx b/apps/admin-ui/src/authentication/components/ExecutionConfigModal.tsx index a9f7b4dc6e..365f303d17 100644 --- a/apps/admin-ui/src/authentication/components/ExecutionConfigModal.tsx +++ b/apps/admin-ui/src/authentication/components/ExecutionConfigModal.tsx @@ -55,7 +55,7 @@ export const ExecutionConfigModal = ({ } = form; const setupForm = (config?: AuthenticatorConfigRepresentation) => { - convertToFormValues(config, setValue); + convertToFormValues(config || {}, setValue); }; useFetch( diff --git a/apps/admin-ui/src/clients/AdvancedTab.tsx b/apps/admin-ui/src/clients/AdvancedTab.tsx index f8e4465e98..a2eed23cff 100644 --- a/apps/admin-ui/src/clients/AdvancedTab.tsx +++ b/apps/admin-ui/src/clients/AdvancedTab.tsx @@ -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(`attributes.${name}`), attributes?.[name] || "" ); } @@ -128,7 +128,6 @@ export const AdvancedTab = ({ save, client }: AdvancedProps) => { {t("clients-help:openIdConnectCompatibilityModes")} save()} reset={() => resetFields(["exclude.session.state.from.auth.response"]) @@ -146,7 +145,6 @@ export const AdvancedTab = ({ save, client }: AdvancedProps) => { {t("clients-help:fineGrainSamlEndpointConfig")} save()} reset={() => resetFields([ @@ -178,7 +176,6 @@ export const AdvancedTab = ({ save, client }: AdvancedProps) => { save()} reset={() => { resetFields([ @@ -201,7 +198,6 @@ export const AdvancedTab = ({ save, client }: AdvancedProps) => { save()} reset={() => { setValue( diff --git a/apps/admin-ui/src/clients/ClientDescription.tsx b/apps/admin-ui/src/clients/ClientDescription.tsx index ce6c569e0d..0b180aeb16 100644 --- a/apps/admin-ui/src/clients/ClientDescription.tsx +++ b/apps/admin-ui/src/clients/ClientDescription.tsx @@ -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(); + } = useFormContext(); return ( - + ( + render={({ field }) => ( )} diff --git a/apps/admin-ui/src/clients/ClientDetails.tsx b/apps/admin-ui/src/clients/ClientDetails.tsx index 880ced5d8c..87a6970c81 100644 --- a/apps/admin-ui/src/clients/ClientDetails.tsx +++ b/apps/admin-ui/src/clients/ClientDetails.tsx @@ -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({ shouldUnregister: false }); + const form = useForm({ shouldUnregister: false }); const { clientId } = useParams(); 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(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(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 }) => ( { const { t } = useTranslation("clients"); - const { watch } = useFormContext(); + const { watch } = useFormContext(); const protocol = watch("protocol"); const { client } = props; diff --git a/apps/admin-ui/src/clients/add/AccessSettings.tsx b/apps/admin-ui/src/clients/add/AccessSettings.tsx index 79b29cbcd2..6ebfc45927 100644 --- a/apps/admin-ui/src/clients/add/AccessSettings.tsx +++ b/apps/admin-ui/src/clients/add/AccessSettings.tsx @@ -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(); + const { register, watch } = useFormContext(); const { realm } = useRealm(); const { hasAccess } = useAccess(); @@ -49,12 +49,7 @@ export const AccessSettings = ({ /> } > - + } > - + @@ -203,12 +187,7 @@ export const AccessSettings = ({ /> } > - + )} {client.bearerOnly && ( diff --git a/apps/admin-ui/src/clients/add/CapabilityConfig.tsx b/apps/admin-ui/src/clients/add/CapabilityConfig.tsx index 8dbd2e9f87..68a4fb4f86 100644 --- a/apps/admin-ui/src/clients/add/CapabilityConfig.tsx +++ b/apps/admin-ui/src/clients/add/CapabilityConfig.tsx @@ -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(); + const { control, watch, setValue } = useFormContext(); 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 }) => ( { - onChange(!value); + field.onChange(!value); if (!value) { setValue("authorizationServicesEnabled", false); setValue("serviceAccountsEnabled", false); setValue( - convertAttributeNameToForm( + convertAttributeNameToForm( "attributes.oidc.ciba.grant.enabled" ), false @@ -97,16 +97,15 @@ export const CapabilityConfig = ({ name="authorizationServicesEnabled" defaultValue={false} control={control} - render={({ onChange, value }) => ( + render={({ field }) => ( { - 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 }) => ( ( + render={({ field }) => ( ( + render={({ field }) => ( ( + render={({ field }) => ( + >("attributes.oauth2.device.authorization.grant.enabled")} defaultValue={false} control={control} - render={({ onChange, value }) => ( + render={({ field }) => ( ( "attributes.oidc.ciba.grant.enabled" )} defaultValue={false} control={control} - render={({ onChange, value }) => ( + render={({ field }) => ( ( + "attributes.saml.encrypt" + )} control={control} defaultValue={false} - render={({ onChange, value }) => ( + render={({ field }) => ( )} @@ -319,19 +316,19 @@ export const CapabilityConfig = ({ hasNoPaddingTop > ( "attributes.saml.client.signature" )} control={control} defaultValue={false} - render={({ onChange, value }) => ( + render={({ field }) => ( )} diff --git a/apps/admin-ui/src/clients/add/GeneralSettings.tsx b/apps/admin-ui/src/clients/add/GeneralSettings.tsx index 57a0e9c744..61d0c7ad41 100644 --- a/apps/admin-ui/src/clients/add/GeneralSettings.tsx +++ b/apps/admin-ui/src/clients/add/GeneralSettings.tsx @@ -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 }) => ( { - 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 }) => { , ...loginThemes.map((theme) => ( @@ -87,13 +89,13 @@ export const LoginSettingsPanel = ({ access }: { access?: boolean }) => { name="consentRequired" defaultValue={false} control={control} - render={({ onChange, value }) => ( + render={({ field }) => ( )} @@ -111,18 +113,18 @@ export const LoginSettingsPanel = ({ access }: { access?: boolean }) => { hasNoPaddingTop > ( "attributes.display.on.consent.screen" )} defaultValue={false} control={control} - render={({ onChange, value }) => ( + render={({ field }) => ( 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 }) => { > ( + "attributes.consent.screen.text" + ) + )} isDisabled={!(consentRequired && displayOnConsentScreen === "true")} /> diff --git a/apps/admin-ui/src/clients/add/LogoutPanel.tsx b/apps/admin-ui/src/clients/add/LogoutPanel.tsx index 2c7e1078ee..ff460455eb 100644 --- a/apps/admin-ui/src/clients/add/LogoutPanel.tsx +++ b/apps/admin-ui/src/clients/add/LogoutPanel.tsx @@ -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(); + } = useFormContext(); 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 }) => ( )} @@ -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 } > ( + "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 } > ( + "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 > ( "attributes.backchannel.logout.session.required" )} defaultValue="true" control={control} - render={({ onChange, value }) => ( + render={({ field }) => ( 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 > ( "attributes.backchannel.logout.revoke.offline.tokens" )} defaultValue="false" control={control} - render={({ onChange, value }) => ( + render={({ field }) => ( onChange(value.toString())} + isChecked={field.value === "true"} + onChange={(value) => field.onChange(value.toString())} aria-label={t("backchannelLogoutRevokeOfflineSessions")} /> )} diff --git a/apps/admin-ui/src/clients/add/NewClientForm.tsx b/apps/admin-ui/src/clients/add/NewClientForm.tsx index e26ad855c0..451d1cada5 100644 --- a/apps/admin-ui/src/clients/add/NewClientForm.tsx +++ b/apps/admin-ui/src/clients/add/NewClientForm.tsx @@ -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({ defaultValues: client }); + const methods = useForm({ defaultValues: client }); const protocol = methods.watch("protocol"); const save = async () => { diff --git a/apps/admin-ui/src/clients/add/SamlConfig.tsx b/apps/admin-ui/src/clients/add/SamlConfig.tsx index d7ee778d10..c392b96fb5 100644 --- a/apps/admin-ui/src/clients/add/SamlConfig.tsx +++ b/apps/admin-ui/src/clients/add/SamlConfig.tsx @@ -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>; + label: string; +}; +export const Toggle = ({ name, label }: ToggleProps) => { const { t } = useTranslation("clients"); - const { control } = useFormContext(); + const { control } = useFormContext(); return ( { name={name} defaultValue="false" control={control} - render={({ onChange, value }) => ( + render={({ field }) => ( 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(); + const { control } = useFormContext(); 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 }) => ( { - 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) => ( @@ -124,27 +125,27 @@ export const SamlSignature = () => { } > ( + "attributes.saml.server.signature.keyinfo.xmlSigKeyInfoKeyNameTransformer" )} defaultValue={KEYNAME_TRANSFORMER[0]} control={control} - render={({ onChange, value }) => ( + render={({ field }) => ( { - 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) => ( diff --git a/apps/admin-ui/src/clients/advanced/AdvancedSettings.tsx b/apps/admin-ui/src/clients/advanced/AdvancedSettings.tsx index 1df2446e07..04692b9314 100644 --- a/apps/admin-ui/src/clients/advanced/AdvancedSettings.tsx +++ b/apps/admin-ui/src/clients/advanced/AdvancedSettings.tsx @@ -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>; 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 ( ( "attributes.saml.assertion.lifespan" )} defaultValue="" control={control} - render={({ onChange, value }) => ( + render={({ field }) => ( )} /> @@ -78,7 +79,6 @@ export const AdvancedSettings = ({ )} defaultValue="" units={["minute", "day", "hour"]} - control={control} /> ( + render={({ field }) => ( onChange("" + value)} + isChecked={field.value === "true"} + onChange={(value) => field.onChange("" + value)} aria-label={t("oAuthMutual")} /> )} @@ -160,22 +156,22 @@ export const AdvancedSettings = ({ } > ( "attributes.pkce.code.challenge.method" )} defaultValue="" control={control} - render={({ onChange, value }) => ( + render={({ field }) => ( { - onChange(value); + field.onChange(value); setBrowserFlowOpen(false); }} - selections={[value]} + selections={[field.value]} > {flows} @@ -109,17 +109,17 @@ export const AuthenticationOverrides = ({ name="authenticationFlowBindingOverrides.direct_grant" defaultValue="" control={control} - render={({ onChange, value }) => ( + render={({ field }) => ( diff --git a/apps/admin-ui/src/clients/advanced/ClusteringPanel.tsx b/apps/admin-ui/src/clients/advanced/ClusteringPanel.tsx index d38be55f09..0b05ad4e2a 100644 --- a/apps/admin-ui/src/clients/advanced/ClusteringPanel.tsx +++ b/apps/admin-ui/src/clients/advanced/ClusteringPanel.tsx @@ -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 }) => ( - + render={({ field }) => ( + )} /> diff --git a/apps/admin-ui/src/clients/advanced/FineGrainOpenIdConnect.tsx b/apps/admin-ui/src/clients/advanced/FineGrainOpenIdConnect.tsx index 885dbec95c..3b45db2db3 100644 --- a/apps/admin-ui/src/clients/advanced/FineGrainOpenIdConnect.tsx +++ b/apps/admin-ui/src/clients/advanced/FineGrainOpenIdConnect.tsx @@ -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 = ({ } > ( "attributes.access.token.signed.response.alg" )} defaultValue="" control={control} - render={({ onChange, value }) => ( + render={({ field }) => ( @@ -193,22 +194,22 @@ export const FineGrainOpenIdConnect = ({ } > ( "attributes.id.token.signed.response.alg" )} defaultValue="" control={control} - render={({ onChange, value }) => ( + render={({ field }) => ( @@ -226,22 +227,22 @@ export const FineGrainOpenIdConnect = ({ } > ( "attributes.id.token.encrypted.response.alg" )} defaultValue="" control={control} - render={({ onChange, value }) => ( + render={({ field }) => ( @@ -259,22 +260,22 @@ export const FineGrainOpenIdConnect = ({ } > ( "attributes.id.token.encrypted.response.enc" )} defaultValue="" control={control} - render={({ onChange, value }) => ( + render={({ field }) => ( @@ -292,22 +293,22 @@ export const FineGrainOpenIdConnect = ({ } > ( "attributes.user.info.response.signature.alg" )} defaultValue="" control={control} - render={({ onChange, value }) => ( + render={({ field }) => ( @@ -325,22 +326,22 @@ export const FineGrainOpenIdConnect = ({ } > ( "attributes.request.object.signature.alg" )} defaultValue="" control={control} - render={({ onChange, value }) => ( + render={({ field }) => ( @@ -358,22 +359,22 @@ export const FineGrainOpenIdConnect = ({ } > ( "attributes.request.object.encryption.alg" )} defaultValue="" control={control} - render={({ onChange, value }) => ( + render={({ field }) => ( @@ -391,22 +392,22 @@ export const FineGrainOpenIdConnect = ({ } > ( "attributes.request.object.encryption.enc" )} defaultValue="" control={control} - render={({ onChange, value }) => ( + render={({ field }) => ( @@ -424,22 +425,22 @@ export const FineGrainOpenIdConnect = ({ } > ( "attributes.request.object.required" )} defaultValue="" control={control} - render={({ onChange, value }) => ( + render={({ field }) => ( @@ -474,22 +475,22 @@ export const FineGrainOpenIdConnect = ({ } > ( "attributes.authorization.signed.response.alg" )} defaultValue="" control={control} - render={({ onChange, value }) => ( + render={({ field }) => ( @@ -507,22 +508,22 @@ export const FineGrainOpenIdConnect = ({ } > ( "attributes.authorization.encrypted.response.alg" )} defaultValue="" control={control} - render={({ onChange, value }) => ( + render={({ field }) => ( @@ -540,22 +541,22 @@ export const FineGrainOpenIdConnect = ({ } > ( "attributes.authorization.encrypted.response.enc" )} defaultValue="" control={control} - render={({ onChange, value }) => ( + render={({ field }) => ( diff --git a/apps/admin-ui/src/clients/advanced/FineGrainSamlEndpointConfig.tsx b/apps/admin-ui/src/clients/advanced/FineGrainSamlEndpointConfig.tsx index 869f5a0dcf..e720e8e5d9 100644 --- a/apps/admin-ui/src/clients/advanced/FineGrainSamlEndpointConfig.tsx +++ b/apps/admin-ui/src/clients/advanced/FineGrainSamlEndpointConfig.tsx @@ -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>; save: () => void; reset: () => void; }; export const FineGrainSamlEndpointConfig = ({ - control: { register }, save, reset, }: FineGrainSamlEndpointConfigProps) => { const { t } = useTranslation("clients"); + const { register } = useFormContext(); return ( @@ -33,10 +32,8 @@ export const FineGrainSamlEndpointConfig = ({ } > diff --git a/apps/admin-ui/src/clients/advanced/OpenIdConnectCompatibilityModes.tsx b/apps/admin-ui/src/clients/advanced/OpenIdConnectCompatibilityModes.tsx index 45a646a540..d10a1f8082 100644 --- a/apps/admin-ui/src/clients/advanced/OpenIdConnectCompatibilityModes.tsx +++ b/apps/admin-ui/src/clients/advanced/OpenIdConnectCompatibilityModes.tsx @@ -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>; save: () => void; reset: () => void; hasConfigureAccess?: boolean; }; export const OpenIdConnectCompatibilityModes = ({ - control, save, reset, hasConfigureAccess, }: OpenIdConnectCompatibilityModesProps) => { const { t } = useTranslation("clients"); + const { control } = useFormContext(); return ( ( "attributes.exclude.session.state.from.auth.response" )} defaultValue="" control={control} - render={({ onChange, value }) => ( + render={({ field }) => ( onChange(value.toString())} + isChecked={field.value === "true"} + onChange={(value) => field.onChange(value.toString())} aria-label={t("excludeSessionStateFromAuthenticationResponse")} /> )} @@ -67,16 +67,18 @@ export const OpenIdConnectCompatibilityModes = ({ } > ( + "attributes.use.refresh.tokens" + )} defaultValue="true" control={control} - render={({ onChange, value }) => ( + render={({ field }) => ( onChange(value.toString())} + isChecked={field.value === "true"} + onChange={(value) => field.onChange(value.toString())} aria-label={t("useRefreshTokens")} /> )} @@ -94,18 +96,18 @@ export const OpenIdConnectCompatibilityModes = ({ } > ( "attributes.client_credentials.use_refresh_token" )} defaultValue="false" control={control} - render={({ onChange, value }) => ( + render={({ field }) => ( onChange(value.toString())} + isChecked={field.value === "true"} + onChange={(value) => field.onChange(value.toString())} aria-label={t("useRefreshTokenForClientCredentialsGrant")} /> )} @@ -123,18 +125,18 @@ export const OpenIdConnectCompatibilityModes = ({ } > ( "attributes.token.response.type.bearer.lower-case" )} defaultValue="false" control={control} - render={({ onChange, value }) => ( + render={({ field }) => ( onChange(value.toString())} + isChecked={field.value === "true"} + onChange={(value) => field.onChange(value.toString())} aria-label={t("useLowerCaseBearerType")} /> )} diff --git a/apps/admin-ui/src/clients/advanced/RevocationPanel.tsx b/apps/admin-ui/src/clients/advanced/RevocationPanel.tsx index be027d1798..ea6ce20872 100644 --- a/apps/admin-ui/src/clients/advanced/RevocationPanel.tsx +++ b/apps/admin-ui/src/clients/advanced/RevocationPanel.tsx @@ -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, diff --git a/apps/admin-ui/src/clients/advanced/TokenLifespan.tsx b/apps/admin-ui/src/clients/advanced/TokenLifespan.tsx index 9d37ac7f1b..b0879782e2 100644 --- a/apps/admin-ui/src/clients/advanced/TokenLifespan.tsx +++ b/apps/admin-ui/src/clients/advanced/TokenLifespan.tsx @@ -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; 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 }) => ( - {isExpireSet(value) && ( + {isExpireSet(field.value) && ( []; }; resources?: Record[]; - 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) => { > value.length > 0 }} - render={({ onChange, value }) => ( + rules={{ validate: (value) => (value || "").length > 0 }} + render={({ field }) => ( { 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) => ( diff --git a/apps/admin-ui/src/clients/authorization/DecisionStragegySelect.tsx b/apps/admin-ui/src/clients/authorization/DecisionStragegySelect.tsx index d78dfd3895..bc46721724 100644 --- a/apps/admin-ui/src/clients/authorization/DecisionStragegySelect.tsx +++ b/apps/admin-ui/src/clients/authorization/DecisionStragegySelect.tsx @@ -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" /> diff --git a/apps/admin-ui/src/clients/import/ImportForm.tsx b/apps/admin-ui/src/clients/import/ImportForm.tsx index 61db551a87..fda001b9f9 100644 --- a/apps/admin-ui/src/clients/import/ImportForm.tsx +++ b/apps/admin-ui/src/clients/import/ImportForm.tsx @@ -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({ shouldUnregister: false }); + const form = useForm({ shouldUnregister: false }); const { register, handleSubmit, setValue } = form; const [imported, setImported] = useState({}); @@ -118,11 +119,9 @@ export default function ImportForm() { diff --git a/apps/admin-ui/src/clients/keys/Keys.tsx b/apps/admin-ui/src/clients/keys/Keys.tsx index 1fecc304a5..ef838eadc4 100644 --- a/apps/admin-ui/src/clients/keys/Keys.tsx +++ b/apps/admin-ui/src/clients/keys/Keys.tsx @@ -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(); + } = useFormContext(); const { adminClient } = useAdminClient(); const { addAlert, addError } = useAlerts(); @@ -149,16 +149,15 @@ export const Keys = ({ clientId, save, hasConfigureAccess }: KeysProps) => { > ( + render={({ field }) => ( 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) => { } > )} diff --git a/apps/admin-ui/src/clients/keys/SamlKeys.tsx b/apps/admin-ui/src/clients/keys/SamlKeys.tsx index 1ca380ecb1..f22f803ff9 100644 --- a/apps/admin-ui/src/clients/keys/SamlKeys.tsx +++ b/apps/admin-ui/src/clients/keys/SamlKeys.tsx @@ -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(); + const { control, watch } = useFormContext(); 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 > ( + render={({ field }) => ( { const v = value.toString(); if (v === "true") { onChanged(attr); - onChange(v); + field.onChange(v); } else { onGenerate(attr, false); } diff --git a/apps/admin-ui/src/components/key-value-form/hook-form-v7/KeyValueInput.tsx b/apps/admin-ui/src/components/key-value-form/hook-form-v7/KeyValueInput.tsx new file mode 100644 index 0000000000..b61ca0d125 --- /dev/null +++ b/apps/admin-ui/src/components/key-value-form/hook-form-v7/KeyValueInput.tsx @@ -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 ( + <> + + + + {t("key")} + + + {t("value")} + + + {fields.map((attribute, index) => ( + + + + + + + + + + + + ))} + + + + + + + + ); +}; diff --git a/apps/admin-ui/src/components/key-value-form/key-value-convert.ts b/apps/admin-ui/src/components/key-value-form/key-value-convert.ts index 1e8437dd65..0f36f1cc77 100644 --- a/apps/admin-ui/src/components/key-value-form/key-value-convert.ts +++ b/apps/admin-ui/src/components/key-value-form/key-value-convert.ts @@ -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 = {}) { +export function arrayToKeyValue(attributes: Record = {}) { const result = Object.entries(attributes).flatMap(([key, value]) => value.map((value) => ({ key, value })) ); - return result.concat({ key: "", value: "" }); + return result.concat({ key: "", value: "" }) as PathValue>; } diff --git a/apps/admin-ui/src/components/multi-line-input/hook-form-v7/MultiLineInput.tsx b/apps/admin-ui/src/components/multi-line-input/hook-form-v7/MultiLineInput.tsx new file mode 100644 index 0000000000..cc7f70bf25 --- /dev/null +++ b/apps/admin-ui/src/components/multi-line-input/hook-form-v7/MultiLineInput.tsx @@ -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 & { + 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([]); + + 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) => ( + + + updateValue(index, value)} + name={`${name}[${index}].value`} + value={value} + isDisabled={isDisabled} + {...rest} + /> + + + {index === fields.length - 1 && ( + + )} + + ))} + + ); +}; diff --git a/apps/admin-ui/src/realm-settings/event-config/EventsTab.tsx b/apps/admin-ui/src/realm-settings/event-config/EventsTab.tsx index 426e03dd53..b53fb4ab45 100644 --- a/apps/admin-ui/src/realm-settings/event-config/EventsTab.tsx +++ b/apps/admin-ui/src/realm-settings/event-config/EventsTab.tsx @@ -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) => { diff --git a/apps/admin-ui/src/user/UserAttributes.tsx b/apps/admin-ui/src/user/UserAttributes.tsx index 0878c1b77c..d4e0494b72 100644 --- a/apps/admin-ui/src/user/UserAttributes.tsx +++ b/apps/admin-ui/src/user/UserAttributes.tsx @@ -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(user.attributes!).filter( + (a: KeyValueType) => + !config?.attributes?.some((attribute) => attribute.name === a.key) ); }; diff --git a/apps/admin-ui/src/util.ts b/apps/admin-ui/src/util.ts index ef54762833..7745350135 100644 --- a/apps/admin-ui/src/util.ts +++ b/apps/admin-ui/src/util.ts @@ -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 = (name: T) => { +export function convertAttributeNameToForm( + name: string +): PathValue> { const index = name.indexOf("."); return `${name.substring(0, index)}.${beerify( name.substring(index + 1) - )}` as ReplaceString; -}; + )}` as PathValue>; +} -const beerify = (name: T) => +export const beerify = (name: T) => name.replaceAll(".", "🍺") as ReplaceString; const debeerify = (name: T) => name.replaceAll("🍺", ".") as ReplaceString; -export const convertToFormValues = ( - obj: any, - setValue: (name: string, value: any) => void -) => { - Object.entries(obj).map(([key, value]) => { +export function convertToFormValues( + obj: FieldValues, + setValue: UseFormSetValue +) { + Object.entries(obj).map((entry) => { + const [key, value] = entry as [Path, any]; if (key === "attributes" && isAttributesObject(value)) { setValue(key, arrayToKeyValue(value as Record)); } 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, v) ); } else { - setValue(key, undefined); + setValue(key, undefined as PathValue>); } } else { setValue(key, value); } }); -}; +} export function convertFormValuesToObject, G = T>( obj: T