@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,140 @@
1
+ import { useCallback } from 'react'
2
+
3
+ interface AmplitudeInstance {
4
+ setOptOut(optOut: boolean): void
5
+ track(eventName: string, eventProperties?: Record<string, any>): void
6
+ identify(userId: string, userProperties?: Record<string, any>): void
7
+ setUserId(userId: string): void
8
+ getUserId(): string | undefined
9
+ }
10
+
11
+ declare global {
12
+ interface Window {
13
+ amplitude: AmplitudeInstance
14
+ }
15
+ }
16
+
17
+ interface AnalyticsUserData {
18
+ userId: string
19
+ projectId: string
20
+ motiaVersion: string
21
+ analyticsEnabled: boolean
22
+ }
23
+
24
+ class WorkbenchAnalytics {
25
+ private isInitialized = false
26
+ private userIdCache: string | null = null
27
+ private projectIdCache: string | null = null
28
+ private motiaVersion: string | null = null
29
+
30
+ constructor() {
31
+ this.initialize()
32
+ }
33
+
34
+ private async initialize() {
35
+ if (typeof window !== 'undefined' && window.amplitude) {
36
+ await this.fetchUserData()
37
+ this.isInitialized = true
38
+ this.identifyUser()
39
+ }
40
+ }
41
+
42
+ private async fetchUserData(): Promise<void> {
43
+ try {
44
+ const response = await fetch('/motia/analytics/user')
45
+ if (response.ok) {
46
+ const data: AnalyticsUserData = await response.json()
47
+ this.userIdCache = data.userId
48
+ this.projectIdCache = data.projectId
49
+ this.motiaVersion = data.motiaVersion
50
+
51
+ window.amplitude.setOptOut(!data.analyticsEnabled)
52
+ // Set the user ID in Amplitude to match backend
53
+ if (window.amplitude && data.userId) {
54
+ window.amplitude.setUserId(data.userId)
55
+ }
56
+ } else {
57
+ console.warn('Failed to fetch user data from backend, using fallback')
58
+ this.userIdCache = this.generateFallbackUserId()
59
+ }
60
+ } catch (error) {
61
+ console.warn('Error fetching user data:', error)
62
+ this.userIdCache = this.generateFallbackUserId()
63
+ }
64
+ }
65
+
66
+ private generateFallbackUserId(): string {
67
+ let userId = localStorage.getItem('motia-user-id')
68
+ if (!userId) {
69
+ userId = `user-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
70
+ localStorage.setItem('motia-user-id', userId)
71
+ }
72
+ return userId
73
+ }
74
+
75
+ private identifyUser() {
76
+ if (!this.isInitialized || !this.userIdCache) return
77
+
78
+ try {
79
+ window.amplitude.identify(this.userIdCache, {
80
+ project_id: this.projectIdCache,
81
+ browser: this.getBrowserInfo(),
82
+ screen_resolution: `${window.screen.width}x${window.screen.height}`,
83
+ workbench_version: this.motiaVersion,
84
+ })
85
+ } catch (error) {
86
+ console.warn('Analytics user identification failed:', error)
87
+ }
88
+ }
89
+
90
+ private getBrowserInfo(): string {
91
+ const ua = navigator.userAgent
92
+ if (ua.includes('Chrome')) return 'Chrome'
93
+ if (ua.includes('Firefox')) return 'Firefox'
94
+ if (ua.includes('Safari')) return 'Safari'
95
+ if (ua.includes('Edge')) return 'Edge'
96
+ return 'Unknown'
97
+ }
98
+
99
+ // Method to get current user and project IDs for external use
100
+ getAnalyticsIds() {
101
+ return {
102
+ userId: this.userIdCache,
103
+ projectId: this.projectIdCache,
104
+ }
105
+ }
106
+
107
+ // Simple track method that preserves the user binding
108
+ track(eventName: string, properties?: Record<string, any>) {
109
+ if (!this.isInitialized) return
110
+
111
+ const eventProperties = {
112
+ project_id: this.projectIdCache,
113
+ source: 'frontend',
114
+ ...properties,
115
+ }
116
+
117
+ try {
118
+ window.amplitude.track(eventName, eventProperties)
119
+ } catch (error) {
120
+ console.warn('Analytics tracking failed:', error)
121
+ }
122
+ }
123
+ }
124
+
125
+ export const motiaAnalytics = new WorkbenchAnalytics()
126
+
127
+ export const useAnalytics = () => {
128
+ const track = useCallback((eventName: string, properties?: Record<string, any>) => {
129
+ motiaAnalytics.track(eventName, properties)
130
+ }, [])
131
+
132
+ const getAnalyticsIds = useCallback(() => {
133
+ return motiaAnalytics.getAnalyticsIds()
134
+ }, [])
135
+
136
+ return {
137
+ track,
138
+ getAnalyticsIds,
139
+ }
140
+ }
@@ -0,0 +1,132 @@
1
+ import { plugins } from 'virtual:motia-plugins'
2
+ import { DynamicIcon, dynamicIconImports, type IconName } from 'lucide-react/dynamic'
3
+ import { memo } from 'react'
4
+ import { type AppTab, TabLocation, useAppTabsStore } from '../stores/use-app-tabs-store'
5
+ import { isValidTabLocation } from './utils'
6
+
7
+ export const registerPluginTabs = (addTab: (position: TabLocation, tab: AppTab) => void): void => {
8
+ if (!Array.isArray(plugins)) {
9
+ console.warn('[Motia] Invalid plugins configuration: expected array')
10
+ return
11
+ }
12
+
13
+ plugins.forEach((plugin, index) => {
14
+ try {
15
+ if (!plugin.label) {
16
+ console.warn(`[Motia] Plugin at index ${index} missing label, skipping`)
17
+ return
18
+ }
19
+
20
+ if (!plugin.component) {
21
+ console.warn(`[Motia] Plugin "${plugin.label}" missing component, skipping`)
22
+ return
23
+ }
24
+
25
+ const position = plugin.position || 'top'
26
+ if (!isValidTabLocation(position)) {
27
+ console.warn(`[Motia] Plugin "${plugin.label}" has invalid position "${position}", defaulting to "top"`)
28
+ }
29
+
30
+ const tabLocation = isValidTabLocation(position) ? position : TabLocation.TOP
31
+
32
+ const PluginTabLabel = memo(() => {
33
+ const hasIcon = Object.keys(dynamicIconImports).includes(plugin.labelIcon as IconName)
34
+ const iconName = hasIcon ? (plugin.labelIcon as IconName) : 'toy-brick'
35
+
36
+ if (!hasIcon) {
37
+ console.warn(
38
+ `[Motia] Plugin "${plugin.label}" has invalid icon "${plugin.labelIcon}", defaulting to "toy-brick"`,
39
+ )
40
+ }
41
+
42
+ return (
43
+ <>
44
+ <DynamicIcon name={iconName} />
45
+ <span>{plugin.label}</span>
46
+ </>
47
+ )
48
+ })
49
+ PluginTabLabel.displayName = `${plugin.label}TabLabel`
50
+
51
+ const PluginContent = memo(() => {
52
+ const Component = plugin.component
53
+ const props = plugin.props || {}
54
+
55
+ if (!Component) {
56
+ return <div>Error: Plugin component not found</div>
57
+ }
58
+
59
+ return <Component {...props} />
60
+ })
61
+ PluginContent.displayName = `${plugin.label}Content`
62
+
63
+ addTab(tabLocation, {
64
+ id: plugin.label.toLowerCase(),
65
+ tabLabel: PluginTabLabel,
66
+ content: PluginContent,
67
+ })
68
+ } catch (error) {
69
+ console.error(`[Motia] Error registering plugin "${plugin.label}":`, error)
70
+ }
71
+ })
72
+ }
73
+
74
+ const refreshPluginTabs = (nextPlugins: typeof plugins): void => {
75
+ try {
76
+ const state = useAppTabsStore.getState()
77
+ const { removeTab, addTab } = state
78
+
79
+ const idsToRefresh = new Set(nextPlugins.map((p) => (p.label || '').toLowerCase()))
80
+
81
+ idsToRefresh.forEach((id) => {
82
+ removeTab(TabLocation.TOP, id)
83
+ removeTab(TabLocation.BOTTOM, id)
84
+ })
85
+
86
+ nextPlugins.forEach((plugin, index) => {
87
+ try {
88
+ if (!plugin.label || !plugin.component) return
89
+
90
+ const position = plugin.position || 'top'
91
+ const tabLocation = isValidTabLocation(position) ? position : TabLocation.TOP
92
+
93
+ const PluginTabLabel = memo(() => {
94
+ const hasIcon = Object.keys(dynamicIconImports).includes(plugin.labelIcon as IconName)
95
+ const iconName: IconName = hasIcon ? (plugin.labelIcon as IconName) : 'toy-brick'
96
+ return (
97
+ <>
98
+ <DynamicIcon name={iconName} />
99
+ <span>{plugin.label}</span>
100
+ </>
101
+ )
102
+ })
103
+ PluginTabLabel.displayName = `${plugin.label}TabLabel_HMR_${index}`
104
+
105
+ const PluginContent = memo(() => {
106
+ const Component = plugin.component as React.ElementType
107
+ const props = plugin.props || {}
108
+ if (!Component) return <div>Error: Plugin component not found</div>
109
+ return <Component {...props} />
110
+ })
111
+ PluginContent.displayName = `${plugin.label}Content_HMR_${index}`
112
+
113
+ addTab(tabLocation, {
114
+ id: plugin.label.toLowerCase(),
115
+ tabLabel: PluginTabLabel,
116
+ content: PluginContent,
117
+ })
118
+ } catch (error) {
119
+ console.error(`[Motia] Error refreshing plugin "${plugin.label}":`, error)
120
+ }
121
+ })
122
+ } catch (err) {
123
+ console.error('[Motia] Failed to refresh plugin tabs via HMR:', err)
124
+ }
125
+ }
126
+
127
+ if (import.meta.hot) {
128
+ import.meta.hot.accept('virtual:motia-plugins', (mod) => {
129
+ const next = (mod as unknown as { plugins?: typeof plugins })?.plugins || []
130
+ refreshPluginTabs(next)
131
+ })
132
+ }
@@ -0,0 +1,37 @@
1
+ import { TabLocation } from '../stores/use-app-tabs-store'
2
+
3
+ export const formatDuration = (duration?: number) => {
4
+ if (duration === undefined || duration === null) return 'N/A'
5
+ if (duration < 1000) return `${duration}ms`
6
+ if (duration < 60000) return `${(duration / 1000).toFixed(1)}s`
7
+ if (duration < 3600000) return `${(duration / 60000).toFixed(1)}min`
8
+ return `${(duration / 3600000).toFixed(1)}h`
9
+ }
10
+
11
+ export const formatTimestamp = (time: number) => {
12
+ const date = new Date(Number(time))
13
+ return `${date.toLocaleDateString('en-US', { year: undefined, month: 'short', day: '2-digit' })}, ${date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', second: '2-digit', hourCycle: 'h24' })}.${date.getMilliseconds().toString().padStart(3, '0')}`
14
+ }
15
+
16
+ export type ViewMode = 'project' | 'system'
17
+ export const DEFAULT_VIEW_MODE: ViewMode = 'system'
18
+
19
+ export const getViewModeFromURL = (): ViewMode => {
20
+ try {
21
+ const url = new URL(window.location.href)
22
+ const viewMode = url.searchParams.get('view-mode')
23
+
24
+ if (viewMode === 'project' || viewMode === 'system') {
25
+ return viewMode
26
+ }
27
+
28
+ return DEFAULT_VIEW_MODE
29
+ } catch (error) {
30
+ console.error('[Motia] Error parsing URL:', error)
31
+ return DEFAULT_VIEW_MODE
32
+ }
33
+ }
34
+
35
+ export const isValidTabLocation = (position: string): position is TabLocation => {
36
+ return Object.values(TabLocation).includes(position as TabLocation)
37
+ }
@@ -0,0 +1,30 @@
1
+ import { MotiaStreamProvider } from '@motiadev/stream-client-react'
2
+ import { StrictMode } from 'react'
3
+ import { createRoot } from 'react-dom/client'
4
+ import { BrowserRouter, Route, Routes } from 'react-router-dom'
5
+ import { App } from './App'
6
+ import { NotFoundPage } from './components/NotFoundPage'
7
+ import { RootMotia } from './components/root-motia'
8
+ import './index.css'
9
+
10
+ const rootElement = document.getElementById('root')!
11
+ if (!rootElement.innerHTML) {
12
+ const basePath = workbenchBase
13
+ const root = createRoot(rootElement)
14
+ const address = window.location.origin.replace('http', 'ws')
15
+
16
+ root.render(
17
+ <StrictMode>
18
+ <MotiaStreamProvider address={address}>
19
+ <RootMotia>
20
+ <BrowserRouter>
21
+ <Routes>
22
+ <Route path={basePath} element={<App />} />
23
+ <Route path="*" element={<NotFoundPage />} />
24
+ </Routes>
25
+ </BrowserRouter>
26
+ </RootMotia>
27
+ </MotiaStreamProvider>
28
+ </StrictMode>,
29
+ )
30
+ }
@@ -0,0 +1,32 @@
1
+ import { APP_SIDEBAR_CONTAINER_ID, Panel } 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
+
6
+ const topTabs = (state: AppTabsState) => state.tabs[TabLocation.TOP]
7
+
8
+ export const ProjectViewMode = memo(() => {
9
+ const tabs = useAppTabsStore(useShallow(topTabs))
10
+
11
+ return (
12
+ <div className="grid grid-rows-1 grid-cols-[1fr_auto] bg-background text-foreground h-screen ">
13
+ <main className="m-2 overflow-hidden">
14
+ <Panel
15
+ contentClassName={'p-0'}
16
+ tabs={tabs.map((tab) => {
17
+ const Element = tab.content
18
+ const LabelComponent = tab.tabLabel
19
+ return {
20
+ label: tab.id,
21
+ labelComponent: <LabelComponent />,
22
+ content: <Element />,
23
+ 'data-testid': tab.id,
24
+ }
25
+ })}
26
+ />
27
+ </main>
28
+ <div id={APP_SIDEBAR_CONTAINER_ID} />
29
+ </div>
30
+ )
31
+ })
32
+ ProjectViewMode.displayName = 'ProjectViewMode'
@@ -0,0 +1,26 @@
1
+ import type { JSX, PropsWithChildren } from 'react'
2
+ import { BaseNode } from './base-node/base-node'
3
+ import type { ApiNodeProps } from './node-props'
4
+
5
+ type Props = PropsWithChildren<ApiNodeProps>
6
+
7
+ export const ApiNode = ({ data, children }: Props): JSX.Element => {
8
+ return (
9
+ <BaseNode
10
+ data={data}
11
+ variant="api"
12
+ title={data.name}
13
+ language={data.language}
14
+ subtitle={data.description}
15
+ disableSourceHandle={!data.emits?.length && !data.virtualEmits?.length}
16
+ disableTargetHandle={!data.subscribes?.length && !data.virtualSubscribes?.length}
17
+ >
18
+ {data.webhookUrl && (
19
+ <div className="flex gap-1 items-center text-muted-foreground">
20
+ <div className="font-mono">{data.webhookUrl}</div>
21
+ </div>
22
+ )}
23
+ {children}
24
+ </BaseNode>
25
+ )
26
+ }
@@ -0,0 +1,50 @@
1
+ import { type HandleProps, Position, Handle as RFHandle } from '@xyflow/react'
2
+ import clsx from 'clsx'
3
+ import type React from 'react'
4
+ import type { HTMLAttributes } from 'react'
5
+
6
+ type Props = HandleProps &
7
+ Omit<HTMLAttributes<HTMLDivElement>, 'id'> & {
8
+ isHidden?: boolean
9
+ onTogglePosition?: () => void
10
+ }
11
+
12
+ export const BaseHandle: React.FC<Props> = (props) => {
13
+ const { isHidden, position, onTogglePosition, ...rest } = props
14
+ const isHorizontal = position === Position.Left || position === Position.Right
15
+
16
+ return (
17
+ <div
18
+ className={clsx(
19
+ 'absolute w-[6px] h-[6px]',
20
+ position === Position.Top && '-top-[4px]',
21
+ position === Position.Bottom && '-bottom-[4px]',
22
+ position === Position.Left && '-left-[4px]',
23
+ position === Position.Right && '-right-[4px]',
24
+ isHorizontal ? 'top-1/2 -mt-[2px]' : 'left-1/2 -ml-[2px]',
25
+ isHidden && 'hidden',
26
+ )}
27
+ onClick={onTogglePosition}
28
+ >
29
+ <RFHandle
30
+ {...rest}
31
+ position={position}
32
+ style={{ background: 'rgb(30,118,231)' }}
33
+ className="
34
+ bg-white/50
35
+ !static
36
+ !w-[6px]
37
+ !h-[6px]
38
+ !min-w-[6px]
39
+ !min-h-[6px]
40
+ !p-0
41
+ !border-none
42
+ !transform-none
43
+ !rounded-full
44
+ !outline-none
45
+ !shadow-none
46
+ "
47
+ />
48
+ </div>
49
+ )
50
+ }
@@ -0,0 +1,114 @@
1
+ import { Button, cn } from '@motiadev/ui'
2
+ import { ScanSearch } from 'lucide-react'
3
+ import type React from 'react'
4
+ import { type PropsWithChildren, useCallback, useEffect, useState } from 'react'
5
+ import { useHandlePositions } from '../../hooks/use-update-handle-positions'
6
+ import type { Feature } from '../../types/file'
7
+ import type { BaseNodeProps } from '../node-props'
8
+ import { BaseHandle } from './base-handle'
9
+ import { NodeHeader } from './node-header'
10
+ import { NodeSidebar } from './node-sidebar'
11
+
12
+ type Props = PropsWithChildren<{
13
+ title: string
14
+ subtitle?: string
15
+ variant: 'event' | 'api' | 'noop' | 'cron'
16
+ language?: string
17
+ className?: string
18
+ disableSourceHandle?: boolean
19
+ disableTargetHandle?: boolean
20
+ data: BaseNodeProps
21
+ }>
22
+
23
+ export const BaseNode: React.FC<Props> = ({
24
+ title,
25
+ variant,
26
+ children,
27
+ disableSourceHandle,
28
+ disableTargetHandle,
29
+ language,
30
+ subtitle,
31
+ data,
32
+ }) => {
33
+ const [isOpen, setIsOpen] = useState(false)
34
+ const { sourcePosition, targetPosition, toggleTargetPosition, toggleSourcePosition } = useHandlePositions(data)
35
+
36
+ const [content, setContent] = useState<string | null>(null)
37
+ const [features, setFeatures] = useState<Feature[]>([])
38
+
39
+ const fetchContent = useCallback(async () => {
40
+ const response = await fetch(`/__motia/step/${data.id}`)
41
+ const responseData = await response.json()
42
+ setContent(responseData.content)
43
+ setFeatures(responseData.features)
44
+ }, [data.id])
45
+
46
+ useEffect(() => {
47
+ if (data.id && isOpen) {
48
+ fetchContent()
49
+ }
50
+ }, [data.id, isOpen, fetchContent])
51
+
52
+ return (
53
+ <div
54
+ className={cn('p-1 rounded-lg max-w-[350px]', {
55
+ 'bg-muted-foreground/20': isOpen,
56
+ })}
57
+ >
58
+ <div
59
+ className="rounded-lg dark:bg-[#101010] bg-background border-1 border-muted-foreground/30 border-solid"
60
+ data-testid={`node-${title?.toLowerCase().replace(/ /g, '-')}`}
61
+ >
62
+ <div className="group relative">
63
+ {/* Main node content */}
64
+ <NodeHeader text={title} variant={variant} className="border-b-2 border-muted-foreground/10">
65
+ <div className="flex justify-end">
66
+ <Button
67
+ data-testid={`open-code-preview-button-${title?.toLowerCase()}`}
68
+ variant="ghost"
69
+ className="h-5 p-0.5"
70
+ onClick={() => setIsOpen(true)}
71
+ >
72
+ <ScanSearch className="w-4 h-4" />
73
+ </Button>
74
+ </div>
75
+ </NodeHeader>
76
+
77
+ {subtitle && <div className="py-4 px-6 text-sm text-muted-foreground">{subtitle}</div>}
78
+ {children && (
79
+ <div className="p-2">
80
+ <div
81
+ className={cn('space-y-3 p-4 text-sm text-muted-foreground', {
82
+ 'bg-card': variant !== 'noop',
83
+ })}
84
+ >
85
+ {children}
86
+ </div>
87
+ </div>
88
+ )}
89
+
90
+ {/* Connection points */}
91
+ {!disableTargetHandle && (
92
+ <BaseHandle type="target" position={targetPosition} onTogglePosition={toggleTargetPosition} />
93
+ )}
94
+ {!disableSourceHandle && (
95
+ <BaseHandle type="source" position={sourcePosition} onTogglePosition={toggleSourcePosition} />
96
+ )}
97
+ </div>
98
+ </div>
99
+
100
+ {content && (
101
+ <NodeSidebar
102
+ features={features}
103
+ content={content}
104
+ title={title}
105
+ subtitle={subtitle}
106
+ variant={variant}
107
+ language={language}
108
+ isOpen={isOpen}
109
+ onClose={() => setIsOpen(false)}
110
+ />
111
+ )}
112
+ </div>
113
+ )
114
+ }
@@ -0,0 +1,119 @@
1
+ import { useThemeStore } from '@motiadev/ui'
2
+ import type React from 'react'
3
+ import { useRef, useState } from 'react'
4
+ import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
5
+ import { oneDark, oneLight } from 'react-syntax-highlighter/dist/esm/styles/prism'
6
+ import type { Feature } from '../../types/file'
7
+ import { FeatureCard } from './feature-card'
8
+ import { LanguageIndicator } from './language-indicator'
9
+
10
+ type CodeDisplayProps = {
11
+ code: string
12
+ language?: string
13
+ features?: Feature[]
14
+ }
15
+
16
+ const codeTagProps = {
17
+ style: {
18
+ fontFamily: 'DM Mono, monospace',
19
+ fontSize: '16px',
20
+ },
21
+ }
22
+
23
+ const customStyle = {
24
+ margin: 0,
25
+ borderRadius: 0,
26
+ padding: 0,
27
+ }
28
+
29
+ const isHighlighted = (lines: string[], lineNumber: number) => {
30
+ return lines.some((line) => {
31
+ const [start, end] = line.split('-').map((num) => parseInt(num, 10))
32
+
33
+ if (end !== undefined) {
34
+ return lineNumber >= start && lineNumber <= end
35
+ }
36
+
37
+ return lineNumber == start
38
+ })
39
+ }
40
+
41
+ const getFirstLineNumber = (line: string) => {
42
+ const [start] = line.split('-').map((num) => parseInt(num, 10))
43
+ return start
44
+ }
45
+
46
+ export const CodeDisplay: React.FC<CodeDisplayProps> = ({ code, language, features }) => {
47
+ const theme = useThemeStore((state) => state.theme)
48
+ const themeStyle = theme === 'dark' ? oneDark : oneLight
49
+ const [highlightedLines, setHighlightedLines] = useState<string[]>([])
50
+ const [selectedFeature, setSelectedFeature] = useState<Feature | null>(null)
51
+ const ref = useRef<HTMLDivElement>(null)
52
+
53
+ const handleFeatureClick = (feature: Feature) => {
54
+ setSelectedFeature(feature)
55
+ setHighlightedLines(feature.lines)
56
+ const lineNumber = getFirstLineNumber(feature.lines[0])
57
+ const line = ref.current?.querySelector(`[data-line-number="${lineNumber}"]`)
58
+
59
+ if (line) {
60
+ line.scrollIntoView({ behavior: 'smooth', block: 'center' })
61
+ }
62
+ }
63
+
64
+ return (
65
+ <div className="flex flex-col h-full overflow-hidden">
66
+ <div className="flex items-center py-2 px-5 dark:bg-[#1e1e1e] gap-2 justify-center">
67
+ <div className="text-sm text-muted-foreground">Read only</div>
68
+ <div className="flex-1" />
69
+ <LanguageIndicator language={language} className="w-4 h-4" size={16} showLabel />
70
+ </div>
71
+ <div className="flex flex-row h-[calc(100%-36px)]">
72
+ {features && features.length > 0 && (
73
+ <div className="flex flex-col gap-2 p-2 bg-card overflow-y-auto min-w-[200px] w-[300px]">
74
+ {features.map((feature, index) => (
75
+ <FeatureCard
76
+ key={index}
77
+ feature={feature}
78
+ highlighted={selectedFeature === feature}
79
+ onClick={() => handleFeatureClick(feature)}
80
+ onHover={() => handleFeatureClick(feature)}
81
+ />
82
+ ))}
83
+ </div>
84
+ )}
85
+
86
+ <div className="overflow-y-auto" ref={ref}>
87
+ <SyntaxHighlighter
88
+ showLineNumbers
89
+ language={language}
90
+ style={themeStyle}
91
+ codeTagProps={codeTagProps}
92
+ customStyle={customStyle}
93
+ wrapLines
94
+ lineProps={(lineNumber) => {
95
+ if (isHighlighted(highlightedLines, lineNumber)) {
96
+ return {
97
+ 'data-line-number': lineNumber,
98
+ style: {
99
+ borderLeft: '2px solid var(--accent-1000)',
100
+ backgroundColor: 'rgb(from var(--accent-1000) r g b / 0.2)',
101
+ },
102
+ }
103
+ }
104
+
105
+ return {
106
+ 'data-line-number': lineNumber,
107
+ style: {
108
+ borderLeft: '2px solid transparent',
109
+ },
110
+ }
111
+ }}
112
+ >
113
+ {code}
114
+ </SyntaxHighlighter>
115
+ </div>
116
+ </div>
117
+ </div>
118
+ )
119
+ }