Refactored scroll form so to easier hide section (#2697)
This commit is contained in:
parent
0d0e086913
commit
48f68358f4
12 changed files with 1249 additions and 1108 deletions
|
@ -1,107 +1,68 @@
|
||||||
import {
|
import React from "react";
|
||||||
ActionGroup,
|
import { useTranslation } from "react-i18next";
|
||||||
AlertVariant,
|
import { useFormContext } from "react-hook-form";
|
||||||
Button,
|
import { AlertVariant, PageSection, Text } from "@patternfly/react-core";
|
||||||
ButtonVariant,
|
|
||||||
ExpandableSection,
|
|
||||||
FormGroup,
|
|
||||||
InputGroup,
|
|
||||||
PageSection,
|
|
||||||
Split,
|
|
||||||
SplitItem,
|
|
||||||
Text,
|
|
||||||
ToolbarItem,
|
|
||||||
Tooltip,
|
|
||||||
} from "@patternfly/react-core";
|
|
||||||
import type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation";
|
import type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation";
|
||||||
import type GlobalRequestResult from "@keycloak/keycloak-admin-client/lib/defs/globalRequestResult";
|
import type GlobalRequestResult from "@keycloak/keycloak-admin-client/lib/defs/globalRequestResult";
|
||||||
import moment from "moment";
|
|
||||||
import React, { useEffect, useRef, useState } from "react";
|
import type { AddAlertFunction } from "../components/alert/Alerts";
|
||||||
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 { ScrollForm } from "../components/scroll-form/ScrollForm";
|
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 { convertToFormValues, toUpperCase } from "../util";
|
||||||
import { AddHostDialog } from "./advanced/AddHostDialog";
|
|
||||||
import { AdvancedSettings } from "./advanced/AdvancedSettings";
|
import { AdvancedSettings } from "./advanced/AdvancedSettings";
|
||||||
import { AuthenticationOverrides } from "./advanced/AuthenticationOverrides";
|
import { AuthenticationOverrides } from "./advanced/AuthenticationOverrides";
|
||||||
import { FineGrainOpenIdConnect } from "./advanced/FineGrainOpenIdConnect";
|
import { FineGrainOpenIdConnect } from "./advanced/FineGrainOpenIdConnect";
|
||||||
import { FineGrainSamlEndpointConfig } from "./advanced/FineGrainSamlEndpointConfig";
|
import { FineGrainSamlEndpointConfig } from "./advanced/FineGrainSamlEndpointConfig";
|
||||||
import { OpenIdConnectCompatibilityModes } from "./advanced/OpenIdConnectCompatibilityModes";
|
import { OpenIdConnectCompatibilityModes } from "./advanced/OpenIdConnectCompatibilityModes";
|
||||||
import type { SaveOptions } from "./ClientDetails";
|
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;
|
save: (options?: SaveOptions) => void;
|
||||||
client: ClientRepresentation;
|
client: ClientRepresentation;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AdvancedTab = ({
|
export const AdvancedTab = ({ save, client }: AdvancedProps) => {
|
||||||
save,
|
const { t } = useTranslation("clients");
|
||||||
client: {
|
const openIdConnect = "openid-connect";
|
||||||
id,
|
|
||||||
|
const { setValue, control, reset } = useFormContext();
|
||||||
|
const {
|
||||||
publicClient,
|
publicClient,
|
||||||
registeredNodes,
|
|
||||||
attributes,
|
attributes,
|
||||||
protocol,
|
protocol,
|
||||||
authenticationFlowBindingOverrides,
|
authenticationFlowBindingOverrides,
|
||||||
adminUrl,
|
} = client;
|
||||||
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
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const resetFields = (names: string[]) => {
|
const resetFields = (names: string[]) => {
|
||||||
const values: { [name: string]: string } = {};
|
const values: { [name: string]: string } = {};
|
||||||
|
@ -111,374 +72,131 @@ export const AdvancedTab = ({
|
||||||
reset(values);
|
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 (
|
return (
|
||||||
<PageSection variant="light" className="pf-u-py-0">
|
<PageSection variant="light" className="pf-u-py-0">
|
||||||
<ScrollForm sections={sections} borders>
|
<ScrollForm
|
||||||
{protocol === openIdConnect && (
|
sections={[
|
||||||
<>
|
{
|
||||||
<Text className="pf-u-pb-lg">
|
title: t("revocation"),
|
||||||
<Trans i18nKey="clients-help:notBeforeIntro">
|
isHidden: protocol !== openIdConnect,
|
||||||
In order to successfully push setup url on
|
panel: <RevocationPanel client={client} save={save} />,
|
||||||
<Link to={toClient({ realm, clientId: id!, tab: "settings" })}>
|
},
|
||||||
{t("settings")}
|
{
|
||||||
</Link>
|
title: t("clustering"),
|
||||||
tab
|
isHidden: !publicClient,
|
||||||
</Trans>
|
panel: <ClusteringPanel client={client} save={save} />,
|
||||||
</Text>
|
},
|
||||||
<FormAccess
|
{
|
||||||
role="manage-clients"
|
title: t("fineGrainOpenIdConnectConfiguration"),
|
||||||
fineGrainedAccess={access?.configure}
|
isHidden: protocol !== openIdConnect,
|
||||||
isHorizontal
|
panel: (
|
||||||
>
|
<>
|
||||||
<FormGroup
|
<Text className="pf-u-pb-lg">
|
||||||
label={t("notBefore")}
|
{t("clients-help:fineGrainOpenIdConnectConfiguration")}
|
||||||
fieldId="kc-not-before"
|
</Text>
|
||||||
labelIcon={
|
<FineGrainOpenIdConnect
|
||||||
<HelpItem
|
save={save}
|
||||||
helpText="clients-help:notBefore"
|
reset={() =>
|
||||||
fieldLabelId="clients:notBefore"
|
convertToFormValues(attributes, (key, value) =>
|
||||||
/>
|
setValue(`attributes.${key}`, value)
|
||||||
}
|
|
||||||
>
|
|
||||||
<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] };
|
|
||||||
})
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
toolbarItem={
|
/>
|
||||||
<>
|
</>
|
||||||
<ToolbarItem>
|
),
|
||||||
<Button
|
},
|
||||||
id="testClusterAvailability"
|
{
|
||||||
onClick={testCluster}
|
title: t("openIdConnectCompatibilityModes"),
|
||||||
variant={ButtonVariant.secondary}
|
isHidden: protocol !== openIdConnect,
|
||||||
isDisabled={Object.keys(nodes).length === 0}
|
panel: (
|
||||||
>
|
<>
|
||||||
{t("testClusterAvailability")}
|
<Text className="pf-u-pb-lg">
|
||||||
</Button>
|
{t("clients-help:openIdConnectCompatibilityModes")}
|
||||||
</ToolbarItem>
|
</Text>
|
||||||
<ToolbarItem>
|
<OpenIdConnectCompatibilityModes
|
||||||
<Button
|
control={control}
|
||||||
id="registerNodeManually"
|
save={() => save()}
|
||||||
onClick={() => setAddNodeOpen(true)}
|
reset={() =>
|
||||||
variant={ButtonVariant.tertiary}
|
resetFields(["exclude.session.state.from.auth.response"])
|
||||||
>
|
|
||||||
{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>
|
</>
|
||||||
</>
|
),
|
||||||
</>
|
},
|
||||||
)}
|
{
|
||||||
<>
|
title: t("fineGrainSamlEndpointConfig"),
|
||||||
{protocol === openIdConnect && (
|
isHidden: protocol === openIdConnect,
|
||||||
<>
|
panel: (
|
||||||
<Text className="pf-u-pb-lg">
|
<>
|
||||||
{t("clients-help:fineGrainOpenIdConnectConfiguration")}
|
<Text className="pf-u-pb-lg">
|
||||||
</Text>
|
{t("clients-help:fineGrainSamlEndpointConfig")}
|
||||||
<FineGrainOpenIdConnect
|
</Text>
|
||||||
save={() => save()}
|
<FineGrainSamlEndpointConfig
|
||||||
reset={() =>
|
control={control}
|
||||||
convertToFormValues(attributes, (key, value) =>
|
save={() => save()}
|
||||||
setValue(`attributes.${key}`, value)
|
reset={() =>
|
||||||
)
|
convertToFormValues(attributes, (key, value) =>
|
||||||
}
|
setValue(`attributes.${key}`, value)
|
||||||
hasConfigureAccess={access?.configure}
|
)
|
||||||
/>
|
}
|
||||||
</>
|
/>
|
||||||
)}
|
</>
|
||||||
{protocol !== openIdConnect && (
|
),
|
||||||
<>
|
},
|
||||||
<Text className="pf-u-pb-lg">
|
{
|
||||||
{t("clients-help:fineGrainSamlEndpointConfig")}
|
title: t("advancedSettings"),
|
||||||
</Text>
|
panel: (
|
||||||
<FineGrainSamlEndpointConfig
|
<>
|
||||||
control={control}
|
<Text className="pf-u-pb-lg">
|
||||||
save={() => save()}
|
{t(
|
||||||
reset={() =>
|
"clients-help:advancedSettings" +
|
||||||
convertToFormValues(attributes, (key, value) =>
|
toUpperCase(protocol || "")
|
||||||
setValue(`attributes.${key}`, value)
|
)}
|
||||||
)
|
</Text>
|
||||||
}
|
<AdvancedSettings
|
||||||
/>
|
protocol={protocol}
|
||||||
</>
|
control={control}
|
||||||
)}
|
save={() => save()}
|
||||||
</>
|
reset={() => {
|
||||||
{protocol === openIdConnect && (
|
resetFields([
|
||||||
<>
|
"saml.assertion.lifespan",
|
||||||
<Text className="pf-u-pb-lg">
|
"access.token.lifespan",
|
||||||
{t("clients-help:openIdConnectCompatibilityModes")}
|
"tls.client.certificate.bound.access.tokens",
|
||||||
</Text>
|
"pkce.code.challenge.method",
|
||||||
<OpenIdConnectCompatibilityModes
|
]);
|
||||||
control={control}
|
}}
|
||||||
save={() => save()}
|
/>
|
||||||
reset={() =>
|
</>
|
||||||
resetFields(["exclude.session.state.from.auth.response"])
|
),
|
||||||
}
|
},
|
||||||
hasConfigureAccess={access?.configure}
|
{
|
||||||
/>
|
title: t("authenticationOverrides"),
|
||||||
</>
|
panel: (
|
||||||
)}
|
<>
|
||||||
<>
|
<Text className="pf-u-pb-lg">
|
||||||
<Text className="pf-u-pb-lg">
|
{t("clients-help:authenticationOverrides")}
|
||||||
{t("clients-help:advancedSettings" + toUpperCase(protocol || ""))}
|
</Text>
|
||||||
</Text>
|
<AuthenticationOverrides
|
||||||
<AdvancedSettings
|
protocol={protocol}
|
||||||
protocol={protocol}
|
control={control}
|
||||||
control={control}
|
save={() => save()}
|
||||||
hasConfigureAccess={access?.configure}
|
reset={() => {
|
||||||
save={() => save()}
|
setValue(
|
||||||
reset={() => {
|
"authenticationFlowBindingOverrides.browser",
|
||||||
resetFields([
|
authenticationFlowBindingOverrides?.browser
|
||||||
"saml.assertion.lifespan",
|
);
|
||||||
"access.token.lifespan",
|
setValue(
|
||||||
"tls.client.certificate.bound.access.tokens",
|
"authenticationFlowBindingOverrides.direct_grant",
|
||||||
"pkce.code.challenge.method",
|
authenticationFlowBindingOverrides?.direct_grant
|
||||||
]);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
<>
|
),
|
||||||
<Text className="pf-u-pb-lg">
|
},
|
||||||
{t("clients-help:authenticationOverrides")}
|
]}
|
||||||
</Text>
|
borders
|
||||||
<AuthenticationOverrides
|
/>
|
||||||
protocol={protocol}
|
|
||||||
control={control}
|
|
||||||
save={() => save()}
|
|
||||||
reset={() => {
|
|
||||||
setValue(
|
|
||||||
"authenticationFlowBindingOverrides.browser",
|
|
||||||
authenticationFlowBindingOverrides?.browser
|
|
||||||
);
|
|
||||||
setValue(
|
|
||||||
"authenticationFlowBindingOverrides.direct_grant",
|
|
||||||
authenticationFlowBindingOverrides?.direct_grant
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
hasConfigureAccess={access?.configure}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
</ScrollForm>
|
|
||||||
</PageSection>
|
</PageSection>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,569 +1,77 @@
|
||||||
import type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation";
|
import React from "react";
|
||||||
import React, { useMemo, useState } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {
|
import { useFormContext } from "react-hook-form";
|
||||||
FormGroup,
|
import { Form } from "@patternfly/react-core";
|
||||||
Form,
|
|
||||||
Switch,
|
|
||||||
Select,
|
|
||||||
SelectVariant,
|
|
||||||
SelectOption,
|
|
||||||
ValidatedOptions,
|
|
||||||
} from "@patternfly/react-core";
|
|
||||||
import { Controller, useFormContext } from "react-hook-form";
|
|
||||||
|
|
||||||
|
import type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation";
|
||||||
import { ScrollForm } from "../components/scroll-form/ScrollForm";
|
import { ScrollForm } from "../components/scroll-form/ScrollForm";
|
||||||
import { ClientDescription } from "./ClientDescription";
|
import { ClientDescription } from "./ClientDescription";
|
||||||
import { CapabilityConfig } from "./add/CapabilityConfig";
|
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 { SamlConfig } from "./add/SamlConfig";
|
||||||
import { SamlSignature } from "./add/SamlSignature";
|
import { SamlSignature } from "./add/SamlSignature";
|
||||||
import environment from "../environment";
|
import { AccessSettings } from "./add/AccessSettings";
|
||||||
import { useRealm } from "../context/realm-context/RealmContext";
|
import { LoginSettingsPanel } from "./add/LoginSettingsPanel";
|
||||||
import { useAccess } from "../context/access/Access";
|
import { LogoutPanel } from "./add/LogoutPanel";
|
||||||
|
|
||||||
type ClientSettingsProps = {
|
export type ClientSettingsProps = {
|
||||||
client: ClientRepresentation;
|
client: ClientRepresentation;
|
||||||
save: () => void;
|
save: () => void;
|
||||||
reset: () => void;
|
reset: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ClientSettings = ({
|
export const ClientSettings = (props: ClientSettingsProps) => {
|
||||||
client,
|
|
||||||
save,
|
|
||||||
reset,
|
|
||||||
}: ClientSettingsProps) => {
|
|
||||||
const {
|
|
||||||
register,
|
|
||||||
control,
|
|
||||||
watch,
|
|
||||||
formState: { errors },
|
|
||||||
} = useFormContext<ClientRepresentation>();
|
|
||||||
const { t } = useTranslation("clients");
|
const { t } = useTranslation("clients");
|
||||||
const { realm } = useRealm();
|
|
||||||
|
|
||||||
const { hasAccess } = useAccess();
|
const { watch } = useFormContext<ClientRepresentation>();
|
||||||
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 protocol = watch("protocol");
|
const protocol = watch("protocol");
|
||||||
const frontchannelLogout = watch("frontchannelLogout");
|
|
||||||
const idpInitiatedSsoUrlName: string = watch(
|
|
||||||
"attributes.saml_idp_initiated_sso_url_name"
|
|
||||||
);
|
|
||||||
|
|
||||||
const sections = useMemo(() => {
|
const { client } = props;
|
||||||
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]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollForm
|
<ScrollForm
|
||||||
className="pf-u-px-lg"
|
className="pf-u-px-lg"
|
||||||
sections={sections.map((section) => t(section))}
|
sections={[
|
||||||
>
|
{
|
||||||
<Form isHorizontal>
|
title: t("generalSettings"),
|
||||||
<ClientDescription
|
panel: (
|
||||||
protocol={client.protocol}
|
<Form isHorizontal>
|
||||||
hasConfigureAccess={client.access?.configure}
|
<ClientDescription
|
||||||
/>
|
protocol={client.protocol}
|
||||||
</Form>
|
hasConfigureAccess={client.access?.configure}
|
||||||
{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}
|
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</Form>
|
||||||
<FormGroup
|
),
|
||||||
label={t("homeURL")}
|
},
|
||||||
fieldId="kc-home-url"
|
{
|
||||||
labelIcon={
|
title: t("accessSettings"),
|
||||||
<HelpItem
|
panel: <AccessSettings {...props} />,
|
||||||
helpText="clients-help:homeURL"
|
},
|
||||||
fieldLabelId="clients:homeURL"
|
{
|
||||||
/>
|
title: t("samlCapabilityConfig"),
|
||||||
}
|
isHidden: protocol !== "saml" || client.bearerOnly,
|
||||||
>
|
panel: <SamlConfig />,
|
||||||
<KeycloakTextInput
|
},
|
||||||
type="text"
|
{
|
||||||
id="kc-home-url"
|
title: t("signatureAndEncryption"),
|
||||||
name="baseUrl"
|
isHidden: protocol !== "saml" || client.bearerOnly,
|
||||||
ref={register}
|
panel: <SamlSignature />,
|
||||||
/>
|
},
|
||||||
</FormGroup>
|
{
|
||||||
<FormGroup
|
title: t("capabilityConfig"),
|
||||||
label={t("validRedirectUri")}
|
isHidden: protocol !== "openid-connect" || client.bearerOnly,
|
||||||
fieldId="kc-redirect"
|
panel: <CapabilityConfig />,
|
||||||
labelIcon={
|
},
|
||||||
<HelpItem
|
{
|
||||||
helpText="clients-help:validRedirectURIs"
|
title: t("loginSettings"),
|
||||||
fieldLabelId="clients:validRedirectUri"
|
isHidden: protocol !== "openid-connect" || client.bearerOnly,
|
||||||
/>
|
panel: <LoginSettingsPanel access={client.access?.configure} />,
|
||||||
}
|
},
|
||||||
>
|
{
|
||||||
<MultiLineInput
|
title: t("logoutSettings"),
|
||||||
name="redirectUris"
|
isHidden: client.bearerOnly,
|
||||||
aria-label={t("validRedirectUri")}
|
panel: <LogoutPanel {...props} />,
|
||||||
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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
203
src/clients/add/AccessSettings.tsx
Normal file
203
src/clients/add/AccessSettings.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
146
src/clients/add/LoginSettingsPanel.tsx
Normal file
146
src/clients/add/LoginSettingsPanel.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
203
src/clients/add/LogoutPanel.tsx
Normal file
203
src/clients/add/LogoutPanel.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
200
src/clients/advanced/ClusteringPanel.tsx
Normal file
200
src/clients/advanced/ClusteringPanel.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
137
src/clients/advanced/RevocationPanel.tsx
Normal file
137
src/clients/advanced/RevocationPanel.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -8,14 +8,17 @@ import useRequiredContext from "../../utils/useRequiredContext";
|
||||||
import useSetTimeout from "../../utils/useSetTimeout";
|
import useSetTimeout from "../../utils/useSetTimeout";
|
||||||
import { AlertPanel, AlertType } from "./AlertPanel";
|
import { AlertPanel, AlertType } from "./AlertPanel";
|
||||||
|
|
||||||
type AlertProps = {
|
export type AddAlertFunction = (
|
||||||
addAlert: (
|
message: string,
|
||||||
message: string,
|
variant?: AlertVariant,
|
||||||
variant?: AlertVariant,
|
description?: string
|
||||||
description?: string
|
) => void;
|
||||||
) => 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);
|
export const AlertContext = createContext<AlertProps | undefined>(undefined);
|
||||||
|
|
|
@ -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 { useTranslation } from "react-i18next";
|
||||||
import {
|
import {
|
||||||
Grid,
|
Grid,
|
||||||
|
@ -15,8 +15,14 @@ import { FormPanel } from "./FormPanel";
|
||||||
|
|
||||||
import "./scroll-form.css";
|
import "./scroll-form.css";
|
||||||
|
|
||||||
|
type ScrollSection = {
|
||||||
|
title: string;
|
||||||
|
panel: ReactNode;
|
||||||
|
isHidden?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
type ScrollFormProps = GridProps & {
|
type ScrollFormProps = GridProps & {
|
||||||
sections: string[];
|
sections: ScrollSection[];
|
||||||
borders?: boolean;
|
borders?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -27,33 +33,34 @@ const spacesToHyphens = (string: string): string => {
|
||||||
export const ScrollForm: FunctionComponent<ScrollFormProps> = ({
|
export const ScrollForm: FunctionComponent<ScrollFormProps> = ({
|
||||||
sections,
|
sections,
|
||||||
borders = false,
|
borders = false,
|
||||||
children,
|
|
||||||
...rest
|
...rest
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation("common");
|
const { t } = useTranslation("common");
|
||||||
const nodes = Children.toArray(children);
|
const shownSections = useMemo(
|
||||||
|
() => sections.filter(({ isHidden }) => !isHidden),
|
||||||
|
[sections]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid hasGutter {...rest}>
|
<Grid hasGutter {...rest}>
|
||||||
<GridItem span={8}>
|
<GridItem span={8}>
|
||||||
{sections.map((cat, index) => {
|
{shownSections.map(({ title, panel }) => {
|
||||||
const scrollId = spacesToHyphens(cat.toLowerCase());
|
const scrollId = spacesToHyphens(title.toLowerCase());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment key={cat}>
|
<Fragment key={title}>
|
||||||
{!borders && (
|
{borders ? (
|
||||||
<ScrollPanel scrollId={scrollId} title={cat}>
|
|
||||||
{nodes[index]}
|
|
||||||
</ScrollPanel>
|
|
||||||
)}
|
|
||||||
{borders && (
|
|
||||||
<FormPanel
|
<FormPanel
|
||||||
scrollId={scrollId}
|
scrollId={scrollId}
|
||||||
title={cat}
|
title={title}
|
||||||
className="kc-form-panel__panel"
|
className="kc-form-panel__panel"
|
||||||
>
|
>
|
||||||
{nodes[index]}
|
{panel}
|
||||||
</FormPanel>
|
</FormPanel>
|
||||||
|
) : (
|
||||||
|
<ScrollPanel scrollId={scrollId} title={title}>
|
||||||
|
{panel}
|
||||||
|
</ScrollPanel>
|
||||||
)}
|
)}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
|
@ -69,17 +76,17 @@ export const ScrollForm: FunctionComponent<ScrollFormProps> = ({
|
||||||
label={t("jumpToSection")}
|
label={t("jumpToSection")}
|
||||||
offset={100}
|
offset={100}
|
||||||
>
|
>
|
||||||
{sections.map((cat) => {
|
{shownSections.map(({ title }) => {
|
||||||
const scrollId = spacesToHyphens(cat.toLowerCase());
|
const scrollId = spacesToHyphens(title.toLowerCase());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// note that JumpLinks currently does not work with spaces in the href
|
// note that JumpLinks currently does not work with spaces in the href
|
||||||
<JumpLinksItem
|
<JumpLinksItem
|
||||||
key={cat}
|
key={title}
|
||||||
href={`#${scrollId}`}
|
href={`#${scrollId}`}
|
||||||
data-testid={`jump-link-${scrollId}`}
|
data-testid={`jump-link-${scrollId}`}
|
||||||
>
|
>
|
||||||
{cat}
|
{title}
|
||||||
</JumpLinksItem>
|
</JumpLinksItem>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
|
@ -238,8 +238,6 @@ export default function DetailSettings() {
|
||||||
return <KeycloakSpinner />;
|
return <KeycloakSpinner />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sections = [t("generalSettings"), t("advancedSettings")];
|
|
||||||
|
|
||||||
const isOIDC = provider.providerId!.includes("oidc");
|
const isOIDC = provider.providerId!.includes("oidc");
|
||||||
const isSAML = provider.providerId!.includes("saml");
|
const isSAML = provider.providerId!.includes("saml");
|
||||||
|
|
||||||
|
@ -268,14 +266,81 @@ export default function DetailSettings() {
|
||||||
return components;
|
return components;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isOIDC) {
|
const sections = [
|
||||||
sections.splice(1, 0, t("oidcSettings"));
|
{
|
||||||
}
|
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) {
|
<ActionGroup className="keycloak__form_actions">
|
||||||
sections.splice(1, 0, t("samlSettings"));
|
<Button data-testid={"save"} type="submit">
|
||||||
sections.splice(2, 0, t("reqAuthnConstraints"));
|
{t("common:save")}
|
||||||
}
|
</Button>
|
||||||
|
<Button
|
||||||
|
data-testid={"revert"}
|
||||||
|
variant="link"
|
||||||
|
onClick={() => {
|
||||||
|
reset();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("common:revert")}
|
||||||
|
</Button>
|
||||||
|
</ActionGroup>
|
||||||
|
</FormAccess>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormProvider {...form}>
|
<FormProvider {...form}>
|
||||||
|
@ -302,61 +367,7 @@ export default function DetailSettings() {
|
||||||
eventKey="settings"
|
eventKey="settings"
|
||||||
title={<TabTitleText>{t("common:settings")}</TabTitleText>}
|
title={<TabTitleText>{t("common:settings")}</TabTitleText>}
|
||||||
>
|
>
|
||||||
<ScrollForm className="pf-u-px-lg" sections={sections}>
|
<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>
|
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab
|
<Tab
|
||||||
id="mappers"
|
id="mappers"
|
||||||
|
|
|
@ -69,17 +69,12 @@ const CreateAttributeFormContent = ({
|
||||||
<UserProfileProvider>
|
<UserProfileProvider>
|
||||||
<ScrollForm
|
<ScrollForm
|
||||||
sections={[
|
sections={[
|
||||||
t("generalSettings"),
|
{ title: t("generalSettings"), panel: <AttributeGeneralSettings /> },
|
||||||
t("permission"),
|
{ title: t("permission"), panel: <AttributePermission /> },
|
||||||
t("validations"),
|
{ title: t("validations"), panel: <AttributeValidations /> },
|
||||||
t("annotations"),
|
{ title: t("annotations"), panel: <AttributeAnnotations /> },
|
||||||
]}
|
]}
|
||||||
>
|
/>
|
||||||
<AttributeGeneralSettings />
|
|
||||||
<AttributePermission />
|
|
||||||
<AttributeValidations />
|
|
||||||
<AttributeAnnotations />
|
|
||||||
</ScrollForm>
|
|
||||||
<Form onSubmit={form.handleSubmit(save)}>
|
<Form onSubmit={form.handleSubmit(save)}>
|
||||||
<ActionGroup className="keycloak__form_actions">
|
<ActionGroup className="keycloak__form_actions">
|
||||||
<Button
|
<Button
|
||||||
|
|
|
@ -55,23 +55,33 @@ const AddLdapFormContent = ({
|
||||||
<>
|
<>
|
||||||
<ScrollForm
|
<ScrollForm
|
||||||
sections={[
|
sections={[
|
||||||
t("generalOptions"),
|
{
|
||||||
t("connectionAndAuthenticationSettings"),
|
title: t("generalOptions"),
|
||||||
t("ldapSearchingAndUpdatingSettings"),
|
panel: <LdapSettingsGeneral form={form} vendorEdit={!!id} />,
|
||||||
t("synchronizationSettings"),
|
},
|
||||||
t("kerberosIntegration"),
|
{
|
||||||
t("cacheSettings"),
|
title: t("connectionAndAuthenticationSettings"),
|
||||||
t("advancedSettings"),
|
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)}>
|
<Form onSubmit={form.handleSubmit(save)}>
|
||||||
<ActionGroup className="keycloak__form_actions">
|
<ActionGroup className="keycloak__form_actions">
|
||||||
<Button
|
<Button
|
||||||
|
|
Loading…
Reference in a new issue