added subflow and create flow to authentication (#1198)

* added subflow and create flow to authentication

* fixed types
This commit is contained in:
Erik Jan de Wit 2021-09-23 17:06:58 +02:00 committed by GitHub
parent 8a9f96e53a
commit b7ebd3260b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 451 additions and 75 deletions

View file

@ -14,7 +14,7 @@ describe("Authentication test", () => {
const detailPage = new FlowDetails(); const detailPage = new FlowDetails();
beforeEach(function () { beforeEach(() => {
keycloakBefore(); keycloakBefore();
loginPage.logIn(); loginPage.logIn();
sidebarPage.goToAuthentication(); sidebarPage.goToAuthentication();
@ -78,4 +78,32 @@ describe("Authentication test", () => {
masthead.checkNotificationMessage("Flow successfully updated"); masthead.checkNotificationMessage("Flow successfully updated");
detailPage.executionExists("Username Password Challenge"); detailPage.executionExists("Username Password Challenge");
}); });
it("should add a sub-flow", () => {
const flowName = "SubFlow";
listingPage.goToItemDetails("Copy of browser");
detailPage.addSubFlow(
"Copy of browser Browser - Conditional OTP",
flowName
);
masthead.checkNotificationMessage("Flow successfully updated");
detailPage.flowExists(flowName);
});
it("should create flow from scratch", () => {
const flowName = "Flow";
listingPage.goToCreateItem();
detailPage.fillCreateForm(
flowName,
"Some nice description about what this flow does so that we can use it later",
"Client flow"
);
masthead.checkNotificationMessage("Flow created");
detailPage.addSubFlowToEmpty(flowName, "EmptySubFlow");
masthead.checkNotificationMessage("Flow successfully updated");
detailPage.flowExists(flowName);
});
}); });

View file

@ -6,6 +6,11 @@ export default class FlowDetails {
return this; return this;
} }
flowExists(name: string) {
cy.findAllByText(name).should("exist");
return this;
}
private getExecution(name: string) { private getExecution(name: string) {
return cy.findByTestId(name); return cy.findByTestId(name);
} }
@ -63,4 +68,38 @@ export default class FlowDetails {
return this; return this;
} }
addSubFlow(subFlowName: string, name: string) {
this.clickEditDropdownForFlow(subFlowName, "Add sub-flow");
this.fillSubFlowModal(subFlowName, name);
return this;
}
private fillSubFlowModal(subFlowName: string, name: string) {
cy.get(".pf-c-modal-box__title-text").contains(
"Add step to " + subFlowName
);
cy.findByTestId("name").type(name);
cy.findByTestId("modal-add").click();
}
fillCreateForm(
name: string,
description: string,
type: "Basic flow" | "Client flow"
) {
cy.findByTestId("alias").type(name);
cy.findByTestId("description").type(description);
cy.get("#flowType").click().parent().contains(type).click();
cy.findByTestId("create").click();
return this;
}
addSubFlowToEmpty(subFlowName: string, name: string) {
cy.findByTestId("addSubFlow").click();
this.fillSubFlowModal(subFlowName, name);
return this;
}
} }

View file

