Skip to content

Commit

Permalink
feat+fix: Remove reliance on graph object data + improve node loading…
Browse files Browse the repository at this point in the history
… states (#519)
  • Loading branch information
daryllimyt authored Nov 13, 2024
1 parent 05255a3 commit 0f94710
Show file tree
Hide file tree
Showing 13 changed files with 212 additions and 329 deletions.
2 changes: 1 addition & 1 deletion frontend/src/components/ui/skeleton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ function Skeleton({
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn("animate-pulse rounded-md bg-primary/10", className)}
className={cn("animate-pulse rounded-md bg-primary/5", className)}
{...props}
/>
)
Expand Down
167 changes: 103 additions & 64 deletions frontend/src/components/workbench/canvas/action-node.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,25 +25,30 @@ import {
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { Separator } from "@/components/ui/separator"
import { Skeleton } from "@/components/ui/skeleton"
import { useToast } from "@/components/ui/use-toast"
import { CopyButton } from "@/components/copy-button"
import { getIcon } from "@/components/icons"
import { CenteredSpinner } from "@/components/loading/spinner"
import { AlertNotification } from "@/components/notifications"
import {
ActionSoruceSuccessHandle,
ActionSourceErrorHandle,
ActionTargetHandle,
} from "@/components/workbench/canvas/custom-handle"

/**
* Represents the data structure for an Action Node
* @deprecated Previous version contained additional fields that are no longer used.
* Extra fields in existing data structures will be ignored.
*/
export interface ActionNodeData {
type: string // alias for key
title: string
namespace: string
status: "online" | "offline"
isConfigured: boolean
numberOfEvents: number

// Allow any additional properties from legacy data
[key: string]: unknown
}

export type ActionNodeType = Node<ActionNodeData>

export default React.memo(function ActionNode({
Expand Down Expand Up @@ -82,72 +87,106 @@ export default React.memo(function ActionNode({
const edges = useEdges()
const incomingEdges = edges.filter((edge) => edge.target === id)

if (actionIsLoading) {
return <CenteredSpinner />
}
// Create a skeleton loading state within the card frame
const renderContent = () => {
if (actionIsLoading) {
return (
<>
<CardHeader className="p-4">
<div className="flex w-full items-center space-x-4">
<Skeleton className="size-10 rounded-full" />
<div className="flex w-full flex-1 justify-between space-x-12">
<div className="flex flex-col space-y-2">
<Skeleton className="h-4 w-32" />
<Skeleton className="h-3 w-24" />
</div>
<Skeleton className="size-6" />
</div>
</div>
</CardHeader>
<Separator />
<CardContent className="p-4 py-2">
<div className="grid grid-cols-2 space-x-4 text-xs text-muted-foreground">
<div className="flex items-center space-x-2">
<Skeleton className="size-4" />
<Skeleton className="h-3 w-16" />
</div>
</div>
</CardContent>
</>
)
}

if (!action) {
return (
<AlertNotification
variant="warning"
title="Could not load action"
message="Please try again."
/>
)
}
if (!action) {
return (
<div className="p-4">
<AlertNotification
variant="warning"
title="Could not load action"
message="Please try again."
/>
</div>
)
}

return (
<Card className={cn("min-w-72", selected && "shadow-xl drop-shadow-xl")}>
<CardHeader className="p-4">
<div className="flex w-full items-center space-x-4">
{getIcon(action.type, {
className: "size-10 p-2",
})}
return (
<>
<CardHeader className="p-4">
<div className="flex w-full items-center space-x-4">
{getIcon(action.type, {
className: "size-10 p-2",
})}

<div className="flex w-full flex-1 justify-between space-x-12">
<div className="flex flex-col">
<CardTitle className="flex w-full items-center space-x-2 text-xs font-medium leading-none">
<span>{action.title}</span>
<CopyButton
value={`\$\{\{ ACTIONS.${slugify(action.title)}.result \}\}`}
toastMessage="Copied action reference to clipboard"
tooltipMessage="Copy action reference"
/>
</CardTitle>
<CardDescription className="mt-2 text-xs text-muted-foreground">
{action.type}
</CardDescription>
<div className="flex w-full flex-1 justify-between space-x-12">
<div className="flex flex-col">
<CardTitle className="flex w-full items-center space-x-2 text-xs font-medium leading-none">
<span>{action.title}</span>
<CopyButton
value={`\$\{\{ ACTIONS.${slugify(action.title)}.result \}\}`}
toastMessage="Copied action reference to clipboard"
tooltipMessage="Copy action reference"
/>
</CardTitle>
<CardDescription className="mt-2 text-xs text-muted-foreground">
{action.type}
</CardDescription>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="m-0 size-6 p-0">
<ChevronDownIcon className="m-1 size-4 text-muted-foreground" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={handleDeleteNode}>
<Trash2Icon className="mr-2 size-4 text-red-600" />
<span className="text-xs text-red-600">Delete</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="m-0 size-6 p-0">
<ChevronDownIcon className="m-1 size-4 text-muted-foreground" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={handleDeleteNode}>
<Trash2Icon className="mr-2 size-4 text-red-600" />
<span className="text-xs text-red-600">Delete</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</CardHeader>
<Separator />
<CardContent className="p-4 py-2">
<div className="grid grid-cols-2 space-x-4 text-xs text-muted-foreground">
<div className="flex items-center space-x-2">
{isConfigured ? (
<CircleCheckBigIcon className="size-4 text-emerald-500" />
) : (
<LayoutListIcon className="size-4 text-gray-400" />
)}
<span className="text-xs capitalize">{isConfiguredMessage}</span>
</CardHeader>
<Separator />
<CardContent className="p-4 py-2">
<div className="grid grid-cols-2 space-x-4 text-xs text-muted-foreground">
<div className="flex items-center space-x-2">
{isConfigured ? (
<CircleCheckBigIcon className="size-4 text-emerald-500" />
) : (
<LayoutListIcon className="size-4 text-gray-400" />
)}
<span className="text-xs capitalize">{isConfiguredMessage}</span>
</div>
</div>
</div>
</CardContent>
</CardContent>
</>
)
}

return (
<Card className={cn("min-w-72", selected && "shadow-xl drop-shadow-xl")}>
{renderContent()}
<ActionTargetHandle
join_strategy={action?.control_flow?.join_strategy}
indegree={incomingEdges.length}
Expand Down
103 changes: 6 additions & 97 deletions frontend/src/components/workbench/canvas/canvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import { useWorkflow } from "@/providers/workflow"
import Dagre from "@dagrejs/dagre"
import { MoveHorizontalIcon, MoveVerticalIcon, PlusIcon } from "lucide-react"

import { createAction, updateWorkflowGraphObject } from "@/lib/workflow"
import { pruneGraphObject } from "@/lib/workflow"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { useToast } from "@/components/ui/use-toast"
Expand Down Expand Up @@ -149,40 +149,6 @@ export function isEphemeral<T>(node: Node<T>): boolean {
return ephemeralNodeTypes.includes(node?.type as string)
}

export async function createNewNode(
type: NodeTypename,
workflowId: string,
workspaceId: string,
nodeData: NodeData,
newPosition: XYPosition
): Promise<NodeType> {
const common = {
type,
position: newPosition,
data: nodeData,
}
let newNode: NodeType
switch (type) {
case "udf":
const actionId = await createAction(
nodeData.type,
nodeData.title,
workflowId,
workspaceId
)
// Then create Action node in React Flow
newNode = {
id: actionId,
...common,
} as ActionNodeType

return newNode
default:
console.error("Invalid node type")
throw new Error("Invalid node type")
}
}

export function WorkflowCanvas() {
const containerRef = useRef<HTMLDivElement>(null)
const connectingNodeId = useRef<string | null>(null)
Expand Down Expand Up @@ -335,60 +301,6 @@ export function WorkflowCanvas() {
event.dataTransfer.dropEffect = "move"
}, [])

// Adding a new node
const onDrop = async (event: React.DragEvent) => {
event.preventDefault()
if (!reactFlowInstance || !workflowId) {
return
}

// Limit total number of nodes
if (nodes.length >= 50) {
toast({
title: "Invalid action",
description: "Maximum 50 nodes allowed.",
})
return
}

const nodeTypename = event.dataTransfer.getData(
"application/reactflow"
) as NodeTypename
console.log("Node Typename:", nodeTypename)

const rawNodeData = event.dataTransfer.getData("application/json")
const nodeData = JSON.parse(rawNodeData) as NodeData

console.log("Action Node Data:", nodeData)

const reactFlowNodePosition = reactFlowInstance.screenToFlowPosition({
x: event.clientX,
y: event.clientY,
})
try {
const newNode = await createNewNode(
nodeTypename,
workflowId,
workspaceId,
nodeData,
reactFlowNodePosition
)
// Create Action in database
setNodes((prevNodes) =>
prevNodes
.map((n) => ({ ...n, selected: false }))
.concat({ ...newNode, selected: true })
)
} catch (error) {
console.error("An error occurred while creating a new node:", error)
toast({
title: "Failed to create new node",
description:
"Could not create new node. Please check the console logs for more information.",
})
}
}

const onNodesDelete = async <T,>(nodesToDelete: Node<T>[]) => {
if (!workflowId || !reactFlowInstance) {
return
Expand All @@ -410,11 +322,9 @@ export function WorkflowCanvas() {
setNodes((nds) =>
nds.filter((n) => !nodesToDelete.map((nd) => nd.id).includes(n.id))
)
await updateWorkflowGraphObject(
workspaceId,
workflowId,
reactFlowInstance
)
await updateWorkflow({
object: pruneGraphObject(reactFlowInstance),
})
console.log("Nodes deleted successfully")
} catch (error) {
console.error("An error occurred while deleting Action nodes:", error)
Expand Down Expand Up @@ -500,13 +410,13 @@ export function WorkflowCanvas() {
// Saving react flow instance state
useEffect(() => {
if (workflowId && reactFlowInstance) {
updateWorkflowGraphObject(workspaceId, workflowId, reactFlowInstance)
updateWorkflow({ object: pruneGraphObject(reactFlowInstance) })
}
}, [edges])

const onNodesDragStop = () => {
if (workflowId && reactFlowInstance) {
updateWorkflowGraphObject(workspaceId, workflowId, reactFlowInstance)
updateWorkflow({ object: pruneGraphObject(reactFlowInstance) })
}
}

Expand All @@ -520,7 +430,6 @@ export function WorkflowCanvas() {
onConnectEnd={onConnectEnd}
onPaneMouseMove={onPaneMouseMove}
onDragOver={onDragOver}
onDrop={onDrop}
onEdgesChange={onEdgesChange}
onEdgesDelete={onEdgesDelete}
onInit={setReactFlowInstance}
Expand Down
Loading

0 comments on commit 0f94710

Please sign in to comment.