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 {
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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} />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
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 { 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);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
})}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue