Changed to know where flow is used (#2532)

This commit is contained in:
Erik Jan de Wit 2022-05-02 16:51:05 +02:00 committed by GitHub
parent 8170c267ef
commit a061ceb4f2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 161 additions and 52 deletions

View file

@ -67,6 +67,8 @@
"flowName": "Flow name", "flowName": "Flow name",
"searchForFlow": "Search for flow", "searchForFlow": "Search for flow",
"usedBy": "Used by", "usedBy": "Used by",
"flowUsedBy": "Flow used by",
"flowUsedByDescription": "This flow is applied by the following {{value}}",
"buildIn": "Built-in", "buildIn": "Built-in",
"appliedByProviders": "Applied by the following providers", "appliedByProviders": "Applied by the following providers",
"appliedByClients": "Applied by the following clients", "appliedByClients": "Applied by the following clients",

View file

@ -8,12 +8,10 @@ import {
ButtonVariant, ButtonVariant,
Label, Label,
PageSection, PageSection,
Popover,
Tab, Tab,
TabTitleText, TabTitleText,
ToolbarItem, ToolbarItem,
} from "@patternfly/react-core"; } from "@patternfly/react-core";
import { CheckCircleIcon } from "@patternfly/react-icons";
import type AuthenticationFlowRepresentation from "@keycloak/keycloak-admin-client/lib/defs/authenticationFlowRepresentation"; import type AuthenticationFlowRepresentation from "@keycloak/keycloak-admin-client/lib/defs/authenticationFlowRepresentation";
import { useAdminClient } from "../context/auth/AdminClient"; import { useAdminClient } from "../context/auth/AdminClient";
@ -33,12 +31,13 @@ import { RequiredActions } from "./RequiredActions";
import { Policies } from "./policies/Policies"; import { Policies } from "./policies/Policies";
import helpUrls from "../help-urls"; import helpUrls from "../help-urls";
import { BindFlowDialog } from "./BindFlowDialog"; import { BindFlowDialog } from "./BindFlowDialog";
import { UsedBy } from "./components/UsedBy";
import "./authentication-section.css"; import "./authentication-section.css";
type UsedBy = "specificClients" | "default" | "specificProviders"; type UsedBy = "specificClients" | "default" | "specificProviders";
type AuthenticationType = AuthenticationFlowRepresentation & { export type AuthenticationType = AuthenticationFlowRepresentation & {
usedBy: { type?: UsedBy; values: string[] }; usedBy: { type?: UsedBy; values: string[] };
}; };
@ -64,7 +63,7 @@ export default function AuthenticationSection() {
const [bindFlowOpen, toggleBindFlow] = useToggle(); const [bindFlowOpen, toggleBindFlow] = useToggle();
const loader = async () => { const loader = async () => {
const [clients, idps, realmRep, flows] = await Promise.all([ const [allClients, allIdps, realmRep, flows] = await Promise.all([
adminClient.clients.find(), adminClient.clients.find(),
adminClient.identityProviders.find(), adminClient.identityProviders.find(),
adminClient.realms.findOne({ realm }), adminClient.realms.findOne({ realm }),
@ -80,31 +79,32 @@ export default function AuthenticationSection() {
for (const flow of flows as AuthenticationType[]) { for (const flow of flows as AuthenticationType[]) {
flow.usedBy = { values: [] }; flow.usedBy = { values: [] };
const client = clients.find( const clients = allClients.filter(
(client) => (client) =>
client.authenticationFlowBindingOverrides && client.authenticationFlowBindingOverrides &&
(client.authenticationFlowBindingOverrides["direct_grant"] === (client.authenticationFlowBindingOverrides["direct_grant"] ===
flow.id || flow.id ||
client.authenticationFlowBindingOverrides["browser"] === flow.id) client.authenticationFlowBindingOverrides["browser"] === flow.id)
); );
if (client) { if (clients.length > 0) {
flow.usedBy.type = "specificClients"; flow.usedBy.type = "specificClients";
flow.usedBy.values.push(client.clientId!); flow.usedBy.values = clients.map(({ clientId }) => clientId!);
} }
const idp = idps.find( const idps = allIdps.filter(
(idp) => (idp) =>
idp.firstBrokerLoginFlowAlias === flow.alias || idp.firstBrokerLoginFlowAlias === flow.alias ||
idp.postBrokerLoginFlowAlias === flow.alias idp.postBrokerLoginFlowAlias === flow.alias
); );
if (idp) { if (idps.length > 0) {
flow.usedBy.type = "specificProviders"; flow.usedBy.type = "specificProviders";
flow.usedBy.values.push(idp.alias!); flow.usedBy.values = idps.map(({ alias }) => alias!);
} }
const isDefault = defaultFlows.includes(flow.alias); const isDefault = defaultFlows.includes(flow.alias);
if (isDefault) { if (isDefault) {
flow.usedBy.type = "default"; flow.usedBy.type = "default";
flow.usedBy.values.push(flow.alias!);
} }
} }
@ -134,47 +134,8 @@ export default function AuthenticationSection() {
}, },
}); });
const UsedBy = ({ id, usedBy: { type, values } }: AuthenticationType) => ( const UsedByRenderer = (authType: AuthenticationType) => (
<> <UsedBy authType={authType} />
{(type === "specificProviders" || type === "specificClients") && (
<Popover
key={id}
aria-label={t("usedBy")}
bodyContent={
<div key={`usedBy-${id}-${values}`}>
{t(
"appliedBy" +
(type === "specificClients" ? "Clients" : "Providers")
)}{" "}
{values.map((used, index) => (
<>
<strong>{used}</strong>
{index < values.length - 1 ? ", " : ""}
</>
))}
</div>
}
>
<>
<CheckCircleIcon
className="keycloak_authentication-section__usedby"
key={`icon-${id}`}
/>{" "}
{t(type)}
</>
</Popover>
)}
{type === "default" && (
<>
<CheckCircleIcon
className="keycloak_authentication-section__usedby"
key={`icon-${id}`}
/>{" "}
{t("default")}
</>
)}
{!type && t("notInUse")}
</>
); );
const AliasRenderer = ({ const AliasRenderer = ({
@ -296,7 +257,7 @@ export default function AuthenticationSection() {
{ {
name: "usedBy", name: "usedBy",
displayKey: "authentication:usedBy", displayKey: "authentication:usedBy",
cellRenderer: UsedBy, cellRenderer: UsedByRenderer,
}, },
{ {
name: "description", name: "description",

View file

@ -0,0 +1,142 @@
import React from "react";
import { useTranslation } from "react-i18next";
import {
Button,
Modal,
ModalVariant,
Popover,
Text,
TextContent,
TextVariants,
} from "@patternfly/react-core";
import { CheckCircleIcon } from "@patternfly/react-icons";
import type { AuthenticationType } from "../AuthenticationSection";
import { KeycloakDataTable } from "../../components/table-toolbar/KeycloakDataTable";
import { toUpperCase } from "../../util";
import useToggle from "../../utils/useToggle";
import "./used-by.css";
type UsedByProps = {
authType: AuthenticationType;
};
const Label = ({ label }: { label: string }) => (
<>
<CheckCircleIcon className="keycloak_authentication-section__usedby" />{" "}
{label}
</>
);
type UsedByModalProps = {
values: string[];
onClose: () => void;
isSpecificClient: boolean;
};
const UsedByModal = ({
values,
isSpecificClient,
onClose,
}: UsedByModalProps) => {
const { t } = useTranslation("authentication");
return (
<Modal
header={
<TextContent>
<Text component={TextVariants.h1}>{t("flowUsedBy")}</Text>
<Text>
{t("flowUsedByDescription", {
value: isSpecificClient ? t("clients") : t("identiyProviders"),
})}
</Text>
</TextContent>
}
variant={ModalVariant.medium}
isOpen
onClose={onClose}
actions={[
<Button
data-testid="cancel"
id="modal-cancel"
key="cancel"
onClick={onClose}
>
{t("common:close")}
</Button>,
]}
>
<KeycloakDataTable
loader={values.map((value) => ({ name: value }))}
ariaLabelKey="authentication:usedBy"
searchPlaceholderKey="common:search"
columns={[
{
name: "name",
},
]}
/>
</Modal>
);
};
export const UsedBy = ({
authType: {
id,
usedBy: { type, values },
},
}: UsedByProps) => {
const { t } = useTranslation("authentication");
const [open, toggle] = useToggle();
return (
<>
{open && (
<UsedByModal
values={values}
onClose={toggle}
isSpecificClient={type === "specificClients"}
/>
)}
{(type === "specificProviders" || type === "specificClients") &&
(values.length < 8 ? (
<Popover
key={id}
aria-label={t("usedBy")}
bodyContent={
<div key={`usedBy-${id}-${values}`}>
{t(
"appliedBy" +
(type === "specificClients" ? "Clients" : "Providers")
)}{" "}
{values.map((used, index) => (
<>
<strong>{used}</strong>
{index < values.length - 1 ? ", " : ""}
</>
))}
</div>
}
>
<Button
variant="link"
className="keycloak__used-by__popover-button"
>
<Label label={t(type!)} />
</Button>
</Popover>
) : (
<Button
variant="link"
className="keycloak__used-by__popover-button"
onClick={toggle}
>
<Label label={t(type!)} />
</Button>
))}
{type === "default" && <Label label={toUpperCase(values[0])} />}
{!type && t("notInUse")}
</>
);
};

View file

@ -0,0 +1,4 @@
.keycloak__used-by__popover-button {
padding: 0;
font-size: var(--pf-c-table--cell--FontSize);
}