Added bind type dialog (#2173)

This commit is contained in:
Erik Jan de Wit 2022-03-07 18:36:52 +01:00 committed by GitHub
parent e3c0bb82a1
commit 1104f9ee20
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 190 additions and 42 deletions

View file

@ -1,6 +1,7 @@
import React, { useState } from "react";
import { Link } from "react-router-dom";
import { Trans, useTranslation } from "react-i18next";
import { sortBy } from "lodash-es";
import {
AlertVariant,
Button,
@ -30,9 +31,10 @@ import { toCreateFlow } from "./routes/CreateFlow";
import { toFlow } from "./routes/Flow";
import { RequiredActions } from "./RequiredActions";
import { Policies } from "./policies/Policies";
import helpUrls from "../help-urls";
import { BindFlowDialog } from "./BindFlowDialog";
import "./authentication-section.css";
import helpUrls from "../help-urls";
type UsedBy = "specificClients" | "default" | "specificProviders";
@ -40,7 +42,7 @@ type AuthenticationType = AuthenticationFlowRepresentation & {
usedBy: { type?: UsedBy; values: string[] };
};
const realmFlows = [
export const REALM_FLOWS = [
"browserFlow",
"registrationFlow",
"directGrantFlow",
@ -54,27 +56,29 @@ export default function AuthenticationSection() {
const adminClient = useAdminClient();
const { realm } = useRealm();
const [key, setKey] = useState(0);
const refresh = () => setKey(new Date().getTime());
const refresh = () => setKey(key + 1);
const { addAlert, addError } = useAlerts();
const [selectedFlow, setSelectedFlow] = useState<AuthenticationType>();
const [open, toggleOpen, setOpen] = useToggle();
const [open, toggleOpen] = useToggle();
const [bindFlowOpen, toggleBindFlow] = useToggle();
const loader = async () => {
const clients = await adminClient.clients.find();
const idps = await adminClient.identityProviders.find();
const realmRep = await adminClient.realms.findOne({ realm });
const [clients, idps, realmRep, flows] = await Promise.all([
adminClient.clients.find(),
adminClient.identityProviders.find(),
adminClient.realms.findOne({ realm }),
adminClient.authenticationManagement.getFlows(),
]);
if (!realmRep) {
throw new Error(t("common:notFound"));
}
const defaultFlows = Object.entries(realmRep)
.filter((entry) => realmFlows.includes(entry[0]))
.filter((entry) => REALM_FLOWS.includes(entry[0]))
.map((entry) => entry[1]);
const flows =
(await adminClient.authenticationManagement.getFlows()) as AuthenticationType[];
for (const flow of flows) {
for (const flow of flows as AuthenticationType[]) {
flow.usedBy = { values: [] };
const client = clients.find(
(client) =>
@ -104,8 +108,9 @@ export default function AuthenticationSection() {
}
}
return flows;
return sortBy(flows as AuthenticationType[], (flow) => flow.usedBy.type);
};
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
titleKey: "authentication:deleteConfirmFlow",
children: (
@ -150,29 +155,25 @@ export default function AuthenticationSection() {
</div>
}
>
<Button variant={ButtonVariant.link} key={`button-${id}`}>
<>
<CheckCircleIcon
className="keycloak_authentication-section__usedby"
key={`icon-${id}`}
/>{" "}
{t(type)}
</Button>
</>
</Popover>
)}
{type === "default" && (
<Button key={id} variant={ButtonVariant.link} isDisabled>
<>
<CheckCircleIcon
className="keycloak_authentication-section__usedby"
key={`icon-${id}`}
/>{" "}
{t("default")}
</Button>
)}
{!type && (
<Button key={id} variant={ButtonVariant.link} isDisabled>
{t("notInUse")}
</Button>
</>
)}
{!type && t("notInUse")}
</>
);
@ -208,10 +209,19 @@ export default function AuthenticationSection() {
toggleDialog={toggleOpen}
onComplete={() => {
refresh();
setOpen(false);
toggleOpen();
}}
/>
)}
{bindFlowOpen && (
<BindFlowDialog
onClose={() => {
toggleBindFlow();
refresh();
}}
flowAlias={selectedFlow?.alias!}
/>
)}
<ViewHeader
titleKey="authentication:title"
subKey="authentication:authenticationExplain"
@ -245,10 +255,21 @@ export default function AuthenticationSection() {
{
title: t("duplicate"),
onClick: () => {
setOpen(true);
toggleOpen();
setSelectedFlow(data);
},
},
...(data.providerId !== "client-flow"
? [
{
title: t("bindFlow"),
onClick: () => {
toggleBindFlow();
setSelectedFlow(data);
},
},
]
: []),
];
// remove delete when it's in use or default flow
if (data.builtIn || data.usedBy.values.length > 0) {

View file

@ -0,0 +1,122 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { Controller, useForm } from "react-hook-form";
import {
Modal,
Button,
ButtonVariant,
Form,
FormGroup,
Select,
SelectVariant,
SelectOption,
AlertVariant,
} from "@patternfly/react-core";
import type RealmRepresentation from "@keycloak/keycloak-admin-client/lib/defs/realmRepresentation";
import useToggle from "../utils/useToggle";
import { REALM_FLOWS } from "./AuthenticationSection";
import { useRealm } from "../context/realm-context/RealmContext";
import { useAdminClient } from "../context/auth/AdminClient";
import { useAlerts } from "../components/alert/Alerts";
type BindingForm = {
bindingType: keyof RealmRepresentation;
};
type BindFlowDialogProps = {
flowAlias: string;
onClose: () => void;
};
export const BindFlowDialog = ({ flowAlias, onClose }: BindFlowDialogProps) => {
const { t } = useTranslation("authentication");
const { control, handleSubmit } = useForm<BindingForm>();
const adminClient = useAdminClient();
const { addAlert, addError } = useAlerts();
const { realm } = useRealm();
const [open, toggle] = useToggle();
const save = async ({ bindingType }: BindingForm) => {
const realmRep = await adminClient.realms.findOne({ realm });
try {
await adminClient.realms.update(
{ realm },
{ ...realmRep, [bindingType]: flowAlias }
);
addAlert(t("updateFlowSuccess"), AlertVariant.success);
} catch (error) {
addError("authentication:updateFlowError", error);
}
onClose();
};
return (
<Modal
title={t("bindFlow")}
isOpen
variant="small"
onClose={onClose}
actions={[
<Button
id="modal-confirm"
key="confirm"
data-testid="save"
type="submit"
form="bind-form"
>
{t("common:save")}
</Button>,
<Button
data-testid="cancel"
id="modal-cancel"
key="cancel"
variant={ButtonVariant.link}
onClick={onClose}
>
{t("common:cancel")}
</Button>,
]}
>
<Form id="bind-form" isHorizontal onSubmit={handleSubmit(save)}>
<FormGroup label={t("chooseBindingType")} fieldId="chooseBindingType">
<Controller
name="bindingType"
defaultValue={REALM_FLOWS[0]}
control={control}
render={({ onChange, value }) => (
<Select
toggleId="chooseBindingType"
onToggle={toggle}
onSelect={(_, value) => {
onChange(value.toString());
toggle();
}}
selections={t(`flow.${value}`)}
variant={SelectVariant.single}
aria-label={t("bindingFlow")}
isOpen={open}
menuAppendTo="parent"
>
{REALM_FLOWS.filter(
(f) => f !== "dockerAuthenticationFlow"
).map((flow) => (
<SelectOption
selected={flow === value}
key={flow}
value={flow}
>
{t(`flow.${flow}`)}
</SelectOption>
))}
</Select>
)}
/>
</FormGroup>
</Form>
</Modal>
);
};

View file

@ -20,7 +20,7 @@ import { CheckCircleIcon, PlusIcon, TableIcon } from "@patternfly/react-icons";
import type AuthenticationExecutionInfoRepresentation from "@keycloak/keycloak-admin-client/lib/defs/authenticationExecutionInfoRepresentation";
import type { AuthenticationProviderRepresentation } from "@keycloak/keycloak-admin-client/lib/defs/authenticatorConfigRepresentation";
import type AuthenticationFlowRepresentation from "@keycloak/keycloak-admin-client/lib/defs/authenticationFlowRepresentation";
import { FlowParams, toFlow } from "./routes/Flow";
import type { FlowParams } from "./routes/Flow";
import { ViewHeader } from "../components/view-header/ViewHeader";
import { useAdminClient, useFetch } from "../context/auth/AdminClient";
import { EmptyExecutionState } from "./EmptyExecutionState";
@ -43,6 +43,7 @@ import { useRealm } from "../context/realm-context/RealmContext";
import useToggle from "../utils/useToggle";
import { toAuthentication } from "./routes/Authentication";
import { EditFlowModal } from "./EditFlowModal";
import { BindFlowDialog } from "./BindFlowDialog";
export const providerConditionFilter = (
value: AuthenticationProviderRepresentation
@ -72,6 +73,7 @@ export default function FlowDetails() {
useState<ExpandableExecution>();
const [open, toggleOpen, setOpen] = useToggle();
const [edit, setEdit] = useState(false);
const [bindFlowOpen, toggleBindFlow] = useToggle();
useFetch(
async () => {
@ -178,20 +180,6 @@ export default function FlowDetails() {
}
};
const setAsDefault = async () => {
try {
const r = await adminClient.realms.findOne({ realm });
await adminClient.realms.update(
{ realm },
{ ...r, browserFlow: flow?.alias }
);
addAlert(t("updateFlowSuccess"), AlertVariant.success);
history.push(toFlow({ id, realm, usedBy: "default", builtIn }));
} catch (error) {
addError("authentication:updateFlowError", error);
}
};
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
titleKey: "authentication:deleteConfirmExecution",
children: (
@ -241,14 +229,14 @@ export default function FlowDetails() {
const hasExecutions = executionList?.expandableList.length !== 0;
const dropdownItems = [
...(usedBy !== "default"
...(usedBy !== "default" && flow?.providerId !== "client-flow"
? [
<DropdownItem
data-testid="set-as-default"
key="default"
onClick={() => setAsDefault()}
onClick={toggleBindFlow}
>
{t("setAsDefault")}
{t("bindFlow")}
</DropdownItem>,
]
: []),
@ -277,6 +265,15 @@ export default function FlowDetails() {
return (
<>
{bindFlowOpen && (
<BindFlowDialog
flowAlias={flow?.alias!}
onClose={() => {
toggleBindFlow();
refresh();
}}
/>
)}
{open && (
<DuplicateFlowModal
name={flow?.alias!}

View file

@ -82,7 +82,15 @@ export default {
default: "Default",
notInUse: "Not in use",
duplicate: "Duplicate",
setAsDefault: "Set as default",
bindFlow: "Bind flow",
chooseBindingType: "Choose binding type",
flow: {
browserFlow: "Browser flow",
registrationFlow: "Registration flow",
directGrantFlow: "Direct grant flow",
resetCredentialsFlow: "Reset credentials flow",
clientAuthenticationFlow: "Client authentication flow",
},
editInfo: "Edit info",
editFlow: "Edit flow",
edit: "Edit",