@motiadev/workbench 0.14.0-beta.165-285707 → 0.15.0-beta.165

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.
Files changed (236) hide show
  1. package/dist/index.d.ts +189 -10
  2. package/dist/index.html +1 -1
  3. package/dist/index.js +1065 -7
  4. package/dist/middleware.d.ts +66 -8
  5. package/dist/middleware.js +694 -86
  6. package/dist/motia-plugin/__tests__/generator.test.ts +129 -0
  7. package/dist/motia-plugin/__tests__/resolver.test.ts +82 -0
  8. package/dist/motia-plugin/__tests__/validator.test.ts +71 -0
  9. package/dist/motia-plugin/{generator.js → generator.ts} +37 -35
  10. package/dist/motia-plugin/hmr.ts +123 -0
  11. package/dist/motia-plugin/index.ts +183 -0
  12. package/dist/motia-plugin/{resolver.d.ts → resolver.ts} +38 -5
  13. package/dist/motia-plugin/types.ts +198 -0
  14. package/dist/motia-plugin/{utils.d.ts → utils.ts} +17 -4
  15. package/dist/motia-plugin/validator.ts +197 -0
  16. package/dist/src/App.tsx +41 -0
  17. package/dist/src/components/NotFoundPage.tsx +11 -0
  18. package/dist/src/components/bottom-panel.tsx +39 -0
  19. package/dist/src/components/flow/base-edge.tsx +61 -0
  20. package/dist/src/components/flow/flow-loader.tsx +3 -0
  21. package/dist/src/components/flow/flow-page.tsx +75 -0
  22. package/dist/src/components/flow/flow-tab-menu-item.tsx +52 -0
  23. package/dist/src/components/flow/flow-view.tsx +66 -0
  24. package/dist/src/components/flow/hooks/use-get-flow-state.tsx +171 -0
  25. package/dist/src/components/flow/hooks/use-save-workflow-config.ts +25 -0
  26. package/dist/src/components/flow/node-organizer.tsx +103 -0
  27. package/dist/src/components/flow/nodes/api-flow-node.tsx +6 -0
  28. package/dist/src/components/flow/nodes/cron-flow-node.tsx +6 -0
  29. package/dist/src/components/flow/nodes/event-flow-node.tsx +6 -0
  30. package/dist/src/components/flow/nodes/noop-flow-node.tsx +6 -0
  31. package/dist/src/components/header/deploy-button.tsx +110 -0
  32. package/dist/src/components/header/header.tsx +39 -0
  33. package/dist/src/components/root-motia.tsx +10 -0
  34. package/dist/src/components/top-panel.tsx +40 -0
  35. package/dist/src/components/tutorial/engine/tutorial-engine.ts +26 -0
  36. package/dist/src/components/tutorial/engine/tutorial-types.ts +26 -0
  37. package/dist/src/components/tutorial/engine/workbench-xpath.ts +53 -0
  38. package/dist/src/components/tutorial/hooks/tutorial-utils.ts +26 -0
  39. package/dist/src/components/tutorial/hooks/use-tutorial-engine.ts +213 -0
  40. package/dist/src/components/tutorial/hooks/use-tutorial.ts +14 -0
  41. package/dist/src/components/tutorial/tutorial-button.tsx +46 -0
  42. package/dist/src/components/tutorial/tutorial-step.tsx +82 -0
  43. package/dist/src/components/tutorial/tutorial.tsx +59 -0
  44. package/dist/src/components/ui/json-editor.tsx +68 -0
  45. package/dist/src/components/ui/table.tsx +75 -0
  46. package/dist/src/components/ui/theme-toggle.tsx +54 -0
  47. package/dist/src/components/ui/tooltip.tsx +26 -0
  48. package/dist/src/hooks/use-debounced.ts +22 -0
  49. package/dist/src/hooks/use-fetch-flows.ts +33 -0
  50. package/dist/src/hooks/use-mobile.ts +19 -0
  51. package/dist/src/hooks/use-update-handle-positions.ts +42 -0
  52. package/dist/src/index.css +5 -5
  53. package/dist/src/lib/__tests__/utils.test.ts +110 -0
  54. package/dist/src/lib/motia-analytics.ts +140 -0
  55. package/dist/src/lib/plugins.tsx +132 -0
  56. package/dist/src/lib/utils.ts +37 -0
  57. package/dist/src/main.tsx +30 -0
  58. package/dist/src/project-view-mode.tsx +32 -0
  59. package/dist/src/publicComponents/api-node.tsx +26 -0
  60. package/dist/src/publicComponents/base-node/base-handle.tsx +50 -0
  61. package/dist/src/publicComponents/base-node/base-node.tsx +114 -0
  62. package/dist/src/publicComponents/base-node/code-display.tsx +119 -0
  63. package/dist/src/publicComponents/base-node/emits.tsx +17 -0
  64. package/dist/src/publicComponents/base-node/feature-card.tsx +32 -0
  65. package/dist/src/publicComponents/base-node/language-indicator.tsx +131 -0
  66. package/dist/src/publicComponents/base-node/node-header.tsx +49 -0
  67. package/dist/src/publicComponents/base-node/node-sidebar.tsx +41 -0
  68. package/dist/src/publicComponents/base-node/subscribe.tsx +13 -0
  69. package/dist/src/publicComponents/cron-node.tsx +24 -0
  70. package/dist/src/publicComponents/event-node.tsx +20 -0
  71. package/dist/src/publicComponents/node-props.tsx +15 -0
  72. package/dist/src/publicComponents/noop-node.tsx +19 -0
  73. package/dist/src/setupTests.ts +1 -0
  74. package/dist/src/stores/use-app-tabs-store.ts +49 -0
  75. package/dist/src/stores/use-flow-store.ts +31 -0
  76. package/dist/src/stores/use-global-store.ts +24 -0
  77. package/dist/src/stores/use-motia-config-store.ts +36 -0
  78. package/dist/src/stores/use-tabs-store.ts +34 -0
  79. package/dist/src/system-view-mode.tsx +28 -0
  80. package/dist/src/types/endpoint.ts +12 -0
  81. package/dist/src/types/file.ts +7 -0
  82. package/dist/src/types/flow.ts +103 -0
  83. package/eslint.config.cjs +22 -0
  84. package/jest.config.cjs +68 -0
  85. package/package.json +53 -51
  86. package/dist/motia-plugin/__tests__/generator.test.d.ts +0 -1
  87. package/dist/motia-plugin/__tests__/generator.test.js +0 -97
  88. package/dist/motia-plugin/__tests__/resolver.test.d.ts +0 -1
  89. package/dist/motia-plugin/__tests__/resolver.test.js +0 -64
  90. package/dist/motia-plugin/__tests__/validator.test.d.ts +0 -1
  91. package/dist/motia-plugin/__tests__/validator.test.js +0 -59
  92. package/dist/motia-plugin/generator.d.ts +0 -78
  93. package/dist/motia-plugin/hmr.d.ts +0 -22
  94. package/dist/motia-plugin/hmr.js +0 -100
  95. package/dist/motia-plugin/index.d.ts +0 -3
  96. package/dist/motia-plugin/index.js +0 -153
  97. package/dist/motia-plugin/resolver.js +0 -92
  98. package/dist/motia-plugin/types.d.ts +0 -169
  99. package/dist/motia-plugin/types.js +0 -36
  100. package/dist/motia-plugin/utils.js +0 -75
  101. package/dist/motia-plugin/validator.d.ts +0 -19
  102. package/dist/motia-plugin/validator.js +0 -163
  103. package/dist/src/App.d.ts +0 -2
  104. package/dist/src/App.js +0 -35
  105. package/dist/src/components/NotFoundPage.d.ts +0 -1
  106. package/dist/src/components/NotFoundPage.js +0 -3
  107. package/dist/src/components/bottom-panel.d.ts +0 -1
  108. package/dist/src/components/bottom-panel.js +0 -15
  109. package/dist/src/components/flow/base-edge.d.ts +0 -3
  110. package/dist/src/components/flow/base-edge.js +0 -39
  111. package/dist/src/components/flow/flow-loader.d.ts +0 -1
  112. package/dist/src/components/flow/flow-loader.js +0 -4
  113. package/dist/src/components/flow/flow-page.d.ts +0 -1
  114. package/dist/src/components/flow/flow-page.js +0 -25
  115. package/dist/src/components/flow/flow-tab-menu-item.d.ts +0 -1
  116. package/dist/src/components/flow/flow-tab-menu-item.js +0 -18
  117. package/dist/src/components/flow/flow-view.d.ts +0 -12
  118. package/dist/src/components/flow/flow-view.js +0 -22
  119. package/dist/src/components/flow/hooks/use-get-flow-state.d.ts +0 -10
  120. package/dist/src/components/flow/hooks/use-get-flow-state.js +0 -133
  121. package/dist/src/components/flow/hooks/use-save-workflow-config.d.ts +0 -2
  122. package/dist/src/components/flow/hooks/use-save-workflow-config.js +0 -22
  123. package/dist/src/components/flow/node-organizer.d.ts +0 -10
  124. package/dist/src/components/flow/node-organizer.js +0 -82
  125. package/dist/src/components/flow/nodes/api-flow-node.d.ts +0 -2
  126. package/dist/src/components/flow/nodes/api-flow-node.js +0 -5
  127. package/dist/src/components/flow/nodes/cron-flow-node.d.ts +0 -2
  128. package/dist/src/components/flow/nodes/cron-flow-node.js +0 -5
  129. package/dist/src/components/flow/nodes/event-flow-node.d.ts +0 -2
  130. package/dist/src/components/flow/nodes/event-flow-node.js +0 -5
  131. package/dist/src/components/flow/nodes/noop-flow-node.d.ts +0 -2
  132. package/dist/src/components/flow/nodes/noop-flow-node.js +0 -5
  133. package/dist/src/components/header/deploy-button.d.ts +0 -1
  134. package/dist/src/components/header/deploy-button.js +0 -28
  135. package/dist/src/components/header/header.d.ts +0 -2
  136. package/dist/src/components/header/header.js +0 -23
  137. package/dist/src/components/root-motia.d.ts +0 -2
  138. package/dist/src/components/root-motia.js +0 -7
  139. package/dist/src/components/top-panel.d.ts +0 -1
  140. package/dist/src/components/top-panel.js +0 -15
  141. package/dist/src/components/tutorial/engine/tutorial-engine.d.ts +0 -12
  142. package/dist/src/components/tutorial/engine/tutorial-engine.js +0 -36
  143. package/dist/src/components/tutorial/engine/tutorial-types.d.ts +0 -22
  144. package/dist/src/components/tutorial/engine/tutorial-types.js +0 -1
  145. package/dist/src/components/tutorial/engine/workbench-xpath.d.ts +0 -45
  146. package/dist/src/components/tutorial/engine/workbench-xpath.js +0 -45
  147. package/dist/src/components/tutorial/hooks/tutorial-utils.d.ts +0 -1
  148. package/dist/src/components/tutorial/hooks/tutorial-utils.js +0 -17
  149. package/dist/src/components/tutorial/hooks/use-tutorial-engine.d.ts +0 -15
  150. package/dist/src/components/tutorial/hooks/use-tutorial-engine.js +0 -183
  151. package/dist/src/components/tutorial/hooks/use-tutorial.d.ts +0 -5
  152. package/dist/src/components/tutorial/hooks/use-tutorial.js +0 -10
  153. package/dist/src/components/tutorial/tutorial-button.d.ts +0 -2
  154. package/dist/src/components/tutorial/tutorial-button.js +0 -21
  155. package/dist/src/components/tutorial/tutorial-step.d.ts +0 -14
  156. package/dist/src/components/tutorial/tutorial-step.js +0 -19
  157. package/dist/src/components/tutorial/tutorial.d.ts +0 -2
  158. package/dist/src/components/tutorial/tutorial.js +0 -32
  159. package/dist/src/components/ui/json-editor.d.ts +0 -12
  160. package/dist/src/components/ui/json-editor.js +0 -35
  161. package/dist/src/components/ui/table.d.ts +0 -10
  162. package/dist/src/components/ui/table.js +0 -20
  163. package/dist/src/components/ui/theme-toggle.d.ts +0 -2
  164. package/dist/src/components/ui/theme-toggle.js +0 -19
  165. package/dist/src/components/ui/tooltip.d.ts +0 -6
  166. package/dist/src/components/ui/tooltip.js +0 -3
  167. package/dist/src/hooks/use-debounced.d.ts +0 -1
  168. package/dist/src/hooks/use-debounced.js +0 -18
  169. package/dist/src/hooks/use-fetch-flows.d.ts +0 -1
  170. package/dist/src/hooks/use-fetch-flows.js +0 -26
  171. package/dist/src/hooks/use-mobile.d.ts +0 -1
  172. package/dist/src/hooks/use-mobile.js +0 -15
  173. package/dist/src/hooks/use-update-handle-positions.d.ts +0 -10
  174. package/dist/src/hooks/use-update-handle-positions.js +0 -35
  175. package/dist/src/lib/__tests__/utils.test.d.ts +0 -1
  176. package/dist/src/lib/__tests__/utils.test.js +0 -94
  177. package/dist/src/lib/motia-analytics.d.ts +0 -38
  178. package/dist/src/lib/motia-analytics.js +0 -132
  179. package/dist/src/lib/plugins.d.ts +0 -2
  180. package/dist/src/lib/plugins.js +0 -105
  181. package/dist/src/lib/utils.d.ts +0 -7
  182. package/dist/src/lib/utils.js +0 -34
  183. package/dist/src/main.d.ts +0 -2
  184. package/dist/src/main.js +0 -17
  185. package/dist/src/project-view-mode.d.ts +0 -1
  186. package/dist/src/project-view-mode.js +0 -20
  187. package/dist/src/publicComponents/api-node.d.ts +0 -5
  188. package/dist/src/publicComponents/api-node.js +0 -5
  189. package/dist/src/publicComponents/base-node/base-handle.d.ts +0 -9
  190. package/dist/src/publicComponents/base-node/base-handle.js +0 -8
  191. package/dist/src/publicComponents/base-node/base-node.d.ts +0 -15
  192. package/dist/src/publicComponents/base-node/base-node.js +0 -30
  193. package/dist/src/publicComponents/base-node/code-display.d.ts +0 -9
  194. package/dist/src/publicComponents/base-node/code-display.js +0 -64
  195. package/dist/src/publicComponents/base-node/emits.d.ts +0 -5
  196. package/dist/src/publicComponents/base-node/emits.js +0 -5
  197. package/dist/src/publicComponents/base-node/feature-card.d.ts +0 -10
  198. package/dist/src/publicComponents/base-node/feature-card.js +0 -5
  199. package/dist/src/publicComponents/base-node/language-indicator.d.ts +0 -10
  200. package/dist/src/publicComponents/base-node/language-indicator.js +0 -29
  201. package/dist/src/publicComponents/base-node/node-header.d.ts +0 -13
  202. package/dist/src/publicComponents/base-node/node-header.js +0 -30
  203. package/dist/src/publicComponents/base-node/node-sidebar.d.ts +0 -14
  204. package/dist/src/publicComponents/base-node/node-sidebar.js +0 -9
  205. package/dist/src/publicComponents/base-node/subscribe.d.ts +0 -4
  206. package/dist/src/publicComponents/base-node/subscribe.js +0 -4
  207. package/dist/src/publicComponents/cron-node.d.ts +0 -4
  208. package/dist/src/publicComponents/cron-node.js +0 -6
  209. package/dist/src/publicComponents/event-node.d.ts +0 -4
  210. package/dist/src/publicComponents/event-node.js +0 -5
  211. package/dist/src/publicComponents/node-props.d.ts +0 -21
  212. package/dist/src/publicComponents/node-props.js +0 -1
  213. package/dist/src/publicComponents/noop-node.d.ts +0 -4
  214. package/dist/src/publicComponents/noop-node.js +0 -5
  215. package/dist/src/setupTests.d.ts +0 -1
  216. package/dist/src/setupTests.js +0 -1
  217. package/dist/src/stores/use-app-tabs-store.d.ts +0 -16
  218. package/dist/src/stores/use-app-tabs-store.js +0 -31
  219. package/dist/src/stores/use-flow-store.d.ts +0 -21
  220. package/dist/src/stores/use-flow-store.js +0 -16
  221. package/dist/src/stores/use-global-store.d.ts +0 -18
  222. package/dist/src/stores/use-global-store.js +0 -12
  223. package/dist/src/stores/use-motia-config-store.d.ts +0 -12
  224. package/dist/src/stores/use-motia-config-store.js +0 -24
  225. package/dist/src/stores/use-tabs-store.d.ts +0 -19
  226. package/dist/src/stores/use-tabs-store.js +0 -22
  227. package/dist/src/system-view-mode.d.ts +0 -1
  228. package/dist/src/system-view-mode.js +0 -10
  229. package/dist/src/types/endpoint.d.ts +0 -14
  230. package/dist/src/types/endpoint.js +0 -1
  231. package/dist/src/types/file.d.ts +0 -7
  232. package/dist/src/types/file.js +0 -1
  233. package/dist/src/types/flow.d.ts +0 -115
  234. package/dist/src/types/flow.js +0 -1
  235. package/dist/tsconfig.app.tsbuildinfo +0 -1
  236. package/dist/tsconfig.node.tsbuildinfo +0 -1
