From 81e03b009254fcc36fe46dcff3f5bdc483e2f50b Mon Sep 17 00:00:00 2001 From: Rajesh Jonnalagadda Date: Mon, 29 Jul 2024 22:07:53 +0530 Subject: [PATCH 01/55] feat:nw basic react workflow builder --- keep-ui/app/alerts/alert-history.tsx | 3 +- keep-ui/app/workflows/builder/CustomEdge.tsx | 66 ++++ keep-ui/app/workflows/builder/CustomNode.tsx | 44 +++ .../workflows/builder/ReactFlowBuilder.tsx | 309 ++++++++++++++++++ keep-ui/app/workflows/builder/SubFlowNode.tsx | 42 +++ keep-ui/app/workflows/builder/ToolBox.tsx | 55 ++++ keep-ui/app/workflows/builder/builder.tsx | 69 +++- keep-ui/package-lock.json | 103 +++++- keep-ui/package.json | 5 +- 9 files changed, 672 insertions(+), 24 deletions(-) create mode 100644 keep-ui/app/workflows/builder/CustomEdge.tsx create mode 100644 keep-ui/app/workflows/builder/CustomNode.tsx create mode 100644 keep-ui/app/workflows/builder/ReactFlowBuilder.tsx create mode 100644 keep-ui/app/workflows/builder/SubFlowNode.tsx create mode 100644 keep-ui/app/workflows/builder/ToolBox.tsx diff --git a/keep-ui/app/alerts/alert-history.tsx b/keep-ui/app/alerts/alert-history.tsx index 7869df473..1c8a32828 100644 --- a/keep-ui/app/alerts/alert-history.tsx +++ b/keep-ui/app/alerts/alert-history.tsx @@ -5,10 +5,11 @@ import { useAlertTableCols } from "./alert-table-utils"; import { Button, Flex, Subtitle, Title, Divider } from "@tremor/react"; import AlertHistoryCharts from "./alert-history-charts"; import { useAlerts } from "utils/hooks/useAlerts"; -import { useRouter, useSearchParams } from "next/navigation"; +import { useRouter } from "next/navigation"; import { toDateObjectWithFallback } from "utils/helpers"; import Image from "next/image"; import Modal from "@/components/ui/Modal"; +import { useSearchParams } from "../hooks/use-search-params"; interface AlertHistoryPanelProps { alertsHistoryWithDate: (Omit & { diff --git a/keep-ui/app/workflows/builder/CustomEdge.tsx b/keep-ui/app/workflows/builder/CustomEdge.tsx new file mode 100644 index 000000000..6cd99fe74 --- /dev/null +++ b/keep-ui/app/workflows/builder/CustomEdge.tsx @@ -0,0 +1,66 @@ +import React from 'react'; +import { + BaseEdge, + EdgeLabelRenderer, + getSmoothStepPath, + useReactFlow, +} from '@xyflow/react'; + +interface CustomEdgeProps { + id: string; + sourceX: number; + sourceY: number; + targetX: number; + targetY: number; + label?: string; +} + +const CustomEdge: React.FC = ({ id, sourceX, sourceY, targetX, targetY, label }) => { + const { setEdges } = useReactFlow(); Provider + + // Calculate the path and midpoint + const [edgePath] = getSmoothStepPath({ + sourceX, + sourceY, + targetX, + targetY, + borderRadius: 10, + }); + + const midpointX = (sourceX + targetX) / 2; + const midpointY = (sourceY + targetY) / 2; + + return ( + <> + + + {!!label && ( +
+ {label} +
+ )} + +
+ + ); +}; + +export default CustomEdge; diff --git a/keep-ui/app/workflows/builder/CustomNode.tsx b/keep-ui/app/workflows/builder/CustomNode.tsx new file mode 100644 index 000000000..7bfbf33af --- /dev/null +++ b/keep-ui/app/workflows/builder/CustomNode.tsx @@ -0,0 +1,44 @@ +import React, { memo } from 'react'; +import { Handle, Position } from '@xyflow/react'; + +function IconUrlProvider(data) { + const { componentType, type } = data; + if (type === "alert" || type === "workflow") return "/keep.png"; + return `/icons/${type.replace("step-", "").replace("action-", "").replace("condition-", "")}-icon.png`; +} + +function CustomNode({ data }) { + return ( +
+ { + data.type !== "sub_flow" && ( +
+
+ {data?.type} +
+
+
{data?.name}
+
{data?.componentType}
+
+
+ ) + } + + +
+ ); +} + +export default memo(CustomNode); diff --git a/keep-ui/app/workflows/builder/ReactFlowBuilder.tsx b/keep-ui/app/workflows/builder/ReactFlowBuilder.tsx new file mode 100644 index 000000000..d8ce23bdb --- /dev/null +++ b/keep-ui/app/workflows/builder/ReactFlowBuilder.tsx @@ -0,0 +1,309 @@ +import React, { + useCallback, + useEffect, + useState, + useRef, + useLayoutEffect, +} from "react"; +import { + applyEdgeChanges, + applyNodeChanges, + addEdge, + Background, + Controls, + ReactFlow, + Node, + Edge, + Connection, +} from "@xyflow/react"; +import { parseWorkflow, generateWorkflow } from "./utils"; +import { useSearchParams } from "next/navigation"; +import { v4 as uuidv4 } from "uuid"; +import "@xyflow/react/dist/style.css"; +import CustomNode from "./CustomNode"; +import CustomEdge from "./CustomEdge"; +import { Provider } from "app/providers/providers"; +import dagre from "dagre"; + +const nodeTypes = { + custom: CustomNode, + // subflow: SubFlowNode, +}; + +const edgeTypes = { + "custom-edge": CustomEdge, +}; + +type CustomNode = Node & { + prevStepId?: string; + edge_label?: string; +}; + +const ReactFlowBuilder = ({ + workflow, + loadedAlertFile, + providers, + toolboxConfiguration, +}: { + workflow: string; + loadedAlertFile: string; + providers: Provider[]; + toolboxConfiguration?: Record; +}) => { + const [nodes, setNodes] = useState([]); + const [edges, setEdges] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [alertName, setAlertName] = useState(null); + const [alertSource, setAlertSource] = useState( + null + ); + const searchParams = useSearchParams(); + const nodeRef = useRef(null); + const [nodeDimensions, setNodeDimensions] = useState({ + width: 200, + height: 100, + }); + + const onConnect = useCallback( + (connection: Connection) => { + const edge = { ...connection, type: "custom-edge" }; + setEdges((eds) => addEdge(edge, eds)); + }, + [setEdges] + ); + + useLayoutEffect(() => { + if (nodeRef.current) { + const { width, height } = nodeRef.current.getBoundingClientRect(); + setNodeDimensions({ width: width + 20, height: height + 20 }); + } + }, [nodes.length]); + + useEffect(() => { + const alertNameParam = searchParams?.get("alertName"); + const alertSourceParam = searchParams?.get("alertSource"); + setAlertName(alertNameParam); + setAlertSource(alertSourceParam); + }, [searchParams]); + + const newEdgesFromNodes = (nodes: CustomNode[]): Edge[] => { + const edges: Edge[] = []; + + nodes.forEach((node) => { + if (node.prevStepId) { + edges.push({ + id: `e${node.prevStepId}-${node.id}`, + source: node.prevStepId, + target: node.id, + type: "custom-edge", + label: node.edge_label || "", + }); + } + }); + return edges; + }; + + useEffect(() => { + const initializeWorkflow = async () => { + setIsLoading(true); + let parsedWorkflow; + + if (workflow) { + parsedWorkflow = parseWorkflow(workflow, providers); + } else if (loadedAlertFile == null) { + const alertUuid = uuidv4(); + let triggers = {}; + if (alertName && alertSource) { + triggers = { alert: { source: alertSource, name: alertName } }; + } + parsedWorkflow = generateWorkflow(alertUuid, "", "", [], [], triggers); + } else { + parsedWorkflow = parseWorkflow(loadedAlertFile, providers); + } + + let newNodes = processWorkflow(parsedWorkflow.sequence); + let newEdges = newEdgesFromNodes(newNodes); // GENERATE EDGES BASED ON NODES + + const { nodes, edges } = getLayoutedElements(newNodes, newEdges); + + setNodes(nodes); + setEdges(edges); + setIsLoading(false); + }; + + initializeWorkflow(); + }, [ + loadedAlertFile, + workflow, + alertName, + alertSource, + providers, + nodeDimensions, + ]); + + const getLayoutedElements = (nodes: CustomNode[], edges: Edge[]) => { + const dagreGraph = new dagre.graphlib.Graph(); + dagreGraph.setDefaultEdgeLabel(() => ({})); + + dagreGraph.setGraph({ rankdir: "TB", nodesep: 100, edgesep: 100 }); + + nodes.forEach((node) => { + dagreGraph.setNode(node.id, { + width: nodeDimensions.width, + height: nodeDimensions.height, + }); + }); + + edges.forEach((edge) => { + dagreGraph.setEdge(edge.source, edge.target); + }); + + dagre.layout(dagreGraph); + + nodes.forEach((node) => { + const nodeWithPosition = dagreGraph.node(node.id); + node.targetPosition = "top"; + node.sourcePosition = "bottom"; + + node.position = { + x: nodeWithPosition.x - nodeDimensions.width / 2, + y: nodeWithPosition.y - nodeDimensions.height / 2, + }; + }); + + return { nodes, edges }; + }; + + const processWorkflow = (sequence: any, parentId?: string) => { + let newNodes: CustomNode[] = []; + + sequence.forEach((step: any, index: number) => { + const newPrevStepId = sequence?.[index - 1]?.id || ""; + const nodes = processStep( + step, + { x: index * 200, y: 50 }, + newPrevStepId, + parentId + ); + newNodes = [...newNodes, ...nodes]; + }); + + return newNodes; + }; + + const processStep = ( + step: any, + position: { x: number; y: number }, + prevStepId?: string, + parentId?: string + ) => { + const nodeId = step.id; + let newNode: CustomNode; + let newNodes: CustomNode[] = []; + + if (step.componentType === "switch") { + const subflowId = uuidv4(); + newNode = { + id: subflowId, + type: "custom", + position, + data: { + label: "Switch", + type: "sub_flow", + }, + style: { + border: "2px solid orange", + width: "100%", + height: "100%", + display: "flex", + flexDirection: "column", + justifyContent: "space-between", + }, + prevStepId: prevStepId, + parentId: parentId, + }; + if (parentId) { + newNode.extent = "parent"; + } + + newNodes.push(newNode); + + const switchNode = { + id: nodeId, + type: "custom", + position: { x: 0, y: 0 }, + data: { + label: step.name, + ...step, + }, + parentId: subflowId, + prevStepId: "", + extent: "parent", + } as CustomNode; + + newNodes.push(switchNode); + + const trueSubflowNodes: CustomNode[] = processWorkflow( + step?.branches?.true, + subflowId + ); + const falseSubflowNodes: CustomNode[] = processWorkflow( + step?.branches?.false, + subflowId + ); + + if (trueSubflowNodes.length > 0) { + trueSubflowNodes[0].edge_label = "True"; + trueSubflowNodes[0].prevStepId = nodeId; // CORRECT THE PREVIOUS STEP ID FOR THE TRUE BRANCH + } + + if (falseSubflowNodes.length > 0) { + falseSubflowNodes[0].edge_label = "False"; + falseSubflowNodes[0].prevStepId = nodeId; // CORRECT THE PREVIOUS STEP ID FOR THE FALSE BRANCH + } + + newNodes = [...newNodes, ...trueSubflowNodes, ...falseSubflowNodes]; + } else { + newNode = { + id: nodeId, + type: "custom", + position, + data: { + label: step.name, + ...step, + }, + prevStepId: prevStepId, + parentId: parentId, + extent: parentId ? "parent" : "", + } as CustomNode; + + newNodes.push(newNode); + } + + return newNodes; + }; + + return ( +
+ + setNodes((nds) => applyNodeChanges(changes, nds)) + } + onEdgesChange={(changes) => + setEdges((eds) => applyEdgeChanges(changes, eds)) + } + onConnect={onConnect} + nodeTypes={nodeTypes} + edgeTypes={edgeTypes} + fitView + > + + + +
+ ); +}; + +export default ReactFlowBuilder; diff --git a/keep-ui/app/workflows/builder/SubFlowNode.tsx b/keep-ui/app/workflows/builder/SubFlowNode.tsx new file mode 100644 index 000000000..7d28e8c98 --- /dev/null +++ b/keep-ui/app/workflows/builder/SubFlowNode.tsx @@ -0,0 +1,42 @@ +import React, { memo } from 'react'; +import { Handle, Position } from '@xyflow/react'; + +function IconUrlProvider({ componentType, type }) { + if (type === "alert" || type === "workflow") return "/keep.png"; + return `/icons/${type + ?.replace("step-", "") + .replace("action-", "") + .replace("condition-", "")}-icon.png`; +} + +function SubFlowNode({ data }) { + return ( +
+
+
+ {data?.type} +
+
+
{data?.label}
+
{data?.componentType}
+
+
+ + +
+ ); +} + +export default memo(SubFlowNode); diff --git a/keep-ui/app/workflows/builder/ToolBox.tsx b/keep-ui/app/workflows/builder/ToolBox.tsx new file mode 100644 index 000000000..cc23ab111 --- /dev/null +++ b/keep-ui/app/workflows/builder/ToolBox.tsx @@ -0,0 +1,55 @@ +// ToolBox.tsx +import React from 'react'; +import { useDrag } from 'react-dnd'; +// import { ItemType } from './constants'; // Define item types + +interface ToolBoxItemProps { + id: string; + name: string; + type: string; +} + +const ToolBoxItem: React.FC = ({ id, name, type }) => { + const [{ isDragging }, drag] = useDrag(() => ({ + type: 'input', + item: { id, type, name }, + collect: (monitor) => ({ + isDragging: monitor.isDragging(), + }), + })); + + return ( +
+ {name} +
+ ); +}; + +const ToolBox: React.FC = ({ toolboxConfiguration }: { toolboxConfiguration: { groups: Record } }) => { + const { groups } = toolboxConfiguration; + + return ( +
+ {groups.map((group) => ( +
+

{group.name}

+ {group.steps.map((step) => ( + + ))} +
+ ))} +
+ ); +}; + +export default ToolBox; diff --git a/keep-ui/app/workflows/builder/builder.tsx b/keep-ui/app/workflows/builder/builder.tsx index 7d74ba12a..836e53e42 100644 --- a/keep-ui/app/workflows/builder/builder.tsx +++ b/keep-ui/app/workflows/builder/builder.tsx @@ -15,7 +15,7 @@ import { } from "sequential-workflow-designer-react"; import { useEffect, useState } from "react"; import StepEditor, { GlobalEditor } from "./editors"; -import { Callout, Card } from "@tremor/react"; +import { Callout, Card, Switch } from "@tremor/react"; import { Provider } from "../../providers/providers"; import { parseWorkflow, @@ -38,6 +38,7 @@ import { useSearchParams } from "next/navigation"; import { v4 as uuidv4 } from "uuid"; import BuilderWorkflowTestRunModalContent from "./builder-workflow-testrun-modal"; import { WorkflowExecution, WorkflowExecutionFailure } from "./types"; +import ReactFlowBuilder from "./ReactFlowBuilder"; interface Props { loadedAlertFile: string | null; @@ -51,7 +52,7 @@ interface Props { workflowId?: string; accessToken?: string; installedProviders?: Provider[] | undefined | null; - isPreview?:boolean; + isPreview?: boolean; } function Builder({ @@ -68,6 +69,8 @@ function Builder({ installedProviders, isPreview, }: Props) { + const [useReactFlow, setUseReactFlow] = useState(false); + const [definition, setDefinition] = useState(() => wrapDefinition({ sequence: [], properties: {} } as Definition) ); @@ -87,6 +90,8 @@ function Builder({ const searchParams = useSearchParams(); + console.log("definition", definition); + const updateWorkflow = () => { const apiUrl = getApiURL(); const url = `${apiUrl}/workflows/${workflowId}`; @@ -282,8 +287,26 @@ function Builder({ setRunningWorkflowExecution(null); }; + const handleSwitchChange = (value: boolean) => { + setUseReactFlow(value); + }; + return ( <> +
+ + +
)} - } - stepEditor={} - /> + {useReactFlow && ( +
+ } + stepEditor={ + + } + /> +
+ )} + {!useReactFlow && ( + } + stepEditor={ + + } + /> + )} )} ); } -export default Builder; +export default Builder; \ No newline at end of file diff --git a/keep-ui/package-lock.json b/keep-ui/package-lock.json index b6940ee24..ce0ea6a5f 100644 --- a/keep-ui/package-lock.json +++ b/keep-ui/package-lock.json @@ -27,8 +27,9 @@ "@svgr/webpack": "^8.0.1", "@tanstack/react-table": "^8.11.0", "@tremor/react": "^3.15.1", + "@types/dagre": "^0.7.52", "@types/react-select": "^5.0.1", - "@xyflow/react": "^12.0.1", + "@xyflow/react": "^12.0.3", "add": "^2.0.6", "ajv": "^6.12.6", "ansi-regex": "^5.0.1", @@ -89,6 +90,7 @@ "d3-time": "^3.1.0", "d3-time-format": "^4.1.0", "d3-timer": "^3.0.1", + "dagre": "^0.8.5", "damerau-levenshtein": "^1.0.8", "date-fns": "^2.30.0", "debug": "^4.3.4", @@ -286,6 +288,7 @@ "react-chrono": "^2.6.1", "react-code-blocks": "^0.1.3", "react-datepicker": "^6.1.0", + "react-dnd": "^16.0.1", "react-dom": "^18.2.0", "react-grid-layout": "^1.4.4", "react-hook-form": "^7.51.5", @@ -3403,6 +3406,21 @@ "react": "^16.x || ^17.x || ^18.x" } }, + "node_modules/@react-dnd/asap": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-5.0.2.tgz", + "integrity": "sha512-WLyfoHvxhs0V9U+GTsGilGgf2QsPl6ZZ44fnv0/b8T3nQyvzxidxsg/ZltbWssbsRDlYW8UKSQMTGotuTotZ6A==" + }, + "node_modules/@react-dnd/invariant": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-4.0.2.tgz", + "integrity": "sha512-xKCTqAK/FFauOM9Ta2pswIyT3D8AQlfrYdOi/toTPEhqCuAs1v5tcJ3Y08Izh1cJ5Jchwy9SeAXmMg6zrKs2iw==" + }, + "node_modules/@react-dnd/shallowequal": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-4.0.2.tgz", + "integrity": "sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==" + }, "node_modules/@rushstack/eslint-patch": { "version": "1.10.1", "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.10.1.tgz", @@ -3991,6 +4009,11 @@ "@types/d3-selection": "*" } }, + "node_modules/@types/dagre": { + "version": "0.7.52", + "resolved": "https://registry.npmjs.org/@types/dagre/-/dagre-0.7.52.tgz", + "integrity": "sha512-XKJdy+OClLk3hketHi9Qg6gTfe1F3y+UFnHxKA2rn9Dw+oXa4Gb378Ztz9HlMgZKSxpPmn4BNVh9wgkpvrK1uw==" + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -4309,11 +4332,11 @@ "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" }, "node_modules/@xyflow/react": { - "version": "12.0.1", - "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.0.1.tgz", - "integrity": "sha512-iGh/nO7key0sVH0c8TW2qvLNU0akJ20Mi3LPUF2pymhRqerrBk0EJhPLXRThbYWy4pNWUnkhpBLB0/gr884qnw==", + "version": "12.0.3", + "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.0.3.tgz", + "integrity": "sha512-PJB9ARsyDesjS9fY3b62mm36nHx9aRA8tvUc5y0ubrMkSCvQRECkOamVDyx+u65UgUkZCgcO/KFdXPdbTWwaJQ==", "dependencies": { - "@xyflow/system": "0.0.35", + "@xyflow/system": "0.0.37", "classcat": "^5.0.3", "zustand": "^4.4.0" }, @@ -4323,9 +4346,9 @@ } }, "node_modules/@xyflow/system": { - "version": "0.0.35", - "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.35.tgz", - "integrity": "sha512-QaUkahvmMs2gY2ykxUfjs5CbkXzU5fQNtmoQQ6HmHoAr8n2D7UyLO/UEXlke2jxuCDuiwpXhrzn4DmffVJd2qA==", + "version": "0.0.37", + "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.37.tgz", + "integrity": "sha512-hSIhezhxgftPUpC+xiQVIorcRILZUOWlLjpYPTyGWRu8s4RJvM4GqvrsFmD5OnMKXLgpU7/PqqUibDVO67oWQQ==", "dependencies": { "@types/d3-drag": "^3.0.7", "@types/d3-selection": "^3.0.10", @@ -5975,6 +5998,15 @@ "node": ">=12" } }, + "node_modules/dagre": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/dagre/-/dagre-0.8.5.tgz", + "integrity": "sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==", + "dependencies": { + "graphlib": "^2.1.8", + "lodash": "^4.17.15" + } + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -6297,6 +6329,16 @@ "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" }, + "node_modules/dnd-core": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-16.0.1.tgz", + "integrity": "sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng==", + "dependencies": { + "@react-dnd/asap": "^5.0.1", + "@react-dnd/invariant": "^4.0.1", + "redux": "^4.2.0" + } + }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -8183,6 +8225,14 @@ "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==" }, + "node_modules/graphlib": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.8.tgz", + "integrity": "sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==", + "dependencies": { + "lodash": "^4.17.15" + } + }, "node_modules/grid-index": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/grid-index/-/grid-index-1.1.0.tgz", @@ -11786,6 +11836,35 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/react-dnd": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-16.0.1.tgz", + "integrity": "sha512-QeoM/i73HHu2XF9aKksIUuamHPDvRglEwdHL4jsp784BgUuWcg6mzfxT0QDdQz8Wj0qyRKx2eMg8iZtWvU4E2Q==", + "dependencies": { + "@react-dnd/invariant": "^4.0.1", + "@react-dnd/shallowequal": "^4.0.1", + "dnd-core": "^16.0.1", + "fast-deep-equal": "^3.1.3", + "hoist-non-react-statics": "^3.3.2" + }, + "peerDependencies": { + "@types/hoist-non-react-statics": ">= 3.3.1", + "@types/node": ">= 12", + "@types/react": ">= 16", + "react": ">= 16.14" + }, + "peerDependenciesMeta": { + "@types/hoist-non-react-statics": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, "node_modules/react-dom": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", @@ -12270,6 +12349,14 @@ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" }, + "node_modules/redux": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", + "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", + "dependencies": { + "@babel/runtime": "^7.9.2" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz", diff --git a/keep-ui/package.json b/keep-ui/package.json index beef7c69b..14385f5d2 100644 --- a/keep-ui/package.json +++ b/keep-ui/package.json @@ -28,8 +28,9 @@ "@svgr/webpack": "^8.0.1", "@tanstack/react-table": "^8.11.0", "@tremor/react": "^3.15.1", + "@types/dagre": "^0.7.52", "@types/react-select": "^5.0.1", - "@xyflow/react": "^12.0.1", + "@xyflow/react": "^12.0.3", "add": "^2.0.6", "ajv": "^6.12.6", "ansi-regex": "^5.0.1", @@ -90,6 +91,7 @@ "d3-time": "^3.1.0", "d3-time-format": "^4.1.0", "d3-timer": "^3.0.1", + "dagre": "^0.8.5", "damerau-levenshtein": "^1.0.8", "date-fns": "^2.30.0", "debug": "^4.3.4", @@ -287,6 +289,7 @@ "react-chrono": "^2.6.1", "react-code-blocks": "^0.1.3", "react-datepicker": "^6.1.0", + "react-dnd": "^16.0.1", "react-dom": "^18.2.0", "react-grid-layout": "^1.4.4", "react-hook-form": "^7.51.5", From 78e69b2cee51fe571290f602c576cffea6ee652b Mon Sep 17 00:00:00 2001 From: Rajesh Jonnalagadda Date: Mon, 29 Jul 2024 22:13:12 +0530 Subject: [PATCH 02/55] reverted back to oiginal --- keep-ui/app/alerts/alert-history.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/keep-ui/app/alerts/alert-history.tsx b/keep-ui/app/alerts/alert-history.tsx index 1c8a32828..7869df473 100644 --- a/keep-ui/app/alerts/alert-history.tsx +++ b/keep-ui/app/alerts/alert-history.tsx @@ -5,11 +5,10 @@ import { useAlertTableCols } from "./alert-table-utils"; import { Button, Flex, Subtitle, Title, Divider } from "@tremor/react"; import AlertHistoryCharts from "./alert-history-charts"; import { useAlerts } from "utils/hooks/useAlerts"; -import { useRouter } from "next/navigation"; +import { useRouter, useSearchParams } from "next/navigation"; import { toDateObjectWithFallback } from "utils/helpers"; import Image from "next/image"; import Modal from "@/components/ui/Modal"; -import { useSearchParams } from "../hooks/use-search-params"; interface AlertHistoryPanelProps { alertsHistoryWithDate: (Omit & { From 364a6567738a4d236a330a09fb279e6d8f1ecb4c Mon Sep 17 00:00:00 2001 From: Rajesh Jonnalagadda Date: Wed, 31 Jul 2024 18:38:38 +0530 Subject: [PATCH 03/55] refactor:use store to isolatet the operations for workflow flow builder and minor refactoring --- keep-ui/app/workflows/builder/CustomEdge.tsx | 66 ++-- keep-ui/app/workflows/builder/CustomNode.tsx | 86 +++-- keep-ui/app/workflows/builder/NodeMenu.tsx | 102 ++++++ .../workflows/builder/ReactFlowBuilder.tsx | 321 ++---------------- keep-ui/app/workflows/builder/ToolBox.tsx | 75 ++-- .../app/workflows/builder/builder-store.tsx | 202 +++++++++++ keep-ui/app/workflows/builder/builder.tsx | 23 +- .../utils/hooks/useWorkflowInitialization.ts | 254 ++++++++++++++ 8 files changed, 738 insertions(+), 391 deletions(-) create mode 100644 keep-ui/app/workflows/builder/NodeMenu.tsx create mode 100644 keep-ui/app/workflows/builder/builder-store.tsx create mode 100644 keep-ui/utils/hooks/useWorkflowInitialization.ts diff --git a/keep-ui/app/workflows/builder/CustomEdge.tsx b/keep-ui/app/workflows/builder/CustomEdge.tsx index 6cd99fe74..d8ab6f03f 100644 --- a/keep-ui/app/workflows/builder/CustomEdge.tsx +++ b/keep-ui/app/workflows/builder/CustomEdge.tsx @@ -3,23 +3,20 @@ import { BaseEdge, EdgeLabelRenderer, getSmoothStepPath, - useReactFlow, } from '@xyflow/react'; +import type { EdgeProps } from '@xyflow/react'; +import useStore from './builder-store'; -interface CustomEdgeProps { - id: string; - sourceX: number; - sourceY: number; - targetX: number; - targetY: number; +interface CustomEdgeProps extends EdgeProps { label?: string; + type?: string; } -const CustomEdge: React.FC = ({ id, sourceX, sourceY, targetX, targetY, label }) => { - const { setEdges } = useReactFlow(); Provider +const CustomEdge = ({ id, sourceX, sourceY, targetX, targetY, label }: CustomEdgeProps) => { + const { deleteEdges } = useStore(); // Calculate the path and midpoint - const [edgePath] = getSmoothStepPath({ + const [edgePath, labelX, labelY] = getSmoothStepPath({ sourceX, sourceY, targetX, @@ -30,9 +27,42 @@ const CustomEdge: React.FC = ({ id, sourceX, sourceY, targetX, const midpointX = (sourceX + targetX) / 2; const midpointY = (sourceY + targetY) / 2; + const handleDelete = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + deleteEdges(id); + }; + return ( <> - + + + + + + + {!!label && (
= ({ id, sourceX, sourceY, targetX,
)}
diff --git a/keep-ui/app/workflows/builder/CustomNode.tsx b/keep-ui/app/workflows/builder/CustomNode.tsx index 7bfbf33af..e1cf251b6 100644 --- a/keep-ui/app/workflows/builder/CustomNode.tsx +++ b/keep-ui/app/workflows/builder/CustomNode.tsx @@ -1,43 +1,63 @@ -import React, { memo } from 'react'; -import { Handle, Position } from '@xyflow/react'; +import React, { memo } from "react"; +import { Handle, Position } from "@xyflow/react"; +import NodeMenu from "./NodeMenu"; +import useStore, { FlowNode } from "./builder-store"; +import Image from "next/image"; -function IconUrlProvider(data) { + +function IconUrlProvider(data: Partial) { const { componentType, type } = data; if (type === "alert" || type === "workflow") return "/keep.png"; - return `/icons/${type.replace("step-", "").replace("action-", "").replace("condition-", "")}-icon.png`; + return `/icons/${type + .replace("step-", "") + .replace("action-", "") + .replace("condition-", "")}-icon.png`; } -function CustomNode({ data }) { +function CustomNode({ data, id}:{data: Partial}) { + console.log("entering this CustomNode", data, id); + const { getNodeById } = useStore(); + const currentNode = getNodeById(id); + return ( -
- { - data.type !== "sub_flow" && ( -
-
- {data?.type} + <> + {!!currentNode && ( +
+ {data?.type !== "sub_flow" && ( +
+
+ {data?.type} +
+
+
{data?.name}
+
{data?.componentType}
+
+
+ +
-
-
{data?.name}
-
{data?.componentType}
-
-
- ) - } - - -
+ + )} + + + +
+ )} + ); } diff --git a/keep-ui/app/workflows/builder/NodeMenu.tsx b/keep-ui/app/workflows/builder/NodeMenu.tsx new file mode 100644 index 000000000..c430e4d02 --- /dev/null +++ b/keep-ui/app/workflows/builder/NodeMenu.tsx @@ -0,0 +1,102 @@ +import { Menu, Transition } from "@headlessui/react"; +import { Fragment } from "react"; +import { CiSquareChevDown } from "react-icons/ci"; +import { TrashIcon } from "@heroicons/react/24/outline"; +import useStore, { FlowNode } from "./builder-store"; +import { HiOutlineDuplicate } from "react-icons/hi"; +import { IoMdSettings } from "react-icons/io"; + + +interface NodeMenuProps { + node: FlowNode; +} + +export default function NodeMenu({ node }: NodeMenuProps) { + const stopPropagation = (e: React.MouseEvent) => { + e.stopPropagation(); + }; + + const { deleteNodes, duplicateNode } = useStore(); + + return ( + <> + {node && ( + +
+ + + +
+ + +
+ + {({ active }) => ( + + )} + + + {({ active }) => ( + + )} + + + {({ active }) => ( + + )} + +
+
+
+
+ )} + + ); +} diff --git a/keep-ui/app/workflows/builder/ReactFlowBuilder.tsx b/keep-ui/app/workflows/builder/ReactFlowBuilder.tsx index d8ce23bdb..0457dd28e 100644 --- a/keep-ui/app/workflows/builder/ReactFlowBuilder.tsx +++ b/keep-ui/app/workflows/builder/ReactFlowBuilder.tsx @@ -1,43 +1,14 @@ -import React, { - useCallback, - useEffect, - useState, - useRef, - useLayoutEffect, -} from "react"; -import { - applyEdgeChanges, - applyNodeChanges, - addEdge, - Background, - Controls, - ReactFlow, - Node, - Edge, - Connection, -} from "@xyflow/react"; -import { parseWorkflow, generateWorkflow } from "./utils"; -import { useSearchParams } from "next/navigation"; -import { v4 as uuidv4 } from "uuid"; -import "@xyflow/react/dist/style.css"; +import React from "react"; +import { ReactFlow, Background, Controls } from "@xyflow/react"; import CustomNode from "./CustomNode"; import CustomEdge from "./CustomEdge"; +import useWorkflowInitialization from "utils/hooks/useWorkflowInitialization"; +import "@xyflow/react/dist/style.css"; +import DragAndDropSidebar from "./ToolBox"; import { Provider } from "app/providers/providers"; -import dagre from "dagre"; - -const nodeTypes = { - custom: CustomNode, - // subflow: SubFlowNode, -}; -const edgeTypes = { - "custom-edge": CustomEdge, -}; - -type CustomNode = Node & { - prevStepId?: string; - edge_label?: string; -}; +const nodeTypes = { custom: CustomNode }; +const edgeTypes = { "custom-edge": CustomEdge }; const ReactFlowBuilder = ({ workflow, @@ -50,258 +21,38 @@ const ReactFlowBuilder = ({ providers: Provider[]; toolboxConfiguration?: Record; }) => { - const [nodes, setNodes] = useState([]); - const [edges, setEdges] = useState([]); - const [isLoading, setIsLoading] = useState(true); - const [alertName, setAlertName] = useState(null); - const [alertSource, setAlertSource] = useState( - null - ); - const searchParams = useSearchParams(); - const nodeRef = useRef(null); - const [nodeDimensions, setNodeDimensions] = useState({ - width: 200, - height: 100, - }); - - const onConnect = useCallback( - (connection: Connection) => { - const edge = { ...connection, type: "custom-edge" }; - setEdges((eds) => addEdge(edge, eds)); - }, - [setEdges] - ); - - useLayoutEffect(() => { - if (nodeRef.current) { - const { width, height } = nodeRef.current.getBoundingClientRect(); - setNodeDimensions({ width: width + 20, height: height + 20 }); - } - }, [nodes.length]); - - useEffect(() => { - const alertNameParam = searchParams?.get("alertName"); - const alertSourceParam = searchParams?.get("alertSource"); - setAlertName(alertNameParam); - setAlertSource(alertSourceParam); - }, [searchParams]); - - const newEdgesFromNodes = (nodes: CustomNode[]): Edge[] => { - const edges: Edge[] = []; - - nodes.forEach((node) => { - if (node.prevStepId) { - edges.push({ - id: `e${node.prevStepId}-${node.id}`, - source: node.prevStepId, - target: node.id, - type: "custom-edge", - label: node.edge_label || "", - }); - } - }); - return edges; - }; - - useEffect(() => { - const initializeWorkflow = async () => { - setIsLoading(true); - let parsedWorkflow; - - if (workflow) { - parsedWorkflow = parseWorkflow(workflow, providers); - } else if (loadedAlertFile == null) { - const alertUuid = uuidv4(); - let triggers = {}; - if (alertName && alertSource) { - triggers = { alert: { source: alertSource, name: alertName } }; - } - parsedWorkflow = generateWorkflow(alertUuid, "", "", [], [], triggers); - } else { - parsedWorkflow = parseWorkflow(loadedAlertFile, providers); - } - - let newNodes = processWorkflow(parsedWorkflow.sequence); - let newEdges = newEdgesFromNodes(newNodes); // GENERATE EDGES BASED ON NODES - - const { nodes, edges } = getLayoutedElements(newNodes, newEdges); - - setNodes(nodes); - setEdges(edges); - setIsLoading(false); - }; - - initializeWorkflow(); - }, [ - loadedAlertFile, - workflow, - alertName, - alertSource, - providers, - nodeDimensions, - ]); - - const getLayoutedElements = (nodes: CustomNode[], edges: Edge[]) => { - const dagreGraph = new dagre.graphlib.Graph(); - dagreGraph.setDefaultEdgeLabel(() => ({})); - - dagreGraph.setGraph({ rankdir: "TB", nodesep: 100, edgesep: 100 }); - - nodes.forEach((node) => { - dagreGraph.setNode(node.id, { - width: nodeDimensions.width, - height: nodeDimensions.height, - }); - }); - - edges.forEach((edge) => { - dagreGraph.setEdge(edge.source, edge.target); - }); - - dagre.layout(dagreGraph); - - nodes.forEach((node) => { - const nodeWithPosition = dagreGraph.node(node.id); - node.targetPosition = "top"; - node.sourcePosition = "bottom"; - - node.position = { - x: nodeWithPosition.x - nodeDimensions.width / 2, - y: nodeWithPosition.y - nodeDimensions.height / 2, - }; - }); - - return { nodes, edges }; - }; - - const processWorkflow = (sequence: any, parentId?: string) => { - let newNodes: CustomNode[] = []; - - sequence.forEach((step: any, index: number) => { - const newPrevStepId = sequence?.[index - 1]?.id || ""; - const nodes = processStep( - step, - { x: index * 200, y: 50 }, - newPrevStepId, - parentId - ); - newNodes = [...newNodes, ...nodes]; - }); - - return newNodes; - }; - - const processStep = ( - step: any, - position: { x: number; y: number }, - prevStepId?: string, - parentId?: string - ) => { - const nodeId = step.id; - let newNode: CustomNode; - let newNodes: CustomNode[] = []; - - if (step.componentType === "switch") { - const subflowId = uuidv4(); - newNode = { - id: subflowId, - type: "custom", - position, - data: { - label: "Switch", - type: "sub_flow", - }, - style: { - border: "2px solid orange", - width: "100%", - height: "100%", - display: "flex", - flexDirection: "column", - justifyContent: "space-between", - }, - prevStepId: prevStepId, - parentId: parentId, - }; - if (parentId) { - newNode.extent = "parent"; - } - - newNodes.push(newNode); - - const switchNode = { - id: nodeId, - type: "custom", - position: { x: 0, y: 0 }, - data: { - label: step.name, - ...step, - }, - parentId: subflowId, - prevStepId: "", - extent: "parent", - } as CustomNode; - - newNodes.push(switchNode); - - const trueSubflowNodes: CustomNode[] = processWorkflow( - step?.branches?.true, - subflowId - ); - const falseSubflowNodes: CustomNode[] = processWorkflow( - step?.branches?.false, - subflowId - ); - - if (trueSubflowNodes.length > 0) { - trueSubflowNodes[0].edge_label = "True"; - trueSubflowNodes[0].prevStepId = nodeId; // CORRECT THE PREVIOUS STEP ID FOR THE TRUE BRANCH - } - - if (falseSubflowNodes.length > 0) { - falseSubflowNodes[0].edge_label = "False"; - falseSubflowNodes[0].prevStepId = nodeId; // CORRECT THE PREVIOUS STEP ID FOR THE FALSE BRANCH - } - - newNodes = [...newNodes, ...trueSubflowNodes, ...falseSubflowNodes]; - } else { - newNode = { - id: nodeId, - type: "custom", - position, - data: { - label: step.name, - ...step, - }, - prevStepId: prevStepId, - parentId: parentId, - extent: parentId ? "parent" : "", - } as CustomNode; - - newNodes.push(newNode); - } + const { + nodes, + edges, + isLoading, + onEdgesChange, + onNodesChange, + onConnect, + onDragOver, + onDrop, + } = useWorkflowInitialization(workflow, loadedAlertFile, providers); - return newNodes; - }; return ( -
- - setNodes((nds) => applyNodeChanges(changes, nds)) - } - onEdgesChange={(changes) => - setEdges((eds) => applyEdgeChanges(changes, eds)) - } - onConnect={onConnect} - nodeTypes={nodeTypes} - edgeTypes={edgeTypes} - fitView - > - - - +
+ {!isLoading && ( + + + + + )} +
); }; diff --git a/keep-ui/app/workflows/builder/ToolBox.tsx b/keep-ui/app/workflows/builder/ToolBox.tsx index cc23ab111..edb17e16a 100644 --- a/keep-ui/app/workflows/builder/ToolBox.tsx +++ b/keep-ui/app/workflows/builder/ToolBox.tsx @@ -1,55 +1,42 @@ -// ToolBox.tsx import React from 'react'; -import { useDrag } from 'react-dnd'; -// import { ItemType } from './constants'; // Define item types -interface ToolBoxItemProps { - id: string; - name: string; - type: string; -} +const DragAndDropSidebar = () => { -const ToolBoxItem: React.FC = ({ id, name, type }) => { - const [{ isDragging }, drag] = useDrag(() => ({ - type: 'input', - item: { id, type, name }, - collect: (monitor) => ({ - isDragging: monitor.isDragging(), - }), - })); + const handleDragStart = (event, nodeType) => { + event.dataTransfer.setData('application/reactflow', nodeType); + event.dataTransfer.effectAllowed = 'move'; + }; return (
- {name} +
+ You can drag these nodes to the pane on the right. +
+
handleDragStart(event, 'custom')} + draggable + > + Input Node +
+
handleDragStart(event, 'custom')} + draggable + > + Default Node +
+
handleDragStart(event, 'custom')} + draggable + > + Output Node +
); }; -const ToolBox: React.FC = ({ toolboxConfiguration }: { toolboxConfiguration: { groups: Record } }) => { - const { groups } = toolboxConfiguration; - - return ( -
- {groups.map((group) => ( -
-

{group.name}

- {group.steps.map((step) => ( - - ))} -
- ))} -
- ); -}; - -export default ToolBox; +export default DragAndDropSidebar; diff --git a/keep-ui/app/workflows/builder/builder-store.tsx b/keep-ui/app/workflows/builder/builder-store.tsx new file mode 100644 index 000000000..8f28a2b64 --- /dev/null +++ b/keep-ui/app/workflows/builder/builder-store.tsx @@ -0,0 +1,202 @@ +import { create } from "zustand"; +import { v4 as uuidv4 } from "uuid"; +import { + addEdge, + applyNodeChanges, + applyEdgeChanges, + OnNodesChange, + OnEdgesChange, + OnConnect, + Edge, + Node, +} from "@xyflow/react"; + +export type FlowNode = Node & { + prevStepId?: string; + edge_label?: string; + data: Node["data"] & { + id: string; + type: string; + componentType: string; + name: string; + }; +}; + +const initialNodes = [ + { + id: "a", + position: { x: 0, y: 0 }, + data: { label: "Node A", type: "custom" }, + type: "custom", + }, + { + id: "b", + position: { x: 0, y: 100 }, + data: { label: "Node B", type: "custom" }, + type: "custom", + }, + { + id: "c", + position: { x: 0, y: 200 }, + data: { label: "Node C", type: "custom" }, + type: "custom", + }, +]; + +const initialEdges = [ + { id: "a->b", type: "custom-edge", source: "a", target: "b" }, + { id: "b->c", type: "custom-edge", source: "b", target: "c" }, +]; + +export type FlowState = { + nodes: FlowNode[]; + edges: Edge[]; + onNodesChange: OnNodesChange; + onEdgesChange: OnEdgesChange; + onConnect: OnConnect; + onDragOver: (event: React.DragEvent) => void; + onDrop: ( + event: React.DragEvent, + screenToFlowPosition: (coords: { x: number; y: number }) => { + x: number; + y: number; + } + ) => void; + setNodes: (nodes: FlowNode[]) => void; + setEdges: (edges: Edge[]) => void; + getNodeById: (id: string) => FlowNode | undefined; + hasNode: (id: string) => boolean; + deleteEdges: (ids: string | string[]) => void; + deleteNodes: (ids: string | string[]) => void; + updateNode: (node: FlowNode) => void; + duplicateNode: (node: FlowNode) => void; + addNode: (node: Partial) => void; // Add this function + createNode: (node: Partial) => FlowNode; +}; + +const useStore = create((set, get) => ({ + nodes: initialNodes as FlowNode[], + edges: initialEdges as Edge[], + onNodesChange: (changes) => + set({ nodes: applyNodeChanges(changes, get().nodes) }), + onEdgesChange: (changes) => + set({ edges: applyEdgeChanges(changes, get().edges) }), + onConnect: (connection) => { + const edge = { ...connection, type: "custom-edge" }; + set({ edges: addEdge(edge, get().edges) }); + }, + onDragOver: (event) => { + event.preventDefault(); + event.dataTransfer.dropEffect = "move"; + }, + // onDrop: (event, position) => { + // event.preventDefault(); + // event.stopPropagation(); + + // const nodeType = event.dataTransfer.getData('application/reactflow'); + // if (!nodeType) return; + + // const newUuid = uuidv4(); + // const newNode = { + // id: newUuid, + // type: nodeType, + // position: { x: position.x, y: position.y }, // Ensure position is an object with x and y + // data: { label: `${nodeType} node`, type: nodeType, name: `${nodeType} node`, componentType: nodeType, id: newUuid }, + // }; + // set({ nodes: [...get().nodes, newNode] }); + // }, + onDrop: (event, screenToFlowPosition) => { + event.preventDefault(); + event.stopPropagation(); + + const nodeType = event.dataTransfer.getData("application/reactflow"); + + console.log("nodeType=======>", nodeType) + if (!nodeType) return; + + // Use the screenToFlowPosition function to get flow coordinates + const position = screenToFlowPosition({ + x: event.clientX, + y: event.clientY, + }); + const newUuid = uuidv4(); + const newNode = { + id: newUuid, + type: nodeType, + position, // Use the position object with x and y + data: { + label: `${nodeType} node`, + type: nodeType, + name: `${nodeType} node`, + componentType: nodeType, + id: newUuid, + }, + }; + + set({ nodes: [...get().nodes, newNode] }); + }, + setNodes: (nodes) => set({ nodes }), + setEdges: (edges) => set({ edges }), + hasNode: (id) => !!get().nodes.find((node) => node.id === id), + getNodeById: (id) => get().nodes.find((node) => node.id === id), + deleteEdges: (ids) => { + const idArray = Array.isArray(ids) ? ids : [ids]; + set({ edges: get().edges.filter((edge) => !idArray.includes(edge.id)) }); + }, + deleteNodes: (ids) => { + const idArray = Array.isArray(ids) ? ids : [ids]; + set({ nodes: get().nodes.filter((node) => !idArray.includes(node.id)) }); + }, + updateNode: (node) => + set({ nodes: get().nodes.map((n) => (n.id === node.id ? node : n)) }), + duplicateNode: (node) => { + const { data, position } = node; + const newUuid = uuidv4(); + const newNode = { + ...node, + data: { ...data, id: newUuid }, + id: newUuid, + position: { x: position.x + 100, y: position.y + 100 }, + }; + set({ nodes: [...get().nodes, newNode] }); + }, + addNode: (node: Partial) => { + const newUuid = uuidv4(); + // console.log("node in addNode", node); + const newNode = { + ...node, + id: uuidv4(), + type: "custom", + data: { + type: "custom", + componentType: "custom", + name: "custom", + ...(node?.data ?? {}), + id: newUuid, + }, + }; + const newNodes = [...get().nodes, newNode]; + // console.log("newNodes in add Node", newNodes , newNode); + set({ + nodes: newNodes, + }); + }, + createNode: (node: Partial) => { + const newUuid = uuidv4(); + const newNode = { + type: "custom", + data: { + type: "custom", + componentType: "custom", + name: "custom", + ...(node?.data ?? {}), + id: newUuid, + }, + ...node, + id: newUuid, + }; + return newNode; + }, +})); + +export default useStore; diff --git a/keep-ui/app/workflows/builder/builder.tsx b/keep-ui/app/workflows/builder/builder.tsx index 836e53e42..07b110ed6 100644 --- a/keep-ui/app/workflows/builder/builder.tsx +++ b/keep-ui/app/workflows/builder/builder.tsx @@ -39,6 +39,7 @@ import { v4 as uuidv4 } from "uuid"; import BuilderWorkflowTestRunModalContent from "./builder-workflow-testrun-modal"; import { WorkflowExecution, WorkflowExecutionFailure } from "./types"; import ReactFlowBuilder from "./ReactFlowBuilder"; +import { ReactFlowProvider } from "@xyflow/react"; interface Props { loadedAlertFile: string | null; @@ -350,16 +351,18 @@ function Builder({ )} {useReactFlow && (
- } - stepEditor={ - - } - /> + + } + stepEditor={ + + } + /> +
)} {!useReactFlow && ( diff --git a/keep-ui/utils/hooks/useWorkflowInitialization.ts b/keep-ui/utils/hooks/useWorkflowInitialization.ts new file mode 100644 index 000000000..eecad7bda --- /dev/null +++ b/keep-ui/utils/hooks/useWorkflowInitialization.ts @@ -0,0 +1,254 @@ +import { useEffect, useState, useLayoutEffect, useRef, useCallback } from "react"; +import { Connection, Edge, Node, Position, useReactFlow } from "@xyflow/react"; +import dagre from "dagre"; +import { parseWorkflow, generateWorkflow } from "app/workflows/builder/utils"; +import { v4 as uuidv4 } from "uuid"; +import { useSearchParams } from "next/navigation"; +import useStore from "../../app/workflows/builder/builder-store"; +import { FlowNode } from "../../app/workflows/builder/builder-store"; + +const useWorkflowInitialization = ( + workflow: string, + loadedAlertFile: string, + providers: any[] +) => { + const { nodes, edges, setNodes, setEdges, onNodesChange, onEdgesChange, onConnect, onDragOver, onDrop } = useStore(); + + const [isLoading, setIsLoading] = useState(true); + const [alertName, setAlertName] = useState(null); + const [alertSource, setAlertSource] = useState( + null + ); + const searchParams = useSearchParams(); + const nodeRef = useRef(null); + const [nodeDimensions, setNodeDimensions] = useState({ + width: 200, + height: 100, + }); + const { screenToFlowPosition } = useReactFlow(); + + const handleDrop = useCallback( + (event) => { + onDrop(event, screenToFlowPosition); + }, + [screenToFlowPosition] + ); + + + + const newEdgesFromNodes = (nodes: FlowNode[]): Edge[] => { + const edges: Edge[] = []; + + nodes.forEach((node) => { + if (node.prevStepId) { + edges.push({ + id: `e${node.prevStepId}-${node.id}`, + source: node.prevStepId, + target: node.id, + type: "custom-edge", + label: node.edge_label || "", + }); + } + }); + return edges; + }; + + useLayoutEffect(() => { + if (nodeRef.current) { + const { width, height } = nodeRef.current.getBoundingClientRect(); + setNodeDimensions({ width: width + 20, height: height + 20 }); + } + }, [nodes.length]); + + useEffect(() => { + const alertNameParam = searchParams?.get("alertName"); + const alertSourceParam = searchParams?.get("alertSource"); + setAlertName(alertNameParam); + setAlertSource(alertSourceParam); + }, [searchParams]); + + useEffect(() => { + const initializeWorkflow = async () => { + setIsLoading(true); + let parsedWorkflow; + + if (workflow) { + parsedWorkflow = parseWorkflow(workflow, providers); + } else if (loadedAlertFile == null) { + const alertUuid = uuidv4(); + let triggers = {}; + if (alertName && alertSource) { + triggers = { alert: { source: alertSource, name: alertName } }; + } + parsedWorkflow = generateWorkflow(alertUuid, "", "", [], [], triggers); + } else { + parsedWorkflow = parseWorkflow(loadedAlertFile, providers); + } + + let newNodes = processWorkflow(parsedWorkflow.sequence); + let newEdges = newEdgesFromNodes(newNodes); + + const { nodes, edges } = getLayoutedElements(newNodes, newEdges); + + setNodes(nodes); + setEdges(edges); + setIsLoading(false); + }; + + initializeWorkflow(); + }, [loadedAlertFile, workflow, alertName, alertSource, providers]); + + const getLayoutedElements = (nodes: FlowNode[], edges: Edge[]) => { + const dagreGraph = new dagre.graphlib.Graph(); + dagreGraph.setDefaultEdgeLabel(() => ({})); + + dagreGraph.setGraph({ rankdir: "TB", nodesep: 100, edgesep: 100 }); + + nodes.forEach((node) => { + dagreGraph.setNode(node.id, { + width: nodeDimensions.width, + height: nodeDimensions.height, + }); + }); + + edges.forEach((edge) => { + dagreGraph.setEdge(edge.source, edge.target); + }); + + dagre.layout(dagreGraph); + + nodes.forEach((node: FlowNode) => { + const nodeWithPosition = dagreGraph.node(node.id); + node.targetPosition = "top" as Position; + node.sourcePosition = "bottom" as Position; + + node.position = { + x: nodeWithPosition.x - nodeDimensions.width / 2, + y: nodeWithPosition.y - nodeDimensions.height / 2, + }; + }); + + return { nodes, edges }; + }; + + const processWorkflow = (sequence: any, parentId?: string) => { + let newNodes: FlowNode[] = []; + + sequence.forEach((step: any, index: number) => { + const newPrevStepId = sequence?.[index - 1]?.id || ""; + const nodes = processStep( + step, + { x: index * 200, y: 50 }, + newPrevStepId, + parentId + ); + newNodes = [...newNodes, ...nodes]; + }); + + return newNodes; + }; + + const processStep = ( + step: any, + position: { x: number; y: number }, + prevStepId?: string, + parentId?: string + ) => { + const nodeId = step.id; + let newNode: FlowNode; + let newNodes: FlowNode[] = []; + + if (step.componentType === "switch") { + const subflowId = uuidv4(); + // newNode = { + // id: subflowId, + // type: "custom", + // position, + // data: { + // label: "Switch", + // type: "sub_flow", + // }, + // style: { + // border: "2px solid orange", + // width: "100%", + // height: "100%", + // display: "flex", + // flexDirection: "column", + // justifyContent: "space-between", + // }, + // prevStepId: prevStepId, + // parentId: parentId, + // }; + // if (parentId) { + // newNode.extent = "parent"; + // } + + // newNodes.push(newNode); + + const switchNode = { + id: nodeId, + type: "custom", + position: { x: 0, y: 0 }, + data: { + label: step.name, + ...step, + }, + prevStepId: prevStepId, + // extent: 'parent', + } as FlowNode; + + newNodes.push(switchNode); + + // const trueSubflowNodes: FlowNode[] = processWorkflow(step?.branches?.true, subflowId); + const trueSubflowNodes: FlowNode[] = processWorkflow( + step?.branches?.true + ); + // const falseSubflowNodes: FlowNode[] = processWorkflow(step?.branches?.false, subflowId); + const falseSubflowNodes: FlowNode[] = processWorkflow( + step?.branches?.false + ); + + if (trueSubflowNodes.length > 0) { + trueSubflowNodes[0].edge_label = "True"; + trueSubflowNodes[0].prevStepId = nodeId; + } + + if (falseSubflowNodes.length > 0) { + falseSubflowNodes[0].edge_label = "False"; + falseSubflowNodes[0].prevStepId = nodeId; + } + + newNodes = [...newNodes, ...trueSubflowNodes, ...falseSubflowNodes]; + } else { + newNode = { + id: nodeId, + type: "custom", + position, + data: { + label: step.name, + ...step, + }, + prevStepId: prevStepId, + // parentId: parentId, + } as FlowNode; + + newNodes.push(newNode); + } + + return newNodes; + }; + + return { + nodes, + edges, + isLoading, + onNodesChange: onNodesChange, + onEdgesChange: onEdgesChange, + onConnect: onConnect, + onDragOver: onDragOver, + onDrop: handleDrop, + + }; +}; + +export default useWorkflowInitialization; From 6054f8ac4fbe630191a3e09cca1b4437e8753808 Mon Sep 17 00:00:00 2001 From: Rajesh Jonnalagadda Date: Thu, 1 Aug 2024 20:49:54 +0530 Subject: [PATCH 04/55] refactor:usiign store for handling the basic operation like add duplicate,seletion and added toolbox and etting on the flow --- keep-ui/app/workflows/builder/CustomNode.tsx | 54 ++-- keep-ui/app/workflows/builder/NodeMenu.tsx | 9 +- .../workflows/builder/ReactFlowBuilder.tsx | 9 +- .../app/workflows/builder/ReactFlowEditor.tsx | 123 ++++++++ keep-ui/app/workflows/builder/ToolBox.tsx | 266 ++++++++++++++++-- .../app/workflows/builder/builder-store.tsx | 188 +++++++------ keep-ui/app/workflows/builder/builder.tsx | 86 +++--- keep-ui/app/workflows/builder/editors.tsx | 102 ++++++- .../utils/hooks/useWorkflowInitialization.ts | 79 ++++-- 9 files changed, 703 insertions(+), 213 deletions(-) create mode 100644 keep-ui/app/workflows/builder/ReactFlowEditor.tsx diff --git a/keep-ui/app/workflows/builder/CustomNode.tsx b/keep-ui/app/workflows/builder/CustomNode.tsx index e1cf251b6..de25f8d8b 100644 --- a/keep-ui/app/workflows/builder/CustomNode.tsx +++ b/keep-ui/app/workflows/builder/CustomNode.tsx @@ -1,33 +1,54 @@ -import React, { memo } from "react"; +import React, { memo, useEffect } from "react"; import { Handle, Position } from "@xyflow/react"; import NodeMenu from "./NodeMenu"; import useStore, { FlowNode } from "./builder-store"; import Image from "next/image"; +import { Properties } from "sequential-workflow-designer"; - -function IconUrlProvider(data: Partial) { - const { componentType, type } = data; +function IconUrlProvider(data: FlowNode["data"]) { + const { componentType, type } = data || {}; if (type === "alert" || type === "workflow") return "/keep.png"; return `/icons/${type - .replace("step-", "") - .replace("action-", "") - .replace("condition-", "")}-icon.png`; + ?.replace("step-", "") + ?.replace("action-", "") + ?.replace("condition-", "")}-icon.png`; } -function CustomNode({ data, id}:{data: Partial}) { +function CustomNode({ data, id }: { data: FlowNode["data"]; id: string }) { console.log("entering this CustomNode", data, id); - const { getNodeById } = useStore(); + const { getNodeById, selectedNode, setSelectedNode } = useStore(); const currentNode = getNodeById(id); + const type = data?.type + ?.replace("step-", "") + ?.replace("action-", "") + ?.replace("condition-", ""); + + if (!currentNode) return null; + + console.log("selectedNode", selectedNode); + console.log("currentNode", currentNode); + return ( <> {!!currentNode && ( -
+
{ + e.stopPropagation(); + console.log("before setting", currentNode); + setSelectedNode(currentNode); + }} + > {data?.type !== "sub_flow" && (
{data?.type}}) {
{data?.name}
-
{data?.componentType}
+
+ {type || data?.componentType} +
+
+
+
-
- -
- )} @@ -79,7 +78,11 @@ export default function NodeMenu({ node }: NodeMenuProps) { {({ active }) => ( +// )} +// {panelToggle && ( +//
+// +//
+//
+// {openGlobalEditor && } +// {!openGlobalEditor && selectedNode && } +//
+//
+//
+// )} +//
+// ); +// }; + +// export default function ReactFlowEditor() { +// const { selectedNode, stepEditorOpenForNode } = useStore(); +// const [forceOpen, setForceOpen] = useState(false); + +// useEffect(() => { +// if (selectedNode && selectedNode.id == stepEditorOpenForNode) { +// setForceOpen(true); +// } else { +// setForceOpen(false); +// } +// }, [stepEditorOpenForNode, selectedNode]); + +// console.log("forceOpen===============>", forceOpen); +// console.log("selectedNode===============>", selectedNode); +// console.log("stepEditorOpenForNode===============>", stepEditorOpenForNode); + + + +// return ( +//
+// +//
+ +// ); +// } + + + +import { useState, useEffect } from 'react'; +import { IoMdSettings, IoMdClose } from 'react-icons/io'; +import useStore from './builder-store'; +import { GlobalEditorV2, StepEditorV2 } from './editors'; +import { Button } from '@tremor/react'; + +const ReactFlowEditor = () => { + const { openGlobalEditor, selectedNode, stepEditorOpenForNode } = useStore(); + const [isOpen, setIsOpen] = useState(false); + + useEffect(() => { + setIsOpen(stepEditorOpenForNode === selectedNode?.id); + }, [stepEditorOpenForNode, selectedNode]); + + return ( +
+ {!isOpen && ( + + )} + {isOpen && ( +
+ +
+
+ {openGlobalEditor && } + {!openGlobalEditor && selectedNode && } +
+
+
+ )} +
+ ); +}; + +export default ReactFlowEditor; diff --git a/keep-ui/app/workflows/builder/ToolBox.tsx b/keep-ui/app/workflows/builder/ToolBox.tsx index edb17e16a..0a4f144a8 100644 --- a/keep-ui/app/workflows/builder/ToolBox.tsx +++ b/keep-ui/app/workflows/builder/ToolBox.tsx @@ -1,41 +1,249 @@ -import React from 'react'; +// import { Menu } from "@/components/navbar/Menu"; +// import { Disclosure } from "@headlessui/react"; +// import { Divider, Subtitle } from "@tremor/react"; +// import React, { useState, useEffect } from "react"; +// import { IoChevronUp } from "react-icons/io5"; +// import classNames from "classnames"; +// import Image from "next/image"; -const DragAndDropSidebar = () => { +// const GroupedMenu = ({ name, steps, searchTerm }) => { +// const [isOpen, setIsOpen] = useState(!!searchTerm); - const handleDragStart = (event, nodeType) => { - event.dataTransfer.setData('application/reactflow', nodeType); - event.dataTransfer.effectAllowed = 'move'; +// useEffect(() => { +// setIsOpen(!!searchTerm); +// }, [searchTerm]); + +// function IconUrlProvider(data: FlowNode["data"]) { +// const { componentType, type } = data || {}; +// if (type === "alert" || type === "workflow") return "/keep.png"; +// return `/icons/${type +// ?.replace("step-", "") +// ?.replace("action-", "") +// ?.replace("condition-", "")}-icon.png`; +// } + +// const handleDragStart = (event, step) => { +// event.dataTransfer.setData("application/reactflow", JSON.stringify(step)); +// event.dataTransfer.effectAllowed = "move"; +// }; + +// return ( +// +// {({ open }) => ( +// <> +// +// +// {name} +// +// +// +// {open && ( +// +// {steps.length > 0 && +// steps.map((step) => ( +//
  • handleDragStart(event, { ...step })} +// draggable +// title={step.name} +// > +// {step?.type} +// {step.name} +//
  • +// ))} +//
    +// )} +// +// )} +//
    +// ); +// }; + +// const DragAndDropSidebar = ({ toolboxConfiguration }) => { +// const [searchTerm, setSearchTerm] = useState(""); + +// const filteredGroups = +// toolboxConfiguration?.groups?.map((group) => ({ +// ...group, +// steps: group.steps.filter((step) => +// step.name.toLowerCase().includes(searchTerm.toLowerCase()) +// ), +// })) || []; + +// return ( +//
    +// setSearchTerm(e.target.value)} +// /> +//
    +// {filteredGroups.length > 0 && +// filteredGroups.map((group) => ( +// +// ))} +//
    +//
    +// ); +// }; + +// export default DragAndDropSidebar; + + + +import { Menu } from "@/components/navbar/Menu"; +import { Disclosure } from "@headlessui/react"; +import { Divider, Subtitle } from "@tremor/react"; +import React, { useState, useEffect } from "react"; +import { IoChevronUp, IoMenu, IoClose } from "react-icons/io5"; +import classNames from "classnames"; +import Image from "next/image"; +import { IoIosArrowDown } from "react-icons/io"; + + + +const GroupedMenu = ({ name, steps, searchTerm }) => { + const [isOpen, setIsOpen] = useState(!!searchTerm); + + useEffect(() => { + setIsOpen(!!searchTerm); + }, [searchTerm]); + + function IconUrlProvider(data) { + const { componentType, type } = data || {}; + if (type === "alert" || type === "workflow") return "/keep.png"; + return `/icons/${type + ?.replace("step-", "") + ?.replace("action-", "") + ?.replace("condition-", "")}-icon.png`; + } + + const handleDragStart = (event, step) => { + event.dataTransfer.setData("application/reactflow", JSON.stringify(step)); + event.dataTransfer.effectAllowed = "move"; }; return ( + + {({ open }) => ( + <> + + + {name} + + + + {open && ( + + {steps.length > 0 && + steps.map((step) => ( +
  • handleDragStart(event, { ...step })} + draggable + title={step.name} + > + {step?.type} + {step.name} +
  • + ))} +
    + )} + + )} +
    + ); +}; + +const DragAndDropSidebar = ({ toolboxConfiguration }) => { + const [searchTerm, setSearchTerm] = useState(""); + const [isVisible, setIsVisible] = useState(false); + + const filteredGroups = + toolboxConfiguration?.groups?.map((group) => ({ + ...group, + steps: group.steps.filter((step) => + step.name.toLowerCase().includes(searchTerm.toLowerCase()) + ), + })) || []; + + const checkForSearchResults = searchTerm && !!filteredGroups?.find((group) => group?.steps?.length>0); + + + return ( + //
    -
    - You can drag these nodes to the pane on the right. -
    -
    handleDragStart(event, 'custom')} - draggable - > - Input Node -
    -
    handleDragStart(event, 'custom')} - draggable - > - Default Node -
    -
    handleDragStart(event, 'custom')} - draggable - > - Output Node +
    + + + setSearchTerm(e.target.value)} + /> +
    + {(isVisible || checkForSearchResults) &&
    + {filteredGroups.length > 0 && + filteredGroups.map((group) => ( + + ))} +
    }
    + //
    ); }; diff --git a/keep-ui/app/workflows/builder/builder-store.tsx b/keep-ui/app/workflows/builder/builder-store.tsx index 8f28a2b64..79c16306f 100644 --- a/keep-ui/app/workflows/builder/builder-store.tsx +++ b/keep-ui/app/workflows/builder/builder-store.tsx @@ -11,18 +11,29 @@ import { Node, } from "@xyflow/react"; +export type V2Properties = Record; + +export type V2Step = { + id: string; + name?: string; + componentType: string; + type: string; + properties?: V2Properties; + branches?: { + "true"?: V2Step[]; + "false"?: V2Step[]; + }; + sequence?: V2Step[] | V2Step; +}; + +export type NodeData = Node['data'] & Record; export type FlowNode = Node & { prevStepId?: string; edge_label?: string; - data: Node["data"] & { - id: string; - type: string; - componentType: string; - name: string; - }; + data: NodeData; }; -const initialNodes = [ +const initialNodes: FlowNode[] = [ { id: "a", position: { x: 0, y: 0 }, @@ -43,7 +54,7 @@ const initialNodes = [ }, ]; -const initialEdges = [ +const initialEdges: Edge[] = [ { id: "a->b", type: "custom-edge", source: "a", target: "b" }, { id: "b->c", type: "custom-edge", source: "b", target: "c" }, ]; @@ -51,6 +62,10 @@ const initialEdges = [ export type FlowState = { nodes: FlowNode[]; edges: Edge[]; + selectedNode: FlowNode | null; + v2Properties: V2Properties; + openGlobalEditor: boolean; + stepEditorOpenForNode: string|null; onNodesChange: OnNodesChange; onEdgesChange: OnEdgesChange; onConnect: OnConnect; @@ -70,13 +85,45 @@ export type FlowState = { deleteNodes: (ids: string | string[]) => void; updateNode: (node: FlowNode) => void; duplicateNode: (node: FlowNode) => void; - addNode: (node: Partial) => void; // Add this function - createNode: (node: Partial) => FlowNode; + // addNode: (node: Partial) => void; + setSelectedNode: (node: FlowNode | null) => void; + setV2Properties: (properties: V2Properties) => void; + setOpneGlobalEditor: (open: boolean) => void; + // updateNodeData: (nodeId: string, key: string, value: any) => void; + updateSelectedNodeData: (key: string, value: any) => void; + updateV2Properties: (key: string, value: any) => void; + setStepEditorOpenForNode: (nodeId: string, open: boolean) => void; }; const useStore = create((set, get) => ({ - nodes: initialNodes as FlowNode[], - edges: initialEdges as Edge[], + nodes: initialNodes, + edges: initialEdges, + selectedNode: null, + v2Properties: {}, + openGlobalEditor: true, + stepEditorOpenForNode: null, + setOpneGlobalEditor: (open) => set({ openGlobalEditor: open }), + updateSelectedNodeData: (key, value) => { + const currentSelectedNode = get().selectedNode; + if (currentSelectedNode) { + const updatedNodes = get().nodes.map((node) => + node.id === currentSelectedNode.id + ? { ...node, data: { ...node.data, [key]: value } } + : node + ); + set({ nodes: updatedNodes, selectedNode: { ...currentSelectedNode, data: { ...currentSelectedNode.data, [key]: value } } }); + } + }, + setV2Properties: (properties) => set({ v2Properties: properties }), + updateV2Properties: (key, value) => { + const updatedProperties = { ...get().v2Properties, [key]: value }; + set({ v2Properties: updatedProperties }); + }, + setSelectedNode: (node) =>{ set({ selectedNode: node }); set({ openGlobalEditor: false }); }, + setStepEditorOpenForNode: (nodeId:string) => { + set({openGlobalEditor: false}); + set({ stepEditorOpenForNode: nodeId }); + }, onNodesChange: (changes) => set({ nodes: applyNodeChanges(changes, get().nodes) }), onEdgesChange: (changes) => @@ -89,51 +136,37 @@ const useStore = create((set, get) => ({ event.preventDefault(); event.dataTransfer.dropEffect = "move"; }, - // onDrop: (event, position) => { - // event.preventDefault(); - // event.stopPropagation(); - - // const nodeType = event.dataTransfer.getData('application/reactflow'); - // if (!nodeType) return; - - // const newUuid = uuidv4(); - // const newNode = { - // id: newUuid, - // type: nodeType, - // position: { x: position.x, y: position.y }, // Ensure position is an object with x and y - // data: { label: `${nodeType} node`, type: nodeType, name: `${nodeType} node`, componentType: nodeType, id: newUuid }, - // }; - // set({ nodes: [...get().nodes, newNode] }); - // }, onDrop: (event, screenToFlowPosition) => { event.preventDefault(); event.stopPropagation(); - const nodeType = event.dataTransfer.getData("application/reactflow"); + try { + let step: any = event.dataTransfer.getData("application/reactflow"); + step = JSON.parse(step); + console.log("nodeType=======>", step); + if (!step) return; - console.log("nodeType=======>", nodeType) - if (!nodeType) return; - - // Use the screenToFlowPosition function to get flow coordinates - const position = screenToFlowPosition({ - x: event.clientX, - y: event.clientY, - }); - const newUuid = uuidv4(); - const newNode = { - id: newUuid, - type: nodeType, - position, // Use the position object with x and y - data: { - label: `${nodeType} node`, - type: nodeType, - name: `${nodeType} node`, - componentType: nodeType, + // Use the screenToFlowPosition function to get flow coordinates + const position = screenToFlowPosition({ + x: event.clientX, + y: event.clientY, + }); + const newUuid = uuidv4(); + const newNode: FlowNode = { id: newUuid, - }, - }; + type: "custom", + position, // Use the position object with x and y + data: { + label: step.name! as string, + ...step, + id: newUuid, + }, + }; - set({ nodes: [...get().nodes, newNode] }); + set({ nodes: [...get().nodes, newNode] }); + } catch (err) { + console.error(err); + } }, setNodes: (nodes) => set({ nodes }), setEdges: (edges) => set({ edges }), @@ -152,7 +185,7 @@ const useStore = create((set, get) => ({ duplicateNode: (node) => { const { data, position } = node; const newUuid = uuidv4(); - const newNode = { + const newNode: FlowNode = { ...node, data: { ...data, id: newUuid }, id: newUuid, @@ -160,43 +193,22 @@ const useStore = create((set, get) => ({ }; set({ nodes: [...get().nodes, newNode] }); }, - addNode: (node: Partial) => { - const newUuid = uuidv4(); - // console.log("node in addNode", node); - const newNode = { - ...node, - id: uuidv4(), - type: "custom", - data: { - type: "custom", - componentType: "custom", - name: "custom", - ...(node?.data ?? {}), - id: newUuid, - }, - }; - const newNodes = [...get().nodes, newNode]; - // console.log("newNodes in add Node", newNodes , newNode); - set({ - nodes: newNodes, - }); - }, - createNode: (node: Partial) => { - const newUuid = uuidv4(); - const newNode = { - type: "custom", - data: { - type: "custom", - componentType: "custom", - name: "custom", - ...(node?.data ?? {}), - id: newUuid, - }, - ...node, - id: newUuid, - }; - return newNode; - }, + // addNode: (node) => { + // const newUuid = uuidv4(); + // const newNode: FlowNode = { + // ...node, + // id: newUuid, + // type: "custom", + // data: { + // type: "custom", + // componentType: "custom", + // name: "custom", + // ...(node?.data ?? {}), + // id: newUuid, + // }, + // }; + // set({ nodes: [...get().nodes, newNode] }); + // }, })); export default useStore; diff --git a/keep-ui/app/workflows/builder/builder.tsx b/keep-ui/app/workflows/builder/builder.tsx index 07b110ed6..be8f06baf 100644 --- a/keep-ui/app/workflows/builder/builder.tsx +++ b/keep-ui/app/workflows/builder/builder.tsx @@ -14,7 +14,7 @@ import { wrapDefinition, } from "sequential-workflow-designer-react"; import { useEffect, useState } from "react"; -import StepEditor, { GlobalEditor } from "./editors"; +import StepEditor, { GlobalEditor, GlobalEditorV2 } from "./editors"; import { Callout, Card, Switch } from "@tremor/react"; import { Provider } from "../../providers/providers"; import { @@ -70,7 +70,7 @@ function Builder({ installedProviders, isPreview, }: Props) { - const [useReactFlow, setUseReactFlow] = useState(false); + const [useReactFlow, setUseReactFlow] = useState(true); const [definition, setDefinition] = useState(() => wrapDefinition({ sequence: [], properties: {} } as Definition) @@ -292,6 +292,28 @@ function Builder({ setUseReactFlow(value); }; + const getworkflowStatus = () => { + return stepValidationError || globalValidationError ? ( + + {stepValidationError || globalValidationError} + + ) : ( + + Alert can be generated successfully + + ); + }; + return ( <>
    @@ -330,55 +352,35 @@ function Builder({ {generateModalIsOpen || testRunModalOpen ? null : ( <> - {stepValidationError || globalValidationError ? ( - - {stepValidationError || globalValidationError} - - ) : ( - - Alert can be generated successfully - - )} + {getworkflowStatus()} {useReactFlow && ( -
    +
    } - stepEditor={ - - } /> - -
    + +
    )} {!useReactFlow && ( - } - stepEditor={ - - } - /> + <> + } + stepEditor={ + + } + /> + )} )} @@ -386,4 +388,4 @@ function Builder({ ); } -export default Builder; \ No newline at end of file +export default Builder; diff --git a/keep-ui/app/workflows/builder/editors.tsx b/keep-ui/app/workflows/builder/editors.tsx index 945c4a30b..bbfca0ada 100644 --- a/keep-ui/app/workflows/builder/editors.tsx +++ b/keep-ui/app/workflows/builder/editors.tsx @@ -22,13 +22,36 @@ import { FunnelIcon, HandRaisedIcon, } from "@heroicons/react/24/outline"; +import useStore, { V2Properties } from "./builder-store"; +import { useEffect } from "react"; function EditorLayout({ children }: { children: React.ReactNode }) { return
    {children}
    ; } export function GlobalEditor() { - const { properties, setProperty } = useGlobalEditor(); + const { properties, setProperty } = useGlobalEditor() + return ( + + Keep Workflow Editor + + Use this visual workflow editor to easily create or edit existing Keep + workflow YAML specifications. + + + Use the toolbox to add steps, conditions and actions to your workflow + and click the `Generate` button to compile the workflow / `Deploy` + button to deploy the workflow to Keep. + + {WorkflowEditor(properties, setProperty)} + + ); +} + +export function GlobalEditorV2() { + const { v2Properties:properties, updateV2Properties: setProperty } = useStore(); + + console.log("properties========>", properties) return ( Keep Workflow Editor @@ -48,10 +71,11 @@ export function GlobalEditor() { interface keepEditorProps { properties: Properties; - updateProperty: (key: string, value: any) => void; + updateProperty: ((key: string, value: any) => void); installedProviders?: Provider[] | null | undefined; providerType?: string; type?: string; + isV2?:boolean } function KeepStepEditor({ @@ -387,6 +411,76 @@ function WorkflowEditor(properties: Properties, updateProperty: any) { ); } + +export function StepEditorV2({ + installedProviders, +}: { + installedProviders?: Provider[] | undefined | null; +}) { + const { + selectedNode, + updateSelectedNodeData, + } = useStore() + + console.log("selectedNode======>in editor", selectedNode); + const {data} = selectedNode || {}; + const {name, type, properties} = data || {}; + + console.log("properties======>in step editor", properties) + function onNameChanged(e: any) { + updateSelectedNodeData( "name", e.target.value); + } + + const setProperty = (key:string, value:any) => { + updateSelectedNodeData('properties', {...properties, [key]: value }) + } + + const providerType = type?.split("-")[1]; + + if(!selectedNode){ + return + Node not found! + + } + + return ( + + {providerType} Editor + Unique Identifier + + {type.includes("step-") || type.includes("action-") ? ( + + ) : type === "condition-threshold" ? ( + + ) : type.includes("foreach") ? ( + + ) : type === "condition-assert" ? ( + + ) : null} + + ); +} + export default function StepEditor({ installedProviders, }: { @@ -399,6 +493,8 @@ export default function StepEditor({ setName(e.target.value); } + console.log("properties======>in step editor", properties) + const providerType = type.split("-")[1]; return ( @@ -437,4 +533,4 @@ export default function StepEditor({ ) : null} ); -} +} \ No newline at end of file diff --git a/keep-ui/utils/hooks/useWorkflowInitialization.ts b/keep-ui/utils/hooks/useWorkflowInitialization.ts index eecad7bda..b4b471b83 100644 --- a/keep-ui/utils/hooks/useWorkflowInitialization.ts +++ b/keep-ui/utils/hooks/useWorkflowInitialization.ts @@ -1,4 +1,10 @@ -import { useEffect, useState, useLayoutEffect, useRef, useCallback } from "react"; +import { + useEffect, + useState, + useLayoutEffect, + useRef, + useCallback, +} from "react"; import { Connection, Edge, Node, Position, useReactFlow } from "@xyflow/react"; import dagre from "dagre"; import { parseWorkflow, generateWorkflow } from "app/workflows/builder/utils"; @@ -6,13 +12,27 @@ import { v4 as uuidv4 } from "uuid"; import { useSearchParams } from "next/navigation"; import useStore from "../../app/workflows/builder/builder-store"; import { FlowNode } from "../../app/workflows/builder/builder-store"; +import { Properties } from 'sequential-workflow-designer'; const useWorkflowInitialization = ( workflow: string, loadedAlertFile: string, providers: any[] ) => { - const { nodes, edges, setNodes, setEdges, onNodesChange, onEdgesChange, onConnect, onDragOver, onDrop } = useStore(); + const { + nodes, + edges, + setNodes, + setEdges, + onNodesChange, + onEdgesChange, + onConnect, + onDragOver, + onDrop, + setV2Properties, + openGlobalEditor, + selectedNode, + } = useStore(); const [isLoading, setIsLoading] = useState(true); const [alertName, setAlertName] = useState(null); @@ -34,8 +54,6 @@ const useWorkflowInitialization = ( [screenToFlowPosition] ); - - const newEdgesFromNodes = (nodes: FlowNode[]): Edge[] => { const edges: Edge[] = []; @@ -84,7 +102,9 @@ const useWorkflowInitialization = ( } else { parsedWorkflow = parseWorkflow(loadedAlertFile, providers); } - + + console.log("parsedWorkflow=======>", parsedWorkflow); + setV2Properties(parsedWorkflow?.properties ?? {}); let newNodes = processWorkflow(parsedWorkflow.sequence); let newEdges = newEdgesFromNodes(newNodes); @@ -119,7 +139,7 @@ const useWorkflowInitialization = ( nodes.forEach((node: FlowNode) => { const nodeWithPosition = dagreGraph.node(node.id); - node.targetPosition = "top" as Position; + node.targetPosition = "top" as Position; node.sourcePosition = "bottom" as Position; node.position = { @@ -160,28 +180,28 @@ const useWorkflowInitialization = ( if (step.componentType === "switch") { const subflowId = uuidv4(); - // newNode = { - // id: subflowId, - // type: "custom", - // position, - // data: { - // label: "Switch", - // type: "sub_flow", - // }, - // style: { - // border: "2px solid orange", - // width: "100%", - // height: "100%", - // display: "flex", - // flexDirection: "column", - // justifyContent: "space-between", - // }, - // prevStepId: prevStepId, - // parentId: parentId, - // }; - // if (parentId) { - // newNode.extent = "parent"; - // } + // newNode = { + // id: subflowId, + // type: "custom", + // position, + // data: { + // label: "Switch", + // type: "sub_flow", + // }, + // style: { + // border: "2px solid orange", + // width: "100%", + // height: "100%", + // display: "flex", + // flexDirection: "column", + // justifyContent: "space-between", + // }, + // prevStepId: prevStepId, + // parentId: parentId, + // }; + // if (parentId) { + // newNode.extent = "parent"; + // } // newNodes.push(newNode); @@ -247,7 +267,8 @@ const useWorkflowInitialization = ( onConnect: onConnect, onDragOver: onDragOver, onDrop: handleDrop, - + openGlobalEditor, + selectedNode, }; }; From 6ed993e092e54d7caf3aaf7cbdb7eea909db4d57 Mon Sep 17 00:00:00 2001 From: Rajesh Jonnalagadda Date: Thu, 1 Aug 2024 21:01:28 +0530 Subject: [PATCH 05/55] chore:clean up --- keep-ui/app/workflows/builder/ToolBox.tsx | 115 ---------------------- 1 file changed, 115 deletions(-) diff --git a/keep-ui/app/workflows/builder/ToolBox.tsx b/keep-ui/app/workflows/builder/ToolBox.tsx index 0a4f144a8..609f256ac 100644 --- a/keep-ui/app/workflows/builder/ToolBox.tsx +++ b/keep-ui/app/workflows/builder/ToolBox.tsx @@ -1,118 +1,3 @@ -// import { Menu } from "@/components/navbar/Menu"; -// import { Disclosure } from "@headlessui/react"; -// import { Divider, Subtitle } from "@tremor/react"; -// import React, { useState, useEffect } from "react"; -// import { IoChevronUp } from "react-icons/io5"; -// import classNames from "classnames"; -// import Image from "next/image"; - -// const GroupedMenu = ({ name, steps, searchTerm }) => { -// const [isOpen, setIsOpen] = useState(!!searchTerm); - -// useEffect(() => { -// setIsOpen(!!searchTerm); -// }, [searchTerm]); - -// function IconUrlProvider(data: FlowNode["data"]) { -// const { componentType, type } = data || {}; -// if (type === "alert" || type === "workflow") return "/keep.png"; -// return `/icons/${type -// ?.replace("step-", "") -// ?.replace("action-", "") -// ?.replace("condition-", "")}-icon.png`; -// } - -// const handleDragStart = (event, step) => { -// event.dataTransfer.setData("application/reactflow", JSON.stringify(step)); -// event.dataTransfer.effectAllowed = "move"; -// }; - -// return ( -// -// {({ open }) => ( -// <> -// -// -// {name} -// -// -// -// {open && ( -// -// {steps.length > 0 && -// steps.map((step) => ( -//
  • handleDragStart(event, { ...step })} -// draggable -// title={step.name} -// > -// {step?.type} -// {step.name} -//
  • -// ))} -//
    -// )} -// -// )} -//
    -// ); -// }; - -// const DragAndDropSidebar = ({ toolboxConfiguration }) => { -// const [searchTerm, setSearchTerm] = useState(""); - -// const filteredGroups = -// toolboxConfiguration?.groups?.map((group) => ({ -// ...group, -// steps: group.steps.filter((step) => -// step.name.toLowerCase().includes(searchTerm.toLowerCase()) -// ), -// })) || []; - -// return ( -//
    -// setSearchTerm(e.target.value)} -// /> -//
    -// {filteredGroups.length > 0 && -// filteredGroups.map((group) => ( -// -// ))} -//
    -//
    -// ); -// }; - -// export default DragAndDropSidebar; - - - import { Menu } from "@/components/navbar/Menu"; import { Disclosure } from "@headlessui/react"; import { Divider, Subtitle } from "@tremor/react"; From c93976360f00b993fb4a6c8d438aa56229797711 Mon Sep 17 00:00:00 2001 From: Rajesh Jonnalagadda Date: Fri, 2 Aug 2024 22:38:45 +0530 Subject: [PATCH 06/55] feat:added connection restrictions and code clean up --- keep-ui/app/workflows/builder/CustomEdge.tsx | 55 ++- .../workflows/builder/ReactFlowBuilder.tsx | 10 +- .../app/workflows/builder/ReactFlowEditor.tsx | 100 +---- keep-ui/app/workflows/builder/SubFlowNode.tsx | 78 ++-- keep-ui/app/workflows/builder/ToolBox.tsx | 33 +- .../app/workflows/builder/builder-store.tsx | 421 ++++++++++-------- keep-ui/app/workflows/builder/editors.tsx | 28 +- .../utils/hooks/useWorkflowInitialization.ts | 32 +- 8 files changed, 382 insertions(+), 375 deletions(-) diff --git a/keep-ui/app/workflows/builder/CustomEdge.tsx b/keep-ui/app/workflows/builder/CustomEdge.tsx index d8ab6f03f..ef3eba24b 100644 --- a/keep-ui/app/workflows/builder/CustomEdge.tsx +++ b/keep-ui/app/workflows/builder/CustomEdge.tsx @@ -1,19 +1,24 @@ -import React from 'react'; -import { - BaseEdge, - EdgeLabelRenderer, - getSmoothStepPath, -} from '@xyflow/react'; -import type { EdgeProps } from '@xyflow/react'; -import useStore from './builder-store'; +import React from "react"; +import { BaseEdge, EdgeLabelRenderer, getSmoothStepPath } from "@xyflow/react"; +import type { Edge, EdgeProps } from "@xyflow/react"; +import useStore from "./builder-store"; interface CustomEdgeProps extends EdgeProps { label?: string; type?: string; } -const CustomEdge = ({ id, sourceX, sourceY, targetX, targetY, label }: CustomEdgeProps) => { - const { deleteEdges } = useStore(); +const CustomEdge:React.FC = ({ + id, + sourceX, + sourceY, + targetX, + targetY, + label, + source, + target, +}: CustomEdgeProps) => { + const { deleteEdges, getNodeById, edges, updateEdge } = useStore(); // Calculate the path and midpoint const [edgePath, labelX, labelY] = getSmoothStepPath({ @@ -33,6 +38,26 @@ const CustomEdge = ({ id, sourceX, sourceY, targetX, targetY, label }: CustomEdg deleteEdges(id); }; + const sourceNode = getNodeById(source); + const targetNode = getNodeById(target); + + let dynamicLabel = label; + const componentType = sourceNode?.data?.componentType || ""; + if (!dynamicLabel && sourceNode && targetNode && componentType == "switch") { + const existEdge = edges?.find( + ({ source, id: edgeId }: Edge) => + edgeId !== id && source === sourceNode.id + ); + + if (!existEdge) { + dynamicLabel = "True"; + updateEdge(id, "label", dynamicLabel); + } + if (existEdge && existEdge.label) { + dynamicLabel = existEdge.label === "True" ? "False" : "True"; + updateEdge(id, "label", dynamicLabel); + } + } return ( <> - {!!label && ( + {!!dynamicLabel && (
    - {label} + {dynamicLabel}
    )} -// )} -// {panelToggle && ( -//
    -// -//
    -//
    -// {openGlobalEditor && } -// {!openGlobalEditor && selectedNode && } -//
    -//
    -//
    -// )} -//
    -// ); -// }; - -// export default function ReactFlowEditor() { -// const { selectedNode, stepEditorOpenForNode } = useStore(); -// const [forceOpen, setForceOpen] = useState(false); - -// useEffect(() => { -// if (selectedNode && selectedNode.id == stepEditorOpenForNode) { -// setForceOpen(true); -// } else { -// setForceOpen(false); -// } -// }, [stepEditorOpenForNode, selectedNode]); - -// console.log("forceOpen===============>", forceOpen); -// console.log("selectedNode===============>", selectedNode); -// console.log("stepEditorOpenForNode===============>", stepEditorOpenForNode); - - - -// return ( -//
    -// -//
    - -// ); -// } - - - -import { useState, useEffect } from 'react'; -import { IoMdSettings, IoMdClose } from 'react-icons/io'; -import useStore from './builder-store'; -import { GlobalEditorV2, StepEditorV2 } from './editors'; -import { Button } from '@tremor/react'; +import { useState, useEffect } from "react"; +import { IoMdSettings, IoMdClose } from "react-icons/io"; +import useStore from "./builder-store"; +import { GlobalEditorV2, StepEditorV2 } from "./editors"; +import { Button } from "@tremor/react"; const ReactFlowEditor = () => { const { openGlobalEditor, selectedNode, stepEditorOpenForNode } = useStore(); const [isOpen, setIsOpen] = useState(false); useEffect(() => { - setIsOpen(stepEditorOpenForNode === selectedNode?.id); - }, [stepEditorOpenForNode, selectedNode]); + if (stepEditorOpenForNode) { + setIsOpen(stepEditorOpenForNode === selectedNode?.id); + } + }, [stepEditorOpenForNode, selectedNode?.id]); return ( -
    +
    {!isOpen && ( @@ -104,7 +33,6 @@ const ReactFlowEditor = () => { diff --git a/keep-ui/app/workflows/builder/SubFlowNode.tsx b/keep-ui/app/workflows/builder/SubFlowNode.tsx index 7d28e8c98..9847f9b45 100644 --- a/keep-ui/app/workflows/builder/SubFlowNode.tsx +++ b/keep-ui/app/workflows/builder/SubFlowNode.tsx @@ -1,42 +1,42 @@ -import React, { memo } from 'react'; -import { Handle, Position } from '@xyflow/react'; +// import React, { memo } from 'react'; +// import { Handle, Position } from '@xyflow/react'; -function IconUrlProvider({ componentType, type }) { - if (type === "alert" || type === "workflow") return "/keep.png"; - return `/icons/${type - ?.replace("step-", "") - .replace("action-", "") - .replace("condition-", "")}-icon.png`; -} +// function IconUrlProvider({ componentType, type }) { +// if (type === "alert" || type === "workflow") return "/keep.png"; +// return `/icons/${type +// ?.replace("step-", "") +// .replace("action-", "") +// .replace("condition-", "")}-icon.png`; +// } -function SubFlowNode({ data }) { - return ( -
    -
    -
    - {data?.type} -
    -
    -
    {data?.label}
    -
    {data?.componentType}
    -
    -
    - - -
    - ); -} +// function SubFlowNode({ data }) { +// return ( +//
    +//
    +//
    +// {data?.type} +//
    +//
    +//
    {data?.label}
    +//
    {data?.componentType}
    +//
    +//
    +// +// +//
    +// ); +// } -export default memo(SubFlowNode); +// export default memo(SubFlowNode); diff --git a/keep-ui/app/workflows/builder/ToolBox.tsx b/keep-ui/app/workflows/builder/ToolBox.tsx index 609f256ac..403beff69 100644 --- a/keep-ui/app/workflows/builder/ToolBox.tsx +++ b/keep-ui/app/workflows/builder/ToolBox.tsx @@ -1,23 +1,20 @@ -import { Menu } from "@/components/navbar/Menu"; -import { Disclosure } from "@headlessui/react"; -import { Divider, Subtitle } from "@tremor/react"; import React, { useState, useEffect } from "react"; -import { IoChevronUp, IoMenu, IoClose } from "react-icons/io5"; import classNames from "classnames"; +import { Disclosure } from "@headlessui/react"; +import { Subtitle } from "@tremor/react"; +import { IoChevronUp, IoClose } from "react-icons/io5"; import Image from "next/image"; import { IoIosArrowDown } from "react-icons/io"; - - -const GroupedMenu = ({ name, steps, searchTerm }) => { +const GroupedMenu = ({ name, steps, searchTerm }:{name:string, steps:any[], searchTerm:string}) => { const [isOpen, setIsOpen] = useState(!!searchTerm); useEffect(() => { setIsOpen(!!searchTerm); }, [searchTerm]); - function IconUrlProvider(data) { - const { componentType, type } = data || {}; + function IconUrlProvider(data:any) { + const { type } = data || {}; if (type === "alert" || type === "workflow") return "/keep.png"; return `/icons/${type ?.replace("step-", "") @@ -25,7 +22,7 @@ const GroupedMenu = ({ name, steps, searchTerm }) => { ?.replace("condition-", "")}-icon.png`; } - const handleDragStart = (event, step) => { + const handleDragStart = (event:React.DragEvent, step:any) => { event.dataTransfer.setData("application/reactflow", JSON.stringify(step)); event.dataTransfer.effectAllowed = "move"; }; @@ -51,7 +48,7 @@ const GroupedMenu = ({ name, steps, searchTerm }) => { className="space-y-2 overflow-auto min-w-[max-content] p-2 pr-4" > {steps.length > 0 && - steps.map((step) => ( + steps.map((step:any) => (
  • { ); }; -const DragAndDropSidebar = ({ toolboxConfiguration }) => { +const DragAndDropSidebar = ({ toolboxConfiguration }: { toolboxConfiguration?: Record }) => { const [searchTerm, setSearchTerm] = useState(""); const [isVisible, setIsVisible] = useState(false); const filteredGroups = - toolboxConfiguration?.groups?.map((group) => ({ + toolboxConfiguration?.groups?.map((group: any) => ({ ...group, - steps: group.steps.filter((step) => - step.name.toLowerCase().includes(searchTerm.toLowerCase()) + steps: group?.steps?.filter((step: any) => + step?.name?.toLowerCase().includes(searchTerm?.toLowerCase()) ), })) || []; - const checkForSearchResults = searchTerm && !!filteredGroups?.find((group) => group?.steps?.length>0); + const checkForSearchResults = searchTerm && !!filteredGroups?.find((group:any) => group?.steps?.length>0); return ( - //
    {
    {(isVisible || checkForSearchResults) &&
    {filteredGroups.length > 0 && - filteredGroups.map((group) => ( + filteredGroups.map((group:Record) => ( { ))}
    }
    - //
  • ); }; diff --git a/keep-ui/app/workflows/builder/builder-store.tsx b/keep-ui/app/workflows/builder/builder-store.tsx index 79c16306f..e2812f250 100644 --- a/keep-ui/app/workflows/builder/builder-store.tsx +++ b/keep-ui/app/workflows/builder/builder-store.tsx @@ -1,214 +1,239 @@ -import { create } from "zustand"; -import { v4 as uuidv4 } from "uuid"; -import { - addEdge, - applyNodeChanges, - applyEdgeChanges, - OnNodesChange, - OnEdgesChange, - OnConnect, - Edge, - Node, -} from "@xyflow/react"; + import { create } from "zustand"; + import { v4 as uuidv4 } from "uuid"; + import { + addEdge, + applyNodeChanges, + applyEdgeChanges, + OnNodesChange, + OnEdgesChange, + OnConnect, + Edge, + Node, + } from "@xyflow/react"; -export type V2Properties = Record; + export type V2Properties = Record; -export type V2Step = { - id: string; - name?: string; - componentType: string; - type: string; - properties?: V2Properties; - branches?: { - "true"?: V2Step[]; - "false"?: V2Step[]; + export type V2Step = { + id: string; + name?: string; + componentType: string; + type: string; + properties?: V2Properties; + branches?: { + true?: V2Step[]; + false?: V2Step[]; + }; + sequence?: V2Step[] | V2Step; }; - sequence?: V2Step[] | V2Step; -}; -export type NodeData = Node['data'] & Record; -export type FlowNode = Node & { - prevStepId?: string; - edge_label?: string; - data: NodeData; -}; + export type NodeData = Node["data"] & Record; + export type FlowNode = Node & { + prevStepId?: string; + edge_label?: string; + data: NodeData; + }; -const initialNodes: FlowNode[] = [ - { - id: "a", - position: { x: 0, y: 0 }, - data: { label: "Node A", type: "custom" }, - type: "custom", - }, - { - id: "b", - position: { x: 0, y: 100 }, - data: { label: "Node B", type: "custom" }, - type: "custom", - }, - { - id: "c", - position: { x: 0, y: 200 }, - data: { label: "Node C", type: "custom" }, - type: "custom", - }, -]; + const initialNodes: FlowNode[] = [ + { + id: "a", + position: { x: 0, y: 0 }, + data: { label: "Node A", type: "custom" }, + type: "custom", + }, + { + id: "b", + position: { x: 0, y: 100 }, + data: { label: "Node B", type: "custom" }, + type: "custom", + }, + { + id: "c", + position: { x: 0, y: 200 }, + data: { label: "Node C", type: "custom" }, + type: "custom", + }, + ]; -const initialEdges: Edge[] = [ - { id: "a->b", type: "custom-edge", source: "a", target: "b" }, - { id: "b->c", type: "custom-edge", source: "b", target: "c" }, -]; + const initialEdges: Edge[] = [ + { id: "a->b", type: "custom-edge", source: "a", target: "b" }, + { id: "b->c", type: "custom-edge", source: "b", target: "c" }, + ]; -export type FlowState = { - nodes: FlowNode[]; - edges: Edge[]; - selectedNode: FlowNode | null; - v2Properties: V2Properties; - openGlobalEditor: boolean; - stepEditorOpenForNode: string|null; - onNodesChange: OnNodesChange; - onEdgesChange: OnEdgesChange; - onConnect: OnConnect; - onDragOver: (event: React.DragEvent) => void; - onDrop: ( - event: React.DragEvent, - screenToFlowPosition: (coords: { x: number; y: number }) => { - x: number; - y: number; - } - ) => void; - setNodes: (nodes: FlowNode[]) => void; - setEdges: (edges: Edge[]) => void; - getNodeById: (id: string) => FlowNode | undefined; - hasNode: (id: string) => boolean; - deleteEdges: (ids: string | string[]) => void; - deleteNodes: (ids: string | string[]) => void; - updateNode: (node: FlowNode) => void; - duplicateNode: (node: FlowNode) => void; - // addNode: (node: Partial) => void; - setSelectedNode: (node: FlowNode | null) => void; - setV2Properties: (properties: V2Properties) => void; - setOpneGlobalEditor: (open: boolean) => void; - // updateNodeData: (nodeId: string, key: string, value: any) => void; - updateSelectedNodeData: (key: string, value: any) => void; - updateV2Properties: (key: string, value: any) => void; - setStepEditorOpenForNode: (nodeId: string, open: boolean) => void; -}; + export type FlowState = { + nodes: FlowNode[]; + edges: Edge[]; + selectedNode: FlowNode | null; + v2Properties: V2Properties; + openGlobalEditor: boolean; + stepEditorOpenForNode: string | null; + onNodesChange: OnNodesChange; + onEdgesChange: OnEdgesChange; + onConnect: OnConnect; + onDragOver: (event: React.DragEvent) => void; + onDrop: ( + event: React.DragEvent, + screenToFlowPosition: (coords: { x: number; y: number }) => { + x: number; + y: number; + } + ) => void; + setNodes: (nodes: FlowNode[]) => void; + setEdges: (edges: Edge[]) => void; + getNodeById: (id: string) => FlowNode | undefined; + hasNode: (id: string) => boolean; + deleteEdges: (ids: string | string[]) => void; + deleteNodes: (ids: string | string[]) => void; + updateNode: (node: FlowNode) => void; + duplicateNode: (node: FlowNode) => void; + // addNode: (node: Partial) => void; + setSelectedNode: (node: FlowNode | null) => void; + setV2Properties: (properties: V2Properties) => void; + setOpneGlobalEditor: (open: boolean) => void; + // updateNodeData: (nodeId: string, key: string, value: any) => void; + updateSelectedNodeData: (key: string, value: any) => void; + updateV2Properties: (key: string, value: any) => void; + setStepEditorOpenForNode: (nodeId: string, open: boolean) => void; + updateEdge: (id: string, key: string, value: any) => void; + }; -const useStore = create((set, get) => ({ - nodes: initialNodes, - edges: initialEdges, - selectedNode: null, - v2Properties: {}, - openGlobalEditor: true, - stepEditorOpenForNode: null, - setOpneGlobalEditor: (open) => set({ openGlobalEditor: open }), - updateSelectedNodeData: (key, value) => { - const currentSelectedNode = get().selectedNode; - if (currentSelectedNode) { - const updatedNodes = get().nodes.map((node) => - node.id === currentSelectedNode.id - ? { ...node, data: { ...node.data, [key]: value } } - : node - ); - set({ nodes: updatedNodes, selectedNode: { ...currentSelectedNode, data: { ...currentSelectedNode.data, [key]: value } } }); - } - }, - setV2Properties: (properties) => set({ v2Properties: properties }), - updateV2Properties: (key, value) => { - const updatedProperties = { ...get().v2Properties, [key]: value }; - set({ v2Properties: updatedProperties }); - }, - setSelectedNode: (node) =>{ set({ selectedNode: node }); set({ openGlobalEditor: false }); }, - setStepEditorOpenForNode: (nodeId:string) => { - set({openGlobalEditor: false}); - set({ stepEditorOpenForNode: nodeId }); - }, - onNodesChange: (changes) => - set({ nodes: applyNodeChanges(changes, get().nodes) }), - onEdgesChange: (changes) => - set({ edges: applyEdgeChanges(changes, get().edges) }), - onConnect: (connection) => { - const edge = { ...connection, type: "custom-edge" }; - set({ edges: addEdge(edge, get().edges) }); - }, - onDragOver: (event) => { - event.preventDefault(); - event.dataTransfer.dropEffect = "move"; - }, - onDrop: (event, screenToFlowPosition) => { - event.preventDefault(); - event.stopPropagation(); + const useStore = create((set, get) => ({ + nodes: initialNodes, + edges: initialEdges, + selectedNode: null, + v2Properties: {}, + openGlobalEditor: true, + stepEditorOpenForNode: null, + setOpneGlobalEditor: (open) => set({ openGlobalEditor: open }), + updateSelectedNodeData: (key, value) => { + const currentSelectedNode = get().selectedNode; + if (currentSelectedNode) { + const updatedNodes = get().nodes.map((node) => + node.id === currentSelectedNode.id + ? { ...node, data: { ...node.data, [key]: value } } + : node + ); + set({ + nodes: updatedNodes, + selectedNode: { + ...currentSelectedNode, + data: { ...currentSelectedNode.data, [key]: value }, + }, + }); + } + }, + setV2Properties: (properties) => set({ v2Properties: properties }), + updateV2Properties: (key, value) => { + const updatedProperties = { ...get().v2Properties, [key]: value }; + set({ v2Properties: updatedProperties }); + }, + setSelectedNode: (node) => { + set({ selectedNode: node }); + set({ openGlobalEditor: false }); + }, + setStepEditorOpenForNode: (nodeId: string) => { + set({ openGlobalEditor: false }); + set({ stepEditorOpenForNode: nodeId }); + }, + onNodesChange: (changes) => + set({ nodes: applyNodeChanges(changes, get().nodes) }), + onEdgesChange: (changes) => + set({ edges: applyEdgeChanges(changes, get().edges) }), + onConnect: (connection) => { + const { source, target } = connection; + const sourceNode = get().getNodeById(source); + const targetNode = get().getNodeById(target); + + // Define the connection restrictions + const canConnect = (sourceNode: FlowNode | undefined, targetNode: FlowNode | undefined) => { + if (!sourceNode || !targetNode) return false; + + const sourceType = sourceNode?.data?.componentType; + const targetType = targetNode?.data?.componentType; + + // Restriction logic based on node types + if (sourceType === 'switch') { + return get().edges.filter(edge => edge.source === source).length < 2; + } + if (sourceType === 'foreach' || sourceNode?.data?.type==='foreach') { + return true; + } + return get().edges.filter(edge => edge.source === source).length === 0; + }; + + // Check if the connection is allowed + if (canConnect(sourceNode, targetNode)) { + const edge = { ...connection, type: "custom-edge" }; + set({ edges: addEdge(edge, get().edges) }); + } else { + console.warn('Connection not allowed based on node types'); + } + }, - try { - let step: any = event.dataTransfer.getData("application/reactflow"); - step = JSON.parse(step); - console.log("nodeType=======>", step); - if (!step) return; + onDragOver: (event) => { + event.preventDefault(); + event.dataTransfer.dropEffect = "move"; + }, + onDrop: (event, screenToFlowPosition) => { + event.preventDefault(); + event.stopPropagation(); - // Use the screenToFlowPosition function to get flow coordinates - const position = screenToFlowPosition({ - x: event.clientX, - y: event.clientY, - }); + try { + let step: any = event.dataTransfer.getData("application/reactflow"); + step = JSON.parse(step); + if (!step) return; + // Use the screenToFlowPosition function to get flow coordinates + const position = screenToFlowPosition({ + x: event.clientX, + y: event.clientY, + }); + const newUuid = uuidv4(); + const newNode: FlowNode = { + id: newUuid, + type: "custom", + position, // Use the position object with x and y + data: { + label: step.name! as string, + ...step, + id: newUuid, + }, + }; + + set({ nodes: [...get().nodes, newNode] }); + } catch (err) { + console.error(err); + } + }, + setNodes: (nodes) => set({ nodes }), + setEdges: (edges) => set({ edges }), + hasNode: (id) => !!get().nodes.find((node) => node.id === id), + getNodeById: (id) => get().nodes.find((node) => node.id === id), + deleteEdges: (ids) => { + const idArray = Array.isArray(ids) ? ids : [ids]; + set({ edges: get().edges.filter((edge) => !idArray.includes(edge.id)) }); + }, + deleteNodes: (ids) => { + const idArray = Array.isArray(ids) ? ids : [ids]; + set({ nodes: get().nodes.filter((node) => !idArray.includes(node.id)) }); + }, + updateEdge: (id: string, key: string, value: any) => { + const edge = get().edges.find((e) => e.id === id); + if (!edge) return; + const newEdge = { ...edge, [key]: value }; + set({ edges: get().edges.map((e) => (e.id === edge.id ? newEdge : e)) }); + }, + updateNode: (node) => + set({ nodes: get().nodes.map((n) => (n.id === node.id ? node : n)) }), + duplicateNode: (node) => { + const { data, position } = node; const newUuid = uuidv4(); const newNode: FlowNode = { + ...node, + data: { ...data, id: newUuid }, id: newUuid, - type: "custom", - position, // Use the position object with x and y - data: { - label: step.name! as string, - ...step, - id: newUuid, - }, + position: { x: position.x + 100, y: position.y + 100 }, }; - set({ nodes: [...get().nodes, newNode] }); - } catch (err) { - console.error(err); - } - }, - setNodes: (nodes) => set({ nodes }), - setEdges: (edges) => set({ edges }), - hasNode: (id) => !!get().nodes.find((node) => node.id === id), - getNodeById: (id) => get().nodes.find((node) => node.id === id), - deleteEdges: (ids) => { - const idArray = Array.isArray(ids) ? ids : [ids]; - set({ edges: get().edges.filter((edge) => !idArray.includes(edge.id)) }); - }, - deleteNodes: (ids) => { - const idArray = Array.isArray(ids) ? ids : [ids]; - set({ nodes: get().nodes.filter((node) => !idArray.includes(node.id)) }); - }, - updateNode: (node) => - set({ nodes: get().nodes.map((n) => (n.id === node.id ? node : n)) }), - duplicateNode: (node) => { - const { data, position } = node; - const newUuid = uuidv4(); - const newNode: FlowNode = { - ...node, - data: { ...data, id: newUuid }, - id: newUuid, - position: { x: position.x + 100, y: position.y + 100 }, - }; - set({ nodes: [...get().nodes, newNode] }); - }, - // addNode: (node) => { - // const newUuid = uuidv4(); - // const newNode: FlowNode = { - // ...node, - // id: newUuid, - // type: "custom", - // data: { - // type: "custom", - // componentType: "custom", - // name: "custom", - // ...(node?.data ?? {}), - // id: newUuid, - // }, - // }; - // set({ nodes: [...get().nodes, newNode] }); - // }, -})); + }, + })); -export default useStore; + export default useStore; diff --git a/keep-ui/app/workflows/builder/editors.tsx b/keep-ui/app/workflows/builder/editors.tsx index bbfca0ada..0863d8fa1 100644 --- a/keep-ui/app/workflows/builder/editors.tsx +++ b/keep-ui/app/workflows/builder/editors.tsx @@ -7,6 +7,7 @@ import { Subtitle, Icon, Button, + Switch, } from "@tremor/react"; import { KeyIcon } from "@heroicons/react/20/solid"; import { Properties } from "sequential-workflow-designer"; @@ -23,7 +24,7 @@ import { HandRaisedIcon, } from "@heroicons/react/24/outline"; import useStore, { V2Properties } from "./builder-store"; -import { useEffect } from "react"; +import { useEffect, useState } from "react"; function EditorLayout({ children }: { children: React.ReactNode }) { return
    {children}
    ; @@ -51,7 +52,6 @@ export function GlobalEditor() { export function GlobalEditorV2() { const { v2Properties:properties, updateV2Properties: setProperty } = useStore(); - console.log("properties========>", properties) return ( Keep Workflow Editor @@ -417,16 +417,16 @@ export function StepEditorV2({ }: { installedProviders?: Provider[] | undefined | null; }) { + const [useGlobalEditor, setGlobalEditor] = useState(false); const { selectedNode, updateSelectedNodeData, + setOpneGlobalEditor } = useStore() - console.log("selectedNode======>in editor", selectedNode); const {data} = selectedNode || {}; const {name, type, properties} = data || {}; - console.log("properties======>in step editor", properties) function onNameChanged(e: any) { updateSelectedNodeData( "name", e.target.value); } @@ -443,8 +443,27 @@ export function StepEditorV2({ } + const handleSwitchChange = (value:boolean)=>{ + setGlobalEditor(value); + setOpneGlobalEditor(true); + } + return ( +
    + + +
    {providerType} Editor Unique Identifier in step editor", properties) const providerType = type.split("-")[1]; diff --git a/keep-ui/utils/hooks/useWorkflowInitialization.ts b/keep-ui/utils/hooks/useWorkflowInitialization.ts index b4b471b83..f3b54ceb9 100644 --- a/keep-ui/utils/hooks/useWorkflowInitialization.ts +++ b/keep-ui/utils/hooks/useWorkflowInitialization.ts @@ -5,19 +5,19 @@ import { useRef, useCallback, } from "react"; -import { Connection, Edge, Node, Position, useReactFlow } from "@xyflow/react"; +import { Edge, Position, useReactFlow } from "@xyflow/react"; import dagre from "dagre"; import { parseWorkflow, generateWorkflow } from "app/workflows/builder/utils"; import { v4 as uuidv4 } from "uuid"; import { useSearchParams } from "next/navigation"; import useStore from "../../app/workflows/builder/builder-store"; import { FlowNode } from "../../app/workflows/builder/builder-store"; -import { Properties } from 'sequential-workflow-designer'; +import { Provider } from "app/providers/providers"; const useWorkflowInitialization = ( - workflow: string, - loadedAlertFile: string, - providers: any[] + workflow: string | undefined, + loadedAlertFile: string | null | undefined, + providers: Provider[] ) => { const { nodes, @@ -48,7 +48,7 @@ const useWorkflowInitialization = ( const { screenToFlowPosition } = useReactFlow(); const handleDrop = useCallback( - (event) => { + (event: React.DragEvent) => { onDrop(event, screenToFlowPosition); }, [screenToFlowPosition] @@ -103,7 +103,6 @@ const useWorkflowInitialization = ( parsedWorkflow = parseWorkflow(loadedAlertFile, providers); } - console.log("parsedWorkflow=======>", parsedWorkflow); setV2Properties(parsedWorkflow?.properties ?? {}); let newNodes = processWorkflow(parsedWorkflow.sequence); let newEdges = newEdgesFromNodes(newNodes); @@ -239,6 +238,25 @@ const useWorkflowInitialization = ( } newNodes = [...newNodes, ...trueSubflowNodes, ...falseSubflowNodes]; + } else if (step.componentType === "container" && step.type === "foreach") { + const forEachhNode = { + id: nodeId, + type: "custom", + position: { x: 0, y: 0 }, + data: { + label: step.name, + ...step, + }, + prevStepId: prevStepId, + // extent: 'parent', + } as FlowNode; + newNodes.push(forEachhNode); + + const sequences: FlowNode[] = processWorkflow( + step?.sequence || [], + nodeId + ); + newNodes = [...newNodes, ...sequences]; } else { newNode = { id: nodeId, From 53e86ac4f8fe1f8f3b18713bad3505a9e7e52ef6 Mon Sep 17 00:00:00 2001 From: Bhavya Jain Date: Sun, 4 Aug 2024 12:57:40 +0530 Subject: [PATCH 07/55] Initial UI changes --- .../workflows/builder/ReactFlowBuilder.tsx | 42 ++++++++++--------- .../app/workflows/builder/ReactFlowEditor.tsx | 8 ++-- keep-ui/app/workflows/builder/builder.tsx | 2 +- keep-ui/app/workflows/builder/page.css | 2 +- 4 files changed, 28 insertions(+), 26 deletions(-) diff --git a/keep-ui/app/workflows/builder/ReactFlowBuilder.tsx b/keep-ui/app/workflows/builder/ReactFlowBuilder.tsx index bfd9d98f1..fd4a0aada 100644 --- a/keep-ui/app/workflows/builder/ReactFlowBuilder.tsx +++ b/keep-ui/app/workflows/builder/ReactFlowBuilder.tsx @@ -34,26 +34,28 @@ const ReactFlowBuilder = ({ } = useWorkflowInitialization(workflow, loadedAlertFile, providers); return ( -
    - - {!isLoading && ( - - - - - )} - +
    +
    + + {!isLoading && ( + + + + + )} + +
    ); }; diff --git a/keep-ui/app/workflows/builder/ReactFlowEditor.tsx b/keep-ui/app/workflows/builder/ReactFlowEditor.tsx index 5d105130e..9526eb820 100644 --- a/keep-ui/app/workflows/builder/ReactFlowEditor.tsx +++ b/keep-ui/app/workflows/builder/ReactFlowEditor.tsx @@ -16,13 +16,13 @@ const ReactFlowEditor = () => { return (
    {!isOpen && ( -
    +
    {openGlobalEditor && } {!openGlobalEditor && selectedNode && } diff --git a/keep-ui/app/workflows/builder/builder.tsx b/keep-ui/app/workflows/builder/builder.tsx index be8f06baf..b06d13b45 100644 --- a/keep-ui/app/workflows/builder/builder.tsx +++ b/keep-ui/app/workflows/builder/builder.tsx @@ -354,7 +354,7 @@ function Builder({ <> {getworkflowStatus()} {useReactFlow && ( -
    +
    Date: Sun, 4 Aug 2024 21:29:34 +0530 Subject: [PATCH 08/55] feat: add non draggable functionality to the node. --- keep-ui/app/workflows/builder/CustomNode.tsx | 11 +++------ .../app/workflows/builder/builder-store.tsx | 24 +++++++++++++++---- .../utils/hooks/useWorkflowInitialization.ts | 6 +++++ 3 files changed, 29 insertions(+), 12 deletions(-) diff --git a/keep-ui/app/workflows/builder/CustomNode.tsx b/keep-ui/app/workflows/builder/CustomNode.tsx index de25f8d8b..02ed03731 100644 --- a/keep-ui/app/workflows/builder/CustomNode.tsx +++ b/keep-ui/app/workflows/builder/CustomNode.tsx @@ -14,8 +14,7 @@ function IconUrlProvider(data: FlowNode["data"]) { ?.replace("condition-", "")}-icon.png`; } -function CustomNode({ data, id }: { data: FlowNode["data"]; id: string }) { - console.log("entering this CustomNode", data, id); +function CustomNode({ id, data}: FlowNode) { const { getNodeById, selectedNode, setSelectedNode } = useStore(); const currentNode = getNodeById(id); const type = data?.type @@ -24,10 +23,7 @@ function CustomNode({ data, id }: { data: FlowNode["data"]; id: string }) { ?.replace("condition-", ""); if (!currentNode) return null; - - console.log("selectedNode", selectedNode); - console.log("currentNode", currentNode); - + const isDraggable = currentNode?.isDraggable; return ( <> @@ -37,10 +33,9 @@ function CustomNode({ data, id }: { data: FlowNode["data"]; id: string }) { currentNode?.id === selectedNode?.id ? "border-orange-500" : "border-stone-400" - }`} + }${isDraggable ? " custom-drag-handle" : ""}`} onClick={(e) => { e.stopPropagation(); - console.log("before setting", currentNode); setSelectedNode(currentNode); }} > diff --git a/keep-ui/app/workflows/builder/builder-store.tsx b/keep-ui/app/workflows/builder/builder-store.tsx index e2812f250..edbcf83cb 100644 --- a/keep-ui/app/workflows/builder/builder-store.tsx +++ b/keep-ui/app/workflows/builder/builder-store.tsx @@ -31,6 +31,7 @@ prevStepId?: string; edge_label?: string; data: NodeData; + isDraggable?: boolean; }; const initialNodes: FlowNode[] = [ @@ -92,7 +93,7 @@ // updateNodeData: (nodeId: string, key: string, value: any) => void; updateSelectedNodeData: (key: string, value: any) => void; updateV2Properties: (key: string, value: any) => void; - setStepEditorOpenForNode: (nodeId: string, open: boolean) => void; + setStepEditorOpenForNode: (nodeId: string) => void; updateEdge: (id: string, key: string, value: any) => void; }; @@ -157,13 +158,23 @@ if (sourceType === 'foreach' || sourceNode?.data?.type==='foreach') { return true; } - return get().edges.filter(edge => edge.source === source).length === 0; + return (get().edges.filter(edge => edge.source === source).length === 0 && + get().edges.filter(edge => edge.target === target).length === 0); }; // Check if the connection is allowed if (canConnect(sourceNode, targetNode)) { const edge = { ...connection, type: "custom-edge" }; set({ edges: addEdge(edge, get().edges) }); + set({nodes: get().nodes.map(node =>{ + if(node.id === target){ + return { ...node, prevStepId: source, isDraggable: false}; + } + if(node.id === source){ + return { ...node, isDraggable: false}; + } + return node; + })}); } else { console.warn('Connection not allowed based on node types'); } @@ -194,8 +205,10 @@ data: { label: step.name! as string, ...step, - id: newUuid, + id: newUuid }, + isDraggable: true, + dragHandle: '.custom-drag-handle', }; set({ nodes: [...get().nodes, newNode] }); @@ -228,9 +241,12 @@ const newUuid = uuidv4(); const newNode: FlowNode = { ...node, - data: { ...data, id: newUuid }, + data: { ...data, id: newUuid, + }, + isDraggable: true, id: newUuid, position: { x: position.x + 100, y: position.y + 100 }, + dragHandle: '.custom-drag-handle' }; set({ nodes: [...get().nodes, newNode] }); }, diff --git a/keep-ui/utils/hooks/useWorkflowInitialization.ts b/keep-ui/utils/hooks/useWorkflowInitialization.ts index f3b54ceb9..ffbce5e5a 100644 --- a/keep-ui/utils/hooks/useWorkflowInitialization.ts +++ b/keep-ui/utils/hooks/useWorkflowInitialization.ts @@ -212,6 +212,8 @@ const useWorkflowInitialization = ( label: step.name, ...step, }, + isDraggable: false, + dragHandle: '.custom-drag-handle', prevStepId: prevStepId, // extent: 'parent', } as FlowNode; @@ -242,11 +244,13 @@ const useWorkflowInitialization = ( const forEachhNode = { id: nodeId, type: "custom", + dragHandle: '.custom-drag-handle', position: { x: 0, y: 0 }, data: { label: step.name, ...step, }, + isDraggable: false, prevStepId: prevStepId, // extent: 'parent', } as FlowNode; @@ -261,11 +265,13 @@ const useWorkflowInitialization = ( newNode = { id: nodeId, type: "custom", + dragHandle: '.custom-drag-handle', position, data: { label: step.name, ...step, }, + isDraggable: false, prevStepId: prevStepId, // parentId: parentId, } as FlowNode; From 84ceed2699b80ce11aa3b6bc2df650e48151fc81 Mon Sep 17 00:00:00 2001 From: Rajesh Jonnalagadda Date: Thu, 8 Aug 2024 05:33:34 +0530 Subject: [PATCH 09/55] refactor:using elkks to layout the workflow and also added auto appending and auto connection to source node on deletion and refactor of useworkflowinitialising --- keep-ui/app/workflows/builder/CustomEdge.tsx | 65 +- keep-ui/app/workflows/builder/CustomNode.tsx | 105 ++-- keep-ui/app/workflows/builder/NodeMenu.tsx | 33 +- .../workflows/builder/ReactFlowBuilder.tsx | 30 +- keep-ui/app/workflows/builder/ToolBox.tsx | 79 ++- .../app/workflows/builder/builder-store.tsx | 567 ++++++++++-------- keep-ui/app/workflows/builder/builder.tsx | 2 + keep-ui/app/workflows/builder/editors.tsx | 7 +- keep-ui/package-lock.json | 6 + keep-ui/package.json | 1 + .../utils/hooks/useWorkflowInitialization.ts | 397 ++++++------ keep-ui/utils/reactFlow.ts | 309 ++++++++++ 12 files changed, 1021 insertions(+), 580 deletions(-) create mode 100644 keep-ui/utils/reactFlow.ts diff --git a/keep-ui/app/workflows/builder/CustomEdge.tsx b/keep-ui/app/workflows/builder/CustomEdge.tsx index ef3eba24b..f377f0553 100644 --- a/keep-ui/app/workflows/builder/CustomEdge.tsx +++ b/keep-ui/app/workflows/builder/CustomEdge.tsx @@ -2,13 +2,17 @@ import React from "react"; import { BaseEdge, EdgeLabelRenderer, getSmoothStepPath } from "@xyflow/react"; import type { Edge, EdgeProps } from "@xyflow/react"; import useStore from "./builder-store"; +import { CiSquarePlus } from "react-icons/ci"; +import { Button } from "@tremor/react"; +import '@xyflow/react/dist/style.css'; interface CustomEdgeProps extends EdgeProps { label?: string; type?: string; + data?: any; } -const CustomEdge:React.FC = ({ +const CustomEdge: React.FC = ({ id, sourceX, sourceY, @@ -17,8 +21,9 @@ const CustomEdge:React.FC = ({ label, source, target, + data, }: CustomEdgeProps) => { - const { deleteEdges, getNodeById, edges, updateEdge } = useStore(); + const { deleteEdges, edges, setSelectedEdge, selectedEdge } = useStore(); // Calculate the path and midpoint const [edgePath, labelX, labelY] = getSmoothStepPath({ @@ -32,38 +37,25 @@ const CustomEdge:React.FC = ({ const midpointX = (sourceX + targetX) / 2; const midpointY = (sourceY + targetY) / 2; - const handleDelete = (e: React.MouseEvent) => { - e.stopPropagation(); - e.preventDefault(); - deleteEdges(id); - }; + // const handleDelete = (e: React.MouseEvent) => { + // e.stopPropagation(); + // e.preventDefault(); + // deleteEdges(id); + // }; - const sourceNode = getNodeById(source); - const targetNode = getNodeById(target); let dynamicLabel = label; - const componentType = sourceNode?.data?.componentType || ""; - if (!dynamicLabel && sourceNode && targetNode && componentType == "switch") { - const existEdge = edges?.find( - ({ source, id: edgeId }: Edge) => - edgeId !== id && source === sourceNode.id - ); + const isLayouted = !!data?.isLayouted; - if (!existEdge) { - dynamicLabel = "True"; - updateEdge(id, "label", dynamicLabel); - } - if (existEdge && existEdge.label) { - dynamicLabel = existEdge.label === "True" ? "False" : "True"; - updateEdge(id, "label", dynamicLabel); - } - } + const color = dynamicLabel === "True" ? "left-0 bg-green-500" : dynamicLabel === "False" ? "bg-red-500" : "bg-orange-500"; return ( <> = ({ @@ -86,31 +79,39 @@ const CustomEdge:React.FC = ({ id={id} path={edgePath} className="stroke-gray-700 stroke-2" - style={{ markerEnd: `url(#arrow-${id})` }} // Add arrowhead + style={{ + markerEnd: `url(#arrow-${id})`, + opacity: isLayouted ? 1 : 0 + }} // Add arrowhead /> {!!dynamicLabel && (
    {dynamicLabel}
    )} - + +
    ); diff --git a/keep-ui/app/workflows/builder/CustomNode.tsx b/keep-ui/app/workflows/builder/CustomNode.tsx index 02ed03731..8ccfbd904 100644 --- a/keep-ui/app/workflows/builder/CustomNode.tsx +++ b/keep-ui/app/workflows/builder/CustomNode.tsx @@ -1,9 +1,10 @@ -import React, { memo, useEffect } from "react"; -import { Handle, Position } from "@xyflow/react"; +import React, { memo, useEffect, useState } from "react"; +import { Handle, NodeToolbar, Position } from "@xyflow/react"; import NodeMenu from "./NodeMenu"; import useStore, { FlowNode } from "./builder-store"; import Image from "next/image"; -import { Properties } from "sequential-workflow-designer"; +import { GoPlus } from "react-icons/go"; + function IconUrlProvider(data: FlowNode["data"]) { const { componentType, type } = data || {}; @@ -14,66 +15,68 @@ function IconUrlProvider(data: FlowNode["data"]) { ?.replace("condition-", "")}-icon.png`; } -function CustomNode({ id, data}: FlowNode) { - const { getNodeById, selectedNode, setSelectedNode } = useStore(); - const currentNode = getNodeById(id); +function CustomNode({ id, data }: FlowNode) { + const { selectedNode, setSelectedNode } = useStore(); const type = data?.type ?.replace("step-", "") ?.replace("action-", "") ?.replace("condition-", ""); - if (!currentNode) return null; - const isDraggable = currentNode?.isDraggable; + const isEmptyNode = !!data?.type?.includes("empty"); + const isLayouted = !!data?.isLayouted; return ( <> - {!!currentNode && ( -
    { - e.stopPropagation(); - setSelectedNode(currentNode); - }} +
    { + e.stopPropagation(); + setSelectedNode(id); + }} + style={{ opacity: data.isLayouted ? 1 : 0 }} + > + {isEmptyNode &&
    - {data?.type !== "sub_flow" && ( -
    -
    - {data?.type} -
    -
    -
    {data?.name}
    -
    - {type || data?.componentType} -
    -
    -
    - + +
    } + {!isEmptyNode && data?.type !== "sub_flow" && ( +
    + {data?.type} +
    +
    {data?.name}
    +
    + {type || data?.componentType}
    - )} +
    + +
    +
    + )} + + + +
    - - -
    - )} ); } diff --git a/keep-ui/app/workflows/builder/NodeMenu.tsx b/keep-ui/app/workflows/builder/NodeMenu.tsx index 016af9962..8823f9cbd 100644 --- a/keep-ui/app/workflows/builder/NodeMenu.tsx +++ b/keep-ui/app/workflows/builder/NodeMenu.tsx @@ -3,23 +3,20 @@ import { Fragment } from "react"; import { CiSquareChevDown } from "react-icons/ci"; import { TrashIcon } from "@heroicons/react/24/outline"; import useStore, { FlowNode } from "./builder-store"; -import { HiOutlineDuplicate } from "react-icons/hi"; import { IoMdSettings } from "react-icons/io"; -interface NodeMenuProps { - node: FlowNode; -} - -export default function NodeMenu({ node }: NodeMenuProps) { +export default function NodeMenu({ data, id }: { data: FlowNode["data"], id: string }) { const stopPropagation = (e: React.MouseEvent) => { e.stopPropagation(); }; + const isEmptyNode = data?.type?.includes("empty") + const { deleteNodes, duplicateNode, setSelectedNode, setStepEditorOpenForNode } = useStore(); return ( <> - {node && ( + {data && !isEmptyNode && (
    { stopPropagation(e); - deleteNodes(node.id); + deleteNodes(id); }} - className={`${ - active ? "bg-slate-200" : "text-gray-900" - } group flex w-full items-center rounded-md px-2 py-2 text-xs`} + className={`${active ? "bg-slate-200" : "text-gray-900" + } group flex w-full items-center rounded-md px-2 py-2 text-xs`} >
    - {(isVisible || checkForSearchResults) &&
    + {(isVisible || checkForSearchResults) &&
    {filteredGroups.length > 0 && - filteredGroups.map((group:Record) => ( + filteredGroups.map((group: Record) => ( ))}
    } diff --git a/keep-ui/app/workflows/builder/builder-store.tsx b/keep-ui/app/workflows/builder/builder-store.tsx index edbcf83cb..371c9e55b 100644 --- a/keep-ui/app/workflows/builder/builder-store.tsx +++ b/keep-ui/app/workflows/builder/builder-store.tsx @@ -1,255 +1,346 @@ - import { create } from "zustand"; - import { v4 as uuidv4 } from "uuid"; - import { - addEdge, - applyNodeChanges, - applyEdgeChanges, - OnNodesChange, - OnEdgesChange, - OnConnect, - Edge, - Node, - } from "@xyflow/react"; - - export type V2Properties = Record; - - export type V2Step = { - id: string; - name?: string; - componentType: string; - type: string; - properties?: V2Properties; - branches?: { - true?: V2Step[]; - false?: V2Step[]; - }; - sequence?: V2Step[] | V2Step; - }; +import { create } from "zustand"; +import { v4 as uuidv4 } from "uuid"; +import { + addEdge, + applyNodeChanges, + applyEdgeChanges, + OnNodesChange, + OnEdgesChange, + OnConnect, + Edge, + Node, +} from "@xyflow/react"; + +import { processStepV2, handleNextEdge, processWorkflowV2 } from "utils/reactFlow"; - export type NodeData = Node["data"] & Record; - export type FlowNode = Node & { - prevStepId?: string; - edge_label?: string; - data: NodeData; - isDraggable?: boolean; +export type V2Properties = Record; + +export type V2Step = { + id: string; + name?: string; + componentType: string; + type: string; + properties?: V2Properties; + branches?: { + true?: V2Step[]; + false?: V2Step[]; }; + sequence?: V2Step[] | V2Step; +}; + +export type NodeData = Node["data"] & Record; + +export type NodeStepMeta = { id: string, label?: string }; +export type FlowNode = Node & { + prevStepId?: string | string[]; + edge_label?: string; + data: NodeData; + isDraggable?: boolean; + nextStepId?: string | string[]; + prevStep?: NodeStepMeta[] | NodeStepMeta | null; + nextStep?: NodeStepMeta[] | NodeStepMeta | null; + prevNodeId?: string | null; + nextNodeId?: string | null; + id:string; +}; + +const initialNodes: FlowNode[] = [ + { + id: "a", + position: { x: 0, y: 0 }, + data: { label: "Node A", type: "custom" }, + type: "custom", + }, + { + id: "b", + position: { x: 0, y: 100 }, + data: { label: "Node B", type: "custom" }, + type: "custom", + }, + { + id: "c", + position: { x: 0, y: 200 }, + data: { label: "Node C", type: "custom" }, + type: "custom", + }, +]; + +const initialEdges: Edge[] = [ + { id: "a->b", type: "custom-edge", source: "a", target: "b" }, + { id: "b->c", type: "custom-edge", source: "b", target: "c" }, +]; - const initialNodes: FlowNode[] = [ - { - id: "a", - position: { x: 0, y: 0 }, - data: { label: "Node A", type: "custom" }, - type: "custom", - }, - { - id: "b", - position: { x: 0, y: 100 }, - data: { label: "Node B", type: "custom" }, - type: "custom", - }, - { - id: "c", - position: { x: 0, y: 200 }, - data: { label: "Node C", type: "custom" }, - type: "custom", - }, - ]; - const initialEdges: Edge[] = [ - { id: "a->b", type: "custom-edge", source: "a", target: "b" }, - { id: "b->c", type: "custom-edge", source: "b", target: "c" }, + +export type FlowState = { + nodes: FlowNode[]; + edges: Edge[]; + selectedNode: string | null; + v2Properties: V2Properties; + openGlobalEditor: boolean; + stepEditorOpenForNode: string | null; + toolboxConfiguration: Record; + onNodesChange: OnNodesChange; + onEdgesChange: OnEdgesChange; + onConnect: OnConnect; + onDragOver: (event: React.DragEvent) => void; + onDrop: ( + event: React.DragEvent, + screenToFlowPosition: (coords: { x: number; y: number }) => { + x: number; + y: number; + } + ) => void; + setNodes: (nodes: FlowNode[]) => void; + setEdges: (edges: Edge[]) => void; + getNodeById: (id: string | null) => FlowNode | undefined; + hasNode: (id: string) => boolean; + deleteEdges: (ids: string | string[]) => void; + deleteNodes: (ids: string | string[]) => void; + updateNode: (node: FlowNode) => void; + duplicateNode: (node: FlowNode) => void; + // addNode: (node: Partial) => void; + setSelectedNode: (id: string | null) => void; + setV2Properties: (properties: V2Properties) => void; + setOpneGlobalEditor: (open: boolean) => void; + // updateNodeData: (nodeId: string, key: string, value: any) => void; + updateSelectedNodeData: (key: string, value: any) => void; + updateV2Properties: (key: string, value: any) => void; + setStepEditorOpenForNode: (nodeId: string | null) => void; + updateEdge: (id: string, key: string, value: any) => void; + setToolBoxConfig: (config: Record) => void; + addNodeBetween: (nodeOrEdge: string | null, step: V2Step, type: string) => void; + isLayouted: boolean; + setIsLayouted: (isLayouted: boolean) => void; + selectedEdge: string | null; + setSelectedEdge: (id: string | null) => void; + getEdgeById: (id: string) => Edge | undefined; +}; + + +export type StoreGet = () => FlowState +export type StoreSet = (state: FlowState | Partial | ((state: FlowState) => FlowState | Partial)) => void + +function addNodeBetween(nodeOrEdge: string|null, step: any, type: string, set: StoreSet, get: StoreGet) { + if (!nodeOrEdge || !step) return; + let edge = {} as Edge; + if (type === 'node') { + edge = get().edges.find((edge) => edge.target === nodeOrEdge) as Edge + } + + if (type === 'edge') { + edge = get().edges.find((edge) => edge.id === nodeOrEdge) as Edge; + } + + const { source: sourceId, target: targetId } = edge || {}; + if (!sourceId || !targetId) return; + + const newNodeId = uuidv4(); + const newStep = { ...step, id: newNodeId } + let { nodes, edges } = processWorkflowV2([ + { id: sourceId, type: 'temp_node', name: 'temp_node', 'componentType': 'temp_node' }, + newStep, + { id: targetId, type: 'temp_node', name: 'temp_node', 'componentType': 'temp_node', edgeNotNeeded: true } + ], { x: 0, y: 0 }, true); + console.log("new nodes, edges", nodes, edges); + const newEdges = [ + ...edges, + ...(get().edges.filter(edge => !(edge.source == sourceId && edge.target == targetId)) || []), ]; + set({ + edges: newEdges, + nodes: [...get().nodes, ...nodes], + isLayouted: false, + }); + if (type == 'edge') { + set({ selectedEdge: edges[edges.length - 1]?.id }); + } - export type FlowState = { - nodes: FlowNode[]; - edges: Edge[]; - selectedNode: FlowNode | null; - v2Properties: V2Properties; - openGlobalEditor: boolean; - stepEditorOpenForNode: string | null; - onNodesChange: OnNodesChange; - onEdgesChange: OnEdgesChange; - onConnect: OnConnect; - onDragOver: (event: React.DragEvent) => void; - onDrop: ( - event: React.DragEvent, - screenToFlowPosition: (coords: { x: number; y: number }) => { - x: number; - y: number; - } - ) => void; - setNodes: (nodes: FlowNode[]) => void; - setEdges: (edges: Edge[]) => void; - getNodeById: (id: string) => FlowNode | undefined; - hasNode: (id: string) => boolean; - deleteEdges: (ids: string | string[]) => void; - deleteNodes: (ids: string | string[]) => void; - updateNode: (node: FlowNode) => void; - duplicateNode: (node: FlowNode) => void; - // addNode: (node: Partial) => void; - setSelectedNode: (node: FlowNode | null) => void; - setV2Properties: (properties: V2Properties) => void; - setOpneGlobalEditor: (open: boolean) => void; - // updateNodeData: (nodeId: string, key: string, value: any) => void; - updateSelectedNodeData: (key: string, value: any) => void; - updateV2Properties: (key: string, value: any) => void; - setStepEditorOpenForNode: (nodeId: string) => void; - updateEdge: (id: string, key: string, value: any) => void; - }; + if (type === 'node') { + set({ selectedNode: nodeOrEdge }); + } + +} + +const useStore = create((set, get) => ({ + nodes: [], + edges: [], + selectedNode: null, + v2Properties: {}, + openGlobalEditor: true, + stepEditorOpenForNode: null, + toolboxConfiguration: {} as Record, + isLayouted: false, + selectedEdge: null, + setSelectedEdge: (id) => set({ selectedEdge: id, selectedNode: null }), + setIsLayouted: (isLayouted) => set({ isLayouted }), + getEdgeById: (id) => get().edges.find((edge) => edge.id === id), + addNodeBetween: (nodeOrEdge: string|null, step: any, type: string) => { + addNodeBetween(nodeOrEdge, step, type, set, get); + }, + setToolBoxConfig: (config) => set({ toolboxConfiguration: config }), + setOpneGlobalEditor: (open) => set({ openGlobalEditor: open }), + updateSelectedNodeData: (key, value) => { + const currentSelectedNode = get().selectedNode; + if (currentSelectedNode) { + const updatedNodes = get().nodes.map((node) => + node.id === currentSelectedNode + ? { ...node, data: { ...node.data, [key]: value } } + : node + ); + set({ + nodes: updatedNodes + }); + } + }, + setV2Properties: (properties) => set({ v2Properties: properties }), + updateV2Properties: (key, value) => { + const updatedProperties = { ...get().v2Properties, [key]: value }; + set({ v2Properties: updatedProperties }); + }, + setSelectedNode: (id) => { + set({ + selectedNode: id || null, + openGlobalEditor: false, + selectedEdge: null + }); + }, + setStepEditorOpenForNode: (nodeId) => { + set({ openGlobalEditor: false }); + set({ stepEditorOpenForNode: nodeId }); + }, + onNodesChange: (changes) => + set({ nodes: applyNodeChanges(changes, get().nodes) }), + onEdgesChange: (changes) => + set({ edges: applyEdgeChanges(changes, get().edges) }), + onConnect: (connection) => { + const { source, target } = connection; + const sourceNode = get().getNodeById(source); + const targetNode = get().getNodeById(target); + + // Define the connection restrictions + const canConnect = (sourceNode: FlowNode | undefined, targetNode: FlowNode | undefined) => { + if (!sourceNode || !targetNode) return false; - const useStore = create((set, get) => ({ - nodes: initialNodes, - edges: initialEdges, - selectedNode: null, - v2Properties: {}, - openGlobalEditor: true, - stepEditorOpenForNode: null, - setOpneGlobalEditor: (open) => set({ openGlobalEditor: open }), - updateSelectedNodeData: (key, value) => { - const currentSelectedNode = get().selectedNode; - if (currentSelectedNode) { - const updatedNodes = get().nodes.map((node) => - node.id === currentSelectedNode.id - ? { ...node, data: { ...node.data, [key]: value } } - : node - ); - set({ - nodes: updatedNodes, - selectedNode: { - ...currentSelectedNode, - data: { ...currentSelectedNode.data, [key]: value }, - }, - }); + const sourceType = sourceNode?.data?.componentType; + const targetType = targetNode?.data?.componentType; + + // Restriction logic based on node types + if (sourceType === 'switch') { + return get().edges.filter(edge => edge.source === source).length < 2; } - }, - setV2Properties: (properties) => set({ v2Properties: properties }), - updateV2Properties: (key, value) => { - const updatedProperties = { ...get().v2Properties, [key]: value }; - set({ v2Properties: updatedProperties }); - }, - setSelectedNode: (node) => { - set({ selectedNode: node }); - set({ openGlobalEditor: false }); - }, - setStepEditorOpenForNode: (nodeId: string) => { - set({ openGlobalEditor: false }); - set({ stepEditorOpenForNode: nodeId }); - }, - onNodesChange: (changes) => - set({ nodes: applyNodeChanges(changes, get().nodes) }), - onEdgesChange: (changes) => - set({ edges: applyEdgeChanges(changes, get().edges) }), - onConnect: (connection) => { - const { source, target } = connection; - const sourceNode = get().getNodeById(source); - const targetNode = get().getNodeById(target); - - // Define the connection restrictions - const canConnect = (sourceNode: FlowNode | undefined, targetNode: FlowNode | undefined) => { - if (!sourceNode || !targetNode) return false; - - const sourceType = sourceNode?.data?.componentType; - const targetType = targetNode?.data?.componentType; - - // Restriction logic based on node types - if (sourceType === 'switch') { - return get().edges.filter(edge => edge.source === source).length < 2; - } - if (sourceType === 'foreach' || sourceNode?.data?.type==='foreach') { - return true; - } - return (get().edges.filter(edge => edge.source === source).length === 0 && - get().edges.filter(edge => edge.target === target).length === 0); - }; - - // Check if the connection is allowed - if (canConnect(sourceNode, targetNode)) { - const edge = { ...connection, type: "custom-edge" }; - set({ edges: addEdge(edge, get().edges) }); - set({nodes: get().nodes.map(node =>{ - if(node.id === target){ - return { ...node, prevStepId: source, isDraggable: false}; + if (sourceType === 'foreach' || sourceNode?.data?.type === 'foreach') { + return true; + } + return (get().edges.filter(edge => edge.source === source).length === 0); + }; + + // Check if the connection is allowed + if (canConnect(sourceNode, targetNode)) { + const edge = { ...connection, type: "custom-edge" }; + set({ edges: addEdge(edge, get().edges) }); + set({ + nodes: get().nodes.map(node => { + if (node.id === target) { + return { ...node, prevStepId: source, isDraggable: false }; } - if(node.id === source){ - return { ...node, isDraggable: false}; + if (node.id === source) { + return { ...node, isDraggable: false }; } return node; - })}); - } else { - console.warn('Connection not allowed based on node types'); - } - }, - - onDragOver: (event) => { - event.preventDefault(); - event.dataTransfer.dropEffect = "move"; - }, - onDrop: (event, screenToFlowPosition) => { - event.preventDefault(); - event.stopPropagation(); - - try { - let step: any = event.dataTransfer.getData("application/reactflow"); - step = JSON.parse(step); - if (!step) return; - // Use the screenToFlowPosition function to get flow coordinates - const position = screenToFlowPosition({ - x: event.clientX, - y: event.clientY, - }); - const newUuid = uuidv4(); - const newNode: FlowNode = { - id: newUuid, - type: "custom", - position, // Use the position object with x and y - data: { - label: step.name! as string, - ...step, - id: newUuid - }, - isDraggable: true, - dragHandle: '.custom-drag-handle', - }; - - set({ nodes: [...get().nodes, newNode] }); - } catch (err) { - console.error(err); - } - }, - setNodes: (nodes) => set({ nodes }), - setEdges: (edges) => set({ edges }), - hasNode: (id) => !!get().nodes.find((node) => node.id === id), - getNodeById: (id) => get().nodes.find((node) => node.id === id), - deleteEdges: (ids) => { - const idArray = Array.isArray(ids) ? ids : [ids]; - set({ edges: get().edges.filter((edge) => !idArray.includes(edge.id)) }); - }, - deleteNodes: (ids) => { - const idArray = Array.isArray(ids) ? ids : [ids]; - set({ nodes: get().nodes.filter((node) => !idArray.includes(node.id)) }); - }, - updateEdge: (id: string, key: string, value: any) => { - const edge = get().edges.find((e) => e.id === id); - if (!edge) return; - const newEdge = { ...edge, [key]: value }; - set({ edges: get().edges.map((e) => (e.id === edge.id ? newEdge : e)) }); - }, - updateNode: (node) => - set({ nodes: get().nodes.map((n) => (n.id === node.id ? node : n)) }), - duplicateNode: (node) => { - const { data, position } = node; + }) + }); + } else { + console.warn('Connection not allowed based on node types'); + } + }, + + onDragOver: (event) => { + event.preventDefault(); + event.dataTransfer.dropEffect = "move"; + }, + onDrop: (event, screenToFlowPosition) => { + event.preventDefault(); + event.stopPropagation(); + + try { + let step: any = event.dataTransfer.getData("application/reactflow"); + step = JSON.parse(step); + if (!step) return; + // Use the screenToFlowPosition function to get flow coordinates + const position = screenToFlowPosition({ + x: event.clientX, + y: event.clientY, + }); const newUuid = uuidv4(); const newNode: FlowNode = { - ...node, - data: { ...data, id: newUuid, - }, - isDraggable: true, id: newUuid, - position: { x: position.x + 100, y: position.y + 100 }, - dragHandle: '.custom-drag-handle' + type: "custom", + position, // Use the position object with x and y + data: { + label: step.name! as string, + ...step, + id: newUuid + }, + isDraggable: true, + dragHandle: '.custom-drag-handle', }; + set({ nodes: [...get().nodes, newNode] }); - }, - })); + } catch (err) { + console.error(err); + } + }, + setNodes: (nodes) => set({ nodes }), + setEdges: (edges) => set({ edges }), + hasNode: (id) => !!get().nodes.find((node) => node.id === id), + getNodeById: (id) => get().nodes.find((node) => node.id === id), + deleteEdges: (ids) => { + const idArray = Array.isArray(ids) ? ids : [ids]; + set({ edges: get().edges.filter((edge) => !idArray.includes(edge.id)) }); + }, + deleteNodes: (ids) => { + //for now handling only single node deletion. can later enhance to multiple deletions + if (typeof ids !== 'string') { + return; + } + const idArray = Array.isArray(ids) ? ids : [ids]; + let finalEdges = get().edges.filter((edge) => !idArray.includes(edge.source) && !idArray.includes(edge.target)); + const sources = [...new Set(get().edges.filter((edge) => idArray.includes(edge.target)))]; + const targets = [...new Set(get().edges.filter((edge) => idArray.includes(edge.source)))]; + targets.forEach((edge) => { + finalEdges = [...finalEdges, ...sources.map((source) => ({ ...edge, source: source.source, id: `e${source.source}-${edge.target}` }))]; + }); + + set({ + edges: finalEdges, + nodes: get().nodes.filter((node) => !idArray.includes(node.id)), + selectedNode: null, + }); + }, + updateEdge: (id: string, key: string, value: any) => { + const edge = get().edges.find((e) => e.id === id); + if (!edge) return; + const newEdge = { ...edge, [key]: value }; + set({ edges: get().edges.map((e) => (e.id === edge.id ? newEdge : e)) }); + }, + updateNode: (node) => + set({ nodes: get().nodes.map((n) => (n.id === node.id ? node : n)) }), + duplicateNode: (node) => { + const { data, position } = node; + const newUuid = uuidv4(); + const newNode: FlowNode = { + ...node, + data: { + ...data, id: newUuid, + }, + isDraggable: true, + id: newUuid, + position: { x: position.x + 100, y: position.y + 100 }, + dragHandle: '.custom-drag-handle' + }; + set({ nodes: [...get().nodes, newNode] }); + }, +})); - export default useStore; +export default useStore; diff --git a/keep-ui/app/workflows/builder/builder.tsx b/keep-ui/app/workflows/builder/builder.tsx index b06d13b45..229fd8292 100644 --- a/keep-ui/app/workflows/builder/builder.tsx +++ b/keep-ui/app/workflows/builder/builder.tsx @@ -360,6 +360,8 @@ function Builder({ workflow={workflow} loadedAlertFile={loadedAlertFile} providers={providers} + definition={definition} + onDefinitionChange={(def: any) => setDefinition(wrapDefinition(def))} toolboxConfiguration={getToolboxConfiguration(providers)} /> diff --git a/keep-ui/app/workflows/builder/editors.tsx b/keep-ui/app/workflows/builder/editors.tsx index 0863d8fa1..f89b475ea 100644 --- a/keep-ui/app/workflows/builder/editors.tsx +++ b/keep-ui/app/workflows/builder/editors.tsx @@ -421,10 +421,13 @@ export function StepEditorV2({ const { selectedNode, updateSelectedNodeData, - setOpneGlobalEditor + setOpneGlobalEditor, + getNodeById } = useStore() + + - const {data} = selectedNode || {}; + const {data} = getNodeById(selectedNode) || {}; const {name, type, properties} = data || {}; function onNameChanged(e: any) { diff --git a/keep-ui/package-lock.json b/keep-ui/package-lock.json index 57631327d..ad135504f 100644 --- a/keep-ui/package-lock.json +++ b/keep-ui/package-lock.json @@ -107,6 +107,7 @@ "dlv": "^1.1.3", "doctrine": "^3.0.0", "dom-helpers": "^5.2.1", + "elkjs": "^0.9.3", "enhanced-resolve": "^5.14.0", "error-ex": "^1.3.2", "es-abstract": "^1.21.2", @@ -6512,6 +6513,11 @@ "strongly-connected-components": "^1.0.1" } }, + "node_modules/elkjs": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.9.3.tgz", + "integrity": "sha512-f/ZeWvW/BCXbhGEf1Ujp29EASo/lk1FDnETgNKwJrsVvGZhUWCZyg3xLJjAsxfOmt8KjswHmI5EwCQcPMpOYhQ==" + }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", diff --git a/keep-ui/package.json b/keep-ui/package.json index 7f0919075..715354fbc 100644 --- a/keep-ui/package.json +++ b/keep-ui/package.json @@ -108,6 +108,7 @@ "dlv": "^1.1.3", "doctrine": "^3.0.0", "dom-helpers": "^5.2.1", + "elkjs": "^0.9.3", "enhanced-resolve": "^5.14.0", "error-ex": "^1.3.2", "es-abstract": "^1.21.2", diff --git a/keep-ui/utils/hooks/useWorkflowInitialization.ts b/keep-ui/utils/hooks/useWorkflowInitialization.ts index ffbce5e5a..4ba265fa1 100644 --- a/keep-ui/utils/hooks/useWorkflowInitialization.ts +++ b/keep-ui/utils/hooks/useWorkflowInitialization.ts @@ -1,23 +1,99 @@ import { useEffect, useState, - useLayoutEffect, useRef, useCallback, } from "react"; -import { Edge, Position, useReactFlow } from "@xyflow/react"; +import { Edge, EdgeProps, MarkerType, Position, useReactFlow } from "@xyflow/react"; import dagre from "dagre"; -import { parseWorkflow, generateWorkflow } from "app/workflows/builder/utils"; +import { + parseWorkflow, + generateWorkflow, + buildAlert, +} from "app/workflows/builder/utils"; import { v4 as uuidv4 } from "uuid"; import { useSearchParams } from "next/navigation"; import useStore from "../../app/workflows/builder/builder-store"; import { FlowNode } from "../../app/workflows/builder/builder-store"; import { Provider } from "app/providers/providers"; +import { Definition, Step } from "sequential-workflow-designer"; +import { WrappedDefinition } from "sequential-workflow-designer-react"; +import ELK from 'elkjs/lib/elk.bundled.js'; +import { processWorkflowV2 } from "utils/reactFlow"; +// import "@xyflow/react/dist/style.css"; + +const layoutOptions = { + "elk.nodeLabels.placement": "INSIDE V_CENTER H_BOTTOM", + "elk.algorithm": "layered", + "elk.direction": "BOTTOM", + "org.eclipse.elk.layered.layering.strategy": "INTRACTIVE", + "org.eclipse.elk.edgeRouting": "ORTHOGONAL", + "elk.layered.unnecessaryBendpoints": "true", + "elk.layered.spacing.edgeNodeBetweenLayers": "50", + "org.eclipse.elk.layered.nodePlacement.bk.fixedAlignment": "BALANCED", + "org.eclipse.elk.layered.cycleBreaking.strategy": "DEPTH_FIRST", + "org.eclipse.elk.insideSelfLoops.activate": true, + "separateConnectedComponents": "false", + "spacing.componentComponent": "70", + "spacing": "75", + "elk.spacing.nodeNodeBetweenLayers": "70", + "elk.spacing.nodeNode": "8", + "elk.layered.spacing.nodeNodeBetweenLayers": "75", + "portConstraints": "FIXED_ORDER", + "nodeSize.constraints": "[MINIMUM_SIZE]", + "elk.alignment": "CENTER", + "elk.spacing.edgeNodeBetweenLayers": "50.0", + "org.eclipse.elk.layoutAncestors": "true", +} + + +const dagreGraph = new dagre.graphlib.Graph(); +dagreGraph.setDefaultEdgeLabel(() => ({})); + +const getLayoutedElements = (nodes: FlowNode[], edges: Edge[], options = {}) => { + const isHorizontal = options?.['elk.direction'] === 'RIGHT'; + const elk = new ELK(); + + const graph = { + id: 'root', + layoutOptions: options, + children: nodes.map((node) => ({ + ...node, + // Adjust the target and source handle positions based on the layout + // direction. + targetPosition: isHorizontal ? 'left' : 'top', + sourcePosition: isHorizontal ? 'right' : 'bottom', + + // Hardcode a width and height for elk to use when layouting. + width: 250, + height: 80, + })), + edges: edges, + }; + + return elk + .layout(graph) + .then((layoutedGraph) => ({ + nodes: layoutedGraph?.children?.map((node) => ({ + ...node, + // React Flow expects a position property on the node instead of `x` + // and `y` fields. + position: { x: node.x, y: node.y }, + })), + + edges: layoutedGraph.edges, + })) + .catch(console.error); +}; + const useWorkflowInitialization = ( workflow: string | undefined, loadedAlertFile: string | null | undefined, - providers: Provider[] + providers: Provider[], + definition: WrappedDefinition, + onDefinitionChange: (def: WrappedDefinition) => void, + toolboxConfiguration: Record ) => { const { nodes, @@ -32,6 +108,9 @@ const useWorkflowInitialization = ( setV2Properties, openGlobalEditor, selectedNode, + setToolBoxConfig, + isLayouted, + setIsLayouted } = useStore(); const [isLoading, setIsLoading] = useState(true); @@ -46,6 +125,9 @@ const useWorkflowInitialization = ( height: 100, }); const { screenToFlowPosition } = useReactFlow(); + // const [isLayouted, setIsLayouted] = useState(false); + const { fitView } = useReactFlow(); + const definitionRef = useRef(null); const handleDrop = useCallback( (event: React.DragEvent) => { @@ -54,29 +136,69 @@ const useWorkflowInitialization = ( [screenToFlowPosition] ); - const newEdgesFromNodes = (nodes: FlowNode[]): Edge[] => { - const edges: Edge[] = []; + const onLayout = useCallback( + ({ direction, useInitialNodes = false, initialNodes, initialEdges }: { + direction: string; + useInitialNodes?: boolean; + initialNodes?: FlowNode[], + initialEdges?: Edge[] + }) => { + const opts = { ...layoutOptions, 'elk.direction': direction }; + const ns = useInitialNodes ? initialNodes : nodes; + const es = useInitialNodes ? initialEdges : edges; + + // @ts-ignore + getLayoutedElements(ns, es, opts).then( + // @ts-ignore + ({ nodes: layoutedNodes, edges: layoutedEdges }) => { + layoutedEdges = layoutedEdges.map((edge: Edge) => { + return { + ...edge, + animated: !!edge?.target?.includes('empty'), + data: { ...edge.data, isLayouted: true } + }; + }) + layoutedNodes.forEach((node: FlowNode) => { + node.data = { ...node.data, isLayouted: true } + }) + setNodes(layoutedNodes); + setEdges(layoutedEdges); + + window.requestAnimationFrame(() => fitView()); + }, + ); + }, + [nodes, edges], + ); + + useEffect(() => { + if (!isLayouted && nodes.length > 0) { + onLayout({ direction: 'DOWN' }) + setIsLayouted(true) + } + + if (!isLayouted && nodes.length === 0) { + setIsLayouted(true); + } + // window.requestAnimationFrame(() => { + // fitView(); + // }); + }, [nodes, edges]) - nodes.forEach((node) => { - if (node.prevStepId) { - edges.push({ - id: `e${node.prevStepId}-${node.id}`, - source: node.prevStepId, - target: node.id, - type: "custom-edge", - label: node.edge_label || "", - }); - } - }); - return edges; - }; - useLayoutEffect(() => { - if (nodeRef.current) { - const { width, height } = nodeRef.current.getBoundingClientRect(); - setNodeDimensions({ width: width + 20, height: height + 20 }); + + const handleSpecialTools = ( + nodes: FlowNode[], + toolMeta: { + type: string; + specialToolNodeId: string; + switchCondition?: string; + } + ) => { + if (!nodes) { + return; } - }, [nodes.length]); + } useEffect(() => { const alertNameParam = searchParams?.get("alertName"); @@ -88,199 +210,50 @@ const useWorkflowInitialization = ( useEffect(() => { const initializeWorkflow = async () => { setIsLoading(true); - let parsedWorkflow; - - if (workflow) { - parsedWorkflow = parseWorkflow(workflow, providers); - } else if (loadedAlertFile == null) { - const alertUuid = uuidv4(); - let triggers = {}; - if (alertName && alertSource) { - triggers = { alert: { source: alertSource, name: alertName } }; - } - parsedWorkflow = generateWorkflow(alertUuid, "", "", [], [], triggers); - } else { - parsedWorkflow = parseWorkflow(loadedAlertFile, providers); - } - + let parsedWorkflow = definition?.value; + console.log("parsedWorkflow", parsedWorkflow); setV2Properties(parsedWorkflow?.properties ?? {}); - let newNodes = processWorkflow(parsedWorkflow.sequence); - let newEdges = newEdgesFromNodes(newNodes); - - const { nodes, edges } = getLayoutedElements(newNodes, newEdges); - + // let { nodes: newNodes, edges: newEdges } = processWorkflow( + // parsedWorkflow?.sequence + // ); + const sequences = [ + { + id: "start", + type: "start", + componentType: "start", + properties: {}, + isLayouted: false, + } as Partial, + ...(parsedWorkflow?.sequence || []), + { + id: "end", + type: "end", + componentType: "end", + properties: {}, + isLayouted: false, + } as Partial, + ]; + const intialPositon = { x: 0, y: 50 }; + let { nodes, edges } = processWorkflowV2(sequences, intialPositon, true); + console.log(nodes, edges); + console.log("nodes", nodes); + console.log("edges", edges); + setIsLayouted(false); setNodes(nodes); setEdges(edges); + setToolBoxConfig(toolboxConfiguration); setIsLoading(false); }; - initializeWorkflow(); - }, [loadedAlertFile, workflow, alertName, alertSource, providers]); + }, [ + loadedAlertFile, + workflow, + alertName, + alertSource, + providers, + definition?.value, + ]); - const getLayoutedElements = (nodes: FlowNode[], edges: Edge[]) => { - const dagreGraph = new dagre.graphlib.Graph(); - dagreGraph.setDefaultEdgeLabel(() => ({})); - - dagreGraph.setGraph({ rankdir: "TB", nodesep: 100, edgesep: 100 }); - - nodes.forEach((node) => { - dagreGraph.setNode(node.id, { - width: nodeDimensions.width, - height: nodeDimensions.height, - }); - }); - - edges.forEach((edge) => { - dagreGraph.setEdge(edge.source, edge.target); - }); - - dagre.layout(dagreGraph); - - nodes.forEach((node: FlowNode) => { - const nodeWithPosition = dagreGraph.node(node.id); - node.targetPosition = "top" as Position; - node.sourcePosition = "bottom" as Position; - - node.position = { - x: nodeWithPosition.x - nodeDimensions.width / 2, - y: nodeWithPosition.y - nodeDimensions.height / 2, - }; - }); - - return { nodes, edges }; - }; - - const processWorkflow = (sequence: any, parentId?: string) => { - let newNodes: FlowNode[] = []; - - sequence.forEach((step: any, index: number) => { - const newPrevStepId = sequence?.[index - 1]?.id || ""; - const nodes = processStep( - step, - { x: index * 200, y: 50 }, - newPrevStepId, - parentId - ); - newNodes = [...newNodes, ...nodes]; - }); - - return newNodes; - }; - - const processStep = ( - step: any, - position: { x: number; y: number }, - prevStepId?: string, - parentId?: string - ) => { - const nodeId = step.id; - let newNode: FlowNode; - let newNodes: FlowNode[] = []; - - if (step.componentType === "switch") { - const subflowId = uuidv4(); - // newNode = { - // id: subflowId, - // type: "custom", - // position, - // data: { - // label: "Switch", - // type: "sub_flow", - // }, - // style: { - // border: "2px solid orange", - // width: "100%", - // height: "100%", - // display: "flex", - // flexDirection: "column", - // justifyContent: "space-between", - // }, - // prevStepId: prevStepId, - // parentId: parentId, - // }; - // if (parentId) { - // newNode.extent = "parent"; - // } - - // newNodes.push(newNode); - - const switchNode = { - id: nodeId, - type: "custom", - position: { x: 0, y: 0 }, - data: { - label: step.name, - ...step, - }, - isDraggable: false, - dragHandle: '.custom-drag-handle', - prevStepId: prevStepId, - // extent: 'parent', - } as FlowNode; - - newNodes.push(switchNode); - - // const trueSubflowNodes: FlowNode[] = processWorkflow(step?.branches?.true, subflowId); - const trueSubflowNodes: FlowNode[] = processWorkflow( - step?.branches?.true - ); - // const falseSubflowNodes: FlowNode[] = processWorkflow(step?.branches?.false, subflowId); - const falseSubflowNodes: FlowNode[] = processWorkflow( - step?.branches?.false - ); - - if (trueSubflowNodes.length > 0) { - trueSubflowNodes[0].edge_label = "True"; - trueSubflowNodes[0].prevStepId = nodeId; - } - - if (falseSubflowNodes.length > 0) { - falseSubflowNodes[0].edge_label = "False"; - falseSubflowNodes[0].prevStepId = nodeId; - } - - newNodes = [...newNodes, ...trueSubflowNodes, ...falseSubflowNodes]; - } else if (step.componentType === "container" && step.type === "foreach") { - const forEachhNode = { - id: nodeId, - type: "custom", - dragHandle: '.custom-drag-handle', - position: { x: 0, y: 0 }, - data: { - label: step.name, - ...step, - }, - isDraggable: false, - prevStepId: prevStepId, - // extent: 'parent', - } as FlowNode; - newNodes.push(forEachhNode); - - const sequences: FlowNode[] = processWorkflow( - step?.sequence || [], - nodeId - ); - newNodes = [...newNodes, ...sequences]; - } else { - newNode = { - id: nodeId, - type: "custom", - dragHandle: '.custom-drag-handle', - position, - data: { - label: step.name, - ...step, - }, - isDraggable: false, - prevStepId: prevStepId, - // parentId: parentId, - } as FlowNode; - - newNodes.push(newNode); - } - - return newNodes; - }; return { nodes, @@ -293,7 +266,11 @@ const useWorkflowInitialization = ( onDrop: handleDrop, openGlobalEditor, selectedNode, + setNodes, + toolboxConfiguration, + isLayouted, }; }; export default useWorkflowInitialization; + diff --git a/keep-ui/utils/reactFlow.ts b/keep-ui/utils/reactFlow.ts new file mode 100644 index 000000000..b2621f0cb --- /dev/null +++ b/keep-ui/utils/reactFlow.ts @@ -0,0 +1,309 @@ +import { v4 as uuidv4 } from "uuid"; +import { FlowNode, V2Step } from "app/workflows/builder/builder-store"; +import { Edge } from "@xyflow/react"; + + + +export function createSwitchNodeV2( + step: any, + nodeId: string, + position: { x: number; y: number }, + nextNodeId?: string | null, + prevNodeId?: string | null +): FlowNode[] { + const customIdentifier = `${step.type}__end__${nodeId}`; + return [ + { + id: nodeId, + type: "custom", + position: { x: 0, y: 0 }, + data: { + label: step.name, + ...step, + }, + isDraggable: false, + prevNodeId, + nextNodeId: customIdentifier, + dragHandle: ".custom-drag-handle", + style: { + margin: "0px 20px 0px 20px", + } + }, + { + id: customIdentifier, + type: "custom", + position: { x: 0, y: 0 }, + data: { + label: "+", + id: customIdentifier, + type: `${step.type}__end_node`, + }, + isDraggable: false, + prevNodeId: nodeId, + nextNodeId: nextNodeId, + dragHandle: ".custom-drag-handle", + }, + ]; +}; + + + +export function handleSwitchNode(step, position, nextNodeId, prevNodeId, nodeId) { + if (step.componentType !== "switch") { + return { nodes: [], edges: [] }; + } + let trueBranches = step?.branches?.true || []; + let falseBranches = step?.branches?.false || []; + + + function _getEmptyNode(type: string) { + const key = `empty_${type}` + return { + id: `${step.type}__${nodeId}__${key}`, + type: key, + componentType: key, + name: "empty", + properties: {}, + } + } + + let [switchStartNode, switchEndNode] = createSwitchNodeV2(step, nodeId, position, nextNodeId, prevNodeId); + trueBranches = [ + { ...switchStartNode.data, type: 'temp_node', componentType: "temp_node" }, + ...trueBranches, + _getEmptyNode("true"), + { ...switchEndNode.data, type: 'temp_node', componentType: "temp_node" } + ]; + falseBranches = [ + { ...switchStartNode.data, type: 'temp_node', componentType: "temp_node" }, + ...falseBranches, + _getEmptyNode("false"), + { ...switchEndNode.data, type: 'temp_node', componentType: "temp_node" } + ] + + let truePostion = { x: position.x - 200, y: position.y - 100 }; + let falsePostion = { x: position.x + 200, y: position.y - 100 }; + + let { nodes: trueBranchNodes, edges: trueSubflowEdges } = + processWorkflowV2(trueBranches, truePostion) || {}; + let { nodes: falseSubflowNodes, edges: falseSubflowEdges } = + processWorkflowV2(falseBranches, falsePostion) || {}; + + function _adjustEdgeConnectionsAndLabelsForSwitch(type: string) { + if (!type) { + return; + } + const subflowEdges = type === 'True' ? trueSubflowEdges : falseSubflowEdges; + const subflowNodes = type === 'True' ? trueBranchNodes : falseSubflowNodes; + const [firstEdge] = subflowEdges; + firstEdge.label = type?.toString(); + firstEdge.id = `e${switchStartNode.prevNodeId}-${firstEdge.target || switchEndNode.id + }`; + firstEdge.source = switchStartNode.id || ""; + firstEdge.target = firstEdge.target || switchEndNode.id; + subflowEdges.pop(); + } + _adjustEdgeConnectionsAndLabelsForSwitch('True'); + _adjustEdgeConnectionsAndLabelsForSwitch('False'); + return { + nodes: [ + switchStartNode, + ...trueBranchNodes, + ...falseSubflowNodes, + switchEndNode, + ], edges: [ + ...trueSubflowEdges, + ...falseSubflowEdges, + //handling the switch end edge + { + id: `e${switchEndNode.id}-${nextNodeId}`, + source: switchEndNode.id, + target: nextNodeId || "", + type: "custom-edge", + label: "", + } + ] + }; + +} + +export const createDefaultNodeV2 = ( + step: any, + nodeId: string, + position: { x: number; y: number }, + nextNodeId?: string | null, + prevNodeId?: string | null +): FlowNode => +({ + id: nodeId, + type: "custom", + dragHandle: ".custom-drag-handle", + position: { x: 0, y: 0 }, + data: { + label: step.name, + ...step, + }, + isDraggable: false, + nextNodeId, + prevNodeId, +} as FlowNode); + +export function handleDefaultNode(step, position, nextNodeId, prevNodeId, nodeId) { + const nodes = []; + const edges = []; + const newNode = createDefaultNodeV2( + step, + nodeId, + position, + nextNodeId, + prevNodeId + ); + if (step.type !== 'temp_node') { + nodes.push(newNode); + } + // Handle edge for default nodes + if (newNode.id !== "end" && !step.edgeNotNeeded) { + edges.push({ + id: `e${newNode.id}-${nextNodeId}`, + source: newNode.id ?? "", + target: nextNodeId ?? "", + type: "custom-edge", + label: "", + }); + } + return { nodes, edges }; +} + + +export function handleNextEdge(step, nodes, edges, nextNodeId) { + if (nodes?.length && step?.needNextEdge) { + const lastNode = nodes[nodes.length - 1]; + edges.push({ + id: `e${lastNode.id}-${nextNodeId}`, + source: lastNode.id ?? "", + target: nextNodeId ?? "", + type: "custom-edge", + label: "", + }); + } +} + +export function getForEachNode(step, position, nodeId, prevNodeId, nextNodeId, parents = []) { + const { sequence, ...rest } = step; + const customIdentifier = `${step.type}__end__${nodeId}`; + + return [ + { + id: nodeId, + data: { ...rest, id: nodeId }, + type: "custom", + position: { x: 0, y: 0 }, + isDraggable: false, + dragHandle: ".custom-drag-handle", + prevNodeId: prevNodeId, + nextNodeId: nextNodeId + }, + { + id: customIdentifier, + data: { ...rest, id: customIdentifier, name: "foreach end", label: "foreach end" }, + type: "custom", + position: { x: 0, y: 0 }, + isDraggable: false, + dragHandle: ".custom-drag-handle", + prevNodeId: prevNodeId, + nextNodeId: nextNodeId, + }, + ]; +} + + +export function handleForeachNode(step, position, nextNodeId, prevNodeId, nodeId) { + + const [forEachStartNode, forEachEndNode] = getForEachNode(step, position, nodeId, prevNodeId, nextNodeId); + + function _getEmptyNode(type: string) { + const key = `empty_${type}` + return { + id: `${step.type}__${nodeId}__${key}`, + type: key, + componentType: key, + name: "empty", + properties: {}, + parents: [nodeId] + } + } + const sequences = [ + { id: prevNodeId, type: "temp_node", componentType: "temp_node", name: "temp_node", properties: {}, edgeNotNeeded: true }, + { id: forEachStartNode.id, type: "temp_node", componentType: "temp_node", name: "temp_node", properties: {} }, + ...step.sequence, + _getEmptyNode("foreach"), + { id: forEachEndNode.id, type: "temp_node", componentType: "temp_node", name: "temp_node", properties: {} }, + { id: nextNodeId, type: "temp_node", componentType: "temp_node", name: "temp_node", properties: {}, edgeNotNeeded: true }, + ]; + const { nodes, edges } = processWorkflowV2(sequences, position); + return { nodes: [forEachStartNode, ...nodes, forEachEndNode], edges: edges }; +} + + +export const processStepV2 = ( + step: any, + position: { x: number; y: number }, + nextNodeId?: string | null, + prevNodeId?: string | null, +) => { + const nodeId = step.id; + let newNode: FlowNode; + let newNodes: FlowNode[] = []; + let newEdges: Edge[] = []; + console.log("nodeId=======>", nodeId, position); + switch (true) { + case step?.componentType === "switch": + { + const { nodes, edges } = handleSwitchNode(step, position, nextNodeId, prevNodeId, nodeId); + newEdges = [...newEdges, ...edges]; + newNodes = [...newNodes, ...nodes]; + break; + } + case step?.componentType === "container" && step?.type === "foreach": + { + const { nodes, edges } = handleForeachNode(step, position, nextNodeId, prevNodeId, nodeId); + newEdges = [...newEdges, ...edges]; + newNodes = [...newNodes, ...nodes]; + break; + } + default: + { + const { nodes, edges } = handleDefaultNode(step, position, nextNodeId, prevNodeId, nodeId); + newEdges = [...newEdges, ...edges]; + newNodes = [...newNodes, ...nodes]; + break; + } + } + + return { nodes: newNodes, edges: newEdges }; +}; + +export const processWorkflowV2 = (sequence: any, position: { x: number, y: number }, isFirstRender = false) => { + let newNodes: FlowNode[] = []; + let newEdges: Edge[] = []; + + sequence?.forEach((step: any, index: number) => { + const prevNodeId = sequence?.[index - 1]?.id || null; + const nextNodeId = sequence?.[index + 1]?.id || null; + position.y += 150; + const { nodes, edges } = processStepV2( + step, + position, + nextNodeId, + prevNodeId + ); + newNodes = [...newNodes, ...nodes]; + newEdges = [...newEdges, ...edges]; + }); + + if (isFirstRender) { + newNodes = newNodes.map((node) => ({ ...node, isLayouted: false })); + newEdges = newEdges.map((edge) => ({ ...edge, isLayouted: false })); + } + return { nodes: newNodes, edges: newEdges }; +}; From b9dac68adc29abf30f6a1f1a069ac171e6287b46 Mon Sep 17 00:00:00 2001 From: Rajesh Jonnalagadda Date: Thu, 8 Aug 2024 17:56:56 +0530 Subject: [PATCH 10/55] chore:refactored the flow layout structure and adjusted the toolbox and some minor chores --- keep-ui/app/workflows/builder/CustomEdge.tsx | 17 ++-- keep-ui/app/workflows/builder/CustomNode.tsx | 79 +++++++++++++++-- keep-ui/app/workflows/builder/NodeMenu.tsx | 4 +- keep-ui/app/workflows/builder/ToolBox.tsx | 67 ++++++++------- .../app/workflows/builder/builder-store.tsx | 10 ++- keep-ui/app/workflows/builder/editors.tsx | 2 + .../utils/hooks/useWorkflowInitialization.ts | 85 ++++++++++++------- keep-ui/utils/reactFlow.ts | 53 +++++------- 8 files changed, 205 insertions(+), 112 deletions(-) diff --git a/keep-ui/app/workflows/builder/CustomEdge.tsx b/keep-ui/app/workflows/builder/CustomEdge.tsx index f377f0553..ad52675ff 100644 --- a/keep-ui/app/workflows/builder/CustomEdge.tsx +++ b/keep-ui/app/workflows/builder/CustomEdge.tsx @@ -22,6 +22,8 @@ const CustomEdge: React.FC = ({ source, target, data, + style, + s }: CustomEdgeProps) => { const { deleteEdges, edges, setSelectedEdge, selectedEdge } = useStore(); @@ -44,24 +46,29 @@ const CustomEdge: React.FC = ({ // }; - let dynamicLabel = label; + let dynamicLabel = label; const isLayouted = !!data?.isLayouted; + console.log("style=======>", id, style) + const color = dynamicLabel === "True" ? "left-0 bg-green-500" : dynamicLabel === "False" ? "bg-red-500" : "bg-orange-500"; return ( <> -
    { e.stopPropagation(); + if (type === 'start' || type === 'end' || id?.includes('end')) { + setSelectedNode(null); + setOpneGlobalEditor(true); + return; + } setSelectedNode(id); }} - style={{ opacity: data.isLayouted ? 1 : 0 }} + style={{ + opacity: data.isLayouted ? 1 : 0 + }} > {isEmptyNode &&
    + {selectedNode === id &&
    Go to Toolbox
    }
    } {!isEmptyNode && data?.type !== "sub_flow" && (
    @@ -68,14 +82,61 @@ function CustomNode({ id, data }: FlowNode) { -
    +
    } + + {specialNodeCheck &&
    { + e.stopPropagation(); + if (type === 'start' || type === 'end' || id?.includes('end')) { + setSelectedNode(null); + return; + } + setSelectedNode(id); + }} + > +
    + {type === 'start' && } + {type === 'end' && } + {['threshold', 'assert', 'foreach'].includes(type) && +
    + {id.includes('end') ? : + {data?.type}} +
    + } + + {['start', 'threshold', 'assert', 'foreach'].includes(type) && } + + {['end', 'threshold', 'assert', 'foreach'].includes(type) && } + + +
    +
    } ); diff --git a/keep-ui/app/workflows/builder/NodeMenu.tsx b/keep-ui/app/workflows/builder/NodeMenu.tsx index 8823f9cbd..c8bf777d3 100644 --- a/keep-ui/app/workflows/builder/NodeMenu.tsx +++ b/keep-ui/app/workflows/builder/NodeMenu.tsx @@ -9,14 +9,14 @@ export default function NodeMenu({ data, id }: { data: FlowNode["data"], id: str const stopPropagation = (e: React.MouseEvent) => { e.stopPropagation(); }; - const isEmptyNode = data?.type?.includes("empty") + const isEmptyOrEndNode = data?.type?.includes("empty") || id?.includes('end') const { deleteNodes, duplicateNode, setSelectedNode, setStepEditorOpenForNode } = useStore(); return ( <> - {data && !isEmptyNode && ( + {data && !isEmptyOrEndNode && (
    -

    Toolbox

    -
    - setSearchTerm(e.target.value)} - /> - -
    - {(isVisible || checkForSearchResults) &&
    - {filteredGroups.length > 0 && - filteredGroups.map((group: Record) => ( - + {/* Sticky header */} +
    +

    Toolbox

    +
    + setSearchTerm(e.target.value)} /> - ))} -
    } + +
    +
    + + {/* Scrollable list */} + {(isVisible || checkForSearchResults) &&
    + {filteredGroups.length > 0 && + filteredGroups.map((group: Record) => ( + + ))} +
    } +
    - ); + + ) }; export default DragAndDropSidebar; diff --git a/keep-ui/app/workflows/builder/builder-store.tsx b/keep-ui/app/workflows/builder/builder-store.tsx index 371c9e55b..94897bfdd 100644 --- a/keep-ui/app/workflows/builder/builder-store.tsx +++ b/keep-ui/app/workflows/builder/builder-store.tsx @@ -138,7 +138,8 @@ function addNodeBetween(nodeOrEdge: string|null, step: any, type: string, set: S const newNodeId = uuidv4(); const newStep = { ...step, id: newNodeId } let { nodes, edges } = processWorkflowV2([ - { id: sourceId, type: 'temp_node', name: 'temp_node', 'componentType': 'temp_node' }, + { id: sourceId, type: 'temp_node', name: 'temp_node', 'componentType': 'temp_node', + edgeLabel: edge.label, edgeColor:edge?.style?.stroke}, newStep, { id: targetId, type: 'temp_node', name: 'temp_node', 'componentType': 'temp_node', edgeNotNeeded: true } ], { x: 0, y: 0 }, true); @@ -172,7 +173,7 @@ const useStore = create((set, get) => ({ toolboxConfiguration: {} as Record, isLayouted: false, selectedEdge: null, - setSelectedEdge: (id) => set({ selectedEdge: id, selectedNode: null }), + setSelectedEdge: (id) => set({ selectedEdge: id, selectedNode: null, openGlobalEditor:true }), setIsLayouted: (isLayouted) => set({ isLayouted }), getEdgeById: (id) => get().edges.find((edge) => edge.id === id), addNodeBetween: (nodeOrEdge: string|null, step: any, type: string) => { @@ -265,6 +266,10 @@ const useStore = create((set, get) => ({ try { let step: any = event.dataTransfer.getData("application/reactflow"); + if(!step){ + return; + } + console.log("step", step); step = JSON.parse(step); if (!step) return; // Use the screenToFlowPosition function to get flow coordinates @@ -316,6 +321,7 @@ const useStore = create((set, get) => ({ edges: finalEdges, nodes: get().nodes.filter((node) => !idArray.includes(node.id)), selectedNode: null, + isLayouted: false }); }, updateEdge: (id: string, key: string, value: any) => { diff --git a/keep-ui/app/workflows/builder/editors.tsx b/keep-ui/app/workflows/builder/editors.tsx index f89b475ea..d62ff797e 100644 --- a/keep-ui/app/workflows/builder/editors.tsx +++ b/keep-ui/app/workflows/builder/editors.tsx @@ -424,6 +424,8 @@ export function StepEditorV2({ setOpneGlobalEditor, getNodeById } = useStore() + + if (!selectedNode) return null; diff --git a/keep-ui/utils/hooks/useWorkflowInitialization.ts b/keep-ui/utils/hooks/useWorkflowInitialization.ts index 4ba265fa1..262a2f35a 100644 --- a/keep-ui/utils/hooks/useWorkflowInitialization.ts +++ b/keep-ui/utils/hooks/useWorkflowInitialization.ts @@ -4,14 +4,13 @@ import { useRef, useCallback, } from "react"; -import { Edge, EdgeProps, MarkerType, Position, useReactFlow } from "@xyflow/react"; +import { Edge, useReactFlow } from "@xyflow/react"; import dagre from "dagre"; import { parseWorkflow, generateWorkflow, buildAlert, } from "app/workflows/builder/utils"; -import { v4 as uuidv4 } from "uuid"; import { useSearchParams } from "next/navigation"; import useStore from "../../app/workflows/builder/builder-store"; import { FlowNode } from "../../app/workflows/builder/builder-store"; @@ -25,27 +24,41 @@ import { processWorkflowV2 } from "utils/reactFlow"; const layoutOptions = { "elk.nodeLabels.placement": "INSIDE V_CENTER H_BOTTOM", "elk.algorithm": "layered", - "elk.direction": "BOTTOM", - "org.eclipse.elk.layered.layering.strategy": "INTRACTIVE", - "org.eclipse.elk.edgeRouting": "ORTHOGONAL", - "elk.layered.unnecessaryBendpoints": "true", - "elk.layered.spacing.edgeNodeBetweenLayers": "50", - "org.eclipse.elk.layered.nodePlacement.bk.fixedAlignment": "BALANCED", - "org.eclipse.elk.layered.cycleBreaking.strategy": "DEPTH_FIRST", - "org.eclipse.elk.insideSelfLoops.activate": true, - "separateConnectedComponents": "false", - "spacing.componentComponent": "70", - "spacing": "75", - "elk.spacing.nodeNodeBetweenLayers": "70", - "elk.spacing.nodeNode": "8", - "elk.layered.spacing.nodeNodeBetweenLayers": "75", - "portConstraints": "FIXED_ORDER", - "nodeSize.constraints": "[MINIMUM_SIZE]", - "elk.alignment": "CENTER", - "elk.spacing.edgeNodeBetweenLayers": "50.0", - "org.eclipse.elk.layoutAncestors": "true", + "elk.direction": "BOTTOM", // Direction of layout + "org.eclipse.elk.layered.layering.strategy": "INTERACTIVE", // Interactive layering strategy + "org.eclipse.elk.edgeRouting": "ORTHOGONAL", // Use orthogonal routing + "elk.layered.unnecessaryBendpoints": "true", // Allow bend points if necessary + "elk.layered.spacing.edgeNodeBetweenLayers": "50", // Spacing between edges and nodes + "org.eclipse.elk.layered.nodePlacement.bk.fixedAlignment": "BALANCED", // Balanced node placement + "org.eclipse.elk.layered.cycleBreaking.strategy": "DEPTH_FIRST", // Strategy for cycle breaking + "elk.insideSelfLoops.activate": true, // Handle self-loops inside nodes + "separateConnectedComponents": "false", // Do not separate connected components + "spacing.componentComponent": "70", // Spacing between components + "spacing": "75", // General spacing + "elk.spacing.nodeNodeBetweenLayers": "70", // Spacing between nodes in different layers + "elk.spacing.nodeNode": "8", // Spacing between nodes + "elk.layered.spacing.nodeNodeBetweenLayers": "75", // Spacing between nodes between layers + "portConstraints": "FIXED_ORDER", // Fixed order for ports + "nodeSize.constraints": "[MINIMUM_SIZE]", // Minimum size constraints for nodes + "elk.alignment": "CENTER", // Center alignment + "elk.spacing.edgeNodeBetweenLayers": "50.0", // Spacing between edges and nodes + "org.eclipse.elk.layoutAncestors": "true", // Layout ancestors + "elk.edgeRouting": "ORTHOGONAL", // Ensure orthogonal edge routing + "elk.layered.edgeRouting": "ORTHOGONAL", // Ensure orthogonal edge routing in layered layout + "elk.layered.nodePlacement.strategy": "BRANDES_KOEPF", // Node placement strategy for symmetry + "elk.layered.nodePlacement.outerSpacing": "20", // Spacing around nodes to prevent overlap + "elk.layered.nodePlacement.outerPadding": "20", // Padding around nodes + "elk.layered.edgeRouting.orthogonal": true } +const getRandomColor = () => { + const letters = '0123456789ABCDEF'; + let color = '#'; + for (let i = 0; i < 6; i++) { + color += letters[Math.floor(Math.random() * 16)]; + } + return color; +}; const dagreGraph = new dagre.graphlib.Graph(); dagreGraph.setDefaultEdgeLabel(() => ({})); @@ -57,17 +70,24 @@ const getLayoutedElements = (nodes: FlowNode[], edges: Edge[], options = {}) => const graph = { id: 'root', layoutOptions: options, - children: nodes.map((node) => ({ - ...node, - // Adjust the target and source handle positions based on the layout - // direction. - targetPosition: isHorizontal ? 'left' : 'top', - sourcePosition: isHorizontal ? 'right' : 'bottom', + children: nodes.map((node) => { + const type = node?.data?.type + ?.replace("step-", "") + ?.replace("action-", "") + ?.replace("condition-", "") + ?.replace("__end", ""); + return ({ + ...node, + // Adjust the target and source handle positions based on the layout + // direction. + targetPosition: isHorizontal ? 'left' : 'top', + sourcePosition: isHorizontal ? 'right' : 'bottom', - // Hardcode a width and height for elk to use when layouting. - width: 250, - height: 80, - })), + // Hardcode a width and height for elk to use when layouting. + width: ['start', 'end'].includes(type) ? 80 : 280, + height: 80, + }) + }), edges: edges, }; @@ -272,5 +292,4 @@ const useWorkflowInitialization = ( }; }; -export default useWorkflowInitialization; - +export default useWorkflowInitialization; \ No newline at end of file diff --git a/keep-ui/utils/reactFlow.ts b/keep-ui/utils/reactFlow.ts index b2621f0cb..9df2ab8cd 100644 --- a/keep-ui/utils/reactFlow.ts +++ b/keep-ui/utils/reactFlow.ts @@ -88,7 +88,7 @@ export function handleSwitchNode(step, position, nextNodeId, prevNodeId, nodeId) processWorkflowV2(trueBranches, truePostion) || {}; let { nodes: falseSubflowNodes, edges: falseSubflowEdges } = processWorkflowV2(falseBranches, falsePostion) || {}; - + function _adjustEdgeConnectionsAndLabelsForSwitch(type: string) { if (!type) { return; @@ -115,13 +115,7 @@ export function handleSwitchNode(step, position, nextNodeId, prevNodeId, nodeId) ...trueSubflowEdges, ...falseSubflowEdges, //handling the switch end edge - { - id: `e${switchEndNode.id}-${nextNodeId}`, - source: switchEndNode.id, - target: nextNodeId || "", - type: "custom-edge", - label: "", - } + createCustomEdgeMeta(switchEndNode.id, nextNodeId) ] }; @@ -148,6 +142,25 @@ export const createDefaultNodeV2 = ( prevNodeId, } as FlowNode); +const getRandomColor = () => { + const letters = '0123456789ABCDEF'; + let color = '#'; + for (let i = 0; i < 6; i++) { + color += letters[Math.floor(Math.random() * 16)]; + } + return color; +}; + +export function createCustomEdgeMeta(source: string, target: string, label?: string, color?: string, type?: string) { + return { + id: `e${source}-${target}`, + source: source ?? "", + target: target ?? "", + type: type || "custom-edge", + label, + style: { stroke: color || getRandomColor() } + } +} export function handleDefaultNode(step, position, nextNodeId, prevNodeId, nodeId) { const nodes = []; const edges = []; @@ -163,31 +176,11 @@ export function handleDefaultNode(step, position, nextNodeId, prevNodeId, nodeId } // Handle edge for default nodes if (newNode.id !== "end" && !step.edgeNotNeeded) { - edges.push({ - id: `e${newNode.id}-${nextNodeId}`, - source: newNode.id ?? "", - target: nextNodeId ?? "", - type: "custom-edge", - label: "", - }); + edges.push(createCustomEdgeMeta(newNode.id, nextNodeId, step.edgeLabel, step.edgeColor)); } return { nodes, edges }; } - -export function handleNextEdge(step, nodes, edges, nextNodeId) { - if (nodes?.length && step?.needNextEdge) { - const lastNode = nodes[nodes.length - 1]; - edges.push({ - id: `e${lastNode.id}-${nextNodeId}`, - source: lastNode.id ?? "", - target: nextNodeId ?? "", - type: "custom-edge", - label: "", - }); - } -} - export function getForEachNode(step, position, nodeId, prevNodeId, nextNodeId, parents = []) { const { sequence, ...rest } = step; const customIdentifier = `${step.type}__end__${nodeId}`; @@ -252,10 +245,8 @@ export const processStepV2 = ( prevNodeId?: string | null, ) => { const nodeId = step.id; - let newNode: FlowNode; let newNodes: FlowNode[] = []; let newEdges: Edge[] = []; - console.log("nodeId=======>", nodeId, position); switch (true) { case step?.componentType === "switch": { From 8410ebc666bfa0dd00125358de8758a2a8928cfe Mon Sep 17 00:00:00 2001 From: Rajesh Jonnalagadda Date: Fri, 9 Aug 2024 22:19:47 +0530 Subject: [PATCH 11/55] feat:delete foreach switch anad added the builde statu tracker --- .../builder/BuilderChanagesTracker.tsx | 66 +++++++++++++ keep-ui/app/workflows/builder/CustomEdge.tsx | 2 - .../app/workflows/builder/builder-store.tsx | 89 ++++++++++++----- keep-ui/app/workflows/builder/builder.tsx | 38 +++++--- keep-ui/utils/reactFlow.ts | 97 ++++++++++++++----- 5 files changed, 230 insertions(+), 62 deletions(-) create mode 100644 keep-ui/app/workflows/builder/BuilderChanagesTracker.tsx diff --git a/keep-ui/app/workflows/builder/BuilderChanagesTracker.tsx b/keep-ui/app/workflows/builder/BuilderChanagesTracker.tsx new file mode 100644 index 000000000..2b5425a81 --- /dev/null +++ b/keep-ui/app/workflows/builder/BuilderChanagesTracker.tsx @@ -0,0 +1,66 @@ +import React, { useEffect, useRef, useState } from 'react' +import useStore, { FlowNode } from './builder-store'; +import { Button } from '@tremor/react'; +import { Edge } from '@xyflow/react'; +import { reConstructWorklowToDefinition } from 'utils/reactFlow'; + +export default function BuilderChanagesTracker() { + const {nodes, edges,setEdges, setNodes, isLayouted, setIsLayouted, v2Properties} = useStore(); + const [changes, setChanges] = useState(0); + const [savedChanges, setSavedChanges] = useState(0); + const [lastSavedChanges, setLastSavedChanges] = useState<{nodes:FlowNode[], edges:Edge[]}>({nodes: nodes, edges: edges}); + const [firstInitilisationDone, setFirstInitilisationDone] = useState(false); + const [proprtiesUpdated, setPropertiesUpdated] = useState(false); + + console.log("isLayouted", isLayouted); + + useEffect(()=>{ + if(isLayouted && firstInitilisationDone) { + setChanges((prev)=>prev+1); + } + if(isLayouted && !firstInitilisationDone) { + setFirstInitilisationDone(true); + setLastSavedChanges({nodes: nodes, edges: edges}); + } + },[isLayouted]) + + + useEffect(()=>{ + reConstructWorklowToDefinition(lastSavedChanges); + }, [lastSavedChanges]) + + + const handleDiscardChanges = (e: React.MouseEvent) => { + if(!isLayouted) return; + setEdges(lastSavedChanges.edges || []); + setNodes(lastSavedChanges.nodes || []); + setChanges(0); + setFirstInitilisationDone(false); + setIsLayouted(false); + } + + const handleSaveChanges = (e: React.MouseEvent) =>{ + e.preventDefault(); + e.stopPropagation(); + setSavedChanges((prev)=>(prev+1 || 0)); + setLastSavedChanges({nodes: nodes, edges: edges}); + setChanges(0); + setPropertiesUpdated(false); + } + + + + + return ( +
    + + +
    + ) +} diff --git a/keep-ui/app/workflows/builder/CustomEdge.tsx b/keep-ui/app/workflows/builder/CustomEdge.tsx index ad52675ff..45f8e5c6d 100644 --- a/keep-ui/app/workflows/builder/CustomEdge.tsx +++ b/keep-ui/app/workflows/builder/CustomEdge.tsx @@ -23,7 +23,6 @@ const CustomEdge: React.FC = ({ target, data, style, - s }: CustomEdgeProps) => { const { deleteEdges, edges, setSelectedEdge, selectedEdge } = useStore(); @@ -49,7 +48,6 @@ const CustomEdge: React.FC = ({ let dynamicLabel = label; const isLayouted = !!data?.isLayouted; - console.log("style=======>", id, style) const color = dynamicLabel === "True" ? "left-0 bg-green-500" : dynamicLabel === "False" ? "bg-red-500" : "bg-orange-500"; return ( diff --git a/keep-ui/app/workflows/builder/builder-store.tsx b/keep-ui/app/workflows/builder/builder-store.tsx index 94897bfdd..c74380d36 100644 --- a/keep-ui/app/workflows/builder/builder-store.tsx +++ b/keep-ui/app/workflows/builder/builder-store.tsx @@ -11,7 +11,7 @@ import { Node, } from "@xyflow/react"; -import { processStepV2, handleNextEdge, processWorkflowV2 } from "utils/reactFlow"; +import { createCustomEdgeMeta, processWorkflowV2 } from "utils/reactFlow"; export type V2Properties = Record; @@ -41,10 +41,11 @@ export type FlowNode = Node & { nextStep?: NodeStepMeta[] | NodeStepMeta | null; prevNodeId?: string | null; nextNodeId?: string | null; - id:string; + id: string; + isNested: boolean; }; -const initialNodes: FlowNode[] = [ +const initialNodes: Partial[] = [ { id: "a", position: { x: 0, y: 0 }, @@ -121,7 +122,7 @@ export type FlowState = { export type StoreGet = () => FlowState export type StoreSet = (state: FlowState | Partial | ((state: FlowState) => FlowState | Partial)) => void -function addNodeBetween(nodeOrEdge: string|null, step: any, type: string, set: StoreSet, get: StoreGet) { +function addNodeBetween(nodeOrEdge: string | null, step: any, type: string, set: StoreSet, get: StoreGet) { if (!nodeOrEdge || !step) return; let edge = {} as Edge; if (type === 'node') { @@ -135,11 +136,19 @@ function addNodeBetween(nodeOrEdge: string|null, step: any, type: string, set: S const { source: sourceId, target: targetId } = edge || {}; if (!sourceId || !targetId) return; + const nodes = get().nodes; + const targetIndex = nodes.findIndex(node => node.id === targetId); + const sourceIndex = nodes.findIndex(node => node.id === sourceId); + if (targetIndex == -1) { + return; + } const newNodeId = uuidv4(); const newStep = { ...step, id: newNodeId } - let { nodes, edges } = processWorkflowV2([ - { id: sourceId, type: 'temp_node', name: 'temp_node', 'componentType': 'temp_node', - edgeLabel: edge.label, edgeColor:edge?.style?.stroke}, + let { nodes: newNodes, edges } = processWorkflowV2([ + { + id: sourceId, type: 'temp_node', name: 'temp_node', 'componentType': 'temp_node', + edgeLabel: edge.label, edgeColor: edge?.style?.stroke + }, newStep, { id: targetId, type: 'temp_node', name: 'temp_node', 'componentType': 'temp_node', edgeNotNeeded: true } ], { x: 0, y: 0 }, true); @@ -148,9 +157,14 @@ function addNodeBetween(nodeOrEdge: string|null, step: any, type: string, set: S ...edges, ...(get().edges.filter(edge => !(edge.source == sourceId && edge.target == targetId)) || []), ]; + + const isNested = !!(nodes[targetIndex]?.isNested || nodes[sourceIndex]?.isNested); + newNodes = newNodes.map((node) => ({ ...node, isNested })); + newNodes = [...nodes.slice(0, targetIndex), ...newNodes, ...nodes.slice(targetIndex)]; + set({ edges: newEdges, - nodes: [...get().nodes, ...nodes], + nodes: newNodes, isLayouted: false, }); if (type == 'edge') { @@ -173,10 +187,10 @@ const useStore = create((set, get) => ({ toolboxConfiguration: {} as Record, isLayouted: false, selectedEdge: null, - setSelectedEdge: (id) => set({ selectedEdge: id, selectedNode: null, openGlobalEditor:true }), + setSelectedEdge: (id) => set({ selectedEdge: id, selectedNode: null, openGlobalEditor: true }), setIsLayouted: (isLayouted) => set({ isLayouted }), getEdgeById: (id) => get().edges.find((edge) => edge.id === id), - addNodeBetween: (nodeOrEdge: string|null, step: any, type: string) => { + addNodeBetween: (nodeOrEdge: string | null, step: any, type: string) => { addNodeBetween(nodeOrEdge, step, type, set, get); }, setToolBoxConfig: (config) => set({ toolboxConfiguration: config }), @@ -184,11 +198,14 @@ const useStore = create((set, get) => ({ updateSelectedNodeData: (key, value) => { const currentSelectedNode = get().selectedNode; if (currentSelectedNode) { - const updatedNodes = get().nodes.map((node) => - node.id === currentSelectedNode - ? { ...node, data: { ...node.data, [key]: value } } - : node - ); + const updatedNodes = get().nodes.map((node) => { + if (node.id === currentSelectedNode) { + //properties changes should not reconstructed the defintion. only recontrreconstructing if there are any structural changes are done on the flow. + node.data[key] = value; + return {...node} + } + return node; + }); set({ nodes: updatedNodes }); @@ -266,7 +283,7 @@ const useStore = create((set, get) => ({ try { let step: any = event.dataTransfer.getData("application/reactflow"); - if(!step){ + if (!step) { return; } console.log("step", step); @@ -309,17 +326,45 @@ const useStore = create((set, get) => ({ if (typeof ids !== 'string') { return; } - const idArray = Array.isArray(ids) ? ids : [ids]; - let finalEdges = get().edges.filter((edge) => !idArray.includes(edge.source) && !idArray.includes(edge.target)); - const sources = [...new Set(get().edges.filter((edge) => idArray.includes(edge.target)))]; - const targets = [...new Set(get().edges.filter((edge) => idArray.includes(edge.source)))]; + const nodes = get().nodes + const nodeStartIndex = nodes.findIndex((node) => ids == node.id); + if (nodeStartIndex === -1) { + return; + } + let idArray = Array.isArray(ids) ? ids : [ids]; + + console.log("nodes", nodes); + + const startNode = nodes[nodeStartIndex]; + console.log("startNode", startNode); + const customIdentifier = `${startNode?.data?.type}__end__${startNode?.id}`; + console.log("customIdentifier", customIdentifier); + + let endIndex = nodes.findIndex((node) => node.id === customIdentifier); + endIndex = endIndex === -1 ? nodeStartIndex : endIndex; + console.log("endIndex", endIndex); + + const endNode = nodes[endIndex]; + console.log("endNode", endNode); + + const edges = get().edges; + let finalEdges = edges; + idArray = nodes.slice(nodeStartIndex, endIndex + 1).map((node) => node.id); + console.log("idArray", idArray); + finalEdges = edges.filter((edge) => !(idArray.includes(edge.source) || idArray.includes(edge.target))); + + const sources = [...new Set(edges.filter((edge) => startNode.id === edge.target))]; + const targets = [...new Set(edges.filter((edge) => endNode.id === edge.source))]; targets.forEach((edge) => { - finalEdges = [...finalEdges, ...sources.map((source) => ({ ...edge, source: source.source, id: `e${source.source}-${edge.target}` }))]; + finalEdges = [...finalEdges, ...sources.map((source) => createCustomEdgeMeta(source.source, edge.target, source.label) + )]; }); + const newNodes = [...nodes.slice(0, nodeStartIndex), ...nodes.slice(endIndex + 1)]; + set({ edges: finalEdges, - nodes: get().nodes.filter((node) => !idArray.includes(node.id)), + nodes: newNodes, selectedNode: null, isLayouted: false }); diff --git a/keep-ui/app/workflows/builder/builder.tsx b/keep-ui/app/workflows/builder/builder.tsx index 229fd8292..fdc3e6032 100644 --- a/keep-ui/app/workflows/builder/builder.tsx +++ b/keep-ui/app/workflows/builder/builder.tsx @@ -40,6 +40,7 @@ import BuilderWorkflowTestRunModalContent from "./builder-workflow-testrun-modal import { WorkflowExecution, WorkflowExecutionFailure } from "./types"; import ReactFlowBuilder from "./ReactFlowBuilder"; import { ReactFlowProvider } from "@xyflow/react"; +import BuilderChanagesTracker from "./BuilderChanagesTracker"; interface Props { loadedAlertFile: string | null; @@ -316,19 +317,22 @@ function Builder({ return ( <> -
    - - +
    +
    + + +
    + {useReactFlow && }
    {generateModalIsOpen || testRunModalOpen ? null : ( <> - {getworkflowStatus()} + {getworkflowStatus()} {useReactFlow && (
    @@ -361,11 +365,13 @@ function Builder({ loadedAlertFile={loadedAlertFile} providers={providers} definition={definition} - onDefinitionChange={(def: any) => setDefinition(wrapDefinition(def))} + onDefinitionChange={(def: any) => + setDefinition(wrapDefinition(def)) + } toolboxConfiguration={getToolboxConfiguration(providers)} /> -
    +
    )} {!useReactFlow && ( <> diff --git a/keep-ui/utils/reactFlow.ts b/keep-ui/utils/reactFlow.ts index 9df2ab8cd..3625ea895 100644 --- a/keep-ui/utils/reactFlow.ts +++ b/keep-ui/utils/reactFlow.ts @@ -1,25 +1,68 @@ import { v4 as uuidv4 } from "uuid"; import { FlowNode, V2Step } from "app/workflows/builder/builder-store"; import { Edge } from "@xyflow/react"; +import { CorrelationForm as CorrelationFormType } from '.'; +function getKeyBasedSquence(step:any, id:string, type:string) { + return `${step.type}__${id}__empty_${type}`; +} + +export function reConstructWorklowToDefinition({ + nodes, + edges, isNested = false}:{ + nodes: FlowNode[], + edges: Edge[] + isNested?:boolean + }) { + const seuqences = []; + //ingoring the start node + const [first, ...rest] = nodes; + //poping the end node + rest.pop(); + const edgeMap: Record = {}; + const nodeMap: Record = {}; + edges.forEach((edge) => { + const { source, target } = edge; + + if (edgeMap[source]) { + edgeMap[source].push(target); + } else { + edgeMap[source] = [target]; + } + }); + + nodes.forEach((node) => { + nodeMap[node.id] = node; + }); + + const sequences = nodes.filter((node) => !node.isNested && !node.id.includes('end')).map((node) => node.data); + console.log("sequences in recontructWorklowToDefinition", sequences) + +} + export function createSwitchNodeV2( step: any, nodeId: string, position: { x: number; y: number }, nextNodeId?: string | null, - prevNodeId?: string | null + prevNodeId?: string | null, + isNested?: boolean, ): FlowNode[] { const customIdentifier = `${step.type}__end__${nodeId}`; + const { name, type, componentType, properties} = step; return [ { id: nodeId, type: "custom", position: { x: 0, y: 0 }, data: { - label: step.name, - ...step, + label: name, + type, + componentType, + id: nodeId, + properties, }, isDraggable: false, prevNodeId, @@ -27,7 +70,8 @@ export function createSwitchNodeV2( dragHandle: ".custom-drag-handle", style: { margin: "0px 20px 0px 20px", - } + }, + isNested: !!isNested }, { id: customIdentifier, @@ -42,13 +86,14 @@ export function createSwitchNodeV2( prevNodeId: nodeId, nextNodeId: nextNodeId, dragHandle: ".custom-drag-handle", + isNested: !!isNested }, ]; }; -export function handleSwitchNode(step, position, nextNodeId, prevNodeId, nodeId) { +export function handleSwitchNode(step, position, nextNodeId, prevNodeId, nodeId, isNested) { if (step.componentType !== "switch") { return { nodes: [], edges: [] }; } @@ -64,10 +109,11 @@ export function handleSwitchNode(step, position, nextNodeId, prevNodeId, nodeId) componentType: key, name: "empty", properties: {}, + isNested: true, } } - let [switchStartNode, switchEndNode] = createSwitchNodeV2(step, nodeId, position, nextNodeId, prevNodeId); + let [switchStartNode, switchEndNode] = createSwitchNodeV2(step, nodeId, position, nextNodeId, prevNodeId, isNested); trueBranches = [ { ...switchStartNode.data, type: 'temp_node', componentType: "temp_node" }, ...trueBranches, @@ -85,9 +131,9 @@ export function handleSwitchNode(step, position, nextNodeId, prevNodeId, nodeId) let falsePostion = { x: position.x + 200, y: position.y - 100 }; let { nodes: trueBranchNodes, edges: trueSubflowEdges } = - processWorkflowV2(trueBranches, truePostion) || {}; + processWorkflowV2(trueBranches, truePostion, false, true) || {}; let { nodes: falseSubflowNodes, edges: falseSubflowEdges } = - processWorkflowV2(falseBranches, falsePostion) || {}; + processWorkflowV2(falseBranches, falsePostion, false, true) || {}; function _adjustEdgeConnectionsAndLabelsForSwitch(type: string) { if (!type) { @@ -126,7 +172,8 @@ export const createDefaultNodeV2 = ( nodeId: string, position: { x: number; y: number }, nextNodeId?: string | null, - prevNodeId?: string | null + prevNodeId?: string | null, + isNested?: boolean, ): FlowNode => ({ id: nodeId, @@ -140,6 +187,7 @@ export const createDefaultNodeV2 = ( isDraggable: false, nextNodeId, prevNodeId, + isNested: !!isNested } as FlowNode); const getRandomColor = () => { @@ -161,7 +209,7 @@ export function createCustomEdgeMeta(source: string, target: string, label?: str style: { stroke: color || getRandomColor() } } } -export function handleDefaultNode(step, position, nextNodeId, prevNodeId, nodeId) { +export function handleDefaultNode(step, position, nextNodeId, prevNodeId, nodeId, isNested) { const nodes = []; const edges = []; const newNode = createDefaultNodeV2( @@ -169,7 +217,8 @@ export function handleDefaultNode(step, position, nextNodeId, prevNodeId, nodeId nodeId, position, nextNodeId, - prevNodeId + prevNodeId, + isNested ); if (step.type !== 'temp_node') { nodes.push(newNode); @@ -181,7 +230,7 @@ export function handleDefaultNode(step, position, nextNodeId, prevNodeId, nodeId return { nodes, edges }; } -export function getForEachNode(step, position, nodeId, prevNodeId, nextNodeId, parents = []) { +export function getForEachNode(step, position, nodeId, prevNodeId, nextNodeId, isNested) { const { sequence, ...rest } = step; const customIdentifier = `${step.type}__end__${nodeId}`; @@ -194,7 +243,8 @@ export function getForEachNode(step, position, nodeId, prevNodeId, nextNodeId, p isDraggable: false, dragHandle: ".custom-drag-handle", prevNodeId: prevNodeId, - nextNodeId: nextNodeId + nextNodeId: nextNodeId, + isNested: !!isNested, }, { id: customIdentifier, @@ -205,14 +255,15 @@ export function getForEachNode(step, position, nodeId, prevNodeId, nextNodeId, p dragHandle: ".custom-drag-handle", prevNodeId: prevNodeId, nextNodeId: nextNodeId, + isNested: !!isNested }, ]; } -export function handleForeachNode(step, position, nextNodeId, prevNodeId, nodeId) { +export function handleForeachNode(step, position, nextNodeId, prevNodeId, nodeId, isNested) { - const [forEachStartNode, forEachEndNode] = getForEachNode(step, position, nodeId, prevNodeId, nextNodeId); + const [forEachStartNode, forEachEndNode] = getForEachNode(step, position, nodeId, prevNodeId, nextNodeId, isNested); function _getEmptyNode(type: string) { const key = `empty_${type}` @@ -222,7 +273,7 @@ export function handleForeachNode(step, position, nextNodeId, prevNodeId, nodeId componentType: key, name: "empty", properties: {}, - parents: [nodeId] + isNested: true, } } const sequences = [ @@ -233,7 +284,7 @@ export function handleForeachNode(step, position, nextNodeId, prevNodeId, nodeId { id: forEachEndNode.id, type: "temp_node", componentType: "temp_node", name: "temp_node", properties: {} }, { id: nextNodeId, type: "temp_node", componentType: "temp_node", name: "temp_node", properties: {}, edgeNotNeeded: true }, ]; - const { nodes, edges } = processWorkflowV2(sequences, position); + const { nodes, edges } = processWorkflowV2(sequences, position, false, true); return { nodes: [forEachStartNode, ...nodes, forEachEndNode], edges: edges }; } @@ -243,6 +294,7 @@ export const processStepV2 = ( position: { x: number; y: number }, nextNodeId?: string | null, prevNodeId?: string | null, + isNested?: boolean ) => { const nodeId = step.id; let newNodes: FlowNode[] = []; @@ -250,21 +302,21 @@ export const processStepV2 = ( switch (true) { case step?.componentType === "switch": { - const { nodes, edges } = handleSwitchNode(step, position, nextNodeId, prevNodeId, nodeId); + const { nodes, edges } = handleSwitchNode(step, position, nextNodeId, prevNodeId, nodeId, isNested); newEdges = [...newEdges, ...edges]; newNodes = [...newNodes, ...nodes]; break; } case step?.componentType === "container" && step?.type === "foreach": { - const { nodes, edges } = handleForeachNode(step, position, nextNodeId, prevNodeId, nodeId); + const { nodes, edges } = handleForeachNode(step, position, nextNodeId, prevNodeId, nodeId, isNested); newEdges = [...newEdges, ...edges]; newNodes = [...newNodes, ...nodes]; break; } default: { - const { nodes, edges } = handleDefaultNode(step, position, nextNodeId, prevNodeId, nodeId); + const { nodes, edges } = handleDefaultNode(step, position, nextNodeId, prevNodeId, nodeId, isNested); newEdges = [...newEdges, ...edges]; newNodes = [...newNodes, ...nodes]; break; @@ -274,7 +326,7 @@ export const processStepV2 = ( return { nodes: newNodes, edges: newEdges }; }; -export const processWorkflowV2 = (sequence: any, position: { x: number, y: number }, isFirstRender = false) => { +export const processWorkflowV2 = (sequence: any, position: { x: number, y: number }, isFirstRender = false, isNested = false) => { let newNodes: FlowNode[] = []; let newEdges: Edge[] = []; @@ -286,7 +338,8 @@ export const processWorkflowV2 = (sequence: any, position: { x: number, y: numbe step, position, nextNodeId, - prevNodeId + prevNodeId, + isNested, ); newNodes = [...newNodes, ...nodes]; newEdges = [...newEdges, ...edges]; From c418246473e8c217375322f8e8989c97b2c8ecc3 Mon Sep 17 00:00:00 2001 From: Rajesh Jonnalagadda Date: Sat, 10 Aug 2024 11:25:52 +0530 Subject: [PATCH 12/55] feat:added recontruction of defintion from nodes data --- .../builder/BuilderChanagesTracker.tsx | 16 +- keep-ui/app/workflows/builder/builder.tsx | 6 +- keep-ui/utils/reactFlow.ts | 140 ++++++++++++++---- 3 files changed, 124 insertions(+), 38 deletions(-) diff --git a/keep-ui/app/workflows/builder/BuilderChanagesTracker.tsx b/keep-ui/app/workflows/builder/BuilderChanagesTracker.tsx index 2b5425a81..9f69d5372 100644 --- a/keep-ui/app/workflows/builder/BuilderChanagesTracker.tsx +++ b/keep-ui/app/workflows/builder/BuilderChanagesTracker.tsx @@ -4,13 +4,12 @@ import { Button } from '@tremor/react'; import { Edge } from '@xyflow/react'; import { reConstructWorklowToDefinition } from 'utils/reactFlow'; -export default function BuilderChanagesTracker() { +export default function BuilderChanagesTracker({onDefinitionChange}:{onDefinitionChange:(def: WrappedDefinition) => void}) { const {nodes, edges,setEdges, setNodes, isLayouted, setIsLayouted, v2Properties} = useStore(); const [changes, setChanges] = useState(0); const [savedChanges, setSavedChanges] = useState(0); const [lastSavedChanges, setLastSavedChanges] = useState<{nodes:FlowNode[], edges:Edge[]}>({nodes: nodes, edges: edges}); const [firstInitilisationDone, setFirstInitilisationDone] = useState(false); - const [proprtiesUpdated, setPropertiesUpdated] = useState(false); console.log("isLayouted", isLayouted); @@ -20,32 +19,29 @@ export default function BuilderChanagesTracker() { } if(isLayouted && !firstInitilisationDone) { setFirstInitilisationDone(true); + setChanges(0); setLastSavedChanges({nodes: nodes, edges: edges}); } },[isLayouted]) - useEffect(()=>{ - reConstructWorklowToDefinition(lastSavedChanges); - }, [lastSavedChanges]) - const handleDiscardChanges = (e: React.MouseEvent) => { if(!isLayouted) return; setEdges(lastSavedChanges.edges || []); setNodes(lastSavedChanges.nodes || []); setChanges(0); - setFirstInitilisationDone(false); setIsLayouted(false); } const handleSaveChanges = (e: React.MouseEvent) =>{ e.preventDefault(); e.stopPropagation(); + setChanges(0); setSavedChanges((prev)=>(prev+1 || 0)); setLastSavedChanges({nodes: nodes, edges: edges}); - setChanges(0); - setPropertiesUpdated(false); + const value = reConstructWorklowToDefinition({nodes: nodes, edges: edges, properties: v2Properties}); + onDefinitionChange(value); } @@ -59,7 +55,7 @@ export default function BuilderChanagesTracker() { >Discard{changes ? `(${changes})`: ""}
    ) diff --git a/keep-ui/app/workflows/builder/builder.tsx b/keep-ui/app/workflows/builder/builder.tsx index fdc3e6032..cd5767fa5 100644 --- a/keep-ui/app/workflows/builder/builder.tsx +++ b/keep-ui/app/workflows/builder/builder.tsx @@ -332,7 +332,11 @@ function Builder({ Switch to New Builder
    - {useReactFlow && } + {useReactFlow && + setDefinition(wrapDefinition(def)) + } + />}
    }) { - const seuqences = []; - //ingoring the start node - const [first, ...rest] = nodes; - //poping the end node - rest.pop(); - const edgeMap: Record = {}; - const nodeMap: Record = {}; - edges.forEach((edge) => { - const { source, target } = edge; - - if (edgeMap[source]) { - edgeMap[source].push(target); - } else { - edgeMap[source] = [target]; + + const originalNodes = nodes.slice(1, nodes.length-1); + function processForeach(startIdx:number, endIdx:number, foreachNode:FlowNode['data'], nodeId:string) { + foreachNode.sequence = []; + + const tempSequence = []; + const foreachEmptyId = `${foreachNode.type}__${nodeId}__empty_foreach`; + + for (let i = startIdx; i < endIdx; i++) { + const currentNode = originalNodes[i]; + const nodeData = currentNode?.data; + const nodeType = nodeData?.type; + if (currentNode.id === foreachEmptyId) { + foreachNode.sequence = tempSequence; + return i + 1; + } + + if (["condition-threshold", "condition-assert"].includes(nodeType)) { + tempSequence.push(nodeData); + i = processCondition(i + 1, endIdx, nodeData, currentNode.id); + continue; + } + + if (nodeType === "foreach") { + tempSequence.push(nodeData); + i = processForeach(i + 1, endIdx, nodeData, currentNode.id); + continue; + } + + tempSequence.push(nodeData); + } + return endIdx; + } + + function processCondition(startIdx:number, endIdx:number, conditionNode:FlowNode['data'], nodeId:string) { + conditionNode.branches = { + true: [], + false: [], + }; + + const trueBranchEmptyId = `${conditionNode?.type}__${nodeId}__empty_true`; + const falseBranchEmptyId = `${conditionNode?.type}__${nodeId}__empty_false`; + let tempSeqs = []; + let trueCaseAdded = false; + let falseCaseAdded = false; + let tempSequence = []; + let i = startIdx; + for (; i < endIdx; i++) { + const currentNode = originalNodes[i]; + const nodeData = currentNode?.data; + const nodeType = nodeData?.type; + + if (trueCaseAdded && falseCaseAdded) { + return i; + } + if (currentNode.id === trueBranchEmptyId) { + conditionNode.branches.true = tempSequence; + trueCaseAdded = true; + tempSequence = []; + continue; + } + + if (currentNode.id === falseBranchEmptyId) { + conditionNode.branches.false = tempSequence; + falseCaseAdded = true; + tempSequence = []; + continue; + } + + if (["condition-threshold", "condition-assert"].includes(nodeType)) { + tempSequence.push(nodeData); + i = processCondition(i + 1, endIdx, nodeData, currentNode.id); + continue; + } + + if (nodeType === "foreach") { + tempSequence.push(nodeData); + i = processForeach(i + 1, endIdx, nodeData, currentNode.id); + continue; + } + tempSequence.push(nodeData); + } + return endIdx; + } + + function buildWorkflowDefinition(startIdx:number, endIdx:number) { + const workflowSequence = []; + for (let i = startIdx; i < endIdx; i++) { + const currentNode = originalNodes[i]; + const nodeData = currentNode?.data; + const nodeType = nodeData?.type; + if (["condition-threshold", "condition-assert"].includes(nodeType)) { + workflowSequence.push(nodeData); + i = processCondition(i + 1, endIdx, nodeData, currentNode.id); + continue; + } + if (nodeType === "foreach") { + workflowSequence.push(nodeData); + i = processForeach(i + 1, endIdx, nodeData, currentNode.id); + continue; + } + workflowSequence.push(nodeData); + } + return workflowSequence; + } + + return { + sequence: buildWorkflowDefinition(0, originalNodes.length), + properties: properties } - }); - - nodes.forEach((node) => { - nodeMap[node.id] = node; - }); - - const sequences = nodes.filter((node) => !node.isNested && !node.id.includes('end')).map((node) => node.data); - console.log("sequences in recontructWorklowToDefinition", sequences) } From 4b283c0cff2e6c055ac78ff02fbd05434444badd Mon Sep 17 00:00:00 2001 From: Bhavya Jain Date: Sat, 10 Aug 2024 13:35:36 +0530 Subject: [PATCH 13/55] Fix rerendering --- keep-ui/app/workflows/builder/editors.tsx | 91 ++++++++++++++--------- 1 file changed, 54 insertions(+), 37 deletions(-) diff --git a/keep-ui/app/workflows/builder/editors.tsx b/keep-ui/app/workflows/builder/editors.tsx index d62ff797e..24326109b 100644 --- a/keep-ui/app/workflows/builder/editors.tsx +++ b/keep-ui/app/workflows/builder/editors.tsx @@ -418,40 +418,50 @@ export function StepEditorV2({ installedProviders?: Provider[] | undefined | null; }) { const [useGlobalEditor, setGlobalEditor] = useState(false); + const [formData, setFormData] = useState<{ name?: string; properties?: any }>({}); const { selectedNode, updateSelectedNodeData, setOpneGlobalEditor, getNodeById - } = useStore() + } = useStore(); - if (!selectedNode) return null; - - - - const {data} = getNodeById(selectedNode) || {}; - const {name, type, properties} = data || {}; + useEffect(() => { + if (selectedNode) { + const { data } = getNodeById(selectedNode) || {}; + const { name, type, properties } = data || {}; + setFormData({ name, type , properties }); + } + }, [selectedNode, getNodeById]); - function onNameChanged(e: any) { - updateSelectedNodeData( "name", e.target.value); - } + if (!selectedNode) return null; - const setProperty = (key:string, value:any) => { - updateSelectedNodeData('properties', {...properties, [key]: value }) - } + const providerType = formData?.type?.split("-")[1]; - const providerType = type?.split("-")[1]; + const handleInputChange = (e: React.ChangeEvent) => { + setFormData({ ...formData, [e.target.name]: e.target.value }); + }; - if(!selectedNode){ - return - Node not found! - - } + const handlePropertyChange = (key: string, value: any) => { + setFormData({ + ...formData, + properties: { ...formData.properties, [key]: value }, + }); + }; - const handleSwitchChange = (value:boolean)=>{ + const handleSwitchChange = (value: boolean) => { setGlobalEditor(value); setOpneGlobalEditor(true); - } + }; + + const handleSubmit = () => { + // Finalize the changes before saving + updateSelectedNodeData('name', formData.name); + updateSelectedNodeData('properties', formData.properties); + + // Perform any additional save logic, such as API calls + console.log('Final data saved:', formData); + }; return ( @@ -467,40 +477,47 @@ export function StepEditorV2({ className="text-tremor-default text-tremor-content dark:text-dark-tremor-content" > Switch to Global Editor - +
    {providerType} Editor Unique Identifier - {type.includes("step-") || type.includes("action-") ? ( + {formData.type?.includes("step-") || formData.type?.includes("action-") ? ( - ) : type === "condition-threshold" ? ( + ) : formData.type === "condition-threshold" ? ( - ) : type.includes("foreach") ? ( + ) : formData.type?.includes("foreach") ? ( - ) : type === "condition-assert" ? ( + ) : formData.type === "condition-assert" ? ( ) : null} + ); } From 1bab77bbbf2adb2cab6f56aa70896e0303391e5b Mon Sep 17 00:00:00 2001 From: Bhavya Jain Date: Sat, 10 Aug 2024 15:17:18 +0530 Subject: [PATCH 14/55] Fix rerendering --- keep-ui/app/workflows/builder/editors.tsx | 82 +++++++++++++++++------ 1 file changed, 61 insertions(+), 21 deletions(-) diff --git a/keep-ui/app/workflows/builder/editors.tsx b/keep-ui/app/workflows/builder/editors.tsx index 24326109b..a85640234 100644 --- a/keep-ui/app/workflows/builder/editors.tsx +++ b/keep-ui/app/workflows/builder/editors.tsx @@ -50,7 +50,14 @@ export function GlobalEditor() { } export function GlobalEditorV2() { - const { v2Properties:properties, updateV2Properties: setProperty } = useStore(); + const { v2Properties: properties, updateV2Properties: setProperty } = useStore(); + const [localProperties, setLocalProperties] = useState(properties); + + const handleSubmit = () => { + // Save the finalized properties + setProperty(localProperties); + console.log('Final properties saved:', localProperties); + }; return ( @@ -60,15 +67,25 @@ export function GlobalEditorV2() { workflow YAML specifications. - Use the toolbox to add steps, conditions and actions to your workflow + Use the toolbox to add steps, conditions, and actions to your workflow and click the `Generate` button to compile the workflow / `Deploy` button to deploy the workflow to Keep. - {WorkflowEditor(properties, setProperty)} + + ); } + interface keepEditorProps { properties: Properties; updateProperty: ((key: string, value: any) => void); @@ -267,19 +284,23 @@ function KeepForeachEditor({ properties, updateProperty }: keepEditorProps) { ); } -function WorkflowEditor(properties: Properties, updateProperty: any) { - /** - * TODO: support generate, add more triggers and complex filters - * Need to think about UX for this - */ - const propertyKeys = Object.keys(properties).filter( - (k) => k !== "isLocked" && k !== "id" - ); +function WorkflowEditor({ + initialProperties, + onUpdate, +}: { + initialProperties: Properties; + onUpdate: (updatedProperties: Properties) => void; +}) { + const [properties, setProperties] = useState(initialProperties); + + useEffect(() => { + setProperties(initialProperties); + }, [initialProperties]); const updateAlertFilter = (filter: string, value: string) => { - const currentFilters = properties.alert as {}; + const currentFilters = properties.alert || {}; const updatedFilters = { ...currentFilters, [filter]: value }; - updateProperty("alert", updatedFilters); + setProperties({ ...properties, alert: updatedFilters }); }; const addFilter = () => { @@ -290,18 +311,31 @@ function WorkflowEditor(properties: Properties, updateProperty: any) { }; const addTrigger = (trigger: "manual" | "interval" | "alert") => { - updateProperty( - trigger, - trigger === "alert" ? { source: "" } : trigger === "manual" ? "true" : "" - ); + setProperties({ + ...properties, + [trigger]: + trigger === "alert" + ? { source: "" } + : trigger === "manual" + ? "true" + : "", + }); }; const deleteFilter = (filter: string) => { - const currentFilters = properties.alert as any; + const currentFilters = { ...properties.alert }; delete currentFilters[filter]; - updateProperty("alert", currentFilters); + setProperties({ ...properties, alert: currentFilters }); }; + const propertyKeys = Object.keys(properties).filter( + (k) => k !== "isLocked" && k !== "id" + ); + + useEffect(() => { + onUpdate(properties); + }, [properties]); + return ( <> Workflow Settings @@ -353,7 +387,10 @@ function WorkflowEditor(properties: Properties, updateProperty: any) { type="checkbox" checked={properties[key] === "true"} onChange={(e) => - updateProperty(key, e.target.checked ? "true" : "false") + setProperties({ + ...properties, + [key]: e.target.checked ? "true" : "false", + }) } />
    @@ -400,7 +437,9 @@ function WorkflowEditor(properties: Properties, updateProperty: any) { ) : ( updateProperty(key, e.target.value)} + onChange={(e: any) => + setProperties({ ...properties, [key]: e.target.value }) + } value={properties[key] as string} /> )} @@ -412,6 +451,7 @@ function WorkflowEditor(properties: Properties, updateProperty: any) { } + export function StepEditorV2({ installedProviders, }: { From 25c3b6fe3e94e787c06a299116548881d5862356 Mon Sep 17 00:00:00 2001 From: Rajesh Jonnalagadda Date: Sat, 10 Aug 2024 15:38:51 +0530 Subject: [PATCH 15/55] fix:discard coutn and disable fix and properties form fix --- .../builder/BuilderChanagesTracker.tsx | 41 ++++++++----------- keep-ui/app/workflows/builder/CustomNode.tsx | 7 ++-- .../app/workflows/builder/builder-store.tsx | 19 ++++++++- .../utils/hooks/useWorkflowInitialization.ts | 15 ++++++- 4 files changed, 50 insertions(+), 32 deletions(-) diff --git a/keep-ui/app/workflows/builder/BuilderChanagesTracker.tsx b/keep-ui/app/workflows/builder/BuilderChanagesTracker.tsx index 9f69d5372..eb5bd6d9c 100644 --- a/keep-ui/app/workflows/builder/BuilderChanagesTracker.tsx +++ b/keep-ui/app/workflows/builder/BuilderChanagesTracker.tsx @@ -5,26 +5,18 @@ import { Edge } from '@xyflow/react'; import { reConstructWorklowToDefinition } from 'utils/reactFlow'; export default function BuilderChanagesTracker({onDefinitionChange}:{onDefinitionChange:(def: WrappedDefinition) => void}) { - const {nodes, edges,setEdges, setNodes, isLayouted, setIsLayouted, v2Properties} = useStore(); - const [changes, setChanges] = useState(0); - const [savedChanges, setSavedChanges] = useState(0); - const [lastSavedChanges, setLastSavedChanges] = useState<{nodes:FlowNode[], edges:Edge[]}>({nodes: nodes, edges: edges}); - const [firstInitilisationDone, setFirstInitilisationDone] = useState(false); - - console.log("isLayouted", isLayouted); - - useEffect(()=>{ - if(isLayouted && firstInitilisationDone) { - setChanges((prev)=>prev+1); - } - if(isLayouted && !firstInitilisationDone) { - setFirstInitilisationDone(true); - setChanges(0); - setLastSavedChanges({nodes: nodes, edges: edges}); - } - },[isLayouted]) - - + const {nodes, + edges, + setEdges, + setNodes, + isLayouted, + setIsLayouted, + v2Properties, + changes, + setChanges, + lastSavedChanges, + setLastSavedChanges + } = useStore(); const handleDiscardChanges = (e: React.MouseEvent) => { if(!isLayouted) return; @@ -37,11 +29,10 @@ export default function BuilderChanagesTracker({onDefinitionChange}:{onDefinitio const handleSaveChanges = (e: React.MouseEvent) =>{ e.preventDefault(); e.stopPropagation(); - setChanges(0); - setSavedChanges((prev)=>(prev+1 || 0)); setLastSavedChanges({nodes: nodes, edges: edges}); const value = reConstructWorklowToDefinition({nodes: nodes, edges: edges, properties: v2Properties}); onDefinitionChange(value); + setChanges(0); } @@ -51,12 +42,12 @@ export default function BuilderChanagesTracker({onDefinitionChange}:{onDefinitio
    + disabled={changes === 0} + >Save
    ) } diff --git a/keep-ui/app/workflows/builder/CustomNode.tsx b/keep-ui/app/workflows/builder/CustomNode.tsx index 72eee7bd3..c563276e7 100644 --- a/keep-ui/app/workflows/builder/CustomNode.tsx +++ b/keep-ui/app/workflows/builder/CustomNode.tsx @@ -43,7 +43,6 @@ function CustomNode({ id, data }: FlowNode) { onClick={(e) => { e.stopPropagation(); if (type === 'start' || type === 'end' || id?.includes('end')) { - setSelectedNode(null); setOpneGlobalEditor(true); return; } @@ -98,7 +97,7 @@ function CustomNode({ id, data }: FlowNode) { onClick={(e) => { e.stopPropagation(); if (type === 'start' || type === 'end' || id?.includes('end')) { - setSelectedNode(null); + setOpneGlobalEditor(true); return; } setSelectedNode(id); @@ -122,13 +121,13 @@ function CustomNode({ id, data }: FlowNode) {
    } - {['start', 'threshold', 'assert', 'foreach'].includes(type) && } - {['end', 'threshold', 'assert', 'foreach'].includes(type) && void; getEdgeById: (id: string) => Edge | undefined; + changes: number; + setChanges: (changes: number)=>void; + firstInitilisationDone: boolean; + setFirstInitilisationDone: (firstInitilisationDone: boolean) => void; + lastSavedChanges: {nodes: FlowNode[] | null, edges: Edge[] | null}; + setLastSavedChanges: ({nodes, edges}: {nodes: FlowNode[], edges: Edge[]}) => void; }; @@ -166,6 +172,7 @@ function addNodeBetween(nodeOrEdge: string | null, step: any, type: string, set: edges: newEdges, nodes: newNodes, isLayouted: false, + changes: get().changes + 1 }); if (type == 'edge') { set({ selectedEdge: edges[edges.length - 1]?.id }); @@ -187,7 +194,13 @@ const useStore = create((set, get) => ({ toolboxConfiguration: {} as Record, isLayouted: false, selectedEdge: null, + changes: 0, + lastSavedChanges:{nodes: [], edges:[]}, + firstInitilisationDone: false, + setFirstInitilisationDone: (firstInitilisationDone) => set({ firstInitilisationDone }), + setLastSavedChanges:({nodes, edges}:{nodes:FlowNode[],edges:Edge[]})=>set({lastSavedChanges: {nodes, edges}}), setSelectedEdge: (id) => set({ selectedEdge: id, selectedNode: null, openGlobalEditor: true }), + setChanges: (changes:number)=>set({changes: changes}), setIsLayouted: (isLayouted) => set({ isLayouted }), getEdgeById: (id) => get().edges.find((edge) => edge.id === id), addNodeBetween: (nodeOrEdge: string | null, step: any, type: string) => { @@ -207,7 +220,8 @@ const useStore = create((set, get) => ({ return node; }); set({ - nodes: updatedNodes + nodes: updatedNodes, + changes: get().changes + 1 }); } }, @@ -366,7 +380,8 @@ const useStore = create((set, get) => ({ edges: finalEdges, nodes: newNodes, selectedNode: null, - isLayouted: false + isLayouted: false, + changes: get().changes + 1 }); }, updateEdge: (id: string, key: string, value: any) => { diff --git a/keep-ui/utils/hooks/useWorkflowInitialization.ts b/keep-ui/utils/hooks/useWorkflowInitialization.ts index 262a2f35a..f333d0b8e 100644 --- a/keep-ui/utils/hooks/useWorkflowInitialization.ts +++ b/keep-ui/utils/hooks/useWorkflowInitialization.ts @@ -130,7 +130,12 @@ const useWorkflowInitialization = ( selectedNode, setToolBoxConfig, isLayouted, - setIsLayouted + setIsLayouted, + setLastSavedChanges, + changes, + setChanges, + firstInitilisationDone, + setFirstInitilisationDone } = useStore(); const [isLoading, setIsLoading] = useState(true); @@ -195,10 +200,18 @@ const useWorkflowInitialization = ( if (!isLayouted && nodes.length > 0) { onLayout({ direction: 'DOWN' }) setIsLayouted(true) + if(!firstInitilisationDone){ + setFirstInitilisationDone(true) + setLastSavedChanges({nodes: nodes, edges: edges}); + setChanges(0) + } } if (!isLayouted && nodes.length === 0) { setIsLayouted(true); + if(!firstInitilisationDone){ + setChanges(0) + } } // window.requestAnimationFrame(() => { // fitView(); From f2eed7cc06765ff83682364d1dea7a0b5ba91dc1 Mon Sep 17 00:00:00 2001 From: Rajesh Jonnalagadda Date: Sat, 10 Aug 2024 16:52:16 +0530 Subject: [PATCH 16/55] fix:globaleditor form issue is handled --- keep-ui/app/workflows/builder/CustomNode.tsx | 5 +- .../app/workflows/builder/builder-store.tsx | 8 +- keep-ui/app/workflows/builder/editors.tsx | 147 +++++++++++++++++- keep-ui/utils/reactFlow.ts | 12 +- 4 files changed, 157 insertions(+), 15 deletions(-) diff --git a/keep-ui/app/workflows/builder/CustomNode.tsx b/keep-ui/app/workflows/builder/CustomNode.tsx index c563276e7..7c01351f4 100644 --- a/keep-ui/app/workflows/builder/CustomNode.tsx +++ b/keep-ui/app/workflows/builder/CustomNode.tsx @@ -15,6 +15,7 @@ function IconUrlProvider(data: FlowNode["data"]) { return `/icons/${type ?.replace("step-", "") ?.replace("action-", "") + ?.replace("__end", "") ?.replace("condition-", "")}-icon.png`; } @@ -23,14 +24,14 @@ function CustomNode({ id, data }: FlowNode) { const type = data?.type ?.replace("step-", "") ?.replace("action-", "") - ?.replace("condition-", ""); + ?.replace("condition-", "") + ?.replace("__end", ""); const isEmptyNode = !!data?.type?.includes("empty"); const isLayouted = !!data?.isLayouted; const specialNodeCheck = ['start', 'end'].includes(type) - return ( <> {!specialNodeCheck &&
    void; // updateNodeData: (nodeId: string, key: string, value: any) => void; updateSelectedNodeData: (key: string, value: any) => void; - updateV2Properties: (key: string, value: any) => void; + updateV2Properties: (properties: V2Properties) => void; setStepEditorOpenForNode: (nodeId: string | null) => void; updateEdge: (id: string, key: string, value: any) => void; setToolBoxConfig: (config: Record) => void; @@ -226,9 +226,9 @@ const useStore = create((set, get) => ({ } }, setV2Properties: (properties) => set({ v2Properties: properties }), - updateV2Properties: (key, value) => { - const updatedProperties = { ...get().v2Properties, [key]: value }; - set({ v2Properties: updatedProperties }); + updateV2Properties: (properties) => { + const updatedProperties = { ...get().v2Properties, ...properties}; + set({ v2Properties: updatedProperties, changes: get().changes+1 }); }, setSelectedNode: (id) => { set({ diff --git a/keep-ui/app/workflows/builder/editors.tsx b/keep-ui/app/workflows/builder/editors.tsx index a85640234..d677adeb8 100644 --- a/keep-ui/app/workflows/builder/editors.tsx +++ b/keep-ui/app/workflows/builder/editors.tsx @@ -71,7 +71,7 @@ export function GlobalEditorV2() { and click the `Generate` button to compile the workflow / `Deploy` button to deploy the workflow to Keep. - @@ -284,7 +284,150 @@ function KeepForeachEditor({ properties, updateProperty }: keepEditorProps) { ); } -function WorkflowEditor({ +function WorkflowEditor(properties: Properties, updateProperty: any) { + /** + * TODO: support generate, add more triggers and complex filters + * Need to think about UX for this + */ + const propertyKeys = Object.keys(properties).filter( + (k) => k !== "isLocked" && k !== "id" + ); + + const updateAlertFilter = (filter: string, value: string) => { + const currentFilters = properties.alert as {}; + const updatedFilters = { ...currentFilters, [filter]: value }; + updateProperty("alert", updatedFilters); + }; + + const addFilter = () => { + const filterName = prompt("Enter filter name"); + if (filterName) { + updateAlertFilter(filterName, ""); + } + }; + + const addTrigger = (trigger: "manual" | "interval" | "alert") => { + updateProperty( + trigger, + trigger === "alert" ? { source: "" } : trigger === "manual" ? "true" : "" + ); + }; + + const deleteFilter = (filter: string) => { + const currentFilters = properties.alert as any; + delete currentFilters[filter]; + updateProperty("alert", currentFilters); + }; + + return ( + <> + Workflow Settings +
    + {Object.keys(properties).includes("manual") ? null : ( + + )} + {Object.keys(properties).includes("interval") ? null : ( + + )} + {Object.keys(properties).includes("alert") ? null : ( + + )} +
    + {propertyKeys.map((key, index) => { + return ( +
    + {key} + {key === "manual" ? ( +
    + + updateProperty(key, e.target.checked ? "true" : "false") + } + /> +
    + ) : key === "alert" ? ( + <> +
    + +
    + {properties.alert && + Object.keys(properties.alert as {}).map((filter) => { + return ( + <> + {filter} +
    + + updateAlertFilter(filter, e.target.value) + } + value={(properties.alert as any)[filter] as string} + /> + deleteFilter(filter)} + /> +
    + + ); + })} + + ) : ( + updateProperty(key, e.target.value)} + value={properties[key] as string} + /> + )} +
    + ); + })} + + ); +} +function WorkflowEditorV2({ initialProperties, onUpdate, }: { diff --git a/keep-ui/utils/reactFlow.ts b/keep-ui/utils/reactFlow.ts index 20b85d05c..7c5c5447d 100644 --- a/keep-ui/utils/reactFlow.ts +++ b/keep-ui/utils/reactFlow.ts @@ -25,7 +25,7 @@ export function reConstructWorklowToDefinition({ for (let i = startIdx; i < endIdx; i++) { const currentNode = originalNodes[i]; - const nodeData = currentNode?.data; + const {isLayOuted, ...nodeData} = currentNode?.data; const nodeType = nodeData?.type; if (currentNode.id === foreachEmptyId) { foreachNode.sequence = tempSequence; @@ -57,16 +57,14 @@ export function reConstructWorklowToDefinition({ const trueBranchEmptyId = `${conditionNode?.type}__${nodeId}__empty_true`; const falseBranchEmptyId = `${conditionNode?.type}__${nodeId}__empty_false`; - let tempSeqs = []; let trueCaseAdded = false; let falseCaseAdded = false; let tempSequence = []; let i = startIdx; for (; i < endIdx; i++) { const currentNode = originalNodes[i]; - const nodeData = currentNode?.data; + const {isLayOuted, ...nodeData} = currentNode?.data; const nodeType = nodeData?.type; - if (trueCaseAdded && falseCaseAdded) { return i; } @@ -104,7 +102,7 @@ export function reConstructWorklowToDefinition({ const workflowSequence = []; for (let i = startIdx; i < endIdx; i++) { const currentNode = originalNodes[i]; - const nodeData = currentNode?.data; + const {isLayOuted, ...nodeData} = currentNode?.data; const nodeType = nodeData?.type; if (["condition-threshold", "condition-assert"].includes(nodeType)) { workflowSequence.push(nodeData); @@ -166,7 +164,7 @@ export function createSwitchNodeV2( data: { label: "+", id: customIdentifier, - type: `${step.type}__end_node`, + type: `${step.type}__end`, }, isDraggable: false, prevNodeId: nodeId, @@ -334,7 +332,7 @@ export function getForEachNode(step, position, nodeId, prevNodeId, nextNodeId, i }, { id: customIdentifier, - data: { ...rest, id: customIdentifier, name: "foreach end", label: "foreach end" }, + data: { ...rest, id: customIdentifier, name: "foreach end", label: "foreach end", type: `${step.type}__end`}, type: "custom", position: { x: 0, y: 0 }, isDraggable: false, From f6dab2021524b01ed530d9f847ef69563019efc2 Mon Sep 17 00:00:00 2001 From: Bhavya Jain Date: Sat, 10 Aug 2024 18:50:47 +0530 Subject: [PATCH 17/55] Remove console logs --- keep-ui/app/workflows/builder/BuilderChanagesTracker.tsx | 2 -- keep-ui/app/workflows/builder/builder-store.tsx | 9 --------- keep-ui/app/workflows/builder/builder.tsx | 2 -- keep-ui/app/workflows/builder/editors.tsx | 4 ---- keep-ui/utils/helpers.ts | 1 - keep-ui/utils/hooks/useWorkflowInitialization.ts | 4 ---- 6 files changed, 22 deletions(-) diff --git a/keep-ui/app/workflows/builder/BuilderChanagesTracker.tsx b/keep-ui/app/workflows/builder/BuilderChanagesTracker.tsx index 9f69d5372..1c5100232 100644 --- a/keep-ui/app/workflows/builder/BuilderChanagesTracker.tsx +++ b/keep-ui/app/workflows/builder/BuilderChanagesTracker.tsx @@ -11,8 +11,6 @@ export default function BuilderChanagesTracker({onDefinitionChange}:{onDefinitio const [lastSavedChanges, setLastSavedChanges] = useState<{nodes:FlowNode[], edges:Edge[]}>({nodes: nodes, edges: edges}); const [firstInitilisationDone, setFirstInitilisationDone] = useState(false); - console.log("isLayouted", isLayouted); - useEffect(()=>{ if(isLayouted && firstInitilisationDone) { setChanges((prev)=>prev+1); diff --git a/keep-ui/app/workflows/builder/builder-store.tsx b/keep-ui/app/workflows/builder/builder-store.tsx index c74380d36..0b1fda643 100644 --- a/keep-ui/app/workflows/builder/builder-store.tsx +++ b/keep-ui/app/workflows/builder/builder-store.tsx @@ -152,7 +152,6 @@ function addNodeBetween(nodeOrEdge: string | null, step: any, type: string, set: newStep, { id: targetId, type: 'temp_node', name: 'temp_node', 'componentType': 'temp_node', edgeNotNeeded: true } ], { x: 0, y: 0 }, true); - console.log("new nodes, edges", nodes, edges); const newEdges = [ ...edges, ...(get().edges.filter(edge => !(edge.source == sourceId && edge.target == targetId)) || []), @@ -286,7 +285,6 @@ const useStore = create((set, get) => ({ if (!step) { return; } - console.log("step", step); step = JSON.parse(step); if (!step) return; // Use the screenToFlowPosition function to get flow coordinates @@ -333,24 +331,17 @@ const useStore = create((set, get) => ({ } let idArray = Array.isArray(ids) ? ids : [ids]; - console.log("nodes", nodes); - const startNode = nodes[nodeStartIndex]; - console.log("startNode", startNode); const customIdentifier = `${startNode?.data?.type}__end__${startNode?.id}`; - console.log("customIdentifier", customIdentifier); let endIndex = nodes.findIndex((node) => node.id === customIdentifier); endIndex = endIndex === -1 ? nodeStartIndex : endIndex; - console.log("endIndex", endIndex); const endNode = nodes[endIndex]; - console.log("endNode", endNode); const edges = get().edges; let finalEdges = edges; idArray = nodes.slice(nodeStartIndex, endIndex + 1).map((node) => node.id); - console.log("idArray", idArray); finalEdges = edges.filter((edge) => !(idArray.includes(edge.source) || idArray.includes(edge.target))); const sources = [...new Set(edges.filter((edge) => startNode.id === edge.target))]; diff --git a/keep-ui/app/workflows/builder/builder.tsx b/keep-ui/app/workflows/builder/builder.tsx index cd5767fa5..19b873991 100644 --- a/keep-ui/app/workflows/builder/builder.tsx +++ b/keep-ui/app/workflows/builder/builder.tsx @@ -92,8 +92,6 @@ function Builder({ const searchParams = useSearchParams(); - console.log("definition", definition); - const updateWorkflow = () => { const apiUrl = getApiURL(); const url = `${apiUrl}/workflows/${workflowId}`; diff --git a/keep-ui/app/workflows/builder/editors.tsx b/keep-ui/app/workflows/builder/editors.tsx index a85640234..2ff0640e0 100644 --- a/keep-ui/app/workflows/builder/editors.tsx +++ b/keep-ui/app/workflows/builder/editors.tsx @@ -56,7 +56,6 @@ export function GlobalEditorV2() { const handleSubmit = () => { // Save the finalized properties setProperty(localProperties); - console.log('Final properties saved:', localProperties); }; return ( @@ -498,9 +497,6 @@ export function StepEditorV2({ // Finalize the changes before saving updateSelectedNodeData('name', formData.name); updateSelectedNodeData('properties', formData.properties); - - // Perform any additional save logic, such as API calls - console.log('Final data saved:', formData); }; return ( diff --git a/keep-ui/utils/helpers.ts b/keep-ui/utils/helpers.ts index 969d19e95..11bf31af5 100644 --- a/keep-ui/utils/helpers.ts +++ b/keep-ui/utils/helpers.ts @@ -57,7 +57,6 @@ export async function installWebhook(provider: Provider, accessToken: string) { success: `${provider.type} webhook installed 👌`, error: { render({ data }) { - console.log(data); // When the promise reject, data will contains the error return `Webhook installation failed 😢 Error: ${ (data as any).detail diff --git a/keep-ui/utils/hooks/useWorkflowInitialization.ts b/keep-ui/utils/hooks/useWorkflowInitialization.ts index 262a2f35a..4fa1fb62a 100644 --- a/keep-ui/utils/hooks/useWorkflowInitialization.ts +++ b/keep-ui/utils/hooks/useWorkflowInitialization.ts @@ -231,7 +231,6 @@ const useWorkflowInitialization = ( const initializeWorkflow = async () => { setIsLoading(true); let parsedWorkflow = definition?.value; - console.log("parsedWorkflow", parsedWorkflow); setV2Properties(parsedWorkflow?.properties ?? {}); // let { nodes: newNodes, edges: newEdges } = processWorkflow( // parsedWorkflow?.sequence @@ -255,9 +254,6 @@ const useWorkflowInitialization = ( ]; const intialPositon = { x: 0, y: 50 }; let { nodes, edges } = processWorkflowV2(sequences, intialPositon, true); - console.log(nodes, edges); - console.log("nodes", nodes); - console.log("edges", edges); setIsLayouted(false); setNodes(nodes); setEdges(edges); From 6ce3092e510502bead42d63f09e69107779c4f91 Mon Sep 17 00:00:00 2001 From: Bhavya Jain Date: Sat, 10 Aug 2024 18:53:57 +0530 Subject: [PATCH 18/55] remove console logs --- keep-ui/app/workflows/builder/builder-store.tsx | 9 --------- keep-ui/app/workflows/builder/builder.tsx | 2 -- keep-ui/app/workflows/builder/editors.tsx | 4 ---- keep-ui/utils/helpers.ts | 1 - keep-ui/utils/hooks/useWorkflowInitialization.ts | 4 ---- 5 files changed, 20 deletions(-) diff --git a/keep-ui/app/workflows/builder/builder-store.tsx b/keep-ui/app/workflows/builder/builder-store.tsx index f737a836c..32a1c7e4c 100644 --- a/keep-ui/app/workflows/builder/builder-store.tsx +++ b/keep-ui/app/workflows/builder/builder-store.tsx @@ -158,7 +158,6 @@ function addNodeBetween(nodeOrEdge: string | null, step: any, type: string, set: newStep, { id: targetId, type: 'temp_node', name: 'temp_node', 'componentType': 'temp_node', edgeNotNeeded: true } ], { x: 0, y: 0 }, true); - console.log("new nodes, edges", nodes, edges); const newEdges = [ ...edges, ...(get().edges.filter(edge => !(edge.source == sourceId && edge.target == targetId)) || []), @@ -300,7 +299,6 @@ const useStore = create((set, get) => ({ if (!step) { return; } - console.log("step", step); step = JSON.parse(step); if (!step) return; // Use the screenToFlowPosition function to get flow coordinates @@ -347,24 +345,17 @@ const useStore = create((set, get) => ({ } let idArray = Array.isArray(ids) ? ids : [ids]; - console.log("nodes", nodes); - const startNode = nodes[nodeStartIndex]; - console.log("startNode", startNode); const customIdentifier = `${startNode?.data?.type}__end__${startNode?.id}`; - console.log("customIdentifier", customIdentifier); let endIndex = nodes.findIndex((node) => node.id === customIdentifier); endIndex = endIndex === -1 ? nodeStartIndex : endIndex; - console.log("endIndex", endIndex); const endNode = nodes[endIndex]; - console.log("endNode", endNode); const edges = get().edges; let finalEdges = edges; idArray = nodes.slice(nodeStartIndex, endIndex + 1).map((node) => node.id); - console.log("idArray", idArray); finalEdges = edges.filter((edge) => !(idArray.includes(edge.source) || idArray.includes(edge.target))); const sources = [...new Set(edges.filter((edge) => startNode.id === edge.target))]; diff --git a/keep-ui/app/workflows/builder/builder.tsx b/keep-ui/app/workflows/builder/builder.tsx index cd5767fa5..19b873991 100644 --- a/keep-ui/app/workflows/builder/builder.tsx +++ b/keep-ui/app/workflows/builder/builder.tsx @@ -92,8 +92,6 @@ function Builder({ const searchParams = useSearchParams(); - console.log("definition", definition); - const updateWorkflow = () => { const apiUrl = getApiURL(); const url = `${apiUrl}/workflows/${workflowId}`; diff --git a/keep-ui/app/workflows/builder/editors.tsx b/keep-ui/app/workflows/builder/editors.tsx index d677adeb8..054466fd9 100644 --- a/keep-ui/app/workflows/builder/editors.tsx +++ b/keep-ui/app/workflows/builder/editors.tsx @@ -56,7 +56,6 @@ export function GlobalEditorV2() { const handleSubmit = () => { // Save the finalized properties setProperty(localProperties); - console.log('Final properties saved:', localProperties); }; return ( @@ -641,9 +640,6 @@ export function StepEditorV2({ // Finalize the changes before saving updateSelectedNodeData('name', formData.name); updateSelectedNodeData('properties', formData.properties); - - // Perform any additional save logic, such as API calls - console.log('Final data saved:', formData); }; return ( diff --git a/keep-ui/utils/helpers.ts b/keep-ui/utils/helpers.ts index 969d19e95..11bf31af5 100644 --- a/keep-ui/utils/helpers.ts +++ b/keep-ui/utils/helpers.ts @@ -57,7 +57,6 @@ export async function installWebhook(provider: Provider, accessToken: string) { success: `${provider.type} webhook installed 👌`, error: { render({ data }) { - console.log(data); // When the promise reject, data will contains the error return `Webhook installation failed 😢 Error: ${ (data as any).detail diff --git a/keep-ui/utils/hooks/useWorkflowInitialization.ts b/keep-ui/utils/hooks/useWorkflowInitialization.ts index f333d0b8e..0c3b68cf0 100644 --- a/keep-ui/utils/hooks/useWorkflowInitialization.ts +++ b/keep-ui/utils/hooks/useWorkflowInitialization.ts @@ -244,7 +244,6 @@ const useWorkflowInitialization = ( const initializeWorkflow = async () => { setIsLoading(true); let parsedWorkflow = definition?.value; - console.log("parsedWorkflow", parsedWorkflow); setV2Properties(parsedWorkflow?.properties ?? {}); // let { nodes: newNodes, edges: newEdges } = processWorkflow( // parsedWorkflow?.sequence @@ -268,9 +267,6 @@ const useWorkflowInitialization = ( ]; const intialPositon = { x: 0, y: 50 }; let { nodes, edges } = processWorkflowV2(sequences, intialPositon, true); - console.log(nodes, edges); - console.log("nodes", nodes); - console.log("edges", edges); setIsLayouted(false); setNodes(nodes); setEdges(edges); From 22eaff0fa75144abe997996b3f431a5b3a3f2d42 Mon Sep 17 00:00:00 2001 From: Rajesh Jonnalagadda Date: Sun, 11 Aug 2024 01:04:50 +0530 Subject: [PATCH 19/55] chore:minor refactoring --- .../builder/BuilderChanagesTracker.tsx | 9 +-- keep-ui/app/workflows/builder/CustomEdge.tsx | 8 --- keep-ui/app/workflows/builder/CustomNode.tsx | 10 +--- .../app/workflows/builder/ReactFlowEditor.tsx | 4 +- keep-ui/app/workflows/builder/SubFlowNode.tsx | 42 -------------- .../utils/hooks/useWorkflowInitialization.ts | 58 +------------------ keep-ui/utils/reactFlow.ts | 6 +- 7 files changed, 13 insertions(+), 124 deletions(-) delete mode 100644 keep-ui/app/workflows/builder/SubFlowNode.tsx diff --git a/keep-ui/app/workflows/builder/BuilderChanagesTracker.tsx b/keep-ui/app/workflows/builder/BuilderChanagesTracker.tsx index 34ccb227d..135485649 100644 --- a/keep-ui/app/workflows/builder/BuilderChanagesTracker.tsx +++ b/keep-ui/app/workflows/builder/BuilderChanagesTracker.tsx @@ -1,10 +1,11 @@ -import React, { useEffect, useRef, useState } from 'react' -import useStore, { FlowNode } from './builder-store'; +import React from 'react' +import useStore from './builder-store'; import { Button } from '@tremor/react'; -import { Edge } from '@xyflow/react'; import { reConstructWorklowToDefinition } from 'utils/reactFlow'; +import { WrappedDefinition } from 'sequential-workflow-designer-react'; +import { Definition } from 'sequential-workflow-designer'; -export default function BuilderChanagesTracker({onDefinitionChange}:{onDefinitionChange:(def: WrappedDefinition) => void}) { +export default function BuilderChanagesTracker({onDefinitionChange}:{onDefinitionChange:(def: Record) => void}) { const { nodes, edges, diff --git a/keep-ui/app/workflows/builder/CustomEdge.tsx b/keep-ui/app/workflows/builder/CustomEdge.tsx index 45f8e5c6d..ac179962e 100644 --- a/keep-ui/app/workflows/builder/CustomEdge.tsx +++ b/keep-ui/app/workflows/builder/CustomEdge.tsx @@ -38,13 +38,6 @@ const CustomEdge: React.FC = ({ const midpointX = (sourceX + targetX) / 2; const midpointY = (sourceY + targetY) / 2; - // const handleDelete = (e: React.MouseEvent) => { - // e.stopPropagation(); - // e.preventDefault(); - // deleteEdges(id); - // }; - - let dynamicLabel = label; const isLayouted = !!data?.isLayouted; @@ -55,7 +48,6 @@ const CustomEdge: React.FC = ({ }
    } - {'start' === type && } - -
    } - ); } diff --git a/keep-ui/app/workflows/builder/ReactFlowEditor.tsx b/keep-ui/app/workflows/builder/ReactFlowEditor.tsx index 9526eb820..d88c7a914 100644 --- a/keep-ui/app/workflows/builder/ReactFlowEditor.tsx +++ b/keep-ui/app/workflows/builder/ReactFlowEditor.tsx @@ -10,9 +10,9 @@ const ReactFlowEditor = () => { useEffect(() => { if (stepEditorOpenForNode) { - setIsOpen(stepEditorOpenForNode === selectedNode?.id); + setIsOpen(stepEditorOpenForNode === selectedNode); } - }, [stepEditorOpenForNode, selectedNode?.id]); + }, [stepEditorOpenForNode, selectedNode]); return (
    -//
    -//
    -// {data?.type} -//
    -//
    -//
    {data?.label}
    -//
    {data?.componentType}
    -//
    -//
    -// -// -//
    -// ); -// } - -// export default memo(SubFlowNode); diff --git a/keep-ui/utils/hooks/useWorkflowInitialization.ts b/keep-ui/utils/hooks/useWorkflowInitialization.ts index 0c3b68cf0..05294cd38 100644 --- a/keep-ui/utils/hooks/useWorkflowInitialization.ts +++ b/keep-ui/utils/hooks/useWorkflowInitialization.ts @@ -5,12 +5,6 @@ import { useCallback, } from "react"; import { Edge, useReactFlow } from "@xyflow/react"; -import dagre from "dagre"; -import { - parseWorkflow, - generateWorkflow, - buildAlert, -} from "app/workflows/builder/utils"; import { useSearchParams } from "next/navigation"; import useStore from "../../app/workflows/builder/builder-store"; import { FlowNode } from "../../app/workflows/builder/builder-store"; @@ -19,7 +13,6 @@ import { Definition, Step } from "sequential-workflow-designer"; import { WrappedDefinition } from "sequential-workflow-designer-react"; import ELK from 'elkjs/lib/elk.bundled.js'; import { processWorkflowV2 } from "utils/reactFlow"; -// import "@xyflow/react/dist/style.css"; const layoutOptions = { "elk.nodeLabels.placement": "INSIDE V_CENTER H_BOTTOM", @@ -51,18 +44,6 @@ const layoutOptions = { "elk.layered.edgeRouting.orthogonal": true } -const getRandomColor = () => { - const letters = '0123456789ABCDEF'; - let color = '#'; - for (let i = 0; i < 6; i++) { - color += letters[Math.floor(Math.random() * 16)]; - } - return color; -}; - -const dagreGraph = new dagre.graphlib.Graph(); -dagreGraph.setDefaultEdgeLabel(() => ({})); - const getLayoutedElements = (nodes: FlowNode[], edges: Edge[], options = {}) => { const isHorizontal = options?.['elk.direction'] === 'RIGHT'; const elk = new ELK(); @@ -150,9 +131,7 @@ const useWorkflowInitialization = ( height: 100, }); const { screenToFlowPosition } = useReactFlow(); - // const [isLayouted, setIsLayouted] = useState(false); const { fitView } = useReactFlow(); - const definitionRef = useRef(null); const handleDrop = useCallback( (event: React.DragEvent) => { @@ -213,41 +192,13 @@ const useWorkflowInitialization = ( setChanges(0) } } - // window.requestAnimationFrame(() => { - // fitView(); - // }); }, [nodes, edges]) - - - const handleSpecialTools = ( - nodes: FlowNode[], - toolMeta: { - type: string; - specialToolNodeId: string; - switchCondition?: string; - } - ) => { - if (!nodes) { - return; - } - } - - useEffect(() => { - const alertNameParam = searchParams?.get("alertName"); - const alertSourceParam = searchParams?.get("alertSource"); - setAlertName(alertNameParam); - setAlertSource(alertSourceParam); - }, [searchParams]); - useEffect(() => { const initializeWorkflow = async () => { setIsLoading(true); let parsedWorkflow = definition?.value; setV2Properties(parsedWorkflow?.properties ?? {}); - // let { nodes: newNodes, edges: newEdges } = processWorkflow( - // parsedWorkflow?.sequence - // ); const sequences = [ { id: "start", @@ -274,14 +225,7 @@ const useWorkflowInitialization = ( setIsLoading(false); }; initializeWorkflow(); - }, [ - loadedAlertFile, - workflow, - alertName, - alertSource, - providers, - definition?.value, - ]); + }, []); return { diff --git a/keep-ui/utils/reactFlow.ts b/keep-ui/utils/reactFlow.ts index 7c5c5447d..5d35571b5 100644 --- a/keep-ui/utils/reactFlow.ts +++ b/keep-ui/utils/reactFlow.ts @@ -25,7 +25,7 @@ export function reConstructWorklowToDefinition({ for (let i = startIdx; i < endIdx; i++) { const currentNode = originalNodes[i]; - const {isLayOuted, ...nodeData} = currentNode?.data; + const {isLayouted, ...nodeData} = currentNode?.data; const nodeType = nodeData?.type; if (currentNode.id === foreachEmptyId) { foreachNode.sequence = tempSequence; @@ -63,7 +63,7 @@ export function reConstructWorklowToDefinition({ let i = startIdx; for (; i < endIdx; i++) { const currentNode = originalNodes[i]; - const {isLayOuted, ...nodeData} = currentNode?.data; + const {isLayouted, ...nodeData} = currentNode?.data; const nodeType = nodeData?.type; if (trueCaseAdded && falseCaseAdded) { return i; @@ -102,7 +102,7 @@ export function reConstructWorklowToDefinition({ const workflowSequence = []; for (let i = startIdx; i < endIdx; i++) { const currentNode = originalNodes[i]; - const {isLayOuted, ...nodeData} = currentNode?.data; + const {isLayouted, ...nodeData} = currentNode?.data; const nodeType = nodeData?.type; if (["condition-threshold", "condition-assert"].includes(nodeType)) { workflowSequence.push(nodeData); From 37a0cb2c0787b64e4132a58b7273303b0f0abb4d Mon Sep 17 00:00:00 2001 From: Rajesh Jonnalagadda Date: Sun, 11 Aug 2024 15:28:57 +0530 Subject: [PATCH 20/55] fix: end node naming and minor form fixes --- keep-ui/app/workflows/builder/CustomNode.tsx | 5 ++++- keep-ui/app/workflows/builder/builder-store.tsx | 3 ++- keep-ui/utils/reactFlow.ts | 10 +++++----- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/keep-ui/app/workflows/builder/CustomNode.tsx b/keep-ui/app/workflows/builder/CustomNode.tsx index e243559c0..96c27bf14 100644 --- a/keep-ui/app/workflows/builder/CustomNode.tsx +++ b/keep-ui/app/workflows/builder/CustomNode.tsx @@ -41,7 +41,10 @@ function CustomNode({ id, data }: FlowNode) { }`} onClick={(e) => { e.stopPropagation(); - if (type === 'start' || type === 'end' || id?.includes('end')) { + if (type === 'start' || type === 'end' || id?.includes('end') || id?.includes('empty') ) { + if(id?.includes('empty')){ + setSelectedNode(id); + } setOpneGlobalEditor(true); return; } diff --git a/keep-ui/app/workflows/builder/builder-store.tsx b/keep-ui/app/workflows/builder/builder-store.tsx index 32a1c7e4c..e48a20af5 100644 --- a/keep-ui/app/workflows/builder/builder-store.tsx +++ b/keep-ui/app/workflows/builder/builder-store.tsx @@ -372,7 +372,8 @@ const useStore = create((set, get) => ({ nodes: newNodes, selectedNode: null, isLayouted: false, - changes: get().changes + 1 + changes: get().changes + 1, + openGlobalEditor: true, }); }, updateEdge: (id: string, key: string, value: any) => { diff --git a/keep-ui/utils/reactFlow.ts b/keep-ui/utils/reactFlow.ts index 5d35571b5..615e8ca0e 100644 --- a/keep-ui/utils/reactFlow.ts +++ b/keep-ui/utils/reactFlow.ts @@ -135,6 +135,7 @@ export function createSwitchNodeV2( isNested?: boolean, ): FlowNode[] { const customIdentifier = `${step.type}__end__${nodeId}`; + const stepType = step?.type?.replace("step-", "")?.replace("condition-", "")?.replace("__end", "")?.replace("action-", ""); const { name, type, componentType, properties} = step; return [ { @@ -147,14 +148,12 @@ export function createSwitchNodeV2( componentType, id: nodeId, properties, + name: name }, isDraggable: false, prevNodeId, nextNodeId: customIdentifier, dragHandle: ".custom-drag-handle", - style: { - margin: "0px 20px 0px 20px", - }, isNested: !!isNested }, { @@ -162,9 +161,10 @@ export function createSwitchNodeV2( type: "custom", position: { x: 0, y: 0 }, data: { - label: "+", + label: `${stepType} End`, id: customIdentifier, type: `${step.type}__end`, + name: `${stepType} End` }, isDraggable: false, prevNodeId: nodeId, @@ -332,7 +332,7 @@ export function getForEachNode(step, position, nodeId, prevNodeId, nextNodeId, i }, { id: customIdentifier, - data: { ...rest, id: customIdentifier, name: "foreach end", label: "foreach end", type: `${step.type}__end`}, + data: { ...rest, id: customIdentifier, name: "foreach end", label: "foreach end", type: `${step.type}__end`, name: 'Foreach End'}, type: "custom", position: { x: 0, y: 0 }, isDraggable: false, From cdfa2bf40df6300e485f8d4aaf79f4d7239f03e5 Mon Sep 17 00:00:00 2001 From: Bhavya Jain Date: Sun, 11 Aug 2024 15:34:46 +0530 Subject: [PATCH 21/55] Remove save and discard and remove toggle to old flow --- keep-ui/app/workflows/builder/builder.tsx | 62 ++++------------------- 1 file changed, 11 insertions(+), 51 deletions(-) diff --git a/keep-ui/app/workflows/builder/builder.tsx b/keep-ui/app/workflows/builder/builder.tsx index 19b873991..24a26b6ee 100644 --- a/keep-ui/app/workflows/builder/builder.tsx +++ b/keep-ui/app/workflows/builder/builder.tsx @@ -315,27 +315,6 @@ function Builder({ return ( <> -
    -
    - - -
    - {useReactFlow && - setDefinition(wrapDefinition(def)) - } - />} -
    {getworkflowStatus()} - {useReactFlow && ( -
    - - - setDefinition(wrapDefinition(def)) - } - toolboxConfiguration={getToolboxConfiguration(providers)} - /> - -
    - )} - {!useReactFlow && ( - <> - + + } - stepEditor={ - + onDefinitionChange={(def: any) => + setDefinition(wrapDefinition(def)) } + toolboxConfiguration={getToolboxConfiguration(providers)} /> - - )} + +
    )} From e03c523c2a502df05b77027090941e7b845d12ba Mon Sep 17 00:00:00 2001 From: Bhavya Jain Date: Sun, 11 Aug 2024 16:06:13 +0530 Subject: [PATCH 22/55] Make form display on load --- keep-ui/app/workflows/builder/ReactFlowEditor.tsx | 2 +- keep-ui/app/workflows/builder/editors.tsx | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/keep-ui/app/workflows/builder/ReactFlowEditor.tsx b/keep-ui/app/workflows/builder/ReactFlowEditor.tsx index d88c7a914..c9036b350 100644 --- a/keep-ui/app/workflows/builder/ReactFlowEditor.tsx +++ b/keep-ui/app/workflows/builder/ReactFlowEditor.tsx @@ -6,7 +6,7 @@ import { Button } from "@tremor/react"; const ReactFlowEditor = () => { const { openGlobalEditor, selectedNode, stepEditorOpenForNode } = useStore(); - const [isOpen, setIsOpen] = useState(false); + const [isOpen, setIsOpen] = useState(true); // Set initial state to true useEffect(() => { if (stepEditorOpenForNode) { diff --git a/keep-ui/app/workflows/builder/editors.tsx b/keep-ui/app/workflows/builder/editors.tsx index 054466fd9..1846c2090 100644 --- a/keep-ui/app/workflows/builder/editors.tsx +++ b/keep-ui/app/workflows/builder/editors.tsx @@ -52,7 +52,6 @@ export function GlobalEditor() { export function GlobalEditorV2() { const { v2Properties: properties, updateV2Properties: setProperty } = useStore(); const [localProperties, setLocalProperties] = useState(properties); - const handleSubmit = () => { // Save the finalized properties setProperty(localProperties); @@ -434,7 +433,6 @@ function WorkflowEditorV2({ onUpdate: (updatedProperties: Properties) => void; }) { const [properties, setProperties] = useState(initialProperties); - useEffect(() => { setProperties(initialProperties); }, [initialProperties]); @@ -477,7 +475,6 @@ function WorkflowEditorV2({ useEffect(() => { onUpdate(properties); }, [properties]); - return ( <> Workflow Settings From b042f863c56e6f04ee76c7b6e095e2f8c7bf63fc Mon Sep 17 00:00:00 2001 From: Rajesh Jonnalagadda Date: Sun, 11 Aug 2024 17:17:57 +0530 Subject: [PATCH 23/55] fix:form initial inputs fixed --- keep-ui/app/workflows/builder/editors.tsx | 4 ++++ keep-ui/utils/hooks/useWorkflowInitialization.ts | 11 ++++++----- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/keep-ui/app/workflows/builder/editors.tsx b/keep-ui/app/workflows/builder/editors.tsx index 1846c2090..8f9ce4eee 100644 --- a/keep-ui/app/workflows/builder/editors.tsx +++ b/keep-ui/app/workflows/builder/editors.tsx @@ -57,6 +57,10 @@ export function GlobalEditorV2() { setProperty(localProperties); }; + useEffect(() => { + setLocalProperties(properties) + }, [properties]); + return ( Keep Workflow Editor diff --git a/keep-ui/utils/hooks/useWorkflowInitialization.ts b/keep-ui/utils/hooks/useWorkflowInitialization.ts index 05294cd38..6333e79fe 100644 --- a/keep-ui/utils/hooks/useWorkflowInitialization.ts +++ b/keep-ui/utils/hooks/useWorkflowInitialization.ts @@ -167,8 +167,9 @@ const useWorkflowInitialization = ( }) setNodes(layoutedNodes); setEdges(layoutedEdges); - - window.requestAnimationFrame(() => fitView()); + if (!firstInitilisationDone) { + window.requestAnimationFrame(() => fitView()); + } }, ); }, @@ -179,16 +180,16 @@ const useWorkflowInitialization = ( if (!isLayouted && nodes.length > 0) { onLayout({ direction: 'DOWN' }) setIsLayouted(true) - if(!firstInitilisationDone){ + if (!firstInitilisationDone) { setFirstInitilisationDone(true) - setLastSavedChanges({nodes: nodes, edges: edges}); + setLastSavedChanges({ nodes: nodes, edges: edges }); setChanges(0) } } if (!isLayouted && nodes.length === 0) { setIsLayouted(true); - if(!firstInitilisationDone){ + if (!firstInitilisationDone) { setChanges(0) } } From beb1403f6cfd869df50fd1ee51d36ab453cdb179 Mon Sep 17 00:00:00 2001 From: Rajesh Jonnalagadda Date: Sun, 11 Aug 2024 19:11:53 +0530 Subject: [PATCH 24/55] fix:form opening issue --- keep-ui/app/workflows/builder/ReactFlowEditor.tsx | 10 ++++------ keep-ui/app/workflows/builder/builder.tsx | 6 +++--- keep-ui/app/workflows/builder/editors.tsx | 4 ---- 3 files changed, 7 insertions(+), 13 deletions(-) diff --git a/keep-ui/app/workflows/builder/ReactFlowEditor.tsx b/keep-ui/app/workflows/builder/ReactFlowEditor.tsx index c9036b350..fc17271fb 100644 --- a/keep-ui/app/workflows/builder/ReactFlowEditor.tsx +++ b/keep-ui/app/workflows/builder/ReactFlowEditor.tsx @@ -6,13 +6,11 @@ import { Button } from "@tremor/react"; const ReactFlowEditor = () => { const { openGlobalEditor, selectedNode, stepEditorOpenForNode } = useStore(); - const [isOpen, setIsOpen] = useState(true); // Set initial state to true + const [isOpen, setIsOpen] = useState(false); // Set initial state to true - useEffect(() => { - if (stepEditorOpenForNode) { - setIsOpen(stepEditorOpenForNode === selectedNode); - } - }, [stepEditorOpenForNode, selectedNode]); + useEffect(()=>{ + setIsOpen(true); + }, [selectedNode]) return (
    +
    {getworkflowStatus()} -
    +
    )} - +
    ); } diff --git a/keep-ui/app/workflows/builder/editors.tsx b/keep-ui/app/workflows/builder/editors.tsx index 8f9ce4eee..1846c2090 100644 --- a/keep-ui/app/workflows/builder/editors.tsx +++ b/keep-ui/app/workflows/builder/editors.tsx @@ -57,10 +57,6 @@ export function GlobalEditorV2() { setProperty(localProperties); }; - useEffect(() => { - setLocalProperties(properties) - }, [properties]); - return ( Keep Workflow Editor From 0c646b2d3f533cbaa084e3996a4b97eb4e52d773 Mon Sep 17 00:00:00 2001 From: Rajesh Jonnalagadda Date: Sun, 11 Aug 2024 21:55:07 +0530 Subject: [PATCH 25/55] refactor: basic new editor view and minor refactoring --- .../app/workflows/builder/ReactFlowEditor.tsx | 43 +++++++++++++---- .../app/workflows/builder/builder-card.tsx | 2 +- keep-ui/app/workflows/builder/editors.tsx | 46 +++++++------------ keep-ui/app/workflows/builder/page.client.tsx | 2 +- 4 files changed, 52 insertions(+), 41 deletions(-) diff --git a/keep-ui/app/workflows/builder/ReactFlowEditor.tsx b/keep-ui/app/workflows/builder/ReactFlowEditor.tsx index fc17271fb..1e834ce3d 100644 --- a/keep-ui/app/workflows/builder/ReactFlowEditor.tsx +++ b/keep-ui/app/workflows/builder/ReactFlowEditor.tsx @@ -1,22 +1,41 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, useRef } from "react"; import { IoMdSettings, IoMdClose } from "react-icons/io"; import useStore from "./builder-store"; import { GlobalEditorV2, StepEditorV2 } from "./editors"; -import { Button } from "@tremor/react"; +import { Button, Divider } from "@tremor/react"; const ReactFlowEditor = () => { - const { openGlobalEditor, selectedNode, stepEditorOpenForNode } = useStore(); - const [isOpen, setIsOpen] = useState(false); // Set initial state to true + const { selectedNode } = useStore(); + const [isOpen, setIsOpen] = useState(false); + const stepEditorRef = useRef(null); + const containerRef = useRef(null); - useEffect(()=>{ + useEffect(() => { setIsOpen(true); - }, [selectedNode]) + if (selectedNode) { + const timer = setTimeout(() => { + if (containerRef.current && stepEditorRef.current) { + const containerRect = containerRef.current.getBoundingClientRect(); + const stepEditorRect = stepEditorRef.current.getBoundingClientRect(); + // Check if StepEditorV2 is already at the top of the container + const isAtTop = stepEditorRect.top <= containerRect.top; + + if (!isAtTop) { + // Scroll the StepEditorV2 into view + stepEditorRef.current.scrollIntoView({ behavior: "smooth", block: "start" }); + } + } + }, 100); + return () => clearTimeout(timer); // Cleanup the timer on unmount + } + }, [selectedNode]); return (
    {!isOpen && (
    - {openGlobalEditor && } - {!openGlobalEditor && selectedNode && } + + +
    + +
    @@ -47,3 +69,4 @@ const ReactFlowEditor = () => { }; export default ReactFlowEditor; + diff --git a/keep-ui/app/workflows/builder/builder-card.tsx b/keep-ui/app/workflows/builder/builder-card.tsx index b1bf5310c..8a1b1ca1b 100644 --- a/keep-ui/app/workflows/builder/builder-card.tsx +++ b/keep-ui/app/workflows/builder/builder-card.tsx @@ -71,7 +71,7 @@ export function BuilderCard({ ); return ( - + {error ? ( { - // Save the finalized properties - setProperty(localProperties); - }; + const { v2Properties: properties, updateV2Properties: setProperty, selectedNode } = useStore(); return ( @@ -65,20 +60,16 @@ export function GlobalEditorV2() { workflow YAML specifications. - Use the toolbox to add steps, conditions, and actions to your workflow + {/* Use the toolbox to add steps, conditions, and actions to your workflow and click the `Generate` button to compile the workflow / `Deploy` - button to deploy the workflow to Keep. + button to deploy the workflow to Keep. */} + Use the edge add button or an empty step (a step with a +) to insert steps, conditions, and actions into your workflow. + Then, click the Generate button to compile the workflow or the Deploy button to deploy it to Keep. - ); } @@ -426,16 +417,16 @@ function WorkflowEditor(properties: Properties, updateProperty: any) { ); } function WorkflowEditorV2({ - initialProperties, - onUpdate, + properties, + setProperties, }: { - initialProperties: Properties; - onUpdate: (updatedProperties: Properties) => void; + properties: Properties; + setProperties: (updatedProperties: Properties) => void; }) { - const [properties, setProperties] = useState(initialProperties); - useEffect(() => { - setProperties(initialProperties); - }, [initialProperties]); + // const [properties, setProperties] = useState(initialProperties); + // useEffect(() => { + // setProperties(initialProperties); + // }, [initialProperties]); const updateAlertFilter = (filter: string, value: string) => { const currentFilters = properties.alert || {}; @@ -472,9 +463,6 @@ function WorkflowEditorV2({ (k) => k !== "isLocked" && k !== "id" ); - useEffect(() => { - onUpdate(properties); - }, [properties]); return ( <> Workflow Settings @@ -641,7 +629,7 @@ export function StepEditorV2({ return ( -
    + {/*
    Switch to Global Editor -
    +
    */} {providerType} Editor Unique Identifier s + 1; return ( -
    +
    From 1bac4541680be8faf876f4029c9a7583f40c5e54 Mon Sep 17 00:00:00 2001 From: Rajesh Jonnalagadda <rajeshjonnalagadda48@gmail.com> Date: Sun, 11 Aug 2024 22:42:03 +0530 Subject: [PATCH 26/55] fix:handle the empty source edge. remove edge add button for edge where source is empty --- keep-ui/app/workflows/builder/CustomEdge.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/keep-ui/app/workflows/builder/CustomEdge.tsx b/keep-ui/app/workflows/builder/CustomEdge.tsx index ac179962e..2c3377763 100644 --- a/keep-ui/app/workflows/builder/CustomEdge.tsx +++ b/keep-ui/app/workflows/builder/CustomEdge.tsx @@ -94,7 +94,7 @@ const CustomEdge: React.FC<CustomEdgeProps> = ({ {dynamicLabel} </div> )} - <Button + {!source?.includes('empty') && <Button style={{ position: "absolute", transform: `translate(-50%, -50%) translate(${labelX}px, ${labelY}px)`, @@ -108,7 +108,7 @@ const CustomEdge: React.FC<CustomEdgeProps> = ({ }} > <CiSquarePlus className={`w-6 h-6 bg-gray-400 text-white text-center ${selectedEdge === id ? " bg-gray-600" : ""} hover:bg-gray-600`} /> - </Button> + </Button>} </EdgeLabelRenderer> </> ); From 360c4a1ca2ac71d91e9b12cf285a61b698720b21 Mon Sep 17 00:00:00 2001 From: Rajesh Jonnalagadda <rajeshjonnalagadda48@gmail.com> Date: Sun, 11 Aug 2024 23:41:38 +0530 Subject: [PATCH 27/55] chore:minor style changes --- keep-ui/app/workflows/builder/CustomEdge.tsx | 2 +- keep-ui/app/workflows/builder/CustomNode.tsx | 4 ++-- keep-ui/app/workflows/builder/ToolBox.tsx | 8 ++++---- keep-ui/app/workflows/builder/builder-store.tsx | 5 ++++- keep-ui/app/workflows/builder/builder.tsx | 2 +- 5 files changed, 12 insertions(+), 9 deletions(-) diff --git a/keep-ui/app/workflows/builder/CustomEdge.tsx b/keep-ui/app/workflows/builder/CustomEdge.tsx index 2c3377763..038d11623 100644 --- a/keep-ui/app/workflows/builder/CustomEdge.tsx +++ b/keep-ui/app/workflows/builder/CustomEdge.tsx @@ -107,7 +107,7 @@ const CustomEdge: React.FC<CustomEdgeProps> = ({ setSelectedEdge(id); }} > - <CiSquarePlus className={`w-6 h-6 bg-gray-400 text-white text-center ${selectedEdge === id ? " bg-gray-600" : ""} hover:bg-gray-600`} /> + <CiSquarePlus className={`w-6 h-6 bg-gray-500 text-white text-center ${selectedEdge === id ? " bg-gray-700" : ""} hover:bg-gray-600`} /> </Button>} </EdgeLabelRenderer> </> diff --git a/keep-ui/app/workflows/builder/CustomNode.tsx b/keep-ui/app/workflows/builder/CustomNode.tsx index 96c27bf14..f6d70d088 100644 --- a/keep-ui/app/workflows/builder/CustomNode.tsx +++ b/keep-ui/app/workflows/builder/CustomNode.tsx @@ -59,7 +59,7 @@ function CustomNode({ id, data }: FlowNode) { <GoPlus className="w-8 h-8 text-gray-600 font-bold" /> {selectedNode === id && <div className="text-gray-600 font-bold">Go to Toolbox</div>} </div>} - {!isEmptyNode && data?.type !== "sub_flow" && ( + {!isEmptyNode && ( <div className="flex flex-row items-center justify-between gap-2 flex-wrap"> <Image src={IconUrlProvider(data) || "/keep.png"} @@ -69,7 +69,7 @@ function CustomNode({ id, data }: FlowNode) { height={32} /> <div className="flex-1 flex-col gap-2 flex-wrap truncate"> - <div className="text-lg font-bold">{data?.name}</div> + <div className="text-lg font-bold truncate">{data?.name}</div> <div className="text-gray-500 truncate"> {type || data?.componentType} </div> diff --git a/keep-ui/app/workflows/builder/ToolBox.tsx b/keep-ui/app/workflows/builder/ToolBox.tsx index ce1d1d454..0228462f7 100644 --- a/keep-ui/app/workflows/builder/ToolBox.tsx +++ b/keep-ui/app/workflows/builder/ToolBox.tsx @@ -130,14 +130,14 @@ const DragAndDropSidebar = ({ isDraggable }: { return ( <div - className={`absolute top-0 left-0 rounded border-2 border-gray-200 bg-white transition-transform duration-300 z-50 ${isVisible ? 'h-full' : 'shadow-lg'}`} + className={`absolute top-2 left-2 rounded border-2 broder-gray-300 bg-white transition-transform duration-300 z-50 ${isVisible ? 'h-[95%]' : 'shadow-lg'}`} style={{ width: '280px' }} // Set a fixed width > <div className="relative h-full flex flex-col"> {/* Sticky header */} - <div className="sticky top-0 left-0 z-10 bg-white border-b border-gray-200"> - <h1 className="p-2 font-bold">Toolbox</h1> - <div className="flex items-center justify-between p-2 pt-0 border-b border-gray-200 bg-white"> + <div className="sticky top-0 left-0 z-10"> + <h1 className="p-3 font-bold">Toolbox</h1> + <div className="flex items-center justify-between p-2 pt-0 border-b-2 bg-white"> <input type="text" placeholder="Search..." diff --git a/keep-ui/app/workflows/builder/builder-store.tsx b/keep-ui/app/workflows/builder/builder-store.tsx index e48a20af5..eb3bbe4ed 100644 --- a/keep-ui/app/workflows/builder/builder-store.tsx +++ b/keep-ui/app/workflows/builder/builder-store.tsx @@ -314,7 +314,10 @@ const useStore = create<FlowState>((set, get) => ({ data: { label: step.name! as string, ...step, - id: newUuid + id: newUuid, + name: step.name, + type: step.type, + componentType: step.componentType }, isDraggable: true, dragHandle: '.custom-drag-handle', diff --git a/keep-ui/app/workflows/builder/builder.tsx b/keep-ui/app/workflows/builder/builder.tsx index f47f84bb9..59175c874 100644 --- a/keep-ui/app/workflows/builder/builder.tsx +++ b/keep-ui/app/workflows/builder/builder.tsx @@ -338,7 +338,7 @@ function Builder({ {generateModalIsOpen || testRunModalOpen ? null : ( <> {getworkflowStatus()} - <div className="h-[95%]"> + <div className="h-[92%]"> <ReactFlowProvider> <ReactFlowBuilder workflow={workflow} From b232086cd7d3ba4309b7f5282335ce7b6793d148 Mon Sep 17 00:00:00 2001 From: Rajesh Jonnalagadda <rajeshjonnalagadda48@gmail.com> Date: Mon, 12 Aug 2024 14:46:28 +0530 Subject: [PATCH 28/55] feat:integrated the validations check and deploy features --- .../workflows/builder/ReactFlowBuilder.tsx | 19 +++-- .../app/workflows/builder/ReactFlowEditor.tsx | 46 ++++++++++-- .../workflows/builder/builder-validators.tsx | 4 ++ keep-ui/app/workflows/builder/builder.tsx | 70 +++++++++++++++---- 4 files changed, 116 insertions(+), 23 deletions(-) diff --git a/keep-ui/app/workflows/builder/ReactFlowBuilder.tsx b/keep-ui/app/workflows/builder/ReactFlowBuilder.tsx index 1f8d4a5eb..ddf6bd7fd 100644 --- a/keep-ui/app/workflows/builder/ReactFlowBuilder.tsx +++ b/keep-ui/app/workflows/builder/ReactFlowBuilder.tsx @@ -6,7 +6,7 @@ import useWorkflowInitialization from "utils/hooks/useWorkflowInitialization"; import DragAndDropSidebar from "./ToolBox"; import { Provider } from "app/providers/providers"; import ReactFlowEditor from "./ReactFlowEditor"; -import { Definition } from 'sequential-workflow-designer'; +import { Definition, ValidatorConfiguration } from 'sequential-workflow-designer'; import "@xyflow/react/dist/style.css"; import { WrappedDefinition } from "sequential-workflow-designer-react"; @@ -20,14 +20,19 @@ const ReactFlowBuilder = ({ providers, toolboxConfiguration, definition, - onDefinitionChange + onDefinitionChange, + validatorConfiguration }: { workflow: string | undefined; loadedAlertFile: string | null; providers: Provider[]; toolboxConfiguration: Record<string, any>; - definition: WrappedDefinition<Definition>; - onDefinitionChange:(def: WrappedDefinition<Definition>) => void; + definition: any; + validatorConfiguration: { + step: (step: any, parent?:any, defnition?:any)=>boolean; + root: (def: any) => boolean + }; + onDefinitionChange:(def: any) => void; }) => { const { @@ -68,7 +73,11 @@ const ReactFlowBuilder = ({ <Background/> </ReactFlow> )} - <ReactFlowEditor /> + <ReactFlowEditor + providers={providers} + onDefinitionChange= {onDefinitionChange} + validatorConfiguration= {validatorConfiguration} + /> </div> </div> ); diff --git a/keep-ui/app/workflows/builder/ReactFlowEditor.tsx b/keep-ui/app/workflows/builder/ReactFlowEditor.tsx index 1e834ce3d..d74adb264 100644 --- a/keep-ui/app/workflows/builder/ReactFlowEditor.tsx +++ b/keep-ui/app/workflows/builder/ReactFlowEditor.tsx @@ -3,9 +3,22 @@ import { IoMdSettings, IoMdClose } from "react-icons/io"; import useStore from "./builder-store"; import { GlobalEditorV2, StepEditorV2 } from "./editors"; import { Button, Divider } from "@tremor/react"; +import { Provider } from "app/providers/providers"; +import { reConstructWorklowToDefinition } from "utils/reactFlow"; -const ReactFlowEditor = () => { - const { selectedNode } = useStore(); +const ReactFlowEditor = ({ + providers, + validatorConfiguration, + onDefinitionChange +}:{ + providers:Provider[]; + validatorConfiguration: { + step: (step: any, defnition?:any)=>boolean; + root: (def: any) => boolean; + }; + onDefinitionChange: (def: any) => void +}) => { + const { selectedNode, changes, v2Properties, nodes, edges } = useStore(); const [isOpen, setIsOpen] = useState(false); const stepEditorRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null); @@ -30,6 +43,29 @@ const ReactFlowEditor = () => { } }, [selectedNode]); + + useEffect(()=>{ + //TODO: add Debounce to mitigate the recontruction of defintion on every change + if(changes > 0){ + let {sequence, properties} = reConstructWorklowToDefinition({nodes: nodes, edges: edges, properties: v2Properties}) || {}; + sequence = sequence || []; + properties = properties || {}; + console.log("sequence", sequence, "properties", properties) + let isValid = true + for(let step of sequence){ + isValid = validatorConfiguration?.step(step); + if(!isValid){ + break; + } + } + if(!isValid){ + return onDefinitionChange({sequence, properties, isValid}); + } + isValid = validatorConfiguration.root({sequence, properties}); + onDefinitionChange({sequence, properties, isValid}); + } + }, [changes]) + return ( <div className={`absolute top-0 right-0 transition-transform duration-300 z-50 ${ @@ -56,10 +92,8 @@ const ReactFlowEditor = () => { <div className="flex-1 p-2 bg-white border-2 overflow-y-auto"> <div style={{ width: "300px" }}> <GlobalEditorV2 /> - <Divider ref={stepEditorRef}/> - <div> - <StepEditorV2 /> - </div> + {!selectedNode?.includes('empty') && <Divider ref={stepEditorRef}/>} + {!selectedNode?.includes('empty') && <StepEditorV2 installedProviders={providers}/>} </div> </div> </div> diff --git a/keep-ui/app/workflows/builder/builder-validators.tsx b/keep-ui/app/workflows/builder/builder-validators.tsx index ab21ace19..2830227c8 100644 --- a/keep-ui/app/workflows/builder/builder-validators.tsx +++ b/keep-ui/app/workflows/builder/builder-validators.tsx @@ -52,6 +52,10 @@ export function stepValidator( setStepValidationError: Dispatch<SetStateAction<string | null>> ): boolean { if (step.type.includes("condition-")) { + if(!step.name) { + setStepValidationError("Step/action name cannot be empty."); + return false; + } const onlyActions = (step as BranchedStep).branches.true.every((step) => step.type.includes("action-") ); diff --git a/keep-ui/app/workflows/builder/builder.tsx b/keep-ui/app/workflows/builder/builder.tsx index 59175c874..3838d19db 100644 --- a/keep-ui/app/workflows/builder/builder.tsx +++ b/keep-ui/app/workflows/builder/builder.tsx @@ -265,7 +265,7 @@ function Builder({ return CanDeleteStep(step, sourceSequence); } - const validatorConfiguration: ValidatorConfiguration = { + const validatorConfiguration: ValidatorConfiguration|{step:(step:any)=>boolean;root:(def:any)=>boolean;} = { step: (step, parent, definition) => stepValidator(step, parent, definition, setStepValidationError), root: (def) => globalValidator(def, setGlobalValidationError), @@ -315,6 +315,27 @@ function Builder({ return ( <div className="h-full"> + <div className="flex items-center justify-between"> + <div className="pl-4 flex items-center space-x-3"> + <Switch + id="switch" + name="switch" + checked={useReactFlow} + onChange={handleSwitchChange} + /> + <label + htmlFor="switch" + className="text-tremor-default text-tremor-content dark:text-dark-tremor-content" + > + Switch to New Builder + </label> + </div> + {/* {useReactFlow && <BuilderChanagesTracker + onDefinitionChange={(def: any) => + setDefinition(wrapDefinition(def)) + } + />} */} + </div> <Modal onRequestClose={closeGenerateModal} isOpen={generateModalIsOpen} @@ -338,20 +359,45 @@ function Builder({ {generateModalIsOpen || testRunModalOpen ? null : ( <> {getworkflowStatus()} - <div className="h-[92%]"> - <ReactFlowProvider> - <ReactFlowBuilder - workflow={workflow} - loadedAlertFile={loadedAlertFile} - providers={providers} + {useReactFlow && ( + <div className="h-[90%]"> + <ReactFlowProvider> + <ReactFlowBuilder + workflow={workflow} + loadedAlertFile={loadedAlertFile} + providers={providers} + definition={definition} + validatorConfiguration={validatorConfiguration} + onDefinitionChange={(def: any) =>{ + setDefinition({value: {sequence: def?.sequence||[], + properties + : def?. + properties + ||{}}, isValid:def?.isValid||false}) + } + } + toolboxConfiguration={getToolboxConfiguration(providers)} + /> + </ReactFlowProvider> + </div> + )} + {!useReactFlow && ( + <> + <SequentialWorkflowDesigner definition={definition} - onDefinitionChange={(def: any) => - setDefinition(wrapDefinition(def)) - } + onDefinitionChange={setDefinition} + stepsConfiguration={stepsConfiguration} + validatorConfiguration={validatorConfiguration} toolboxConfiguration={getToolboxConfiguration(providers)} + undoStackSize={10} + controlBar={true} + globalEditor={<GlobalEditor />} + stepEditor={ + <StepEditor installedProviders={installedProviders} /> + } /> - </ReactFlowProvider> - </div> + </> + )} </> )} </div> From 31d8764debe22816a59c1b02a9db2737d0f9a78a Mon Sep 17 00:00:00 2001 From: Rajesh Jonnalagadda <rajeshjonnalagadda48@gmail.com> Date: Mon, 12 Aug 2024 16:36:33 +0530 Subject: [PATCH 29/55] chore:hidding the switch --- keep-ui/app/workflows/builder/builder.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keep-ui/app/workflows/builder/builder.tsx b/keep-ui/app/workflows/builder/builder.tsx index 3838d19db..06a174c99 100644 --- a/keep-ui/app/workflows/builder/builder.tsx +++ b/keep-ui/app/workflows/builder/builder.tsx @@ -315,7 +315,7 @@ function Builder({ return ( <div className="h-full"> - <div className="flex items-center justify-between"> + <div className="flex items-center justify-between hidden"> <div className="pl-4 flex items-center space-x-3"> <Switch id="switch" From 107f2402982cff23009030d4648ff45b6507454e Mon Sep 17 00:00:00 2001 From: Rajesh Jonnalagadda <rajeshjonnalagadda48@gmail.com> Date: Mon, 12 Aug 2024 20:36:42 +0530 Subject: [PATCH 30/55] chore:add debounce for recontruction of defintion for evry changes --- .../app/workflows/builder/ReactFlowEditor.tsx | 57 ++++++++++++------- keep-ui/package-lock.json | 16 ++++++ keep-ui/package.json | 1 + 3 files changed, 54 insertions(+), 20 deletions(-) diff --git a/keep-ui/app/workflows/builder/ReactFlowEditor.tsx b/keep-ui/app/workflows/builder/ReactFlowEditor.tsx index d74adb264..0b94d7dc5 100644 --- a/keep-ui/app/workflows/builder/ReactFlowEditor.tsx +++ b/keep-ui/app/workflows/builder/ReactFlowEditor.tsx @@ -5,6 +5,7 @@ import { GlobalEditorV2, StepEditorV2 } from "./editors"; import { Button, Divider } from "@tremor/react"; import { Provider } from "app/providers/providers"; import { reConstructWorklowToDefinition } from "utils/reactFlow"; +import debounce from "lodash.debounce"; const ReactFlowEditor = ({ providers, @@ -43,28 +44,44 @@ const ReactFlowEditor = ({ } }, [selectedNode]); - - useEffect(()=>{ - //TODO: add Debounce to mitigate the recontruction of defintion on every change - if(changes > 0){ - let {sequence, properties} = reConstructWorklowToDefinition({nodes: nodes, edges: edges, properties: v2Properties}) || {}; - sequence = sequence || []; - properties = properties || {}; - console.log("sequence", sequence, "properties", properties) - let isValid = true - for(let step of sequence){ - isValid = validatorConfiguration?.step(step); - if(!isValid){ - break; + useEffect(() => { + const handleDefinitionChange = () => { + if (changes > 0) { + let { sequence, properties } = + reConstructWorklowToDefinition({ + nodes: nodes, + edges: edges, + properties: v2Properties, + }) || {}; + sequence = sequence || []; + properties = properties || {}; + console.log("sequence", sequence, "properties", properties); + + let isValid = true; + for (let step of sequence) { + isValid = validatorConfiguration?.step(step); + if (!isValid) { + break; + } } + + if (!isValid) { + return onDefinitionChange({ sequence, properties, isValid }); + } + + isValid = validatorConfiguration.root({ sequence, properties }); + onDefinitionChange({ sequence, properties, isValid }); } - if(!isValid){ - return onDefinitionChange({sequence, properties, isValid}); - } - isValid = validatorConfiguration.root({sequence, properties}); - onDefinitionChange({sequence, properties, isValid}); - } - }, [changes]) + }; + + const debouncedHandleDefinitionChange = debounce(handleDefinitionChange, 300); + + debouncedHandleDefinitionChange(); + + return () => { + debouncedHandleDefinitionChange.cancel(); + }; + }, [changes]); return ( <div diff --git a/keep-ui/package-lock.json b/keep-ui/package-lock.json index 7ff2f593c..6c7ea8ede 100644 --- a/keep-ui/package-lock.json +++ b/keep-ui/package-lock.json @@ -384,6 +384,7 @@ "@types/js-yaml": "^4.0.5", "@types/json-logic-js": "^2.0.7", "@types/json-query": "^2.2.6", + "@types/lodash.debounce": "^4.0.9", "@types/node": "20.2.1", "@types/react-datepicker": "^6.0.2", "@types/react-grid-layout": "^1.3.5", @@ -4090,6 +4091,21 @@ "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==" }, + "node_modules/@types/lodash": { + "version": "4.17.7", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.7.tgz", + "integrity": "sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==", + "dev": true + }, + "node_modules/@types/lodash.debounce": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/lodash.debounce/-/lodash.debounce-4.0.9.tgz", + "integrity": "sha512-Ma5JcgTREwpLRwMM+XwBR7DaWe96nC38uCBDFKZWbNKD+osjVzdpnUSwBcqCptrp16sSOLBAUb50Car5I0TCsQ==", + "dev": true, + "dependencies": { + "@types/lodash": "*" + } + }, "node_modules/@types/mdast": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.3.tgz", diff --git a/keep-ui/package.json b/keep-ui/package.json index 421328039..3cf0142c7 100644 --- a/keep-ui/package.json +++ b/keep-ui/package.json @@ -385,6 +385,7 @@ "@types/js-yaml": "^4.0.5", "@types/json-logic-js": "^2.0.7", "@types/json-query": "^2.2.6", + "@types/lodash.debounce": "^4.0.9", "@types/node": "20.2.1", "@types/react-datepicker": "^6.0.2", "@types/react-grid-layout": "^1.3.5", From 18b7cdd922b2a7da213bae7890d5f31da606930b Mon Sep 17 00:00:00 2001 From: Rajesh Jonnalagadda <rajeshjonnalagadda48@gmail.com> Date: Mon, 12 Aug 2024 20:47:25 +0530 Subject: [PATCH 31/55] chore:removed unwanted packages and logs --- .../app/workflows/builder/ReactFlowEditor.tsx | 4 +- keep-ui/package-lock.json | 87 ------------------- keep-ui/package.json | 3 - 3 files changed, 1 insertion(+), 93 deletions(-) diff --git a/keep-ui/app/workflows/builder/ReactFlowEditor.tsx b/keep-ui/app/workflows/builder/ReactFlowEditor.tsx index 0b94d7dc5..4f2061c0b 100644 --- a/keep-ui/app/workflows/builder/ReactFlowEditor.tsx +++ b/keep-ui/app/workflows/builder/ReactFlowEditor.tsx @@ -54,9 +54,7 @@ const ReactFlowEditor = ({ properties: v2Properties, }) || {}; sequence = sequence || []; - properties = properties || {}; - console.log("sequence", sequence, "properties", properties); - + properties = properties || {}; let isValid = true; for (let step of sequence) { isValid = validatorConfiguration?.step(step); diff --git a/keep-ui/package-lock.json b/keep-ui/package-lock.json index 6c7ea8ede..c2be9c069 100644 --- a/keep-ui/package-lock.json +++ b/keep-ui/package-lock.json @@ -28,7 +28,6 @@ "@svgr/webpack": "^8.0.1", "@tanstack/react-table": "^8.11.0", "@tremor/react": "^3.15.1", - "@types/dagre": "^0.7.52", "@types/react-select": "^5.0.1", "@xyflow/react": "^12.0.3", "add": "^2.0.6", @@ -91,7 +90,6 @@ "d3-time": "^3.1.0", "d3-time-format": "^4.1.0", "d3-timer": "^3.0.1", - "dagre": "^0.8.5", "damerau-levenshtein": "^1.0.8", "date-fns": "^2.30.0", "debug": "^4.3.4", @@ -290,7 +288,6 @@ "react-chrono": "^2.6.1", "react-code-blocks": "^0.1.3", "react-datepicker": "^6.1.0", - "react-dnd": "^16.0.1", "react-dom": "^18.2.0", "react-grid-layout": "^1.4.4", "react-hook-form": "^7.51.5", @@ -3425,21 +3422,6 @@ "react": "^16.x || ^17.x || ^18.x" } }, - "node_modules/@react-dnd/asap": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-5.0.2.tgz", - "integrity": "sha512-WLyfoHvxhs0V9U+GTsGilGgf2QsPl6ZZ44fnv0/b8T3nQyvzxidxsg/ZltbWssbsRDlYW8UKSQMTGotuTotZ6A==" - }, - "node_modules/@react-dnd/invariant": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-4.0.2.tgz", - "integrity": "sha512-xKCTqAK/FFauOM9Ta2pswIyT3D8AQlfrYdOi/toTPEhqCuAs1v5tcJ3Y08Izh1cJ5Jchwy9SeAXmMg6zrKs2iw==" - }, - "node_modules/@react-dnd/shallowequal": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-4.0.2.tgz", - "integrity": "sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==" - }, "node_modules/@rushstack/eslint-patch": { "version": "1.10.1", "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.10.1.tgz", @@ -4028,11 +4010,6 @@ "@types/d3-selection": "*" } }, - "node_modules/@types/dagre": { - "version": "0.7.52", - "resolved": "https://registry.npmjs.org/@types/dagre/-/dagre-0.7.52.tgz", - "integrity": "sha512-XKJdy+OClLk3hketHi9Qg6gTfe1F3y+UFnHxKA2rn9Dw+oXa4Gb378Ztz9HlMgZKSxpPmn4BNVh9wgkpvrK1uw==" - }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -6032,15 +6009,6 @@ "node": ">=12" } }, - "node_modules/dagre": { - "version": "0.8.5", - "resolved": "https://registry.npmjs.org/dagre/-/dagre-0.8.5.tgz", - "integrity": "sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==", - "dependencies": { - "graphlib": "^2.1.8", - "lodash": "^4.17.15" - } - }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -6363,16 +6331,6 @@ "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" }, - "node_modules/dnd-core": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-16.0.1.tgz", - "integrity": "sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng==", - "dependencies": { - "@react-dnd/asap": "^5.0.1", - "@react-dnd/invariant": "^4.0.1", - "redux": "^4.2.0" - } - }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -8264,14 +8222,6 @@ "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==" }, - "node_modules/graphlib": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.8.tgz", - "integrity": "sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==", - "dependencies": { - "lodash": "^4.17.15" - } - }, "node_modules/grid-index": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/grid-index/-/grid-index-1.1.0.tgz", @@ -11876,35 +11826,6 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, - "node_modules/react-dnd": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-16.0.1.tgz", - "integrity": "sha512-QeoM/i73HHu2XF9aKksIUuamHPDvRglEwdHL4jsp784BgUuWcg6mzfxT0QDdQz8Wj0qyRKx2eMg8iZtWvU4E2Q==", - "dependencies": { - "@react-dnd/invariant": "^4.0.1", - "@react-dnd/shallowequal": "^4.0.1", - "dnd-core": "^16.0.1", - "fast-deep-equal": "^3.1.3", - "hoist-non-react-statics": "^3.3.2" - }, - "peerDependencies": { - "@types/hoist-non-react-statics": ">= 3.3.1", - "@types/node": ">= 12", - "@types/react": ">= 16", - "react": ">= 16.14" - }, - "peerDependenciesMeta": { - "@types/hoist-non-react-statics": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@types/react": { - "optional": true - } - } - }, "node_modules/react-dom": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", @@ -12389,14 +12310,6 @@ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" }, - "node_modules/redux": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", - "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", - "dependencies": { - "@babel/runtime": "^7.9.2" - } - }, "node_modules/reflect.getprototypeof": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz", diff --git a/keep-ui/package.json b/keep-ui/package.json index 3cf0142c7..fca166a6f 100644 --- a/keep-ui/package.json +++ b/keep-ui/package.json @@ -29,7 +29,6 @@ "@svgr/webpack": "^8.0.1", "@tanstack/react-table": "^8.11.0", "@tremor/react": "^3.15.1", - "@types/dagre": "^0.7.52", "@types/react-select": "^5.0.1", "@xyflow/react": "^12.0.3", "add": "^2.0.6", @@ -92,7 +91,6 @@ "d3-time": "^3.1.0", "d3-time-format": "^4.1.0", "d3-timer": "^3.0.1", - "dagre": "^0.8.5", "damerau-levenshtein": "^1.0.8", "date-fns": "^2.30.0", "debug": "^4.3.4", @@ -291,7 +289,6 @@ "react-chrono": "^2.6.1", "react-code-blocks": "^0.1.3", "react-datepicker": "^6.1.0", - "react-dnd": "^16.0.1", "react-dom": "^18.2.0", "react-grid-layout": "^1.4.4", "react-hook-form": "^7.51.5", From 0b7f891f1e8b00e01beedd2ac7808896a8e04237 Mon Sep 17 00:00:00 2001 From: Rajesh Jonnalagadda <rajeshjonnalagadda48@gmail.com> Date: Mon, 12 Aug 2024 23:20:04 +0530 Subject: [PATCH 32/55] chore:refactoring the with type checks --- keep-ui/app/workflows/builder/NodeMenu.tsx | 4 +- .../workflows/builder/ReactFlowBuilder.tsx | 9 +- .../app/workflows/builder/ReactFlowEditor.tsx | 38 +-- .../app/workflows/builder/builder-store.tsx | 31 ++- .../utils/hooks/useWorkflowInitialization.ts | 26 +- keep-ui/utils/reactFlow.ts | 236 +++++++++--------- 6 files changed, 176 insertions(+), 168 deletions(-) diff --git a/keep-ui/app/workflows/builder/NodeMenu.tsx b/keep-ui/app/workflows/builder/NodeMenu.tsx index c8bf777d3..8ea8e5c15 100644 --- a/keep-ui/app/workflows/builder/NodeMenu.tsx +++ b/keep-ui/app/workflows/builder/NodeMenu.tsx @@ -10,9 +10,7 @@ export default function NodeMenu({ data, id }: { data: FlowNode["data"], id: str e.stopPropagation(); }; const isEmptyOrEndNode = data?.type?.includes("empty") || id?.includes('end') - - - const { deleteNodes, duplicateNode, setSelectedNode, setStepEditorOpenForNode } = useStore(); + const { deleteNodes, setSelectedNode, setStepEditorOpenForNode } = useStore(); return ( <> diff --git a/keep-ui/app/workflows/builder/ReactFlowBuilder.tsx b/keep-ui/app/workflows/builder/ReactFlowBuilder.tsx index ddf6bd7fd..fbe673cdc 100644 --- a/keep-ui/app/workflows/builder/ReactFlowBuilder.tsx +++ b/keep-ui/app/workflows/builder/ReactFlowBuilder.tsx @@ -6,9 +6,8 @@ import useWorkflowInitialization from "utils/hooks/useWorkflowInitialization"; import DragAndDropSidebar from "./ToolBox"; import { Provider } from "app/providers/providers"; import ReactFlowEditor from "./ReactFlowEditor"; -import { Definition, ValidatorConfiguration } from 'sequential-workflow-designer'; import "@xyflow/react/dist/style.css"; -import { WrappedDefinition } from "sequential-workflow-designer-react"; +import { ReactFlowDefinition, V2Step, Definition} from "./builder-store"; const nodeTypes = { custom: CustomNode as any }; @@ -29,10 +28,10 @@ const ReactFlowBuilder = ({ toolboxConfiguration: Record<string, any>; definition: any; validatorConfiguration: { - step: (step: any, parent?:any, defnition?:any)=>boolean; - root: (def: any) => boolean + step: (step: V2Step, parent?:V2Step, defnition?: ReactFlowDefinition)=>boolean; + root: (def: Definition) => boolean }; - onDefinitionChange:(def: any) => void; + onDefinitionChange:(def: Definition) => void; }) => { const { diff --git a/keep-ui/app/workflows/builder/ReactFlowEditor.tsx b/keep-ui/app/workflows/builder/ReactFlowEditor.tsx index 4f2061c0b..ca4a405f4 100644 --- a/keep-ui/app/workflows/builder/ReactFlowEditor.tsx +++ b/keep-ui/app/workflows/builder/ReactFlowEditor.tsx @@ -1,8 +1,8 @@ import { useState, useEffect, useRef } from "react"; import { IoMdSettings, IoMdClose } from "react-icons/io"; -import useStore from "./builder-store"; +import useStore, { V2Properties, V2Step, ReactFlowDefinition, Definition } from "./builder-store"; import { GlobalEditorV2, StepEditorV2 } from "./editors"; -import { Button, Divider } from "@tremor/react"; +import { Divider } from "@tremor/react"; import { Provider } from "app/providers/providers"; import { reConstructWorklowToDefinition } from "utils/reactFlow"; import debounce from "lodash.debounce"; @@ -11,13 +11,13 @@ const ReactFlowEditor = ({ providers, validatorConfiguration, onDefinitionChange -}:{ - providers:Provider[]; +}: { + providers: Provider[]; validatorConfiguration: { - step: (step: any, defnition?:any)=>boolean; - root: (def: any) => boolean; + step: (step: V2Step, parent?: V2Step, defnition?: ReactFlowDefinition) => boolean; + root: (def: Definition) => boolean; }; - onDefinitionChange: (def: any) => void + onDefinitionChange: (def: Definition) => void }) => { const { selectedNode, changes, v2Properties, nodes, edges } = useStore(); const [isOpen, setIsOpen] = useState(false); @@ -46,6 +46,7 @@ const ReactFlowEditor = ({ useEffect(() => { const handleDefinitionChange = () => { + name: "foreach end" if (changes > 0) { let { sequence, properties } = reConstructWorklowToDefinition({ @@ -53,8 +54,8 @@ const ReactFlowEditor = ({ edges: edges, properties: v2Properties, }) || {}; - sequence = sequence || []; - properties = properties || {}; + sequence = (sequence || []) as V2Step[]; + properties = (properties || {}) as V2Properties; let isValid = true; for (let step of sequence) { isValid = validatorConfiguration?.step(step); @@ -62,20 +63,20 @@ const ReactFlowEditor = ({ break; } } - + if (!isValid) { return onDefinitionChange({ sequence, properties, isValid }); } - + isValid = validatorConfiguration.root({ sequence, properties }); onDefinitionChange({ sequence, properties, isValid }); } }; - + const debouncedHandleDefinitionChange = debounce(handleDefinitionChange, 300); - + debouncedHandleDefinitionChange(); - + return () => { debouncedHandleDefinitionChange.cancel(); }; @@ -83,9 +84,8 @@ const ReactFlowEditor = ({ return ( <div - className={`absolute top-0 right-0 transition-transform duration-300 z-50 ${ - isOpen ? "h-full" : "h-14" - }`} + className={`absolute top-0 right-0 transition-transform duration-300 z-50 ${isOpen ? "h-full" : "h-14" + }`} ref={containerRef} > {!isOpen && ( @@ -107,8 +107,8 @@ const ReactFlowEditor = ({ <div className="flex-1 p-2 bg-white border-2 overflow-y-auto"> <div style={{ width: "300px" }}> <GlobalEditorV2 /> - {!selectedNode?.includes('empty') && <Divider ref={stepEditorRef}/>} - {!selectedNode?.includes('empty') && <StepEditorV2 installedProviders={providers}/>} + {!selectedNode?.includes('empty') && <Divider ref={stepEditorRef} />} + {!selectedNode?.includes('empty') && <StepEditorV2 installedProviders={providers} />} </div> </div> </div> diff --git a/keep-ui/app/workflows/builder/builder-store.tsx b/keep-ui/app/workflows/builder/builder-store.tsx index eb3bbe4ed..578a4c814 100644 --- a/keep-ui/app/workflows/builder/builder-store.tsx +++ b/keep-ui/app/workflows/builder/builder-store.tsx @@ -15,6 +15,22 @@ import { createCustomEdgeMeta, processWorkflowV2 } from "utils/reactFlow"; export type V2Properties = Record<string, any>; +export type Definition = { + sequence: V2Step[]; + properties: V2Properties; + isValid?: boolean; +} + + + +export type ReactFlowDefinition = { + value: { + sequence: V2Step[], + properties: V2Properties + }, + isValid?: boolean +} + export type V2Step = { id: string; name?: string; @@ -25,7 +41,10 @@ export type V2Step = { true?: V2Step[]; false?: V2Step[]; }; - sequence?: V2Step[] | V2Step; + sequence?: V2Step[]; + edgeNotNeeded?:boolean; + edgeLabel?:string; + edgeColor?: string; }; export type NodeData = Node["data"] & Record<string, any>; @@ -128,7 +147,7 @@ export type FlowState = { export type StoreGet = () => FlowState export type StoreSet = (state: FlowState | Partial<FlowState> | ((state: FlowState) => FlowState | Partial<FlowState>)) => void -function addNodeBetween(nodeOrEdge: string | null, step: any, type: string, set: StoreSet, get: StoreGet) { +function addNodeBetween(nodeOrEdge: string | null, step: V2Step, type: string, set: StoreSet, get: StoreGet) { if (!nodeOrEdge || !step) return; let edge = {} as Edge; if (type === 'node') { @@ -157,7 +176,7 @@ function addNodeBetween(nodeOrEdge: string | null, step: any, type: string, set: }, newStep, { id: targetId, type: 'temp_node', name: 'temp_node', 'componentType': 'temp_node', edgeNotNeeded: true } - ], { x: 0, y: 0 }, true); + ] as V2Step[], { x: 0, y: 0 }, true); const newEdges = [ ...edges, ...(get().edges.filter(edge => !(edge.source == sourceId && edge.target == targetId)) || []), @@ -307,7 +326,7 @@ const useStore = create<FlowState>((set, get) => ({ y: event.clientY, }); const newUuid = uuidv4(); - const newNode: FlowNode = { + const newNode = { id: newUuid, type: "custom", position, // Use the position object with x and y @@ -321,7 +340,7 @@ const useStore = create<FlowState>((set, get) => ({ }, isDraggable: true, dragHandle: '.custom-drag-handle', - }; + } as FlowNode; set({ nodes: [...get().nodes, newNode] }); } catch (err) { @@ -364,7 +383,7 @@ const useStore = create<FlowState>((set, get) => ({ const sources = [...new Set(edges.filter((edge) => startNode.id === edge.target))]; const targets = [...new Set(edges.filter((edge) => endNode.id === edge.source))]; targets.forEach((edge) => { - finalEdges = [...finalEdges, ...sources.map((source) => createCustomEdgeMeta(source.source, edge.target, source.label) + finalEdges = [...finalEdges, ...sources.map((source:Edge) => createCustomEdgeMeta(source.source, edge.target, source.label as string) )]; }); diff --git a/keep-ui/utils/hooks/useWorkflowInitialization.ts b/keep-ui/utils/hooks/useWorkflowInitialization.ts index 6333e79fe..b7b23eccd 100644 --- a/keep-ui/utils/hooks/useWorkflowInitialization.ts +++ b/keep-ui/utils/hooks/useWorkflowInitialization.ts @@ -6,11 +6,9 @@ import { } from "react"; import { Edge, useReactFlow } from "@xyflow/react"; import { useSearchParams } from "next/navigation"; -import useStore from "../../app/workflows/builder/builder-store"; +import useStore, { Definition, ReactFlowDefinition, V2Step } from "../../app/workflows/builder/builder-store"; import { FlowNode } from "../../app/workflows/builder/builder-store"; import { Provider } from "app/providers/providers"; -import { Definition, Step } from "sequential-workflow-designer"; -import { WrappedDefinition } from "sequential-workflow-designer-react"; import ELK from 'elkjs/lib/elk.bundled.js'; import { processWorkflowV2 } from "utils/reactFlow"; @@ -45,6 +43,7 @@ const layoutOptions = { } const getLayoutedElements = (nodes: FlowNode[], edges: Edge[], options = {}) => { + // @ts-ignore const isHorizontal = options?.['elk.direction'] === 'RIGHT'; const elk = new ELK(); @@ -73,6 +72,7 @@ const getLayoutedElements = (nodes: FlowNode[], edges: Edge[], options = {}) => }; return elk + // @ts-ignore .layout(graph) .then((layoutedGraph) => ({ nodes: layoutedGraph?.children?.map((node) => ({ @@ -92,8 +92,8 @@ const useWorkflowInitialization = ( workflow: string | undefined, loadedAlertFile: string | null | undefined, providers: Provider[], - definition: WrappedDefinition<Definition>, - onDefinitionChange: (def: WrappedDefinition<Definition>) => void, + definition: ReactFlowDefinition, + onDefinitionChange: (def: Definition) => void, toolboxConfiguration: Record<string, any> ) => { const { @@ -120,16 +120,6 @@ const useWorkflowInitialization = ( } = useStore(); const [isLoading, setIsLoading] = useState(true); - const [alertName, setAlertName] = useState<string | null | undefined>(null); - const [alertSource, setAlertSource] = useState<string | null | undefined>( - null - ); - const searchParams = useSearchParams(); - const nodeRef = useRef<HTMLDivElement | null>(null); - const [nodeDimensions, setNodeDimensions] = useState({ - width: 200, - height: 100, - }); const { screenToFlowPosition } = useReactFlow(); const { fitView } = useReactFlow(); @@ -207,7 +197,8 @@ const useWorkflowInitialization = ( componentType: "start", properties: {}, isLayouted: false, - } as Partial<Step>, + name: "start" + } as V2Step, ...(parsedWorkflow?.sequence || []), { id: "end", @@ -215,7 +206,8 @@ const useWorkflowInitialization = ( componentType: "end", properties: {}, isLayouted: false, - } as Partial<Step>, + name: "end" + } as V2Step, ]; const intialPositon = { x: 0, y: 50 }; let { nodes, edges } = processWorkflowV2(sequences, intialPositon, true); diff --git a/keep-ui/utils/reactFlow.ts b/keep-ui/utils/reactFlow.ts index 615e8ca0e..312059ec6 100644 --- a/keep-ui/utils/reactFlow.ts +++ b/keep-ui/utils/reactFlow.ts @@ -1,142 +1,144 @@ import { v4 as uuidv4 } from "uuid"; -import { FlowNode, V2Step } from "app/workflows/builder/builder-store"; +import { FlowNode, V2Properties, V2Step } from "app/workflows/builder/builder-store"; import { Edge } from "@xyflow/react"; -function getKeyBasedSquence(step:any, id:string, type:string) { +function getKeyBasedSquence(step: V2Step, id: string, type: string) { return `${step.type}__${id}__empty_${type}`; } export function reConstructWorklowToDefinition({ nodes, - edges, properties ={}}:{ - nodes: FlowNode[], - edges: Edge[], - properties: Record<string,any> - }) { - - const originalNodes = nodes.slice(1, nodes.length-1); - function processForeach(startIdx:number, endIdx:number, foreachNode:FlowNode['data'], nodeId:string) { - foreachNode.sequence = []; - - const tempSequence = []; - const foreachEmptyId = `${foreachNode.type}__${nodeId}__empty_foreach`; - - for (let i = startIdx; i < endIdx; i++) { - const currentNode = originalNodes[i]; - const {isLayouted, ...nodeData} = currentNode?.data; - const nodeType = nodeData?.type; - if (currentNode.id === foreachEmptyId) { + edges, + properties = {} +}: { + nodes: FlowNode[], + edges: Edge[], + properties: Record<string, any> +}) { + + const originalNodes = nodes.slice(1, nodes.length - 1); + function processForeach(startIdx: number, endIdx: number, foreachNode: FlowNode['data'], nodeId: string) { + foreachNode.sequence = []; + + const tempSequence = []; + const foreachEmptyId = `${foreachNode.type}__${nodeId}__empty_foreach`; + + for (let i = startIdx; i < endIdx; i++) { + const currentNode = originalNodes[i]; + const { isLayouted, ...nodeData } = currentNode?.data; + const nodeType = nodeData?.type; + if (currentNode.id === foreachEmptyId) { foreachNode.sequence = tempSequence; return i + 1; - } - - if (["condition-threshold", "condition-assert"].includes(nodeType)) { + } + + if (["condition-threshold", "condition-assert"].includes(nodeType)) { tempSequence.push(nodeData); i = processCondition(i + 1, endIdx, nodeData, currentNode.id); continue; - } - - if (nodeType === "foreach") { + } + + if (nodeType === "foreach") { tempSequence.push(nodeData); i = processForeach(i + 1, endIdx, nodeData, currentNode.id); continue; - } - - tempSequence.push(nodeData); } - return endIdx; - } - - function processCondition(startIdx:number, endIdx:number, conditionNode:FlowNode['data'], nodeId:string) { - conditionNode.branches = { - true: [], - false: [], - }; - - const trueBranchEmptyId = `${conditionNode?.type}__${nodeId}__empty_true`; - const falseBranchEmptyId = `${conditionNode?.type}__${nodeId}__empty_false`; - let trueCaseAdded = false; - let falseCaseAdded = false; - let tempSequence = []; - let i = startIdx; - for (; i < endIdx; i++) { - const currentNode = originalNodes[i]; - const {isLayouted, ...nodeData} = currentNode?.data; - const nodeType = nodeData?.type; - if (trueCaseAdded && falseCaseAdded) { + + tempSequence.push(nodeData); + } + return endIdx; + } + + function processCondition(startIdx: number, endIdx: number, conditionNode: FlowNode['data'], nodeId: string) { + conditionNode.branches = { + true: [], + false: [], + }; + + const trueBranchEmptyId = `${conditionNode?.type}__${nodeId}__empty_true`; + const falseBranchEmptyId = `${conditionNode?.type}__${nodeId}__empty_false`; + let trueCaseAdded = false; + let falseCaseAdded = false; + let tempSequence = []; + let i = startIdx; + for (; i < endIdx; i++) { + const currentNode = originalNodes[i]; + const { isLayouted, ...nodeData } = currentNode?.data; + const nodeType = nodeData?.type; + if (trueCaseAdded && falseCaseAdded) { return i; - } - if (currentNode.id === trueBranchEmptyId) { + } + if (currentNode.id === trueBranchEmptyId) { conditionNode.branches.true = tempSequence; trueCaseAdded = true; tempSequence = []; continue; - } - - if (currentNode.id === falseBranchEmptyId) { + } + + if (currentNode.id === falseBranchEmptyId) { conditionNode.branches.false = tempSequence; falseCaseAdded = true; tempSequence = []; continue; - } - - if (["condition-threshold", "condition-assert"].includes(nodeType)) { + } + + if (["condition-threshold", "condition-assert"].includes(nodeType)) { tempSequence.push(nodeData); i = processCondition(i + 1, endIdx, nodeData, currentNode.id); continue; - } - - if (nodeType === "foreach") { + } + + if (nodeType === "foreach") { tempSequence.push(nodeData); i = processForeach(i + 1, endIdx, nodeData, currentNode.id); continue; - } - tempSequence.push(nodeData); } - return endIdx; - } - - function buildWorkflowDefinition(startIdx:number, endIdx:number) { - const workflowSequence = []; - for (let i = startIdx; i < endIdx; i++) { - const currentNode = originalNodes[i]; - const {isLayouted, ...nodeData} = currentNode?.data; - const nodeType = nodeData?.type; - if (["condition-threshold", "condition-assert"].includes(nodeType)) { + tempSequence.push(nodeData); + } + return endIdx; + } + + function buildWorkflowDefinition(startIdx: number, endIdx: number) { + const workflowSequence = []; + for (let i = startIdx; i < endIdx; i++) { + const currentNode = originalNodes[i]; + const { isLayouted, ...nodeData } = currentNode?.data; + const nodeType = nodeData?.type; + if (["condition-threshold", "condition-assert"].includes(nodeType)) { workflowSequence.push(nodeData); i = processCondition(i + 1, endIdx, nodeData, currentNode.id); continue; - } - if (nodeType === "foreach") { + } + if (nodeType === "foreach") { workflowSequence.push(nodeData); i = processForeach(i + 1, endIdx, nodeData, currentNode.id); continue; - } - workflowSequence.push(nodeData); } - return workflowSequence; - } - - return { - sequence: buildWorkflowDefinition(0, originalNodes.length), - properties: properties - } + workflowSequence.push(nodeData); + } + return workflowSequence; + } + + return { + sequence: buildWorkflowDefinition(0, originalNodes.length) as V2Step[], + properties: properties as V2Properties + } } export function createSwitchNodeV2( - step: any, + step: V2Step, nodeId: string, - position: { x: number; y: number }, + position: FlowNode['position'], nextNodeId?: string | null, prevNodeId?: string | null, isNested?: boolean, ): FlowNode[] { const customIdentifier = `${step.type}__end__${nodeId}`; const stepType = step?.type?.replace("step-", "")?.replace("condition-", "")?.replace("__end", "")?.replace("action-", ""); - const { name, type, componentType, properties} = step; + const { name, type, componentType, properties } = step; return [ { id: nodeId, @@ -149,7 +151,7 @@ export function createSwitchNodeV2( id: nodeId, properties, name: name - }, + } as V2Step, isDraggable: false, prevNodeId, nextNodeId: customIdentifier, @@ -164,8 +166,9 @@ export function createSwitchNodeV2( label: `${stepType} End`, id: customIdentifier, type: `${step.type}__end`, - name: `${stepType} End` - }, + name: `${stepType} End`, + componentType: `${step.type}__end`, + } as V2Step, isDraggable: false, prevNodeId: nodeId, nextNodeId: nextNodeId, @@ -177,12 +180,9 @@ export function createSwitchNodeV2( -export function handleSwitchNode(step, position, nextNodeId, prevNodeId, nodeId, isNested) { - if (step.componentType !== "switch") { - return { nodes: [], edges: [] }; - } - let trueBranches = step?.branches?.true || []; - let falseBranches = step?.branches?.false || []; +export function handleSwitchNode(step: V2Step, position: FlowNode['position'], nextNodeId: string, prevNodeId: string, nodeId: string, isNested: boolean) { + let trueBranches = step?.branches?.true || [] as FlowNode[]; + let falseBranches = step?.branches?.false || [] as Edge[]; function _getEmptyNode(type: string) { @@ -194,7 +194,7 @@ export function handleSwitchNode(step, position, nextNodeId, prevNodeId, nodeId, name: "empty", properties: {}, isNested: true, - } + } as V2Step } let [switchStartNode, switchEndNode] = createSwitchNodeV2(step, nodeId, position, nextNodeId, prevNodeId, isNested); @@ -203,13 +203,13 @@ export function handleSwitchNode(step, position, nextNodeId, prevNodeId, nodeId, ...trueBranches, _getEmptyNode("true"), { ...switchEndNode.data, type: 'temp_node', componentType: "temp_node" } - ]; + ] as V2Step[]; falseBranches = [ { ...switchStartNode.data, type: 'temp_node', componentType: "temp_node" }, ...falseBranches, _getEmptyNode("false"), { ...switchEndNode.data, type: 'temp_node', componentType: "temp_node" } - ] + ] as V2Step[] let truePostion = { x: position.x - 200, y: position.y - 100 }; let falsePostion = { x: position.x + 200, y: position.y - 100 }; @@ -252,9 +252,9 @@ export function handleSwitchNode(step, position, nextNodeId, prevNodeId, nodeId, } export const createDefaultNodeV2 = ( - step: any, + step: V2Step, nodeId: string, - position: { x: number; y: number }, + position: FlowNode['position'], nextNodeId?: string | null, prevNodeId?: string | null, isNested?: boolean, @@ -291,9 +291,9 @@ export function createCustomEdgeMeta(source: string, target: string, label?: str type: type || "custom-edge", label, style: { stroke: color || getRandomColor() } - } + } as Edge } -export function handleDefaultNode(step, position, nextNodeId, prevNodeId, nodeId, isNested) { +export function handleDefaultNode(step: V2Step, position: FlowNode['position'], nextNodeId: string, prevNodeId: string, nodeId: string, isNested: boolean) { const nodes = []; const edges = []; const newNode = createDefaultNodeV2( @@ -314,14 +314,14 @@ export function handleDefaultNode(step, position, nextNodeId, prevNodeId, nodeId return { nodes, edges }; } -export function getForEachNode(step, position, nodeId, prevNodeId, nextNodeId, isNested) { +export function getForEachNode(step: V2Step, position: FlowNode['position'], nodeId: string, prevNodeId: string, nextNodeId: string, isNested: boolean) { const { sequence, ...rest } = step; const customIdentifier = `${step.type}__end__${nodeId}`; return [ { id: nodeId, - data: { ...rest, id: nodeId }, + data: { ...rest, id: nodeId } as V2Step, type: "custom", position: { x: 0, y: 0 }, isDraggable: false, @@ -332,7 +332,7 @@ export function getForEachNode(step, position, nodeId, prevNodeId, nextNodeId, i }, { id: customIdentifier, - data: { ...rest, id: customIdentifier, name: "foreach end", label: "foreach end", type: `${step.type}__end`, name: 'Foreach End'}, + data: { ...rest, id: customIdentifier, label: "foreach end", type: `${step.type}__end`, name: 'Foreach End' } as V2Step, type: "custom", position: { x: 0, y: 0 }, isDraggable: false, @@ -341,11 +341,11 @@ export function getForEachNode(step, position, nodeId, prevNodeId, nextNodeId, i nextNodeId: nextNodeId, isNested: !!isNested }, - ]; + ] as FlowNode[]; } -export function handleForeachNode(step, position, nextNodeId, prevNodeId, nodeId, isNested) { +export function handleForeachNode(step: V2Step, position: FlowNode['position'], nextNodeId: string, prevNodeId: string, nodeId: string, isNested: boolean) { const [forEachStartNode, forEachEndNode] = getForEachNode(step, position, nodeId, prevNodeId, nextNodeId, isNested); @@ -358,27 +358,27 @@ export function handleForeachNode(step, position, nextNodeId, prevNodeId, nodeId name: "empty", properties: {}, isNested: true, - } + } as V2Step } const sequences = [ { id: prevNodeId, type: "temp_node", componentType: "temp_node", name: "temp_node", properties: {}, edgeNotNeeded: true }, { id: forEachStartNode.id, type: "temp_node", componentType: "temp_node", name: "temp_node", properties: {} }, - ...step.sequence, + ...(step?.sequence || []), _getEmptyNode("foreach"), { id: forEachEndNode.id, type: "temp_node", componentType: "temp_node", name: "temp_node", properties: {} }, { id: nextNodeId, type: "temp_node", componentType: "temp_node", name: "temp_node", properties: {}, edgeNotNeeded: true }, - ]; + ] as V2Step[]; const { nodes, edges } = processWorkflowV2(sequences, position, false, true); return { nodes: [forEachStartNode, ...nodes, forEachEndNode], edges: edges }; } export const processStepV2 = ( - step: any, - position: { x: number; y: number }, - nextNodeId?: string | null, - prevNodeId?: string | null, - isNested?: boolean + step: V2Step, + position: FlowNode['position'], + nextNodeId: string, + prevNodeId: string, + isNested: boolean ) => { const nodeId = step.id; let newNodes: FlowNode[] = []; @@ -410,13 +410,13 @@ export const processStepV2 = ( return { nodes: newNodes, edges: newEdges }; }; -export const processWorkflowV2 = (sequence: any, position: { x: number, y: number }, isFirstRender = false, isNested = false) => { +export const processWorkflowV2 = (sequence: V2Step[], position: FlowNode['position'], isFirstRender = false, isNested = false) => { let newNodes: FlowNode[] = []; let newEdges: Edge[] = []; sequence?.forEach((step: any, index: number) => { - const prevNodeId = sequence?.[index - 1]?.id || null; - const nextNodeId = sequence?.[index + 1]?.id || null; + const prevNodeId = sequence?.[index - 1]?.id || ""; + const nextNodeId = sequence?.[index + 1]?.id || ""; position.y += 150; const { nodes, edges } = processStepV2( step, From 6e4eb5bf9c4943be3ab277d08e9261c6e0334042 Mon Sep 17 00:00:00 2001 From: Rajesh Jonnalagadda <rajeshjonnalagadda48@gmail.com> Date: Tue, 13 Aug 2024 00:01:12 +0530 Subject: [PATCH 33/55] chore:define new v22 validators for new flow and handle types --- .../app/workflows/builder/builder-store.tsx | 4 +- .../workflows/builder/builder-validators.tsx | 81 ++++++++++++++++++- keep-ui/app/workflows/builder/builder.tsx | 36 ++++++--- 3 files changed, 108 insertions(+), 13 deletions(-) diff --git a/keep-ui/app/workflows/builder/builder-store.tsx b/keep-ui/app/workflows/builder/builder-store.tsx index 578a4c814..ebf268c67 100644 --- a/keep-ui/app/workflows/builder/builder-store.tsx +++ b/keep-ui/app/workflows/builder/builder-store.tsx @@ -38,8 +38,8 @@ export type V2Step = { type: string; properties?: V2Properties; branches?: { - true?: V2Step[]; - false?: V2Step[]; + true: V2Step[]; + false: V2Step[]; }; sequence?: V2Step[]; edgeNotNeeded?:boolean; diff --git a/keep-ui/app/workflows/builder/builder-validators.tsx b/keep-ui/app/workflows/builder/builder-validators.tsx index 2830227c8..fde47fd88 100644 --- a/keep-ui/app/workflows/builder/builder-validators.tsx +++ b/keep-ui/app/workflows/builder/builder-validators.tsx @@ -6,9 +6,10 @@ import { BranchedStep, Sequence, } from "sequential-workflow-designer"; +import { ReactFlowDefinition, V2Step, Definition as FlowDefinition } from "./builder-store"; export function globalValidator( - definition: Definition, + definition: Definition | FlowDefinition, setGlobalValidationError: Dispatch<SetStateAction<string | null>> ): boolean { const anyStepOrAction = definition?.sequence?.length > 0; @@ -44,6 +45,46 @@ export function globalValidator( if (valid) setGlobalValidationError(null); return valid; } +export function globalValidatorV2( + definition: FlowDefinition, + setGlobalValidationError: Dispatch<SetStateAction<string | null>> +): boolean { + const anyStepOrAction = definition?.sequence?.length > 0; + if (!anyStepOrAction) { + setGlobalValidationError( + "At least 1 step/action is required." + ); + } + const anyActionsInMainSequence = ( + definition.sequence[0] as V2Step + )?.sequence?.some((step) => step?.type?.includes("action-")); + if (anyActionsInMainSequence) { + // This checks to see if there's any steps after the first action + const actionIndex = ( + definition?.sequence?.[0] as V2Step + )?.sequence?.findIndex((step) => step.type.includes("action-")); + if(actionIndex && definition?.sequence){ + const sequence = definition?.sequence?.[0]?.sequence || []; + for ( + let i = actionIndex + 1; + i < sequence.length; + i++ + ) { + if ( + sequence[i]?.type?.includes( + "step-" + ) + ) { + setGlobalValidationError("Steps cannot be placed after actions."); + return false; + } + } + } + } + const valid = anyStepOrAction; + if (valid) setGlobalValidationError(null); + return valid; +} export function stepValidator( step: Step | BranchedStep, @@ -82,3 +123,41 @@ export function stepValidator( } return true; } +export function stepValidatorV2( + step: V2Step, + setStepValidationError: Dispatch<SetStateAction<string | null>>, + parentSequence?: V2Step, + definition?: ReactFlowDefinition, +): boolean { + if (step.type.includes("condition-")) { + if(!step.name) { + setStepValidationError("Step/action name cannot be empty."); + return false; + } + const branches = (step?.branches || {true:[], false:[]}) as V2Step['branches']; + const onlyActions = branches?.true?.every((step:V2Step) => + step.type.includes("action-") + ); + if (!onlyActions) { + setStepValidationError("Conditions can only contain actions."); + return false; + } + const conditionHasActions = branches?.true ? branches?.true.length > 0 : false; + if (!conditionHasActions) + setStepValidationError("Conditions must contain at least one action."); + const valid = conditionHasActions && onlyActions; + if (valid) setStepValidationError(null); + return valid; + } + if (step?.componentType === "task") { + const valid = step?.name !== ""; + if (!valid) setStepValidationError("Step name cannot be empty."); + if (!step?.properties?.with) + setStepValidationError( + "There is step/action with no parameters configured!" + ); + if (valid && step?.properties?.with) setStepValidationError(null); + return valid; + } + return true; +} diff --git a/keep-ui/app/workflows/builder/builder.tsx b/keep-ui/app/workflows/builder/builder.tsx index 06a174c99..831805faf 100644 --- a/keep-ui/app/workflows/builder/builder.tsx +++ b/keep-ui/app/workflows/builder/builder.tsx @@ -27,7 +27,7 @@ import { CheckCircleIcon, ExclamationCircleIcon, } from "@heroicons/react/20/solid"; -import { globalValidator, stepValidator } from "./builder-validators"; +import { globalValidator, globalValidatorV2, stepValidator, stepValidatorV2 } from "./builder-validators"; import Modal from "react-modal"; import { Alert } from "./alert"; import BuilderModalContent from "./builder-modal"; @@ -41,6 +41,7 @@ import { WorkflowExecution, WorkflowExecutionFailure } from "./types"; import ReactFlowBuilder from "./ReactFlowBuilder"; import { ReactFlowProvider } from "@xyflow/react"; import BuilderChanagesTracker from "./BuilderChanagesTracker"; +import { ReactFlowDefinition, V2Step, Definition as FlowDefinition } from "./builder-store"; interface Props { loadedAlertFile: string | null; @@ -265,7 +266,18 @@ function Builder({ return CanDeleteStep(step, sourceSequence); } - const validatorConfiguration: ValidatorConfiguration|{step:(step:any)=>boolean;root:(def:any)=>boolean;} = { + + const ValidatorConfigurationV2: { + step: (step: V2Step, + parent?: V2Step, + definition?: ReactFlowDefinition) => boolean; + root: (def: FlowDefinition) => boolean; + } = { + step: (step, parent, definition) => + stepValidatorV2(step, setStepValidationError, parent, definition), + root: (def) => globalValidatorV2(def, setGlobalValidationError), + } + const validatorConfiguration: ValidatorConfiguration = { step: (step, parent, definition) => stepValidator(step, parent, definition, setStepValidationError), root: (def) => globalValidator(def, setGlobalValidationError), @@ -315,7 +327,7 @@ function Builder({ return ( <div className="h-full"> - <div className="flex items-center justify-between hidden"> + <div className="flex items-center justify-between hidden"> <div className="pl-4 flex items-center space-x-3"> <Switch id="switch" @@ -367,13 +379,17 @@ function Builder({ loadedAlertFile={loadedAlertFile} providers={providers} definition={definition} - validatorConfiguration={validatorConfiguration} - onDefinitionChange={(def: any) =>{ - setDefinition({value: {sequence: def?.sequence||[], - properties - : def?. - properties - ||{}}, isValid:def?.isValid||false}) + validatorConfiguration={ValidatorConfigurationV2} + onDefinitionChange={(def: any) => { + setDefinition({ + value: { + sequence: def?.sequence || [], + properties + : def?. + properties + || {} + }, isValid: def?.isValid || false + }) } } toolboxConfiguration={getToolboxConfiguration(providers)} From cc097a095ffaa56bd62dca6b25d36d2880c1e848 Mon Sep 17 00:00:00 2001 From: Rajesh Jonnalagadda <rajeshjonnalagadda48@gmail.com> Date: Tue, 13 Aug 2024 00:05:00 +0530 Subject: [PATCH 34/55] chore:clean up --- keep-ui/app/workflows/builder/builder-validators.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keep-ui/app/workflows/builder/builder-validators.tsx b/keep-ui/app/workflows/builder/builder-validators.tsx index fde47fd88..5bf68ab5c 100644 --- a/keep-ui/app/workflows/builder/builder-validators.tsx +++ b/keep-ui/app/workflows/builder/builder-validators.tsx @@ -9,7 +9,7 @@ import { import { ReactFlowDefinition, V2Step, Definition as FlowDefinition } from "./builder-store"; export function globalValidator( - definition: Definition | FlowDefinition, + definition: Definition, setGlobalValidationError: Dispatch<SetStateAction<string | null>> ): boolean { const anyStepOrAction = definition?.sequence?.length > 0; From 5eadbb7dd22243a52e59c4560087352e3ce672c5 Mon Sep 17 00:00:00 2001 From: Rajesh Jonnalagadda <rajeshjonnalagadda48@gmail.com> Date: Thu, 15 Aug 2024 17:44:01 +0530 Subject: [PATCH 35/55] fix:cenetering issue and highlight the node which cause the error and some minor changes --- keep-ui/app/workflows/builder/CustomEdge.tsx | 2 +- keep-ui/app/workflows/builder/CustomNode.tsx | 10 ++- .../app/workflows/builder/builder-store.tsx | 4 ++ .../workflows/builder/builder-validators.tsx | 23 ++++--- keep-ui/app/workflows/builder/builder.tsx | 21 ++++-- .../utils/hooks/useWorkflowInitialization.ts | 67 ++++++++++--------- keep-ui/utils/reactFlow.ts | 4 +- 7 files changed, 81 insertions(+), 50 deletions(-) diff --git a/keep-ui/app/workflows/builder/CustomEdge.tsx b/keep-ui/app/workflows/builder/CustomEdge.tsx index 038d11623..5c7776bd4 100644 --- a/keep-ui/app/workflows/builder/CustomEdge.tsx +++ b/keep-ui/app/workflows/builder/CustomEdge.tsx @@ -86,7 +86,7 @@ const CustomEdge: React.FC<CustomEdgeProps> = ({ <div className={`absolute ${color} text-white rounded px-3 py-1 border border-gray-700`} style={{ - transform: `translate(${midpointX}px, ${midpointY}px)`, + transform: `translate(-50%, -50%) translate(${dynamicLabel === "True" ? labelX-45 : labelX+45}px, ${labelY}px)`, pointerEvents: "none", opacity: isLayouted ? 1 : 0 }} diff --git a/keep-ui/app/workflows/builder/CustomNode.tsx b/keep-ui/app/workflows/builder/CustomNode.tsx index f6d70d088..1741d9872 100644 --- a/keep-ui/app/workflows/builder/CustomNode.tsx +++ b/keep-ui/app/workflows/builder/CustomNode.tsx @@ -7,6 +7,8 @@ import { GoPlus } from "react-icons/go"; import { MdNotStarted } from "react-icons/md"; import { GoSquareFill } from "react-icons/go"; import { PiSquareLogoFill } from "react-icons/pi"; +import { BiSolidError } from "react-icons/bi"; + function IconUrlProvider(data: FlowNode["data"]) { @@ -20,7 +22,7 @@ function IconUrlProvider(data: FlowNode["data"]) { } function CustomNode({ id, data }: FlowNode) { - const { selectedNode, setSelectedNode, setOpneGlobalEditor } = useStore(); + const { selectedNode, setSelectedNode, setOpneGlobalEditor, errorNode } = useStore(); const type = data?.type ?.replace("step-", "") ?.replace("action-", "") @@ -29,6 +31,7 @@ function CustomNode({ id, data }: FlowNode) { const isEmptyNode = !!data?.type?.includes("empty"); const specialNodeCheck = ['start', 'end'].includes(type) + console.log("errorNode=========>", errorNode); return ( <> @@ -51,7 +54,9 @@ function CustomNode({ id, data }: FlowNode) { setSelectedNode(id); }} style={{ - opacity: data.isLayouted ? 1 : 0 + opacity: data.isLayouted ? 1 : 0, + borderStyle: isEmptyNode ? 'dashed': "", + borderColor: errorNode == id? 'red': '' }} > {isEmptyNode && <div className="flex flex-col items-center justify-center" @@ -59,6 +64,7 @@ function CustomNode({ id, data }: FlowNode) { <GoPlus className="w-8 h-8 text-gray-600 font-bold" /> {selectedNode === id && <div className="text-gray-600 font-bold">Go to Toolbox</div>} </div>} + {errorNode ===id && <BiSolidError className="size-16 text-red-500 absolute right-[-40px] top-[-40px]"/>} {!isEmptyNode && ( <div className="flex flex-row items-center justify-between gap-2 flex-wrap"> <Image diff --git a/keep-ui/app/workflows/builder/builder-store.tsx b/keep-ui/app/workflows/builder/builder-store.tsx index ebf268c67..f00f820b8 100644 --- a/keep-ui/app/workflows/builder/builder-store.tsx +++ b/keep-ui/app/workflows/builder/builder-store.tsx @@ -141,6 +141,8 @@ export type FlowState = { setFirstInitilisationDone: (firstInitilisationDone: boolean) => void; lastSavedChanges: {nodes: FlowNode[] | null, edges: Edge[] | null}; setLastSavedChanges: ({nodes, edges}: {nodes: FlowNode[], edges: Edge[]}) => void; + setErrorNode: (id:string|null)=>void; + errorNode: string|null; }; @@ -215,6 +217,8 @@ const useStore = create<FlowState>((set, get) => ({ changes: 0, lastSavedChanges:{nodes: [], edges:[]}, firstInitilisationDone: false, + errorNode:null, + setErrorNode: (id)=>set({errorNode: id}), setFirstInitilisationDone: (firstInitilisationDone) => set({ firstInitilisationDone }), setLastSavedChanges:({nodes, edges}:{nodes:FlowNode[],edges:Edge[]})=>set({lastSavedChanges: {nodes, edges}}), setSelectedEdge: (id) => set({ selectedEdge: id, selectedNode: null, openGlobalEditor: true }), diff --git a/keep-ui/app/workflows/builder/builder-validators.tsx b/keep-ui/app/workflows/builder/builder-validators.tsx index 5bf68ab5c..bad06aa0d 100644 --- a/keep-ui/app/workflows/builder/builder-validators.tsx +++ b/keep-ui/app/workflows/builder/builder-validators.tsx @@ -125,13 +125,13 @@ export function stepValidator( } export function stepValidatorV2( step: V2Step, - setStepValidationError: Dispatch<SetStateAction<string | null>>, + setStepValidationError: (step:V2Step, error:string|null)=>void, parentSequence?: V2Step, definition?: ReactFlowDefinition, ): boolean { if (step.type.includes("condition-")) { if(!step.name) { - setStepValidationError("Step/action name cannot be empty."); + setStepValidationError(step, "Step/action name cannot be empty."); return false; } const branches = (step?.branches || {true:[], false:[]}) as V2Step['branches']; @@ -139,25 +139,30 @@ export function stepValidatorV2( step.type.includes("action-") ); if (!onlyActions) { - setStepValidationError("Conditions can only contain actions."); + setStepValidationError(step, "Conditions can only contain actions."); return false; } const conditionHasActions = branches?.true ? branches?.true.length > 0 : false; if (!conditionHasActions) - setStepValidationError("Conditions must contain at least one action."); + setStepValidationError(step, "Conditions must contain at least one action."); const valid = conditionHasActions && onlyActions; - if (valid) setStepValidationError(null); + if (valid) setStepValidationError(step, null); return valid; } if (step?.componentType === "task") { const valid = step?.name !== ""; - if (!valid) setStepValidationError("Step name cannot be empty."); - if (!step?.properties?.with) - setStepValidationError( + if (!valid) setStepValidationError(step, "Step name cannot be empty."); + if (!step?.properties?.with){ + setStepValidationError(step, "There is step/action with no parameters configured!" ); - if (valid && step?.properties?.with) setStepValidationError(null); + return false; + } + if (valid && step?.properties?.with) { + setStepValidationError(step, null); + } return valid; } + setStepValidationError(step, null); return true; } diff --git a/keep-ui/app/workflows/builder/builder.tsx b/keep-ui/app/workflows/builder/builder.tsx index 831805faf..eaf928417 100644 --- a/keep-ui/app/workflows/builder/builder.tsx +++ b/keep-ui/app/workflows/builder/builder.tsx @@ -41,7 +41,7 @@ import { WorkflowExecution, WorkflowExecutionFailure } from "./types"; import ReactFlowBuilder from "./ReactFlowBuilder"; import { ReactFlowProvider } from "@xyflow/react"; import BuilderChanagesTracker from "./BuilderChanagesTracker"; -import { ReactFlowDefinition, V2Step, Definition as FlowDefinition } from "./builder-store"; +import useStore, { ReactFlowDefinition, V2Step, Definition as FlowDefinition } from "./builder-store"; interface Props { loadedAlertFile: string | null; @@ -92,6 +92,15 @@ function Builder({ const [compiledAlert, setCompiledAlert] = useState<Alert | null>(null); const searchParams = useSearchParams(); + const {setErrorNode} = useStore(); + + const setStepValidationErrorV2 = (step:V2Step, error:string|null)=>{ + setStepValidationError(error); + if(error && step){ + return setErrorNode(step.id) + } + setErrorNode(null); + } const updateWorkflow = () => { const apiUrl = getApiURL(); @@ -274,7 +283,7 @@ function Builder({ root: (def: FlowDefinition) => boolean; } = { step: (step, parent, definition) => - stepValidatorV2(step, setStepValidationError, parent, definition), + stepValidatorV2(step, setStepValidationErrorV2, parent, definition), root: (def) => globalValidatorV2(def, setGlobalValidationError), } const validatorConfiguration: ValidatorConfiguration = { @@ -327,7 +336,7 @@ function Builder({ return ( <div className="h-full"> - <div className="flex items-center justify-between hidden"> + <div className="flex items-center justify-between"> <div className="pl-4 flex items-center space-x-3"> <Switch id="switch" @@ -372,7 +381,7 @@ function Builder({ <> {getworkflowStatus()} {useReactFlow && ( - <div className="h-[90%]"> + <div className="h-[94%]"> <ReactFlowProvider> <ReactFlowBuilder workflow={workflow} @@ -398,7 +407,7 @@ function Builder({ </div> )} {!useReactFlow && ( - <> + <div className="h-[93%]"> <SequentialWorkflowDesigner definition={definition} onDefinitionChange={setDefinition} @@ -412,7 +421,7 @@ function Builder({ <StepEditor installedProviders={installedProviders} /> } /> - </> + </div> )} </> )} diff --git a/keep-ui/utils/hooks/useWorkflowInitialization.ts b/keep-ui/utils/hooks/useWorkflowInitialization.ts index b7b23eccd..fa6b94819 100644 --- a/keep-ui/utils/hooks/useWorkflowInitialization.ts +++ b/keep-ui/utils/hooks/useWorkflowInitialization.ts @@ -1,47 +1,51 @@ import { useEffect, useState, - useRef, useCallback, } from "react"; import { Edge, useReactFlow } from "@xyflow/react"; -import { useSearchParams } from "next/navigation"; import useStore, { Definition, ReactFlowDefinition, V2Step } from "../../app/workflows/builder/builder-store"; import { FlowNode } from "../../app/workflows/builder/builder-store"; import { Provider } from "app/providers/providers"; import ELK from 'elkjs/lib/elk.bundled.js'; import { processWorkflowV2 } from "utils/reactFlow"; -const layoutOptions = { +const layoutOptions = { "elk.nodeLabels.placement": "INSIDE V_CENTER H_BOTTOM", "elk.algorithm": "layered", - "elk.direction": "BOTTOM", // Direction of layout - "org.eclipse.elk.layered.layering.strategy": "INTERACTIVE", // Interactive layering strategy - "org.eclipse.elk.edgeRouting": "ORTHOGONAL", // Use orthogonal routing - "elk.layered.unnecessaryBendpoints": "true", // Allow bend points if necessary - "elk.layered.spacing.edgeNodeBetweenLayers": "50", // Spacing between edges and nodes - "org.eclipse.elk.layered.nodePlacement.bk.fixedAlignment": "BALANCED", // Balanced node placement - "org.eclipse.elk.layered.cycleBreaking.strategy": "DEPTH_FIRST", // Strategy for cycle breaking - "elk.insideSelfLoops.activate": true, // Handle self-loops inside nodes - "separateConnectedComponents": "false", // Do not separate connected components - "spacing.componentComponent": "70", // Spacing between components - "spacing": "75", // General spacing - "elk.spacing.nodeNodeBetweenLayers": "70", // Spacing between nodes in different layers - "elk.spacing.nodeNode": "8", // Spacing between nodes - "elk.layered.spacing.nodeNodeBetweenLayers": "75", // Spacing between nodes between layers - "portConstraints": "FIXED_ORDER", // Fixed order for ports - "nodeSize.constraints": "[MINIMUM_SIZE]", // Minimum size constraints for nodes - "elk.alignment": "CENTER", // Center alignment - "elk.spacing.edgeNodeBetweenLayers": "50.0", // Spacing between edges and nodes - "org.eclipse.elk.layoutAncestors": "true", // Layout ancestors - "elk.edgeRouting": "ORTHOGONAL", // Ensure orthogonal edge routing - "elk.layered.edgeRouting": "ORTHOGONAL", // Ensure orthogonal edge routing in layered layout - "elk.layered.nodePlacement.strategy": "BRANDES_KOEPF", // Node placement strategy for symmetry - "elk.layered.nodePlacement.outerSpacing": "20", // Spacing around nodes to prevent overlap - "elk.layered.nodePlacement.outerPadding": "20", // Padding around nodes - "elk.layered.edgeRouting.orthogonal": true + "elk.direction": "BOTTOM", + "org.eclipse.elk.layered.layering.strategy": "INTERACTIVE", + "elk.edgeRouting": "ORTHOGONAL", + "elk.layered.unnecessaryBendpoints": false, + "elk.layered.spacing.edgeNodeBetweenLayers": "70", + "org.eclipse.elk.layered.nodePlacement.bk.fixedAlignment": "BALANCED", + "org.eclipse.elk.layered.cycleBreaking.strategy": "DEPTH_FIRST", + "elk.insideSelfLoops.activate": true, + "separateConnectedComponents": "false", + "spacing.componentComponent": "80", + "spacing": "80", + "elk.spacing.nodeNodeBetweenLayers": "80", + "elk.spacing.nodeNode": "120", + "elk.layered.spacing.nodeNodeBetweenLayers": "80", + "portConstraints": "FIXED_ORDER", + "nodeSize.constraints": "[MINIMUM_SIZE]", + "elk.alignment": "CENTER", + "elk.spacing.edgeNodeBetweenLayers": "70.0", + "org.eclipse.elk.layoutAncestors": "true", + "elk.layered.nodePlacement.strategy": "BRANDES_KOEPF", + "elk.layered.nodePlacement.outerSpacing": "30", + "elk.layered.nodePlacement.outerPadding": "30", + "elk.layered.edgeRouting.orthogonal": true, + + // Avoid bending towards nodes + "elk.layered.allowEdgeLabelOverlap": false, + "elk.layered.edgeRouting.avoidNodes": true, + "elk.layered.edgeRouting.avoidEdges": true, + "elk.layered.nodePlacement.nodeNodeOverlapAllowed": false, + "elk.layered.consistentLevelSpacing": true } + const getLayoutedElements = (nodes: FlowNode[], edges: Edge[], options = {}) => { // @ts-ignore const isHorizontal = options?.['elk.direction'] === 'RIGHT'; @@ -87,7 +91,6 @@ const getLayoutedElements = (nodes: FlowNode[], edges: Edge[], options = {}) => .catch(console.error); }; - const useWorkflowInitialization = ( workflow: string | undefined, loadedAlertFile: string | null | undefined, @@ -115,6 +118,7 @@ const useWorkflowInitialization = ( setLastSavedChanges, changes, setChanges, + setSelectedNode, firstInitilisationDone, setFirstInitilisationDone } = useStore(); @@ -155,6 +159,7 @@ const useWorkflowInitialization = ( layoutedNodes.forEach((node: FlowNode) => { node.data = { ...node.data, isLayouted: true } }) + setIsLayouted(true); setNodes(layoutedNodes); setEdges(layoutedEdges); if (!firstInitilisationDone) { @@ -169,7 +174,6 @@ const useWorkflowInitialization = ( useEffect(() => { if (!isLayouted && nodes.length > 0) { onLayout({ direction: 'DOWN' }) - setIsLayouted(true) if (!firstInitilisationDone) { setFirstInitilisationDone(true) setLastSavedChanges({ nodes: nodes, edges: edges }); @@ -211,6 +215,9 @@ const useWorkflowInitialization = ( ]; const intialPositon = { x: 0, y: 50 }; let { nodes, edges } = processWorkflowV2(sequences, intialPositon, true); + setSelectedNode(null); + setFirstInitilisationDone(false) + setChanges(0); setIsLayouted(false); setNodes(nodes); setEdges(edges); diff --git a/keep-ui/utils/reactFlow.ts b/keep-ui/utils/reactFlow.ts index 312059ec6..f5f044bdf 100644 --- a/keep-ui/utils/reactFlow.ts +++ b/keep-ui/utils/reactFlow.ts @@ -238,12 +238,12 @@ export function handleSwitchNode(step: V2Step, position: FlowNode['position'], n return { nodes: [ switchStartNode, - ...trueBranchNodes, ...falseSubflowNodes, + ...trueBranchNodes, switchEndNode, ], edges: [ - ...trueSubflowEdges, ...falseSubflowEdges, + ...trueSubflowEdges, //handling the switch end edge createCustomEdgeMeta(switchEndNode.id, nextNodeId) ] From fa5785b02f988fd65672839cbc4d2bd19dfafb3d Mon Sep 17 00:00:00 2001 From: Rajesh Jonnalagadda <rajeshjonnalagadda48@gmail.com> Date: Thu, 15 Aug 2024 20:32:32 +0530 Subject: [PATCH 36/55] chore:clean up and fixed the deletion flicker issue --- keep-ui/app/workflows/builder/CustomNode.tsx | 1 - .../workflows/builder/ReactFlowBuilder.tsx | 1 + .../app/workflows/builder/builder-store.tsx | 13 +++++++++- keep-ui/app/workflows/builder/builder.tsx | 2 +- .../utils/hooks/useWorkflowInitialization.ts | 26 ++++++------------- keep-ui/utils/reactFlow.ts | 6 ++--- 6 files changed, 25 insertions(+), 24 deletions(-) diff --git a/keep-ui/app/workflows/builder/CustomNode.tsx b/keep-ui/app/workflows/builder/CustomNode.tsx index 1741d9872..2d2cd3d05 100644 --- a/keep-ui/app/workflows/builder/CustomNode.tsx +++ b/keep-ui/app/workflows/builder/CustomNode.tsx @@ -31,7 +31,6 @@ function CustomNode({ id, data }: FlowNode) { const isEmptyNode = !!data?.type?.includes("empty"); const specialNodeCheck = ['start', 'end'].includes(type) - console.log("errorNode=========>", errorNode); return ( <> diff --git a/keep-ui/app/workflows/builder/ReactFlowBuilder.tsx b/keep-ui/app/workflows/builder/ReactFlowBuilder.tsx index fbe673cdc..d54f71e04 100644 --- a/keep-ui/app/workflows/builder/ReactFlowBuilder.tsx +++ b/keep-ui/app/workflows/builder/ReactFlowBuilder.tsx @@ -67,6 +67,7 @@ const ReactFlowBuilder = ({ onDragOver={onDragOver} nodeTypes={nodeTypes} edgeTypes={edgeTypes} + fitView > <Controls orientation="horizontal"/> <Background/> diff --git a/keep-ui/app/workflows/builder/builder-store.tsx b/keep-ui/app/workflows/builder/builder-store.tsx index f00f820b8..5098bbab0 100644 --- a/keep-ui/app/workflows/builder/builder-store.tsx +++ b/keep-ui/app/workflows/builder/builder-store.tsx @@ -12,6 +12,7 @@ import { } from "@xyflow/react"; import { createCustomEdgeMeta, processWorkflowV2 } from "utils/reactFlow"; +import { createDefaultNodeV2 } from '../../../utils/reactFlow'; export type V2Properties = Record<string, any>; @@ -236,7 +237,12 @@ const useStore = create<FlowState>((set, get) => ({ const updatedNodes = get().nodes.map((node) => { if (node.id === currentSelectedNode) { //properties changes should not reconstructed the defintion. only recontrreconstructing if there are any structural changes are done on the flow. + if(value){ node.data[key] = value; + } + if(!value){ + delete node.data[key]; + } return {...node} } return node; @@ -391,7 +397,12 @@ const useStore = create<FlowState>((set, get) => ({ )]; }); - const newNodes = [...nodes.slice(0, nodeStartIndex), ...nodes.slice(endIndex + 1)]; + + nodes[endIndex+1].position = {x: 0, y:0}; + + const newNode = createDefaultNodeV2({...nodes[endIndex+1].data, islayouted: false}, nodes[endIndex+1].id); + + const newNodes = [...nodes.slice(0, nodeStartIndex), newNode, ...nodes.slice(endIndex + 2)]; set({ edges: finalEdges, diff --git a/keep-ui/app/workflows/builder/builder.tsx b/keep-ui/app/workflows/builder/builder.tsx index eaf928417..f53eb6772 100644 --- a/keep-ui/app/workflows/builder/builder.tsx +++ b/keep-ui/app/workflows/builder/builder.tsx @@ -336,7 +336,7 @@ function Builder({ return ( <div className="h-full"> - <div className="flex items-center justify-between"> + <div className="flex items-center justify-between hidden"> <div className="pl-4 flex items-center space-x-3"> <Switch id="switch" diff --git a/keep-ui/utils/hooks/useWorkflowInitialization.ts b/keep-ui/utils/hooks/useWorkflowInitialization.ts index fa6b94819..1e97ee6c7 100644 --- a/keep-ui/utils/hooks/useWorkflowInitialization.ts +++ b/keep-ui/utils/hooks/useWorkflowInitialization.ts @@ -126,6 +126,8 @@ const useWorkflowInitialization = ( const [isLoading, setIsLoading] = useState(true); const { screenToFlowPosition } = useReactFlow(); const { fitView } = useReactFlow(); + const [finalNodes, setFinalNodes] = useState<FlowNode[]>([]); + const [finalEdges, setFinalEdges] = useState<Edge[]>([]); const handleDrop = useCallback( (event: React.DragEvent<HTMLDivElement>) => { @@ -159,12 +161,12 @@ const useWorkflowInitialization = ( layoutedNodes.forEach((node: FlowNode) => { node.data = { ...node.data, isLayouted: true } }) - setIsLayouted(true); setNodes(layoutedNodes); setEdges(layoutedEdges); - if (!firstInitilisationDone) { - window.requestAnimationFrame(() => fitView()); - } + setIsLayouted(true); + setFinalEdges(layoutedEdges); + setFinalNodes(layoutedNodes); + }, ); }, @@ -174,18 +176,6 @@ const useWorkflowInitialization = ( useEffect(() => { if (!isLayouted && nodes.length > 0) { onLayout({ direction: 'DOWN' }) - if (!firstInitilisationDone) { - setFirstInitilisationDone(true) - setLastSavedChanges({ nodes: nodes, edges: edges }); - setChanges(0) - } - } - - if (!isLayouted && nodes.length === 0) { - setIsLayouted(true); - if (!firstInitilisationDone) { - setChanges(0) - } } }, [nodes, edges]) @@ -229,8 +219,8 @@ const useWorkflowInitialization = ( return { - nodes, - edges, + nodes: finalNodes, + edges: finalEdges, isLoading, onNodesChange: onNodesChange, onEdgesChange: onEdgesChange, diff --git a/keep-ui/utils/reactFlow.ts b/keep-ui/utils/reactFlow.ts index f5f044bdf..25914bed4 100644 --- a/keep-ui/utils/reactFlow.ts +++ b/keep-ui/utils/reactFlow.ts @@ -1,5 +1,5 @@ import { v4 as uuidv4 } from "uuid"; -import { FlowNode, V2Properties, V2Step } from "app/workflows/builder/builder-store"; +import { FlowNode, NodeData, V2Properties, V2Step } from "app/workflows/builder/builder-store"; import { Edge } from "@xyflow/react"; @@ -252,9 +252,9 @@ export function handleSwitchNode(step: V2Step, position: FlowNode['position'], n } export const createDefaultNodeV2 = ( - step: V2Step, + step: V2Step | NodeData, nodeId: string, - position: FlowNode['position'], + position?: FlowNode['position'], nextNodeId?: string | null, prevNodeId?: string | null, isNested?: boolean, From 5fc0cc301f9e7b5233ae0f39c3e78c6691a715a6 Mon Sep 17 00:00:00 2001 From: Rajesh Jonnalagadda <rajeshjonnalagadda48@gmail.com> Date: Fri, 16 Aug 2024 17:17:17 +0530 Subject: [PATCH 37/55] fix:empty node plus icon centering --- keep-ui/app/workflows/builder/CustomNode.tsx | 49 +++++++++++--------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/keep-ui/app/workflows/builder/CustomNode.tsx b/keep-ui/app/workflows/builder/CustomNode.tsx index 2d2cd3d05..12a76d83a 100644 --- a/keep-ui/app/workflows/builder/CustomNode.tsx +++ b/keep-ui/app/workflows/builder/CustomNode.tsx @@ -35,7 +35,7 @@ function CustomNode({ id, data }: FlowNode) { return ( <> {!specialNodeCheck && <div - className={`p-2 py-4 shadow-md rounded-md bg-white border-2 w-full h-full ${id === selectedNode + className={`p-2 flex shadow-md rounded-md bg-white border-2 w-full h-full ${id === selectedNode ? "border-orange-500" : "border-stone-400" // } custom-drag-handle`} @@ -43,8 +43,8 @@ function CustomNode({ id, data }: FlowNode) { }`} onClick={(e) => { e.stopPropagation(); - if (type === 'start' || type === 'end' || id?.includes('end') || id?.includes('empty') ) { - if(id?.includes('empty')){ + if (type === 'start' || type === 'end' || id?.includes('end') || id?.includes('empty')) { + if (id?.includes('empty')) { setSelectedNode(id); } setOpneGlobalEditor(true); @@ -54,18 +54,21 @@ function CustomNode({ id, data }: FlowNode) { }} style={{ opacity: data.isLayouted ? 1 : 0, - borderStyle: isEmptyNode ? 'dashed': "", - borderColor: errorNode == id? 'red': '' + borderStyle: isEmptyNode ? 'dashed' : "", + borderColor: errorNode == id ? 'red' : '' }} > - {isEmptyNode && <div className="flex flex-col items-center justify-center" - > - <GoPlus className="w-8 h-8 text-gray-600 font-bold" /> - {selectedNode === id && <div className="text-gray-600 font-bold">Go to Toolbox</div>} - </div>} - {errorNode ===id && <BiSolidError className="size-16 text-red-500 absolute right-[-40px] top-[-40px]"/>} + {isEmptyNode && ( + <div className="flex-1 flex flex-col items-center justify-center"> + <GoPlus className="w-8 h-8 text-gray-600 font-bold p-0" /> + {selectedNode === id && ( + <div className="text-gray-600 font-bold text-center">Go to Toolbox</div> + )} + </div> + )} + {errorNode === id && <BiSolidError className="size-16 text-red-500 absolute right-[-40px] top-[-40px]" />} {!isEmptyNode && ( - <div className="flex flex-row items-center justify-between gap-2 flex-wrap"> + <div className="flex-1 flex flex-row items-center justify-between gap-2 flex-wrap"> <Image src={IconUrlProvider(data) || "/keep.png"} alt={data?.type} @@ -115,17 +118,17 @@ function CustomNode({ id, data }: FlowNode) { {type === 'end' && <GoSquareFill className="size-20 bg-orange-500 text-white rounded-full font-bold mb-2" />} {['threshold', 'assert', 'foreach'].includes(type) && <div className={`border-2 ${id === selectedNode - ? "border-orange-500" - : "border-stone-400"}`}> - {id.includes('end') ? <PiSquareLogoFill className="size-20 rounded bg-white-400 p-2" /> : - <Image - src={IconUrlProvider(data) || "/keep.png"} - alt={data?.type} - className="object-contain size-20 rounded bg-white-400 p-2" - width={32} - height={32} - />} - </div> + ? "border-orange-500" + : "border-stone-400"}`}> + {id.includes('end') ? <PiSquareLogoFill className="size-20 rounded bg-white-400 p-2" /> : + <Image + src={IconUrlProvider(data) || "/keep.png"} + alt={data?.type} + className="object-contain size-20 rounded bg-white-400 p-2" + width={32} + height={32} + />} + </div> } {'start' === type && <Handle type="source" From 636d919e05d12f4c668c7a48cf8cc3392eb52342 Mon Sep 17 00:00:00 2001 From: Bhavya Jain <rishikabhavya@gmail.com> Date: Fri, 16 Aug 2024 22:25:55 +0530 Subject: [PATCH 38/55] chore:edge button updates --- keep-ui/app/workflows/builder/CustomEdge.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/keep-ui/app/workflows/builder/CustomEdge.tsx b/keep-ui/app/workflows/builder/CustomEdge.tsx index 5c7776bd4..173ca5abc 100644 --- a/keep-ui/app/workflows/builder/CustomEdge.tsx +++ b/keep-ui/app/workflows/builder/CustomEdge.tsx @@ -2,7 +2,7 @@ import React from "react"; import { BaseEdge, EdgeLabelRenderer, getSmoothStepPath } from "@xyflow/react"; import type { Edge, EdgeProps } from "@xyflow/react"; import useStore from "./builder-store"; -import { CiSquarePlus } from "react-icons/ci"; +import { PlusIcon } from "@radix-ui/react-icons"; import { Button } from "@tremor/react"; import '@xyflow/react/dist/style.css'; @@ -107,7 +107,7 @@ const CustomEdge: React.FC<CustomEdgeProps> = ({ setSelectedEdge(id); }} > - <CiSquarePlus className={`w-6 h-6 bg-gray-500 text-white text-center ${selectedEdge === id ? " bg-gray-700" : ""} hover:bg-gray-600`} /> + <PlusIcon className="size-5 hover:text-black hover:border-black rounded-none text-sm bg-white border border-gray-700 text-gray-700"/> </Button>} </EdgeLabelRenderer> </> From 463f31b2a062d1c3b00839c4bffedca5408b5110 Mon Sep 17 00:00:00 2001 From: Bhavya Jain <rishikabhavya@gmail.com> Date: Fri, 16 Aug 2024 22:45:26 +0530 Subject: [PATCH 39/55] chore:edge button label adjustments --- keep-ui/app/workflows/builder/CustomEdge.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/keep-ui/app/workflows/builder/CustomEdge.tsx b/keep-ui/app/workflows/builder/CustomEdge.tsx index 173ca5abc..673d6fc2f 100644 --- a/keep-ui/app/workflows/builder/CustomEdge.tsx +++ b/keep-ui/app/workflows/builder/CustomEdge.tsx @@ -86,7 +86,7 @@ const CustomEdge: React.FC<CustomEdgeProps> = ({ <div className={`absolute ${color} text-white rounded px-3 py-1 border border-gray-700`} style={{ - transform: `translate(-50%, -50%) translate(${dynamicLabel === "True" ? labelX-45 : labelX+45}px, ${labelY}px)`, + transform: `translate(-50%, -50%) translate(${dynamicLabel === "True" ? labelX-45 : labelX+48}px, ${labelY}px)`, pointerEvents: "none", opacity: isLayouted ? 1 : 0 }} @@ -107,7 +107,7 @@ const CustomEdge: React.FC<CustomEdgeProps> = ({ setSelectedEdge(id); }} > - <PlusIcon className="size-5 hover:text-black hover:border-black rounded-none text-sm bg-white border border-gray-700 text-gray-700"/> + <PlusIcon className="size-7 hover:text-black hover:border-black rounded text-sm bg-white border border-gray-700 text-gray-700 font-bold"/> </Button>} </EdgeLabelRenderer> </> From 2015c86a601ec6a5f0deefbd389a351bfe604feb Mon Sep 17 00:00:00 2001 From: Rajesh Jonnalagadda <rajeshjonnalagadda48@gmail.com> Date: Fri, 16 Aug 2024 22:51:45 +0530 Subject: [PATCH 40/55] chore:code format and clena up --- keep-ui/app/workflows/builder/CustomEdge.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keep-ui/app/workflows/builder/CustomEdge.tsx b/keep-ui/app/workflows/builder/CustomEdge.tsx index 673d6fc2f..db54e2766 100644 --- a/keep-ui/app/workflows/builder/CustomEdge.tsx +++ b/keep-ui/app/workflows/builder/CustomEdge.tsx @@ -107,7 +107,7 @@ const CustomEdge: React.FC<CustomEdgeProps> = ({ setSelectedEdge(id); }} > - <PlusIcon className="size-7 hover:text-black hover:border-black rounded text-sm bg-white border border-gray-700 text-gray-700 font-bold"/> + <PlusIcon className="size-7 hover:text-black hover:border-black rounded text-sm bg-white border border-gray-700 text-gray-700"/> </Button>} </EdgeLabelRenderer> </> From 9e4dc2cf45d74b1501ef63f93646684c1cc75aca Mon Sep 17 00:00:00 2001 From: Rajesh Jonnalagadda <rajeshjonnalagadda48@gmail.com> Date: Tue, 20 Aug 2024 19:37:40 +0530 Subject: [PATCH 41/55] chore:cleanup the old builder --- .../workflows/builder/ReactFlowBuilder.tsx | 16 +- .../app/workflows/builder/ReactFlowEditor.tsx | 2 +- .../workflows/builder/builder-validators.tsx | 81 ------ keep-ui/app/workflows/builder/builder.tsx | 150 ++--------- keep-ui/app/workflows/builder/editors.tsx | 254 +----------------- .../utils/hooks/useWorkflowInitialization.ts | 4 - 6 files changed, 42 insertions(+), 465 deletions(-) diff --git a/keep-ui/app/workflows/builder/ReactFlowBuilder.tsx b/keep-ui/app/workflows/builder/ReactFlowBuilder.tsx index d54f71e04..ba303a9e2 100644 --- a/keep-ui/app/workflows/builder/ReactFlowBuilder.tsx +++ b/keep-ui/app/workflows/builder/ReactFlowBuilder.tsx @@ -14,17 +14,13 @@ const nodeTypes = { custom: CustomNode as any }; const edgeTypes: EdgeTypesType = { "custom-edge": CustomEdge as React.ComponentType<any> }; const ReactFlowBuilder = ({ - workflow, - loadedAlertFile, - providers, + installedProviders, toolboxConfiguration, definition, onDefinitionChange, validatorConfiguration }: { - workflow: string | undefined; - loadedAlertFile: string | null; - providers: Provider[]; + installedProviders: Provider[] | undefined | null; toolboxConfiguration: Record<string, any>; definition: any; validatorConfiguration: { @@ -38,17 +34,13 @@ const ReactFlowBuilder = ({ nodes, edges, isLoading, - selectedNode, onEdgesChange, onNodesChange, onConnect, onDragOver, onDrop, - } = useWorkflowInitialization(workflow, - loadedAlertFile, - providers, + } = useWorkflowInitialization( definition, - onDefinitionChange, toolboxConfiguration, ); @@ -74,7 +66,7 @@ const ReactFlowBuilder = ({ </ReactFlow> )} <ReactFlowEditor - providers={providers} + providers={installedProviders} onDefinitionChange= {onDefinitionChange} validatorConfiguration= {validatorConfiguration} /> diff --git a/keep-ui/app/workflows/builder/ReactFlowEditor.tsx b/keep-ui/app/workflows/builder/ReactFlowEditor.tsx index ca4a405f4..b38bedab0 100644 --- a/keep-ui/app/workflows/builder/ReactFlowEditor.tsx +++ b/keep-ui/app/workflows/builder/ReactFlowEditor.tsx @@ -12,7 +12,7 @@ const ReactFlowEditor = ({ validatorConfiguration, onDefinitionChange }: { - providers: Provider[]; + providers: Provider[] | undefined | null; validatorConfiguration: { step: (step: V2Step, parent?: V2Step, defnition?: ReactFlowDefinition) => boolean; root: (def: Definition) => boolean; diff --git a/keep-ui/app/workflows/builder/builder-validators.tsx b/keep-ui/app/workflows/builder/builder-validators.tsx index bad06aa0d..cd703efeb 100644 --- a/keep-ui/app/workflows/builder/builder-validators.tsx +++ b/keep-ui/app/workflows/builder/builder-validators.tsx @@ -1,50 +1,6 @@ import { Dispatch, SetStateAction } from "react"; -import { - Definition, - SequentialStep, - Step, - BranchedStep, - Sequence, -} from "sequential-workflow-designer"; import { ReactFlowDefinition, V2Step, Definition as FlowDefinition } from "./builder-store"; -export function globalValidator( - definition: Definition, - setGlobalValidationError: Dispatch<SetStateAction<string | null>> -): boolean { - const anyStepOrAction = definition?.sequence?.length > 0; - if (!anyStepOrAction) { - setGlobalValidationError( - "At least 1 step/action is required." - ); - } - const anyActionsInMainSequence = ( - definition.sequence[0] as SequentialStep - )?.sequence?.some((step) => step.type.includes("action-")); - if (anyActionsInMainSequence) { - // This checks to see if there's any steps after the first action - const actionIndex = ( - definition.sequence[0] as SequentialStep - )?.sequence.findIndex((step) => step.type.includes("action-")); - for ( - let i = actionIndex + 1; - i < (definition.sequence[0] as SequentialStep)?.sequence.length; - i++ - ) { - if ( - (definition.sequence[0] as SequentialStep)?.sequence[i].type.includes( - "step-" - ) - ) { - setGlobalValidationError("Steps cannot be placed after actions."); - return false; - } - } - } - const valid = anyStepOrAction; - if (valid) setGlobalValidationError(null); - return valid; -} export function globalValidatorV2( definition: FlowDefinition, setGlobalValidationError: Dispatch<SetStateAction<string | null>> @@ -86,43 +42,6 @@ export function globalValidatorV2( return valid; } -export function stepValidator( - step: Step | BranchedStep, - parentSequence: Sequence, - definition: Definition, - setStepValidationError: Dispatch<SetStateAction<string | null>> -): boolean { - if (step.type.includes("condition-")) { - if(!step.name) { - setStepValidationError("Step/action name cannot be empty."); - return false; - } - const onlyActions = (step as BranchedStep).branches.true.every((step) => - step.type.includes("action-") - ); - if (!onlyActions) { - setStepValidationError("Conditions can only contain actions."); - return false; - } - const conditionHasActions = (step as BranchedStep).branches.true.length > 0; - if (!conditionHasActions) - setStepValidationError("Conditions must contain at least one action."); - const valid = conditionHasActions && onlyActions; - if (valid) setStepValidationError(null); - return valid; - } - if (step.componentType === "task") { - const valid = step.name !== ""; - if (!valid) setStepValidationError("Step name cannot be empty."); - if (!step.properties.with) - setStepValidationError( - "There is step/action with no parameters configured!" - ); - if (valid && step.properties.with) setStepValidationError(null); - return valid; - } - return true; -} export function stepValidatorV2( step: V2Step, setStepValidationError: (step:V2Step, error:string|null)=>void, diff --git a/keep-ui/app/workflows/builder/builder.tsx b/keep-ui/app/workflows/builder/builder.tsx index f53eb6772..14a4108a9 100644 --- a/keep-ui/app/workflows/builder/builder.tsx +++ b/keep-ui/app/workflows/builder/builder.tsx @@ -1,21 +1,12 @@ -import "sequential-workflow-designer/css/designer.css"; -import "sequential-workflow-designer/css/designer-light.css"; -import "sequential-workflow-designer/css/designer-dark.css"; import "./page.css"; import { Definition, - StepsConfiguration, - ValidatorConfiguration, - Step, - Sequence, } from "sequential-workflow-designer"; import { - SequentialWorkflowDesigner, wrapDefinition, } from "sequential-workflow-designer-react"; import { useEffect, useState } from "react"; -import StepEditor, { GlobalEditor, GlobalEditorV2 } from "./editors"; -import { Callout, Card, Switch } from "@tremor/react"; +import { Callout, Card } from "@tremor/react"; import { Provider } from "../../providers/providers"; import { parseWorkflow, @@ -27,7 +18,7 @@ import { CheckCircleIcon, ExclamationCircleIcon, } from "@heroicons/react/20/solid"; -import { globalValidator, globalValidatorV2, stepValidator, stepValidatorV2 } from "./builder-validators"; +import { globalValidatorV2, stepValidatorV2 } from "./builder-validators"; import Modal from "react-modal"; import { Alert } from "./alert"; import BuilderModalContent from "./builder-modal"; @@ -40,7 +31,6 @@ import BuilderWorkflowTestRunModalContent from "./builder-workflow-testrun-modal import { WorkflowExecution, WorkflowExecutionFailure } from "./types"; import ReactFlowBuilder from "./ReactFlowBuilder"; import { ReactFlowProvider } from "@xyflow/react"; -import BuilderChanagesTracker from "./BuilderChanagesTracker"; import useStore, { ReactFlowDefinition, V2Step, Definition as FlowDefinition } from "./builder-store"; interface Props { @@ -72,7 +62,6 @@ function Builder({ installedProviders, isPreview, }: Props) { - const [useReactFlow, setUseReactFlow] = useState(true); const [definition, setDefinition] = useState(() => wrapDefinition({ sequence: [], properties: {} } as Definition) @@ -92,14 +81,14 @@ function Builder({ const [compiledAlert, setCompiledAlert] = useState<Alert | null>(null); const searchParams = useSearchParams(); - const {setErrorNode} = useStore(); + const { setErrorNode } = useStore(); - const setStepValidationErrorV2 = (step:V2Step, error:string|null)=>{ + const setStepValidationErrorV2 = (step: V2Step, error: string | null) => { setStepValidationError(error); - if(error && step){ - return setErrorNode(step.id) - } - setErrorNode(null); + if (error && step) { + return setErrorNode(step.id) + } + setErrorNode(null); } const updateWorkflow = () => { @@ -233,7 +222,7 @@ function Builder({ (definition.isValid && stepValidationError === null && globalValidationError === null) || - false + false ); }, [ stepValidationError, @@ -250,31 +239,6 @@ function Builder({ ); } - function IconUrlProvider(componentType: string, type: string): string | null { - if (type === "alert" || type === "workflow") return "/keep.png"; - return `/icons/${type - .replace("step-", "") - .replace("action-", "") - .replace("condition-", "")}-icon.png`; - } - - function CanDeleteStep(step: Step, parentSequence: Sequence): boolean { - return !step.properties["isLocked"]; - } - - function IsStepDraggable(step: Step, parentSequence: Sequence): boolean { - return CanDeleteStep(step, parentSequence); - } - - function CanMoveStep( - sourceSequence: any, - step: any, - targetSequence: Sequence, - targetIndex: number - ): boolean { - return CanDeleteStep(step, sourceSequence); - } - const ValidatorConfigurationV2: { step: (step: V2Step, @@ -286,18 +250,6 @@ function Builder({ stepValidatorV2(step, setStepValidationErrorV2, parent, definition), root: (def) => globalValidatorV2(def, setGlobalValidationError), } - const validatorConfiguration: ValidatorConfiguration = { - step: (step, parent, definition) => - stepValidator(step, parent, definition, setStepValidationError), - root: (def) => globalValidator(def, setGlobalValidationError), - }; - - const stepsConfiguration: StepsConfiguration = { - iconUrlProvider: IconUrlProvider, - canDeleteStep: CanDeleteStep, - canMoveStep: CanMoveStep, - isDraggable: IsStepDraggable, - }; function closeGenerateModal() { setGenerateModalIsOpen(false); @@ -308,10 +260,6 @@ function Builder({ setRunningWorkflowExecution(null); }; - const handleSwitchChange = (value: boolean) => { - setUseReactFlow(value); - }; - const getworkflowStatus = () => { return stepValidationError || globalValidationError ? ( <Callout @@ -336,27 +284,6 @@ function Builder({ return ( <div className="h-full"> - <div className="flex items-center justify-between hidden"> - <div className="pl-4 flex items-center space-x-3"> - <Switch - id="switch" - name="switch" - checked={useReactFlow} - onChange={handleSwitchChange} - /> - <label - htmlFor="switch" - className="text-tremor-default text-tremor-content dark:text-dark-tremor-content" - > - Switch to New Builder - </label> - </div> - {/* {useReactFlow && <BuilderChanagesTracker - onDefinitionChange={(def: any) => - setDefinition(wrapDefinition(def)) - } - />} */} - </div> <Modal onRequestClose={closeGenerateModal} isOpen={generateModalIsOpen} @@ -380,49 +307,28 @@ function Builder({ {generateModalIsOpen || testRunModalOpen ? null : ( <> {getworkflowStatus()} - {useReactFlow && ( - <div className="h-[94%]"> - <ReactFlowProvider> - <ReactFlowBuilder - workflow={workflow} - loadedAlertFile={loadedAlertFile} - providers={providers} - definition={definition} - validatorConfiguration={ValidatorConfigurationV2} - onDefinitionChange={(def: any) => { - setDefinition({ - value: { - sequence: def?.sequence || [], - properties - : def?. - properties - || {} - }, isValid: def?.isValid || false - }) - } - } - toolboxConfiguration={getToolboxConfiguration(providers)} - /> - </ReactFlowProvider> - </div> - )} - {!useReactFlow && ( - <div className="h-[93%]"> - <SequentialWorkflowDesigner + <div className="h-[94%]"> + <ReactFlowProvider> + <ReactFlowBuilder + installedProviders={installedProviders} definition={definition} - onDefinitionChange={setDefinition} - stepsConfiguration={stepsConfiguration} - validatorConfiguration={validatorConfiguration} - toolboxConfiguration={getToolboxConfiguration(providers)} - undoStackSize={10} - controlBar={true} - globalEditor={<GlobalEditor />} - stepEditor={ - <StepEditor installedProviders={installedProviders} /> + validatorConfiguration={ValidatorConfigurationV2} + onDefinitionChange={(def: any) => { + setDefinition({ + value: { + sequence: def?.sequence || [], + properties + : def?. + properties + || {} + }, isValid: def?.isValid || false + }) } + } + toolboxConfiguration={getToolboxConfiguration(providers)} /> - </div> - )} + </ReactFlowProvider> + </div> </> )} </div> diff --git a/keep-ui/app/workflows/builder/editors.tsx b/keep-ui/app/workflows/builder/editors.tsx index 221a03c1e..a0505be92 100644 --- a/keep-ui/app/workflows/builder/editors.tsx +++ b/keep-ui/app/workflows/builder/editors.tsx @@ -7,14 +7,9 @@ import { Subtitle, Icon, Button, - Switch, } from "@tremor/react"; import { KeyIcon } from "@heroicons/react/20/solid"; import { Properties } from "sequential-workflow-designer"; -import { - useStepEditor, - useGlobalEditor, -} from "sequential-workflow-designer-react"; import { Provider } from "app/providers/providers"; import { BackspaceIcon, @@ -30,24 +25,6 @@ function EditorLayout({ children }: { children: React.ReactNode }) { return <div className="flex flex-col m-2.5">{children}</div>; } -export function GlobalEditor() { - const { properties, setProperty } = useGlobalEditor() - return ( - <EditorLayout> - <Title>Keep Workflow Editor - - Use this visual workflow editor to easily create or edit existing Keep - workflow YAML specifications. - - - Use the toolbox to add steps, conditions and actions to your workflow - and click the `Generate` button to compile the workflow / `Deploy` - button to deploy the workflow to Keep. - - {WorkflowEditor(properties, setProperty)} - - ); -} export function GlobalEditorV2() { const { v2Properties: properties, updateV2Properties: setProperty, selectedNode } = useStore(); @@ -76,7 +53,7 @@ export function GlobalEditorV2() { interface keepEditorProps { - properties: Properties; + properties: V2Properties; updateProperty: ((key: string, value: any) => void); installedProviders?: Provider[] | null | undefined; providerType?: string; @@ -273,154 +250,11 @@ function KeepForeachEditor({ properties, updateProperty }: keepEditorProps) { ); } -function WorkflowEditor(properties: Properties, updateProperty: any) { - /** - * TODO: support generate, add more triggers and complex filters - * Need to think about UX for this - */ - const propertyKeys = Object.keys(properties).filter( - (k) => k !== "isLocked" && k !== "id" - ); - - const updateAlertFilter = (filter: string, value: string) => { - const currentFilters = properties.alert as {}; - const updatedFilters = { ...currentFilters, [filter]: value }; - updateProperty("alert", updatedFilters); - }; - - const addFilter = () => { - const filterName = prompt("Enter filter name"); - if (filterName) { - updateAlertFilter(filterName, ""); - } - }; - - const addTrigger = (trigger: "manual" | "interval" | "alert") => { - updateProperty( - trigger, - trigger === "alert" ? { source: "" } : trigger === "manual" ? "true" : "" - ); - }; - - const deleteFilter = (filter: string) => { - const currentFilters = properties.alert as any; - delete currentFilters[filter]; - updateProperty("alert", currentFilters); - }; - - return ( - <> - Workflow Settings -
    - {Object.keys(properties).includes("manual") ? null : ( - - )} - {Object.keys(properties).includes("interval") ? null : ( - - )} - {Object.keys(properties).includes("alert") ? null : ( - - )} -
    - {propertyKeys.map((key, index) => { - return ( -
    - {key} - {key === "manual" ? ( -
    - - updateProperty(key, e.target.checked ? "true" : "false") - } - /> -
    - ) : key === "alert" ? ( - <> -
    - -
    - {properties.alert && - Object.keys(properties.alert as {}).map((filter) => { - return ( - <> - {filter} -
    - - updateAlertFilter(filter, e.target.value) - } - value={(properties.alert as any)[filter] as string} - /> - deleteFilter(filter)} - /> -
    - - ); - })} - - ) : ( - updateProperty(key, e.target.value)} - value={properties[key] as string} - /> - )} -
    - ); - })} - - ); -} function WorkflowEditorV2({ properties, setProperties, }: { - properties: Properties; + properties: V2Properties; setProperties: (updatedProperties: Properties) => void; }) { // const [properties, setProperties] = useState(initialProperties); @@ -584,8 +418,7 @@ export function StepEditorV2({ }: { installedProviders?: Provider[] | undefined | null; }) { - const [useGlobalEditor, setGlobalEditor] = useState(false); - const [formData, setFormData] = useState<{ name?: string; properties?: any }>({}); + const [formData, setFormData] = useState<{ name?: string; properties?: V2Properties, type?:string }>({}); const { selectedNode, updateSelectedNodeData, @@ -616,10 +449,6 @@ export function StepEditorV2({ }); }; - const handleSwitchChange = (value: boolean) => { - setGlobalEditor(value); - setOpneGlobalEditor(true); - }; const handleSubmit = () => { // Finalize the changes before saving @@ -627,22 +456,10 @@ export function StepEditorV2({ updateSelectedNodeData('properties', formData.properties); }; + const type = formData ? formData.type?.includes("step-") || formData.type?.includes("action-") : ""; + return ( - {/*
    - - -
    */} {providerType} Editor Unique Identifier - {formData.type?.includes("step-") || formData.type?.includes("action-") ? ( + {type && formData.properties ? ( ) : formData.type === "condition-threshold" ? ( ) : formData.type?.includes("foreach") ? ( ) : formData.type === "condition-assert" ? ( ) : null} @@ -684,57 +501,4 @@ export function StepEditorV2({
    ); -} - -export default function StepEditor({ - installedProviders, -}: { - installedProviders?: Provider[] | undefined | null; -}) { - const { type, componentType, name, setName, properties, setProperty } = - useStepEditor(); - - function onNameChanged(e: any) { - setName(e.target.value); - } - - - const providerType = type.split("-")[1]; - - return ( - - {providerType} Editor - Unique Identifier - - {type.includes("step-") || type.includes("action-") ? ( - - ) : type === "condition-threshold" ? ( - - ) : type.includes("foreach") ? ( - - ) : type === "condition-assert" ? ( - - ) : null} - - ); } \ No newline at end of file diff --git a/keep-ui/utils/hooks/useWorkflowInitialization.ts b/keep-ui/utils/hooks/useWorkflowInitialization.ts index 1e97ee6c7..9ffb1a12b 100644 --- a/keep-ui/utils/hooks/useWorkflowInitialization.ts +++ b/keep-ui/utils/hooks/useWorkflowInitialization.ts @@ -92,11 +92,7 @@ const getLayoutedElements = (nodes: FlowNode[], edges: Edge[], options = {}) => }; const useWorkflowInitialization = ( - workflow: string | undefined, - loadedAlertFile: string | null | undefined, - providers: Provider[], definition: ReactFlowDefinition, - onDefinitionChange: (def: Definition) => void, toolboxConfiguration: Record ) => { const { From 495656ca479344ed5a6e99c8941d292739bb78d2 Mon Sep 17 00:00:00 2001 From: Rajesh Jonnalagadda Date: Mon, 26 Aug 2024 12:51:12 +0530 Subject: [PATCH 42/55] chore:fix the toolbox position issue and cleanup the sequential flow design completely --- .../builder/BuilderChanagesTracker.tsx | 2 - keep-ui/app/workflows/builder/ToolBox.tsx | 2 +- keep-ui/app/workflows/builder/alert.tsx | 4 + .../app/workflows/builder/builder-store.tsx | 2 +- keep-ui/app/workflows/builder/builder.tsx | 15 ++-- keep-ui/app/workflows/builder/editors.tsx | 3 +- keep-ui/app/workflows/builder/types.tsx | 3 - keep-ui/app/workflows/builder/utils.tsx | 83 +++++++++++-------- keep-ui/utils/reactFlow.ts | 1 + 9 files changed, 60 insertions(+), 55 deletions(-) diff --git a/keep-ui/app/workflows/builder/BuilderChanagesTracker.tsx b/keep-ui/app/workflows/builder/BuilderChanagesTracker.tsx index 135485649..b861786be 100644 --- a/keep-ui/app/workflows/builder/BuilderChanagesTracker.tsx +++ b/keep-ui/app/workflows/builder/BuilderChanagesTracker.tsx @@ -2,8 +2,6 @@ import React from 'react' import useStore from './builder-store'; import { Button } from '@tremor/react'; import { reConstructWorklowToDefinition } from 'utils/reactFlow'; -import { WrappedDefinition } from 'sequential-workflow-designer-react'; -import { Definition } from 'sequential-workflow-designer'; export default function BuilderChanagesTracker({onDefinitionChange}:{onDefinitionChange:(def: Record) => void}) { const { diff --git a/keep-ui/app/workflows/builder/ToolBox.tsx b/keep-ui/app/workflows/builder/ToolBox.tsx index 0228462f7..7b3d187c7 100644 --- a/keep-ui/app/workflows/builder/ToolBox.tsx +++ b/keep-ui/app/workflows/builder/ToolBox.tsx @@ -130,7 +130,7 @@ const DragAndDropSidebar = ({ isDraggable }: { return (
    diff --git a/keep-ui/app/workflows/builder/alert.tsx b/keep-ui/app/workflows/builder/alert.tsx index d56a17b0e..0e6d2fafc 100644 --- a/keep-ui/app/workflows/builder/alert.tsx +++ b/keep-ui/app/workflows/builder/alert.tsx @@ -1,3 +1,5 @@ +import { V2Step } from "./builder-store"; + interface Provider { type: string; config: string; @@ -37,4 +39,6 @@ export interface Alert { services?: string[]; steps: Step[]; actions?: Action[]; + triggers?: any; + name?: string; } diff --git a/keep-ui/app/workflows/builder/builder-store.tsx b/keep-ui/app/workflows/builder/builder-store.tsx index 5098bbab0..946f613f2 100644 --- a/keep-ui/app/workflows/builder/builder-store.tsx +++ b/keep-ui/app/workflows/builder/builder-store.tsx @@ -37,7 +37,7 @@ export type V2Step = { name?: string; componentType: string; type: string; - properties?: V2Properties; + properties: V2Properties; branches?: { true: V2Step[]; false: V2Step[]; diff --git a/keep-ui/app/workflows/builder/builder.tsx b/keep-ui/app/workflows/builder/builder.tsx index 14a4108a9..c7a3bd436 100644 --- a/keep-ui/app/workflows/builder/builder.tsx +++ b/keep-ui/app/workflows/builder/builder.tsx @@ -1,10 +1,4 @@ import "./page.css"; -import { - Definition, -} from "sequential-workflow-designer"; -import { - wrapDefinition, -} from "sequential-workflow-designer-react"; import { useEffect, useState } from "react"; import { Callout, Card } from "@tremor/react"; import { Provider } from "../../providers/providers"; @@ -13,6 +7,7 @@ import { generateWorkflow, getToolboxConfiguration, buildAlert, + wrapDefinitionV2, } from "./utils"; import { CheckCircleIcon, @@ -64,7 +59,7 @@ function Builder({ }: Props) { const [definition, setDefinition] = useState(() => - wrapDefinition({ sequence: [], properties: {} } as Definition) + wrapDefinitionV2({ sequence: [], properties: {}, isValid: false }) ); const [isLoading, setIsLoading] = useState(true); const [stepValidationError, setStepValidationError] = useState( @@ -173,7 +168,7 @@ function Builder({ useEffect(() => { setIsLoading(true); if (workflow) { - setDefinition(wrapDefinition(parseWorkflow(workflow, providers))); + setDefinition(wrapDefinitionV2({...parseWorkflow(workflow, providers), isValid:true})); } else if (loadedAlertFile == null) { const alertUuid = uuidv4(); const alertName = searchParams?.get("alertName"); @@ -183,10 +178,10 @@ function Builder({ triggers = { alert: { source: alertSource, name: alertName } }; } setDefinition( - wrapDefinition(generateWorkflow(alertUuid, "", "", [], [], triggers)) + wrapDefinitionV2({...generateWorkflow(alertUuid, "", "", [], [], triggers), isValid: true}) ); } else { - setDefinition(wrapDefinition(parseWorkflow(loadedAlertFile!, providers))); + setDefinition(wrapDefinitionV2({...parseWorkflow(loadedAlertFile!, providers), isValid:true})); } setIsLoading(false); }, [loadedAlertFile, workflow, searchParams]); diff --git a/keep-ui/app/workflows/builder/editors.tsx b/keep-ui/app/workflows/builder/editors.tsx index a0505be92..14a70737d 100644 --- a/keep-ui/app/workflows/builder/editors.tsx +++ b/keep-ui/app/workflows/builder/editors.tsx @@ -9,7 +9,6 @@ import { Button, } from "@tremor/react"; import { KeyIcon } from "@heroicons/react/20/solid"; -import { Properties } from "sequential-workflow-designer"; import { Provider } from "app/providers/providers"; import { BackspaceIcon, @@ -255,7 +254,7 @@ function WorkflowEditorV2({ setProperties, }: { properties: V2Properties; - setProperties: (updatedProperties: Properties) => void; + setProperties: (updatedProperties: V2Properties) => void; }) { // const [properties, setProperties] = useState(initialProperties); // useEffect(() => { diff --git a/keep-ui/app/workflows/builder/types.tsx b/keep-ui/app/workflows/builder/types.tsx index 9f690da77..84a3928df 100644 --- a/keep-ui/app/workflows/builder/types.tsx +++ b/keep-ui/app/workflows/builder/types.tsx @@ -1,6 +1,3 @@ -import { Step } from "sequential-workflow-designer"; -export interface KeepStep extends Step {} - export interface LogEntry { timestamp: string; message: string; diff --git a/keep-ui/app/workflows/builder/utils.tsx b/keep-ui/app/workflows/builder/utils.tsx index 253ddbdde..77a0404ac 100644 --- a/keep-ui/app/workflows/builder/utils.tsx +++ b/keep-ui/app/workflows/builder/utils.tsx @@ -1,16 +1,10 @@ import { load, JSON_SCHEMA } from "js-yaml"; import { Provider } from "../../providers/providers"; -import { - BranchedStep, - Definition, - SequentialStep, - Step, - StepDefinition, - Uid, -} from "sequential-workflow-designer"; -import { KeepStep } from "./types"; import { Action, Alert } from "./alert"; import { stringify } from "yaml"; +import { V2Properties, V2Step, Definition } from "./builder-store"; +import { v4 as uuidv4 } from "uuid"; + export function getToolboxConfiguration(providers: Provider[]) { /** @@ -24,7 +18,7 @@ export function getToolboxConfiguration(providers: Provider[]) { stepParams: provider.query_params!, actionParams: provider.notify_params!, }, - }; + } as Partial; if (provider.can_query) steps.push({ ...step, @@ -39,7 +33,7 @@ export function getToolboxConfiguration(providers: Provider[]) { }); return [steps, actions]; }, - [[] as StepDefinition[], [] as StepDefinition[]] + [[] as Partial[], [] as Partial[]] ); return { groups: [ @@ -103,14 +97,14 @@ export function getActionOrStepObj( actionOrStep: any, type: "action" | "step", providers?: Provider[] -): KeepStep { +): V2Step { /** * Generate a step or action definition (both are kinda the same) */ const providerType = actionOrStep.provider?.type; const provider = providers?.find((p) => p.type === providerType); return { - id: Uid.next(), + id: actionOrStep?.id || uuidv4(), name: actionOrStep.name, componentType: "task", type: `${type}-${providerType}`, @@ -134,7 +128,7 @@ function generateForeach( sequence?: any ) { return { - id: Uid.next(), + id: actionOrStep?.id || uuidv4(), type: "foreach", componentType: "container", name: "Foreach", @@ -154,7 +148,7 @@ export function generateCondition( ): any { const stepOrAction = action.type === "step" ? "step" : "action"; const generatedCondition = { - id: Uid.next(), + id: condition.id || uuidv4(), name: condition.name, type: `condition-${condition.type}`, componentType: "switch", @@ -183,8 +177,8 @@ export function generateWorkflow( workflowId: string, name: string, description: string, - steps: Step[], - conditions: Step[], + steps: V2Step[], + conditions: V2Step[], triggers: { [key: string]: { [key: string]: string } } = {} ): Definition { /** @@ -217,9 +211,9 @@ export function parseWorkflow( const workflow = parsedWorkflowFile.alert ? parsedWorkflowFile.alert : parsedWorkflowFile.workflow; - const steps = [] as any; + const steps = [] as V2Step[]; const workflowSteps = - workflow.steps?.map((s: any) => { + workflow.steps?.map((s: V2Step) => { s.type = "step"; return s; }) || []; @@ -283,7 +277,11 @@ export function parseWorkflow( ); } -function getWithParams(s: Step): any { +function getWithParams(s: V2Step): any { + if(!s){ + return; + } + s.properties = (s.properties || {}) as V2Properties; const withParams = (s.properties.with as { [key: string]: string | number | boolean | object; @@ -294,14 +292,14 @@ function getWithParams(s: Step): any { const withParamValue = withParams[key] as string; const withParamJson = JSON.parse(withParamValue); withParams[key] = withParamJson; - } catch {} + } catch { } }); } return withParams; } function getActionsFromCondition( - condition: BranchedStep, + condition: V2Step, foreach?: string ): Action[] { const compiledCondition = { @@ -309,11 +307,12 @@ function getActionsFromCondition( type: condition.type.replace("condition-", ""), ...condition.properties, }; - const compiledActions = condition.branches.true.map((a) => { + const steps = condition?.branches?.true || [] as V2Step[]; + const compiledActions = steps.map((a:V2Step) => { const withParams = getWithParams(a); - const providerType = a.type.replace("action-", ""); + const providerType = a?.type?.replace("action-", ""); const providerName = - (a.properties.config as string)?.trim() || `default-${providerType}`; + (a?.properties?.config as string)?.trim() || `default-${providerType}`; const provider = { type: a.type.replace("action-", ""), config: `{{ providers.${providerName} }}`, @@ -393,7 +392,7 @@ export function buildAlert(definition: Definition): Alert { if: ifParam as string, }; } - else{ + else { return { name: s.name, provider: provider, @@ -404,15 +403,16 @@ export function buildAlert(definition: Definition): Alert { alert.sequence .filter((step) => step.type === "foreach") ?.forEach((forEach) => { - const forEachValue = forEach.properties.value as string; - const condition = (forEach as SequentialStep).sequence.find((c) => + const forEachValue = forEach?.properties?.value as string; + const condition = forEach?.sequence?.find((c) => c.type.startsWith("condition-") - ) as BranchedStep; + ) as V2Step; let foreachActions = [] as Action[]; if (condition) { foreachActions = getActionsFromCondition(condition, forEachValue); } else { - const stepOrAction = (forEach as SequentialStep).sequence[0]; + const forEachSequence = forEach?.sequence || [] as V2Step[]; + const stepOrAction = forEachSequence[0] || {} as V2Step[]; const withParams = getWithParams(stepOrAction); const providerType = stepOrAction.type .replace("action-", "") @@ -421,14 +421,14 @@ export function buildAlert(definition: Definition): Alert { const providerName = (stepOrAction.properties.config as string)?.trim() || `default-${providerType}`; - const provider: any = { + const provider = { type: stepOrAction.type.replace("action-", "").replace("step-", ""), config: `{{ providers.${providerName} }}`, with: withParams, }; foreachActions = [ { - name: stepOrAction.name, + name: stepOrAction.name || '', provider: provider, foreach: forEachValue, if: ifParam as string, @@ -442,7 +442,7 @@ export function buildAlert(definition: Definition): Alert { .filter((step) => step.type.startsWith("condition-")) ?.forEach((condition) => { const conditionActions = getActionsFromCondition( - condition as BranchedStep + condition as V2Step ); actions = [...actions, ...conditionActions]; }); @@ -470,7 +470,8 @@ export function buildAlert(definition: Definition): Alert { value: alert.properties.interval, }); } - const compiledAlert = { + + return { id: alertId, name: name, triggers: triggers, @@ -479,6 +480,16 @@ export function buildAlert(definition: Definition): Alert { services: services, steps: steps, actions: actions, - }; - return compiledAlert; + } as Alert; } + + +export function wrapDefinitionV2({ properties, sequence, isValid }: { properties: V2Properties, sequence: V2Step[], isValid?: boolean }) { + return { + value: { + sequence: sequence, + properties: properties + }, + isValid: !!isValid + } +} \ No newline at end of file diff --git a/keep-ui/utils/reactFlow.ts b/keep-ui/utils/reactFlow.ts index 25914bed4..5a6687792 100644 --- a/keep-ui/utils/reactFlow.ts +++ b/keep-ui/utils/reactFlow.ts @@ -168,6 +168,7 @@ export function createSwitchNodeV2( type: `${step.type}__end`, name: `${stepType} End`, componentType: `${step.type}__end`, + properties:{} } as V2Step, isDraggable: false, prevNodeId: nodeId, From 9c5dcd3ff8f891167fca52626d164339f1b97dfa Mon Sep 17 00:00:00 2001 From: Rajesh Jonnalagadda Date: Mon, 26 Aug 2024 14:52:33 +0530 Subject: [PATCH 43/55] fix:preview deployment is resolved --- .../app/workflows/builder/builder-validators.tsx | 5 +++++ keep-ui/app/workflows/builder/editors.tsx | 4 ---- keep-ui/app/workflows/preview/[workflowId]/page.tsx | 2 +- keep-ui/utils/hooks/useWorkflowInitialization.ts | 13 +++++-------- keep-ui/utils/reactFlow.ts | 1 - 5 files changed, 11 insertions(+), 14 deletions(-) diff --git a/keep-ui/app/workflows/builder/builder-validators.tsx b/keep-ui/app/workflows/builder/builder-validators.tsx index cd703efeb..b08f531e5 100644 --- a/keep-ui/app/workflows/builder/builder-validators.tsx +++ b/keep-ui/app/workflows/builder/builder-validators.tsx @@ -5,6 +5,11 @@ export function globalValidatorV2( definition: FlowDefinition, setGlobalValidationError: Dispatch> ): boolean { + const workflowName = definition?.properties?.name; + if(!workflowName) { + setGlobalValidationError("Workflow name cannot be empty."); + return false; + } const anyStepOrAction = definition?.sequence?.length > 0; if (!anyStepOrAction) { setGlobalValidationError( diff --git a/keep-ui/app/workflows/builder/editors.tsx b/keep-ui/app/workflows/builder/editors.tsx index 14a70737d..ddb6d0852 100644 --- a/keep-ui/app/workflows/builder/editors.tsx +++ b/keep-ui/app/workflows/builder/editors.tsx @@ -256,10 +256,6 @@ function WorkflowEditorV2({ properties: V2Properties; setProperties: (updatedProperties: V2Properties) => void; }) { - // const [properties, setProperties] = useState(initialProperties); - // useEffect(() => { - // setProperties(initialProperties); - // }, [initialProperties]); const updateAlertFilter = (filter: string, value: string) => { const currentFilters = properties.alert || {}; diff --git a/keep-ui/app/workflows/preview/[workflowId]/page.tsx b/keep-ui/app/workflows/preview/[workflowId]/page.tsx index 58826f465..fb6461939 100644 --- a/keep-ui/app/workflows/preview/[workflowId]/page.tsx +++ b/keep-ui/app/workflows/preview/[workflowId]/page.tsx @@ -29,7 +29,7 @@ export default function PageWithId({ {workflowPreviewData && workflowPreviewData.name === key && ( )} {workflowPreviewData && workflowPreviewData.name !== key && ( diff --git a/keep-ui/utils/hooks/useWorkflowInitialization.ts b/keep-ui/utils/hooks/useWorkflowInitialization.ts index 9ffb1a12b..bfb9a376a 100644 --- a/keep-ui/utils/hooks/useWorkflowInitialization.ts +++ b/keep-ui/utils/hooks/useWorkflowInitialization.ts @@ -10,7 +10,7 @@ import { Provider } from "app/providers/providers"; import ELK from 'elkjs/lib/elk.bundled.js'; import { processWorkflowV2 } from "utils/reactFlow"; -const layoutOptions = { +const layoutOptions = { "elk.nodeLabels.placement": "INSIDE V_CENTER H_BOTTOM", "elk.algorithm": "layered", "elk.direction": "BOTTOM", @@ -76,7 +76,7 @@ const getLayoutedElements = (nodes: FlowNode[], edges: Edge[], options = {}) => }; return elk - // @ts-ignore + // @ts-ignore .layout(graph) .then((layoutedGraph) => ({ nodes: layoutedGraph?.children?.map((node) => ({ @@ -111,17 +111,13 @@ const useWorkflowInitialization = ( setToolBoxConfig, isLayouted, setIsLayouted, - setLastSavedChanges, - changes, setChanges, setSelectedNode, - firstInitilisationDone, setFirstInitilisationDone } = useStore(); const [isLoading, setIsLoading] = useState(true); const { screenToFlowPosition } = useReactFlow(); - const { fitView } = useReactFlow(); const [finalNodes, setFinalNodes] = useState([]); const [finalEdges, setFinalEdges] = useState([]); @@ -162,7 +158,7 @@ const useWorkflowInitialization = ( setIsLayouted(true); setFinalEdges(layoutedEdges); setFinalNodes(layoutedNodes); - + }, ); }, @@ -179,7 +175,8 @@ const useWorkflowInitialization = ( const initializeWorkflow = async () => { setIsLoading(true); let parsedWorkflow = definition?.value; - setV2Properties(parsedWorkflow?.properties ?? {}); + setV2Properties({ ...(parsedWorkflow?.properties ?? {}), name: parsedWorkflow?.properties?.name ?? parsedWorkflow?.properties?.id }); + const sequences = [ { id: "start", diff --git a/keep-ui/utils/reactFlow.ts b/keep-ui/utils/reactFlow.ts index 5a6687792..3356364b8 100644 --- a/keep-ui/utils/reactFlow.ts +++ b/keep-ui/utils/reactFlow.ts @@ -1,4 +1,3 @@ -import { v4 as uuidv4 } from "uuid"; import { FlowNode, NodeData, V2Properties, V2Step } from "app/workflows/builder/builder-store"; import { Edge } from "@xyflow/react"; From bcb8bdd0d6be22e73eb9575b42111b53b320a5ac Mon Sep 17 00:00:00 2001 From: Bhavya Jain Date: Tue, 27 Aug 2024 15:24:29 +0530 Subject: [PATCH 44/55] remove unused comments --- keep-ui/app/workflows/builder/CustomNode.tsx | 2 -- keep-ui/app/workflows/builder/editors.tsx | 3 --- 2 files changed, 5 deletions(-) diff --git a/keep-ui/app/workflows/builder/CustomNode.tsx b/keep-ui/app/workflows/builder/CustomNode.tsx index 12a76d83a..e3a50ce95 100644 --- a/keep-ui/app/workflows/builder/CustomNode.tsx +++ b/keep-ui/app/workflows/builder/CustomNode.tsx @@ -38,8 +38,6 @@ function CustomNode({ id, data }: FlowNode) { className={`p-2 flex shadow-md rounded-md bg-white border-2 w-full h-full ${id === selectedNode ? "border-orange-500" : "border-stone-400" - // } custom-drag-handle`} - //to make the node draggale uncomment above line and }`} onClick={(e) => { e.stopPropagation(); diff --git a/keep-ui/app/workflows/builder/editors.tsx b/keep-ui/app/workflows/builder/editors.tsx index ddb6d0852..45e581791 100644 --- a/keep-ui/app/workflows/builder/editors.tsx +++ b/keep-ui/app/workflows/builder/editors.tsx @@ -36,9 +36,6 @@ export function GlobalEditorV2() { workflow YAML specifications. - {/* Use the toolbox to add steps, conditions, and actions to your workflow - and click the `Generate` button to compile the workflow / `Deploy` - button to deploy the workflow to Keep. */} Use the edge add button or an empty step (a step with a +) to insert steps, conditions, and actions into your workflow. Then, click the Generate button to compile the workflow or the Deploy button to deploy it to Keep. From ce7da4c0f47694e2a11dccff1d69c0886aa5533c Mon Sep 17 00:00:00 2001 From: Rajesh Jonnalagadda Date: Tue, 27 Aug 2024 15:53:35 +0530 Subject: [PATCH 45/55] chore:remove commented lines --- keep-ui/app/workflows/builder/NodeMenu.tsx | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/keep-ui/app/workflows/builder/NodeMenu.tsx b/keep-ui/app/workflows/builder/NodeMenu.tsx index 8ea8e5c15..76104569c 100644 --- a/keep-ui/app/workflows/builder/NodeMenu.tsx +++ b/keep-ui/app/workflows/builder/NodeMenu.tsx @@ -50,25 +50,6 @@ export default function NodeMenu({ data, id }: { data: FlowNode["data"], id: str )} - {/* - {({ active }) => ( - - )} - */} {({ active }) => ( } + } ); diff --git a/keep-ui/app/workflows/builder/CustomNode.tsx b/keep-ui/app/workflows/builder/CustomNode.tsx index e3a50ce95..370bbc0bc 100644 --- a/keep-ui/app/workflows/builder/CustomNode.tsx +++ b/keep-ui/app/workflows/builder/CustomNode.tsx @@ -1,19 +1,20 @@ import React, { memo } from "react"; import { Handle, Position } from "@xyflow/react"; import NodeMenu from "./NodeMenu"; -import useStore, { FlowNode } from "./builder-store"; +import useStore, { FlowNode, V2Step } from "./builder-store"; import Image from "next/image"; import { GoPlus } from "react-icons/go"; import { MdNotStarted } from "react-icons/md"; import { GoSquareFill } from "react-icons/go"; -import { PiSquareLogoFill } from "react-icons/pi"; +import { PiDiamondsFourFill, PiSquareLogoFill } from "react-icons/pi"; import { BiSolidError } from "react-icons/bi"; +import { FaHandPointer } from "react-icons/fa"; function IconUrlProvider(data: FlowNode["data"]) { const { componentType, type } = data || {}; - if (type === "alert" || type === "workflow") return "/keep.png"; + if (type === "alert" || type === "workflow" || type === "trigger") return "/keep.png"; return `/icons/${type ?.replace("step-", "") ?.replace("action-", "") @@ -27,10 +28,21 @@ function CustomNode({ id, data }: FlowNode) { ?.replace("step-", "") ?.replace("action-", "") ?.replace("condition-", "") - ?.replace("__end", ""); + ?.replace("__end", "") + ?.replace("trigger_", ""); const isEmptyNode = !!data?.type?.includes("empty"); - const specialNodeCheck = ['start', 'end'].includes(type) + const specialNodeCheck = ['start', 'end', 'trigger_end', 'trigger_start'].includes(type) + + function getTriggerIcon(step: any) { + const { type } = step; + switch (type) { + case "manual": + return + case "interval": + return + } + } return ( <> @@ -67,13 +79,14 @@ function CustomNode({ id, data }: FlowNode) { {errorNode === id && } {!isEmptyNode && (
    - {data?.type} + />}
    {data?.name}
    diff --git a/keep-ui/app/workflows/builder/NodeMenu.tsx b/keep-ui/app/workflows/builder/NodeMenu.tsx index 76104569c..3b59220e7 100644 --- a/keep-ui/app/workflows/builder/NodeMenu.tsx +++ b/keep-ui/app/workflows/builder/NodeMenu.tsx @@ -9,12 +9,12 @@ export default function NodeMenu({ data, id }: { data: FlowNode["data"], id: str const stopPropagation = (e: React.MouseEvent) => { e.stopPropagation(); }; - const isEmptyOrEndNode = data?.type?.includes("empty") || id?.includes('end') + const hideMenu = data?.type?.includes("empty") || id?.includes('end') || id?.includes("start") const { deleteNodes, setSelectedNode, setStepEditorOpenForNode } = useStore(); return ( <> - {data && !isEmptyOrEndNode && ( + {data && !hideMenu && (
    void }) => { - const { selectedNode, changes, v2Properties, nodes, edges } = useStore(); + const { selectedNode, changes, v2Properties, nodes, edges, setOpneGlobalEditor } = useStore(); const [isOpen, setIsOpen] = useState(false); const stepEditorRef = useRef(null); const containerRef = useRef(null); + const isTrigger = ['interval', 'manual', 'alert'].includes(selectedNode || '') + const [synced, setSynced] = useState(true); useEffect(() => { setIsOpen(true); if (selectedNode) { const timer = setTimeout(() => { + if(isTrigger){ + setOpneGlobalEditor(true); + return; + } if (containerRef.current && stepEditorRef.current) { const containerRect = containerRef.current.getBoundingClientRect(); const stepEditorRect = stepEditorRef.current.getBoundingClientRect(); @@ -45,8 +51,9 @@ const ReactFlowEditor = ({ }, [selectedNode]); useEffect(() => { + setSynced(false); + const handleDefinitionChange = () => { - name: "foreach end" if (changes > 0) { let { sequence, properties } = reConstructWorklowToDefinition({ @@ -65,11 +72,14 @@ const ReactFlowEditor = ({ } if (!isValid) { - return onDefinitionChange({ sequence, properties, isValid }); + onDefinitionChange({ sequence, properties, isValid }); + setSynced(true); + return; } isValid = validatorConfiguration.root({ sequence, properties }); onDefinitionChange({ sequence, properties, isValid }); + setSynced(true); } }; @@ -106,9 +116,9 @@ const ReactFlowEditor = ({
    - - {!selectedNode?.includes('empty') && } - {!selectedNode?.includes('empty') && } + + {!selectedNode?.includes('empty') && !isTrigger && } + {!selectedNode?.includes('empty') && !isTrigger && }
    diff --git a/keep-ui/app/workflows/builder/ToolBox.tsx b/keep-ui/app/workflows/builder/ToolBox.tsx index 7b3d187c7..a2eebe5e6 100644 --- a/keep-ui/app/workflows/builder/ToolBox.tsx +++ b/keep-ui/app/workflows/builder/ToolBox.tsx @@ -6,6 +6,8 @@ import { IoChevronUp, IoClose } from "react-icons/io5"; import Image from "next/image"; import { IoIosArrowDown } from "react-icons/io"; import useStore, { V2Step } from "./builder-store"; +import { FaHandPointer } from "react-icons/fa"; +import { PiDiamondsFourFill } from "react-icons/pi"; const GroupedMenu = ({ name, steps, searchTerm, isDraggable = true }: { name: string, @@ -39,6 +41,17 @@ const GroupedMenu = ({ name, steps, searchTerm, isDraggable = true }: { ?.replace("condition-", "")}-icon.png`; } + + function getTriggerIcon(step: any) { + const { type } = step; + switch(type) { + case "manual": + return + case "interval": + return + } + } + const handleDragStart = (event: React.DragEvent, step: any) => { if (!isDraggable) { event.stopPropagation(); @@ -78,13 +91,14 @@ const GroupedMenu = ({ name, steps, searchTerm, isDraggable = true }: { title={step.name} onClick={(e) => handleAddNode(e, step)} > - {step?.type} + />} {step.name} ))} diff --git a/keep-ui/app/workflows/builder/builder-store.tsx b/keep-ui/app/workflows/builder/builder-store.tsx index 946f613f2..3af080e7a 100644 --- a/keep-ui/app/workflows/builder/builder-store.tsx +++ b/keep-ui/app/workflows/builder/builder-store.tsx @@ -16,9 +16,9 @@ import { createDefaultNodeV2 } from '../../../utils/reactFlow'; export type V2Properties = Record; -export type Definition = { - sequence: V2Step[]; - properties: V2Properties; +export type Definition = { + sequence: V2Step[]; + properties: V2Properties; isValid?: boolean; } @@ -43,9 +43,11 @@ export type V2Step = { false: V2Step[]; }; sequence?: V2Step[]; - edgeNotNeeded?:boolean; - edgeLabel?:string; + edgeNotNeeded?: boolean; + edgeLabel?: string; edgeColor?: string; + edgeSource?: string; + edgeTarget?: string; }; export type NodeData = Node["data"] & Record; @@ -137,13 +139,13 @@ export type FlowState = { setSelectedEdge: (id: string | null) => void; getEdgeById: (id: string) => Edge | undefined; changes: number; - setChanges: (changes: number)=>void; + setChanges: (changes: number) => void; firstInitilisationDone: boolean; setFirstInitilisationDone: (firstInitilisationDone: boolean) => void; - lastSavedChanges: {nodes: FlowNode[] | null, edges: Edge[] | null}; - setLastSavedChanges: ({nodes, edges}: {nodes: FlowNode[], edges: Edge[]}) => void; - setErrorNode: (id:string|null)=>void; - errorNode: string|null; + lastSavedChanges: { nodes: FlowNode[] | null, edges: Edge[] | null }; + setLastSavedChanges: ({ nodes, edges }: { nodes: FlowNode[], edges: Edge[] }) => void; + setErrorNode: (id: string | null) => void; + errorNode: string | null; }; @@ -161,18 +163,40 @@ function addNodeBetween(nodeOrEdge: string | null, step: V2Step, type: string, s edge = get().edges.find((edge) => edge.id === nodeOrEdge) as Edge; } - const { source: sourceId, target: targetId } = edge || {}; + let { source: sourceId, target: targetId } = edge || {}; if (!sourceId || !targetId) return; + const isTriggerComponent = step.componentType === 'trigger' + + if (sourceId !== 'trigger_start' && isTriggerComponent) { + return; + } + + if (sourceId == 'trigger_start' && !isTriggerComponent) { + return; + } + + const nodes = get().nodes; - const targetIndex = nodes.findIndex(node => node.id === targetId); + if (sourceId === 'trigger_start' && isTriggerComponent && nodes.find(node => node && step.id === node.id)) { + return; + } + + let targetIndex = nodes.findIndex(node => node.id === targetId); const sourceIndex = nodes.findIndex(node => node.id === sourceId); if (targetIndex == -1) { return; } - const newNodeId = uuidv4(); - const newStep = { ...step, id: newNodeId } - let { nodes: newNodes, edges } = processWorkflowV2([ + + if (sourceId === 'trigger_start') { + targetId = 'trigger_end'; + } + const newNodeId = isTriggerComponent ? step.id : uuidv4(); + const cloneStep = JSON.parse(JSON.stringify(step)); + const newStep = { ...cloneStep, id: newNodeId } + const edges = get().edges; + + let { nodes: newNodes, edges: newEdges } = processWorkflowV2([ { id: sourceId, type: 'temp_node', name: 'temp_node', 'componentType': 'temp_node', edgeLabel: edge.label, edgeColor: edge?.style?.stroke @@ -180,23 +204,27 @@ function addNodeBetween(nodeOrEdge: string | null, step: V2Step, type: string, s newStep, { id: targetId, type: 'temp_node', name: 'temp_node', 'componentType': 'temp_node', edgeNotNeeded: true } ] as V2Step[], { x: 0, y: 0 }, true); - const newEdges = [ - ...edges, - ...(get().edges.filter(edge => !(edge.source == sourceId && edge.target == targetId)) || []), + + + const finalEdges = [ + ...newEdges, + ...(edges.filter(edge => !(edge.source == sourceId && edge.target == targetId)) || []), ]; const isNested = !!(nodes[targetIndex]?.isNested || nodes[sourceIndex]?.isNested); newNodes = newNodes.map((node) => ({ ...node, isNested })); newNodes = [...nodes.slice(0, targetIndex), ...newNodes, ...nodes.slice(targetIndex)]; - set({ - edges: newEdges, + edges: finalEdges, nodes: newNodes, isLayouted: false, changes: get().changes + 1 }); if (type == 'edge') { - set({ selectedEdge: edges[edges.length - 1]?.id }); + set({ + selectedEdge: edges[edges.length - 1]?.id, + selectedNode: newNodeId + }); } if (type === 'node') { @@ -216,14 +244,14 @@ const useStore = create((set, get) => ({ isLayouted: false, selectedEdge: null, changes: 0, - lastSavedChanges:{nodes: [], edges:[]}, + lastSavedChanges: { nodes: [], edges: [] }, firstInitilisationDone: false, - errorNode:null, - setErrorNode: (id)=>set({errorNode: id}), + errorNode: null, + setErrorNode: (id) => set({ errorNode: id }), setFirstInitilisationDone: (firstInitilisationDone) => set({ firstInitilisationDone }), - setLastSavedChanges:({nodes, edges}:{nodes:FlowNode[],edges:Edge[]})=>set({lastSavedChanges: {nodes, edges}}), + setLastSavedChanges: ({ nodes, edges }: { nodes: FlowNode[], edges: Edge[] }) => set({ lastSavedChanges: { nodes, edges } }), setSelectedEdge: (id) => set({ selectedEdge: id, selectedNode: null, openGlobalEditor: true }), - setChanges: (changes:number)=>set({changes: changes}), + setChanges: (changes: number) => set({ changes: changes }), setIsLayouted: (isLayouted) => set({ isLayouted }), getEdgeById: (id) => get().edges.find((edge) => edge.id === id), addNodeBetween: (nodeOrEdge: string | null, step: any, type: string) => { @@ -237,13 +265,13 @@ const useStore = create((set, get) => ({ const updatedNodes = get().nodes.map((node) => { if (node.id === currentSelectedNode) { //properties changes should not reconstructed the defintion. only recontrreconstructing if there are any structural changes are done on the flow. - if(value){ - node.data[key] = value; + if (value) { + node.data[key] = value; } - if(!value){ + if (!value) { delete node.data[key]; } - return {...node} + return { ...node } } return node; }); @@ -255,8 +283,8 @@ const useStore = create((set, get) => ({ }, setV2Properties: (properties) => set({ v2Properties: properties }), updateV2Properties: (properties) => { - const updatedProperties = { ...get().v2Properties, ...properties}; - set({ v2Properties: updatedProperties, changes: get().changes+1 }); + const updatedProperties = { ...get().v2Properties, ...properties }; + set({ v2Properties: updatedProperties, changes: get().changes + 1 }); }, setSelectedNode: (id) => { set({ @@ -385,25 +413,32 @@ const useStore = create((set, get) => ({ const endNode = nodes[endIndex]; - const edges = get().edges; + let edges = get().edges; let finalEdges = edges; idArray = nodes.slice(nodeStartIndex, endIndex + 1).map((node) => node.id); - finalEdges = edges.filter((edge) => !(idArray.includes(edge.source) || idArray.includes(edge.target))); + + finalEdges = edges.filter((edge) => !(idArray.includes(edge.source) || idArray.includes(edge.target))); + if (['interval', 'alert', 'manual'].includes(ids) && edges.some((edge) => edge.source === 'trigger_start' && edge.target !== ids)) { + edges = edges.filter((edge) => !(idArray.includes(edge.source))); + } const sources = [...new Set(edges.filter((edge) => startNode.id === edge.target))]; const targets = [...new Set(edges.filter((edge) => endNode.id === edge.source))]; targets.forEach((edge) => { - finalEdges = [...finalEdges, ...sources.map((source:Edge) => createCustomEdgeMeta(source.source, edge.target, source.label as string) - )]; + const target = edge.source === 'trigger_start' ? 'triggger_end' : edge.target; + + finalEdges = [...finalEdges, ...sources.map((source: Edge) => createCustomEdgeMeta(source.source, target, source.label as string) + ).flat(1)]; }); + // } - nodes[endIndex+1].position = {x: 0, y:0}; - const newNode = createDefaultNodeV2({...nodes[endIndex+1].data, islayouted: false}, nodes[endIndex+1].id); + nodes[endIndex + 1].position = { x: 0, y: 0 }; - const newNodes = [...nodes.slice(0, nodeStartIndex), newNode, ...nodes.slice(endIndex + 2)]; + const newNode = createDefaultNodeV2({ ...nodes[endIndex + 1].data, islayouted: false }, nodes[endIndex + 1].id); + const newNodes = [...nodes.slice(0, nodeStartIndex), newNode, ...nodes.slice(endIndex + 2)]; set({ edges: finalEdges, nodes: newNodes, diff --git a/keep-ui/app/workflows/builder/editors.tsx b/keep-ui/app/workflows/builder/editors.tsx index 45e581791..c068362f4 100644 --- a/keep-ui/app/workflows/builder/editors.tsx +++ b/keep-ui/app/workflows/builder/editors.tsx @@ -25,7 +25,7 @@ function EditorLayout({ children }: { children: React.ReactNode }) { } -export function GlobalEditorV2() { +export function GlobalEditorV2({synced}: {synced: boolean}) { const { v2Properties: properties, updateV2Properties: setProperty, selectedNode } = useStore(); return ( @@ -39,9 +39,11 @@ export function GlobalEditorV2() { Use the edge add button or an empty step (a step with a +) to insert steps, conditions, and actions into your workflow. Then, click the Generate button to compile the workflow or the Deploy button to deploy it to Keep. +
    {synced ? "Synced" : "Not Synced"}
    ); @@ -249,10 +251,14 @@ function KeepForeachEditor({ properties, updateProperty }: keepEditorProps) { function WorkflowEditorV2({ properties, setProperties, + selectedNode }: { properties: V2Properties; setProperties: (updatedProperties: V2Properties) => void; + selectedNode: string | null; }) { + const isTrigger = ['interval', 'manual', 'alert'].includes(selectedNode || '') + const updateAlertFilter = (filter: string, value: string) => { const currentFilters = properties.alert || {}; @@ -267,17 +273,17 @@ function WorkflowEditorV2({ } }; - const addTrigger = (trigger: "manual" | "interval" | "alert") => { - setProperties({ - ...properties, - [trigger]: - trigger === "alert" - ? { source: "" } - : trigger === "manual" - ? "true" - : "", - }); - }; + // const addTrigger = (trigger: "manual" | "interval" | "alert") => { + // setProperties({ + // ...properties, + // [trigger]: + // trigger === "alert" + // ? { source: "" } + // : trigger === "manual" + // ? "true" + // : "", + // }); + // }; const deleteFilter = (filter: string) => { const currentFilters = { ...properties.alert }; @@ -293,7 +299,7 @@ function WorkflowEditorV2({ <> Workflow Settings
    - {Object.keys(properties).includes("manual") ? null : ( + {/* {Object.keys(properties).includes("manual") ? null : ( - )} + )} */}
    {propertyKeys.map((key, index) => { return ( -
    - {key} +
    + {((key === selectedNode)||(!["manual", "alert", 'interval'].includes(key))) && {key}} {key === "manual" ? ( -
    + selectedNode === 'manual' &&
    ) : key === "alert" ? ( - <> -
    + selectedNode === 'alert' && <> +
    ); })} @@ -407,8 +420,10 @@ function WorkflowEditorV2({ export function StepEditorV2({ installedProviders, + setSynced }: { installedProviders?: Provider[] | undefined | null; + setSynced: (sync:boolean) => void; }) { const [formData, setFormData] = useState<{ name?: string; properties?: V2Properties, type?:string }>({}); const { @@ -432,6 +447,7 @@ export function StepEditorV2({ const handleInputChange = (e: React.ChangeEvent) => { setFormData({ ...formData, [e.target.name]: e.target.value }); + setSynced(false); }; const handlePropertyChange = (key: string, value: any) => { @@ -439,6 +455,7 @@ export function StepEditorV2({ ...formData, properties: { ...formData.properties, [key]: value }, }); + setSynced(false); }; diff --git a/keep-ui/app/workflows/builder/utils.tsx b/keep-ui/app/workflows/builder/utils.tsx index 77a0404ac..12312f7f5 100644 --- a/keep-ui/app/workflows/builder/utils.tsx +++ b/keep-ui/app/workflows/builder/utils.tsx @@ -2,7 +2,7 @@ import { load, JSON_SCHEMA } from "js-yaml"; import { Provider } from "../../providers/providers"; import { Action, Alert } from "./alert"; import { stringify } from "yaml"; -import { V2Properties, V2Step, Definition } from "./builder-store"; +import { V2Properties, V2Step, Definition } from "./builder-store"; import { v4 as uuidv4 } from "uuid"; @@ -37,6 +37,41 @@ export function getToolboxConfiguration(providers: Provider[]) { ); return { groups: [ + { + name: "Triggers", + steps: [ + { + type: "manual", + componentType: "trigger", + name: "Manual", + id: 'manual', + properties: { + name: "", + description: "", + }, + }, + { + type: "interval", + componentType: "trigger", + name: "Interval", + id: 'interval', + properties: { + interval: "" + }, + }, + { + type: "alert", + componentType: "trigger", + name: "Alert", + id: 'alert', + properties: { + alert: { + source: "", + } + }, + }, + ], + }, { name: "Steps", steps: steps, @@ -148,7 +183,7 @@ export function generateCondition( ): any { const stepOrAction = action.type === "step" ? "step" : "action"; const generatedCondition = { - id: condition.id || uuidv4(), + id: condition.id || uuidv4(), name: condition.name, type: `condition-${condition.type}`, componentType: "switch", @@ -278,7 +313,7 @@ export function parseWorkflow( } function getWithParams(s: V2Step): any { - if(!s){ + if (!s) { return; } s.properties = (s.properties || {}) as V2Properties; @@ -308,7 +343,7 @@ function getActionsFromCondition( ...condition.properties, }; const steps = condition?.branches?.true || [] as V2Step[]; - const compiledActions = steps.map((a:V2Step) => { + const compiledActions = steps.map((a: V2Step) => { const withParams = getWithParams(a); const providerType = a?.type?.replace("action-", ""); const providerName = diff --git a/keep-ui/utils/hooks/useWorkflowInitialization.ts b/keep-ui/utils/hooks/useWorkflowInitialization.ts index bfb9a376a..828376da9 100644 --- a/keep-ui/utils/hooks/useWorkflowInitialization.ts +++ b/keep-ui/utils/hooks/useWorkflowInitialization.ts @@ -8,7 +8,7 @@ import useStore, { Definition, ReactFlowDefinition, V2Step } from "../../app/wor import { FlowNode } from "../../app/workflows/builder/builder-store"; import { Provider } from "app/providers/providers"; import ELK from 'elkjs/lib/elk.bundled.js'; -import { processWorkflowV2 } from "utils/reactFlow"; +import { processWorkflowV2, getTriggerStep } from "utils/reactFlow"; const layoutOptions = { "elk.nodeLabels.placement": "INSIDE V_CENTER H_BOTTOM", @@ -186,6 +186,7 @@ const useWorkflowInitialization = ( isLayouted: false, name: "start" } as V2Step, + ...(getTriggerStep(parsedWorkflow?.properties)), ...(parsedWorkflow?.sequence || []), { id: "end", diff --git a/keep-ui/utils/reactFlow.ts b/keep-ui/utils/reactFlow.ts index 3356364b8..4d4ecd0aa 100644 --- a/keep-ui/utils/reactFlow.ts +++ b/keep-ui/utils/reactFlow.ts @@ -17,7 +17,8 @@ export function reConstructWorklowToDefinition({ properties: Record }) { - const originalNodes = nodes.slice(1, nodes.length - 1); + let originalNodes = nodes.slice(1, nodes.length - 1); + originalNodes = originalNodes.filter((node) => !node.data.componentType.includes('trigger')); function processForeach(startIdx: number, endIdx: number, foreachNode: FlowNode['data'], nodeId: string) { foreachNode.sequence = []; @@ -120,6 +121,29 @@ export function reConstructWorklowToDefinition({ return workflowSequence; } + const triggerNodes = nodes.reduce((obj, node) => { + if (['interval', 'alert', 'manual'].includes(node.id)) { + obj[node.id] = true; + } + return obj; + }, {} as Record); + + ['interval', 'alert', 'manual'].forEach(type => { + if (!triggerNodes[type]) { + switch (type) { + case "interval": + properties['interval'] = ""; + break; + case "alert": + properties['alert'] = {}; + break; + case "manual": + properties['manual'] = ""; + break; + default: //do nothing + } + } + }); return { sequence: buildWorkflowDefinition(0, originalNodes.length) as V2Step[], properties: properties as V2Properties @@ -167,7 +191,7 @@ export function createSwitchNodeV2( type: `${step.type}__end`, name: `${stepType} End`, componentType: `${step.type}__end`, - properties:{} + properties: {} } as V2Step, isDraggable: false, prevNodeId: nodeId, @@ -245,7 +269,7 @@ export function handleSwitchNode(step: V2Step, position: FlowNode['position'], n ...falseSubflowEdges, ...trueSubflowEdges, //handling the switch end edge - createCustomEdgeMeta(switchEndNode.id, nextNodeId) + ...createCustomEdgeMeta(switchEndNode.id, nextNodeId) ] }; @@ -283,19 +307,31 @@ const getRandomColor = () => { return color; }; -export function createCustomEdgeMeta(source: string, target: string, label?: string, color?: string, type?: string) { - return { - id: `e${source}-${target}`, - source: source ?? "", - target: target ?? "", - type: type || "custom-edge", - label, - style: { stroke: color || getRandomColor() } - } as Edge +export function createCustomEdgeMeta(source: string | string[], target: string | string[], label?: string, color?: string, type?: string) { + + source = Array.isArray(source) ? source : [source]; + target = Array.isArray(target) ? target : [target]; + + const edges = [] as Edge[]; + console.log("source", source); + console.log("target", target); + source.forEach((source) => { + target.forEach((target) => { + edges.push({ + id: `e${source}-${target}`, + source: source ?? "", + target: target ?? "", + type: type || "custom-edge", + label, + style: { stroke: color || getRandomColor() } + } as Edge) + }) + }) + return edges; } export function handleDefaultNode(step: V2Step, position: FlowNode['position'], nextNodeId: string, prevNodeId: string, nodeId: string, isNested: boolean) { const nodes = []; - const edges = []; + let edges = [] as Edge[]; const newNode = createDefaultNodeV2( step, nodeId, @@ -309,7 +345,7 @@ export function handleDefaultNode(step: V2Step, position: FlowNode['position'], } // Handle edge for default nodes if (newNode.id !== "end" && !step.edgeNotNeeded) { - edges.push(createCustomEdgeMeta(newNode.id, nextNodeId, step.edgeLabel, step.edgeColor)); + edges = [...edges, ...createCustomEdgeMeta(newNode.id, step.edgeTarget || nextNodeId, step.edgeLabel, step.edgeColor)]; } return { nodes, edges }; } @@ -435,3 +471,52 @@ export const processWorkflowV2 = (sequence: V2Step[], position: FlowNode['positi } return { nodes: newNodes, edges: newEdges }; }; + + +export function getTriggerStep(properties: V2Properties) { + const _steps = [] as V2Step[]; + function _triggerSteps() { + + if (!properties) { + return _steps; + } + + Object.keys(properties).forEach((key) => { + if (['interval', 'manual', 'alert'].includes(key) && properties[key]) { + _steps.push({ + id: key, + type: key, + componentType: "trigger", + properties: properties[key], + name: key, + edgeTarget: 'trigger_end', + } as V2Step); + } + }) + return _steps; + } + + const steps = _triggerSteps(); + let triggerStartTargets: string | string[] = steps.map((step) => step.id); + triggerStartTargets = (triggerStartTargets.length ? triggerStartTargets : ""); + return [ + { + id: 'trigger_start', + name: 'Trigger Start', + type: 'trigger', + componentType: 'trigger', + edgeTarget: triggerStartTargets, + cantDelete: true, + }, + ...steps, + { + id: 'trigger_end', + name: 'Trigger End', + type: 'trigger', + componentType: 'trigger', + cantDelete: true, + } + + ] as V2Step[]; + +} From 5a68c3816deebdfe0e9961e3cebbb5c581b22235 Mon Sep 17 00:00:00 2001 From: Rajesh Jonnalagadda Date: Wed, 28 Aug 2024 15:24:12 +0530 Subject: [PATCH 48/55] fix: trigger input issue reoslved --- keep-ui/app/workflows/builder/builder-store.tsx | 13 +++++++++++-- keep-ui/app/workflows/builder/utils.tsx | 3 +-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/keep-ui/app/workflows/builder/builder-store.tsx b/keep-ui/app/workflows/builder/builder-store.tsx index 3af080e7a..77b318e55 100644 --- a/keep-ui/app/workflows/builder/builder-store.tsx +++ b/keep-ui/app/workflows/builder/builder-store.tsx @@ -176,7 +176,6 @@ function addNodeBetween(nodeOrEdge: string | null, step: V2Step, type: string, s return; } - const nodes = get().nodes; if (sourceId === 'trigger_start' && isTriggerComponent && nodes.find(node => node && step.id === node.id)) { return; @@ -223,7 +222,6 @@ function addNodeBetween(nodeOrEdge: string | null, step: V2Step, type: string, s if (type == 'edge') { set({ selectedEdge: edges[edges.length - 1]?.id, - selectedNode: newNodeId }); } @@ -231,6 +229,17 @@ function addNodeBetween(nodeOrEdge: string | null, step: V2Step, type: string, s set({ selectedNode: nodeOrEdge }); } + switch(newNodeId){ + case "interval": + case "manual": { + set({v2Properties: {...get().v2Properties, [newNodeId]: ""}}); + break; + } + case "alert": { + set({v2Properties: {...get().v2Properties, [newNodeId]: {}}}); + break; + } + } } const useStore = create((set, get) => ({ diff --git a/keep-ui/app/workflows/builder/utils.tsx b/keep-ui/app/workflows/builder/utils.tsx index 12312f7f5..6ebfaf2b5 100644 --- a/keep-ui/app/workflows/builder/utils.tsx +++ b/keep-ui/app/workflows/builder/utils.tsx @@ -46,8 +46,7 @@ export function getToolboxConfiguration(providers: Provider[]) { name: "Manual", id: 'manual', properties: { - name: "", - description: "", + manual: "", }, }, { From 41e9c8ebfb10db2a97a429035e0a987b4d00dda6 Mon Sep 17 00:00:00 2001 From: Rajesh Jonnalagadda Date: Wed, 28 Aug 2024 15:29:01 +0530 Subject: [PATCH 49/55] chore:remove commented code and unwanted logs --- keep-ui/app/workflows/builder/editors.tsx | 49 ----------------------- keep-ui/utils/reactFlow.ts | 2 - 2 files changed, 51 deletions(-) diff --git a/keep-ui/app/workflows/builder/editors.tsx b/keep-ui/app/workflows/builder/editors.tsx index c068362f4..f452d59e8 100644 --- a/keep-ui/app/workflows/builder/editors.tsx +++ b/keep-ui/app/workflows/builder/editors.tsx @@ -273,17 +273,6 @@ function WorkflowEditorV2({ } }; - // const addTrigger = (trigger: "manual" | "interval" | "alert") => { - // setProperties({ - // ...properties, - // [trigger]: - // trigger === "alert" - // ? { source: "" } - // : trigger === "manual" - // ? "true" - // : "", - // }); - // }; const deleteFilter = (filter: string) => { const currentFilters = { ...properties.alert }; @@ -298,44 +287,6 @@ function WorkflowEditorV2({ return ( <> Workflow Settings -
    - {/* {Object.keys(properties).includes("manual") ? null : ( - - )} - {Object.keys(properties).includes("interval") ? null : ( - - )} - {Object.keys(properties).includes("alert") ? null : ( - - )} */} -
    {propertyKeys.map((key, index) => { return (
    diff --git a/keep-ui/utils/reactFlow.ts b/keep-ui/utils/reactFlow.ts index 4d4ecd0aa..d9b6e07c3 100644 --- a/keep-ui/utils/reactFlow.ts +++ b/keep-ui/utils/reactFlow.ts @@ -313,8 +313,6 @@ export function createCustomEdgeMeta(source: string | string[], target: string | target = Array.isArray(target) ? target : [target]; const edges = [] as Edge[]; - console.log("source", source); - console.log("target", target); source.forEach((source) => { target.forEach((target) => { edges.push({ From 136259efad76333a642e8e0008a0db3e6710b82e Mon Sep 17 00:00:00 2001 From: Rajesh Jonnalagadda Date: Wed, 28 Aug 2024 18:24:49 +0530 Subject: [PATCH 50/55] chore:fix the lint issues --- keep-ui/utils/reactFlow.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/keep-ui/utils/reactFlow.ts b/keep-ui/utils/reactFlow.ts index d9b6e07c3..07cace66c 100644 --- a/keep-ui/utils/reactFlow.ts +++ b/keep-ui/utils/reactFlow.ts @@ -309,12 +309,12 @@ const getRandomColor = () => { export function createCustomEdgeMeta(source: string | string[], target: string | string[], label?: string, color?: string, type?: string) { - source = Array.isArray(source) ? source : [source]; - target = Array.isArray(target) ? target : [target]; + source = (Array.isArray(source) ? source : [source || ""]) as string[]; + target = (Array.isArray(target) ? target : [target || ""]) as string[]; const edges = [] as Edge[]; - source.forEach((source) => { - target.forEach((target) => { + source?.forEach((source) => { + target?.forEach((target) => { edges.push({ id: `e${source}-${target}`, source: source ?? "", From d6e4734cb695fe182c3525f7b1f56a2445004448 Mon Sep 17 00:00:00 2001 From: Rajesh Jonnalagadda Date: Wed, 28 Aug 2024 18:43:17 +0530 Subject: [PATCH 51/55] fix:fix build es lint issue --- keep-ui/utils/reactFlow.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/keep-ui/utils/reactFlow.ts b/keep-ui/utils/reactFlow.ts index 07cace66c..f9913494f 100644 --- a/keep-ui/utils/reactFlow.ts +++ b/keep-ui/utils/reactFlow.ts @@ -309,12 +309,12 @@ const getRandomColor = () => { export function createCustomEdgeMeta(source: string | string[], target: string | string[], label?: string, color?: string, type?: string) { - source = (Array.isArray(source) ? source : [source || ""]) as string[]; - target = (Array.isArray(target) ? target : [target || ""]) as string[]; + const finalSource = (Array.isArray(source) ? source : [source || ""]) as string[]; + const finalTarget = (Array.isArray(target) ? target : [target || ""]) as string[]; const edges = [] as Edge[]; - source?.forEach((source) => { - target?.forEach((target) => { + finalSource?.forEach((source) => { + finalTarget?.forEach((target) => { edges.push({ id: `e${source}-${target}`, source: source ?? "", From f662e58984796164203c23cc3f625c9ef1720015 Mon Sep 17 00:00:00 2001 From: Rajesh Jonnalagadda Date: Sun, 1 Sep 2024 19:15:48 +0530 Subject: [PATCH 52/55] chore:handled the golbal step validations and added toast for save and hide the tigger steps if it is already there in the workflow --- keep-ui/app/workflows/builder/CustomNode.tsx | 11 +++++- .../app/workflows/builder/ReactFlowEditor.tsx | 4 +- keep-ui/app/workflows/builder/ToolBox.tsx | 27 +++++++------ .../app/workflows/builder/builder-store.tsx | 10 +++++ .../workflows/builder/builder-validators.tsx | 38 ++++++++++++++++--- keep-ui/app/workflows/builder/builder.tsx | 10 ++++- keep-ui/app/workflows/builder/editors.tsx | 5 ++- keep-ui/app/workflows/builder/utils.tsx | 2 +- keep-ui/utils/reactFlow.ts | 19 +--------- 9 files changed, 85 insertions(+), 41 deletions(-) diff --git a/keep-ui/app/workflows/builder/CustomNode.tsx b/keep-ui/app/workflows/builder/CustomNode.tsx index 370bbc0bc..ed98207fb 100644 --- a/keep-ui/app/workflows/builder/CustomNode.tsx +++ b/keep-ui/app/workflows/builder/CustomNode.tsx @@ -9,6 +9,7 @@ import { GoSquareFill } from "react-icons/go"; import { PiDiamondsFourFill, PiSquareLogoFill } from "react-icons/pi"; import { BiSolidError } from "react-icons/bi"; import { FaHandPointer } from "react-icons/fa"; +import { toast } from "react-toastify"; @@ -23,7 +24,7 @@ function IconUrlProvider(data: FlowNode["data"]) { } function CustomNode({ id, data }: FlowNode) { - const { selectedNode, setSelectedNode, setOpneGlobalEditor, errorNode } = useStore(); + const { selectedNode, setSelectedNode, setOpneGlobalEditor, errorNode, synced } = useStore(); const type = data?.type ?.replace("step-", "") ?.replace("action-", "") @@ -53,6 +54,10 @@ function CustomNode({ id, data }: FlowNode) { }`} onClick={(e) => { e.stopPropagation(); + if(!synced){ + toast('Please save the previous step or wait while properties sync with the workflow.'); + return; + } if (type === 'start' || type === 'end' || id?.includes('end') || id?.includes('empty')) { if (id?.includes('empty')) { setSelectedNode(id); @@ -117,6 +122,10 @@ function CustomNode({ id, data }: FlowNode) { }} onClick={(e) => { e.stopPropagation(); + if(!synced){ + toast('Please save the previous step or wait while properties sync with the workflow.'); + return; + } if (type === 'start' || type === 'end' || id?.includes('end')) { setOpneGlobalEditor(true); return; diff --git a/keep-ui/app/workflows/builder/ReactFlowEditor.tsx b/keep-ui/app/workflows/builder/ReactFlowEditor.tsx index 8c6834aa7..22a574110 100644 --- a/keep-ui/app/workflows/builder/ReactFlowEditor.tsx +++ b/keep-ui/app/workflows/builder/ReactFlowEditor.tsx @@ -19,12 +19,11 @@ const ReactFlowEditor = ({ }; onDefinitionChange: (def: Definition) => void }) => { - const { selectedNode, changes, v2Properties, nodes, edges, setOpneGlobalEditor } = useStore(); + const { selectedNode, changes, v2Properties, nodes, edges, setOpneGlobalEditor, synced, setSynced } = useStore(); const [isOpen, setIsOpen] = useState(false); const stepEditorRef = useRef(null); const containerRef = useRef(null); const isTrigger = ['interval', 'manual', 'alert'].includes(selectedNode || '') - const [synced, setSynced] = useState(true); useEffect(() => { setIsOpen(true); @@ -81,6 +80,7 @@ const ReactFlowEditor = ({ onDefinitionChange({ sequence, properties, isValid }); setSynced(true); } + setSynced(true); }; const debouncedHandleDefinitionChange = debounce(handleDefinitionChange, 300); diff --git a/keep-ui/app/workflows/builder/ToolBox.tsx b/keep-ui/app/workflows/builder/ToolBox.tsx index a2eebe5e6..2f77cb8b3 100644 --- a/keep-ui/app/workflows/builder/ToolBox.tsx +++ b/keep-ui/app/workflows/builder/ToolBox.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useMemo } from "react"; import classNames from "classnames"; import { Disclosure } from "@headlessui/react"; import { Subtitle } from "@tremor/react"; @@ -18,7 +18,6 @@ const GroupedMenu = ({ name, steps, searchTerm, isDraggable = true }: { const [isOpen, setIsOpen] = useState(!!searchTerm || isDraggable); const { selectedNode, selectedEdge, addNodeBetween } = useStore(); - useEffect(() => { setIsOpen(!!searchTerm || !isDraggable); }, [searchTerm, isDraggable]); @@ -44,11 +43,11 @@ const GroupedMenu = ({ name, steps, searchTerm, isDraggable = true }: { function getTriggerIcon(step: any) { const { type } = step; - switch(type) { + switch (type) { case "manual": - return + return case "interval": - return + return } } @@ -91,8 +90,8 @@ const GroupedMenu = ({ name, steps, searchTerm, isDraggable = true }: { title={step.name} onClick={(e) => handleAddNode(e, step)} > - {getTriggerIcon(step)} - {!!step && !['interval', 'manual'].includes(step.type) &&{step?.type} { @@ -127,14 +126,20 @@ const DragAndDropSidebar = ({ isDraggable }: { setIsVisible(!isDraggable) }, [selectedNode, selectedEdge, isDraggable]); + const triggerNodeMap = nodes.filter((node: any) => ['interval', 'manual', 'alert'].includes(node?.id)).reduce((obj: any, node: any) => { + obj[node.id] = true; + return obj; + }, {} as Record); + - const filteredGroups = - toolboxConfiguration?.groups?.map((group: any) => ({ + const filteredGroups = useMemo(() => { + return toolboxConfiguration?.groups?.map((group: any) => ({ ...group, steps: group?.steps?.filter((step: any) => - step?.name?.toLowerCase().includes(searchTerm?.toLowerCase()) + step?.name?.toLowerCase().includes(searchTerm?.toLowerCase()) && !triggerNodeMap[step?.id] ), })) || []; + }, [toolboxConfiguration, searchTerm, nodes?.length]); const checkForSearchResults = searchTerm && !!filteredGroups?.find((group: any) => group?.steps?.length > 0); diff --git a/keep-ui/app/workflows/builder/builder-store.tsx b/keep-ui/app/workflows/builder/builder-store.tsx index 77b318e55..8d6131734 100644 --- a/keep-ui/app/workflows/builder/builder-store.tsx +++ b/keep-ui/app/workflows/builder/builder-store.tsx @@ -146,6 +146,8 @@ export type FlowState = { setLastSavedChanges: ({ nodes, edges }: { nodes: FlowNode[], edges: Edge[] }) => void; setErrorNode: (id: string | null) => void; errorNode: string | null; + synced: boolean; + setSynced: (synced: boolean) => void; }; @@ -256,6 +258,8 @@ const useStore = create((set, get) => ({ lastSavedChanges: { nodes: [], edges: [] }, firstInitilisationDone: false, errorNode: null, + synced: true, + setSynced: (sync) => set({ synced: sync }), setErrorNode: (id) => set({ errorNode: id }), setFirstInitilisationDone: (firstInitilisationDone) => set({ firstInitilisationDone }), setLastSavedChanges: ({ nodes, edges }: { nodes: FlowNode[], edges: Edge[] }) => set({ lastSavedChanges: { nodes, edges } }), @@ -448,6 +452,11 @@ const useStore = create((set, get) => ({ const newNode = createDefaultNodeV2({ ...nodes[endIndex + 1].data, islayouted: false }, nodes[endIndex + 1].id); const newNodes = [...nodes.slice(0, nodeStartIndex), newNode, ...nodes.slice(endIndex + 2)]; + if(['manual', 'alert', 'interval'].includes(ids)) { + const v2Properties = get().v2Properties; + delete v2Properties[ids]; + set({ v2Properties }); + } set({ edges: finalEdges, nodes: newNodes, @@ -456,6 +465,7 @@ const useStore = create((set, get) => ({ changes: get().changes + 1, openGlobalEditor: true, }); + }, updateEdge: (id: string, key: string, value: any) => { const edge = get().edges.find((e) => e.id === id); diff --git a/keep-ui/app/workflows/builder/builder-validators.tsx b/keep-ui/app/workflows/builder/builder-validators.tsx index b08f531e5..e1e131ac2 100644 --- a/keep-ui/app/workflows/builder/builder-validators.tsx +++ b/keep-ui/app/workflows/builder/builder-validators.tsx @@ -3,16 +3,44 @@ import { ReactFlowDefinition, V2Step, Definition as FlowDefinition } from "./bui export function globalValidatorV2( definition: FlowDefinition, - setGlobalValidationError: Dispatch> + setGlobalValidationError: (id:string|null, error:string|null)=>void, ): boolean { const workflowName = definition?.properties?.name; + const workflowDescription = definition?.properties?.description; if(!workflowName) { - setGlobalValidationError("Workflow name cannot be empty."); + setGlobalValidationError(null, "Workflow name cannot be empty."); return false; } + if(!workflowDescription){ + setGlobalValidationError(null, "Workflow description cannot be empty."); + return false; + } + + if ( + !!definition?.properties && + !('manual' in definition.properties) && + !('interval' in definition.properties) && + !('alert' in definition.properties) + ) { + setGlobalValidationError('trigger_start', "Workflow Should alteast have one trigger."); + return false; + + } + + if(definition?.properties && 'interval' in definition.properties && !definition.properties.interval){ + setGlobalValidationError('interval', "Workflow interval cannot be empty."); + return false; + } + + const alertSources = Object.values(definition.properties.alert||{}).filter(Boolean) + if(definition?.properties && 'alert' in definition.properties && alertSources.length==0){ + setGlobalValidationError('alert', "Workflow alert trigger cannot be empty."); + return false; + } + const anyStepOrAction = definition?.sequence?.length > 0; if (!anyStepOrAction) { - setGlobalValidationError( + setGlobalValidationError(null, "At least 1 step/action is required." ); } @@ -36,14 +64,14 @@ export function globalValidatorV2( "step-" ) ) { - setGlobalValidationError("Steps cannot be placed after actions."); + setGlobalValidationError(sequence[i].id,"Steps cannot be placed after actions."); return false; } } } } const valid = anyStepOrAction; - if (valid) setGlobalValidationError(null); + if (valid) setGlobalValidationError(null, ""); return valid; } diff --git a/keep-ui/app/workflows/builder/builder.tsx b/keep-ui/app/workflows/builder/builder.tsx index c7a3bd436..b2c7565e6 100644 --- a/keep-ui/app/workflows/builder/builder.tsx +++ b/keep-ui/app/workflows/builder/builder.tsx @@ -86,6 +86,14 @@ function Builder({ setErrorNode(null); } + const setGlobalValidationErrorV2 = (id:string|null, error: string | null) => { + setGlobalValidationError(error); + if (error && id) { + return setErrorNode(id) + } + setErrorNode(null); + } + const updateWorkflow = () => { const apiUrl = getApiURL(); const url = `${apiUrl}/workflows/${workflowId}`; @@ -243,7 +251,7 @@ function Builder({ } = { step: (step, parent, definition) => stepValidatorV2(step, setStepValidationErrorV2, parent, definition), - root: (def) => globalValidatorV2(def, setGlobalValidationError), + root: (def) => globalValidatorV2(def, setGlobalValidationErrorV2), } function closeGenerateModal() { diff --git a/keep-ui/app/workflows/builder/editors.tsx b/keep-ui/app/workflows/builder/editors.tsx index f452d59e8..f746f4dd8 100644 --- a/keep-ui/app/workflows/builder/editors.tsx +++ b/keep-ui/app/workflows/builder/editors.tsx @@ -289,19 +289,20 @@ function WorkflowEditorV2({ Workflow Settings {propertyKeys.map((key, index) => { return ( -
    +
    {((key === selectedNode)||(!["manual", "alert", 'interval'].includes(key))) && {key}} {key === "manual" ? ( selectedNode === 'manual' &&
    setProperties({ ...properties, [key]: e.target.checked ? "true" : "false", }) } + disabled={true} />
    ) : key === "alert" ? ( diff --git a/keep-ui/app/workflows/builder/utils.tsx b/keep-ui/app/workflows/builder/utils.tsx index 6ebfaf2b5..d89ef574e 100644 --- a/keep-ui/app/workflows/builder/utils.tsx +++ b/keep-ui/app/workflows/builder/utils.tsx @@ -46,7 +46,7 @@ export function getToolboxConfiguration(providers: Provider[]) { name: "Manual", id: 'manual', properties: { - manual: "", + manual: "true", }, }, { diff --git a/keep-ui/utils/reactFlow.ts b/keep-ui/utils/reactFlow.ts index f9913494f..88a3d9976 100644 --- a/keep-ui/utils/reactFlow.ts +++ b/keep-ui/utils/reactFlow.ts @@ -127,23 +127,6 @@ export function reConstructWorklowToDefinition({ } return obj; }, {} as Record); - - ['interval', 'alert', 'manual'].forEach(type => { - if (!triggerNodes[type]) { - switch (type) { - case "interval": - properties['interval'] = ""; - break; - case "alert": - properties['alert'] = {}; - break; - case "manual": - properties['manual'] = ""; - break; - default: //do nothing - } - } - }); return { sequence: buildWorkflowDefinition(0, originalNodes.length) as V2Step[], properties: properties as V2Properties @@ -509,7 +492,7 @@ export function getTriggerStep(properties: V2Properties) { ...steps, { id: 'trigger_end', - name: 'Trigger End', + name: 'Worfklow start', type: 'trigger', componentType: 'trigger', cantDelete: true, From 15d0cafe9866c39b06d151b9881b24d4a5e01f07 Mon Sep 17 00:00:00 2001 From: Rajesh Jonnalagadda Date: Sun, 1 Sep 2024 19:35:41 +0530 Subject: [PATCH 53/55] chore: fix typo --- keep-ui/utils/reactFlow.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/keep-ui/utils/reactFlow.ts b/keep-ui/utils/reactFlow.ts index 88a3d9976..4c4d7aa3b 100644 --- a/keep-ui/utils/reactFlow.ts +++ b/keep-ui/utils/reactFlow.ts @@ -492,8 +492,8 @@ export function getTriggerStep(properties: V2Properties) { ...steps, { id: 'trigger_end', - name: 'Worfklow start', - type: 'trigger', + name: 'Workflow start', + type: '', componentType: 'trigger', cantDelete: true, } From c86c313233e929924969c1cf3eb0f4084677a526 Mon Sep 17 00:00:00 2001 From: Rajesh Jonnalagadda Date: Sun, 1 Sep 2024 20:18:24 +0530 Subject: [PATCH 54/55] chore: added divider in gloabl editor --- keep-ui/app/workflows/builder/editors.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/keep-ui/app/workflows/builder/editors.tsx b/keep-ui/app/workflows/builder/editors.tsx index f746f4dd8..c7a44771f 100644 --- a/keep-ui/app/workflows/builder/editors.tsx +++ b/keep-ui/app/workflows/builder/editors.tsx @@ -7,6 +7,7 @@ import { Subtitle, Icon, Button, + Divider, } from "@tremor/react"; import { KeyIcon } from "@heroicons/react/20/solid"; import { Provider } from "app/providers/providers"; @@ -283,14 +284,17 @@ function WorkflowEditorV2({ const propertyKeys = Object.keys(properties).filter( (k) => k !== "isLocked" && k !== "id" ); - + let renderDivider = false; return ( <> Workflow Settings {propertyKeys.map((key, index) => { + const isTrigger = ["manual", "alert", 'interval'].includes(key) ; + renderDivider = isTrigger && key === selectedNode ? !renderDivider : false; return (
    - {((key === selectedNode)||(!["manual", "alert", 'interval'].includes(key))) && {key}} + { renderDivider && } + {((key === selectedNode)||(!isTrigger)) && {key}} {key === "manual" ? ( selectedNode === 'manual' &&
    Date: Sun, 1 Sep 2024 20:43:30 +0530 Subject: [PATCH 55/55] fix:truncate issue --- keep-ui/app/workflows/builder/CustomNode.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/keep-ui/app/workflows/builder/CustomNode.tsx b/keep-ui/app/workflows/builder/CustomNode.tsx index ed98207fb..4785a3940 100644 --- a/keep-ui/app/workflows/builder/CustomNode.tsx +++ b/keep-ui/app/workflows/builder/CustomNode.tsx @@ -15,7 +15,7 @@ import { toast } from "react-toastify"; function IconUrlProvider(data: FlowNode["data"]) { const { componentType, type } = data || {}; - if (type === "alert" || type === "workflow" || type === "trigger") return "/keep.png"; + if (type === "alert" || type === "workflow" || type === "trigger" || !type) return "/keep.png"; return `/icons/${type ?.replace("step-", "") ?.replace("action-", "") @@ -83,7 +83,7 @@ function CustomNode({ id, data }: FlowNode) { )} {errorNode === id && } {!isEmptyNode && ( -
    +
    {getTriggerIcon(data)} {!!data && !['interval', 'manual'].includes(data.type) &&
    {data?.name}
    - {type || data?.componentType} + {type}