@motiadev/workbench 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -0
- package/components.json +21 -0
- package/dist/.empty +0 -0
- package/dist/assets/index-DGmArPOa.css +1 -0
- package/dist/assets/index-hQsWtfVb.js +182 -0
- package/dist/index.html +20 -0
- package/eslint.config.js +28 -0
- package/index.html +19 -0
- package/index.tsx +10 -0
- package/middleware.ts +46 -0
- package/package.json +56 -0
- package/postcss.config.js +6 -0
- package/public/.empty +0 -0
- package/src/assets/.empty +0 -0
- package/src/components/app-sidebar.tsx +55 -0
- package/src/components/log-console.tsx +76 -0
- package/src/components/log-level-badge.tsx +12 -0
- package/src/components/ui/badge.tsx +31 -0
- package/src/components/ui/button.tsx +47 -0
- package/src/components/ui/collapsible.tsx +9 -0
- package/src/components/ui/dialog.tsx +120 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/label.tsx +26 -0
- package/src/components/ui/select.tsx +157 -0
- package/src/components/ui/separator.tsx +22 -0
- package/src/components/ui/sheet.tsx +106 -0
- package/src/components/ui/sidebar.tsx +637 -0
- package/src/components/ui/skeleton.tsx +7 -0
- package/src/components/ui/switch.tsx +27 -0
- package/src/components/ui/table.tsx +76 -0
- package/src/components/ui/textarea.tsx +22 -0
- package/src/components/ui/tooltip.tsx +32 -0
- package/src/hooks/use-list-flows.tsx +20 -0
- package/src/hooks/use-log-listener.tsx +32 -0
- package/src/hooks/use-mobile.tsx +19 -0
- package/src/index.css +190 -0
- package/src/lib/utils.ts +6 -0
- package/src/main.tsx +28 -0
- package/src/publicComponents/api-node.tsx +28 -0
- package/src/publicComponents/base-handle.tsx +43 -0
- package/src/publicComponents/base-node.tsx +57 -0
- package/src/publicComponents/emits.tsx +22 -0
- package/src/publicComponents/event-node.tsx +36 -0
- package/src/publicComponents/node-props.tsx +15 -0
- package/src/publicComponents/noop-node.tsx +21 -0
- package/src/publicComponents/subscribe.tsx +19 -0
- package/src/route-wrapper.tsx +9 -0
- package/src/routeTree.gen.ts +109 -0
- package/src/routes/__root.tsx +26 -0
- package/src/routes/flow/$id.tsx +21 -0
- package/src/routes/index.tsx +13 -0
- package/src/stores/use-logs.ts +22 -0
- package/src/views/flow/arrow-head.tsx +13 -0
- package/src/views/flow/base-edge.tsx +44 -0
- package/src/views/flow/flow-loader.tsx +3 -0
- package/src/views/flow/flow-view.tsx +72 -0
- package/src/views/flow/hooks/use-get-flow-state.tsx +109 -0
- package/src/views/flow/hooks/use-organize-nodes.ts +60 -0
- package/src/views/flow/legend.tsx +59 -0
- package/src/views/flow/node-organizer.tsx +70 -0
- package/src/views/flow/nodes/api-flow-node.tsx +6 -0
- package/src/views/flow/nodes/event-flow-node.tsx +6 -0
- package/src/views/flow/nodes/json-schema-form.tsx +110 -0
- package/src/views/flow/nodes/language-indicator.tsx +74 -0
- package/src/views/flow/nodes/nodes.types.ts +36 -0
- package/src/views/flow/nodes/noop-flow-node.tsx +6 -0
- package/src/vite-env.d.ts +1 -0
- package/tailwind.config.ts +75 -0
- package/tsconfig.app.json +30 -0
- package/tsconfig.json +13 -0
- package/tsconfig.node.json +22 -0
- package/tsconfig.node.tsbuildinfo +1 -0
- package/vite.config.ts +14 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { AppSidebar } from './components/app-sidebar'
|
|
2
|
+
import { SidebarProvider } from './components/ui/sidebar'
|
|
3
|
+
|
|
4
|
+
export const RouteWrapper = ({ children }: { children: React.ReactNode }) => (
|
|
5
|
+
<SidebarProvider>
|
|
6
|
+
<AppSidebar />
|
|
7
|
+
{children}
|
|
8
|
+
</SidebarProvider>
|
|
9
|
+
)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
|
|
3
|
+
// @ts-nocheck
|
|
4
|
+
|
|
5
|
+
// noinspection JSUnusedGlobalSymbols
|
|
6
|
+
|
|
7
|
+
// This file was automatically generated by TanStack Router.
|
|
8
|
+
// You should NOT make any changes in this file as it will be overwritten.
|
|
9
|
+
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
|
10
|
+
|
|
11
|
+
// Import Routes
|
|
12
|
+
|
|
13
|
+
import { Route as rootRoute } from './routes/__root'
|
|
14
|
+
import { Route as IndexImport } from './routes/index'
|
|
15
|
+
import { Route as FlowIdImport } from './routes/flow/$id'
|
|
16
|
+
|
|
17
|
+
// Create/Update Routes
|
|
18
|
+
|
|
19
|
+
const IndexRoute = IndexImport.update({
|
|
20
|
+
id: '/',
|
|
21
|
+
path: '/',
|
|
22
|
+
getParentRoute: () => rootRoute,
|
|
23
|
+
} as any)
|
|
24
|
+
|
|
25
|
+
const FlowIdRoute = FlowIdImport.update({
|
|
26
|
+
id: '/flow/$id',
|
|
27
|
+
path: '/flow/$id',
|
|
28
|
+
getParentRoute: () => rootRoute,
|
|
29
|
+
} as any)
|
|
30
|
+
|
|
31
|
+
// Populate the FileRoutesByPath interface
|
|
32
|
+
|
|
33
|
+
declare module '@tanstack/react-router' {
|
|
34
|
+
interface FileRoutesByPath {
|
|
35
|
+
'/': {
|
|
36
|
+
id: '/'
|
|
37
|
+
path: '/'
|
|
38
|
+
fullPath: '/'
|
|
39
|
+
preLoaderRoute: typeof IndexImport
|
|
40
|
+
parentRoute: typeof rootRoute
|
|
41
|
+
}
|
|
42
|
+
'/flow/$id': {
|
|
43
|
+
id: '/flow/$id'
|
|
44
|
+
path: '/flow/$id'
|
|
45
|
+
fullPath: '/flow/$id'
|
|
46
|
+
preLoaderRoute: typeof FlowIdImport
|
|
47
|
+
parentRoute: typeof rootRoute
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Create and export the route tree
|
|
53
|
+
|
|
54
|
+
export interface FileRoutesByFullPath {
|
|
55
|
+
'/': typeof IndexRoute
|
|
56
|
+
'/flow/$id': typeof FlowIdRoute
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface FileRoutesByTo {
|
|
60
|
+
'/': typeof IndexRoute
|
|
61
|
+
'/flow/$id': typeof FlowIdRoute
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface FileRoutesById {
|
|
65
|
+
__root__: typeof rootRoute
|
|
66
|
+
'/': typeof IndexRoute
|
|
67
|
+
'/flow/$id': typeof FlowIdRoute
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface FileRouteTypes {
|
|
71
|
+
fileRoutesByFullPath: FileRoutesByFullPath
|
|
72
|
+
fullPaths: '/' | '/flow/$id'
|
|
73
|
+
fileRoutesByTo: FileRoutesByTo
|
|
74
|
+
to: '/' | '/flow/$id'
|
|
75
|
+
id: '__root__' | '/' | '/flow/$id'
|
|
76
|
+
fileRoutesById: FileRoutesById
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface RootRouteChildren {
|
|
80
|
+
IndexRoute: typeof IndexRoute
|
|
81
|
+
FlowIdRoute: typeof FlowIdRoute
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const rootRouteChildren: RootRouteChildren = {
|
|
85
|
+
IndexRoute: IndexRoute,
|
|
86
|
+
FlowIdRoute: FlowIdRoute,
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export const routeTree = rootRoute._addFileChildren(rootRouteChildren)._addFileTypes<FileRouteTypes>()
|
|
90
|
+
|
|
91
|
+
/* ROUTE_MANIFEST_START
|
|
92
|
+
{
|
|
93
|
+
"routes": {
|
|
94
|
+
"__root__": {
|
|
95
|
+
"filePath": "__root.tsx",
|
|
96
|
+
"children": [
|
|
97
|
+
"/",
|
|
98
|
+
"/flow/$id"
|
|
99
|
+
]
|
|
100
|
+
},
|
|
101
|
+
"/": {
|
|
102
|
+
"filePath": "index.tsx"
|
|
103
|
+
},
|
|
104
|
+
"/flow/$id": {
|
|
105
|
+
"filePath": "flow/$id.tsx"
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
ROUTE_MANIFEST_END */
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { RouteWrapper } from '@/route-wrapper'
|
|
2
|
+
import { createRootRoute, Outlet } from '@tanstack/react-router'
|
|
3
|
+
import React, { Suspense } from 'react'
|
|
4
|
+
|
|
5
|
+
const TanStackRouterDevtools =
|
|
6
|
+
process.env.NODE_ENV === 'production'
|
|
7
|
+
? () => null // Render nothing in production
|
|
8
|
+
: React.lazy(() =>
|
|
9
|
+
// Lazy load in development
|
|
10
|
+
import('@tanstack/router-devtools').then((res) => ({
|
|
11
|
+
default: res.TanStackRouterDevtools,
|
|
12
|
+
// For Embedded Mode
|
|
13
|
+
// default: res.TanStackRouterDevtoolsPanel
|
|
14
|
+
})),
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
const RouteComponent = () => (
|
|
18
|
+
<RouteWrapper>
|
|
19
|
+
<Outlet />
|
|
20
|
+
<Suspense>
|
|
21
|
+
<TanStackRouterDevtools />
|
|
22
|
+
</Suspense>
|
|
23
|
+
</RouteWrapper>
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
export const Route = createRootRoute({ component: RouteComponent })
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { FlowView } from '@/views/flow/flow-view'
|
|
2
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
3
|
+
|
|
4
|
+
export const Route = createFileRoute('/flow/$id')({
|
|
5
|
+
component: Flow,
|
|
6
|
+
loader: async ({ params }) => {
|
|
7
|
+
return fetch(`/flows/${params.id}`)
|
|
8
|
+
.then((res) => res.json())
|
|
9
|
+
.then((flow) => ({ flow }))
|
|
10
|
+
},
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
function Flow() {
|
|
14
|
+
const { flow } = Route.useLoaderData()
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<div className="w-screen h-screen">
|
|
18
|
+
<FlowView flow={flow} />
|
|
19
|
+
</div>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
2
|
+
|
|
3
|
+
export const Route = createFileRoute('/')({
|
|
4
|
+
component: Index,
|
|
5
|
+
})
|
|
6
|
+
|
|
7
|
+
function Index() {
|
|
8
|
+
return (
|
|
9
|
+
<div className="flex items-center justify-center w-full h-screen">
|
|
10
|
+
<p className="text-gray-500">Select a flow</p>
|
|
11
|
+
</div>
|
|
12
|
+
)
|
|
13
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { create } from 'zustand'
|
|
2
|
+
|
|
3
|
+
export type Log = {
|
|
4
|
+
level: string
|
|
5
|
+
time: number
|
|
6
|
+
msg: string
|
|
7
|
+
traceId: string
|
|
8
|
+
flows: string[]
|
|
9
|
+
[key: string]: any
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type LogsState = {
|
|
13
|
+
logs: Log[]
|
|
14
|
+
addLog: (log: Log) => void
|
|
15
|
+
resetLogs: () => void
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const useLogs = create<LogsState>()((set) => ({
|
|
19
|
+
logs: [],
|
|
20
|
+
addLog: (log) => set((state) => ({ ...state, logs: [...state.logs, log] })),
|
|
21
|
+
resetLogs: () => set({ logs: [] }),
|
|
22
|
+
}))
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
type Props = {
|
|
4
|
+
color: string
|
|
5
|
+
id: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const ArrowHead: React.FC<Props> = (props) => (
|
|
9
|
+
<marker id={props.id} viewBox="-5 -5 10 10" markerUnits="strokeWidth" markerWidth="10" markerHeight="10">
|
|
10
|
+
<line x1={0} y1={0} x2={2} y2={-2} stroke={props.color} strokeWidth="1" strokeOpacity="1" strokeLinecap="round" />
|
|
11
|
+
<line x1={-2} y1={-2} x2={0} y2={0} stroke={props.color} strokeWidth="1" strokeOpacity="1" strokeLinecap="round" />
|
|
12
|
+
</marker>
|
|
13
|
+
)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BaseEdge as BaseReactFlowEdge,
|
|
3
|
+
EdgeProps,
|
|
4
|
+
getSmoothStepPath,
|
|
5
|
+
} from '@xyflow/react'
|
|
6
|
+
import React from 'react'
|
|
7
|
+
|
|
8
|
+
export const BaseEdge: React.FC<EdgeProps> = (props) => {
|
|
9
|
+
const {
|
|
10
|
+
sourceX,
|
|
11
|
+
sourceY,
|
|
12
|
+
targetX,
|
|
13
|
+
targetY,
|
|
14
|
+
sourcePosition,
|
|
15
|
+
targetPosition,
|
|
16
|
+
} = props
|
|
17
|
+
|
|
18
|
+
// getSmoothStepPath returns an array: [edgePath, labelX, labelY, etc.]
|
|
19
|
+
// We just need edgePath here.
|
|
20
|
+
const [edgePath] = getSmoothStepPath({
|
|
21
|
+
sourceX,
|
|
22
|
+
sourceY,
|
|
23
|
+
targetX,
|
|
24
|
+
targetY,
|
|
25
|
+
sourcePosition,
|
|
26
|
+
targetPosition,
|
|
27
|
+
borderRadius: 20, // <— Tweak this for roundness
|
|
28
|
+
offset: 10, // <— How far the line extends before curving
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<BaseReactFlowEdge
|
|
33
|
+
path={edgePath}
|
|
34
|
+
style={{
|
|
35
|
+
stroke: 'rgba(100, 100, 100)',
|
|
36
|
+
strokeWidth: 0.5,
|
|
37
|
+
shapeRendering: 'geometricPrecision',
|
|
38
|
+
fill: 'none',
|
|
39
|
+
mixBlendMode: 'screen', // or 'screen'
|
|
40
|
+
}}
|
|
41
|
+
className="edge-animated"
|
|
42
|
+
/>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { Background, BackgroundVariant, ReactFlow, Node } from '@xyflow/react'
|
|
2
|
+
import '@xyflow/react/dist/style.css'
|
|
3
|
+
import { BaseEdge } from './base-edge'
|
|
4
|
+
import { ArrowHead } from './arrow-head'
|
|
5
|
+
import { useGetFlowState, FlowResponse } from './hooks/use-get-flow-state'
|
|
6
|
+
import { useCallback, useEffect, useState } from 'react'
|
|
7
|
+
import { NodeOrganizer } from './node-organizer'
|
|
8
|
+
import { FlowLoader } from './flow-loader'
|
|
9
|
+
import { useLogListener } from '@/hooks/use-log-listener'
|
|
10
|
+
import { LogConsole } from '@/components/log-console'
|
|
11
|
+
import { Legend } from './legend'
|
|
12
|
+
|
|
13
|
+
const edgeTypes = {
|
|
14
|
+
base: BaseEdge,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type Props = {
|
|
18
|
+
flow: FlowResponse
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const FlowView: React.FC<Props> = ({ flow }) => {
|
|
22
|
+
const { nodes, edges, onNodesChange, onEdgesChange, nodeTypes } = useGetFlowState(flow)
|
|
23
|
+
const [initialized, setInitialized] = useState(false)
|
|
24
|
+
const [hoveredType, setHoveredType] = useState<string | null>(null)
|
|
25
|
+
|
|
26
|
+
useLogListener()
|
|
27
|
+
|
|
28
|
+
useEffect(() => setInitialized(false), [flow])
|
|
29
|
+
const onInitialized = useCallback(() => {
|
|
30
|
+
setTimeout(() => setInitialized(true), 10)
|
|
31
|
+
}, [])
|
|
32
|
+
|
|
33
|
+
const highlightClass = (nodeType?: string) => {
|
|
34
|
+
if (!hoveredType) return ''
|
|
35
|
+
return nodeType === hoveredType
|
|
36
|
+
? 'shadow-[0_0_15px_rgba(255,255,255,0.15)] border border-white/30 scale-[1.02] transition-all duration-300'
|
|
37
|
+
: 'opacity-30 transition-all duration-300'
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const nodesWithHighlights = nodes.map((node) => ({
|
|
41
|
+
...node,
|
|
42
|
+
className: highlightClass(node.data.type), // Access type from `data.type`
|
|
43
|
+
}))
|
|
44
|
+
|
|
45
|
+
if (!nodeTypes) {
|
|
46
|
+
return null
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<div className="w-full h-full relative bg-black">
|
|
51
|
+
{!initialized && <FlowLoader />}
|
|
52
|
+
<Legend onHover={setHoveredType} />
|
|
53
|
+
<ReactFlow
|
|
54
|
+
nodes={nodesWithHighlights}
|
|
55
|
+
edges={edges}
|
|
56
|
+
nodeTypes={nodeTypes}
|
|
57
|
+
edgeTypes={edgeTypes}
|
|
58
|
+
onNodesChange={onNodesChange}
|
|
59
|
+
onEdgesChange={onEdgesChange}
|
|
60
|
+
>
|
|
61
|
+
<Background variant={BackgroundVariant.Dots} gap={20} size={1} color="#444" />
|
|
62
|
+
<NodeOrganizer onInitialized={onInitialized} />
|
|
63
|
+
<svg>
|
|
64
|
+
<defs>
|
|
65
|
+
<ArrowHead color="#B3B3B3" id="arrowhead" />
|
|
66
|
+
</defs>
|
|
67
|
+
</svg>
|
|
68
|
+
</ReactFlow>
|
|
69
|
+
<LogConsole />
|
|
70
|
+
</div>
|
|
71
|
+
)
|
|
72
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { Edge, Node, useEdgesState, useNodesState } from '@xyflow/react'
|
|
2
|
+
import { useEffect, useState } from 'react'
|
|
3
|
+
import type { EdgeData, NodeData } from '../nodes/nodes.types'
|
|
4
|
+
import { ApiFlowNode } from '../nodes/api-flow-node'
|
|
5
|
+
import { NoopFlowNode } from '../nodes/noop-flow-node'
|
|
6
|
+
import { EventFlowNode } from '../nodes/event-flow-node'
|
|
7
|
+
|
|
8
|
+
type Emit = string | { type: string; label?: string; conditional?: boolean }
|
|
9
|
+
|
|
10
|
+
type FlowStep = {
|
|
11
|
+
id: string
|
|
12
|
+
name: string
|
|
13
|
+
type: 'event' | 'api' | 'noop'
|
|
14
|
+
description?: string
|
|
15
|
+
subscribes?: string[]
|
|
16
|
+
emits: Emit[]
|
|
17
|
+
action: 'webhook'
|
|
18
|
+
webhookUrl?: string
|
|
19
|
+
language?: string
|
|
20
|
+
nodeComponentPath?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type FlowResponse = {
|
|
24
|
+
id: string
|
|
25
|
+
name: string
|
|
26
|
+
steps: FlowStep[]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type FlowState = {
|
|
30
|
+
nodes: Node<NodeData>[]
|
|
31
|
+
edges: Edge<EdgeData>[]
|
|
32
|
+
nodeTypes: Record<string, React.ComponentType<any>>
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function importFlow(flow: FlowResponse): Promise<FlowState> {
|
|
36
|
+
const nodeTypes: Record<string, React.ComponentType<any>> = {
|
|
37
|
+
event: EventFlowNode,
|
|
38
|
+
api: ApiFlowNode,
|
|
39
|
+
noop: NoopFlowNode,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
for (const step of flow.steps) {
|
|
43
|
+
if (step.nodeComponentPath) {
|
|
44
|
+
const module = await import(/* @vite-ignore */ step.nodeComponentPath)
|
|
45
|
+
nodeTypes[step.nodeComponentPath] = module.default
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// we need to check all subscribes and emits to connect the nodes using edges
|
|
50
|
+
const nodes: Node<NodeData>[] = flow.steps.map((step) => ({
|
|
51
|
+
id: step.id,
|
|
52
|
+
type: step.nodeComponentPath ? step.nodeComponentPath : step.type,
|
|
53
|
+
position: { x: 0, y: 0 },
|
|
54
|
+
data: step,
|
|
55
|
+
language: step.language,
|
|
56
|
+
}))
|
|
57
|
+
|
|
58
|
+
const edges: Edge<EdgeData>[] = []
|
|
59
|
+
|
|
60
|
+
// For each node that emits events
|
|
61
|
+
flow.steps.forEach((sourceNode) => {
|
|
62
|
+
const emits = sourceNode.emits || []
|
|
63
|
+
|
|
64
|
+
// Check all other nodes that subscribe to those events
|
|
65
|
+
flow.steps.forEach((targetNode) => {
|
|
66
|
+
const subscribes = targetNode.subscribes || []
|
|
67
|
+
|
|
68
|
+
// For each matching emit->subscribe, create an edge
|
|
69
|
+
emits.forEach((emit) => {
|
|
70
|
+
const emitType = typeof emit === 'string' ? emit : emit.type
|
|
71
|
+
|
|
72
|
+
if (subscribes.includes(emitType)) {
|
|
73
|
+
const label = typeof emit !== 'string' ? emit.label : undefined
|
|
74
|
+
const variant = typeof emit !== 'string' && emit.conditional ? 'conditional' : 'default'
|
|
75
|
+
const data: EdgeData = { variant, label }
|
|
76
|
+
|
|
77
|
+
edges.push({
|
|
78
|
+
id: `${sourceNode.id}-${targetNode.id}`,
|
|
79
|
+
type: 'base',
|
|
80
|
+
source: sourceNode.id,
|
|
81
|
+
target: targetNode.id,
|
|
82
|
+
label,
|
|
83
|
+
data,
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
return { nodes, edges, nodeTypes }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export const useGetFlowState = (flow: FlowResponse) => {
|
|
94
|
+
const [nodeTypes, setNodeTypes] = useState<Record<string, React.ComponentType<any>>>()
|
|
95
|
+
const [nodes, setNodes, onNodesChange] = useNodesState<Node<NodeData>>([])
|
|
96
|
+
const [edges, setEdges, onEdgesChange] = useEdgesState<Edge<EdgeData>>([])
|
|
97
|
+
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
if (!flow) return
|
|
100
|
+
|
|
101
|
+
importFlow(flow).then(({ nodes, edges, nodeTypes }) => {
|
|
102
|
+
setNodes(nodes)
|
|
103
|
+
setEdges(edges)
|
|
104
|
+
setNodeTypes(nodeTypes)
|
|
105
|
+
})
|
|
106
|
+
}, [flow])
|
|
107
|
+
|
|
108
|
+
return { nodes, edges, onNodesChange, onEdgesChange, nodeTypes }
|
|
109
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Edge, Node } from '@xyflow/react'
|
|
2
|
+
import dagre from 'dagre'
|
|
3
|
+
import { useEffect, useRef } from 'react'
|
|
4
|
+
import { EventNodeData, EdgeData, ApiNodeData } from '../nodes/nodes.types'
|
|
5
|
+
|
|
6
|
+
const organizeNodes = (
|
|
7
|
+
nodes: Node<EventNodeData | ApiNodeData>[],
|
|
8
|
+
edges: Edge<EdgeData>[],
|
|
9
|
+
): Node<EventNodeData | ApiNodeData>[] => {
|
|
10
|
+
const dagreGraph = new dagre.graphlib.Graph({ compound: true })
|
|
11
|
+
dagreGraph.setDefaultEdgeLabel(() => ({}))
|
|
12
|
+
|
|
13
|
+
// Top-to-bottom layout
|
|
14
|
+
dagreGraph.setGraph({ rankdir: 'TB', ranksep: 80, nodesep: 60, edgesep: 20, ranker: 'tight-tree' })
|
|
15
|
+
|
|
16
|
+
nodes.forEach((node) => {
|
|
17
|
+
dagreGraph.setNode(node.id, { width: node.measured?.width, height: node.measured?.height })
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
edges.forEach((edge) => {
|
|
21
|
+
if (typeof edge.label === 'string') {
|
|
22
|
+
dagreGraph.setEdge(edge.source, edge.target, {
|
|
23
|
+
label: edge.label ?? '',
|
|
24
|
+
width: edge.label.length * 40, // Add width for the label
|
|
25
|
+
height: 30, // Add height for the label
|
|
26
|
+
labelpos: 'c', // Position label in center
|
|
27
|
+
})
|
|
28
|
+
} else {
|
|
29
|
+
dagreGraph.setEdge(edge.source, edge.target)
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
dagre.layout(dagreGraph)
|
|
34
|
+
|
|
35
|
+
return nodes.map((node) => {
|
|
36
|
+
const { x, y } = dagreGraph.node(node.id)
|
|
37
|
+
const position = {
|
|
38
|
+
x: x - (node.measured?.width ?? 0) / 2,
|
|
39
|
+
y: y - (node.measured?.height ?? 0) / 2,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return { ...node, position }
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const useOrganizeNodes = (
|
|
47
|
+
nodes: Node<EventNodeData | ApiNodeData>[],
|
|
48
|
+
edges: Edge<EdgeData>[],
|
|
49
|
+
setNodes: (nodes: Node<EventNodeData | ApiNodeData>[]) => void,
|
|
50
|
+
) => {
|
|
51
|
+
const organizedRef = useRef<boolean>(false)
|
|
52
|
+
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
if (!nodes.length || !edges.length || !nodes[0].measured || organizedRef.current) return
|
|
55
|
+
|
|
56
|
+
const layoutedNodes = organizeNodes(nodes, edges)
|
|
57
|
+
setNodes(layoutedNodes)
|
|
58
|
+
organizedRef.current = true
|
|
59
|
+
}, [nodes, edges, setNodes])
|
|
60
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export const Legend = ({ onHover }: { onHover: (type: string | null) => void }) => {
|
|
2
|
+
const legendItems = [
|
|
3
|
+
{
|
|
4
|
+
label: 'Event (Core)',
|
|
5
|
+
type: 'event',
|
|
6
|
+
bgColor: 'bg-green-950/40',
|
|
7
|
+
description: 'Core logic components that process events',
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
label: 'API',
|
|
11
|
+
type: 'api',
|
|
12
|
+
bgColor: 'bg-blue-950/40',
|
|
13
|
+
description: 'HTTP endpoints that trigger flows',
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
label: 'Noop (Non-Operation)',
|
|
17
|
+
type: 'noop',
|
|
18
|
+
bgColor: 'bg-zinc-950/40',
|
|
19
|
+
description: 'Placeholder nodes for external processes',
|
|
20
|
+
},
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
const renderSwatch = (bgColor: string) => (
|
|
24
|
+
<div className="relative group">
|
|
25
|
+
{/* Border gradient container */}
|
|
26
|
+
<div className="absolute -inset-[1px] rounded bg-gradient-to-r from-white/20 to-white/10" />
|
|
27
|
+
|
|
28
|
+
{/* Main swatch */}
|
|
29
|
+
<div className={`relative ${bgColor} w-8 h-8 rounded border border-white/10`} />
|
|
30
|
+
|
|
31
|
+
{/* Stacked effect */}
|
|
32
|
+
<div className="absolute inset-0 -z-10 translate-y-0.5 translate-x-0.5 bg-black/20 rounded border border-white/5" />
|
|
33
|
+
</div>
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<div className="absolute right-4 top-4 font-mono rounded-lg border border-white/20 p-4 z-10 shadow-xl">
|
|
38
|
+
<div className="text-sm text-white mb-3 font-semibold">Flow Legend</div>
|
|
39
|
+
<div className="flex flex-col gap-3">
|
|
40
|
+
{legendItems.map((item) => (
|
|
41
|
+
<div
|
|
42
|
+
key={item.type}
|
|
43
|
+
onMouseEnter={() => onHover(item.type)}
|
|
44
|
+
onMouseLeave={() => onHover(null)}
|
|
45
|
+
className="group cursor-pointer transition-all hover:bg-white/5 rounded p-1 -mx-1"
|
|
46
|
+
>
|
|
47
|
+
<div className="flex items-start gap-3">
|
|
48
|
+
{renderSwatch(item.bgColor)}
|
|
49
|
+
<div className="flex-1">
|
|
50
|
+
<div className="text-white text-xs font-medium">{item.label}</div>
|
|
51
|
+
<div className="text-white/60 text-xs mt-0.5">{item.description}</div>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
))}
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { Edge, Node, useNodesInitialized, useReactFlow } from '@xyflow/react'
|
|
2
|
+
import dagre from 'dagre'
|
|
3
|
+
import { useEffect } from 'react'
|
|
4
|
+
import { EventNodeData, EdgeData, ApiNodeData } from './nodes/nodes.types'
|
|
5
|
+
|
|
6
|
+
const organizeNodes = (
|
|
7
|
+
nodes: Node<EventNodeData | ApiNodeData>[],
|
|
8
|
+
edges: Edge<EdgeData>[],
|
|
9
|
+
): Node<EventNodeData | ApiNodeData>[] => {
|
|
10
|
+
const dagreGraph = new dagre.graphlib.Graph({ compound: true })
|
|
11
|
+
dagreGraph.setDefaultEdgeLabel(() => ({}))
|
|
12
|
+
|
|
13
|
+
// Top-to-bottom layout
|
|
14
|
+
dagreGraph.setGraph({ rankdir: 'TB', ranksep: 80, nodesep: 60, edgesep: 20, ranker: 'tight-tree' })
|
|
15
|
+
|
|
16
|
+
nodes.forEach((node) => {
|
|
17
|
+
dagreGraph.setNode(node.id, { width: node.measured?.width, height: node.measured?.height })
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
edges.forEach((edge) => {
|
|
21
|
+
if (typeof edge.label === 'string') {
|
|
22
|
+
dagreGraph.setEdge(edge.source, edge.target, {
|
|
23
|
+
label: edge.label ?? '',
|
|
24
|
+
width: edge.label.length * 40, // Add width for the label
|
|
25
|
+
height: 30, // Add height for the label
|
|
26
|
+
labelpos: 'c', // Position label in center
|
|
27
|
+
})
|
|
28
|
+
} else {
|
|
29
|
+
dagreGraph.setEdge(edge.source, edge.target)
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
dagre.layout(dagreGraph)
|
|
34
|
+
|
|
35
|
+
return nodes.map((node) => {
|
|
36
|
+
const { x, y } = dagreGraph.node(node.id)
|
|
37
|
+
const position = {
|
|
38
|
+
x: x - (node.measured?.width ?? 0) / 2,
|
|
39
|
+
y: y - (node.measured?.height ?? 0) / 2,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return { ...node, position }
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
type Props = {
|
|
47
|
+
onInitialized: () => void
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const NodeOrganizer: React.FC<Props> = ({ onInitialized }) => {
|
|
51
|
+
const { setNodes, getNodes, getEdges, fitView } = useReactFlow()
|
|
52
|
+
const nodesInitialized = useNodesInitialized()
|
|
53
|
+
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
if (nodesInitialized) {
|
|
56
|
+
const nodes = getNodes() as Node<EventNodeData | ApiNodeData>[]
|
|
57
|
+
const edges = getEdges() as Edge<EdgeData>[]
|
|
58
|
+
const organizedNodes = organizeNodes(nodes, edges)
|
|
59
|
+
|
|
60
|
+
setNodes(organizedNodes)
|
|
61
|
+
onInitialized()
|
|
62
|
+
|
|
63
|
+
setTimeout(async () => {
|
|
64
|
+
await fitView()
|
|
65
|
+
}, 1)
|
|
66
|
+
}
|
|
67
|
+
}, [nodesInitialized, onInitialized])
|
|
68
|
+
|
|
69
|
+
return null
|
|
70
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { EventNode } from '../../../publicComponents/event-node'
|
|
2
|
+
import { EventNodeProps } from '../../../publicComponents/node-props'
|
|
3
|
+
|
|
4
|
+
export const EventFlowNode = ({ data }: EventNodeProps) => {
|
|
5
|
+
return <EventNode className="relative" data={data}></EventNode>
|
|
6
|
+
}
|