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", + }, +] +`;