added subflow and create flow to authentication (#1198)
* added subflow and create flow to authentication * fixed types
This commit is contained in:
parent
8a9f96e53a
commit
b7ebd3260b
13 changed files with 451 additions and 75 deletions
|
@ -14,7 +14,7 @@ describe("Authentication test", () => {
|
|||
|
||||
const detailPage = new FlowDetails();
|
||||
|
||||
beforeEach(function () {
|
||||
beforeEach(() => {
|
||||
keycloakBefore();
|
||||
loginPage.logIn();
|
||||
sidebarPage.goToAuthentication();
|
||||
|
@ -78,4 +78,32 @@ describe("Authentication test", () => {
|
|||
masthead.checkNotificationMessage("Flow successfully updated");
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,6 +6,11 @@ export default class FlowDetails {
|
|||
return this;
|
||||
}
|
||||
|
||||
flowExists(name: string) {
|
||||
cy.findAllByText(name).should("exist");
|
||||
return this;
|
||||
}
|
||||
|
||||
private getExecution(name: string) {
|
||||
return cy.findByTestId(name);
|
||||
}
|
||||
|
@ -63,4 +68,38 @@ export default class FlowDetails {
|
|||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React from "react";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Button,
|
||||
|
@ -8,23 +8,62 @@ import {
|
|||
TitleSizes,
|
||||
} 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 { AddStepModal } from "./components/modals/AddStepModal";
|
||||
import { AddSubFlowModal, Flow } from "./components/modals/AddSubFlowModal";
|
||||
|
||||
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 [show, setShow] = useState<SectionType>();
|
||||
|
||||
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
|
||||
message={t("emptyExecution")}
|
||||
instructions={t("emptyExecutionInstructions")}
|
||||
/>
|
||||
|
||||
<div className="keycloak__empty-execution-state__block">
|
||||
{sections.map((section) => (
|
||||
{SECTIONS.map((section) => (
|
||||
<Flex key={section} className="keycloak__empty-execution-state__help">
|
||||
<FlexItem flex={{ default: "flex_1" }}>
|
||||
<Title headingLevel="h3" size={TitleSizes.md}>
|
||||
|
@ -34,7 +73,13 @@ export const EmptyExecutionState = () => {
|
|||
</FlexItem>
|
||||
<Flex alignSelf={{ default: "alignSelfCenter" }}>
|
||||
<FlexItem>
|
||||
<Button variant="tertiary">{t(section)}</Button>
|
||||
<Button
|
||||
data-testId={section}
|
||||
variant="tertiary"
|
||||
onClick={() => setShow(section)}
|
||||
>
|
||||
{t(section)}
|
||||
</Button>
|
||||
</FlexItem>
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import {
|
||||
DataList,
|
||||
Label,
|
||||
|
@ -10,8 +10,11 @@ import {
|
|||
ToggleGroup,
|
||||
ToggleGroupItem,
|
||||
AlertVariant,
|
||||
ActionGroup,
|
||||
Button,
|
||||
ButtonVariant,
|
||||
} 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 { AuthenticationProviderRepresentation } from "@keycloak/keycloak-admin-client/lib/defs/authenticatorConfigRepresentation";
|
||||
|
@ -31,6 +34,9 @@ import {
|
|||
} from "./execution-model";
|
||||
import { FlowDiagram } from "./components/FlowDiagram";
|
||||
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 = (
|
||||
value: AuthenticationProviderRepresentation
|
||||
|
@ -51,6 +57,12 @@ export const FlowDetails = () => {
|
|||
useState<AuthenticationExecutionInfoRepresentation>();
|
||||
const [liveText, setLiveText] = useState("");
|
||||
|
||||
const [showAddExecutionDialog, setShowAddExecutionDialog] =
|
||||
useState<boolean>();
|
||||
const [showAddSubFlowDialog, setShowSubFlowDialog] = useState<boolean>();
|
||||
const [selectedExecution, setSelectedExecution] =
|
||||
useState<ExpandableExecution>();
|
||||
|
||||
useFetch(
|
||||
async () => {
|
||||
const flows = await adminClient.authenticationManagement.getFlows();
|
||||
|
@ -84,23 +96,17 @@ export const FlowDetails = () => {
|
|||
id = result.id!;
|
||||
}
|
||||
const times = change.newIndex - change.oldIndex;
|
||||
const requests = [];
|
||||
for (let index = 0; index < Math.abs(times); index++) {
|
||||
if (times > 0) {
|
||||
requests.push(
|
||||
adminClient.authenticationManagement.lowerPriorityExecution({
|
||||
id,
|
||||
})
|
||||
);
|
||||
await adminClient.authenticationManagement.lowerPriorityExecution({
|
||||
id,
|
||||
});
|
||||
} else {
|
||||
requests.push(
|
||||
adminClient.authenticationManagement.raisePriorityExecution({
|
||||
id,
|
||||
})
|
||||
);
|
||||
await adminClient.authenticationManagement.raisePriorityExecution({
|
||||
id,
|
||||
});
|
||||
}
|
||||
}
|
||||
await Promise.all(requests);
|
||||
refresh();
|
||||
addAlert(t("updateFlowSuccess"), AlertVariant.success);
|
||||
} catch (error: any) {
|
||||
|
@ -124,12 +130,12 @@ export const FlowDetails = () => {
|
|||
};
|
||||
|
||||
const addExecution = async (
|
||||
execution: ExpandableExecution,
|
||||
name: string,
|
||||
type: AuthenticationProviderRepresentation
|
||||
) => {
|
||||
try {
|
||||
await adminClient.authenticationManagement.addExecutionToFlow({
|
||||
flow: execution.displayName!,
|
||||
flow: name,
|
||||
provider: type.id!,
|
||||
});
|
||||
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 (
|
||||
<>
|
||||
<ViewHeader
|
||||
|
@ -161,7 +211,7 @@ export const FlowDetails = () => {
|
|||
]}
|
||||
/>
|
||||
<PageSection variant="light">
|
||||
{executionList?.expandableList?.length && (
|
||||
{hasExecutions && (
|
||||
<Toolbar id="toolbar">
|
||||
<ToolbarContent>
|
||||
<ToggleGroup>
|
||||
|
@ -183,8 +233,9 @@ export const FlowDetails = () => {
|
|||
</ToolbarContent>
|
||||
</Toolbar>
|
||||
)}
|
||||
{tableView && executionList?.expandableList?.length && (
|
||||
{tableView && executionList && hasExecutions && (
|
||||
<>
|
||||
<DeleteConfirm />
|
||||
<DataList
|
||||
aria-label="flows"
|
||||
onDragFinish={(order) => {
|
||||
|
@ -204,7 +255,7 @@ export const FlowDetails = () => {
|
|||
t("common:onDragStart", { item: item.displayName })
|
||||
);
|
||||
setDragged(item);
|
||||
if (item.executionList && !item.isCollapsed) {
|
||||
if (!item.isCollapsed) {
|
||||
item.isCollapsed = true;
|
||||
setExecutionList(executionList.clone());
|
||||
}
|
||||
|
@ -231,11 +282,62 @@ export const FlowDetails = () => {
|
|||
setExecutionList(executionList.clone());
|
||||
}}
|
||||
onRowChange={update}
|
||||
onAddExecution={addExecution}
|
||||
onAddExecution={(execution, type) =>
|
||||
addExecution(execution.displayName!, type)
|
||||
}
|
||||
onAddFlow={(flow) => addFlow(execution.displayName!, flow)}
|
||||
onDelete={() => {
|
||||
setSelectedExecution(execution);
|
||||
toggleDeleteDialog();
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
</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">
|
||||
{liveText}
|
||||
</div>
|
||||
|
@ -245,8 +347,12 @@ export const FlowDetails = () => {
|
|||
<FlowDiagram executionList={executionList} />
|
||||
)}
|
||||
{!executionList?.expandableList ||
|
||||
(executionList.expandableList.length === 0 && (
|
||||
<EmptyExecutionState />
|
||||
(flow && !hasExecutions && (
|
||||
<EmptyExecutionState
|
||||
flow={flow}
|
||||
onAddExecution={(type) => addExecution(flow.alias!, type)}
|
||||
onAddFlow={(newFlow) => addFlow(flow.alias!, newFlow)}
|
||||
/>
|
||||
))}
|
||||
</PageSection>
|
||||
</>
|
||||
|
|
|
@ -7,7 +7,7 @@ import type { AuthenticationProviderRepresentation } from "@keycloak/keycloak-ad
|
|||
import type { ExpandableExecution } from "../execution-model";
|
||||
import { AddStepModal, FlowType } from "./modals/AddStepModal";
|
||||
import { useAdminClient, useFetch } from "../../context/auth/AdminClient";
|
||||
import { AddSubFlowModal } from "./modals/AddSubFlowModal";
|
||||
import { AddSubFlowModal, Flow } from "./modals/AddSubFlowModal";
|
||||
|
||||
type EditFlowDropdownProps = {
|
||||
execution: ExpandableExecution;
|
||||
|
@ -15,11 +15,13 @@ type EditFlowDropdownProps = {
|
|||
execution: ExpandableExecution,
|
||||
type: AuthenticationProviderRepresentation
|
||||
) => void;
|
||||
onAddFlow: (flow: Flow) => void;
|
||||
};
|
||||
|
||||
export const EditFlowDropdown = ({
|
||||
execution,
|
||||
onAddExecution,
|
||||
onAddFlow,
|
||||
}: EditFlowDropdownProps) => {
|
||||
const { t } = useTranslation("authentication");
|
||||
const adminClient = useAdminClient();
|
||||
|
@ -83,7 +85,10 @@ export const EditFlowDropdown = ({
|
|||
<AddSubFlowModal
|
||||
name={execution.displayName!}
|
||||
onCancel={() => setType(undefined)}
|
||||
onConfirm={() => setType(undefined)}
|
||||
onConfirm={(flow) => {
|
||||
onAddFlow(flow);
|
||||
setType(undefined);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
|
|
@ -139,7 +139,7 @@ const renderSubFlow = (
|
|||
elements.push(createEdge(endSubFlowId, end.id!));
|
||||
|
||||
return elements.concat(
|
||||
renderFlow(execution, execution.executionList, {
|
||||
renderFlow(execution, execution.executionList || [], {
|
||||
...execution,
|
||||
id: endSubFlowId,
|
||||
})
|
||||
|
|
|
@ -10,11 +10,14 @@ import {
|
|||
DataListToggle,
|
||||
Text,
|
||||
TextVariants,
|
||||
Button,
|
||||
} from "@patternfly/react-core";
|
||||
import { TrashIcon } 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 { ExpandableExecution } from "../execution-model";
|
||||
import type { Flow } from "./modals/AddSubFlowModal";
|
||||
import { FlowTitle } from "./FlowTitle";
|
||||
import { FlowRequirementDropdown } from "./FlowRequirementDropdown";
|
||||
import { ExecutionConfigModal } from "./ExecutionConfigModal";
|
||||
|
@ -30,6 +33,8 @@ type FlowRowProps = {
|
|||
execution: ExpandableExecution,
|
||||
type: AuthenticationProviderRepresentation
|
||||
) => void;
|
||||
onAddFlow: (flow: Flow) => void;
|
||||
onDelete: () => void;
|
||||
};
|
||||
|
||||
export const FlowRow = ({
|
||||
|
@ -37,6 +42,8 @@ export const FlowRow = ({
|
|||
onRowClick,
|
||||
onRowChange,
|
||||
onAddExecution,
|
||||
onAddFlow,
|
||||
onDelete,
|
||||
}: FlowRowProps) => {
|
||||
const { t } = useTranslation("authentication");
|
||||
const hasSubList = !!execution.executionList?.length;
|
||||
|
@ -69,13 +76,13 @@ export const FlowRow = ({
|
|||
<DataListItemCells
|
||||
dataListCells={[
|
||||
<DataListCell key={`${execution.id}-name`}>
|
||||
{!hasSubList && (
|
||||
{!execution.authenticationFlow && (
|
||||
<FlowTitle
|
||||
key={execution.id}
|
||||
title={execution.displayName!}
|
||||
/>
|
||||
)}
|
||||
{hasSubList && (
|
||||
{execution.authenticationFlow && (
|
||||
<>
|
||||
{execution.displayName} <br />{" "}
|
||||
<Text component={TextVariants.small}>
|
||||
|
@ -98,26 +105,34 @@ export const FlowRow = ({
|
|||
<EditFlowDropdown
|
||||
execution={execution}
|
||||
onAddExecution={onAddExecution}
|
||||
onAddFlow={onAddFlow}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
variant="plain"
|
||||
aria-label={t("common:delete")}
|
||||
onClick={onDelete}
|
||||
>
|
||||
<TrashIcon />
|
||||
</Button>
|
||||
</DataListCell>,
|
||||
]}
|
||||
/>
|
||||
</DataListItemRow>
|
||||
</DataListItem>
|
||||
{!execution.isCollapsed && hasSubList && (
|
||||
<>
|
||||
{execution.executionList.map((execution) => (
|
||||
<FlowRow
|
||||
key={execution.id}
|
||||
execution={execution}
|
||||
onRowClick={onRowClick}
|
||||
onRowChange={onRowChange}
|
||||
onAddExecution={onAddExecution}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
{!execution.isCollapsed &&
|
||||
hasSubList &&
|
||||
execution.executionList?.map((execution) => (
|
||||
<FlowRow
|
||||
key={execution.id}
|
||||
execution={execution}
|
||||
onRowClick={onRowClick}
|
||||
onRowChange={onRowChange}
|
||||
onAddExecution={onAddExecution}
|
||||
onAddFlow={onAddFlow}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React from "react";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import {
|
||||
Button,
|
||||
ButtonVariant,
|
||||
|
@ -8,25 +8,53 @@ import {
|
|||
FormGroup,
|
||||
Modal,
|
||||
ModalVariant,
|
||||
Select,
|
||||
SelectOption,
|
||||
SelectVariant,
|
||||
TextInput,
|
||||
ValidatedOptions,
|
||||
} from "@patternfly/react-core";
|
||||
|
||||
import type { AuthenticationProviderRepresentation } from "@keycloak/keycloak-admin-client/lib/defs/authenticatorConfigRepresentation";
|
||||
import { HelpItem } from "../../../components/help-enabler/HelpItem";
|
||||
import { useAdminClient, useFetch } from "../../../context/auth/AdminClient";
|
||||
|
||||
type AddSubFlowProps = {
|
||||
name: string;
|
||||
onConfirm: () => void;
|
||||
onConfirm: (flow: Flow) => 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 = ({
|
||||
name,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
}: AddSubFlowProps) => {
|
||||
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 (
|
||||
<Modal
|
||||
|
@ -39,7 +67,8 @@ export const AddSubFlowModal = ({
|
|||
id="modal-add"
|
||||
data-testid="modal-add"
|
||||
key="add"
|
||||
onClick={() => onConfirm()}
|
||||
type="submit"
|
||||
form="sub-flow-form"
|
||||
>
|
||||
{t("common:add")}
|
||||
</Button>,
|
||||
|
@ -56,11 +85,7 @@ export const AddSubFlowModal = ({
|
|||
</Button>,
|
||||
]}
|
||||
>
|
||||
<Form
|
||||
id="execution-config-form"
|
||||
isHorizontal
|
||||
onSubmit={handleSubmit(onConfirm)}
|
||||
>
|
||||
<Form id="sub-flow-form" isHorizontal onSubmit={handleSubmit(onConfirm)}>
|
||||
<FormGroup
|
||||
label={t("common:name")}
|
||||
fieldId="name"
|
||||
|
@ -107,6 +132,100 @@ export const AddSubFlowModal = ({
|
|||
ref={register}
|
||||
/>
|
||||
</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>
|
||||
</Modal>
|
||||
);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import type AuthenticationExecutionInfoRepresentation from "@keycloak/keycloak-admin-client/lib/defs/authenticationExecutionInfoRepresentation";
|
||||
|
||||
export type ExpandableExecution = AuthenticationExecutionInfoRepresentation & {
|
||||
executionList: ExpandableExecution[];
|
||||
executionList?: ExpandableExecution[];
|
||||
isCollapsed: boolean;
|
||||
};
|
||||
|
||||
|
@ -34,10 +34,11 @@ export class ExecutionList {
|
|||
|
||||
constructor(list: AuthenticationExecutionInfoRepresentation[]) {
|
||||
this.list = list as ExpandableExecution[];
|
||||
this.expandableList = this.transformToExpandableList(0, 0, {
|
||||
executionList: [],
|
||||
isCollapsed: false,
|
||||
}).executionList;
|
||||
this.expandableList =
|
||||
this.transformToExpandableList(0, 0, {
|
||||
executionList: [],
|
||||
isCollapsed: false,
|
||||
}).executionList || [];
|
||||
}
|
||||
|
||||
private transformToExpandableList(
|
||||
|
@ -50,7 +51,7 @@ export class ExecutionList {
|
|||
const nextRowLevel = this.list[index + 1]?.level || 0;
|
||||
|
||||
if (ex.level === level && nextRowLevel <= level) {
|
||||
execution.executionList.push(ex);
|
||||
execution.executionList?.push(ex);
|
||||
} else if (ex.level === level && nextRowLevel > level) {
|
||||
const expandable = this.transformToExpandableList(
|
||||
nextRowLevel,
|
||||
|
@ -61,7 +62,7 @@ export class ExecutionList {
|
|||
isCollapsed: false,
|
||||
}
|
||||
);
|
||||
execution.executionList.push(expandable);
|
||||
execution.executionList?.push(expandable);
|
||||
}
|
||||
}
|
||||
return execution;
|
||||
|
|
|
@ -17,6 +17,7 @@ import { useAdminClient } from "../../context/auth/AdminClient";
|
|||
import { useAlerts } from "../../components/alert/Alerts";
|
||||
import { NameDescription } from "./NameDescription";
|
||||
import { FlowType } from "./FlowType";
|
||||
import { toFlow } from "../routes/Flow";
|
||||
|
||||
export const CreateFlow = () => {
|
||||
const { t } = useTranslation("authentication");
|
||||
|
@ -32,8 +33,17 @@ export const CreateFlow = () => {
|
|||
|
||||
const save = async (flow: AuthenticationFlowRepresentation) => {
|
||||
try {
|
||||
await adminClient.authenticationManagement.createFlow(flow);
|
||||
const { id } = (await adminClient.authenticationManagement.createFlow(
|
||||
flow
|
||||
)) as unknown as AuthenticationFlowRepresentation;
|
||||
addAlert(t("flowCreatedSuccess"), AlertVariant.success);
|
||||
history.push(
|
||||
toFlow({
|
||||
realm,
|
||||
id: id!,
|
||||
usedBy: "notInUse",
|
||||
})
|
||||
);
|
||||
} catch (error: any) {
|
||||
addAlert(
|
||||
t("flowCreateError", {
|
||||
|
|
|
@ -10,7 +10,7 @@ import {
|
|||
|
||||
import { HelpItem } from "../../components/help-enabler/HelpItem";
|
||||
|
||||
const types = ["basic-flow", "client-flow"];
|
||||
const TYPES = ["basic-flow", "client-flow"] as const;
|
||||
|
||||
export const FlowType = () => {
|
||||
const { t } = useTranslation("authentication");
|
||||
|
@ -23,7 +23,7 @@ export const FlowType = () => {
|
|||
label={t("flowType")}
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="authentication-help:flowType"
|
||||
helpText="authentication-help:topLevelFlowType"
|
||||
forLabel={t("flowType")}
|
||||
forID="flowType"
|
||||
/>
|
||||
|
@ -32,7 +32,7 @@ export const FlowType = () => {
|
|||
>
|
||||
<Controller
|
||||
name="providerId"
|
||||
defaultValue={types[0]}
|
||||
defaultValue={TYPES[0]}
|
||||
control={control}
|
||||
render={({ onChange, value }) => (
|
||||
<Select
|
||||
|
@ -42,17 +42,15 @@ export const FlowType = () => {
|
|||
onChange(value as string);
|
||||
setOpen(false);
|
||||
}}
|
||||
selections={t(`flow-type.${value}`)}
|
||||
selections={t(`top-level-flow-type.${value}`)}
|
||||
variant={SelectVariant.single}
|
||||
aria-label={t("flowType")}
|
||||
isOpen={open}
|
||||
>
|
||||
{types.map((type) => (
|
||||
<SelectOption
|
||||
selected={type === value}
|
||||
key={type}
|
||||
value={t(`flow-type.${type}`)}
|
||||
/>
|
||||
{TYPES.map((type) => (
|
||||
<SelectOption selected={type === value} key={type} value={type}>
|
||||
{t(`top-level-flow-type.${type}`)}
|
||||
</SelectOption>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
|
|
|
@ -3,7 +3,8 @@ export default {
|
|||
name: "Help text for the name 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",
|
||||
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",
|
||||
addExecution:
|
||||
"Execution can have a wide range of actions, from sending a reset email to validating an OTP",
|
||||
|
|
|
@ -18,6 +18,11 @@ export default {
|
|||
deleteFlowSuccess: "Flow successfully deleted",
|
||||
deleteFlowError: "Could not delete flow: {{error}}",
|
||||
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",
|
||||
updateFlowError: "Could not update flow: {{error}}",
|
||||
copyOf: "Copy of {{name}}",
|
||||
|
@ -26,6 +31,10 @@ export default {
|
|||
createFlow: "Create flow",
|
||||
flowType: "Flow type",
|
||||
"flow-type": {
|
||||
"basic-flow": "Generic",
|
||||
"form-flow": "Form",
|
||||
},
|
||||
"top-level-flow-type": {
|
||||
"basic-flow": "Basic flow",
|
||||
"client-flow": "Client flow",
|
||||
},
|
||||
|
|
Loading…
Reference in a new issue