diff --git a/src/authentication/AuthenticationSection.tsx b/src/authentication/AuthenticationSection.tsx index c7e031e620..76fb03bfb4 100644 --- a/src/authentication/AuthenticationSection.tsx +++ b/src/authentication/AuthenticationSection.tsx @@ -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(); - 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() { } > - + )} {type === "default" && ( - - )} - {!type && ( - + )} + {!type && t("notInUse")} ); @@ -208,10 +209,19 @@ export default function AuthenticationSection() { toggleDialog={toggleOpen} onComplete={() => { refresh(); - setOpen(false); + toggleOpen(); }} /> )} + {bindFlowOpen && ( + { + toggleBindFlow(); + refresh(); + }} + flowAlias={selectedFlow?.alias!} + /> + )} { - 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) { diff --git a/src/authentication/BindFlowDialog.tsx b/src/authentication/BindFlowDialog.tsx new file mode 100644 index 0000000000..8fa12fe492 --- /dev/null +++ b/src/authentication/BindFlowDialog.tsx @@ -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(); + + 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 ( + + {t("common:save")} + , + , + ]} + > +
+ + ( + + )} + /> + +
+
+ ); +}; diff --git a/src/authentication/FlowDetails.tsx b/src/authentication/FlowDetails.tsx index 8c25d926f2..0f5e3d0a6a 100644 --- a/src/authentication/FlowDetails.tsx +++ b/src/authentication/FlowDetails.tsx @@ -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(); 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" ? [ setAsDefault()} + onClick={toggleBindFlow} > - {t("setAsDefault")} + {t("bindFlow")} , ] : []), @@ -277,6 +265,15 @@ export default function FlowDetails() { return ( <> + {bindFlowOpen && ( + { + toggleBindFlow(); + refresh(); + }} + /> + )} {open && (