Refactored scroll form so to easier hide section (#2697)

This commit is contained in:
Erik Jan de Wit 2022-05-30 13:07:33 +02:00 committed by GitHub
parent 0d0e086913
commit 48f68358f4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 1249 additions and 1108 deletions

View file

@ -1,107 +1,68 @@
import {
ActionGroup,
AlertVariant,
Button,
ButtonVariant,
ExpandableSection,
FormGroup,
InputGroup,
PageSection,
Split,
SplitItem,
Text,
ToolbarItem,
Tooltip,
} from "@patternfly/react-core";
import React from "react";
import { useTranslation } from "react-i18next";
import { useFormContext } from "react-hook-form";
import { AlertVariant, PageSection, Text } from "@patternfly/react-core";
import type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation";
import type GlobalRequestResult from "@keycloak/keycloak-admin-client/lib/defs/globalRequestResult";
import moment from "moment";
import React, { useEffect, useRef, useState } from "react";
import { Controller, useFormContext } from "react-hook-form";
import { Trans, useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
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 { ListEmptyState } from "../components/list-empty-state/ListEmptyState";
import type { AddAlertFunction } from "../components/alert/Alerts";
import { ScrollForm } from "../components/scroll-form/ScrollForm";
import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable";
import { TimeSelector } from "../components/time-selector/TimeSelector";
import { KeycloakTextInput } from "../components/keycloak-text-input/KeycloakTextInput";
import { useAdminClient } from "../context/auth/AdminClient";
import { useRealm } from "../context/realm-context/RealmContext";
import { convertToFormValues, toUpperCase } from "../util";
import { AddHostDialog } from "./advanced/AddHostDialog";
import { AdvancedSettings } from "./advanced/AdvancedSettings";
import { AuthenticationOverrides } from "./advanced/AuthenticationOverrides";
import { FineGrainOpenIdConnect } from "./advanced/FineGrainOpenIdConnect";
import { FineGrainSamlEndpointConfig } from "./advanced/FineGrainSamlEndpointConfig";
import { OpenIdConnectCompatibilityModes } from "./advanced/OpenIdConnectCompatibilityModes";
import type { SaveOptions } from "./ClientDetails";
import { toClient } from "./routes/Client";
import type { TFunction } from "i18next";
import { RevocationPanel } from "./advanced/RevocationPanel";
import { ClusteringPanel } from "./advanced/ClusteringPanel";
type AdvancedProps = {
export const parseResult = (
result: GlobalRequestResult,
prefixKey: string,
addAlert: AddAlertFunction,
t: TFunction
) => {
const successCount = result.successRequests?.length || 0;
const failedCount = result.failedRequests?.length || 0;
if (successCount === 0 && failedCount === 0) {
addAlert(t("noAdminUrlSet"), AlertVariant.warning);
} else if (failedCount > 0) {
addAlert(
t(prefixKey + "Success", { successNodes: result.successRequests }),
AlertVariant.success
);
addAlert(
t(prefixKey + "Fail", { failedNodes: result.failedRequests }),
AlertVariant.danger
);
} else {
addAlert(
t(prefixKey + "Success", { successNodes: result.successRequests }),
AlertVariant.success
);
}
};
export type AdvancedProps = {
save: (options?: SaveOptions) => void;
client: ClientRepresentation;
};
export const AdvancedTab = ({
save,
client: {
id,
export const AdvancedTab = ({ save, client }: AdvancedProps) => {
const { t } = useTranslation("clients");
const openIdConnect = "openid-connect";
const { setValue, control, reset } = useFormContext();
const {
publicClient,
registeredNodes,
attributes,
protocol,
authenticationFlowBindingOverrides,
adminUrl,
access,
},
}: AdvancedProps) => {
const { t } = useTranslation("clients");
const adminClient = useAdminClient();
const { realm } = useRealm();
const { addAlert, addError } = useAlerts();
const revocationFieldName = "notBefore";
const openIdConnect = "openid-connect";
const { getValues, setValue, register, control, reset } = useFormContext();
const [expanded, setExpanded] = useState(false);
const [selectedNode, setSelectedNode] = useState("");
const [addNodeOpen, setAddNodeOpen] = useState(false);
const [key, setKey] = useState(0);
const refresh = () => setKey(new Date().getTime());
const [nodes, setNodes] = useState(registeredNodes || {});
const pushRevocationButtonRef = useRef<HTMLElement>();
const setNotBefore = (time: number, messageKey: string) => {
setValue(revocationFieldName, time);
save({ messageKey });
};
const parseResult = (result: GlobalRequestResult, prefixKey: string) => {
const successCount = result.successRequests?.length || 0;
const failedCount = result.failedRequests?.length || 0;
if (successCount === 0 && failedCount === 0) {
addAlert(t("noAdminUrlSet"), AlertVariant.warning);
} else if (failedCount > 0) {
addAlert(
t(prefixKey + "Success", { successNodes: result.successRequests }),
AlertVariant.success
);
addAlert(
t(prefixKey + "Fail", { failedNodes: result.failedRequests }),
AlertVariant.danger
);
} else {
addAlert(
t(prefixKey + "Success", { successNodes: result.successRequests }),
AlertVariant.success
);
}
};
} = client;
const resetFields = (names: string[]) => {
const values: { [name: string]: string } = {};
@ -111,374 +72,131 @@ export const AdvancedTab = ({
reset(values);
};
const push = async () => {
const result = await adminClient.clients.pushRevocation({
id: id!,
});
parseResult(result, "notBeforePush");
};
const testCluster = async () => {
const result = await adminClient.clients.testNodesAvailable({ id: id! });
parseResult(result, "testCluster");
};
const [toggleDeleteNodeConfirm, DeleteNodeConfirm] = useConfirmDialog({
titleKey: "clients:deleteNode",
messageKey: t("deleteNodeBody", {
node: selectedNode,
}),
continueButtonLabel: "common:delete",
continueButtonVariant: ButtonVariant.danger,
onConfirm: async () => {
try {
await adminClient.clients.deleteClusterNode({
id: id!,
node: selectedNode,
});
setNodes({
...Object.keys(nodes).reduce((object: any, key) => {
if (key !== selectedNode) {
object[key] = nodes[key];
}
return object;
}, {}),
});
refresh();
addAlert(t("deleteNodeSuccess"), AlertVariant.success);
} catch (error) {
addError("clients:deleteNodeFail", error);
}
},
});
useEffect(() => {
register(revocationFieldName);
}, [register]);
const formatDate = () => {
const date = getValues(revocationFieldName);
if (date > 0) {
return moment(date * 1000).format("LLL");
} else {
return t("common:none");
}
};
const sections = [
t("revocation"),
t("clustering"),
protocol === openIdConnect
? t("fineGrainOpenIdConnectConfiguration")
: t("fineGrainSamlEndpointConfig"),
t("advancedSettings"),
t("authenticationOverrides"),
];
if (protocol === openIdConnect) {
sections.splice(3, 0, t("openIdConnectCompatibilityModes"));
}
if (!publicClient) {
sections.splice(1, 1);
}
if (protocol !== openIdConnect) {
sections.splice(0, 1);
}
return (
<PageSection variant="light" className="pf-u-py-0">
<ScrollForm sections={sections} borders>
{protocol === openIdConnect && (
<>
<Text className="pf-u-pb-lg">
<Trans i18nKey="clients-help:notBeforeIntro">
In order to successfully push setup url on
<Link to={toClient({ realm, clientId: id!, tab: "settings" })}>
{t("settings")}
</Link>
tab
</Trans>
</Text>
<FormAccess
role="manage-clients"
fineGrainedAccess={access?.configure}
isHorizontal
>
<FormGroup
label={t("notBefore")}
fieldId="kc-not-before"
labelIcon={
<HelpItem
helpText="clients-help:notBefore"
fieldLabelId="clients:notBefore"
/>
}
>
<InputGroup>
<KeycloakTextInput
type="text"
id="kc-not-before"
name="notBefore"
isReadOnly
value={formatDate()}
/>
<Button
id="setToNow"
variant="control"
onClick={() => {
setNotBefore(moment.now() / 1000, "notBeforeSetToNow");
}}
>
{t("setToNow")}
</Button>
<Button
id="clear"
variant="control"
onClick={() => {
setNotBefore(0, "notBeforeNowClear");
}}
>
{t("clear")}
</Button>
</InputGroup>
</FormGroup>
<ActionGroup>
{!adminUrl && (
<Tooltip
reference={pushRevocationButtonRef}
content={t("clients-help:notBeforeTooltip")}
/>
)}
<Button
id="push"
variant="secondary"
onClick={push}
isAriaDisabled={!adminUrl}
ref={pushRevocationButtonRef}
>
{t("push")}
</Button>
</ActionGroup>
</FormAccess>
</>
)}
{publicClient && (
<>
<FormAccess
role="manage-clients"
fineGrainedAccess={access?.configure}
isHorizontal
>
<FormGroup
label={t("nodeReRegistrationTimeout")}
fieldId="kc-node-reregistration-timeout"
labelIcon={
<HelpItem
helpText="clients-help:nodeReRegistrationTimeout"
fieldLabelId="clients:nodeReRegistrationTimeout"
/>
}
>
<Split hasGutter>
<SplitItem>
<Controller
name="nodeReRegistrationTimeout"
defaultValue=""
control={control}
render={({ onChange, value }) => (
<TimeSelector value={value} onChange={onChange} />
)}
/>
</SplitItem>
<SplitItem>
<Button
variant={ButtonVariant.secondary}
onClick={() => save()}
>
{t("common:save")}
</Button>
</SplitItem>
</Split>
</FormGroup>
</FormAccess>
<>
<DeleteNodeConfirm />
<AddHostDialog
clientId={id!}
isOpen={addNodeOpen}
onAdded={(node) => {
nodes[node] = moment.now() / 1000;
refresh();
}}
onClose={() => setAddNodeOpen(false)}
/>
<ExpandableSection
toggleText={t("registeredClusterNodes")}
onToggle={setExpanded}
isExpanded={expanded}
>
<KeycloakDataTable
key={key}
ariaLabelKey="registeredClusterNodes"
loader={() =>
Promise.resolve(
Object.entries(nodes || {}).map((entry) => {
return { host: entry[0], registration: entry[1] };
})
<ScrollForm
sections={[
{
title: t("revocation"),
isHidden: protocol !== openIdConnect,
panel: <RevocationPanel client={client} save={save} />,
},
{
title: t("clustering"),
isHidden: !publicClient,
panel: <ClusteringPanel client={client} save={save} />,
},
{
title: t("fineGrainOpenIdConnectConfiguration"),
isHidden: protocol !== openIdConnect,
panel: (
<>
<Text className="pf-u-pb-lg">
{t("clients-help:fineGrainOpenIdConnectConfiguration")}
</Text>
<FineGrainOpenIdConnect
save={save}
reset={() =>
convertToFormValues(attributes, (key, value) =>
setValue(`attributes.${key}`, value)
)
}
toolbarItem={
<>
<ToolbarItem>
<Button
id="testClusterAvailability"
onClick={testCluster}
variant={ButtonVariant.secondary}
isDisabled={Object.keys(nodes).length === 0}
>
{t("testClusterAvailability")}
</Button>
</ToolbarItem>
<ToolbarItem>
<Button
id="registerNodeManually"
onClick={() => setAddNodeOpen(true)}
variant={ButtonVariant.tertiary}
>
{t("registerNodeManually")}
</Button>
</ToolbarItem>
</>
}
actions={[
{
title: t("common:delete"),
onRowClick: (node) => {
setSelectedNode(node.host);
toggleDeleteNodeConfirm();
},
},
]}
columns={[
{
name: "host",
displayKey: "clients:nodeHost",
},
{
name: "registration",
displayKey: "clients:lastRegistration",
cellFormatters: [
(value) =>
value
? moment(parseInt(value.toString()) * 1000).format(
"LLL"
)
: "",
],
},
]}
emptyState={
<ListEmptyState
message={t("noNodes")}
instructions={t("noNodesInstructions")}
primaryActionText={t("registerNodeManually")}
onPrimaryAction={() => setAddNodeOpen(true)}
/>
/>
</>
),
},
{
title: t("openIdConnectCompatibilityModes"),
isHidden: protocol !== openIdConnect,
panel: (
<>
<Text className="pf-u-pb-lg">
{t("clients-help:openIdConnectCompatibilityModes")}
</Text>
<OpenIdConnectCompatibilityModes
control={control}
save={() => save()}
reset={() =>
resetFields(["exclude.session.state.from.auth.response"])
}
/>
</ExpandableSection>
</>
</>
)}
<>
{protocol === openIdConnect && (
<>
<Text className="pf-u-pb-lg">
{t("clients-help:fineGrainOpenIdConnectConfiguration")}
</Text>
<FineGrainOpenIdConnect
save={() => save()}
reset={() =>
convertToFormValues(attributes, (key, value) =>
setValue(`attributes.${key}`, value)
)
}
hasConfigureAccess={access?.configure}
/>
</>
)}
{protocol !== openIdConnect && (
<>
<Text className="pf-u-pb-lg">
{t("clients-help:fineGrainSamlEndpointConfig")}
</Text>
<FineGrainSamlEndpointConfig
control={control}
save={() => save()}
reset={() =>
convertToFormValues(attributes, (key, value) =>
setValue(`attributes.${key}`, value)
)
}
/>
</>
)}
</>
{protocol === openIdConnect && (
<>
<Text className="pf-u-pb-lg">
{t("clients-help:openIdConnectCompatibilityModes")}
</Text>
<OpenIdConnectCompatibilityModes
control={control}
save={() => save()}
reset={() =>
resetFields(["exclude.session.state.from.auth.response"])
}
hasConfigureAccess={access?.configure}
/>
</>
)}
<>
<Text className="pf-u-pb-lg">
{t("clients-help:advancedSettings" + toUpperCase(protocol || ""))}
</Text>
<AdvancedSettings
protocol={protocol}
control={control}
hasConfigureAccess={access?.configure}
save={() => save()}
reset={() => {
resetFields([
"saml.assertion.lifespan",
"access.token.lifespan",
"tls.client.certificate.bound.access.tokens",
"pkce.code.challenge.method",
]);
}}
/>
</>
<>
<Text className="pf-u-pb-lg">
{t("clients-help:authenticationOverrides")}
</Text>
<AuthenticationOverrides
protocol={protocol}
control={control}
save={() => save()}
reset={() => {
setValue(
"authenticationFlowBindingOverrides.browser",
authenticationFlowBindingOverrides?.browser
);
setValue(
"authenticationFlowBindingOverrides.direct_grant",
authenticationFlowBindingOverrides?.direct_grant
);
}}
hasConfigureAccess={access?.configure}
/>
</>
</ScrollForm>
</>
),
},
{
title: t("fineGrainSamlEndpointConfig"),
isHidden: protocol === openIdConnect,
panel: (
<>
<Text className="pf-u-pb-lg">
{t("clients-help:fineGrainSamlEndpointConfig")}
</Text>
<FineGrainSamlEndpointConfig
control={control}
save={() => save()}
reset={() =>
convertToFormValues(attributes, (key, value) =>
setValue(`attributes.${key}`, value)
)
}
/>
</>
),
},
{
title: t("advancedSettings"),
panel: (
<>
<Text className="pf-u-pb-lg">
{t(
"clients-help:advancedSettings" +
toUpperCase(protocol || "")
)}
</Text>
<AdvancedSettings
protocol={protocol}
control={control}
save={() => save()}
reset={() => {
resetFields([
"saml.assertion.lifespan",
"access.token.lifespan",
"tls.client.certificate.bound.access.tokens",
"pkce.code.challenge.method",
]);
}}
/>
</>
),
},
{
title: t("authenticationOverrides"),
panel: (
<>
<Text className="pf-u-pb-lg">
{t("clients-help:authenticationOverrides")}
</Text>
<AuthenticationOverrides
protocol={protocol}
control={control}
save={() => save()}
reset={() => {
setValue(
"authenticationFlowBindingOverrides.browser",
authenticationFlowBindingOverrides?.browser
);
setValue(
"authenticationFlowBindingOverrides.direct_grant",
authenticationFlowBindingOverrides?.direct_grant
);
}}
/>
</>
),
},
]}
borders
/>
</PageSection>
);
};

View file

@ -1,569 +1,77 @@
import type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation";
import React, { useMemo, useState } from "react";
import React from "react";
import { useTranslation } from "react-i18next";
import {
FormGroup,
Form,
Switch,
Select,
SelectVariant,
SelectOption,
ValidatedOptions,
} from "@patternfly/react-core";
import { Controller, useFormContext } from "react-hook-form";
import { useFormContext } from "react-hook-form";
import { Form } from "@patternfly/react-core";
import type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation";
import { ScrollForm } from "../components/scroll-form/ScrollForm";
import { ClientDescription } from "./ClientDescription";
import { CapabilityConfig } from "./add/CapabilityConfig";
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 { KeycloakTextArea } from "../components/keycloak-text-area/KeycloakTextArea";
import { useServerInfo } from "../context/server-info/ServerInfoProvider";
import { SaveReset } from "./advanced/SaveReset";
import { SamlConfig } from "./add/SamlConfig";
import { SamlSignature } from "./add/SamlSignature";
import environment from "../environment";
import { useRealm } from "../context/realm-context/RealmContext";
import { useAccess } from "../context/access/Access";
import { AccessSettings } from "./add/AccessSettings";
import { LoginSettingsPanel } from "./add/LoginSettingsPanel";
import { LogoutPanel } from "./add/LogoutPanel";
type ClientSettingsProps = {
export type ClientSettingsProps = {
client: ClientRepresentation;
save: () => void;
reset: () => void;
};
export const ClientSettings = ({
client,
save,
reset,
}: ClientSettingsProps) => {
const {
register,
control,
watch,
formState: { errors },
} = useFormContext<ClientRepresentation>();
export const ClientSettings = (props: ClientSettingsProps) => {
const { t } = useTranslation("clients");
const { realm } = useRealm();
const { hasAccess } = useAccess();
const isManager = hasAccess("manage-clients") || client.access?.configure;
const [loginThemeOpen, setLoginThemeOpen] = useState(false);
const loginThemes = useServerInfo().themes!["login"];
const consentRequired = watch("consentRequired");
const displayOnConsentScreen: string = watch(
"attributes.display.on.consent.screen"
);
const { watch } = useFormContext<ClientRepresentation>();
const protocol = watch("protocol");
const frontchannelLogout = watch("frontchannelLogout");
const idpInitiatedSsoUrlName: string = watch(
"attributes.saml_idp_initiated_sso_url_name"
);
const sections = useMemo(() => {
let result = ["generalSettings", "accessSettings"];
if (protocol === "saml") {
return [
...result,
"samlCapabilityConfig",
"signatureAndEncryption",
"loginSettings",
];
} else if (!client.bearerOnly) {
result = [...result, "capabilityConfig"];
} else {
return result;
}
return [...result, "loginSettings", "logoutSettings"];
}, [protocol, client]);
const { client } = props;
return (
<ScrollForm
className="pf-u-px-lg"
sections={sections.map((section) => t(section))}
>
<Form isHorizontal>
<ClientDescription
protocol={client.protocol}
hasConfigureAccess={client.access?.configure}
/>
</Form>
{protocol === "saml" ? (
<SamlConfig />
) : (
!client.bearerOnly && <CapabilityConfig />
)}
{protocol === "saml" && <SamlSignature />}
<FormAccess
isHorizontal
role="manage-clients"
fineGrainedAccess={client.access?.configure}
>
{!client.bearerOnly && (
<>
<FormGroup
label={t("rootUrl")}
fieldId="kc-root-url"
labelIcon={
<HelpItem
helpText="clients-help:rootUrl"
fieldLabelId="clients:rootUrl"
/>
}
>
<KeycloakTextInput
type="text"
id="kc-root-url"
name="rootUrl"
ref={register}
sections={[
{
title: t("generalSettings"),
panel: (
<Form isHorizontal>
<ClientDescription
protocol={client.protocol}
hasConfigureAccess={client.access?.configure}
/>
</FormGroup>
<FormGroup
label={t("homeURL")}
fieldId="kc-home-url"
labelIcon={
<HelpItem
helpText="clients-help:homeURL"
fieldLabelId="clients:homeURL"
/>
}
>
<KeycloakTextInput
type="text"
id="kc-home-url"
name="baseUrl"
ref={register}
/>
</FormGroup>
<FormGroup
label={t("validRedirectUri")}
fieldId="kc-redirect"
labelIcon={
<HelpItem
helpText="clients-help:validRedirectURIs"
fieldLabelId="clients:validRedirectUri"
/>
}
>
<MultiLineInput
name="redirectUris"
aria-label={t("validRedirectUri")}
addButtonLabel="clients:addRedirectUri"
/>
</FormGroup>
{protocol === "saml" && (
<>
<FormGroup
label={t("idpInitiatedSsoUrlName")}
fieldId="idpInitiatedSsoUrlName"
labelIcon={
<HelpItem
helpText="clients-help:idpInitiatedSsoUrlName"
fieldLabelId="clients:idpInitiatedSsoUrlName"
/>
}
helperText={
idpInitiatedSsoUrlName !== "" &&
t("idpInitiatedSsoUrlNameHelp", {
url: `${environment.authServerUrl}/realms/${realm}/protocol/saml/clients/${idpInitiatedSsoUrlName}`,
})
}
>
<KeycloakTextInput
type="text"
id="idpInitiatedSsoUrlName"
name="attributes.saml_idp_initiated_sso_url_name"
ref={register}
/>
</FormGroup>
<FormGroup
label={t("idpInitiatedSsoRelayState")}
fieldId="idpInitiatedSsoRelayState"
labelIcon={
<HelpItem
helpText="clients-help:idpInitiatedSsoRelayState"
fieldLabelId="clients:idpInitiatedSsoRelayState"
/>
}
>
<KeycloakTextInput
type="text"
id="idpInitiatedSsoRelayState"
name="attributes.saml_idp_initiated_sso_relay_state"
ref={register}
/>
</FormGroup>
<FormGroup
label={t("masterSamlProcessingUrl")}
fieldId="masterSamlProcessingUrl"
labelIcon={
<HelpItem
helpText="clients-help:masterSamlProcessingUrl"
fieldLabelId="clients:masterSamlProcessingUrl"
/>
}
>
<KeycloakTextInput
type="text"
id="masterSamlProcessingUrl"
name="adminUrl"
ref={register}
/>
</FormGroup>
</>
)}
{protocol !== "saml" && (
<FormGroup
label={t("webOrigins")}
fieldId="kc-web-origins"
labelIcon={
<HelpItem
helpText="clients-help:webOrigins"
fieldLabelId="clients:webOrigins"
/>
}
>
<MultiLineInput
name="webOrigins"
aria-label={t("webOrigins")}
addButtonLabel="clients:addWebOrigins"
/>
</FormGroup>
)}
</>
)}
{protocol !== "saml" && (
<FormGroup
label={t("adminURL")}
fieldId="kc-admin-url"
labelIcon={
<HelpItem
helpText="clients-help:adminURL"
fieldLabelId="clients:adminURL"
/>
}
>
<KeycloakTextInput
type="text"
id="kc-admin-url"
name="adminUrl"
ref={register}
/>
</FormGroup>
)}
{client.bearerOnly && (
<SaveReset
className="keycloak__form_actions"
name="settings"
save={save}
reset={reset}
isActive={!isManager}
/>
)}
</FormAccess>
<FormAccess
isHorizontal
role="manage-clients"
fineGrainedAccess={client.access?.configure}
>
<FormGroup
label={t("loginTheme")}
labelIcon={
<HelpItem
helpText="clients-help:loginTheme"
fieldLabelId="clients:loginTheme"
/>
}
fieldId="loginTheme"
>
<Controller
name="attributes.login_theme"
defaultValue=""
control={control}
render={({ onChange, value }) => (
<Select
toggleId="loginTheme"
onToggle={setLoginThemeOpen}
onSelect={(_, value) => {
onChange(value.toString());
setLoginThemeOpen(false);
}}
selections={value || t("common:choose")}
variant={SelectVariant.single}
aria-label={t("loginTheme")}
isOpen={loginThemeOpen}
>
{[
<SelectOption key="empty" value="">
{t("common:choose")}
</SelectOption>,
...loginThemes.map((theme) => (
<SelectOption
selected={theme.name === value}
key={theme.name}
value={theme.name}
/>
)),
]}
</Select>
)}
/>
</FormGroup>
<FormGroup
label={t("consentRequired")}
labelIcon={
<HelpItem
helpText="clients-help:consentRequired"
fieldLabelId="clients:consentRequired"
/>
}
fieldId="kc-consent"
hasNoPaddingTop
>
<Controller
name="consentRequired"
defaultValue={false}
control={control}
render={({ onChange, value }) => (
<Switch
id="kc-consent-switch"
label={t("common:on")}
labelOff={t("common:off")}
isChecked={value}
onChange={onChange}
/>
)}
/>
</FormGroup>
<FormGroup
label={t("displayOnClient")}
labelIcon={
<HelpItem
helpText="clients-help:displayOnClient"
fieldLabelId="clients:displayOnClient"
/>
}
fieldId="kc-display-on-client"
hasNoPaddingTop
>
<Controller
name="attributes.display.on.consent.screen"
defaultValue={false}
control={control}
render={({ onChange, value }) => (
<Switch
id="kc-display-on-client-switch"
label={t("common:on")}
labelOff={t("common:off")}
isChecked={value === "true"}
onChange={(value) => onChange("" + value)}
isDisabled={!consentRequired}
/>
)}
/>
</FormGroup>
<FormGroup
label={t("consentScreenText")}
labelIcon={
<HelpItem
helpText="clients-help:consentScreenText"
fieldLabelId="clients:consentScreenText"
/>
}
fieldId="kc-consent-screen-text"
>
<KeycloakTextArea
id="kc-consent-screen-text"
name="attributes.consent.screen.text"
ref={register}
isDisabled={!(consentRequired && displayOnConsentScreen === "true")}
/>
</FormGroup>
{protocol === "saml" && (
<SaveReset
className="keycloak__form_actions"
name="settings"
save={save}
reset={reset}
isActive={isManager}
/>
)}
</FormAccess>
<FormAccess
isHorizontal
role="manage-clients"
fineGrainedAccess={client.access?.configure}
>
{protocol === "openid-connect" && (
<>
<FormGroup
label={t("frontchannelLogout")}
labelIcon={
<HelpItem
helpText="clients-help:frontchannelLogout"
fieldLabelId="clients:frontchannelLogout"
/>
}
fieldId="frontchannelLogout"
hasNoPaddingTop
>
<Controller
name="frontchannelLogout"
defaultValue={true}
control={control}
render={({ onChange, value }) => (
<Switch
id="frontchannelLogout"
label={t("common:on")}
labelOff={t("common:off")}
isChecked={value.toString() === "true"}
onChange={(value) => onChange(value.toString())}
/>
)}
/>
</FormGroup>
{frontchannelLogout?.toString() === "true" && (
<FormGroup
label={t("frontchannelLogoutUrl")}
fieldId="frontchannelLogoutUrl"
labelIcon={
<HelpItem
helpText="clients-help:frontchannelLogoutUrl"
fieldLabelId="clients:frontchannelLogoutUrl"
/>
}
helperTextInvalid={
errors.attributes?.frontchannel?.logout?.url?.message
}
validated={
errors.attributes?.frontchannel?.logout?.url?.message
? ValidatedOptions.error
: ValidatedOptions.default
}
>
<KeycloakTextInput
type="text"
id="frontchannelLogoutUrl"
name="attributes.frontchannel.logout.url"
ref={register({
validate: (uri) =>
((uri.startsWith("https://") ||
uri.startsWith("http://")) &&
!uri.includes("*")) ||
uri === "" ||
t("frontchannelUrlInvalid").toString(),
})}
validated={
errors.attributes?.frontchannel?.logout?.url?.message
? ValidatedOptions.error
: ValidatedOptions.default
}
/>
</FormGroup>
)}
</>
)}
<FormGroup
label={t("backchannelLogoutUrl")}
fieldId="backchannelLogoutUrl"
labelIcon={
<HelpItem
helpText="clients-help:backchannelLogoutUrl"
fieldLabelId="clients:backchannelLogoutUrl"
/>
}
helperTextInvalid={
errors.attributes?.backchannel?.logout?.url?.message
}
validated={
errors.attributes?.backchannel?.logout?.url?.message
? ValidatedOptions.error
: ValidatedOptions.default
}
>
<KeycloakTextInput
type="text"
id="backchannelLogoutUrl"
name="attributes.backchannel.logout.url"
ref={register({
validate: (uri) =>
((uri.startsWith("https://") || uri.startsWith("http://")) &&
!uri.includes("*")) ||
uri === "" ||
t("backchannelUrlInvalid").toString(),
})}
validated={
errors.attributes?.backchannel?.logout?.url?.message
? ValidatedOptions.error
: ValidatedOptions.default
}
/>
</FormGroup>
<FormGroup
label={t("backchannelLogoutSessionRequired")}
labelIcon={
<HelpItem
helpText="clients-help:backchannelLogoutSessionRequired"
fieldLabelId="clients:backchannelLogoutSessionRequired"
/>
}
fieldId="backchannelLogoutSessionRequired"
hasNoPaddingTop
>
<Controller
name="attributes.backchannel.logout.session.required"
defaultValue="true"
control={control}
render={({ onChange, value }) => (
<Switch
id="backchannelLogoutSessionRequired"
label={t("common:on")}
labelOff={t("common:off")}
isChecked={value === "true"}
onChange={(value) => onChange(value.toString())}
/>
)}
/>
</FormGroup>
<FormGroup
label={t("backchannelLogoutRevokeOfflineSessions")}
labelIcon={
<HelpItem
helpText="clients-help:backchannelLogoutRevokeOfflineSessions"
fieldLabelId="clients:backchannelLogoutRevokeOfflineSessions"
/>
}
fieldId="backchannelLogoutRevokeOfflineSessions"
hasNoPaddingTop
>
<Controller
name="attributes.backchannel.logout.revoke.offline.tokens"
defaultValue="false"
control={control}
render={({ onChange, value }) => (
<Switch
id="backchannelLogoutRevokeOfflineSessions"
label={t("common:on")}
labelOff={t("common:off")}
isChecked={value === "true"}
onChange={(value) => onChange(value.toString())}
/>
)}
/>
</FormGroup>
<SaveReset
className="keycloak__form_actions"
name="settings"
save={save}
reset={reset}
isActive={isManager}
/>
</FormAccess>
</ScrollForm>
</Form>
),
},
{
title: t("accessSettings"),
panel: <AccessSettings {...props} />,
},
{
title: t("samlCapabilityConfig"),
isHidden: protocol !== "saml" || client.bearerOnly,
panel: <SamlConfig />,
},
{
title: t("signatureAndEncryption"),
isHidden: protocol !== "saml" || client.bearerOnly,
panel: <SamlSignature />,
},
{
title: t("capabilityConfig"),
isHidden: protocol !== "openid-connect" || client.bearerOnly,
panel: <CapabilityConfig />,
},
{
title: t("loginSettings"),
isHidden: protocol !== "openid-connect" || client.bearerOnly,
panel: <LoginSettingsPanel access={client.access?.configure} />,
},
{
title: t("logoutSettings"),
isHidden: client.bearerOnly,
panel: <LogoutPanel {...props} />,
},
]}
/>
);
};

View file

@ -0,0 +1,203 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { useFormContext } from "react-hook-form";
import { FormGroup } from "@patternfly/react-core";
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 { useAccess } from "../../context/access/Access";
export const AccessSettings = ({
client,
save,
reset,
}: ClientSettingsProps) => {
const { t } = useTranslation("clients");
const { register, watch } = useFormContext<ClientRepresentation>();
const { realm } = useRealm();
const { hasAccess } = useAccess();
const isManager = hasAccess("manage-clients") || client.access?.configure;
const protocol = watch("protocol");
const idpInitiatedSsoUrlName: string = watch(
"attributes.saml_idp_initiated_sso_url_name"
);
return (
<FormAccess
isHorizontal
fineGrainedAccess={client.access?.configure}
role="manage-clients"
>
{!client.bearerOnly && (
<>
<FormGroup
label={t("rootUrl")}
fieldId="kc-root-url"
labelIcon={
<HelpItem
helpText="clients-help:rootUrl"
fieldLabelId="clients:rootUrl"
/>
}
>
<KeycloakTextInput
type="text"
id="kc-root-url"
name="rootUrl"
ref={register}
/>
</FormGroup>
<FormGroup
label={t("homeURL")}
fieldId="kc-home-url"
labelIcon={
<HelpItem
helpText="clients-help:homeURL"
fieldLabelId="clients:homeURL"
/>
}
>
<KeycloakTextInput
type="text"
id="kc-home-url"
name="baseUrl"
ref={register}
/>
</FormGroup>
<FormGroup
label={t("validRedirectUri")}
fieldId="kc-redirect"
labelIcon={
<HelpItem
helpText="clients-help:validRedirectURIs"
fieldLabelId="clients:validRedirectUri"
/>
}
>
<MultiLineInput
name="redirectUris"
aria-label={t("validRedirectUri")}
addButtonLabel="clients:addRedirectUri"
/>
</FormGroup>
{protocol === "saml" && (
<>
<FormGroup
label={t("idpInitiatedSsoUrlName")}
fieldId="idpInitiatedSsoUrlName"
labelIcon={
<HelpItem
helpText="clients-help:idpInitiatedSsoUrlName"
fieldLabelId="clients:idpInitiatedSsoUrlName"
/>
}
helperText={
idpInitiatedSsoUrlName !== "" &&
t("idpInitiatedSsoUrlNameHelp", {
url: `${environment.authServerUrl}/realms/${realm}/protocol/saml/clients/${idpInitiatedSsoUrlName}`,
})
}
>
<KeycloakTextInput
type="text"
id="idpInitiatedSsoUrlName"
name="attributes.saml_idp_initiated_sso_url_name"
ref={register}
/>
</FormGroup>
<FormGroup
label={t("idpInitiatedSsoRelayState")}
fieldId="idpInitiatedSsoRelayState"
labelIcon={
<HelpItem
helpText="clients-help:idpInitiatedSsoRelayState"
fieldLabelId="clients:idpInitiatedSsoRelayState"
/>
}
>
<KeycloakTextInput
type="text"
id="idpInitiatedSsoRelayState"
name="attributes.saml_idp_initiated_sso_relay_state"
ref={register}
/>
</FormGroup>
<FormGroup
label={t("masterSamlProcessingUrl")}
fieldId="masterSamlProcessingUrl"
labelIcon={
<HelpItem
helpText="clients-help:masterSamlProcessingUrl"
fieldLabelId="clients:masterSamlProcessingUrl"
/>
}
>
<KeycloakTextInput
type="text"
id="masterSamlProcessingUrl"
name="adminUrl"
ref={register}
/>
</FormGroup>
</>
)}
{protocol !== "saml" && (
<FormGroup
label={t("webOrigins")}
fieldId="kc-web-origins"
labelIcon={
<HelpItem
helpText="clients-help:webOrigins"
fieldLabelId="clients:webOrigins"
/>
}
>
<MultiLineInput
name="webOrigins"
aria-label={t("webOrigins")}
addButtonLabel="clients:addWebOrigins"
/>
</FormGroup>
)}
</>
)}
{protocol !== "saml" && (
<FormGroup
label={t("adminURL")}
fieldId="kc-admin-url"
labelIcon={
<HelpItem
helpText="clients-help:adminURL"
fieldLabelId="clients:adminURL"
/>
}
>
<KeycloakTextInput
type="text"
id="kc-admin-url"
name="adminUrl"
ref={register}
/>
</FormGroup>
)}
{client.bearerOnly && (
<SaveReset
className="keycloak__form_actions"
name="settings"
save={save}
reset={reset}
isActive={!isManager}
/>
)}
</FormAccess>
);
};

View file

@ -0,0 +1,146 @@
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Controller, useFormContext } from "react-hook-form";
import {
FormGroup,
Select,
SelectOption,
SelectVariant,
Switch,
} from "@patternfly/react-core";
import type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation";
import { FormAccess } from "../../components/form-access/FormAccess";
import { HelpItem } from "../../components/help-enabler/HelpItem";
import { useServerInfo } from "../../context/server-info/ServerInfoProvider";
import { KeycloakTextArea } from "../../components/keycloak-text-area/KeycloakTextArea";
export const LoginSettingsPanel = ({ access }: { access?: boolean }) => {
const { t } = useTranslation("clients");
const { register, control, watch } = useFormContext<ClientRepresentation>();
const [loginThemeOpen, setLoginThemeOpen] = useState(false);
const loginThemes = useServerInfo().themes!["login"];
const consentRequired = watch("consentRequired");
const displayOnConsentScreen: string = watch(
"attributes.display.on.consent.screen"
);
return (
<FormAccess isHorizontal fineGrainedAccess={access} role="manage-clients">
<FormGroup
label={t("loginTheme")}
labelIcon={
<HelpItem
helpText="clients-help:loginTheme"
fieldLabelId="clients:loginTheme"
/>
}
fieldId="loginTheme"
>
<Controller
name="attributes.login_theme"
defaultValue=""
control={control}
render={({ onChange, value }) => (
<Select
toggleId="loginTheme"
onToggle={setLoginThemeOpen}
onSelect={(_, value) => {
onChange(value.toString());
setLoginThemeOpen(false);
}}
selections={value || t("common:choose")}
variant={SelectVariant.single}
aria-label={t("loginTheme")}
isOpen={loginThemeOpen}
>
{[
<SelectOption key="empty" value="">
{t("common:choose")}
</SelectOption>,
...loginThemes.map((theme) => (
<SelectOption
selected={theme.name === value}
key={theme.name}
value={theme.name}
/>
)),
]}
</Select>
)}
/>
</FormGroup>
<FormGroup
label={t("consentRequired")}
labelIcon={
<HelpItem
helpText="clients-help:consentRequired"
fieldLabelId="clients:consentRequired"
/>
}
fieldId="kc-consent"
hasNoPaddingTop
>
<Controller
name="consentRequired"
defaultValue={false}
control={control}
render={({ onChange, value }) => (
<Switch
id="kc-consent-switch"
label={t("common:on")}
labelOff={t("common:off")}
isChecked={value}
onChange={onChange}
/>
)}
/>
</FormGroup>
<FormGroup
label={t("displayOnClient")}
labelIcon={
<HelpItem
helpText="clients-help:displayOnClient"
fieldLabelId="clients:displayOnClient"
/>
}
fieldId="kc-display-on-client"
hasNoPaddingTop
>
<Controller
name="attributes.display.on.consent.screen"
defaultValue={false}
control={control}
render={({ onChange, value }) => (
<Switch
id="kc-display-on-client-switch"
label={t("common:on")}
labelOff={t("common:off")}
isChecked={value === "true"}
onChange={(value) => onChange("" + value)}
isDisabled={!consentRequired}
/>
)}
/>
</FormGroup>
<FormGroup
label={t("consentScreenText")}
labelIcon={
<HelpItem
helpText="clients-help:consentScreenText"
fieldLabelId="clients:consentScreenText"
/>
}
fieldId="kc-consent-screen-text"
>
<KeycloakTextArea
id="kc-consent-screen-text"
name="attributes.consent.screen.text"
ref={register}
isDisabled={!(consentRequired && displayOnConsentScreen === "true")}
/>
</FormGroup>
</FormAccess>
);
};

View file

@ -0,0 +1,203 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { Controller, useFormContext } from "react-hook-form";
import { FormGroup, Switch, ValidatedOptions } from "@patternfly/react-core";
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 { SaveReset } from "../advanced/SaveReset";
export const LogoutPanel = ({
save,
reset,
client: { access },
}: ClientSettingsProps) => {
const { t } = useTranslation("clients");
const {
register,
control,
watch,
formState: { errors },
} = useFormContext<ClientRepresentation>();
const { hasAccess } = useAccess();
const isManager = hasAccess("manage-clients") || access?.configure;
const protocol = watch("protocol");
const frontchannelLogout = watch("frontchannelLogout");
return (
<FormAccess
isHorizontal
fineGrainedAccess={access?.configure}
role="manage-clients"
>
{protocol === "openid-connect" && (
<>
<FormGroup
label={t("frontchannelLogout")}
labelIcon={
<HelpItem
helpText="clients-help:frontchannelLogout"
fieldLabelId="clients:frontchannelLogout"
/>
}
fieldId="frontchannelLogout"
hasNoPaddingTop
>
<Controller
name="frontchannelLogout"
defaultValue={true}
control={control}
render={({ onChange, value }) => (
<Switch
id="frontchannelLogout"
label={t("common:on")}
labelOff={t("common:off")}
isChecked={value.toString() === "true"}
onChange={(value) => onChange(value.toString())}
/>
)}
/>
</FormGroup>
{frontchannelLogout?.toString() === "true" && (
<FormGroup
label={t("frontchannelLogoutUrl")}
fieldId="frontchannelLogoutUrl"
labelIcon={
<HelpItem
helpText="clients-help:frontchannelLogoutUrl"
fieldLabelId="clients:frontchannelLogoutUrl"
/>
}
helperTextInvalid={
errors.attributes?.frontchannel?.logout?.url?.message
}
validated={
errors.attributes?.frontchannel?.logout?.url?.message
? ValidatedOptions.error
: ValidatedOptions.default
}
>
<KeycloakTextInput
type="text"
id="frontchannelLogoutUrl"
name="attributes.frontchannel.logout.url"
ref={register({
validate: (uri) =>
((uri.startsWith("https://") ||
uri.startsWith("http://")) &&
!uri.includes("*")) ||
uri === "" ||
t("frontchannelUrlInvalid").toString(),
})}
validated={
errors.attributes?.frontchannel?.logout?.url?.message
? ValidatedOptions.error
: ValidatedOptions.default
}
/>
</FormGroup>
)}
</>
)}
<FormGroup
label={t("backchannelLogoutUrl")}
fieldId="backchannelLogoutUrl"
labelIcon={
<HelpItem
helpText="clients-help:backchannelLogoutUrl"
fieldLabelId="clients:backchannelLogoutUrl"
/>
}
helperTextInvalid={errors.attributes?.backchannel?.logout?.url?.message}
validated={
errors.attributes?.backchannel?.logout?.url?.message
? ValidatedOptions.error
: ValidatedOptions.default
}
>
<KeycloakTextInput
type="text"
id="backchannelLogoutUrl"
name="attributes.backchannel.logout.url"
ref={register({
validate: (uri) =>
((uri.startsWith("https://") || uri.startsWith("http://")) &&
!uri.includes("*")) ||
uri === "" ||
t("backchannelUrlInvalid").toString(),
})}
validated={
errors.attributes?.backchannel?.logout?.url?.message
? ValidatedOptions.error
: ValidatedOptions.default
}
/>
</FormGroup>
<FormGroup
label={t("backchannelLogoutSessionRequired")}
labelIcon={
<HelpItem
helpText="clients-help:backchannelLogoutSessionRequired"
fieldLabelId="clients:backchannelLogoutSessionRequired"
/>
}
fieldId="backchannelLogoutSessionRequired"
hasNoPaddingTop
>
<Controller
name="attributes.backchannel.logout.session.required"
defaultValue="true"
control={control}
render={({ onChange, value }) => (
<Switch
id="backchannelLogoutSessionRequired"
label={t("common:on")}
labelOff={t("common:off")}
isChecked={value === "true"}
onChange={(value) => onChange(value.toString())}
/>
)}
/>
</FormGroup>
<FormGroup
label={t("backchannelLogoutRevokeOfflineSessions")}
labelIcon={
<HelpItem
helpText="clients-help:backchannelLogoutRevokeOfflineSessions"
fieldLabelId="clients:backchannelLogoutRevokeOfflineSessions"
/>
}
fieldId="backchannelLogoutRevokeOfflineSessions"
hasNoPaddingTop
>
<Controller
name="attributes.backchannel.logout.revoke.offline.tokens"
defaultValue="false"
control={control}
render={({ onChange, value }) => (
<Switch
id="backchannelLogoutRevokeOfflineSessions"
label={t("common:on")}
labelOff={t("common:off")}
isChecked={value === "true"}
onChange={(value) => onChange(value.toString())}
/>
)}
/>
</FormGroup>
<SaveReset
className="keycloak__form_actions"
name="settings"
save={save}
reset={reset}
isActive={isManager}
/>
</FormAccess>
);
};

View file

@ -0,0 +1,200 @@
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Controller, useFormContext } from "react-hook-form";
import {
AlertVariant,
Button,
ButtonVariant,
ExpandableSection,
FormGroup,
Split,
SplitItem,
ToolbarItem,
} from "@patternfly/react-core";
import moment from "moment";
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";
import { HelpItem } from "../../components/help-enabler/HelpItem";
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";
export const ClusteringPanel = ({
save,
client: { id, registeredNodes, access },
}: AdvancedProps) => {
const { t } = useTranslation("clients");
const { control } = useFormContext();
const adminClient = useAdminClient();
const { addAlert, addError } = useAlerts();
const [nodes, setNodes] = useState(registeredNodes || {});
const [expanded, setExpanded] = useState(false);
const [selectedNode, setSelectedNode] = useState("");
const [addNodeOpen, setAddNodeOpen] = useState(false);
const [key, setKey] = useState(0);
const refresh = () => setKey(new Date().getTime());
const testCluster = async () => {
const result = await adminClient.clients.testNodesAvailable({ id: id! });
parseResult(result, "testCluster", addAlert, t);
};
const [toggleDeleteNodeConfirm, DeleteNodeConfirm] = useConfirmDialog({
titleKey: "clients:deleteNode",
messageKey: t("deleteNodeBody", {
node: selectedNode,
}),
continueButtonLabel: "common:delete",
continueButtonVariant: ButtonVariant.danger,
onConfirm: async () => {
try {
await adminClient.clients.deleteClusterNode({
id: id!,
node: selectedNode,
});
setNodes({
...Object.keys(nodes).reduce((object: any, key) => {
if (key !== selectedNode) {
object[key] = nodes[key];
}
return object;
}, {}),
});
refresh();
addAlert(t("deleteNodeSuccess"), AlertVariant.success);
} catch (error) {
addError("clients:deleteNodeFail", error);
}
},
});
return (
<>
<FormAccess
role="manage-clients"
fineGrainedAccess={access?.configure}
isHorizontal
>
<FormGroup
label={t("nodeReRegistrationTimeout")}
fieldId="kc-node-reregistration-timeout"
labelIcon={
<HelpItem
helpText="clients-help:nodeReRegistrationTimeout"
fieldLabelId="clients:nodeReRegistrationTimeout"
/>
}
>
<Split hasGutter>
<SplitItem>
<Controller
name="nodeReRegistrationTimeout"
defaultValue=""
control={control}
render={({ onChange, value }) => (
<TimeSelector value={value} onChange={onChange} />
)}
/>
</SplitItem>
<SplitItem>
<Button variant={ButtonVariant.secondary} onClick={() => save()}>
{t("common:save")}
</Button>
</SplitItem>
</Split>
</FormGroup>
</FormAccess>
<>
<DeleteNodeConfirm />
<AddHostDialog
clientId={id!}
isOpen={addNodeOpen}
onAdded={(node) => {
nodes[node] = moment.now() / 1000;
refresh();
}}
onClose={() => setAddNodeOpen(false)}
/>
<ExpandableSection
toggleText={t("registeredClusterNodes")}
onToggle={setExpanded}
isExpanded={expanded}
>
<KeycloakDataTable
key={key}
ariaLabelKey="registeredClusterNodes"
loader={() =>
Promise.resolve(
Object.entries(nodes || {}).map((entry) => {
return { host: entry[0], registration: entry[1] };
})
)
}
toolbarItem={
<>
<ToolbarItem>
<Button
id="testClusterAvailability"
onClick={testCluster}
variant={ButtonVariant.secondary}
isDisabled={Object.keys(nodes).length === 0}
>
{t("testClusterAvailability")}
</Button>
</ToolbarItem>
<ToolbarItem>
<Button
id="registerNodeManually"
onClick={() => setAddNodeOpen(true)}
variant={ButtonVariant.tertiary}
>
{t("registerNodeManually")}
</Button>
</ToolbarItem>
</>
}
actions={[
{
title: t("common:delete"),
onRowClick: (node) => {
setSelectedNode(node.host);
toggleDeleteNodeConfirm();
},
},
]}
columns={[
{
name: "host",
displayKey: "clients:nodeHost",
},
{
name: "registration",
displayKey: "clients:lastRegistration",
cellFormatters: [
(value) =>
value
? moment(parseInt(value.toString()) * 1000).format("LLL")
: "",
],
},
]}
emptyState={
<ListEmptyState
message={t("noNodes")}
instructions={t("noNodesInstructions")}
primaryActionText={t("registerNodeManually")}
onPrimaryAction={() => setAddNodeOpen(true)}
/>
}
/>
</ExpandableSection>
</>
</>
);
};

View file

@ -0,0 +1,137 @@
import React, { useEffect, useRef } from "react";
import { Link } from "react-router-dom";
import { Trans, useTranslation } from "react-i18next";
import { useFormContext } from "react-hook-form";
import {
FormGroup,
InputGroup,
Button,
ActionGroup,
Tooltip,
Text,
} from "@patternfly/react-core";
import moment from "moment";
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";
export const RevocationPanel = ({
save,
client: { id, adminUrl, access },
}: AdvancedProps) => {
const revocationFieldName = "notBefore";
const pushRevocationButtonRef = useRef<HTMLElement>();
const { t } = useTranslation("clients");
const adminClient = useAdminClient();
const { realm } = useRealm();
const { addAlert } = useAlerts();
const { getValues, setValue, register } = useFormContext();
const setNotBefore = (time: number, messageKey: string) => {
setValue(revocationFieldName, time);
save({ messageKey });
};
useEffect(() => {
register(revocationFieldName);
}, [register]);
const formatDate = () => {
const date = getValues(revocationFieldName);
if (date > 0) {
return moment(date * 1000).format("LLL");
} else {
return t("common:none");
}
};
const push = async () => {
const result = await adminClient.clients.pushRevocation({
id: id!,
});
parseResult(result, "notBeforePush", addAlert, t);
};
return (
<>
<Text className="pf-u-pb-lg">
<Trans i18nKey="clients-help:notBeforeIntro">
In order to successfully push setup url on
<Link to={toClient({ realm, clientId: id!, tab: "settings" })}>
{t("settings")}
</Link>
tab
</Trans>
</Text>
<FormAccess
role="manage-clients"
fineGrainedAccess={access?.configure}
isHorizontal
>
<FormGroup
label={t("notBefore")}
fieldId="kc-not-before"
labelIcon={
<HelpItem
helpText="clients-help:notBefore"
fieldLabelId="clients:notBefore"
/>
}
>
<InputGroup>
<KeycloakTextInput
type="text"
id="kc-not-before"
name="notBefore"
isReadOnly
value={formatDate()}
/>
<Button
id="setToNow"
variant="control"
onClick={() => {
setNotBefore(moment.now() / 1000, "notBeforeSetToNow");
}}
>
{t("setToNow")}
</Button>
<Button
id="clear"
variant="control"
onClick={() => {
setNotBefore(0, "notBeforeNowClear");
}}
>
{t("clear")}
</Button>
</InputGroup>
</FormGroup>
<ActionGroup>
{!adminUrl && (
<Tooltip
reference={pushRevocationButtonRef}
content={t("clients-help:notBeforeTooltip")}
/>
)}
<Button
id="push"
variant="secondary"
onClick={push}
isAriaDisabled={!adminUrl}
ref={pushRevocationButtonRef}
>
{t("push")}
</Button>
</ActionGroup>
</FormAccess>
</>
);
};

View file

@ -8,14 +8,17 @@ import useRequiredContext from "../../utils/useRequiredContext";
import useSetTimeout from "../../utils/useSetTimeout";
import { AlertPanel, AlertType } from "./AlertPanel";
type AlertProps = {
addAlert: (
message: string,
variant?: AlertVariant,
description?: string
) => void;
export type AddAlertFunction = (
message: string,
variant?: AlertVariant,
description?: string
) => void;
addError: (message: string, error: any) => void;
export type AddErrorFunction = (message: string, error: any) => void;
type AlertProps = {
addAlert: AddAlertFunction;
addError: AddErrorFunction;
};
export const AlertContext = createContext<AlertProps | undefined>(undefined);

View file

@ -1,4 +1,4 @@
import React, { Children, Fragment, FunctionComponent } from "react";
import React, { Fragment, FunctionComponent, ReactNode, useMemo } from "react";
import { useTranslation } from "react-i18next";
import {
Grid,
@ -15,8 +15,14 @@ import { FormPanel } from "./FormPanel";
import "./scroll-form.css";
type ScrollSection = {
title: string;
panel: ReactNode;
isHidden?: boolean;
};
type ScrollFormProps = GridProps & {
sections: string[];
sections: ScrollSection[];
borders?: boolean;
};
@ -27,33 +33,34 @@ const spacesToHyphens = (string: string): string => {
export const ScrollForm: FunctionComponent<ScrollFormProps> = ({
sections,
borders = false,
children,
...rest
}) => {
const { t } = useTranslation("common");
const nodes = Children.toArray(children);
const shownSections = useMemo(
() => sections.filter(({ isHidden }) => !isHidden),
[sections]
);
return (
<Grid hasGutter {...rest}>
<GridItem span={8}>
{sections.map((cat, index) => {
const scrollId = spacesToHyphens(cat.toLowerCase());
{shownSections.map(({ title, panel }) => {
const scrollId = spacesToHyphens(title.toLowerCase());
return (
<Fragment key={cat}>
{!borders && (
<ScrollPanel scrollId={scrollId} title={cat}>
{nodes[index]}
</ScrollPanel>
)}
{borders && (
<Fragment key={title}>
{borders ? (
<FormPanel
scrollId={scrollId}
title={cat}
title={title}
className="kc-form-panel__panel"
>
{nodes[index]}
{panel}
</FormPanel>
) : (
<ScrollPanel scrollId={scrollId} title={title}>
{panel}
</ScrollPanel>
)}
</Fragment>
);
@ -69,17 +76,17 @@ export const ScrollForm: FunctionComponent<ScrollFormProps> = ({
label={t("jumpToSection")}
offset={100}
>
{sections.map((cat) => {
const scrollId = spacesToHyphens(cat.toLowerCase());
{shownSections.map(({ title }) => {
const scrollId = spacesToHyphens(title.toLowerCase());
return (
// note that JumpLinks currently does not work with spaces in the href
<JumpLinksItem
key={cat}
key={title}
href={`#${scrollId}`}
data-testid={`jump-link-${scrollId}`}
>
{cat}
{title}
</JumpLinksItem>
);
})}

View file

@ -238,8 +238,6 @@ export default function DetailSettings() {
return <KeycloakSpinner />;
}
const sections = [t("generalSettings"), t("advancedSettings")];
const isOIDC = provider.providerId!.includes("oidc");
const isSAML = provider.providerId!.includes("saml");
@ -268,14 +266,81 @@ export default function DetailSettings() {
return components;
};
if (isOIDC) {
sections.splice(1, 0, t("oidcSettings"));
}
const sections = [
{
title: t("generalSettings"),
panel: (
<FormAccess
role="manage-identity-providers"
isHorizontal
onSubmit={handleSubmit(save)}
>
{!isOIDC && !isSAML && <GeneralSettings create={false} id={alias} />}
{isOIDC && <OIDCGeneralSettings id={alias} />}
{isSAML && <SamlGeneralSettings id={alias} />}
</FormAccess>
),
},
{
title: t("oidcSettings"),
isHidden: !isOIDC,
panel: (
<>
<DiscoverySettings readOnly={false} />
<Form isHorizontal className="pf-u-py-lg">
<Divider />
<OIDCAuthentication create={false} />
</Form>
<ExtendedNonDiscoverySettings />
</>
),
},
{
title: t("samlSettings"),
isHidden: !isSAML,
panel: <DescriptorSettings readOnly={false} />,
},
{
title: t("reqAuthnConstraints"),
isHidden: !isSAML,
panel: (
<FormAccess
role="manage-identity-providers"
isHorizontal
onSubmit={handleSubmit(save)}
>
<ReqAuthnConstraints />
</FormAccess>
),
},
{
title: t("advancedSettings"),
panel: (
<FormAccess
role="manage-identity-providers"
isHorizontal
onSubmit={handleSubmit(save)}
>
<AdvancedSettings isOIDC={isOIDC!} isSAML={isSAML!} />
if (isSAML) {
sections.splice(1, 0, t("samlSettings"));
sections.splice(2, 0, t("reqAuthnConstraints"));
}
<ActionGroup className="keycloak__form_actions">
<Button data-testid={"save"} type="submit">
{t("common:save")}
</Button>
<Button
data-testid={"revert"}
variant="link"
onClick={() => {
reset();
}}
>
{t("common:revert")}
</Button>
</ActionGroup>
</FormAccess>
),
},
];
return (
<FormProvider {...form}>
@ -302,61 +367,7 @@ export default function DetailSettings() {
eventKey="settings"
title={<TabTitleText>{t("common:settings")}</TabTitleText>}
>
<ScrollForm className="pf-u-px-lg" sections={sections}>
<FormAccess
role="manage-identity-providers"
isHorizontal
onSubmit={handleSubmit(save)}
>
{!isOIDC && !isSAML && (
<GeneralSettings create={false} id={alias} />
)}
{isOIDC && <OIDCGeneralSettings id={alias} />}
{isSAML && <SamlGeneralSettings id={alias} />}
</FormAccess>
{isOIDC && (
<>
<DiscoverySettings readOnly={false} />
<Form isHorizontal className="pf-u-py-lg">
<Divider />
<OIDCAuthentication create={false} />
</Form>
<ExtendedNonDiscoverySettings />
</>
)}
{isSAML && <DescriptorSettings readOnly={false} />}
{isSAML && (
<FormAccess
role="manage-identity-providers"
isHorizontal
onSubmit={handleSubmit(save)}
>
<ReqAuthnConstraints />
</FormAccess>
)}
<FormAccess
role="manage-identity-providers"
isHorizontal
onSubmit={handleSubmit(save)}
>
<AdvancedSettings isOIDC={isOIDC!} isSAML={isSAML!} />
<ActionGroup className="keycloak__form_actions">
<Button data-testid={"save"} type="submit">
{t("common:save")}
</Button>
<Button
data-testid={"revert"}
variant="link"
onClick={() => {
reset();
}}
>
{t("common:revert")}
</Button>
</ActionGroup>
</FormAccess>
</ScrollForm>
<ScrollForm className="pf-u-px-lg" sections={sections} />
</Tab>
<Tab
id="mappers"

View file

@ -69,17 +69,12 @@ const CreateAttributeFormContent = ({
<UserProfileProvider>
<ScrollForm
sections={[
t("generalSettings"),
t("permission"),
t("validations"),
t("annotations"),
{ title: t("generalSettings"), panel: <AttributeGeneralSettings /> },
{ title: t("permission"), panel: <AttributePermission /> },
{ title: t("validations"), panel: <AttributeValidations /> },
{ title: t("annotations"), panel: <AttributeAnnotations /> },
]}
>
<AttributeGeneralSettings />
<AttributePermission />
<AttributeValidations />
<AttributeAnnotations />
</ScrollForm>
/>
<Form onSubmit={form.handleSubmit(save)}>
<ActionGroup className="keycloak__form_actions">
<Button

View file

@ -55,23 +55,33 @@ const AddLdapFormContent = ({
<>
<ScrollForm
sections={[
t("generalOptions"),
t("connectionAndAuthenticationSettings"),
t("ldapSearchingAndUpdatingSettings"),
t("synchronizationSettings"),
t("kerberosIntegration"),
t("cacheSettings"),
t("advancedSettings"),
{
title: t("generalOptions"),
panel: <LdapSettingsGeneral form={form} vendorEdit={!!id} />,
},
{
title: t("connectionAndAuthenticationSettings"),
panel: <LdapSettingsConnection form={form} id={id} />,
},
{
title: t("ldapSearchingAndUpdatingSettings"),
panel: <LdapSettingsSearching form={form} />,
},
{
title: t("synchronizationSettings"),
panel: <LdapSettingsSynchronization form={form} />,
},
{
title: t("kerberosIntegration"),
panel: <LdapSettingsKerberosIntegration form={form} />,
},
{ title: t("cacheSettings"), panel: <SettingsCache form={form} /> },
{
title: t("advancedSettings"),
panel: <LdapSettingsAdvanced form={form} id={id} />,
},
]}
>
<LdapSettingsGeneral form={form} vendorEdit={!!id} />
<LdapSettingsConnection form={form} id={id} />
<LdapSettingsSearching form={form} />
<LdapSettingsSynchronization form={form} />
<LdapSettingsKerberosIntegration form={form} />
<SettingsCache form={form} />
<LdapSettingsAdvanced form={form} id={id} />
</ScrollForm>
/>
<Form onSubmit={form.handleSubmit(save)}>
<ActionGroup className="keycloak__form_actions">
<Button