From 0734f0f28b118e3a3a35b6f6b3992ac61ca049f8 Mon Sep 17 00:00:00 2001 From: Christopher Lo <46541035+topher-lo@users.noreply.github.com> Date: Sat, 2 Mar 2024 21:29:03 +0000 Subject: [PATCH] feat(ui): Implement drag and drop logic --- frontend/src/components/action-node.tsx | 97 +++++++++++++ frontend/src/components/action-tiles.tsx | 71 ++++++---- frontend/src/components/canvas.tsx | 117 ++++++++++++++++ frontend/src/components/workspace.tsx | 168 ++++++++++++----------- 4 files changed, 347 insertions(+), 106 deletions(-) create mode 100644 frontend/src/components/action-node.tsx create mode 100644 frontend/src/components/canvas.tsx diff --git a/frontend/src/components/action-node.tsx b/frontend/src/components/action-node.tsx new file mode 100644 index 000000000..a123ed5d9 --- /dev/null +++ b/frontend/src/components/action-node.tsx @@ -0,0 +1,97 @@ +import React from "react" +import { Handle, NodeProps, Position } from "reactflow" + +import { + ChevronsDownIcon, + CircleIcon, + GanttChartIcon, + PlayIcon, + TestTubeIcon, +} from "lucide-react" +import { Button } from "@/components/ui/button" +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" + +export interface ActionNodeData { + title: string + name: string + status: "online" | "error" | "offline" + numberOfEvents: number + // Generic metadata +} + +const handleStyle = { width: 8, height: 8 } + +export default React.memo(function ActionNode({ + data: { title, name, status, numberOfEvents }, +}: NodeProps) { + return ( + + +
+ {title} + {name} +
+
+ + + + + + + Run + + + Test action + + + View events + + + +
+
+ +
+
+ {status} +
+
+ {numberOfEvents} events +
+
+
+ + + +
+ ) +}) diff --git a/frontend/src/components/action-tiles.tsx b/frontend/src/components/action-tiles.tsx index 1937807f6..5ae10f3de 100644 --- a/frontend/src/components/action-tiles.tsx +++ b/frontend/src/components/action-tiles.tsx @@ -1,6 +1,6 @@ "use client" -import Link from "next/link" +import { DragEvent } from "react" import { LucideIcon } from "lucide-react" import { cn } from "@/lib/utils" @@ -13,7 +13,7 @@ import { interface ActionTilesProps { isCollapsed: boolean - links: { + tiles: { title: string label?: string icon: LucideIcon @@ -21,64 +21,87 @@ interface ActionTilesProps { }[] } -export function ActionTiles({ links, isCollapsed }: ActionTilesProps) { +export function ActionTiles({ tiles, isCollapsed }: ActionTilesProps) { + + const onDragStart = ( + event: DragEvent, + tile: { title: string; label?: string; icon: LucideIcon; variant: "default" | "ghost" } + ) => { + + const actionNodeData = { + title: tile.title, + name: tile.label || "", + status: "offline", + numberOfEvents: 0 + }; + event.dataTransfer.setData("application/reactflow", "action") + event.dataTransfer.setData("application/json", JSON.stringify(actionNodeData)) + event.dataTransfer.effectAllowed = "move"; + }; + return (
- {link.title} - {link.label && ( + {tile.title} + {tile.label && ( - {link.label} + {tile.label} )} ) : ( - e.currentTarget.style.cursor = "grab"} + onMouseOut={(e) => e.currentTarget.style.cursor = ""} + onDragStart={(event) => onDragStart(event, tile)} > - - {link.title} - {link.label && ( + + {tile.title} + {tile.label && ( - {link.label} + {tile.label} )} - + ) )} diff --git a/frontend/src/components/canvas.tsx b/frontend/src/components/canvas.tsx new file mode 100644 index 000000000..12a6483ff --- /dev/null +++ b/frontend/src/components/canvas.tsx @@ -0,0 +1,117 @@ +import React, { useCallback, useRef, useState } from "react" +import ReactFlow, { + Background, + Connection, + Controls, + Edge, + MarkerType, + Node, + OnConnect, + ReactFlowInstance, + ReactFlowProvider, + addEdge, + useEdgesState, + useNodesState, + useReactFlow, +} from "reactflow" + +import "reactflow/dist/style.css" +import { useToast } from "@/components/ui/use-toast" +import ActionNode, { ActionNodeData } from "@/components/action-node" + +let id = 0 +const getActionNodeId = (): string => `node_${id++}` + +const nodeTypes = { + action: ActionNode, +} + +const defaultEdgeOptions = { + markerEnd: { + type: MarkerType.ArrowClosed, + }, + style: { strokeWidth: 3 }, +} + +const Workflow: React.FC = () => { + const reactFlowWrapper = useRef(null) + const [nodes, setNodes, onNodesChange] = useNodesState([]) + const [edges, setEdges, onEdgesChange] = useEdgesState([]) + const [reactFlowInstance, setReactFlowInstance] = + useState(null) + const { toast } = useToast() + + const onConnect = useCallback( + (params: Edge | Connection) => { + setEdges((eds) => addEdge(params, eds)) + }, + [toast, edges, setEdges] + ) + + const onDragOver = useCallback((event: React.DragEvent) => { + event.preventDefault() + event.dataTransfer.dropEffect = "move" + }, []) + + const onDrop = (event: React.DragEvent) => { + event.preventDefault() + + // Limit total number of nodes + if (nodes.length >= 50) { + toast({ + title: "Invalid action", + description: "Maximum 50 nodes allowed.", + }) + return + } + + const reactFlowNodeType = event.dataTransfer.getData("application/reactflow"); + const actionNodeData = JSON.parse( + event.dataTransfer.getData("application/json") + ) as ActionNodeData + + if (!actionNodeData || !reactFlowNodeType || !reactFlowInstance) return + + const reactFlowNodePosition = reactFlowInstance.screenToFlowPosition({ + x: event.clientX, + y: event.clientY, + }) + + const newNode = { + id: getActionNodeId(), + type: reactFlowNodeType, + position: reactFlowNodePosition, + data: actionNodeData, + } as Node + + setNodes((nds) => nds.concat(newNode)) + } + + return ( +
+ + + + +
+ ) +} + +const WorkflowBuilder = ReactFlowProvider +const useWorkflowBuilder = useReactFlow + +export { WorkflowBuilder, useWorkflowBuilder } +export default Workflow diff --git a/frontend/src/components/workspace.tsx b/frontend/src/components/workspace.tsx index 804bd8c30..d55defdf1 100644 --- a/frontend/src/components/workspace.tsx +++ b/frontend/src/components/workspace.tsx @@ -13,6 +13,7 @@ import { } from "lucide-react" import { ActionTiles } from "@/components/action-tiles" +import Canvas, { WorkflowBuilder } from "@/components/canvas" import { cn } from "@/lib/utils" import { Separator } from "@/components/ui/separator" import { TooltipProvider } from "@/components/ui/tooltip" @@ -46,88 +47,91 @@ export function Workspace({ }; return ( - - { - document.cookie = `react-resizable-panels:layout=${JSON.stringify( - sizes - )}` - }} - className="h-full max-h-[800px] items-stretch" - > - + + { + document.cookie = `react-resizable-panels:layout=${JSON.stringify( + sizes + )}` + }} + className="h-full max-h-[800px] items-stretch" > - - - - - - - - - - - + + + + + + + + + + + + + + ) }