Work around object collisions in forms (#3035)

This commit is contained in:
Erik Jan de Wit 2022-08-09 10:32:16 +02:00 committed by GitHub
parent cfe9706baf
commit 09d7194f9b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 267 additions and 141 deletions

View file

@ -17,13 +17,13 @@ import { CogIcon, TrashIcon } from "@patternfly/react-icons";
import type AuthenticatorConfigRepresentation from "@keycloak/keycloak-admin-client/lib/defs/authenticatorConfigRepresentation";
import type AuthenticatorConfigInfoRepresentation from "@keycloak/keycloak-admin-client/lib/defs/authenticatorConfigInfoRepresentation";
import type { ConfigPropertyRepresentation } from "@keycloak/keycloak-admin-client/lib/defs/authenticatorConfigInfoRepresentation";
import type { ExpandableExecution } from "../execution-model";
import { useAdminClient, useFetch } from "../../context/auth/AdminClient";
import { useAlerts } from "../../components/alert/Alerts";
import { HelpItem } from "../../components/help-enabler/HelpItem";
import { DynamicComponents } from "../../components/dynamic/DynamicComponents";
import { KeycloakTextInput } from "../../components/keycloak-text-input/KeycloakTextInput";
import { convertToFormValues } from "../../util";
type ExecutionConfigModalForm = {
alias: string;
@ -54,22 +54,8 @@ export const ExecutionConfigModal = ({
formState: { errors },
} = form;
const setupForm = (
configDescription: AuthenticatorConfigInfoRepresentation,
config?: AuthenticatorConfigRepresentation
) => {
configDescription.properties!.map(
(property: ConfigPropertyRepresentation) => {
setValue(
`config.${property.name}`,
config?.config?.[property.name!] || property.defaultValue || ""
);
}
);
if (config) {
setValue("alias", config.alias);
setValue("id", config.id);
}
const setupForm = (config?: AuthenticatorConfigRepresentation) => {
convertToFormValues(config, setValue);
};
useFetch(
@ -94,7 +80,7 @@ export const ExecutionConfigModal = ({
);
useEffect(() => {
if (configDescription) setupForm(configDescription, config);
if (configDescription) setupForm(config);
}, [show]);
const save = async (changedConfig: ExecutionConfigModalForm) => {

View file

@ -21,7 +21,7 @@ import {
} from "../../components/client-scope/ClientScopeTypes";
import { HelpItem } from "../../components/help-enabler/HelpItem";
import { useLoginProviders } from "../../context/server-info/ServerInfoProvider";
import { convertToFormValues } from "../../util";
import { convertAttributeNameToForm, convertToFormValues } from "../../util";
import { useRealm } from "../../context/realm-context/RealmContext";
import { getProtocolName } from "../../clients/utils";
import { toClientScopes } from "../routes/ClientScopes";
@ -211,7 +211,9 @@ export const ScopeForm = ({ clientScope, save }: ScopeFormProps) => {
fieldId="kc-display.on.consent.screen"
>
<Controller
name="attributes.display.on.consent.screen"
name={convertAttributeNameToForm(
"attributes.display.on.consent.screen"
)}
control={control}
defaultValue={displayOnConsentScreen}
render={({ onChange, value }) => (
@ -240,7 +242,7 @@ export const ScopeForm = ({ clientScope, save }: ScopeFormProps) => {
ref={register}
type="text"
id="kc-consent-screen-text"
name="attributes.consent.screen.text"
name={convertAttributeNameToForm("attributes.consent.screen.text")}
/>
</FormGroup>
)}
@ -256,7 +258,7 @@ export const ScopeForm = ({ clientScope, save }: ScopeFormProps) => {
fieldId="includeInTokenScope"
>
<Controller
name="attributes.include.in.token.scope"
name={convertAttributeNameToForm("attributes.include.in.token.scope")}
control={control}
defaultValue="true"
render={({ onChange, value }) => (
@ -281,7 +283,7 @@ export const ScopeForm = ({ clientScope, save }: ScopeFormProps) => {
fieldId="kc-gui-order"
>
<Controller
name="attributes.gui.order"
name={convertAttributeNameToForm("attributes.gui.order")}
defaultValue=""
control={control}
render={({ onChange, value }) => (

View file

@ -36,6 +36,7 @@ import { useAdminClient, useFetch } from "../context/auth/AdminClient";
import { useRealm } from "../context/realm-context/RealmContext";
import { RolesList } from "../realm-roles/RolesList";
import {
convertAttributeNameToForm,
convertFormValuesToObject,
convertToFormValues,
exportClient,
@ -237,12 +238,12 @@ export default function ClientDetails() {
form.reset({ ...client });
convertToFormValues(client, form.setValue);
form.setValue(
"attributes.request.uris",
convertAttributeNameToForm("attributes.request.uris"),
stringToMultiline(client.attributes?.["request.uris"])
);
if (client.attributes?.["acr.loa.map"]) {
form.setValue(
"attributes.acr.loa.map",
convertAttributeNameToForm("attributes.acr.loa.map"),
Object.entries(JSON.parse(client.attributes["acr.loa.map"])).flatMap(
([key, value]) => ({ key, value })
)
@ -250,21 +251,16 @@ export default function ClientDetails() {
}
if (client.attributes?.["default.acr.values"]) {
form.setValue(
"attributes.default.acr.values",
convertAttributeNameToForm("attributes.default.acr.values"),
stringToMultiline(client.attributes["default.acr.values"])
);
}
if (client.attributes?.["post.logout.redirect.uris"]) {
form.setValue(
"attributes.post.logout.redirect.uris",
convertAttributeNameToForm("attributes.post.logout.redirect.uris"),
stringToMultiline(client.attributes["post.logout.redirect.uris"])
);
}
Object.entries(client.attributes || {})
.filter(([key]) => key.startsWith("saml.server.signature"))
.map(([key, value]) =>
form.setValue("attributes." + key.replaceAll(".", "$"), value)
);
};
useFetch(
@ -295,36 +291,29 @@ export default function ClientDetails() {
return;
}
const values = form.getValues();
const values = convertFormValuesToObject(form.getValues());
if (values.attributes?.request.uris) {
if (values.attributes?.["request.uris"]) {
values.attributes["request.uris"] = toStringValue(
values.attributes.request.uris
values.attributes["request.uris"]
);
}
if (values.attributes?.default?.acr?.values) {
if (values.attributes?.["default.acr.values"]) {
values.attributes["default.acr.values"] = toStringValue(
values.attributes.default.acr.values
values.attributes["default.acr.values"]
);
}
if (values.attributes?.post.logout.redirect.uris) {
if (values.attributes?.["post.logout.redirect.uris"]) {
values.attributes["post.logout.redirect.uris"] = toStringValue(
values.attributes.post.logout.redirect.uris
values.attributes["post.logout.redirect.uris"]
);
}
const submittedClient =
convertFormValuesToObject<ClientRepresentation>(values);
Object.entries(values.attributes || {})
.filter(([key]) => key.includes("$"))
.map(
([key, value]) =>
(submittedClient.attributes![key.replaceAll("$", ".")] = value)
);
if (submittedClient.attributes?.["acr.loa.map"]) {
submittedClient.attributes["acr.loa.map"] = JSON.stringify(
Object.fromEntries(

View file

@ -12,6 +12,7 @@ import { SaveReset } from "../advanced/SaveReset";
import environment from "../../environment";
import { useRealm } from "../../context/realm-context/RealmContext";
import { useAccess } from "../../context/access/Access";
import { convertAttributeNameToForm } from "../../util";
export const AccessSettings = ({
client,
@ -99,7 +100,9 @@ export const AccessSettings = ({
}
>
<MultiLineInput
name="attributes.post.logout.redirect.uris"
name={convertAttributeNameToForm(
"attributes.post.logout.redirect.uris"
)}
aria-label={t("validPostLogoutRedirectUri")}
addButtonLabel="clients:addPostLogoutRedirectUri"
/>

View file

@ -12,6 +12,7 @@ import {
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 "./capability-config.css";
@ -68,7 +69,12 @@ export const CapabilityConfig = ({
if (!value) {
setValue("authorizationServicesEnabled", false);
setValue("serviceAccountsEnabled", false);
setValue("attributes.oidc.ciba.grant.enabled", false);
setValue(
convertAttributeNameToForm(
"attributes.oidc.ciba.grant.enabled"
),
false
);
}
}}
/>
@ -216,7 +222,9 @@ export const CapabilityConfig = ({
</GridItem>
<GridItem lg={8} sm={6}>
<Controller
name="attributes.oauth2.device.authorization.grant.enabled"
name={convertAttributeNameToForm(
"attributes.oauth2.device.authorization.grant.enabled"
)}
defaultValue={false}
control={control}
render={({ onChange, value }) => (
@ -239,7 +247,9 @@ export const CapabilityConfig = ({
</GridItem>
<GridItem lg={8} sm={6}>
<Controller
name="attributes.oidc.ciba.grant.enabled"
name={convertAttributeNameToForm(
"attributes.oidc.ciba.grant.enabled"
)}
defaultValue={false}
control={control}
render={({ onChange, value }) => (
@ -279,7 +289,7 @@ export const CapabilityConfig = ({
hasNoPaddingTop
>
<Controller
name="attributes.saml.encrypt"
name={convertAttributeNameToForm("attributes.saml.encrypt")}
control={control}
defaultValue={false}
render={({ onChange, value }) => (
@ -306,7 +316,9 @@ export const CapabilityConfig = ({
hasNoPaddingTop
>
<Controller
name="attributes.saml.client.signature"
name={convertAttributeNameToForm(
"attributes.saml.client.signature"
)}
control={control}
defaultValue={false}
render={({ onChange, value }) => (

View file

@ -14,6 +14,7 @@ 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 { convertAttributeNameToForm } from "../../util";
export const LoginSettingsPanel = ({ access }: { access?: boolean }) => {
const { t } = useTranslation("clients");
@ -109,7 +110,9 @@ export const LoginSettingsPanel = ({ access }: { access?: boolean }) => {
hasNoPaddingTop
>
<Controller
name="attributes.display.on.consent.screen"
name={convertAttributeNameToForm(
"attributes.display.on.consent.screen"
)}
defaultValue={false}
control={control}
render={({ onChange, value }) => (
@ -136,7 +139,7 @@ export const LoginSettingsPanel = ({ access }: { access?: boolean }) => {
>
<KeycloakTextArea
id="kc-consent-screen-text"
name="attributes.consent.screen.text"
name={convertAttributeNameToForm("attributes.consent.screen.text")}
ref={register}
isDisabled={!(consentRequired && displayOnConsentScreen === "true")}
/>

View file

@ -9,6 +9,7 @@ import { HelpItem } from "../../components/help-enabler/HelpItem";
import { KeycloakTextInput } from "../../components/keycloak-text-input/KeycloakTextInput";
import { useAccess } from "../../context/access/Access";
import { SaveReset } from "../advanced/SaveReset";
import { convertAttributeNameToForm } from "../../util";
export const LogoutPanel = ({
save,
@ -84,7 +85,9 @@ export const LogoutPanel = ({
<KeycloakTextInput
type="text"
id="frontchannelLogoutUrl"
name="attributes.frontchannel.logout.url"
name={convertAttributeNameToForm(
"attributes.frontchannel.logout.url"
)}
ref={register({
validate: (uri) =>
((uri.startsWith("https://") || uri.startsWith("http://")) &&
@ -123,7 +126,9 @@ export const LogoutPanel = ({
<KeycloakTextInput
type="text"
id="backchannelLogoutUrl"
name="attributes.backchannel.logout.url"
name={convertAttributeNameToForm(
"attributes.backchannel.logout.url"
)}
ref={register({
validate: (uri) =>
((uri.startsWith("https://") || uri.startsWith("http://")) &&
@ -150,7 +155,9 @@ export const LogoutPanel = ({
hasNoPaddingTop
>
<Controller
name="attributes.backchannel.logout.session.required"
name={convertAttributeNameToForm(
"attributes.backchannel.logout.session.required"
)}
defaultValue="true"
control={control}
render={({ onChange, value }) => (
@ -176,7 +183,9 @@ export const LogoutPanel = ({
hasNoPaddingTop
>
<Controller
name="attributes.backchannel.logout.revoke.offline.tokens"
name={convertAttributeNameToForm(
"attributes.backchannel.logout.revoke.offline.tokens"
)}
defaultValue="false"
control={control}
render={({ onChange, value }) => (

View file

@ -12,6 +12,7 @@ import {
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";
export const Toggle = ({ name, label }: { name: string; label: string }) => {
const { t } = useTranslation("clients");
@ -98,27 +99,33 @@ export const SamlConfig = () => {
/>
</FormGroup>
<Toggle
name="attributes.saml.force.name.id.format"
name={convertAttributeNameToForm(
"attributes.saml.force.name.id.format"
)}
label="forceNameIdFormat"
/>
<Toggle
name="attributes.saml.force.post.binding"
name={convertAttributeNameToForm("attributes.saml.force.post.binding")}
label="forcePostBinding"
/>
<Toggle
name="attributes.saml.artifact.binding"
name={convertAttributeNameToForm("attributes.saml.artifact.binding")}
label="forceArtifactBinding"
/>
<Toggle
name="attributes.saml.authnstatement"
name={convertAttributeNameToForm("attributes.saml.authnstatement")}
label="includeAuthnStatement"
/>
<Toggle
name="attributes.saml.onetimeuse.condition"
name={convertAttributeNameToForm(
"attributes.saml.onetimeuse.condition"
)}
label="includeOneTimeUseCondition"
/>
<Toggle
name="attributes.saml.server.signature.keyinfo.ext"
name={convertAttributeNameToForm(
"attributes.saml.server.signature.keyinfo.ext"
)}
label="optimizeLookup"
/>
</FormAccess>

View file

@ -9,6 +9,7 @@ import {
} from "@patternfly/react-core";
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 { Toggle } from "./SamlConfig";
@ -48,7 +49,7 @@ export const SamlSignature = () => {
const { control, watch } = useFormContext<ClientRepresentation>();
const signDocs = watch("attributes.saml$server$signature");
const signDocs = watch("attributes.saml.server.signature");
const signAssertion = watch("attributes.saml.assertion.signature");
return (
@ -57,9 +58,12 @@ export const SamlSignature = () => {
role="manage-clients"
className="keycloak__capability-config__form"
>
<Toggle name="attributes.saml$server$signature" label="signDocuments" />
<Toggle
name="attributes.saml.assertion.signature"
name={convertAttributeNameToForm("attributes.saml.server.signature")}
label="signDocuments"
/>
<Toggle
name={convertAttributeNameToForm("attributes.saml.assertion.signature")}
label="signAssertions"
/>
{(signDocs === "true" || signAssertion === "true") && (
@ -75,7 +79,9 @@ export const SamlSignature = () => {
}
>
<Controller
name="attributes.saml.signature.algorithm"
name={convertAttributeNameToForm(
"attributes.saml.signature.algorithm"
)}
defaultValue={SIGNATURE_ALGORITHMS[0]}
Key
control={control}
@ -114,7 +120,9 @@ export const SamlSignature = () => {
}
>
<Controller
name="attributes.saml$server$signature$keyinfo$xmlSigKeyInfoKeyNameTransformer"
name={convertAttributeNameToForm(
"attributes.saml.server.signature.keyinfo$xmlSigKeyInfoKeyNameTransformer"
)}
defaultValue={KEYNAME_TRANSFORMER[0]}
control={control}
render={({ onChange, value }) => (

View file

@ -17,6 +17,7 @@ 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";
type AdvancedSettingsProps = {
control: Control<Record<string, any>>;
@ -53,7 +54,9 @@ export const AdvancedSettings = ({
}
>
<Controller
name="attributes.saml.assertion.lifespan"
name={convertAttributeNameToForm(
"attributes.saml.assertion.lifespan"
)}
defaultValue=""
control={control}
render={({ onChange, value }) => (
@ -70,7 +73,9 @@ export const AdvancedSettings = ({
<>
<TokenLifespan
id="accessTokenLifespan"
name="attributes.access.token.lifespan"
name={convertAttributeNameToForm(
"attributes.access.token.lifespan"
)}
defaultValue=""
units={["minute", "day", "hour"]}
control={control}
@ -114,7 +119,9 @@ export const AdvancedSettings = ({
}
>
<Controller
name="attributes.pkce.code.challenge.method"
name={convertAttributeNameToForm(
"attributes.pkce.code.challenge.method"
)}
defaultValue=""
control={control}
render={({ onChange, value }) => (
@ -149,7 +156,9 @@ export const AdvancedSettings = ({
}
>
<Controller
name="attributes.require.pushed.authorization.requests"
name={convertAttributeNameToForm(
"attributes.require.pushed.authorization.requests"
)}
defaultValue="false"
control={control}
render={({ onChange, value }) => (
@ -173,7 +182,9 @@ export const AdvancedSettings = ({
/>
}
>
<KeyValueInput name="attributes.acr.loa.map" />
<KeyValueInput
name={convertAttributeNameToForm("attributes.acr.loa.map")}
/>
</FormGroup>
<FormGroup
label={t("defaultACRValues")}
@ -185,7 +196,9 @@ export const AdvancedSettings = ({
/>
}
>
<MultiLineInput name="attributes.default.acr.values" />
<MultiLineInput
name={convertAttributeNameToForm("attributes.default.acr.values")}
/>
</FormGroup>
</>
)}

View file

@ -13,7 +13,7 @@ import {
import { FormAccess } from "../../components/form-access/FormAccess";
import { HelpItem } from "../../components/help-enabler/HelpItem";
import { useServerInfo } from "../../context/server-info/ServerInfoProvider";
import { sortProviders } from "../../util";
import { convertAttributeNameToForm, sortProviders } from "../../util";
import { MultiLineInput } from "../../components/multi-line-input/MultiLineInput";
import { KeycloakTextInput } from "../../components/keycloak-text-input/KeycloakTextInput";
@ -161,7 +161,7 @@ export const FineGrainOpenIdConnect = ({
<KeycloakTextInput
type="text"
id="logoUrl"
name="attributes.logoUri"
name={convertAttributeNameToForm("attributes.logoUri")}
data-testid="logoUrl"
ref={register}
/>
@ -179,7 +179,7 @@ export const FineGrainOpenIdConnect = ({
<KeycloakTextInput
type="text"
id="policyUrl"
name="attributes.policyUri"
name={convertAttributeNameToForm("attributes.policyUri")}
data-testid="policyUrl"
ref={register}
/>
@ -197,7 +197,7 @@ export const FineGrainOpenIdConnect = ({
<KeycloakTextInput
type="text"
id="termsOfServiceUrl"
name="attributes.tosUri"
name={convertAttributeNameToForm("attributes.tosUri")}
data-testid="termsOfServiceUrl"
ref={register}
/>
@ -213,7 +213,9 @@ export const FineGrainOpenIdConnect = ({
}
>
<Controller
name="attributes.access.token.signed.response.alg"
name={convertAttributeNameToForm(
"attributes.access.token.signed.response.alg"
)}
defaultValue=""
control={control}
render={({ onChange, value }) => (
@ -244,7 +246,9 @@ export const FineGrainOpenIdConnect = ({
}
>
<Controller
name="attributes.id.token.signed.response.alg"
name={convertAttributeNameToForm(
"attributes.id.token.signed.response.alg"
)}
defaultValue=""
control={control}
render={({ onChange, value }) => (
@ -275,7 +279,9 @@ export const FineGrainOpenIdConnect = ({
}
>
<Controller
name="attributes.id.token.encrypted.response.alg"
name={convertAttributeNameToForm(
"attributes.id.token.encrypted.response.alg"
)}
defaultValue=""
control={control}
render={({ onChange, value }) => (
@ -306,7 +312,9 @@ export const FineGrainOpenIdConnect = ({
}
>
<Controller
name="attributes.id.token.encrypted.response.enc"
name={convertAttributeNameToForm(
"attributes.id.token.encrypted.response.enc"
)}
defaultValue=""
control={control}
render={({ onChange, value }) => (
@ -337,7 +345,9 @@ export const FineGrainOpenIdConnect = ({
}
>
<Controller
name="attributes.user.info.response.signature.alg"
name={convertAttributeNameToForm(
"attributes.user.info.response.signature.alg"
)}
defaultValue=""
control={control}
render={({ onChange, value }) => (
@ -368,7 +378,9 @@ export const FineGrainOpenIdConnect = ({
}
>
<Controller
name="attributes.request.object.signature.alg"
name={convertAttributeNameToForm(
"attributes.request.object.signature.alg"
)}
defaultValue=""
control={control}
render={({ onChange, value }) => (
@ -399,7 +411,9 @@ export const FineGrainOpenIdConnect = ({
}
>
<Controller
name="attributes.request.object.encryption.alg"
name={convertAttributeNameToForm(
"attributes.request.object.encryption.alg"
)}
defaultValue=""
control={control}
render={({ onChange, value }) => (
@ -430,7 +444,9 @@ export const FineGrainOpenIdConnect = ({
}
>
<Controller
name="attributes.request.object.encryption.enc"
name={convertAttributeNameToForm(
"attributes.request.object.encryption.enc"
)}
defaultValue=""
control={control}
render={({ onChange, value }) => (
@ -461,7 +477,9 @@ export const FineGrainOpenIdConnect = ({
}
>
<Controller
name="attributes.request.object.required"
name={convertAttributeNameToForm(
"attributes.request.object.required"
)}
defaultValue=""
control={control}
render={({ onChange, value }) => (
@ -492,7 +510,7 @@ export const FineGrainOpenIdConnect = ({
}
>
<MultiLineInput
name="attributes.request.uris"
name={convertAttributeNameToForm("attributes.request.uris")}
aria-label={t("validRequestURIs")}
addButtonLabel="clients:addRequestUri"
/>
@ -508,7 +526,9 @@ export const FineGrainOpenIdConnect = ({
}
>
<Controller
name="attributes.authorization.signed.response.alg"
name={convertAttributeNameToForm(
"attributes.authorization.signed.response.alg"
)}
defaultValue=""
control={control}
render={({ onChange, value }) => (
@ -539,7 +559,9 @@ export const FineGrainOpenIdConnect = ({
}
>
<Controller
name="attributes.authorization.encrypted.response.alg"
name={convertAttributeNameToForm(
"attributes.authorization.encrypted.response.alg"
)}
defaultValue=""
control={control}
render={({ onChange, value }) => (
@ -570,7 +592,9 @@ export const FineGrainOpenIdConnect = ({
}
>
<Controller
name="attributes.authorization.encrypted.response.enc"
name={convertAttributeNameToForm(
"attributes.authorization.encrypted.response.enc"
)}
defaultValue=""
control={control}
render={({ onChange, value }) => (

View file

@ -4,6 +4,7 @@ import { ActionGroup, Button, FormGroup, Switch } from "@patternfly/react-core";
import { FormAccess } from "../../components/form-access/FormAccess";
import { HelpItem } from "../../components/help-enabler/HelpItem";
import { convertAttributeNameToForm } from "../../util";
type OpenIdConnectCompatibilityModesProps = {
control: Control<Record<string, any>>;
@ -37,7 +38,9 @@ export const OpenIdConnectCompatibilityModes = ({
}
>
<Controller
name="attributes.exclude.session.state.from.auth.response"
name={convertAttributeNameToForm(
"attributes.exclude.session.state.from.auth.response"
)}
defaultValue=""
control={control}
render={({ onChange, value }) => (
@ -63,7 +66,7 @@ export const OpenIdConnectCompatibilityModes = ({
}
>
<Controller
name="attributes.use.refresh.tokens"
name={convertAttributeNameToForm("attributes.use.refresh.tokens")}
defaultValue="true"
control={control}
render={({ onChange, value }) => (
@ -115,7 +118,9 @@ export const OpenIdConnectCompatibilityModes = ({
}
>
<Controller
name="attributes.token.response.type.bearer.lower-case"
name={convertAttributeNameToForm(
"attributes.token.response.type.bearer.lower-case"
)}
defaultValue="false"
control={control}
render={({ onChange, value }) => (

View file

@ -10,7 +10,7 @@ import {
import { useServerInfo } from "../../context/server-info/ServerInfoProvider";
import { HelpItem } from "../../components/help-enabler/HelpItem";
import { sortProviders } from "../../util";
import { convertAttributeNameToForm, sortProviders } from "../../util";
export const SignedJWT = () => {
const { control } = useFormContext();
@ -32,7 +32,9 @@ export const SignedJWT = () => {
}
>
<Controller
name="attributes.token.endpoint.auth.signing.alg"
name={convertAttributeNameToForm(
"attributes.token.endpoint.auth.signing.alg"
)}
defaultValue=""
control={control}
render={({ onChange, value }) => (

View file

@ -3,6 +3,7 @@ import { Controller, useFormContext } from "react-hook-form";
import { FormGroup, Switch, ValidatedOptions } from "@patternfly/react-core";
import { HelpItem } from "../../components/help-enabler/HelpItem";
import { KeycloakTextInput } from "../../components/keycloak-text-input/KeycloakTextInput";
import { convertAttributeNameToForm } from "../../util";
export const X509 = () => {
const { t } = useTranslation("clients");
@ -25,7 +26,9 @@ export const X509 = () => {
hasNoPaddingTop
>
<Controller
name="attributes.x509.allow.regex.pattern.comparison"
name={convertAttributeNameToForm(
"attributes.x509.allow.regex.pattern.comparison"
)}
defaultValue="false"
control={control}
render={({ onChange, value }) => (
@ -60,7 +63,7 @@ export const X509 = () => {
ref={register({ required: true })}
type="text"
id="kc-subject"
name="attributes.x509.subjectdn"
name={convertAttributeNameToForm("attributes.x509.subjectdn")}
validated={
errors.attributes?.["x509.subjectdn"]
? ValidatedOptions.error

View file

@ -27,6 +27,7 @@ import { GenerateKeyDialog } from "./GenerateKeyDialog";
import { useFetch, useAdminClient } from "../../context/auth/AdminClient";
import { useAlerts } from "../../components/alert/Alerts";
import useToggle from "../../utils/useToggle";
import { convertAttributeNameToForm } from "../../util";
import { ImportKeyDialog, ImportFile } from "./ImportKeyDialog";
import { Certificate } from "./Certificate";
@ -143,7 +144,7 @@ export const Keys = ({ clientId, save, hasConfigureAccess }: KeysProps) => {
}
>
<Controller
name="attributes.use.jwks.url"
name={convertAttributeNameToForm("attributes.use.jwks.url")}
defaultValue="false"
control={control}
render={({ onChange, value }) => (
@ -178,7 +179,7 @@ export const Keys = ({ clientId, save, hasConfigureAccess }: KeysProps) => {
<KeycloakTextInput
type="text"
id="jwksUrl"
name="attributes.jwks.url"
name={convertAttributeNameToForm("attributes.jwks.url")}
ref={register}
/>
</FormGroup>

View file

@ -4,6 +4,7 @@ import { FormGroup, Switch } from "@patternfly/react-core";
import type { ComponentProps } from "./components";
import { HelpItem } from "../help-enabler/HelpItem";
import { convertToName } from "./DynamicComponents";
export const BooleanComponent = ({
name,
@ -25,7 +26,7 @@ export const BooleanComponent = ({
}
>
<Controller
name={`config.${name}`}
name={convertToName(name!)}
data-testid={name}
defaultValue={defaultValue || false}
control={control}

View file

@ -1,11 +1,12 @@
import type { ComponentProps } from "./components";
import { ClientSelect } from "../client/ClientSelect";
import { convertToName } from "./DynamicComponents";
export const ClientSelectComponent = (props: ComponentProps) => {
return (
<ClientSelect
{...props}
name={`config.${props.name}`}
name={convertToName(props.name!)}
namespace="dynamic"
/>
);

View file

@ -1,6 +1,7 @@
import type { ConfigPropertyRepresentation } from "@keycloak/keycloak-admin-client/lib/defs/authenticatorConfigInfoRepresentation";
import { COMPONENTS, isValidComponentType } from "./components";
import { convertAttributeNameToForm } from "../../util";
type DynamicComponentProps = {
properties: ConfigPropertyRepresentation[];
@ -24,3 +25,6 @@ export const DynamicComponents = ({
})}
</>
);
export const convertToName = (name: string) =>
convertAttributeNameToForm(`config.${name}`);

View file

@ -5,6 +5,7 @@ import { FileUpload, FormGroup } from "@patternfly/react-core";
import { HelpItem } from "../help-enabler/HelpItem";
import type { ComponentProps } from "./components";
import { convertToName } from "./DynamicComponents";
export const FileComponent = ({
name,
@ -26,7 +27,7 @@ export const FileComponent = ({
fieldId={name!}
>
<Controller
name={`config.${name}`}
name={convertToName(name!)}
control={control}
defaultValue={defaultValue || ""}
render={({ onChange, value }) => (

View file

@ -12,6 +12,7 @@ import {
import type { ComponentProps } from "./components";
import { HelpItem } from "../help-enabler/HelpItem";
import { GroupPickerDialog } from "../group/GroupPickerDialog";
import { convertToName } from "./DynamicComponents";
export const GroupComponent = ({ name, label, helpText }: ComponentProps) => {
const { t } = useTranslation("dynamic");
@ -20,7 +21,7 @@ export const GroupComponent = ({ name, label, helpText }: ComponentProps) => {
return (
<Controller
name={`config.${name}`}
name={convertToName(name!)}
defaultValue=""
typeAheadAriaLabel={t("selectGroup")}
control={control}

View file

@ -10,6 +10,7 @@ import {
import type { ComponentProps } from "./components";
import { HelpItem } from "../help-enabler/HelpItem";
import { convertToName } from "./DynamicComponents";
export const ListComponent = ({
name,
@ -32,7 +33,7 @@ export const ListComponent = ({
fieldId={name!}
>
<Controller
name={`config.${name}`}
name={convertToName(name!)}
data-testid={name}
defaultValue={defaultValue || ""}
control={control}

View file

@ -4,6 +4,7 @@ import { FormGroup } from "@patternfly/react-core";
import type { ComponentProps } from "./components";
import { HelpItem } from "../help-enabler/HelpItem";
import { KeyValueInput } from "../key-value-form/KeyValueInput";
import { convertToName } from "./DynamicComponents";
export const MapComponent = ({ name, label, helpText }: ComponentProps) => {
const { t } = useTranslation("dynamic");
@ -16,7 +17,7 @@ export const MapComponent = ({ name, label, helpText }: ComponentProps) => {
}
fieldId={name!}
>
<KeyValueInput name={`config.${name}`} />
<KeyValueInput name={convertToName(name!)} />
</FormGroup>
);
};

View file

@ -10,6 +10,7 @@ import {
import { HelpItem } from "../help-enabler/HelpItem";
import type { ComponentProps } from "./components";
import { convertToName } from "./DynamicComponents";
export const MultiValuedListComponent = ({
name,
@ -32,7 +33,7 @@ export const MultiValuedListComponent = ({
fieldId={name!}
>
<Controller
name={`config.${name}`}
name={convertToName(name!)}
control={control}
defaultValue={defaultValue ? [defaultValue] : []}
render={({ onChange, value }) => (

View file

@ -4,6 +4,7 @@ import { FormGroup } from "@patternfly/react-core";
import type { ComponentProps } from "./components";
import { HelpItem } from "../../components/help-enabler/HelpItem";
import { MultiLineInput } from "../multi-line-input/MultiLineInput";
import { convertToName } from "./DynamicComponents";
export const MultiValuedStringComponent = ({
name,
@ -13,7 +14,7 @@ export const MultiValuedStringComponent = ({
isDisabled = false,
}: ComponentProps) => {
const { t } = useTranslation("dynamic");
const fieldName = `config.${name}`;
const fieldName = convertToName(name!);
return (
<FormGroup

View file

@ -18,6 +18,7 @@ import { useAdminClient, useFetch } from "../../context/auth/AdminClient";
import { useRealm } from "../../context/realm-context/RealmContext";
import { HelpItem } from "../help-enabler/HelpItem";
import type { ComponentProps } from "./components";
import { convertToName } from "./DynamicComponents";
const RealmClient = (realm: string): ClientRepresentation => ({
name: "realmRoles",
@ -47,7 +48,7 @@ export const RoleComponent = ({
const [clientRoles, setClientRoles] = useState<RoleRepresentation[]>([]);
const [selectedRole, setSelectedRole] = useState<RoleRepresentation>();
const fieldName = `config.${name}`;
const fieldName = convertToName(name!);
useFetch(
async () => {

View file

@ -5,6 +5,7 @@ import { CodeEditor, Language } from "@patternfly/react-code-editor";
import { HelpItem } from "../help-enabler/HelpItem";
import type { ComponentProps } from "./components";
import { convertToName } from "./DynamicComponents";
export const ScriptComponent = ({
name,
@ -28,7 +29,7 @@ export const ScriptComponent = ({
fieldId={name!}
>
<Controller
name={`config.${name}`}
name={convertToName(name!)}
defaultValue={defaultValue}
control={control}
render={({ onChange, value }) => (

View file

@ -5,6 +5,7 @@ import { FormGroup } from "@patternfly/react-core";
import { HelpItem } from "../help-enabler/HelpItem";
import { KeycloakTextInput } from "../keycloak-text-input/KeycloakTextInput";
import type { ComponentProps } from "./components";
import { convertToName } from "./DynamicComponents";
export const StringComponent = ({
name,
@ -30,7 +31,7 @@ export const StringComponent = ({
isDisabled={isDisabled}
ref={register()}
type="text"
name={`config.${name}`}
name={convertToName(name!)}
defaultValue={defaultValue?.toString()}
/>
</FormGroup>

View file

@ -166,8 +166,10 @@ export default function AddMapper() {
const setupForm = (mapper: IdentityProviderMapperRepresentation) => {
convertToFormValues(mapper, form.setValue);
form.setValue("config.attributes", JSON.parse(mapper.config.attributes));
form.setValue("config.claims", JSON.parse(mapper.config.claims));
form.setValue(
"config.attributes",
JSON.parse(mapper.config.attributes || "{}")
);
};
if (!mapperTypes || !currentMapper) {
@ -241,7 +243,7 @@ export default function AddMapper() {
/>
<FormProvider {...form}>
<DynamicComponents properties={currentMapper.properties!} />
</FormProvider>{" "}
</FormProvider>
</>
)}

View file

@ -16,7 +16,11 @@ import {
} from "@patternfly/react-core";
import type RealmRepresentation from "@keycloak/keycloak-admin-client/lib/defs/realmRepresentation";
import { addTrailingSlash, convertToFormValues } from "../util";
import {
addTrailingSlash,
convertAttributeNameToForm,
convertToFormValues,
} from "../util";
import useIsFeatureEnabled, { Feature } from "../utils/useIsFeatureEnabled";
import { useAdminClient } from "../context/auth/AdminClient";
import { useRealm } from "../context/realm-context/RealmContext";
@ -58,7 +62,7 @@ export const RealmSettingsGeneralTab = ({
JSON.parse(realm.attributes["acr.loa.map"])
).flatMap(([key, value]) => ({ key, value }));
result.concat({ key: "", value: "" });
setValue("attributes.acr.loa.map", result);
setValue(convertAttributeNameToForm("attributes.acr.loa.map"), result);
}
};
@ -113,7 +117,7 @@ export const RealmSettingsGeneralTab = ({
<KeycloakTextInput
type="text"
id="kc-frontend-url"
name="attributes.frontendUrl"
name={convertAttributeNameToForm("attributes.frontendUrl")}
ref={register}
/>
</FormGroup>
@ -168,7 +172,9 @@ export const RealmSettingsGeneralTab = ({
}
>
<FormProvider {...form}>
<KeyValueInput name="attributes.acr.loa.map" />
<KeyValueInput
name={convertAttributeNameToForm("attributes.acr.loa.map")}
/>
</FormProvider>
</FormGroup>
<FormGroup
@ -211,7 +217,7 @@ export const RealmSettingsGeneralTab = ({
fieldId="kc-user-profile-enabled"
>
<Controller
name="attributes.userProfileEnabled"
name={convertAttributeNameToForm("attributes.userProfileEnabled")}
control={control}
defaultValue={false}
render={({ onChange, value }) => (

View file

@ -1,8 +1,14 @@
import { describe, expect, it, vi } from "vitest";
import { convertFormValuesToObject, convertToFormValues } from "./util";
import {
convertAttributeNameToForm,
convertFormValuesToObject,
convertToFormValues,
} from "./util";
vi.mock("react");
const TOKEN = "🍺";
describe("Tests the form convert util functions", () => {
it("convert to form values", () => {
const given = {
@ -31,7 +37,7 @@ describe("Tests the form convert util functions", () => {
const given = {
name: "client",
attributes: [{ key: "one", value: "1" }],
config: { one: { two: "3" } },
config: { [`one${TOKEN}two`]: "3" },
};
//when
@ -51,10 +57,10 @@ describe("Tests the form convert util functions", () => {
description: "",
type: "default",
attributes: {
display: { on: { consent: { screen: "true" } } },
include: { in: { token: { scope: "true" } } },
gui: { order: "1" },
consent: { screen: { text: "" } },
[`display${TOKEN}on${TOKEN}consent${TOKEN}screen`]: "true",
[`include${TOKEN}in${TOKEN}token${TOKEN}scope`]: "true",
[`gui${TOKEN}order`]: "1",
[`consent${TOKEN}screen${TOKEN}text`]: "",
},
};
@ -92,12 +98,10 @@ describe("Tests the form convert util functions", () => {
//then
expect(values).toEqual({
attributes: {
display: { on: { consent: { screen: "true" } } },
include: { in: { token: { scope: "true" } } },
gui: { order: "1" },
consent: { screen: { text: "" } },
},
[`attributes.display${TOKEN}on${TOKEN}consent${TOKEN}screen`]: "true",
[`attributes.include${TOKEN}in${TOKEN}token${TOKEN}scope`]: "true",
[`attributes.gui${TOKEN}order`]: "1",
[`attributes.consent${TOKEN}screen${TOKEN}text`]: "",
});
});
@ -115,17 +119,31 @@ describe("Tests the form convert util functions", () => {
it("convert single element arrays to string", () => {
const given = {
config: { group: ["one"], another: { nested: ["value"] } },
config: {
group: ["one"],
"another.nested": ["value"],
},
};
const setValue = vi.fn();
const values: { [index: string]: any } = {};
const spy = (name: string, value: any) => (values[name] = value);
//when
convertToFormValues(given, setValue);
convertToFormValues(given, spy);
//then
expect(setValue).toHaveBeenCalledWith("config", {
group: "one",
another: { nested: "value" },
expect(values).toEqual({
"config.group": "one",
[`config.another${TOKEN}nested`]: "value",
});
});
it("should convert attribute name to form", () => {
const given = "attributes.some.strange.attribute";
//when
const form = convertAttributeNameToForm(given);
//then
expect(form).toEqual(`attributes.some${TOKEN}strange${TOKEN}attribute`);
});
});

View file

@ -1,7 +1,7 @@
import { cloneDeep } from "lodash-es";
import FileSaver from "file-saver";
import type { IFormatter, IFormatterValueType } from "@patternfly/react-table";
import { unflatten, flatten } from "flat";
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";
@ -85,6 +85,16 @@ const isAttributeArray = (value: any) => {
const isEmpty = (obj: any) => Object.keys(obj).length === 0;
export const convertAttributeNameToForm = (name: string) => {
const index = name.indexOf(".");
return `${name.substring(0, index)}.${convertAttribute(
name.substring(index + 1)
)}`;
};
const convertAttribute = (name: string) => name.replace(/\./g, "🍺");
const convertFormNameToAttribute = (name: string) => name.replace(/🍺/g, ".");
export const convertToFormValues = (
obj: any,
setValue: (name: string, value: any) => void
@ -98,7 +108,10 @@ export const convertToFormValues = (
const convertedValues = Object.entries(flattened).map(([key, value]) =>
Array.isArray(value) ? [key, value[0]] : [key, value]
);
setValue(key, unflatten(Object.fromEntries(convertedValues)));
convertedValues.forEach(([k, v]) =>
setValue(`${key}.${convertAttribute(k)}`, v)
);
} else {
setValue(key, undefined);
}
@ -114,7 +127,12 @@ export function convertFormValuesToObject<T, G = T>(obj: T): G {
if (isAttributeArray(value)) {
result[key] = keyValueToArray(value as KeyValueType[]);
} else if (key === "config" || key === "attributes") {
result[key] = flatten(value as Record<string, any>, { safe: true });
result[key] = Object.fromEntries(
Object.entries(value).map(([k, v]) => [
convertFormNameToAttribute(k),
v,
])
);
} else {
result[key] = value;
}