From 45cda0a9695323ad13cf5320c08a70f9c2534ee4 Mon Sep 17 00:00:00 2001 From: Erik Jan de Wit Date: Wed, 13 Jan 2021 20:59:45 +0100 Subject: [PATCH] initial version realm settings section (#281) * initial version realm settings section * simpified save * fixed reload * fixed merge error --- src/clients/ClientsSection.tsx | 6 +- src/components/form-access/FormAccess.tsx | 8 +- .../realm-selector/RealmSelector.tsx | 4 +- src/i18n.ts | 4 + src/realm-settings/RealmSettingsSection.tsx | 332 +++++++++++++++++- src/realm-settings/help.json | 8 + src/realm-settings/messages.json | 30 ++ src/util.ts | 10 + 8 files changed, 392 insertions(+), 10 deletions(-) create mode 100644 src/realm-settings/help.json create mode 100644 src/realm-settings/messages.json diff --git a/src/clients/ClientsSection.tsx b/src/clients/ClientsSection.tsx index 91070b34af..3197c98b7b 100644 --- a/src/clients/ClientsSection.tsx +++ b/src/clients/ClientsSection.tsx @@ -11,7 +11,7 @@ import { import { ViewHeader } from "../components/view-header/ViewHeader"; import { useAdminClient } from "../context/auth/AdminClient"; import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable"; -import { emptyFormatter, exportClient } from "../util"; +import { emptyFormatter, exportClient, getBaseUrl } from "../util"; import { useAlerts } from "../components/alert/Alerts"; import ClientRepresentation from "keycloak-admin/lib/defs/clientRepresentation"; import { formattedLinkTableCell } from "../components/external-link/FormattedLink"; @@ -23,9 +23,7 @@ export const ClientsSection = () => { const { url } = useRouteMatch(); const adminClient = useAdminClient(); - const baseUrl = adminClient.keycloak - ? adminClient.keycloak.authServerUrl! - : adminClient.baseUrl + "/"; + const baseUrl = getBaseUrl(adminClient); const loader = async (first?: number, max?: number, search?: string) => { const params: { [name: string]: string | number } = { diff --git a/src/components/form-access/FormAccess.tsx b/src/components/form-access/FormAccess.tsx index 6d6c3bebaa..b3d98456d7 100644 --- a/src/components/form-access/FormAccess.tsx +++ b/src/components/form-access/FormAccess.tsx @@ -7,11 +7,14 @@ import React, { import { Controller } from "react-hook-form"; import { ActionGroup, + ClipboardCopy, Form, FormGroup, FormProps, Grid, GridItem, + Stack, + StackItem, TextArea, } from "@patternfly/react-core"; import { AccessType } from "keycloak-admin/lib/defs/whoAmIRepresentation"; @@ -91,7 +94,10 @@ export const FormAccess = ({ child.type === FormGroup || child.type === GridItem || child.type === Grid || - child.type === ActionGroup + child.type === ActionGroup || + child.type === ClipboardCopy || + child.type === Stack || + child.type === StackItem ? { children } : { ...newProps, children } ); diff --git a/src/components/realm-selector/RealmSelector.tsx b/src/components/realm-selector/RealmSelector.tsx index f013ede71d..578306a9a8 100644 --- a/src/components/realm-selector/RealmSelector.tsx +++ b/src/components/realm-selector/RealmSelector.tsx @@ -20,6 +20,7 @@ import { useRealm } from "../../context/realm-context/RealmContext"; import { WhoAmIContext } from "../../context/whoami/WhoAmI"; import "./realm-selector.css"; +import { toUpperCase } from "../../util"; type RealmSelectorProps = { realmList: RealmRepresentation[]; @@ -34,9 +35,6 @@ export const RealmSelector = ({ realmList }: RealmSelectorProps) => { const history = useHistory(); const { t } = useTranslation("common"); - const toUpperCase = (realmName: string) => - realmName.charAt(0).toUpperCase() + realmName.slice(1); - const RealmText = ({ value }: { value: string }) => ( {toUpperCase(value)} diff --git a/src/i18n.ts b/src/i18n.ts index d1789649bb..96885e965f 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -14,6 +14,8 @@ import roles from "./realm-roles/messages.json"; import users from "./user/messages.json"; import sessions from "./sessions/messages.json"; import events from "./events/messages.json"; +import realmSettings from "./realm-settings/messages.json"; +import realmSettingsHelp from "./realm-settings/help.json"; import storybook from "./stories/messages.json"; import userFederation from "./user-federation/messages.json"; import userFederationHelp from "./user-federation/help.json"; @@ -36,6 +38,8 @@ const initOptions = { ...sessions, ...userFederation, ...events, + ...realmSettings, + ...realmSettingsHelp, ...storybook, ...userFederation, ...userFederationHelp, diff --git a/src/realm-settings/RealmSettingsSection.tsx b/src/realm-settings/RealmSettingsSection.tsx index ef615697fc..2c243df80b 100644 --- a/src/realm-settings/RealmSettingsSection.tsx +++ b/src/realm-settings/RealmSettingsSection.tsx @@ -1,5 +1,333 @@ -import React from "react"; +import React, { useEffect, useState } from "react"; +import { useHistory } from "react-router-dom"; +import { useTranslation } from "react-i18next"; +import { Controller, useForm } from "react-hook-form"; +import { + ActionGroup, + AlertVariant, + Button, + ButtonVariant, + ClipboardCopy, + DropdownItem, + DropdownSeparator, + FormGroup, + PageSection, + Select, + SelectOption, + SelectVariant, + Stack, + StackItem, + Switch, + Tab, + Tabs, + TabTitleText, + TextInput, +} from "@patternfly/react-core"; + +import RealmRepresentation from "keycloak-admin/lib/defs/realmRepresentation"; +import { getBaseUrl, toUpperCase } from "../util"; +import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog"; +import { useAdminClient, asyncStateFetch } from "../context/auth/AdminClient"; +import { useRealm } from "../context/realm-context/RealmContext"; +import { ViewHeader } from "../components/view-header/ViewHeader"; +import { useAlerts } from "../components/alert/Alerts"; +import { FormAccess } from "../components/form-access/FormAccess"; +import { HelpItem } from "../components/help-enabler/HelpItem"; +import { FormattedLink } from "../components/external-link/FormattedLink"; + +type RealmSettingsHeaderProps = { + onChange: (...event: any[]) => void; + value: boolean; + save: () => void; + realmName: string; +}; + +const RealmSettingsHeader = ({ + save, + onChange, + value, + realmName, +}: RealmSettingsHeaderProps) => { + const { t } = useTranslation("realm-settings"); + const adminClient = useAdminClient(); + const { addAlert } = useAlerts(); + const history = useHistory(); + const { setRealm } = useRealm(); + + const [toggleDisableDialog, DisableConfirm] = useConfirmDialog({ + titleKey: "realm-settings:disableConfirmTitle", + messageKey: "realm-settings:disableConfirm", + continueButtonLabel: "common:disable", + onConfirm: () => { + onChange(!value); + save(); + }, + }); + + const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({ + titleKey: "realm-settings:deleteConfirmTitle", + messageKey: "realm-settings:deleteConfirm", + continueButtonLabel: "common:delete", + continueButtonVariant: ButtonVariant.danger, + onConfirm: async () => { + try { + await adminClient.realms.del({ realm: realmName }); + addAlert(t("deletedSuccess"), AlertVariant.success); + setRealm("master"); + history.push("/master"); + } catch (error) { + addAlert(t("deleteError", { error }), AlertVariant.danger); + } + }, + }); + + return ( + <> + + + {}}> + {t("partialImport")} + , + {}}> + {t("partialExport")} + , + , + + {t("common:delete")} + , + ]} + isEnabled={value} + onToggle={(value) => { + if (!value) { + toggleDisableDialog(); + } else { + onChange(value); + save(); + } + }} + /> + + ); +}; + +const requireSslTypes = ["all", "external", "none"]; export const RealmSettingsSection = () => { - return <>The Realm Settings Page; + const { t } = useTranslation("realm-settings"); + const adminClient = useAdminClient(); + const { realm: realmName } = useRealm(); + const { addAlert } = useAlerts(); + const { register, control, getValues, setValue, handleSubmit } = useForm(); + const [realm, setRealm] = useState(); + const [activeTab, setActiveTab] = useState(0); + const [open, setOpen] = useState(false); + + const baseUrl = getBaseUrl(adminClient); + + useEffect(() => { + return asyncStateFetch( + () => adminClient.realms.findOne({ realm: realmName }), + (realm) => { + setRealm(realm); + setupForm(realm); + } + ); + }, []); + + const setupForm = (realm: RealmRepresentation) => { + Object.entries(realm).map((entry) => setValue(entry[0], entry[1])); + }; + + const save = async (realm: RealmRepresentation) => { + try { + await adminClient.realms.update({ realm: realmName }, realm); + setRealm(realm); + addAlert(t("saveSuccess"), AlertVariant.success); + } catch (error) { + addAlert(t("saveError", { error }), AlertVariant.danger); + } + }; + + return ( + <> + ( + save(getValues())} + /> + )} + /> + + + setActiveTab(key as number)} + isBox + > + {t("general")}}> + + + {realmName} + + + + + + + + + } + > + + + + } + > + ( + + )} + /> + + + } + fieldId="kc-user-manged-access" + > + ( + + )} + /> + + + } + fieldId="kc-endpoints" + > + + + + + + + + + + + + + + + + + + + + ); }; diff --git a/src/realm-settings/help.json b/src/realm-settings/help.json new file mode 100644 index 0000000000..3c9bc26a1e --- /dev/null +++ b/src/realm-settings/help.json @@ -0,0 +1,8 @@ +{ + "realm-settings-help": { + "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.", + "endpoints": "Shows the configuration of the protocol endpoints" + } +} \ No newline at end of file diff --git a/src/realm-settings/messages.json b/src/realm-settings/messages.json new file mode 100644 index 0000000000..58b1cbefe2 --- /dev/null +++ b/src/realm-settings/messages.json @@ -0,0 +1,30 @@ +{ + "realm-settings": { + "partialImport": "Partial import", + "partialExport": "Partial export", + "deleteRealm": "Delete realm", + "deleteConfirmTitle": "Delete realm?", + "deleteConfirm": "If you delete this realm, all associated data will be removed.", + "deletedSuccess": "The realm has been deleted", + "deleteError": "Could not delete realm: {{error}}", + "disableConfirmTitle": "Disable realm?", + "disableConfirm": "User and clients can't access the realm if it's disabled. Are you sure you want to continue?", + "saveSuccess": "Realm successfully updated", + "saveError": "Realm could not be updated: {error}", + "general": "General", + "realmId": "Realm ID", + "displayName": "Display name", + "htmlDisplayName": "HTML Display name", + "frontendUrl": "Frontend URL", + "requireSsl": "Require SSL", + "sslType": { + "all": "All requests", + "external": "External requests", + "none": "None" + }, + "userManagedAccess": "User-managed access", + "endpoints": "Endpoints", + "openEndpointConfiguration": "Open Endpoint Configuration", + "samlIdentityProviderMetadata": "SAML 2.0 Identity Provider Metadata" + } +} \ No newline at end of file diff --git a/src/util.ts b/src/util.ts index e580e81f50..74c1146f69 100644 --- a/src/util.ts +++ b/src/util.ts @@ -3,6 +3,7 @@ import FileSaver from "file-saver"; import _ from "lodash"; import ClientRepresentation from "keycloak-admin/lib/defs/clientRepresentation"; import { ProviderRepresentation } from "keycloak-admin/lib/defs/serverInfoRepesentation"; +import KeycloakAdminClient from "keycloak-admin"; export const sortProviders = (providers: { [index: string]: ProviderRepresentation; @@ -49,6 +50,9 @@ export const exportClient = (client: ClientRepresentation): void => { ); }; +export const toUpperCase = (realmName: string) => + realmName.charAt(0).toUpperCase() + realmName.slice(1); + export const convertToFormValues = ( obj: any, prefix: string, @@ -73,3 +77,9 @@ export const emptyFormatter = (): IFormatter => ( ) => { return data ? data : "—"; }; + +export const getBaseUrl = (adminClient: KeycloakAdminClient) => { + return adminClient.keycloak + ? adminClient.keycloak.authServerUrl! + : adminClient.baseUrl + "/"; +};