@@ -0,0 +1,41 @@
1
+ import { type FC, memo, useEffect, useMemo } from 'react'
2
+ import { FlowPage } from './components/flow/flow-page'
3
+ import { FlowTabMenuItem } from './components/flow/flow-tab-menu-item'
4
+ import { registerPluginTabs } from './lib/plugins'
5
+ import { getViewModeFromURL, type ViewMode } from './lib/utils'
6
+ import { ProjectViewMode } from './project-view-mode'
7
+ import { type AppTab, TabLocation, useAppTabsStore } from './stores/use-app-tabs-store'
8
+ import { SystemViewMode } from './system-view-mode'
9
+
10
+ const TAB_IDS = {
11
+ FLOW: 'flow',
12
+ LOGS: 'logs',
13
+ } as const
14
+
15
+ const topTabs: AppTab[] = [
16
+ {
17
+ id: TAB_IDS.FLOW,
18
+ tabLabel: FlowTabMenuItem,
19
+ content: FlowPage,
20
+ },
21
+ ]
22
+
23
+ export const App: FC = memo(() => {
24
+ const setTabs = useAppTabsStore((state) => state.setTabs)
25
+ const addTab = useAppTabsStore((state) => state.addTab)
26
+
27
+ useEffect(() => {
28
+ const timeout = setTimeout(() => {
29
+ setTabs(TabLocation.TOP, topTabs)
30
+ registerPluginTabs(addTab)
31
+ }, 10)
32
+ return () => clearTimeout(timeout)
33
+ }, [setTabs, addTab])
34
+
35
+ const viewMode = useMemo<ViewMode>(getViewModeFromURL, [])
36
+
37
+ const ViewComponent = viewMode === 'project' ? ProjectViewMode : SystemViewMode
38
+
39
+ return <ViewComponent />
40
+ })
41
+ App.displayName = 'App'
@@ -0,0 +1,11 @@
1
+ import { Header } from './header/header'
2
+
3
+ export const NotFoundPage = () => (
4
+ <div className="grid grid-rows-[auto_1fr] h-screen bg-background text-foreground">
5
+ <Header />
6
+ <div className="flex flex-col items-center justify-center">
7
+ <h1 className="text-4xl font-bold mb-4">404 – Page Not Found</h1>
8
+ <p className="text-lg opacity-80 mb-6">This route doesn’t exist.</p>
9
+ </div>
10
+ </div>
11
+ )
@@ -0,0 +1,39 @@
1
+ import { CollapsiblePanel, TabsContent, TabsList, TabsTrigger } from '@motiadev/ui'
2
+ import { memo } from 'react'
3
+ import { useShallow } from 'zustand/react/shallow'
4
+ import { type AppTabsState, TabLocation, useAppTabsStore } from '../stores/use-app-tabs-store'
5
+ import { useTabsStore } from '../stores/use-tabs-store'
6
+
7
+ const bottomTabsSelector = (state: AppTabsState) => state.tabs[TabLocation.BOTTOM]
8
+ const bottomPanelId = 'bottom-panel'
9
+
10
+ export const BottomPanel = memo(() => {
11
+ const defaultTab = useTabsStore((state) => state.tab.bottom)
12
+ const setBottomTab = useTabsStore((state) => state.setBottomTab)
13
+ const tabs = useAppTabsStore(useShallow(bottomTabsSelector))
14
+
15
+ return (
16
+ <CollapsiblePanel
17
+ id={bottomPanelId}
18
+ variant={'tabs'}
19
+ defaultTab={defaultTab}
20
+ onTabChange={setBottomTab}
21
+ header={
22
+ <TabsList>
23
+ {tabs.map(({ id, tabLabel: Label }) => (
24
+ <TabsTrigger key={id} value={id} data-testid={`${id.toLowerCase()}-link`} className="cursor-pointer">
25
+ <Label />
26
+ </TabsTrigger>
27
+ ))}
28
+ </TabsList>
29
+ }
30
+ >
31
+ {tabs.map(({ id, content: Element }) => (
32
+ <TabsContent key={id} value={id} className="h-full">
33
+ <Element />
34
+ </TabsContent>
35
+ ))}
36
+ </CollapsiblePanel>
37
+ )
38
+ })
39
+ BottomPanel.displayName = 'BottomPanel'
@@ -0,0 +1,61 @@
1
+ import { cn, useThemeStore } from '@motiadev/ui'
2
+ import { BaseEdge as BaseReactFlowEdge, EdgeLabelRenderer, type EdgeProps, getSmoothStepPath } from '@xyflow/react'
3
+ import { cva } from 'class-variance-authority'
4
+ import type React from 'react'
5
+
6
+ const labelVariants = cva('absolute pointer-events-all text-cs border p-1 px-2', {
7
+ variants: {
8
+ color: {
9
+ default: 'border-[#b3b3b3] bg-[#060014] text-gray-100 font-semibold border-solid rounded-full',
10
+ conditional: 'bg-amber-300 border-amber-950 text-amber-950 border-solid font-semibold italic rounded-lg',
11
+ },
12
+ },
13
+ defaultVariants: {
14
+ color: 'default',
15
+ },
16
+ })
17
+
18
+ export const BaseEdge: React.FC<EdgeProps> = (props: EdgeProps) => {
19
+ const theme = useThemeStore((state) => state.theme)
20
+ const { sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, data } = props
21
+ const label = data?.label as string | undefined
22
+ const labelVariant = data?.labelVariant as 'default' | 'conditional' | null | undefined
23
+ const virtualColor = theme === 'dark' ? 'rgb(225, 225, 225)' : 'rgb(85, 85, 85)'
24
+
25
+ const [edgePath, labelX, labelY] = getSmoothStepPath({
26
+ sourceX,
27
+ sourceY,
28
+ targetX,
29
+ targetY,
30
+ sourcePosition,
31
+ targetPosition,
32
+ borderRadius: 20,
33
+ offset: 10,
34
+ })
35
+
36
+ return (
37
+ <>
38
+ <BaseReactFlowEdge
39
+ path={edgePath}
40
+ style={{
41
+ stroke: data?.variant === 'virtual' ? virtualColor : '#0094FF',
42
+ strokeWidth: 2,
43
+ shapeRendering: 'geometricPrecision',
44
+ fill: 'none',
45
+ mixBlendMode: 'screen',
46
+ }}
47
+ className="edge-animated"
48
+ />
49
+ {label && (
50
+ <EdgeLabelRenderer>
51
+ <div
52
+ className={cn(labelVariants({ color: labelVariant }))}
53
+ style={{ transform: `translateX(-50%) translateY(-50%) translate(${labelX}px, ${labelY}px)` }}
54
+ >
55
+ <div className="text-xs font-mono">{label}</div>
56
+ </div>
57
+ </EdgeLabelRenderer>
58
+ )}
59
+ </>
60
+ )
61
+ }
@@ -0,0 +1,3 @@
1
+ export const FlowLoader = () => {
2
+ return <div className="absolute z-10 inset-0 w-full h-full bg-background" />
3
+ }
@@ -0,0 +1,75 @@
1
+ import { useStreamItem } from '@motiadev/stream-client-react'
2
+ import { Button, Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from '@motiadev/ui'
3
+ import { ReactFlowProvider } from '@xyflow/react'
4
+ import { ExternalLink, Workflow } from 'lucide-react'
5
+ import { memo, useMemo } from 'react'
6
+ import { useShallow } from 'zustand/react/shallow'
7
+ import { motiaAnalytics } from '../../lib/motia-analytics'
8
+ import { useFlowStore } from '../../stores/use-flow-store'
9
+ import type { FlowConfigResponse, FlowResponse } from '../../types/flow'
10
+ import { FlowView } from './flow-view'
11
+
12
+ export const FlowPage = memo(() => {
13
+ const selectedFlowId = useFlowStore((state) => state.selectedFlowId)
14
+ const flows = useFlowStore(useShallow((state) => Object.values(state.flows)))
15
+
16
+ const streamItemArgs = useMemo(
17
+ () => ({ streamName: '__motia.flows', groupId: 'default', id: selectedFlowId ?? '' }),
18
+ [selectedFlowId],
19
+ )
20
+ const { data: flow } = useStreamItem<FlowResponse>(streamItemArgs)
21
+
22
+ const streamItemArgsConfig = useMemo(
23
+ () => ({ streamName: '__motia.flowsConfig', groupId: 'default', id: selectedFlowId ?? '' }),
24
+ [selectedFlowId],
25
+ )
26
+ const { data: flowConfig } = useStreamItem<FlowConfigResponse>(streamItemArgsConfig)
27
+
28
+ if (flows.length === 0 || flow?.error) {
29
+ return (
30
+ <div className="flex w-full h-full bg-background">
31
+ <Empty>
32
+ <EmptyHeader>
33
+ <EmptyMedia variant="icon">
34
+ <Workflow />
35
+ </EmptyMedia>
36
+ {flow?.error ? (
37
+ <>
38
+ <EmptyTitle>Error loading flow</EmptyTitle>
39
+ <EmptyDescription>{flow.error}</EmptyDescription>
40
+ </>
41
+ ) : (
42
+ <>
43
+ <EmptyTitle>No flows registered</EmptyTitle>
44
+ <EmptyDescription>
45
+ You haven't registered any flows yet. Get started by registering your first flow.
46
+ </EmptyDescription>
47
+ </>
48
+ )}
49
+ </EmptyHeader>
50
+ <EmptyContent>
51
+ <Button variant="link" asChild size="sm">
52
+ <a
53
+ href="https://www.motia.dev/docs/development-guide/flows"
54
+ target="_blank"
55
+ rel="noopener noreferrer"
56
+ onClick={() => motiaAnalytics.track('flows_docs_link_clicked')}
57
+ >
58
+ Learn more <ExternalLink />
59
+ </a>
60
+ </Button>
61
+ </EmptyContent>
62
+ </Empty>
63
+ </div>
64
+ )
65
+ }
66
+
67
+ if (!flow) return null
68
+
69
+ return (
70
+ <ReactFlowProvider>
71
+ <FlowView flow={flow} flowConfig={flowConfig!} />
72
+ </ReactFlowProvider>
73
+ )
74
+ })
75
+ FlowPage.displayName = 'FlowPage'
@@ -0,0 +1,52 @@
1
+ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@motiadev/ui'
2
+ import { ChevronsUpDown, Workflow } from 'lucide-react'
3
+ import { useShallow } from 'zustand/react/shallow'
4
+ import { useFetchFlows } from '../../hooks/use-fetch-flows'
5
+ import { motiaAnalytics } from '../../lib/motia-analytics'
6
+ import { useFlowStore } from '../../stores/use-flow-store'
7
+
8
+ export const FlowTabMenuItem = () => {
9
+ useFetchFlows()
10
+
11
+ const selectFlowId = useFlowStore((state) => state.selectFlowId)
12
+ const flows = useFlowStore(useShallow((state) => Object.values(state.flows)))
13
+ const selectedFlowId = useFlowStore((state) => state.selectedFlowId)
14
+
15
+ const handleFlowSelect = (flowId: string) => {
16
+ selectFlowId(flowId)
17
+ motiaAnalytics.track('flow_selected', { flow: flowId })
18
+ }
19
+
20
+ return (
21
+ <div className="flex flex-row justify-center items-center gap-2 cursor-pointer">
22
+ <Workflow />
23
+ {flows.length > 0 && selectedFlowId ? selectedFlowId : 'Flows'}
24
+ <DropdownMenu>
25
+ <DropdownMenuTrigger asChild>
26
+ <div
27
+ className="flex flex-row justify-center items-center gap-2 cursor-pointer"
28
+ data-testid="flows-dropdown-trigger"
29
+ >
30
+ <ChevronsUpDown className="size-4" />
31
+ </div>
32
+ </DropdownMenuTrigger>
33
+ <DropdownMenuContent className="bg-background text-foreground flows-dropdown">
34
+ {flows.length === 0 ? (
35
+ <DropdownMenuItem disabled>Loading flows...</DropdownMenuItem>
36
+ ) : (
37
+ flows.map((item) => (
38
+ <DropdownMenuItem
39
+ data-testid={`dropdown-${item}`}
40
+ key={`dropdown-${item}`}
41
+ className="cursor-pointer gap-2 flow-link"
42
+ onSelect={() => handleFlowSelect(item)}
43
+ >
44
+ {item}
45
+ </DropdownMenuItem>
46
+ ))
47
+ )}
48
+ </DropdownMenuContent>
49
+ </DropdownMenu>
50
+ </div>
51
+ )
52
+ }
@@ -0,0 +1,66 @@
1
+ import {
2
+ Background,
3
+ BackgroundVariant,
4
+ type NodeChange,
5
+ type OnNodesChange,
6
+ ReactFlow,
7
+ type Edge as ReactFlowEdge,
8
+ type Node as ReactFlowNode,
9
+ } from '@xyflow/react'
10
+ import type React from 'react'
11
+ import { useCallback, useState } from 'react'
12
+ import type { EdgeData, FlowConfigResponse, FlowResponse, NodeData } from '../../types/flow'
13
+ import { BaseEdge } from './base-edge'
14
+ import { FlowLoader } from './flow-loader'
15
+ import { useGetFlowState } from './hooks/use-get-flow-state'
16
+ import { NodeOrganizer } from './node-organizer'
17
+
18
+ import '@xyflow/react/dist/style.css'
19
+ import { BackgroundEffect } from '@motiadev/ui'
20
+
21
+ export type FlowNode = ReactFlowNode<NodeData>
22
+ export type FlowEdge = ReactFlowEdge<EdgeData>
23
+
24
+ const edgeTypes = {
25
+ base: BaseEdge,
26
+ }
27
+
28
+ type Props = {
29
+ flow: FlowResponse
30
+ flowConfig: FlowConfigResponse
31
+ }
32
+
33
+ export const FlowView: React.FC<Props> = ({ flow, flowConfig }) => {
34
+ const { nodes, edges, onNodesChange, onEdgesChange, nodeTypes } = useGetFlowState(flow, flowConfig)
35
+ const [initialized, setInitialized] = useState(false)
36
+ const onInitialized = useCallback(() => setInitialized(true), [])
37
+
38
+ const onNodesChangeHandler = useCallback<OnNodesChange<FlowNode>>(
39
+ (changes: NodeChange<FlowNode>[]) => onNodesChange(changes),
40
+ [onNodesChange],
41
+ )
42
+
43
+ if (!nodeTypes) {
44
+ return null
45
+ }
46
+
47
+ return (
48
+ <div className="w-full h-full relative">
49
+ {!initialized && <FlowLoader />}
50
+ <ReactFlow
51
+ minZoom={0.1}
52
+ nodes={nodes}
53
+ edges={edges}
54
+ nodeTypes={nodeTypes}
55
+ edgeTypes={edgeTypes}
56
+ onNodesChange={onNodesChangeHandler}
57
+ onEdgesChange={onEdgesChange}
58
+ className="isolate"
59
+ >
60
+ <BackgroundEffect />
61
+ <Background variant={BackgroundVariant.Dots} gap={20} size={1} />
62
+ <NodeOrganizer onInitialized={onInitialized} nodes={nodes} edges={edges} />
63
+ </ReactFlow>
64
+ </div>
65
+ )
66
+ }
@@ -0,0 +1,171 @@
1
+ import { type Edge, type Node, useEdgesState, useNodesState } from '@xyflow/react'
2
+ import isEqual from 'fast-deep-equal'
3
+ import type React from 'react'
4
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
5
+ import type { EdgeData, FlowConfigResponse, FlowResponse, NodeConfig, NodeData } from '../../../types/flow'
6
+ import { ApiFlowNode } from '../nodes/api-flow-node'
7
+ import { CronFlowNode } from '../nodes/cron-flow-node'
8
+ import { EventFlowNode } from '../nodes/event-flow-node'
9
+ import { NoopFlowNode } from '../nodes/noop-flow-node'
10
+ import { useSaveWorkflowConfig } from './use-save-workflow-config'
11
+
12
+ const DEFAULT_CONFIG: NodeConfig = { x: 0, y: 0 }
13
+
14
+ const getNodePosition = (flowConfig: FlowConfigResponse | null, stepName: string): NodeConfig => {
15
+ return flowConfig?.config[stepName] || DEFAULT_CONFIG
16
+ }
17
+
18
+ type FlowState = {
19
+ nodes: Node<NodeData>[]
20
+ edges: Edge<EdgeData>[]
21
+ nodeTypes: Record<string, React.ComponentType<any>>
22
+ }
23
+
24
+ const nodeComponentCache = new Map<string, React.ComponentType<any>>()
25
+
26
+ const BASE_NODE_TYPES: Record<string, React.ComponentType<any>> = {
27
+ event: EventFlowNode,
28
+ api: ApiFlowNode,
29
+ noop: NoopFlowNode,
30
+ cron: CronFlowNode,
31
+ }
32
+
33
+ async function importFlow(flow: FlowResponse, flowConfig: FlowConfigResponse | null): Promise<FlowState> {
34
+ const nodeTypes: Record<string, React.ComponentType<any>> = { ...BASE_NODE_TYPES }
35
+
36
+ const customNodePromises = flow.steps
37
+ .filter((step) => step.nodeComponentPath)
38
+ .map(async (step) => {
39
+ const path = step.nodeComponentPath!
40
+
41
+ // Check cache first
42
+ if (nodeComponentCache.has(path)) {
43
+ nodeTypes[path] = nodeComponentCache.get(path)!
44
+ return
45
+ }
46
+
47
+ try {
48
+ const module = await import(/* @vite-ignore */ `/@fs/${path}`)
49
+ const component = module.Node ?? module.default
50
+ nodeComponentCache.set(path, component)
51
+ nodeTypes[path] = component
52
+ } catch (error) {
53
+ console.error(`Failed to load custom node component: ${path}`, error)
54
+ }
55
+ })
56
+
57
+ await Promise.all(customNodePromises)
58
+
59
+ const nodes: Node<NodeData>[] = flow.steps.map((step) => ({
60
+ id: step.id,
61
+ type: step.nodeComponentPath || step.type,
62
+ filePath: step.filePath,
63
+ position: step.filePath ? getNodePosition(flowConfig, step.filePath) : DEFAULT_CONFIG,
64
+ data: { ...step, nodeConfig: step.filePath ? getNodePosition(flowConfig, step.filePath) : DEFAULT_CONFIG },
65
+ language: step.language,
66
+ }))
67
+
68
+ const edges: Edge<EdgeData>[] = flow.edges.map((edge) => ({
69
+ ...edge,
70
+ type: 'base',
71
+ }))
72
+
73
+ return { nodes, edges, nodeTypes }
74
+ }
75
+
76
+ export const useGetFlowState = (flow: FlowResponse, flowConfig: FlowConfigResponse) => {
77
+ const [nodeTypes, setNodeTypes] = useState<Record<string, React.ComponentType<any>>>(BASE_NODE_TYPES)
78
+ const [nodes, setNodes, onNodesChange] = useNodesState<Node<NodeData>>([])
79
+ const [edges, setEdges, onEdgesChange] = useEdgesState<Edge<EdgeData>>([])
80
+
81
+ const saveConfig = useSaveWorkflowConfig()
82
+
83
+ const flowIdRef = useRef<string>('')
84
+ const saveTimeoutRef = useRef<ReturnType<typeof setTimeout>>(null)
85
+ const lastSavedConfigRef = useRef<FlowConfigResponse['config']>(null)
86
+ const lastSavedFlowRef = useRef<FlowResponse>(null)
87
+
88
+ // eslint-disable-next-line react-hooks/exhaustive-deps
89
+ const memoizedFlowConfig = useMemo(() => flowConfig, [flowConfig?.id, flowConfig?.config])
90
+
91
+ useEffect(() => {
92
+ if (!flow || flow.error) return
93
+ const hasSameConfig = isEqual(lastSavedConfigRef.current, memoizedFlowConfig?.config)
94
+ const hasSameFlow = isEqual(lastSavedFlowRef.current, flow)
95
+
96
+ if (hasSameConfig && hasSameFlow) return
97
+
98
+ lastSavedConfigRef.current = memoizedFlowConfig?.config
99
+ flowIdRef.current = flow.id
100
+ lastSavedFlowRef.current = flow
101
+
102
+ const importFlowAsync = async () => {
103
+ try {
104
+ const { nodes, edges, nodeTypes } = await importFlow(flow, flowConfig)
105
+ setNodes(nodes)
106
+ setEdges(edges)
107
+ setNodeTypes(nodeTypes)
108
+ } catch (error) {
109
+ console.error('Failed to import flow:', error)
110
+ }
111
+ }
112
+
113
+ importFlowAsync()
114
+ }, [flow, memoizedFlowConfig, setNodes, setEdges, flowConfig])
115
+
116
+ const saveFlowConfig = useCallback(
117
+ (nodesToSave: Node<NodeData>[]) => {
118
+ if (saveTimeoutRef.current) {
119
+ clearTimeout(saveTimeoutRef.current)
120
+ }
121
+
122
+ saveTimeoutRef.current = setTimeout(async () => {
123
+ const steps = nodesToSave.reduce<FlowConfigResponse['config']>((acc, node) => {
124
+ if (node.data.filePath) {
125
+ acc[node.data.filePath] = {
126
+ x: Math.round(node.position.x),
127
+ y: Math.round(node.position.y),
128
+ }
129
+
130
+ if (node.data.nodeConfig?.sourceHandlePosition) {
131
+ acc[node.data.filePath].sourceHandlePosition = node.data.nodeConfig.sourceHandlePosition
132
+ }
133
+ if (node.data.nodeConfig?.targetHandlePosition) {
134
+ acc[node.data.filePath].targetHandlePosition = node.data.nodeConfig.targetHandlePosition
135
+ }
136
+ }
137
+ return acc
138
+ }, {})
139
+
140
+ if (!isEqual(steps, lastSavedConfigRef.current)) {
141
+ lastSavedConfigRef.current = steps
142
+ const newConfig = { id: flowIdRef.current, config: steps }
143
+
144
+ try {
145
+ await saveConfig(newConfig)
146
+ } catch (error) {
147
+ console.error('Failed to save flow config:', error)
148
+ }
149
+ }
150
+ }, 300)
151
+ },
152
+ [saveConfig],
153
+ )
154
+
155
+ useEffect(() => {
156
+ if (nodes.length > 0) {
157
+ saveFlowConfig(nodes)
158
+ }
159
+
160
+ return () => {
161
+ if (saveTimeoutRef.current) {
162
+ clearTimeout(saveTimeoutRef.current)
163
+ }
164
+ }
165
+ }, [nodes, saveFlowConfig])
166
+
167
+ return useMemo(
168
+ () => ({ nodes, edges, onNodesChange, onEdgesChange, nodeTypes }),
169
+ [nodes, edges, onNodesChange, onEdgesChange, nodeTypes],
170
+ )
171
+ }
@@ -0,0 +1,25 @@
1
+ import { useCallback } from 'react'
2
+ import type { FlowConfigResponse } from '../../../types/flow'
3
+
4
+ export const useSaveWorkflowConfig = () => {
5
+ return useCallback(async (body: FlowConfigResponse) => {
6
+ try {
7
+ const response = await fetch(`/__motia/flows/${body.id}/config`, {
8
+ method: 'POST',
9
+ headers: {
10
+ 'Content-Type': 'application/json',
11
+ },
12
+ body: JSON.stringify(body),
13
+ })
14
+
15
+ if (!response.ok) {
16
+ throw new Error(`Failed to save config: ${response.statusText}`)
17
+ }
18
+
19
+ return await response.json()
20
+ } catch (error) {
21
+ console.error('Error saving workflow config:', error)
22
+ throw error
23
+ }
24
+ }, [])
25
+ }
@@ -0,0 +1,103 @@
1
+ import { type Edge, type Node, useNodesInitialized, useReactFlow } from '@xyflow/react'
2
+ import dagre from 'dagre'
3
+ import isEqual from 'fast-deep-equal'
4
+ import type React from 'react'
5
+ import { useEffect, useRef } from 'react'
6
+ import type { EdgeData, NodeData } from '../../types/flow'
7
+
8
+ const organizeNodes = (nodes: Node<NodeData>[], edges: Edge<EdgeData>[]): Node<NodeData>[] => {
9
+ const dagreGraph = new dagre.graphlib.Graph({ directed: true, compound: false, multigraph: false })
10
+
11
+ dagreGraph.setDefaultEdgeLabel(() => ({}))
12
+ dagreGraph.setGraph({ rankdir: 'LR', ranksep: 0, nodesep: 20, edgesep: 0 })
13
+
14
+ nodes.forEach((node) => {
15
+ if (node.position.x !== 0 || node.position.y !== 0) {
16
+ dagreGraph.setNode(node.id, {
17
+ width: node.measured?.width,
18
+ height: node.measured?.height,
19
+ x: node.position.x,
20
+ y: node.position.y,
21
+ })
22
+ } else {
23
+ dagreGraph.setNode(node.id, {
24
+ width: node.measured?.width,
25
+ height: node.measured?.height,
26
+ })
27
+ }
28
+ })
29
+
30
+ edges.forEach((edge) => {
31
+ if (typeof edge.label === 'string') {
32
+ dagreGraph.setEdge(edge.source, edge.target, {
33
+ label: edge.label ?? '',
34
+ width: edge.label.length * 40, // Add width for the label
35
+ height: 30, // Add height for the label
36
+ labelpos: 'c', // Position label in center
37
+ })
38
+ } else {
39
+ dagreGraph.setEdge(edge.source, edge.target)
40
+ }
41
+ })
42
+
43
+ dagre.layout(dagreGraph)
44
+
45
+ return nodes.map((node) => {
46
+ if (node.position.x !== 0 || node.position.y !== 0) {
47
+ return node
48
+ }
49
+
50
+ const { x, y } = dagreGraph.node(node.id)
51
+ const position = {
52
+ x: x - (node.measured?.width ?? 0) / 2,
53
+ y: y - (node.measured?.height ?? 0) / 2,
54
+ }
55
+
56
+ return { ...node, position }
57
+ })
58
+ }
59
+
60
+ type Props = {
61
+ onInitialized: () => void
62
+ nodes: Node<NodeData>[]
63
+ edges: Edge<EdgeData>[]
64
+ }
65
+
66
+ export const NodeOrganizer: React.FC<Props> = ({ onInitialized, nodes, edges }) => {
67
+ const { setNodes, getNodes, getEdges, fitView } = useReactFlow()
68
+ const nodesInitialized = useNodesInitialized()
69
+ const initialized = useRef(false)
70
+
71
+ const lastNodesRef = useRef<Node<NodeData>[]>([])
72
+ const lastEdgesRef = useRef<Edge<EdgeData>[]>([])
73
+
74
+ useEffect(() => {
75
+ if (nodesInitialized) {
76
+ if (isEqual(lastNodesRef.current, nodes) && isEqual(lastEdgesRef.current, edges)) {
77
+ return
78
+ }
79
+
80
+ lastNodesRef.current = nodes
81
+ lastEdgesRef.current = edges
82
+
83
+ try {
84
+ const nodesToOrganize = nodes.some((node) => node.position.x === 0 && node.position.y === 0)
85
+
86
+ if (nodesToOrganize) {
87
+ const organizedNodes = organizeNodes(nodes, edges)
88
+ setNodes(organizedNodes)
89
+ }
90
+
91
+ if (!initialized.current) {
92
+ initialized.current = true
93
+ onInitialized()
94
+ setTimeout(() => fitView(), 1)
95
+ }
96
+ } catch (error) {
97
+ console.error('Error organizing nodes:', error)
98
+ }
99
+ }
100
+ }, [nodesInitialized, onInitialized, setNodes, getNodes, getEdges, fitView, nodes, edges])
101
+
102
+ return null
103
+ }
@@ -0,0 +1,6 @@
1
+ import { ApiNode } from '../../../publicComponents/api-node'
2
+ import type { ApiNodeProps } from '../../../publicComponents/node-props'
3
+
4
+ export const ApiFlowNode = ({ data }: ApiNodeProps) => {
5
+ return <ApiNode data={data} />
6
+ }
@@ -0,0 +1,6 @@
1
+ import { CronNode } from '../../../publicComponents/cron-node'
2
+ import type { CronNodeProps } from '../../../publicComponents/node-props'
3
+
4
+ export const CronFlowNode = ({ data }: CronNodeProps) => {
5
+ return <CronNode data={data} />
6
+ }
@@ -0,0 +1,6 @@
1
+ import { EventNode } from '../../../publicComponents/event-node'
2
+ import type { EventNodeProps } from '../../../publicComponents/node-props'
3
+
4
+ export const EventFlowNode = ({ data }: EventNodeProps) => {
5
+ return <EventNode data={data} />
6
+ }