diff --git a/web/src/components/createupdate/CreateUpdateWorkflow.js b/web/src/components/createupdate/CreateUpdateWorkflow.js
index 4bb2620b..12dc866a 100644
--- a/web/src/components/createupdate/CreateUpdateWorkflow.js
+++ b/web/src/components/createupdate/CreateUpdateWorkflow.js
@@ -39,7 +39,7 @@ export default function CreateUpdateWorkflow(props) {
const [openCreatePipeline, setOpenCreatePipeline] = useState(false);
const [asset, setAsset] = useState(null);
const [pipelines, setPipelines] = useState([]);
- const [workflowPipelines, setWorkflowPipelines] = useState([]);
+ const [workflowPipelines, setWorkflowPipelines] = useState([null]);
const [loadedWorkflowPipelines, setLoadedWorkflowPipelines] = useState([]);
const [activeTab, setActiveTab] = useState("asset");
const [workflowIdNew, setWorkflowIDNew] = useState(workflowId);
@@ -76,6 +76,7 @@ export default function CreateUpdateWorkflow(props) {
};
});
setLoadedWorkflowPipelines(loadedPipelines);
+ setWorkflowPipelines(loadedPipelines);
setLoaded(true);
}
};
diff --git a/web/src/components/interactive/WorkflowEditor.js b/web/src/components/interactive/WorkflowEditor.js
deleted file mode 100644
index 6065c081..00000000
--- a/web/src/components/interactive/WorkflowEditor.js
+++ /dev/null
@@ -1,258 +0,0 @@
-/* eslint-disable react-hooks/exhaustive-deps */
-/*
- * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import { useState, useCallback, useRef, useEffect, useContext } from "react";
-
-import ReactFlow, {
- removeElements,
- addEdge,
- MiniMap,
- Controls,
- Background,
-} from "react-flow-renderer";
-import { Button, Icon } from "@cloudscape-design/components";
-import { useParams } from "react-router";
-import AssetSelector from "../selectors/AssetSelector";
-import WorkflowPipelineSelector from "../selectors/WorkflowPipelineSelector";
-import { WorkflowContext } from "../../context/WorkflowContex";
-
-const AssetID = (props) => {
- const { asset } = useContext(WorkflowContext);
-
- return <>{asset ? asset.value : ""}>;
-};
-
-const PipelineDetail = (props) => {
- const { index, prop } = props;
- const { pipelines, workflowPipelines } = useContext(WorkflowContext);
- const [pipelineId, setPipelneId] = useState(null);
- useEffect(() => {
- if (workflowPipelines[index]) {
- setPipelneId(workflowPipelines[index].value);
- }
- }, [workflowPipelines]);
- return <>{pipelineId && pipelines[pipelineId] ? pipelines[pipelineId][prop] : "?"}>;
-};
-
-let cacheInstance;
-
-const onLoad = (reactFlowInstance) => {
- cacheInstance = reactFlowInstance;
- reactFlowInstance.fitView();
-};
-
-const WorkflowEditor = (props) => {
- let { databaseId } = useParams();
- const { loaded, loadedWorkflowPipelines } = props;
- const { workflowPipelines, setWorkflowPipelines, setActiveTab } = useContext(WorkflowContext);
- const [firstload, setFirstLoad] = useState(false);
-
- const initialElements = [
- {
- id: "asset1",
- type: "input",
- data: {
- label: (
- <>
-
- >
- ),
- },
- sourcePosition: "bottom",
- position: { x: 0, y: 0 },
- },
- ];
-
- const [elements, setElements] = useState(initialElements);
- const yPos = useRef(0);
- const xPos = useRef(0);
- const columnCounter = useRef(0);
- const onElementsRemove = (elementsToRemove) =>
- setElements((els) => removeElements(elementsToRemove, els));
- const onConnect = (params) => setElements((els) => addEdge(params, els));
-
- const handleAddPipeline = () => {
- setActiveTab("pipelines");
- const newPipelines = workflowPipelines.slice();
- newPipelines.push(null);
- setWorkflowPipelines(newPipelines);
- updateElementsList();
- };
- const updateElementsList = useCallback(async () => {
- if (yPos.current === 0) yPos.current = 75;
- else if (columnCounter.current === 4) {
- xPos.current = 0;
- columnCounter.current = 0;
- yPos.current += 230;
- } else {
- xPos.current += 350;
- }
- columnCounter.current += 1;
- setElements((els) => {
- const lastElement = elements[elements.length - 1];
- const lastId = Number((lastElement.target || lastElement.id).replace("asset", ""));
- const currentId = lastId + 1;
- const pipelineIndex = currentId - 2;
- return [
- ...els,
- {
- id: currentId + "",
- position: { x: xPos.current, y: yPos.current },
- data: {
- label: (
-
- ),
- },
- sourcePosition: "bottom",
- targetPosition: columnCounter.current === 1 ? "top" : "left",
- },
- {
- id: `asset${lastId}-${currentId}`,
- source: `asset${lastId}`,
- target: currentId + "",
- type: "smoothstep",
- },
- {
- id: `asset${currentId}`,
- position: { x: xPos.current, y: yPos.current + 65 },
- data: {
- label: (
- <>
- -
-
-
- >
- ),
- },
- sourcePosition: columnCounter.current === 4 ? "bottom" : "right",
- targetPosition: "top",
- },
- {
- id: `${currentId}-asset${currentId}`,
- source: currentId + "",
- target: `asset${currentId}`,
- type: "smoothstep",
- },
- ];
- });
- });
-
- useEffect(() => {
- if (loaded && workflowPipelines.length === 0) {
- // updateElementsList();
- }
- });
-
- const handleRemovePipeline = useCallback(() => {
- setActiveTab("pipelines");
- const newPipelines = workflowPipelines.slice();
- newPipelines.pop();
- setWorkflowPipelines(newPipelines);
-
- if (yPos.current === 0 && columnCounter.current === 0) {
- return;
- }
-
- if (yPos.current === 75 && columnCounter.current === 1) {
- yPos.current = 0;
- }
-
- if (yPos.current > 75 && columnCounter.current === 1) {
- yPos.current -= 130;
- }
-
- if (columnCounter.current > 1 && columnCounter.current <= 4) {
- xPos.current -= 250;
- }
-
- if (columnCounter.current === 1 && yPos.current !== 0) {
- columnCounter.current = 4;
- xPos.current = 750;
- } else {
- columnCounter.current -= 1;
- }
-
- const newElements = elements.slice(0, -4);
- setElements(newElements);
- });
-
- // when elements changes, center and zoom the view so that the graph fills the center of the screen
- useEffect(() => {
- if (cacheInstance && cacheInstance.fitView) cacheInstance.fitView();
- setTimeout(() => cacheInstance.fitView(), 100);
- }, [elements]);
-
- useEffect(() => {
- if (loaded && loadedWorkflowPipelines.length > 0) {
- setFirstLoad(true);
- }
- }, [loaded]);
-
- useEffect(() => {
- if (firstload) {
- if (loadedWorkflowPipelines.length > 0) {
- const shiftedPipeline = loadedWorkflowPipelines.shift();
- updateElementsList();
- const newPipelines = workflowPipelines.slice();
- newPipelines.push(shiftedPipeline);
- setWorkflowPipelines(newPipelines);
- } else {
- setFirstLoad(false);
- }
- }
- }, [firstload, elements]);
-
- return (
- <>
-
-
- {/*@todo implement undo redo*/}
- {/*
*/}
- {/*
*/}
-
-
-
-
- {
- if (n.style?.background) return n.style.background;
- if (n.type === "input") return "#0041d0";
- if (n.type === "output") return "#ff0072";
- if (n.type === "default") return "#1a192b";
-
- return "#eee";
- }}
- nodeColor={(n) => {
- if (n.style?.background) return n.style.background;
-
- return "#fff";
- }}
- nodeBorderRadius={2}
- />
-
-
-
-
- >
- );
-};
-
-export default WorkflowEditor;
diff --git a/web/src/components/interactive/WorkflowEditor.test.tsx b/web/src/components/interactive/WorkflowEditor.test.tsx
new file mode 100644
index 00000000..e4bf7081
--- /dev/null
+++ b/web/src/components/interactive/WorkflowEditor.test.tsx
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { render } from "@testing-library/react";
+import { act } from "react-dom/test-utils";
+import createWrapper from "@cloudscape-design/components/test-utils/dom";
+import WorkflowEditor, { workflowPipelineToElements } from "./WorkflowEditor";
+import { WorkflowContext } from "../../context/WorkflowContex";
+
+class ResizeObserver {
+ observe() {}
+ unobserve() {}
+ disconnect() {}
+}
+
+describe("Workflow Editor", () => {
+ window.ResizeObserver = ResizeObserver;
+
+ it("renders", async () => {
+ const setAsset = jest.fn();
+ const setPipelines = jest.fn();
+ const setWorkflowPipelines = jest.fn();
+ const reloadPipelines = jest.fn();
+ const setReloadPipelines = jest.fn();
+ const setActiveTab = jest.fn();
+
+ const asset = {};
+ const pipelines: any[] = [];
+ const workflowPipelines: any[] = [];
+
+ render(
+
+
+
+
+
+ );
+ });
+
+ it("renders with wf pipeline", async () => {
+ const setAsset = jest.fn();
+ const setPipelines = jest.fn();
+ const setWorkflowPipelines = jest.fn();
+ const reloadPipelines = jest.fn();
+ const setReloadPipelines = jest.fn();
+ const setActiveTab = jest.fn();
+
+ const asset = {};
+ const pipelines: any[] = [];
+ const workflowPipelines: any[] = [null];
+
+ render(
+
+
+
+
+
+ );
+ });
+
+ it("makes elements a function of workflow pipelines", () => {
+ const result = workflowPipelineToElements([], "databaseid");
+ expect(result.find((x) => x.id === "asset0")).toBeTruthy();
+ expect(result.length).toEqual(1);
+ });
+
+ it("makes elements a function of workflow pipelines with one pipeline", () => {
+ const result = workflowPipelineToElements([null], "databaseid");
+ expect(result.find((x) => x.id === "asset0")).toBeTruthy();
+ expect(result.find((x) => x.id === "pipeline0")).toBeTruthy();
+ expect(result.length).toEqual(5);
+ });
+
+ it("matches the snapshot for an empty workflow pipeline list", () => {
+ const result = workflowPipelineToElements([null], "databaseid");
+ expect(result).toMatchSnapshot();
+ });
+
+ it("matches the snapshot for zero length pipeline list", () => {
+ const result = workflowPipelineToElements([], "databaseid");
+ expect(result).toMatchSnapshot();
+ });
+});
diff --git a/web/src/components/interactive/WorkflowEditor.tsx b/web/src/components/interactive/WorkflowEditor.tsx
new file mode 100644
index 00000000..41d1908e
--- /dev/null
+++ b/web/src/components/interactive/WorkflowEditor.tsx
@@ -0,0 +1,195 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+/*
+ * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { useState, useEffect, useContext } from "react";
+
+import ReactFlow, { MiniMap, Controls, Background, Elements, Position } from "react-flow-renderer";
+import { Button, Icon } from "@cloudscape-design/components";
+import { useParams } from "react-router";
+import AssetSelector from "../selectors/AssetSelector";
+import WorkflowPipelineSelector from "../selectors/WorkflowPipelineSelector";
+import { WorkflowContext } from "../../context/WorkflowContex";
+
+const AssetID = (props: any) => {
+ const { asset } = useContext(WorkflowContext);
+
+ return <>{asset ? asset.value : ""}>;
+};
+
+const PipelineDetail = (props: any) => {
+ const { index, prop } = props;
+ const { pipelines, workflowPipelines } = useContext(WorkflowContext);
+ const [pipelineId, setPipelneId] = useState(null);
+ useEffect(() => {
+ if (workflowPipelines[index]) {
+ setPipelneId(workflowPipelines[index].value);
+ }
+ }, [workflowPipelines]);
+ return <>{pipelineId && pipelines[pipelineId] ? pipelines[pipelineId][prop] : "?"}>;
+};
+
+let cacheInstance: any;
+
+const onLoad = (reactFlowInstance: any) => {
+ cacheInstance = reactFlowInstance;
+ reactFlowInstance.fitView();
+};
+
+export const workflowPipelineToElements = (
+ workflowPipelines: any,
+ databaseId: string | undefined
+): Elements => {
+ let yPos = 0;
+ let xPos = 0;
+ let columnCounter = 0;
+ const yOffsetIncrement = 75;
+ return workflowPipelines.reduce(
+ (arry: Elements, elem: any, idx: number) => {
+ if (yPos === 0) yPos = 75;
+ else if (idx % 4 === 0) {
+ xPos = 0;
+ columnCounter = 0;
+ yPos += 230;
+ } else {
+ xPos += 350;
+ }
+
+ columnCounter += 1;
+
+ console.log("reducer", elem, idx);
+
+ arry.push({
+ id: `pipeline${idx}`,
+ position: { x: xPos, y: yPos },
+ data: {
+ label: (
+
+ ),
+ },
+ sourcePosition: Position.Bottom,
+ targetPosition: idx % 4 === 0 ? Position.Top : Position.Left,
+ });
+ arry.push({
+ id: `asset${idx}-pipeline${idx}`,
+ source: `asset${idx}`,
+ target: `pipeline${idx}`,
+ type: "smoothstep",
+ });
+ arry.push({
+ id: `asset${idx + 1}`,
+ position: { x: xPos, y: yPos + yOffsetIncrement },
+ data: {
+ label: (
+ <>
+ -
+
+
+ >
+ ),
+ },
+ sourcePosition: columnCounter === 4 ? Position.Bottom : Position.Right,
+ targetPosition: Position.Top,
+ });
+ arry.push({
+ id: `pipeline${idx}-asset${idx + 1}`,
+ source: `pipeline${idx}`,
+ target: `asset${idx + 1}`,
+ type: "smoothstep",
+ });
+
+ return arry;
+ },
+ [
+ {
+ id: `asset0`,
+ type: "input",
+ data: {
+ label: (
+ <>
+
+ >
+ ),
+ },
+ sourcePosition: Position.Bottom,
+ position: { x: 0, y: 0 },
+ },
+ ]
+ );
+};
+
+const WorkflowEditor = (props: any) => {
+ let { databaseId } = useParams();
+ const { workflowPipelines, setWorkflowPipelines, setActiveTab } = useContext(WorkflowContext);
+
+ const elements = workflowPipelineToElements(workflowPipelines, databaseId);
+
+ const handleAddPipeline = () => {
+ setActiveTab("pipelines");
+ const newPipelines = workflowPipelines.slice();
+ newPipelines.push(null);
+ setWorkflowPipelines(newPipelines);
+ };
+
+ // when elements changes, center and zoom the view so that the graph fills the center of the screen
+ useEffect(() => {
+ if (cacheInstance && cacheInstance.fitView) cacheInstance.fitView();
+ setTimeout(() => cacheInstance && cacheInstance.fitView(), 100);
+ }, [elements]);
+
+ return (
+ <>
+
+
+ {/*@todo implement undo redo*/}
+ {/*
*/}
+ {/*
*/}
+
+
+
+
+ {
+ if (n.style?.background) return n.style.background.toString();
+ if (n.type === "input") return "#0041d0";
+ if (n.type === "output") return "#ff0072";
+ if (n.type === "default") return "#1a192b";
+
+ return "#eee";
+ }}
+ nodeColor={(n) => {
+ if (n.style?.background) return n.style.background.toString();
+
+ return "#fff";
+ }}
+ nodeBorderRadius={2}
+ />
+
+
+
+
+ >
+ );
+};
+
+export default WorkflowEditor;
diff --git a/web/src/components/interactive/__snapshots__/WorkflowEditor.test.tsx.snap b/web/src/components/interactive/__snapshots__/WorkflowEditor.test.tsx.snap
new file mode 100644
index 00000000..1c87fee3
--- /dev/null
+++ b/web/src/components/interactive/__snapshots__/WorkflowEditor.test.tsx.snap
@@ -0,0 +1,94 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Workflow Editor matches the snapshot for an empty workflow pipeline list 1`] = `
+Array [
+ Object {
+ "data": Object {
+ "label":
+
+ ,
+ },
+ "id": "asset0",
+ "position": Object {
+ "x": 0,
+ "y": 0,
+ },
+ "sourcePosition": "bottom",
+ "type": "input",
+ },
+ Object {
+ "data": Object {
+ "label": ,
+ },
+ "id": "pipeline0",
+ "position": Object {
+ "x": 0,
+ "y": 75,
+ },
+ "sourcePosition": "bottom",
+ "targetPosition": "top",
+ },
+ Object {
+ "id": "asset0-pipeline0",
+ "source": "asset0",
+ "target": "pipeline0",
+ "type": "smoothstep",
+ },
+ Object {
+ "data": Object {
+ "label":
+
+ -
+
+
+ ,
+ },
+ "id": "asset1",
+ "position": Object {
+ "x": 0,
+ "y": 150,
+ },
+ "sourcePosition": "right",
+ "targetPosition": "top",
+ },
+ Object {
+ "id": "pipeline0-asset1",
+ "source": "pipeline0",
+ "target": "asset1",
+ "type": "smoothstep",
+ },
+]
+`;
+
+exports[`Workflow Editor matches the snapshot for zero length pipeline list 1`] = `
+Array [
+ Object {
+ "data": Object {
+ "label":
+
+ ,
+ },
+ "id": "asset0",
+ "position": Object {
+ "x": 0,
+ "y": 0,
+ },
+ "sourcePosition": "bottom",
+ "type": "input",
+ },
+]
+`;