From bbc177abb882c5a04cd24c35e0ef69e9a9a81352 Mon Sep 17 00:00:00 2001 From: Erik Jan de Wit Date: Thu, 27 May 2021 09:01:55 +0200 Subject: [PATCH] fixes realm save (#633) * fixes realm save currently save on realm screen didn't work as the email settings where required. Fixed this by changing email to be it's own section. Also email section was using the wrong attributes therefor the changes where not reflected in the old console * fix empty realmComponent array * fixed password help text --- src/realm-settings/EmailTab.tsx | 126 +++++++++++++------- src/realm-settings/KeysListTab.tsx | 58 ++++----- src/realm-settings/KeysProvidersTab.tsx | 11 +- src/realm-settings/RealmSettingsSection.tsx | 95 +++++++-------- src/realm-settings/help.json | 1 + 5 files changed, 158 insertions(+), 133 deletions(-) diff --git a/src/realm-settings/EmailTab.tsx b/src/realm-settings/EmailTab.tsx index 7f121792f6..995f3435da 100644 --- a/src/realm-settings/EmailTab.tsx +++ b/src/realm-settings/EmailTab.tsx @@ -1,8 +1,9 @@ -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; -import { Controller, useFormContext, UseFormMethods } from "react-hook-form"; +import { Controller, useForm } from "react-hook-form"; import { ActionGroup, + AlertVariant, Button, Checkbox, FormGroup, @@ -15,26 +16,60 @@ import type RealmRepresentation from "keycloak-admin/lib/defs/realmRepresentatio import { FormAccess } from "../components/form-access/FormAccess"; import { HelpItem } from "../components/help-enabler/HelpItem"; import { FormPanel } from "../components/scroll-form/FormPanel"; +import { emailRegexPattern } from "../util"; +import { useAdminClient } from "../context/auth/AdminClient"; +import { useAlerts } from "../components/alert/Alerts"; +import { useRealm } from "../context/realm-context/RealmContext"; import "./RealmSettingsSection.css"; -import { emailRegexPattern } from "../util"; - -export type UserFormProps = { - form: UseFormMethods; -}; type RealmSettingsEmailTabProps = { - save: (realm: RealmRepresentation) => void; - reset: () => void; + realm: RealmRepresentation; }; export const RealmSettingsEmailTab = ({ - save, - reset, + realm: initialRealm, }: RealmSettingsEmailTabProps) => { const { t } = useTranslation("realm-settings"); - const [isAuthenticationEnabled, setAuthenticationEnabled] = useState(""); - const { register, control, handleSubmit, errors } = useFormContext(); + const adminClient = useAdminClient(); + const { realm: realmName } = useRealm(); + const { addAlert } = useAlerts(); + + const [isAuthenticationEnabled, setAuthenticationEnabled] = useState("true"); + const [realm, setRealm] = useState(initialRealm); + const { + register, + control, + handleSubmit, + errors, + setValue, + reset: resetForm, + } = useForm(); + + useEffect(() => { + reset(); + }, [realm]); + + const save = async (form: RealmRepresentation) => { + try { + const savedRealm = { ...realm, ...form }; + await adminClient.realms.update({ realm: realmName }, savedRealm); + setRealm(savedRealm); + addAlert(t("saveSuccess"), AlertVariant.success); + } catch (error) { + addAlert( + t("saveError", { error: error.response?.data?.errorMessage || error }), + AlertVariant.danger + ); + } + }; + + const reset = () => { + if (realm) { + resetForm(realm); + Object.entries(realm).map((entry) => setValue(entry[0], entry[1])); + } + }; return ( <> @@ -50,23 +85,20 @@ export const RealmSettingsEmailTab = ({ label={t("from")} fieldId="kc-display-name" isRequired - validated={ - errors.attributes?.from?.type === "pattern" - ? "error" - : "default" - } + validated={errors.smtpServer?.from ? "error" : "default"} helperTextInvalid={t("users:emailInvalid")} > @@ -92,21 +124,18 @@ export const RealmSettingsEmailTab = ({ @@ -142,7 +171,7 @@ export const RealmSettingsEmailTab = ({ @@ -159,34 +188,40 @@ export const RealmSettingsEmailTab = ({ className="pf-u-mt-lg" onSubmit={handleSubmit(save)} > - + ( ( ( @@ -240,24 +274,29 @@ export const RealmSettingsEmailTab = ({ @@ -267,9 +306,12 @@ export const RealmSettingsEmailTab = ({ type="password" id="kc-password" data-testid="password-input" - name="attributes.loginPassword" - ref={register} + name="smtpServer.password" + ref={register({ required: true })} placeholder="Login password" + validated={ + errors.smtpServer?.password ? "error" : "default" + } /> diff --git a/src/realm-settings/KeysListTab.tsx b/src/realm-settings/KeysListTab.tsx index 06bcfca113..ea18825da1 100644 --- a/src/realm-settings/KeysListTab.tsx +++ b/src/realm-settings/KeysListTab.tsx @@ -2,42 +2,51 @@ import React, { useState } from "react"; import { useHistory, useRouteMatch } from "react-router-dom"; import { useTranslation } from "react-i18next"; import { Button, ButtonVariant, PageSection } from "@patternfly/react-core"; +import { cellWidth } from "@patternfly/react-table"; + import type { KeyMetadataRepresentation } from "keycloak-admin/lib/defs/keyMetadataRepresentation"; +import type ComponentRepresentation from "keycloak-admin/lib/defs/componentRepresentation"; import { ListEmptyState } from "../components/list-empty-state/ListEmptyState"; import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable"; import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog"; import { emptyFormatter } from "../util"; -import type ComponentRepresentation from "keycloak-admin/lib/defs/componentRepresentation"; +import { useAdminClient } from "../context/auth/AdminClient"; +import { useRealm } from "../context/realm-context/RealmContext"; import "./RealmSettingsSection.css"; -import { cellWidth } from "@patternfly/react-table"; type KeyData = KeyMetadataRepresentation & { provider?: string; - type?: string; }; -type KeysTabInnerProps = { - keys: KeyData[]; +type KeysListTabProps = { + realmComponents: ComponentRepresentation[]; }; -export const KeysTabInner = ({ keys }: KeysTabInnerProps) => { +export const KeysListTab = ({ realmComponents }: KeysListTabProps) => { const { t } = useTranslation("roles"); const history = useHistory(); const { url } = useRouteMatch(); - const [key, setKey] = useState(0); - const refresh = () => setKey(new Date().getTime()); const [publicKey, setPublicKey] = useState(""); const [certificate, setCertificate] = useState(""); - const loader = async () => { - return keys; - }; + const adminClient = useAdminClient(); + const { realm: realmName } = useRealm(); - React.useEffect(() => { - refresh(); - }, [keys]); + const loader = async () => { + const keysMetaData = await adminClient.realms.getKeys({ + realm: realmName, + }); + const keys = keysMetaData.keys; + + return keys?.map((key) => { + const provider = realmComponents.find( + (component: ComponentRepresentation) => component.id === key.providerId + ); + return { ...key, provider: provider?.name } as KeyData; + })!; + }; const [togglePublicKeyDialog, PublicKeyDialog] = useConfirmDialog({ titleKey: t("realm-settings:publicKeys").slice(0, -1), @@ -115,7 +124,6 @@ export const KeysTabInner = ({ keys }: KeysTabInnerProps) => { { ); }; - -type KeysProps = { - keys: KeyMetadataRepresentation[]; - realmComponents: ComponentRepresentation[]; -}; - -export const KeysListTab = ({ keys, realmComponents, ...props }: KeysProps) => { - return ( - { - const provider = realmComponents.find( - (component: ComponentRepresentation) => - component.id === key.providerId - ); - return { ...key, provider: provider?.name }; - })} - {...props} - /> - ); -}; diff --git a/src/realm-settings/KeysProvidersTab.tsx b/src/realm-settings/KeysProvidersTab.tsx index b271bfdfb0..115cdf92d9 100644 --- a/src/realm-settings/KeysProvidersTab.tsx +++ b/src/realm-settings/KeysProvidersTab.tsx @@ -19,12 +19,13 @@ import { ToolbarGroup, ToolbarItem, } from "@patternfly/react-core"; +import { SearchIcon } from "@patternfly/react-icons"; + import type { KeyMetadataRepresentation } from "keycloak-admin/lib/defs/keyMetadataRepresentation"; import type ComponentRepresentation from "keycloak-admin/lib/defs/componentRepresentation"; +import type ComponentTypeRepresentation from "keycloak-admin/lib/defs/componentTypeRepresentation"; import "./RealmSettingsSection.css"; -import type ComponentTypeRepresentation from "keycloak-admin/lib/defs/componentTypeRepresentation"; -import { SearchIcon } from "@patternfly/react-icons"; type ComponentData = KeyMetadataRepresentation & { providerDescription?: string; @@ -33,8 +34,6 @@ type ComponentData = KeyMetadataRepresentation & { type KeysTabInnerProps = { components: ComponentData[]; - realmComponents: ComponentRepresentation[]; - keyProviderComponentTypes: ComponentTypeRepresentation[]; }; export const KeysTabInner = ({ components }: KeysTabInnerProps) => { @@ -46,7 +45,7 @@ export const KeysTabInner = ({ components }: KeysTabInnerProps) => { [] ); - const itemIds = components.map((item, idx) => "data" + idx); + const itemIds = components.map((_, idx) => "data" + idx); const [itemOrder, setItemOrder] = useState([]); @@ -222,7 +221,6 @@ export const KeysTabInner = ({ components }: KeysTabInnerProps) => { type KeysProps = { components: ComponentRepresentation[]; - realmComponents: ComponentRepresentation[]; keyProviderComponentTypes: ComponentTypeRepresentation[]; }; @@ -240,7 +238,6 @@ export const KeysProviderTab = ({ ); return { ...component, providerDescription: provider?.helpText }; })} - keyProviderComponentTypes={keyProviderComponentTypes} {...props} /> ); diff --git a/src/realm-settings/RealmSettingsSection.tsx b/src/realm-settings/RealmSettingsSection.tsx index c0063e726f..d9b11fc6aa 100644 --- a/src/realm-settings/RealmSettingsSection.tsx +++ b/src/realm-settings/RealmSettingsSection.tsx @@ -27,7 +27,6 @@ import { PartialImportDialog } from "./PartialImport"; import { RealmSettingsThemesTab } from "./ThemesTab"; import { RealmSettingsEmailTab } from "./EmailTab"; import { KeysListTab } from "./KeysListTab"; -import type { KeyMetadataRepresentation } from "keycloak-admin/lib/defs/keyMetadataRepresentation"; import type ComponentRepresentation from "keycloak-admin/lib/defs/componentRepresentation"; import { KeysProviderTab } from "./KeysProvidersTab"; import { useServerInfo } from "../context/server-info/ServerInfoProvider"; @@ -128,43 +127,40 @@ export const RealmSettingsSection = () => { const { realm: realmName } = useRealm(); const { addAlert } = useAlerts(); const form = useForm(); - const { control, getValues, setValue } = form; + const { control, getValues, setValue, reset: resetForm } = form; const [realm, setRealm] = useState(); - const [activeTab2, setActiveTab2] = useState(0); - const [keys, setKeys] = useState([]); + const [activeTab, setActiveTab] = useState(0); const [realmComponents, setRealmComponents] = useState< ComponentRepresentation[] - >([]); + >(); const kpComponentTypes = useServerInfo().componentTypes![ "org.keycloak.keys.KeyProvider" ]; useFetch( - () => adminClient.realms.findOne({ realm: realmName }), - (realm) => { - setupForm(realm); - setRealm(realm); + async () => { + const realm = await adminClient.realms.findOne({ realm: realmName }); + const realmComponents = await adminClient.components.find({ + type: "org.keycloak.keys.KeyProvider", + realm: realmName, + }); + + return { realm, realmComponents }; + }, + (result) => { + setRealm(result.realm); + setRealmComponents(result.realmComponents); }, [] ); useEffect(() => { - const update = async () => { - const keysMetaData = await adminClient.realms.getKeys({ - realm: realmName, - }); - setKeys(keysMetaData.keys!); - const realmComponents = await adminClient.components.find({ - type: "org.keycloak.keys.KeyProvider", - realm: realmName, - }); - setRealmComponents(realmComponents); - }; - setTimeout(update, 100); - }, []); + if (realm) setupForm(realm); + }, [realm]); const setupForm = (realm: RealmRepresentation) => { + resetForm(realm); Object.entries(realm).map((entry) => setValue(entry[0], entry[1])); }; @@ -174,7 +170,10 @@ export const RealmSettingsSection = () => { setRealm(realm); addAlert(t("saveSuccess"), AlertVariant.success); } catch (error) { - addAlert(t("saveError", { error }), AlertVariant.danger); + addAlert( + t("saveError", { error: error.response?.data?.errorMessage || error }), + AlertVariant.danger + ); } }; @@ -218,10 +217,7 @@ export const RealmSettingsSection = () => { title={{t("realm-settings:email")}} data-testid="rs-email-tab" > - setupForm(realm!)} - /> + {realm && } { title={{t("realm-settings:keys")}} data-testid="rs-keys-tab" > - setActiveTab2(key as number)} - > - {t("keysList")}} + {realmComponents && ( + setActiveTab(key as number)} > - - - {t("providers")}} - > - - - + {t("keysList")}} + > + + + {t("providers")}} + > + + + + )} diff --git a/src/realm-settings/help.json b/src/realm-settings/help.json index bff586f239..5af2778135 100644 --- a/src/realm-settings/help.json +++ b/src/realm-settings/help.json @@ -3,6 +3,7 @@ "fromDisplayName": "A user-friendly name for the 'From' address (optional).", "replyToDisplayName": "A user-friendly name for the 'Reply-To' address (optional).", "envelopeFrom": "An email address used for bounces (optional).", + "password": "SMTP password. This field is able to obtain its value from vault, use ${vault.ID} format.", "frontendUrl": "Set the frontend URL for the realm. Use in combination with the default hostname provider to override the base URL for frontend requests for a specific realm.", "requireSsl": "Is HTTPS required? 'None' means HTTPS is not required for any client IP address. 'External requests' means localhost and private IP addresses can access without HTTPS. 'All requests' means HTTPS is required for all IP addresses.", "userManagedAccess": "If enabled, users are allowed to manage their resources and permissions using the Account Management Console.",