* Refactor node creation in FlowDiagram Signed-off-by: Anil Dhurjaty <anil.dhurjaty@appfolio.com> * Fix flow diagram incorrect branching display Signed-off-by: Anil Dhurjaty <anil.dhurjaty@appfolio.com> * Enable react unit testing with vitest Signed-off-by: Anil Dhurjaty <anil.dhurjaty@appfolio.com> * Add react tests for FlowDiagram Signed-off-by: Anil Dhurjaty <anil.dhurjaty@appfolio.com> * Add cypress test for browser flow diagram Signed-off-by: Anil Dhurjaty <anil.dhurjaty@appfolio.com> --------- Signed-off-by: Anil Dhurjaty <anil.dhurjaty@appfolio.com>
This commit is contained in:
parent
2ebad818f9
commit
6a38be6239
6 changed files with 662 additions and 188 deletions
|
@ -15,6 +15,7 @@ import BindFlowModal from "../support/pages/admin-ui/manage/authentication/BindF
|
|||
import OTPPolicies from "../support/pages/admin-ui/manage/authentication/OTPPolicies";
|
||||
import WebAuthnPolicies from "../support/pages/admin-ui/manage/authentication/WebAuthnPolicies";
|
||||
import CIBAPolicyPage from "../support/pages/admin-ui/manage/authentication/CIBAPolicyPage";
|
||||
import FlowDiagram from "../support/pages/admin-ui/manage/authentication/FlowDiagram";
|
||||
|
||||
const loginPage = new LoginPage();
|
||||
const masthead = new Masthead();
|
||||
|
@ -25,6 +26,7 @@ const realmName = "test" + uuid();
|
|||
|
||||
describe("Authentication test", () => {
|
||||
const detailPage = new FlowDetails();
|
||||
const diagramView = new FlowDiagram();
|
||||
const duplicateFlowModal = new DuplicateFlowModal();
|
||||
const modalUtil = new ModalUtils();
|
||||
|
||||
|
@ -118,7 +120,7 @@ describe("Authentication test", () => {
|
|||
|
||||
detailPage.goToDiagram();
|
||||
|
||||
cy.get(".react-flow").should("exist");
|
||||
diagramView.exists();
|
||||
});
|
||||
|
||||
it("Should add a execution", () => {
|
||||
|
@ -207,6 +209,29 @@ describe("Authentication test", () => {
|
|||
new BindFlowModal().fill("Direct grant flow").save();
|
||||
masthead.checkNotificationMessage("Flow successfully updated");
|
||||
});
|
||||
|
||||
it("Should display the default browser flow diagram", () => {
|
||||
listingPage.goToItemDetails("browser");
|
||||
|
||||
detailPage.goToDiagram();
|
||||
|
||||
diagramView.exists();
|
||||
|
||||
diagramView.edgesExist([
|
||||
{ from: "Start", to: "Cookie" },
|
||||
{ from: "Cookie", to: "End" },
|
||||
{ from: "Start", to: "Kerberos" },
|
||||
{ from: "Kerberos", to: "End" },
|
||||
{ from: "Start", to: "Identity Provider Redirector" },
|
||||
{ from: "Identity Provider Redirector", to: "End" },
|
||||
{ from: "Start", to: "Start forms" },
|
||||
{ from: "Start forms", to: "Username Password Form" },
|
||||
{ from: "Username Password Form", to: "Condition - user configured" },
|
||||
{ from: "Condition - user configured", to: "OTP Form" },
|
||||
{ from: "Condition - user configured", to: "End forms" },
|
||||
{ from: "End forms", to: "End" },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Required actions", () => {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import CommonElements from "../CommonElements";
|
||||
|
||||
export default class SidebarPage extends CommonElements {
|
||||
#realmsDrpDwn = "realmSelectorToggle";
|
||||
#realmsDrpDwn = "realmSelector";
|
||||
#createRealmBtn = "add-realm";
|
||||
|
||||
#clientsBtn = "#nav-item-clients";
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
type Edge = { from: string; to: string };
|
||||
|
||||
export default class FlowDiagram {
|
||||
exists() {
|
||||
cy.get(".react-flow").should("exist");
|
||||
}
|
||||
|
||||
edgesExist(edges: Edge[]) {
|
||||
edges.forEach((edge) => {
|
||||
this.#labelToId(edge.from).then((fromId) => {
|
||||
this.#labelToId(edge.to).then((toId) => {
|
||||
const label = `Edge from ${fromId} to ${toId}`;
|
||||
cy.get(`[aria-label="${label}"]`);
|
||||
});
|
||||
});
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
#labelToId(label: string) {
|
||||
return cy.findByText(label).then((node) => {
|
||||
const id = node.attr("data-id");
|
||||
if (id) {
|
||||
return cy.wrap(id);
|
||||
}
|
||||
// if data-id does not exist, we're looking at a subflow, which has the data-id
|
||||
// on the grandparent
|
||||
return cy
|
||||
.wrap(node)
|
||||
.parent()
|
||||
.parent()
|
||||
.then((n) => cy.wrap(n.attr("data-id")));
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,473 @@
|
|||
// eslint-disable-next-line no-restricted-imports, @typescript-eslint/no-unused-vars
|
||||
import * as React from "react";
|
||||
import { render } from "@testing-library/react";
|
||||
import { FlowDiagram } from "../components/FlowDiagram";
|
||||
import { describe, expect, it, beforeEach } from "vitest";
|
||||
import { ExecutionList } from "../execution-model";
|
||||
|
||||
// mock react-flow
|
||||
// code from https://reactflow.dev/learn/advanced-use/testing
|
||||
class ResizeObserver {
|
||||
callback: globalThis.ResizeObserverCallback;
|
||||
|
||||
constructor(callback: globalThis.ResizeObserverCallback) {
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
observe(target: Element) {
|
||||
this.callback([{ target } as globalThis.ResizeObserverEntry], this);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
unobserve() {}
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
disconnect() {}
|
||||
}
|
||||
|
||||
class DOMMatrixReadOnly {
|
||||
m22: number;
|
||||
constructor(transform: string) {
|
||||
const scale = transform.match(/scale\(([1-9.])\)/)?.[1];
|
||||
this.m22 = scale !== undefined ? +scale : 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Only run the shim once when requested
|
||||
let init = false;
|
||||
|
||||
export const mockReactFlow = () => {
|
||||
if (init) return;
|
||||
init = true;
|
||||
|
||||
global.ResizeObserver = ResizeObserver;
|
||||
|
||||
// @ts-ignore
|
||||
global.DOMMatrixReadOnly = DOMMatrixReadOnly;
|
||||
|
||||
Object.defineProperties(global.HTMLElement.prototype, {
|
||||
offsetHeight: {
|
||||
get() {
|
||||
return parseFloat(this.style.height) || 1;
|
||||
},
|
||||
},
|
||||
offsetWidth: {
|
||||
get() {
|
||||
return parseFloat(this.style.width) || 1;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
(global.SVGElement as any).prototype.getBBox = () => ({
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
});
|
||||
};
|
||||
|
||||
describe("<FlowDiagram />", () => {
|
||||
beforeEach(() => {
|
||||
mockReactFlow();
|
||||
});
|
||||
|
||||
const reactFlowTester = (container: HTMLElement) => ({
|
||||
expectEdgeLabels: (expectedEdges: string[]) => {
|
||||
const edges = Array.from(
|
||||
container.getElementsByClassName("react-flow__edge"),
|
||||
);
|
||||
expect(
|
||||
edges.map((edge) => edge.getAttribute("aria-label")).sort(),
|
||||
).toEqual(expectedEdges.sort());
|
||||
},
|
||||
expectNodeIds: (expectedNodes: string[]) => {
|
||||
const nodes = Array.from(
|
||||
container.getElementsByClassName("react-flow__node"),
|
||||
);
|
||||
expect(nodes.map((node) => node.getAttribute("data-id")).sort()).toEqual(
|
||||
expectedNodes.sort(),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
it("should render a flow with one required step", () => {
|
||||
const executionList = new ExecutionList([
|
||||
{ id: "single", displayName: "Single", level: 0 },
|
||||
]);
|
||||
|
||||
const { container } = render(<FlowDiagram executionList={executionList} />);
|
||||
|
||||
// const nodes = Array.from(container.getElementsByClassName("react-flow__node"));
|
||||
const testHelper = reactFlowTester(container);
|
||||
|
||||
const expectedEdges = [
|
||||
"Edge from start to single",
|
||||
"Edge from single to end",
|
||||
];
|
||||
testHelper.expectEdgeLabels(expectedEdges);
|
||||
|
||||
const expectedNodes = new Set(["start", "single", "end"]);
|
||||
testHelper.expectNodeIds(Array.from(expectedNodes));
|
||||
});
|
||||
|
||||
it("should render a start connected to end with no steps", () => {
|
||||
const executionList = new ExecutionList([]);
|
||||
|
||||
const { container } = render(<FlowDiagram executionList={executionList} />);
|
||||
|
||||
const testHelper = reactFlowTester(container);
|
||||
|
||||
const expectedEdges = ["Edge from start to end"];
|
||||
testHelper.expectEdgeLabels(expectedEdges);
|
||||
|
||||
const expectedNodes = new Set(["start", "end"]);
|
||||
testHelper.expectNodeIds(Array.from(expectedNodes));
|
||||
});
|
||||
|
||||
it("should render two branches with two alternative steps", () => {
|
||||
const executionList = new ExecutionList([
|
||||
{
|
||||
id: "alt1",
|
||||
displayName: "Alt1",
|
||||
requirement: "ALTERNATIVE",
|
||||
},
|
||||
{
|
||||
id: "alt2",
|
||||
displayName: "Alt2",
|
||||
requirement: "ALTERNATIVE",
|
||||
},
|
||||
]);
|
||||
|
||||
const { container } = render(<FlowDiagram executionList={executionList} />);
|
||||
|
||||
const testHelper = reactFlowTester(container);
|
||||
|
||||
const expectedEdges = [
|
||||
"Edge from start to alt1",
|
||||
"Edge from alt1 to end",
|
||||
"Edge from start to alt2",
|
||||
"Edge from alt2 to end",
|
||||
];
|
||||
testHelper.expectEdgeLabels(expectedEdges);
|
||||
|
||||
const expectedNodes = new Set(["start", "alt1", "alt2", "end"]);
|
||||
testHelper.expectNodeIds(Array.from(expectedNodes));
|
||||
});
|
||||
|
||||
it("should render a flow with a subflow", () => {
|
||||
const executionList = new ExecutionList([
|
||||
{
|
||||
id: "requiredElement",
|
||||
displayName: "Required Element",
|
||||
requirement: "REQUIRED",
|
||||
level: 0,
|
||||
},
|
||||
{
|
||||
id: "subflow",
|
||||
displayName: "Subflow",
|
||||
requirement: "REQUIRED",
|
||||
level: 0,
|
||||
},
|
||||
{
|
||||
id: "subElement",
|
||||
displayName: "Sub Element",
|
||||
requirement: "REQUIRED",
|
||||
level: 1,
|
||||
},
|
||||
]);
|
||||
|
||||
const { container } = render(<FlowDiagram executionList={executionList} />);
|
||||
|
||||
const testHelper = reactFlowTester(container);
|
||||
const expectedEdges = [
|
||||
"Edge from start to requiredElement",
|
||||
"Edge from requiredElement to subflow",
|
||||
"Edge from subflow to subElement",
|
||||
"Edge from subElement to flow-end-subflow",
|
||||
"Edge from flow-end-subflow to end",
|
||||
];
|
||||
testHelper.expectEdgeLabels(expectedEdges);
|
||||
|
||||
const expectedNodes = [
|
||||
"start",
|
||||
"requiredElement",
|
||||
"subflow",
|
||||
"subElement",
|
||||
"flow-end-subflow",
|
||||
"end",
|
||||
];
|
||||
testHelper.expectNodeIds(expectedNodes);
|
||||
});
|
||||
|
||||
it("should render a flow with a subflow with alternative steps", () => {
|
||||
const executionList = new ExecutionList([
|
||||
{
|
||||
id: "requiredElement",
|
||||
displayName: "Required Element",
|
||||
requirement: "REQUIRED",
|
||||
level: 0,
|
||||
},
|
||||
{
|
||||
id: "subflow",
|
||||
displayName: "Subflow",
|
||||
requirement: "REQUIRED",
|
||||
level: 0,
|
||||
},
|
||||
{
|
||||
id: "subElement1",
|
||||
displayName: "Sub Element",
|
||||
requirement: "ALTERNATIVE",
|
||||
level: 1,
|
||||
},
|
||||
{
|
||||
id: "subElement2",
|
||||
displayName: "Sub Element",
|
||||
requirement: "ALTERNATIVE",
|
||||
level: 1,
|
||||
},
|
||||
]);
|
||||
|
||||
const { container } = render(<FlowDiagram executionList={executionList} />);
|
||||
|
||||
const testHelper = reactFlowTester(container);
|
||||
const expectedEdges = [
|
||||
"Edge from start to requiredElement",
|
||||
"Edge from requiredElement to subflow",
|
||||
"Edge from subflow to subElement1",
|
||||
"Edge from subElement1 to flow-end-subflow",
|
||||
"Edge from subflow to subElement2",
|
||||
"Edge from subElement2 to flow-end-subflow",
|
||||
"Edge from flow-end-subflow to end",
|
||||
];
|
||||
testHelper.expectEdgeLabels(expectedEdges);
|
||||
|
||||
const expectedNodes = [
|
||||
"start",
|
||||
"requiredElement",
|
||||
"subflow",
|
||||
"subElement1",
|
||||
"subElement2",
|
||||
"flow-end-subflow",
|
||||
"end",
|
||||
];
|
||||
testHelper.expectNodeIds(expectedNodes);
|
||||
});
|
||||
|
||||
it("should render a flow with a subflow with alternative steps and combine to a required step", () => {
|
||||
const executionList = new ExecutionList([
|
||||
{
|
||||
id: "requiredElement",
|
||||
displayName: "Required Element",
|
||||
requirement: "REQUIRED",
|
||||
level: 0,
|
||||
},
|
||||
{
|
||||
id: "subflow",
|
||||
displayName: "Subflow",
|
||||
requirement: "REQUIRED",
|
||||
level: 0,
|
||||
},
|
||||
{
|
||||
id: "subElement1",
|
||||
displayName: "Sub Element",
|
||||
requirement: "ALTERNATIVE",
|
||||
level: 1,
|
||||
},
|
||||
{
|
||||
id: "subElement2",
|
||||
displayName: "Sub Element",
|
||||
requirement: "ALTERNATIVE",
|
||||
level: 1,
|
||||
},
|
||||
{
|
||||
id: "finalStep",
|
||||
displayName: "Final Step",
|
||||
requirement: "REQUIRED",
|
||||
level: 0,
|
||||
},
|
||||
]);
|
||||
|
||||
const { container } = render(<FlowDiagram executionList={executionList} />);
|
||||
|
||||
const testHelper = reactFlowTester(container);
|
||||
const expectedEdges = [
|
||||
"Edge from start to requiredElement",
|
||||
"Edge from requiredElement to subflow",
|
||||
"Edge from subflow to subElement1",
|
||||
"Edge from subElement1 to flow-end-subflow",
|
||||
"Edge from subflow to subElement2",
|
||||
"Edge from subElement2 to flow-end-subflow",
|
||||
"Edge from flow-end-subflow to finalStep",
|
||||
"Edge from finalStep to end",
|
||||
];
|
||||
testHelper.expectEdgeLabels(expectedEdges);
|
||||
|
||||
const expectedNodes = [
|
||||
"start",
|
||||
"requiredElement",
|
||||
"subflow",
|
||||
"subElement1",
|
||||
"subElement2",
|
||||
"flow-end-subflow",
|
||||
"finalStep",
|
||||
"end",
|
||||
];
|
||||
testHelper.expectNodeIds(expectedNodes);
|
||||
});
|
||||
|
||||
it("should render a flow with a conditional subflow followed by a required step", () => {
|
||||
const executionList = new ExecutionList([
|
||||
{
|
||||
id: "chooseUser",
|
||||
displayName: "Required Element",
|
||||
requirement: "REQUIRED",
|
||||
level: 0,
|
||||
},
|
||||
{
|
||||
id: "sendReset",
|
||||
displayName: "Send Reset",
|
||||
requirement: "REQUIRED",
|
||||
level: 0,
|
||||
},
|
||||
{
|
||||
id: "conditionalOTP",
|
||||
displayName: "Conditional OTP",
|
||||
requirement: "CONDITIONAL",
|
||||
level: 0,
|
||||
},
|
||||
{
|
||||
id: "conditionOtpConfigured",
|
||||
displayName: "Condition - User Configured",
|
||||
requirement: "REQUIRED",
|
||||
level: 1,
|
||||
},
|
||||
{
|
||||
id: "otpForm",
|
||||
displayName: "OTP Form",
|
||||
requirement: "REQUIRED",
|
||||
level: 1,
|
||||
},
|
||||
{
|
||||
id: "resetPassword",
|
||||
displayName: "Reset Password",
|
||||
requirement: "REQUIRED",
|
||||
level: 0,
|
||||
},
|
||||
]);
|
||||
|
||||
const { container } = render(<FlowDiagram executionList={executionList} />);
|
||||
|
||||
const testHelper = reactFlowTester(container);
|
||||
const expectedEdges = [
|
||||
"Edge from start to chooseUser",
|
||||
"Edge from chooseUser to sendReset",
|
||||
"Edge from sendReset to conditionOtpConfigured",
|
||||
"Edge from conditionOtpConfigured to otpForm",
|
||||
"Edge from conditionOtpConfigured to resetPassword",
|
||||
"Edge from otpForm to resetPassword",
|
||||
"Edge from resetPassword to end",
|
||||
];
|
||||
testHelper.expectEdgeLabels(expectedEdges);
|
||||
});
|
||||
|
||||
it("should render a complex flow with serial conditionals", () => {
|
||||
// flow inspired by ![conditional flow PR](https://github.com/keycloak/keycloak/pull/28481)
|
||||
const executionList = new ExecutionList([
|
||||
{
|
||||
id: "exampleForms",
|
||||
displayName: "Example Forms",
|
||||
requirement: "ALTERNATIVE",
|
||||
level: 0,
|
||||
},
|
||||
{
|
||||
id: "usernamePasswordForm",
|
||||
displayName: "Username Password Form",
|
||||
requirement: "REQUIRED",
|
||||
level: 1,
|
||||
},
|
||||
{
|
||||
id: "conditionalOTP",
|
||||
displayName: "Conditional OTP",
|
||||
requirement: "CONDITIONAL",
|
||||
level: 1,
|
||||
},
|
||||
{
|
||||
id: "conditionUserConfigured",
|
||||
displayName: "Condition - User Configured",
|
||||
requirement: "REQUIRED",
|
||||
level: 2,
|
||||
},
|
||||
{
|
||||
id: "conditionUserAttribute",
|
||||
displayName: "Condition - User Attribute",
|
||||
requirement: "REQUIRED",
|
||||
level: 2,
|
||||
},
|
||||
{
|
||||
id: "otpForm",
|
||||
displayName: "OTP Form",
|
||||
requirement: "REQUIRED",
|
||||
level: 2,
|
||||
},
|
||||
{
|
||||
id: "confirmLink",
|
||||
displayName: "Confirm Link",
|
||||
requirement: "REQUIRED",
|
||||
level: 2,
|
||||
},
|
||||
{
|
||||
id: "conditionalReviewProfile",
|
||||
displayName: "Conditional Review Profile",
|
||||
requirement: "CONDITIONAL",
|
||||
level: 0,
|
||||
},
|
||||
{
|
||||
id: "conditionLoa",
|
||||
displayName: "Condition - Loa",
|
||||
requirement: "REQUIRED",
|
||||
level: 1,
|
||||
},
|
||||
{
|
||||
id: "reviewProfile",
|
||||
displayName: "Review Profile",
|
||||
requirement: "REQUIRED",
|
||||
level: 1,
|
||||
},
|
||||
]);
|
||||
|
||||
const { container } = render(<FlowDiagram executionList={executionList} />);
|
||||
|
||||
const testHelper = reactFlowTester(container);
|
||||
const expectedEdges = [
|
||||
"Edge from start to exampleForms",
|
||||
"Edge from exampleForms to usernamePasswordForm",
|
||||
"Edge from usernamePasswordForm to conditionUserConfigured",
|
||||
"Edge from conditionUserConfigured to conditionUserAttribute",
|
||||
"Edge from conditionUserConfigured to flow-end-exampleForms",
|
||||
"Edge from conditionUserAttribute to otpForm",
|
||||
"Edge from conditionUserAttribute to flow-end-exampleForms",
|
||||
"Edge from otpForm to confirmLink",
|
||||
"Edge from confirmLink to flow-end-exampleForms",
|
||||
"Edge from flow-end-exampleForms to end",
|
||||
"Edge from start to conditionLoa",
|
||||
"Edge from conditionLoa to reviewProfile",
|
||||
"Edge from conditionLoa to end",
|
||||
"Edge from reviewProfile to end",
|
||||
];
|
||||
testHelper.expectEdgeLabels(expectedEdges);
|
||||
|
||||
const expectedNodes = [
|
||||
"start",
|
||||
"exampleForms",
|
||||
"usernamePasswordForm",
|
||||
"conditionUserConfigured",
|
||||
"conditionUserAttribute",
|
||||
"otpForm",
|
||||
"confirmLink",
|
||||
"flow-end-exampleForms",
|
||||
"conditionLoa",
|
||||
"reviewProfile",
|
||||
"end",
|
||||
];
|
||||
testHelper.expectNodeIds(expectedNodes);
|
||||
});
|
||||
});
|
|
@ -1,4 +1,3 @@
|
|||
import type AuthenticationExecutionInfoRepresentation from "@keycloak/keycloak-admin-client/lib/defs/authenticationExecutionInfoRepresentation";
|
||||
import { MouseEvent as ReactMouseEvent, useMemo, useState } from "react";
|
||||
import {
|
||||
Background,
|
||||
|
@ -8,7 +7,6 @@ import {
|
|||
MiniMap,
|
||||
Node,
|
||||
NodeMouseHandler,
|
||||
NodeTypes,
|
||||
Position,
|
||||
ReactFlow,
|
||||
ReactFlowInstance,
|
||||
|
@ -31,10 +29,24 @@ type FlowDiagramProps = {
|
|||
executionList: ExecutionList;
|
||||
};
|
||||
|
||||
type ConditionLabel = "true" | "false";
|
||||
|
||||
const nodeTypes = {
|
||||
conditional: ConditionalNode,
|
||||
startSubFlow: StartSubFlowNode,
|
||||
endSubFlow: EndSubFlowNode,
|
||||
} as const;
|
||||
|
||||
type NodeType = keyof typeof nodeTypes;
|
||||
|
||||
const isBypassable = (execution: ExpandableExecution) =>
|
||||
execution.requirement === "ALTERNATIVE" ||
|
||||
execution.requirement === "DISABLED";
|
||||
|
||||
const createEdge = (
|
||||
fromNode: string,
|
||||
toNode: string,
|
||||
label?: string,
|
||||
label?: ConditionLabel,
|
||||
): Edge => ({
|
||||
id: `edge-${fromNode}-to-${toNode}`,
|
||||
type: "buttonEdge",
|
||||
|
@ -52,11 +64,10 @@ const createEdge = (
|
|||
},
|
||||
});
|
||||
|
||||
const createNode = (ex: ExpandableExecution): Node => {
|
||||
let nodeType: string | undefined = undefined;
|
||||
if (providerConditionFilter(ex)) {
|
||||
nodeType = "conditional";
|
||||
}
|
||||
const createNode = (
|
||||
ex: { id?: string; displayName?: string },
|
||||
nodeType?: NodeType,
|
||||
): Node => {
|
||||
return {
|
||||
id: ex.id!,
|
||||
type: nodeType,
|
||||
|
@ -67,211 +78,139 @@ const createNode = (ex: ExpandableExecution): Node => {
|
|||
};
|
||||
};
|
||||
|
||||
const renderParallelNodes = (execution: ExpandableExecution): Node[] => [
|
||||
createNode(execution),
|
||||
];
|
||||
|
||||
const renderParallelEdges = (
|
||||
start: AuthenticationExecutionInfoRepresentation,
|
||||
execution: ExpandableExecution,
|
||||
end: AuthenticationExecutionInfoRepresentation,
|
||||
): Edge[] => {
|
||||
const falseConditionLabel = providerConditionFilter(execution) ? "false" : "";
|
||||
return [
|
||||
createEdge(start.id!, execution.id!),
|
||||
createEdge(execution.id!, end.id!, falseConditionLabel),
|
||||
];
|
||||
};
|
||||
|
||||
const renderSequentialNodes = (execution: ExpandableExecution): Node[] => [
|
||||
createNode(execution),
|
||||
];
|
||||
|
||||
const renderSequentialEdges = (
|
||||
start: AuthenticationExecutionInfoRepresentation,
|
||||
execution: ExpandableExecution,
|
||||
end: AuthenticationExecutionInfoRepresentation,
|
||||
prefExecution: ExpandableExecution,
|
||||
isFirst: boolean,
|
||||
isLast: boolean,
|
||||
): Edge[] => {
|
||||
const edges: Edge[] = [];
|
||||
|
||||
if (isFirst) {
|
||||
edges.push(createEdge(start.id!, execution.id!));
|
||||
} else {
|
||||
const trueConditionLabel = providerConditionFilter(prefExecution)
|
||||
? "true"
|
||||
: "";
|
||||
edges.push(
|
||||
createEdge(prefExecution.id!, execution.id!, trueConditionLabel),
|
||||
);
|
||||
}
|
||||
|
||||
if (isLast || providerConditionFilter(execution)) {
|
||||
const falseConditionLabel = providerConditionFilter(execution)
|
||||
? "false"
|
||||
: "";
|
||||
edges.push(createEdge(execution.id!, end.id!, falseConditionLabel));
|
||||
}
|
||||
|
||||
return edges;
|
||||
};
|
||||
|
||||
const renderConditionalSubFlowNodes = (
|
||||
execution: ExpandableExecution,
|
||||
): Node[] => renderFlowNodes(execution.executionList || []);
|
||||
|
||||
const renderConditionalSubFlowEdges = (
|
||||
execution: ExpandableExecution,
|
||||
start: AuthenticationExecutionInfoRepresentation,
|
||||
end: AuthenticationExecutionInfoRepresentation,
|
||||
prefExecution?: ExpandableExecution,
|
||||
): Edge[] => {
|
||||
const conditionalSubFlowStart =
|
||||
prefExecution && prefExecution.requirement !== "ALTERNATIVE"
|
||||
? prefExecution
|
||||
: start!;
|
||||
|
||||
return renderFlowEdges(
|
||||
conditionalSubFlowStart,
|
||||
execution.executionList || [],
|
||||
end,
|
||||
);
|
||||
};
|
||||
|
||||
const renderSubFlowNodes = (execution: ExpandableExecution): Node[] => {
|
||||
const nodes: Node[] = [];
|
||||
|
||||
nodes.push({
|
||||
id: execution.id!,
|
||||
type: "startSubFlow",
|
||||
sourcePosition: Position.Right,
|
||||
targetPosition: Position.Left,
|
||||
data: { label: execution.displayName! },
|
||||
position: { x: 0, y: 0 },
|
||||
});
|
||||
if (execution.requirement !== "CONDITIONAL") {
|
||||
nodes.push(createNode(execution, "startSubFlow"));
|
||||
|
||||
const endSubFlowId = `flow-end-${execution.id}`;
|
||||
|
||||
nodes.push({
|
||||
id: endSubFlowId,
|
||||
type: "endSubFlow",
|
||||
sourcePosition: Position.Right,
|
||||
targetPosition: Position.Left,
|
||||
data: { label: execution.displayName! },
|
||||
position: { x: 0, y: 0 },
|
||||
});
|
||||
const endSubFlowId = `flow-end-${execution.id}`;
|
||||
nodes.push(
|
||||
createNode(
|
||||
{
|
||||
id: endSubFlowId,
|
||||
displayName: execution.displayName!,
|
||||
},
|
||||
"endSubFlow",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return nodes.concat(renderFlowNodes(execution.executionList || []));
|
||||
};
|
||||
|
||||
const renderSubFlowEdges = (
|
||||
execution: ExpandableExecution,
|
||||
start: AuthenticationExecutionInfoRepresentation,
|
||||
end: AuthenticationExecutionInfoRepresentation,
|
||||
prefExecution?: ExpandableExecution,
|
||||
): Edge[] => {
|
||||
const edges: Edge[] = [];
|
||||
|
||||
const endSubFlowId = `flow-end-${execution.id}`;
|
||||
|
||||
edges.push(
|
||||
createEdge(
|
||||
prefExecution && prefExecution.requirement !== "ALTERNATIVE"
|
||||
? prefExecution.id!
|
||||
: start.id!,
|
||||
execution.id!,
|
||||
),
|
||||
);
|
||||
edges.push(createEdge(endSubFlowId, end.id!));
|
||||
|
||||
return edges.concat(
|
||||
renderFlowEdges(execution, execution.executionList || [], {
|
||||
...execution,
|
||||
id: endSubFlowId,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const renderFlowNodes = (executionList: ExpandableExecution[]): Node[] => {
|
||||
let elements: Node[] = [];
|
||||
|
||||
for (let index = 0; index < executionList.length; index++) {
|
||||
const execution = executionList[index];
|
||||
if (execution.executionList) {
|
||||
if (execution.requirement === "CONDITIONAL") {
|
||||
elements = elements.concat(renderConditionalSubFlowNodes(execution));
|
||||
} else {
|
||||
elements = elements.concat(renderSubFlowNodes(execution));
|
||||
}
|
||||
elements = elements.concat(renderSubFlowNodes(execution));
|
||||
} else {
|
||||
if (
|
||||
execution.requirement === "ALTERNATIVE" ||
|
||||
execution.requirement === "DISABLED"
|
||||
) {
|
||||
elements = elements.concat(renderParallelNodes(execution));
|
||||
} else {
|
||||
elements = elements.concat(renderSequentialNodes(execution));
|
||||
}
|
||||
elements.push(
|
||||
createNode(
|
||||
execution,
|
||||
providerConditionFilter(execution) ? "conditional" : undefined,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return elements;
|
||||
};
|
||||
|
||||
const renderSubFlowEdges = (
|
||||
execution: ExpandableExecution,
|
||||
flowEndId: string,
|
||||
): { startId: string; edges: Edge[]; endId: string } => {
|
||||
if (!execution.executionList)
|
||||
throw new Error("Execution list is required for subflow");
|
||||
|
||||
if (execution.requirement === "CONDITIONAL") {
|
||||
const startId = execution.executionList![0].id!;
|
||||
|
||||
return {
|
||||
startId: startId,
|
||||
edges: renderFlowEdges(startId, execution.executionList!, flowEndId),
|
||||
endId: execution.executionList![execution.executionList!.length - 1].id!,
|
||||
};
|
||||
}
|
||||
const elements: Edge[] = [];
|
||||
const subFlowEndId = `flow-end-${execution.id}`;
|
||||
|
||||
return {
|
||||
startId: execution.id!,
|
||||
edges: elements.concat(
|
||||
renderFlowEdges(execution.id!, execution.executionList!, subFlowEndId),
|
||||
),
|
||||
endId: subFlowEndId,
|
||||
};
|
||||
};
|
||||
|
||||
const renderFlowEdges = (
|
||||
start: AuthenticationExecutionInfoRepresentation,
|
||||
startId: string,
|
||||
executionList: ExpandableExecution[],
|
||||
end: AuthenticationExecutionInfoRepresentation,
|
||||
endId: string,
|
||||
): Edge[] => {
|
||||
let elements: Edge[] = [];
|
||||
let prevExecutionId = startId;
|
||||
let isLastExecutionBypassable = false;
|
||||
const conditionals = [];
|
||||
|
||||
for (let index = 0; index < executionList.length; index++) {
|
||||
const execution = executionList[index];
|
||||
if (execution.executionList) {
|
||||
if (execution.requirement === "CONDITIONAL") {
|
||||
elements = elements.concat(
|
||||
renderConditionalSubFlowEdges(
|
||||
execution,
|
||||
start,
|
||||
end,
|
||||
executionList[index - 1],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
elements = elements.concat(
|
||||
renderSubFlowEdges(execution, start, end, executionList[index - 1]),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (
|
||||
execution.requirement === "ALTERNATIVE" ||
|
||||
execution.requirement === "DISABLED"
|
||||
) {
|
||||
elements = elements.concat(renderParallelEdges(start, execution, end));
|
||||
} else {
|
||||
elements = elements.concat(
|
||||
renderSequentialEdges(
|
||||
start,
|
||||
execution,
|
||||
end,
|
||||
executionList[index - 1],
|
||||
index === 0,
|
||||
index === executionList.length - 1,
|
||||
),
|
||||
);
|
||||
}
|
||||
let executionId = execution.id!;
|
||||
const isPrevConditional =
|
||||
conditionals[conditionals.length - 1] === prevExecutionId;
|
||||
const connectToPrevious = (id: string) =>
|
||||
elements.push(
|
||||
createEdge(prevExecutionId, id, isPrevConditional ? "true" : undefined),
|
||||
);
|
||||
|
||||
if (providerConditionFilter(execution)) {
|
||||
conditionals.push(executionId);
|
||||
}
|
||||
if (startId === executionId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (execution.executionList) {
|
||||
const nextRequired =
|
||||
executionList.slice(index + 1).find((e) => !isBypassable(e))?.id ??
|
||||
endId;
|
||||
const {
|
||||
startId: subFlowStartId,
|
||||
edges,
|
||||
endId: subflowEndId,
|
||||
} = renderSubFlowEdges(execution, nextRequired);
|
||||
|
||||
connectToPrevious(subFlowStartId);
|
||||
elements = elements.concat(edges);
|
||||
executionId = subflowEndId;
|
||||
} else {
|
||||
connectToPrevious(executionId);
|
||||
}
|
||||
|
||||
const isExecutionBypassable = isBypassable(execution);
|
||||
|
||||
if (isExecutionBypassable) {
|
||||
elements.push(createEdge(executionId, endId));
|
||||
} else {
|
||||
prevExecutionId = executionId;
|
||||
}
|
||||
|
||||
isLastExecutionBypassable = isExecutionBypassable;
|
||||
}
|
||||
|
||||
return elements;
|
||||
};
|
||||
// subflows with conditionals automatically connect to the end, so don't do it twice
|
||||
if (!isLastExecutionBypassable && conditionals.length === 0) {
|
||||
elements.push(createEdge(prevExecutionId, endId));
|
||||
}
|
||||
elements = elements.concat(
|
||||
conditionals.map((id) => createEdge(id, endId, "false")),
|
||||
);
|
||||
|
||||
const nodeTypes: NodeTypes = {
|
||||
conditional: ConditionalNode,
|
||||
startSubFlow: StartSubFlowNode,
|
||||
endSubFlow: EndSubFlowNode,
|
||||
return elements;
|
||||
};
|
||||
|
||||
const edgeTypes: ButtonEdges = {
|
||||
|
@ -301,11 +240,7 @@ function renderNodes(expandableList: ExpandableExecution[]) {
|
|||
}
|
||||
|
||||
function renderEdges(expandableList: ExpandableExecution[]): Edge[] {
|
||||
return getLayoutedEdges(
|
||||
renderFlowEdges({ id: "start" }, expandableList, {
|
||||
id: "end",
|
||||
}),
|
||||
);
|
||||
return getLayoutedEdges(renderFlowEdges("start", expandableList, "end"));
|
||||
}
|
||||
|
||||
export const FlowDiagram = ({
|
||||
|
|
|
@ -20,5 +20,11 @@ export default defineConfig({
|
|||
plugins: [react(), checker({ typescript: true })],
|
||||
test: {
|
||||
watch: false,
|
||||
environment: "jsdom",
|
||||
server: {
|
||||
deps: {
|
||||
inline: [/@patternfly\/.*/],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue