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 React, { useState } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { Trans, useTranslation } from "react-i18next"; import { Trans, useTranslation } from "react-i18next";
import { sortBy } from "lodash-es";
import { import {
AlertVariant, AlertVariant,
Button, Button,
@ -30,9 +31,10 @@ import { toCreateFlow } from "./routes/CreateFlow";
import { toFlow } from "./routes/Flow"; import { toFlow } from "./routes/Flow";
import { RequiredActions } from "./RequiredActions"; import { RequiredActions } from "./RequiredActions";
import { Policies } from "./policies/Policies"; import { Policies } from "./policies/Policies";
import helpUrls from "../help-urls";
import { BindFlowDialog } from "./BindFlowDialog";
import "./authentication-section.css"; import "./authentication-section.css";
import helpUrls from "../help-urls";
type UsedBy = "specificClients" | "default" | "specificProviders"; type UsedBy = "specificClients" | "default" | "specificProviders";
@ -40,7 +42,7 @@ type AuthenticationType = AuthenticationFlowRepresentation & {
usedBy: { type?: UsedBy; values: string[] }; usedBy: { type?: UsedBy; values: string[] };
}; };
const realmFlows = [ export const REALM_FLOWS = [
"browserFlow", "browserFlow",
"registrationFlow", "registrationFlow",
"directGrantFlow", "directGrantFlow",
@ -54,27 +56,29 @@ export default function AuthenticationSection() {
const adminClient = useAdminClient(); const adminClient = useAdminClient();
const { realm } = useRealm(); const { realm } = useRealm();
const [key, setKey] = useState(0); const [key, setKey] = useState(0);
const refresh = () => setKey(new Date().getTime()); const refresh = () => setKey(key + 1);
const { addAlert, addError } = useAlerts(); const { addAlert, addError } = useAlerts();
const [selectedFlow, setSelectedFlow] = useState<AuthenticationType>(); const [selectedFlow, setSelectedFlow] = useState<AuthenticationType>();
const [open, toggleOpen, setOpen] = useToggle(); const [open, toggleOpen] = useToggle();
const [bindFlowOpen, toggleBindFlow] = useToggle();
const loader = async () => { const loader = async () => {
const clients = await adminClient.clients.find(); const [clients, idps, realmRep, flows] = await Promise.all([
const idps = await adminClient.identityProviders.find(); adminClient.clients.find(),
const realmRep = await adminClient.realms.findOne({ realm }); adminClient.identityProviders.find(),
adminClient.realms.findOne({ realm }),
adminClient.authenticationManagement.getFlows(),
]);
if (!realmRep) { if (!realmRep) {
throw new Error(t("common:notFound")); throw new Error(t("common:notFound"));
} }
const defaultFlows = Object.entries(realmRep) const defaultFlows = Object.entries(realmRep)
.filter((entry) => realmFlows.includes(entry[0])) .filter((entry) => REALM_FLOWS.includes(entry[0]))
.map((entry) => entry[1]); .map((entry) => entry[1]);
const flows = for (const flow of flows as AuthenticationType[]) {
(await adminClient.authenticationManagement.getFlows()) as AuthenticationType[];
for (const flow of flows) {
flow.usedBy = { values: [] }; flow.usedBy = { values: [] };
const client = clients.find( const client = clients.find(
(client) => (client) =>
@ -104,8 +108,9 @@ export default function AuthenticationSection() {
} }
} }
return flows; return sortBy(flows as AuthenticationType[], (flow) => flow.usedBy.type);
}; };
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({ const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
titleKey: "authentication:deleteConfirmFlow", titleKey: "authentication:deleteConfirmFlow",
children: ( children: (
@ -150,29 +155,25 @@ export default function AuthenticationSection() {
</div> </div>
} }
> >
<Button variant={ButtonVariant.link} key={`button-${id}`}> <>
<CheckCircleIcon <CheckCircleIcon
className="keycloak_authentication-section__usedby" className="keycloak_authentication-section__usedby"
key={`icon-${id}`} key={`icon-${id}`}
/>{" "} />{" "}
{t(type)} {t(type)}
</Button> </>
</Popover> </Popover>
)} )}
{type === "default" && ( {type === "default" && (
<Button key={id} variant={ButtonVariant.link} isDisabled> <>
<CheckCircleIcon <CheckCircleIcon
className="keycloak_authentication-section__usedby" className="keycloak_authentication-section__usedby"
key={`icon-${id}`} key={`icon-${id}`}
/>{" "} />{" "}
{t("default")} {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} toggleDialog={toggleOpen}
onComplete={() => { onComplete={() => {
refresh(); refresh();
setOpen(false); toggleOpen();
}} }}
/> />
)} )}
{bindFlowOpen && (
<BindFlowDialog
onClose={() => {
toggleBindFlow();
refresh();
}}
flowAlias={selectedFlow?.alias!}
/>
)}
<ViewHeader <ViewHeader
titleKey="authentication:title" titleKey="authentication:title"
subKey="authentication:authenticationExplain" subKey="authentication:authenticationExplain"
@ -245,10 +255,21 @@ export default function AuthenticationSection() {
{ {
title: t("duplicate"), title: t("duplicate"),
onClick: () => { onClick: () => {
setOpen(true); toggleOpen();
setSelectedFlow(data); setSelectedFlow(data);
}, },
}, },
...(data.providerId !== "client-flow"
? [
{
title: t("bindFlow"),
onClick: () => {
toggleBindFlow();
setSelectedFlow(data);
},
},
]
: []),
]; ];
// remove delete when it's in use or default flow // remove delete when it's in use or default flow
if (data.builtIn || data.usedBy.values.length > 0) { 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 AuthenticationExecutionInfoRepresentation from "@keycloak/keycloak-admin-client/lib/defs/authenticationExecutionInfoRepresentation";
import type { AuthenticationProviderRepresentation } from "@keycloak/keycloak-admin-client/lib/defs/authenticatorConfigRepresentation"; import type { AuthenticationProviderRepresentation } from "@keycloak/keycloak-admin-client/lib/defs/authenticatorConfigRepresentation";
import type AuthenticationFlowRepresentation from "@keycloak/keycloak-admin-client/lib/defs/authenticationFlowRepresentation"; 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 { ViewHeader } from "../components/view-header/ViewHeader";
import { useAdminClient, useFetch } from "../context/auth/AdminClient"; import { useAdminClient, useFetch } from "../context/auth/AdminClient";
import { EmptyExecutionState } from "./EmptyExecutionState"; import { EmptyExecutionState } from "./EmptyExecutionState";
@ -43,6 +43,7 @@ import { useRealm } from "../context/realm-context/RealmContext";
import useToggle from "../utils/useToggle"; import useToggle from "../utils/useToggle";
import { toAuthentication } from "./routes/Authentication"; import { toAuthentication } from "./routes/Authentication";
import { EditFlowModal } from "./EditFlowModal"; import { EditFlowModal } from "./EditFlowModal";
import { BindFlowDialog } from "./BindFlowDialog";
export const providerConditionFilter = ( export const providerConditionFilter = (
value: AuthenticationProviderRepresentation value: AuthenticationProviderRepresentation
@ -72,6 +73,7 @@ export default function FlowDetails() {
useState<ExpandableExecution>(); useState<ExpandableExecution>();
const [open, toggleOpen, setOpen] = useToggle(); const [open, toggleOpen, setOpen] = useToggle();
const [edit, setEdit] = useState(false); const [edit, setEdit] = useState(false);
const [bindFlowOpen, toggleBindFlow] = useToggle();
useFetch( useFetch(
async () => { 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({ const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
titleKey: "authentication:deleteConfirmExecution", titleKey: "authentication:deleteConfirmExecution",
children: ( children: (
@ -241,14 +229,14 @@ export default function FlowDetails() {
const hasExecutions = executionList?.expandableList.length !== 0; const hasExecutions = executionList?.expandableList.length !== 0;
const dropdownItems = [ const dropdownItems = [
...(usedBy !== "default" ...(usedBy !== "default" && flow?.providerId !== "client-flow"
? [ ? [
<DropdownItem <DropdownItem
data-testid="set-as-default" data-testid="set-as-default"
key="default" key="default"
onClick={() => setAsDefault()} onClick={toggleBindFlow}
> >
{t("setAsDefault")} {t("bindFlow")}
</DropdownItem>, </DropdownItem>,
] ]
: []), : []),
@ -277,6 +265,15 @@ export default function FlowDetails() {
return ( return (
<> <>
{bindFlowOpen && (
<BindFlowDialog
flowAlias={flow?.alias!}
onClose={() => {
toggleBindFlow();
refresh();
}}
/>
)}
{open && ( {open && (
<DuplicateFlowModal <DuplicateFlowModal
name={flow?.alias!} name={flow?.alias!}

View file

@ -82,7 +82,15 @@ export default {
default: "Default", default: "Default",
notInUse: "Not in use", notInUse: "Not in use",
duplicate: "Duplicate", 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", editInfo: "Edit info",
editFlow: "Edit flow", editFlow: "Edit flow",
edit: "Edit", edit: "Edit",