Changed to know where flow is used (#2532)
This commit is contained in:
parent
8170c267ef
commit
a061ceb4f2
4 changed files with 161 additions and 52 deletions
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
142
src/authentication/components/UsedBy.tsx
Normal file
142
src/authentication/components/UsedBy.tsx
Normal 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")}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
4
src/authentication/components/used-by.css
Normal file
4
src/authentication/components/used-by.css
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
.keycloak__used-by__popover-button {
|
||||||
|
padding: 0;
|
||||||
|
font-size: var(--pf-c-table--cell--FontSize);
|
||||||
|
}
|
Loading…
Reference in a new issue