@ -1,4 +1,4 @@
import React from "react"; import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { import {
Button, Button,
@ -8,23 +8,62 @@ import {
TitleSizes, TitleSizes,
} from "@patternfly/react-core"; } from "@patternfly/react-core";
import type AuthenticationFlowRepresentation from "@keycloak/keycloak-admin-client/lib/defs/authenticationFlowRepresentation";
import type { AuthenticationProviderRepresentation } from "@keycloak/keycloak-admin-client/lib/defs/authenticatorConfigRepresentation";
import { ListEmptyState } from "../components/list-empty-state/ListEmptyState"; import { ListEmptyState } from "../components/list-empty-state/ListEmptyState";
import { AddStepModal } from "./components/modals/AddStepModal";
import { AddSubFlowModal, Flow } from "./components/modals/AddSubFlowModal";
import "./empty-execution-state.css"; import "./empty-execution-state.css";
const sections = ["addExecution", "addSubFlow"]; const SECTIONS = ["addExecution", "addSubFlow"] as const;
type SectionType = typeof SECTIONS[number] | undefined;
export const EmptyExecutionState = () => { type EmptyExecutionStateProps = {
flow: AuthenticationFlowRepresentation;
onAddExecution: (type: AuthenticationProviderRepresentation) => void;
onAddFlow: (flow: Flow) => void;
};
export const EmptyExecutionState = ({
flow,
onAddExecution,
onAddFlow,
}: EmptyExecutionStateProps) => {
const { t } = useTranslation("authentication"); const { t } = useTranslation("authentication");
const [show, setShow] = useState<SectionType>();
return ( return (
<> <>
{show === "addExecution" && (
<AddStepModal
name={flow.alias!}
type={flow.providerId === "client-flow" ? "client" : "basic"}
onSelect={(type) => {
if (type) {
onAddExecution(type);
}
setShow(undefined);
}}
/>
)}
{show === "addSubFlow" && (
<AddSubFlowModal
name={flow.alias!}
onCancel={() => setShow(undefined)}
onConfirm={(newFlow) => {
onAddFlow(newFlow);
setShow(undefined);
}}
/>
)}
<ListEmptyState <ListEmptyState
message={t("emptyExecution")} message={t("emptyExecution")}
instructions={t("emptyExecutionInstructions")} instructions={t("emptyExecutionInstructions")}
/> />
<div className="keycloak__empty-execution-state__block"> <div className="keycloak__empty-execution-state__block">
{sections.map((section) => ( {SECTIONS.map((section) => (
<Flex key={section} className="keycloak__empty-execution-state__help"> <Flex key={section} className="keycloak__empty-execution-state__help">
<FlexItem flex={{ default: "flex_1" }}> <FlexItem flex={{ default: "flex_1" }}>
<Title headingLevel="h3" size={TitleSizes.md}> <Title headingLevel="h3" size={TitleSizes.md}>
@ -34,7 +73,13 @@ export const EmptyExecutionState = () => {
</FlexItem> </FlexItem>
<Flex alignSelf={{ default: "alignSelfCenter" }}> <Flex alignSelf={{ default: "alignSelfCenter" }}>
<FlexItem> <FlexItem>
<Button variant="tertiary">{t(section)}</Button> <Button
data-testId={section}
variant="tertiary"
onClick={() => setShow(section)}
>
{t(section)}
</Button>
</FlexItem> </FlexItem>
</Flex> </Flex>
</Flex> </Flex>

View file

@ -1,6 +1,6 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { useTranslation } from "react-i18next"; import { Trans, useTranslation } from "react-i18next";
import { import {
DataList, DataList,
Label, Label,
@ -10,8 +10,11 @@ import {
ToggleGroup, ToggleGroup,
ToggleGroupItem, ToggleGroupItem,
AlertVariant, AlertVariant,
ActionGroup,
Button,
ButtonVariant,
} from "@patternfly/react-core"; } from "@patternfly/react-core";
import { CheckCircleIcon, TableIcon } from "@patternfly/react-icons"; 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";
@ -31,6 +34,9 @@ import {
} from "./execution-model"; } from "./execution-model";
import { FlowDiagram } from "./components/FlowDiagram"; import { FlowDiagram } from "./components/FlowDiagram";
import { useAlerts } from "../components/alert/Alerts"; import { useAlerts } from "../components/alert/Alerts";
import { AddStepModal } from "./components/modals/AddStepModal";
import { AddSubFlowModal, Flow } from "./components/modals/AddSubFlowModal";
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
export const providerConditionFilter = ( export const providerConditionFilter = (
value: AuthenticationProviderRepresentation value: AuthenticationProviderRepresentation
@ -51,6 +57,12 @@ export const FlowDetails = () => {
useState<AuthenticationExecutionInfoRepresentation>(); useState<AuthenticationExecutionInfoRepresentation>();
const [liveText, setLiveText] = useState(""); const [liveText, setLiveText] = useState("");
const [showAddExecutionDialog, setShowAddExecutionDialog] =
useState<boolean>();
const [showAddSubFlowDialog, setShowSubFlowDialog] = useState<boolean>();
const [selectedExecution, setSelectedExecution] =
useState<ExpandableExecution>();
useFetch( useFetch(
async () => { async () => {
const flows = await adminClient.authenticationManagement.getFlows(); const flows = await adminClient.authenticationManagement.getFlows();
@ -84,23 +96,17 @@ export const FlowDetails = () => {
id = result.id!; id = result.id!;
} }
const times = change.newIndex - change.oldIndex; const times = change.newIndex - change.oldIndex;
const requests = [];
for (let index = 0; index < Math.abs(times); index++) { for (let index = 0; index < Math.abs(times); index++) {
if (times > 0) { if (times > 0) {
requests.push( await adminClient.authenticationManagement.lowerPriorityExecution({
adminClient.authenticationManagement.lowerPriorityExecution({
id, id,
}) });
);
} else { } else {
requests.push( await adminClient.authenticationManagement.raisePriorityExecution({
adminClient.authenticationManagement.raisePriorityExecution({
id, id,
}) });
);
} }
} }
await Promise.all(requests);
refresh(); refresh();
addAlert(t("updateFlowSuccess"), AlertVariant.success); addAlert(t("updateFlowSuccess"), AlertVariant.success);
} catch (error: any) { } catch (error: any) {
@ -124,12 +130,12 @@ export const FlowDetails = () => {
}; };
const addExecution = async ( const addExecution = async (
execution: ExpandableExecution, name: string,
type: AuthenticationProviderRepresentation type: AuthenticationProviderRepresentation
) => { ) => {
try { try {
await adminClient.authenticationManagement.addExecutionToFlow({ await adminClient.authenticationManagement.addExecutionToFlow({
flow: execution.displayName!, flow: name,
provider: type.id!, provider: type.id!,
}); });
refresh(); refresh();
@ -139,6 +145,50 @@ export const FlowDetails = () => {
} }
}; };
const addFlow = async (
flow: string,
{ name, description = "", type, provider }: Flow
) => {
try {
await adminClient.authenticationManagement.addFlowToFlow({
flow,
alias: name,
description,
provider,
type,
});
refresh();
addAlert(t("updateFlowSuccess"), AlertVariant.success);
} catch (error) {
addError("authentication:updateFlowError", error);
}
};
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
titleKey: "authentication:deleteConfirmExecution",
children: (
<Trans i18nKey="authentication:deleteConfirmExecutionMessage">
{" "}
<strong>{{ name: selectedExecution?.displayName }}</strong>.
</Trans>
),
continueButtonLabel: "common:delete",
continueButtonVariant: ButtonVariant.danger,
onConfirm: async () => {
try {
await adminClient.authenticationManagement.delExecution({
id: selectedExecution?.id!,
});
addAlert(t("deleteExecutionSuccess"), AlertVariant.success);
refresh();
} catch (error) {
addError("authentication:deleteExecutionError", error);
}
},
});
const hasExecutions = executionList?.expandableList.length !== 0;
return ( return (
<> <>
<ViewHeader <ViewHeader
@ -161,7 +211,7 @@ export const FlowDetails = () => {
]} ]}
/> />
<PageSection variant="light"> <PageSection variant="light">
{executionList?.expandableList?.length && ( {hasExecutions && (
<Toolbar id="toolbar"> <Toolbar id="toolbar">
<ToolbarContent> <ToolbarContent>
<ToggleGroup> <ToggleGroup>
@ -183,8 +233,9 @@ export const FlowDetails = () => {
</ToolbarContent> </ToolbarContent>
</Toolbar> </Toolbar>
)} )}
{tableView && executionList?.expandableList?.length && ( {tableView && executionList && hasExecutions && (
<> <>
<DeleteConfirm />
<DataList <DataList
aria-label="flows" aria-label="flows"
onDragFinish={(order) => { onDragFinish={(order) => {
@ -204,7 +255,7 @@ export const FlowDetails = () => {
t("common:onDragStart", { item: item.displayName }) t("common:onDragStart", { item: item.displayName })
); );
setDragged(item); setDragged(item);
if (item.executionList && !item.isCollapsed) { if (!item.isCollapsed) {
item.isCollapsed = true; item.isCollapsed = true;
setExecutionList(executionList.clone()); setExecutionList(executionList.clone());
} }
@ -231,11 +282,62 @@ export const FlowDetails = () => {
setExecutionList(executionList.clone()); setExecutionList(executionList.clone());
}} }}
onRowChange={update} onRowChange={update}
onAddExecution={addExecution} onAddExecution={(execution, type) =>
addExecution(execution.displayName!, type)
}
onAddFlow={(flow) => addFlow(execution.displayName!, flow)}
onDelete={() => {
setSelectedExecution(execution);
toggleDeleteDialog();
}}
/> />
))} ))}
</> </>
</DataList> </DataList>
{flow && (
<>
{showAddExecutionDialog && (
<AddStepModal
name={flow.alias!}
type={
flow.providerId === "client-flow" ? "client" : "basic"
}
onSelect={(type) => {
if (type) {
addExecution(flow.alias!, type);
}
setShowAddExecutionDialog(false);
}}
/>
)}
{showAddSubFlowDialog && (
<AddSubFlowModal
name={flow.alias!}
onCancel={() => setShowSubFlowDialog(false)}
onConfirm={(newFlow) => {
addFlow(flow.alias!, newFlow);
setShowSubFlowDialog(false);
}}
/>
)}
<ActionGroup>
<Button
data-testid="addStep"
variant="link"
onClick={() => setShowAddExecutionDialog(true)}
>
<PlusIcon /> {t("addStep")}
</Button>
<Button
data-testid="addSubFlow"
variant="link"
onClick={() => setShowSubFlowDialog(true)}
>
<PlusIcon /> {t("addSubFlow")}
</Button>
</ActionGroup>
</>
)}
<div className="pf-screen-reader" aria-live="assertive"> <div className="pf-screen-reader" aria-live="assertive">
{liveText} {liveText}
</div> </div>
@ -245,8 +347,12 @@ export const FlowDetails = () => {
<FlowDiagram executionList={executionList} /> <FlowDiagram executionList={executionList} />
)} )}
{!executionList?.expandableList || {!executionList?.expandableList ||
(executionList.expandableList.length === 0 && ( (flow && !hasExecutions && (
<EmptyExecutionState /> <EmptyExecutionState
flow={flow}
onAddExecution={(type) => addExecution(flow.alias!, type)}
onAddFlow={(newFlow) => addFlow(flow.alias!, newFlow)}
/>
))} ))}
</PageSection> </PageSection>
</> </>

View file

@ -7,7 +7,7 @@ import type { AuthenticationProviderRepresentation } from "@keycloak/keycloak-ad
import type { ExpandableExecution } from "../execution-model"; import type { ExpandableExecution } from "../execution-model";
import { AddStepModal, FlowType } from "./modals/AddStepModal"; import { AddStepModal, FlowType } from "./modals/AddStepModal";
import { useAdminClient, useFetch } from "../../context/auth/AdminClient"; import { useAdminClient, useFetch } from "../../context/auth/AdminClient";
import { AddSubFlowModal } from "./modals/AddSubFlowModal"; import { AddSubFlowModal, Flow } from "./modals/AddSubFlowModal";
type EditFlowDropdownProps = { type EditFlowDropdownProps = {
execution: ExpandableExecution; execution: ExpandableExecution;
@ -15,11 +15,13 @@ type EditFlowDropdownProps = {
execution: ExpandableExecution, execution: ExpandableExecution,
type: AuthenticationProviderRepresentation type: AuthenticationProviderRepresentation
) => void; ) => void;
onAddFlow: (flow: Flow) => void;
}; };
export const EditFlowDropdown = ({ export const EditFlowDropdown = ({
execution, execution,
onAddExecution, onAddExecution,
onAddFlow,
}: EditFlowDropdownProps) => { }: EditFlowDropdownProps) => {
const { t } = useTranslation("authentication"); const { t } = useTranslation("authentication");
const adminClient = useAdminClient(); const adminClient = useAdminClient();
@ -83,7 +85,10 @@ export const EditFlowDropdown = ({
<AddSubFlowModal <AddSubFlowModal
name={execution.displayName!} name={execution.displayName!}
onCancel={() => setType(undefined)} onCancel={() => setType(undefined)}
onConfirm={() => setType(undefined)} onConfirm={(flow) => {
onAddFlow(flow);
setType(undefined);
}}
/> />
)} )}
</> </>

View file

@ -139,7 +139,7 @@ const renderSubFlow = (
elements.push(createEdge(endSubFlowId, end.id!)); elements.push(createEdge(endSubFlowId, end.id!));
return elements.concat( return elements.concat(
renderFlow(execution, execution.executionList, { renderFlow(execution, execution.executionList || [], {
...execution, ...execution,
id: endSubFlowId, id: endSubFlowId,
}) })

View file

@ -10,11 +10,14 @@ import {
DataListToggle, DataListToggle,
Text, Text,
TextVariants, TextVariants,
Button,
} from "@patternfly/react-core"; } from "@patternfly/react-core";
import { TrashIcon } 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 { ExpandableExecution } from "../execution-model"; import type { ExpandableExecution } from "../execution-model";
import type { Flow } from "./modals/AddSubFlowModal";
import { FlowTitle } from "./FlowTitle"; import { FlowTitle } from "./FlowTitle";
import { FlowRequirementDropdown } from "./FlowRequirementDropdown"; import { FlowRequirementDropdown } from "./FlowRequirementDropdown";
import { ExecutionConfigModal } from "./ExecutionConfigModal"; import { ExecutionConfigModal } from "./ExecutionConfigModal";
@ -30,6 +33,8 @@ type FlowRowProps = {
execution: ExpandableExecution, execution: ExpandableExecution,
type: AuthenticationProviderRepresentation type: AuthenticationProviderRepresentation
) => void; ) => void;
onAddFlow: (flow: Flow) => void;
onDelete: () => void;
}; };
export const FlowRow = ({ export const FlowRow = ({
@ -37,6 +42,8 @@ export const FlowRow = ({
onRowClick, onRowClick,
onRowChange, onRowChange,
onAddExecution, onAddExecution,
onAddFlow,
onDelete,
}: FlowRowProps) => { }: FlowRowProps) => {
const { t } = useTranslation("authentication"); const { t } = useTranslation("authentication");
const hasSubList = !!execution.executionList?.length; const hasSubList = !!execution.executionList?.length;
@ -69,13 +76,13 @@ export const FlowRow = ({
<DataListItemCells <DataListItemCells
dataListCells={[ dataListCells={[
<DataListCell key={`${execution.id}-name`}> <DataListCell key={`${execution.id}-name`}>
{!hasSubList && ( {!execution.authenticationFlow && (
<FlowTitle <FlowTitle
key={execution.id} key={execution.id}
title={execution.displayName!} title={execution.displayName!}
/> />
)} )}
{hasSubList && ( {execution.authenticationFlow && (
<> <>
{execution.displayName} <br />{" "} {execution.displayName} <br />{" "}
<Text component={TextVariants.small}> <Text component={TextVariants.small}>
@ -98,26 +105,34 @@ export const FlowRow = ({
<EditFlowDropdown <EditFlowDropdown
execution={execution} execution={execution}
onAddExecution={onAddExecution} onAddExecution={onAddExecution}
onAddFlow={onAddFlow}
/> />
)} )}
<Button
variant="plain"
aria-label={t("common:delete")}
onClick={onDelete}
>
<TrashIcon />
</Button>
</DataListCell>, </DataListCell>,
]} ]}
/> />
</DataListItemRow> </DataListItemRow>
</DataListItem> </DataListItem>
{!execution.isCollapsed && hasSubList && ( {!execution.isCollapsed &&
<> hasSubList &&
{execution.executionList.map((execution) => ( execution.executionList?.map((execution) => (
<FlowRow <FlowRow
key={execution.id} key={execution.id}
execution={execution} execution={execution}
onRowClick={onRowClick} onRowClick={onRowClick}
onRowChange={onRowChange} onRowChange={onRowChange}
onAddExecution={onAddExecution} onAddExecution={onAddExecution}
onAddFlow={onAddFlow}
onDelete={onDelete}
/> />
))} ))}
</> </>
)}
</>
); );
}; };

View file

@ -1,6 +1,6 @@
import React from "react"; import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { import {
Button, Button,
ButtonVariant, ButtonVariant,
@ -8,25 +8,53 @@ import {
FormGroup, FormGroup,
Modal, Modal,
ModalVariant, ModalVariant,
Select,
SelectOption,
SelectVariant,
TextInput, TextInput,
ValidatedOptions, ValidatedOptions,
} from "@patternfly/react-core"; } from "@patternfly/react-core";
import type { AuthenticationProviderRepresentation } from "@keycloak/keycloak-admin-client/lib/defs/authenticatorConfigRepresentation";
import { HelpItem } from "../../../components/help-enabler/HelpItem"; import { HelpItem } from "../../../components/help-enabler/HelpItem";
import { useAdminClient, useFetch } from "../../../context/auth/AdminClient";
type AddSubFlowProps = { type AddSubFlowProps = {
name: string; name: string;
onConfirm: () => void; onConfirm: (flow: Flow) => void;
onCancel: () => void; onCancel: () => void;
}; };
const types = ["basic-flow", "form-flow"] as const;
export type Flow = {
name: string;
description?: string;
type: typeof types[number];
provider: string;
};
export const AddSubFlowModal = ({ export const AddSubFlowModal = ({
name, name,
onConfirm, onConfirm,
onCancel, onCancel,
}: AddSubFlowProps) => { }: AddSubFlowProps) => {
const { t } = useTranslation("authentication"); const { t } = useTranslation("authentication");
const { register, errors, handleSubmit } = useForm(); const { register, control, errors, handleSubmit } = useForm<Flow>();
const [open, setOpen] = useState(false);
const [openProvider, setOpenProvider] = useState(false);
const [formProviders, setFormProviders] =
useState<AuthenticationProviderRepresentation[]>();
const adminClient = useAdminClient();
useFetch(
() => adminClient.authenticationManagement.getFormProviders(),
(providers) =>
setFormProviders(
providers as unknown as AuthenticationProviderRepresentation[]
),
[]
);
return ( return (
<Modal <Modal
@ -39,7 +67,8 @@ export const AddSubFlowModal = ({
id="modal-add" id="modal-add"
data-testid="modal-add" data-testid="modal-add"
key="add" key="add"
onClick={() => onConfirm()} type="submit"
form="sub-flow-form"
> >
{t("common:add")} {t("common:add")}
</Button>, </Button>,
@ -56,11 +85,7 @@ export const AddSubFlowModal = ({
</Button>, </Button>,
]} ]}
> >
<Form <Form id="sub-flow-form" isHorizontal onSubmit={handleSubmit(onConfirm)}>
id="execution-config-form"
isHorizontal
onSubmit={handleSubmit(onConfirm)}
>
<FormGroup <FormGroup
label={t("common:name")} label={t("common:name")}
fieldId="name" fieldId="name"
@ -107,6 +132,100 @@ export const AddSubFlowModal = ({
ref={register} ref={register}
/> />
</FormGroup> </FormGroup>
<FormGroup
label={t("flowType")}
labelIcon={
<HelpItem
helpText="authentication-help:flowType"
forLabel={t("flowType")}
forID="flowType"
/>
}
fieldId="flowType"
>
<Controller
name="type"
defaultValue={types[0]}
control={control}
render={({ onChange, value }) => (
<Select
menuAppendTo="parent"
toggleId="flowType"
onToggle={() => setOpen(!open)}
onSelect={(_, value) => {
onChange(value as string);
setOpen(false);
}}
selections={t(`flow-type.${value}`)}
variant={SelectVariant.single}
aria-label={t("flowType")}
isOpen={open}
>
{types.map((type) => (
<SelectOption
selected={type === value}
key={type}
value={type}
>
{t(`flow-type.${type}`)}
</SelectOption>
))}
</Select>
)}
/>
</FormGroup>
{formProviders && formProviders.length > 1 && (
<FormGroup
label={t("flowType")}
labelIcon={
<HelpItem
helpText="authentication-help:flowType"
forLabel={t("flowType")}
forID="flowType"
/>
}
fieldId="flowType"
>
<Controller
name="provider"
defaultValue=""
control={control}
render={({ onChange, value }) => (
<Select
menuAppendTo="parent"
toggleId="provider"
onToggle={(toggle) => setOpenProvider(toggle)}
onSelect={(_, value) => {
onChange(value as string);
setOpenProvider(false);
}}
selections={value.displayName}
variant={SelectVariant.single}
aria-label={t("flowType")}
isOpen={openProvider}
>
{formProviders.map((provider) => (
<SelectOption
selected={provider.displayName === value}
key={provider.id}
value={provider}
>
{provider.displayName}
</SelectOption>
))}
</Select>
)}
/>
</FormGroup>
)}
{formProviders?.length === 1 && (
<input
name="provider"
type="hidden"
ref={register}
value={formProviders[0].id}
/>
)}
</Form> </Form>
</Modal> </Modal>
); );

View file

@ -1,7 +1,7 @@
import type AuthenticationExecutionInfoRepresentation from "@keycloak/keycloak-admin-client/lib/defs/authenticationExecutionInfoRepresentation"; import type AuthenticationExecutionInfoRepresentation from "@keycloak/keycloak-admin-client/lib/defs/authenticationExecutionInfoRepresentation";
export type ExpandableExecution = AuthenticationExecutionInfoRepresentation & { export type ExpandableExecution = AuthenticationExecutionInfoRepresentation & {
executionList: ExpandableExecution[]; executionList?: ExpandableExecution[];
isCollapsed: boolean; isCollapsed: boolean;
}; };
@ -34,10 +34,11 @@ export class ExecutionList {
constructor(list: AuthenticationExecutionInfoRepresentation[]) { constructor(list: AuthenticationExecutionInfoRepresentation[]) {
this.list = list as ExpandableExecution[]; this.list = list as ExpandableExecution[];
this.expandableList = this.transformToExpandableList(0, 0, { this.expandableList =
this.transformToExpandableList(0, 0, {
executionList: [], executionList: [],
isCollapsed: false, isCollapsed: false,
}).executionList; }).executionList || [];
} }
private transformToExpandableList( private transformToExpandableList(
@ -50,7 +51,7 @@ export class ExecutionList {
const nextRowLevel = this.list[index + 1]?.level || 0; const nextRowLevel = this.list[index + 1]?.level || 0;
if (ex.level === level && nextRowLevel <= level) { if (ex.level === level && nextRowLevel <= level) {
execution.executionList.push(ex); execution.executionList?.push(ex);
} else if (ex.level === level && nextRowLevel > level) { } else if (ex.level === level && nextRowLevel > level) {
const expandable = this.transformToExpandableList( const expandable = this.transformToExpandableList(
nextRowLevel, nextRowLevel,
@ -61,7 +62,7 @@ export class ExecutionList {
isCollapsed: false, isCollapsed: false,
} }
); );
execution.executionList.push(expandable); execution.executionList?.push(expandable);
} }
} }
return execution; return execution;

View file

@ -17,6 +17,7 @@ import { useAdminClient } from "../../context/auth/AdminClient";
import { useAlerts } from "../../components/alert/Alerts"; import { useAlerts } from "../../components/alert/Alerts";
import { NameDescription } from "./NameDescription"; import { NameDescription } from "./NameDescription";
import { FlowType } from "./FlowType"; import { FlowType } from "./FlowType";
import { toFlow } from "../routes/Flow";
export const CreateFlow = () => { export const CreateFlow = () => {
const { t } = useTranslation("authentication"); const { t } = useTranslation("authentication");
@ -32,8 +33,17 @@ export const CreateFlow = () => {
const save = async (flow: AuthenticationFlowRepresentation) => { const save = async (flow: AuthenticationFlowRepresentation) => {
try { try {
await adminClient.authenticationManagement.createFlow(flow); const { id } = (await adminClient.authenticationManagement.createFlow(
flow
)) as unknown as AuthenticationFlowRepresentation;
addAlert(t("flowCreatedSuccess"), AlertVariant.success); addAlert(t("flowCreatedSuccess"), AlertVariant.success);
history.push(
toFlow({
realm,
id: id!,
usedBy: "notInUse",
})
);
} catch (error: any) { } catch (error: any) {
addAlert( addAlert(
t("flowCreateError", { t("flowCreateError", {

View file

@ -10,7 +10,7 @@ import {
import { HelpItem } from "../../components/help-enabler/HelpItem"; import { HelpItem } from "../../components/help-enabler/HelpItem";
const types = ["basic-flow", "client-flow"]; const TYPES = ["basic-flow", "client-flow"] as const;
export const FlowType = () => { export const FlowType = () => {
const { t } = useTranslation("authentication"); const { t } = useTranslation("authentication");
@ -23,7 +23,7 @@ export const FlowType = () => {
label={t("flowType")} label={t("flowType")}
labelIcon={ labelIcon={
<HelpItem <HelpItem
helpText="authentication-help:flowType" helpText="authentication-help:topLevelFlowType"
forLabel={t("flowType")} forLabel={t("flowType")}
forID="flowType" forID="flowType"
/> />
@ -32,7 +32,7 @@ export const FlowType = () => {
> >
<Controller <Controller
name="providerId" name="providerId"
defaultValue={types[0]} defaultValue={TYPES[0]}
control={control} control={control}
render={({ onChange, value }) => ( render={({ onChange, value }) => (
<Select <Select
@ -42,17 +42,15 @@ export const FlowType = () => {
onChange(value as string); onChange(value as string);
setOpen(false); setOpen(false);
}} }}
selections={t(`flow-type.${value}`)} selections={t(`top-level-flow-type.${value}`)}
variant={SelectVariant.single} variant={SelectVariant.single}
aria-label={t("flowType")} aria-label={t("flowType")}
isOpen={open} isOpen={open}
> >
{types.map((type) => ( {TYPES.map((type) => (
<SelectOption <SelectOption selected={type === value} key={type} value={type}>
selected={type === value} {t(`top-level-flow-type.${type}`)}
key={type} </SelectOption>
value={t(`flow-type.${type}`)}
/>
))} ))}
</Select> </Select>
)} )}

View file

@ -3,7 +3,8 @@ export default {
name: "Help text for the name of the new flow", name: "Help text for the name of the new flow",
description: "Help text for the description of the new flow", description: "Help text for the description of the new flow",
createFlow: "You can create a top level flow within this from", createFlow: "You can create a top level flow within this from",
flowType: flowType: "What kind of form is it",
topLevelFlowType:
"What kind of top level flow is it? Type 'client' is used for authentication of clients (applications) when generic is for users and everything else", "What kind of top level flow is it? Type 'client' is used for authentication of clients (applications) when generic is for users and everything else",
addExecution: addExecution:
"Execution can have a wide range of actions, from sending a reset email to validating an OTP", "Execution can have a wide range of actions, from sending a reset email to validating an OTP",

View file

@ -18,6 +18,11 @@ export default {
deleteFlowSuccess: "Flow successfully deleted", deleteFlowSuccess: "Flow successfully deleted",
deleteFlowError: "Could not delete flow: {{error}}", deleteFlowError: "Could not delete flow: {{error}}",
duplicateFlow: "Duplicate flow", duplicateFlow: "Duplicate flow",
deleteConfirmExecution: "Delete execution?",
deleteConfirmExecutionMessage:
'Are you sure you want to permanently delete the execution "<1>{{name}}</1>".',
deleteExecutionSuccess: "Execution successfully deleted",
deleteExecutionError: "Could not delete execution: {{error}}",
updateFlowSuccess: "Flow successfully updated", updateFlowSuccess: "Flow successfully updated",
updateFlowError: "Could not update flow: {{error}}", updateFlowError: "Could not update flow: {{error}}",
copyOf: "Copy of {{name}}", copyOf: "Copy of {{name}}",
@ -26,6 +31,10 @@ export default {
createFlow: "Create flow", createFlow: "Create flow",
flowType: "Flow type", flowType: "Flow type",
"flow-type": { "flow-type": {
"basic-flow": "Generic",
"form-flow": "Form",
},
"top-level-flow-type": {
"basic-flow": "Basic flow", "basic-flow": "Basic flow",
"client-flow": "Client flow", "client-flow": "Client flow",
}, },