Added bind type dialog (#2173)
This commit is contained in:
parent
e3c0bb82a1
commit
1104f9ee20
4 changed files with 190 additions and 42 deletions
|
@ -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) {
|
||||||
|
|
122
src/authentication/BindFlowDialog.tsx
Normal file
122
src/authentication/BindFlowDialog.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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!}
|
||||||
|
|
|
@ -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",
|
||||||
|
|
Loading…
Reference in a new issue