Fix incorrect branching for some authentication flow diagrams. Fixes #28479 (#28480)

* 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:
Anil 2024-05-06 04:21:32 -07:00 committed by GitHub
parent 2ebad818f9
commit 6a38be6239
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 662 additions and 188 deletions

View file

@ -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", () => {

View file

@ -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";

View file

@ -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")));
});
}
}

View file

@ -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);
});
});

View file

@ -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 = ({

View file

@ -20,5 +20,11 @@ export default defineConfig({
plugins: [react(), checker({ typescript: true })],
test: {
watch: false,
environment: "jsdom",
server: {
deps: {
inline: [/@patternfly\/.*/],
},
},
},
});