@nvent-addon/app 0.4.5 → 0.5.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.
Files changed (153) hide show
  1. package/dist/module.d.mts +19 -1
  2. package/dist/module.mjs +20 -8
  3. package/dist/runtime/app/components/{nhealth/component-router.d.vue.ts → ComponentRouter.d.vue.ts} +1 -5
  4. package/dist/runtime/app/components/{nhealth/component-router.vue.d.ts → ComponentRouter.vue.d.ts} +1 -5
  5. package/dist/runtime/app/components/{nhealth/component-shell.d.vue.ts → ComponentShell.d.vue.ts} +4 -9
  6. package/dist/runtime/app/components/ComponentShell.vue +87 -0
  7. package/dist/runtime/app/components/{nhealth/component-shell.vue.d.ts → ComponentShell.vue.d.ts} +4 -9
  8. package/dist/runtime/app/components/ConfirmDialog.d.vue.ts +1 -6
  9. package/dist/runtime/app/components/ConfirmDialog.vue.d.ts +1 -6
  10. package/dist/runtime/app/components/ListItem.d.vue.ts +3 -6
  11. package/dist/runtime/app/components/ListItem.vue.d.ts +3 -6
  12. package/dist/runtime/app/components/LiveIndicator.d.vue.ts +7 -0
  13. package/dist/runtime/app/components/LiveIndicator.vue +30 -0
  14. package/dist/runtime/app/components/LiveIndicator.vue.d.ts +7 -0
  15. package/dist/runtime/app/components/{QueueConfigDetails.d.vue.ts → QueueConfiguration.d.vue.ts} +1 -10
  16. package/dist/runtime/app/components/QueueConfiguration.vue +387 -0
  17. package/dist/runtime/app/components/{QueueConfigDetails.vue.d.ts → QueueConfiguration.vue.d.ts} +1 -10
  18. package/dist/runtime/app/components/StatCard.d.vue.ts +9 -0
  19. package/dist/runtime/app/components/StatCard.vue +57 -0
  20. package/dist/runtime/app/components/StatCard.vue.d.ts +9 -0
  21. package/dist/runtime/app/components/TimelineList.vue +67 -0
  22. package/dist/runtime/app/components/flow/AwaitNode.d.vue.ts +18 -0
  23. package/dist/runtime/app/components/flow/AwaitNode.vue +91 -0
  24. package/dist/runtime/app/components/flow/AwaitNode.vue.d.ts +18 -0
  25. package/dist/runtime/app/components/{FlowDiagram.d.vue.ts → flow/Diagram.d.vue.ts} +12 -1
  26. package/dist/runtime/app/components/{FlowDiagram.vue → flow/Diagram.vue} +92 -11
  27. package/dist/runtime/app/components/{FlowDiagram.vue.d.ts → flow/Diagram.vue.d.ts} +12 -1
  28. package/dist/runtime/app/components/{FlowRunOverview.d.vue.ts → flow/RunOverview.d.vue.ts} +3 -0
  29. package/dist/runtime/app/components/{FlowRunOverview.vue → flow/RunOverview.vue} +94 -8
  30. package/dist/runtime/app/components/{FlowRunOverview.vue.d.ts → flow/RunOverview.vue.d.ts} +3 -0
  31. package/dist/runtime/app/components/{FlowRunStatusBadge.d.vue.ts → flow/RunStatusBadge.d.vue.ts} +2 -8
  32. package/dist/runtime/app/components/{FlowRunStatusBadge.vue → flow/RunStatusBadge.vue} +8 -1
  33. package/dist/runtime/app/components/{FlowRunStatusBadge.vue.d.ts → flow/RunStatusBadge.vue.d.ts} +2 -8
  34. package/dist/runtime/app/components/{FlowRunTimeline.vue → flow/RunTimeline.vue} +1 -1
  35. package/dist/runtime/app/components/{FlowStepSelector.d.vue.ts → flow/StepSelector.d.vue.ts} +1 -0
  36. package/dist/runtime/app/components/flow/StepSelector.vue +553 -0
  37. package/dist/runtime/app/components/{FlowStepSelector.vue.d.ts → flow/StepSelector.vue.d.ts} +1 -0
  38. package/dist/runtime/app/components/trigger/BasicInfoCard.d.vue.ts +33 -0
  39. package/dist/runtime/app/components/trigger/BasicInfoCard.vue +168 -0
  40. package/dist/runtime/app/components/trigger/BasicInfoCard.vue.d.ts +33 -0
  41. package/dist/runtime/app/components/{FlowSchedulesList.d.vue.ts → trigger/DangerZone.d.vue.ts} +4 -6
  42. package/dist/runtime/app/components/trigger/DangerZone.vue +46 -0
  43. package/dist/runtime/app/components/{FlowSchedulesList.vue.d.ts → trigger/DangerZone.vue.d.ts} +4 -6
  44. package/dist/runtime/app/components/trigger/EditHeader.d.vue.ts +15 -0
  45. package/dist/runtime/app/components/trigger/EditHeader.vue +55 -0
  46. package/dist/runtime/app/components/trigger/EditHeader.vue.d.ts +15 -0
  47. package/dist/runtime/app/components/trigger/EventConfig.d.vue.ts +24 -0
  48. package/dist/runtime/app/components/trigger/EventConfig.vue +68 -0
  49. package/dist/runtime/app/components/trigger/EventConfig.vue.d.ts +24 -0
  50. package/dist/runtime/app/components/trigger/FlowSubscriptions.d.vue.ts +14 -0
  51. package/dist/runtime/app/components/trigger/FlowSubscriptions.vue +128 -0
  52. package/dist/runtime/app/components/trigger/FlowSubscriptions.vue.d.ts +14 -0
  53. package/dist/runtime/app/components/trigger/ScheduleConfig.d.vue.ts +27 -0
  54. package/dist/runtime/app/components/trigger/ScheduleConfig.vue +375 -0
  55. package/dist/runtime/app/components/trigger/ScheduleConfig.vue.d.ts +27 -0
  56. package/dist/runtime/app/components/{FlowScheduleDialog.d.vue.ts → trigger/StatusConfig.d.vue.ts} +6 -6
  57. package/dist/runtime/app/components/trigger/StatusConfig.vue +78 -0
  58. package/dist/runtime/app/components/{FlowScheduleDialog.vue.d.ts → trigger/StatusConfig.vue.d.ts} +6 -6
  59. package/dist/runtime/app/components/trigger/WebhookConfig.d.vue.ts +30 -0
  60. package/dist/runtime/app/components/trigger/WebhookConfig.vue +97 -0
  61. package/dist/runtime/app/components/trigger/WebhookConfig.vue.d.ts +30 -0
  62. package/dist/runtime/app/composables/useAnalyzedFlows.d.ts +5 -0
  63. package/dist/runtime/app/composables/useAnalyzedFlows.js +15 -1
  64. package/dist/runtime/app/composables/useComponentRouter.d.ts +8 -0
  65. package/dist/runtime/app/composables/useComponentRouter.js +10 -2
  66. package/dist/runtime/app/composables/useFlowRunsInfinite.d.ts +1 -1
  67. package/dist/runtime/app/composables/useFlowState.js +65 -0
  68. package/dist/runtime/app/composables/useFlowWebSocket.d.ts +11 -2
  69. package/dist/runtime/app/composables/useFlowWebSocket.js +181 -65
  70. package/dist/runtime/app/composables/useQueueJobs.d.ts +12 -1
  71. package/dist/runtime/app/composables/useQueueJobs.js +13 -7
  72. package/dist/runtime/app/composables/useTrigger.d.ts +137 -0
  73. package/dist/runtime/app/composables/useTrigger.js +116 -0
  74. package/dist/runtime/app/composables/useTriggerWebSocket.d.ts +35 -0
  75. package/dist/runtime/app/composables/useTriggerWebSocket.js +333 -0
  76. package/dist/runtime/app/pages/dashboard.d.vue.ts +3 -0
  77. package/dist/runtime/app/pages/dashboard.vue +738 -0
  78. package/dist/runtime/app/pages/dashboard.vue.d.ts +3 -0
  79. package/dist/runtime/app/pages/flows/[name].d.vue.ts +3 -0
  80. package/dist/runtime/app/pages/flows/[name].vue +680 -0
  81. package/dist/runtime/app/pages/flows/[name].vue.d.ts +3 -0
  82. package/dist/runtime/app/pages/flows/index.vue +321 -620
  83. package/dist/runtime/app/pages/index.vue +39 -9
  84. package/dist/runtime/app/pages/queues/index.vue +202 -194
  85. package/dist/runtime/app/pages/queues/jobs.vue +534 -207
  86. package/dist/runtime/app/pages/settings/scheduler.d.vue.ts +3 -0
  87. package/dist/runtime/app/pages/settings/scheduler.vue +310 -0
  88. package/dist/runtime/app/pages/settings/scheduler.vue.d.ts +3 -0
  89. package/dist/runtime/app/pages/triggers/[name]/edit.d.vue.ts +3 -0
  90. package/dist/runtime/app/pages/triggers/[name]/edit.vue +429 -0
  91. package/dist/runtime/app/pages/triggers/[name]/edit.vue.d.ts +3 -0
  92. package/dist/runtime/app/pages/triggers/[name].d.vue.ts +3 -0
  93. package/dist/runtime/app/pages/triggers/[name].vue +898 -0
  94. package/dist/runtime/app/pages/triggers/[name].vue.d.ts +3 -0
  95. package/dist/runtime/app/pages/triggers/index.d.vue.ts +3 -0
  96. package/dist/runtime/app/pages/triggers/index.vue +528 -0
  97. package/dist/runtime/app/pages/triggers/index.vue.d.ts +3 -0
  98. package/dist/runtime/app/pages/triggers/new.d.vue.ts +3 -0
  99. package/dist/runtime/app/pages/triggers/new.vue +610 -0
  100. package/dist/runtime/app/pages/triggers/new.vue.d.ts +3 -0
  101. package/dist/runtime/server/api/_flows/[name]/clear-history.delete.d.ts +10 -0
  102. package/dist/runtime/server/api/_flows/[name]/clear-history.delete.js +49 -0
  103. package/dist/runtime/server/api/_flows/[name]/runs/[runId]/cancel.post.d.ts +2 -0
  104. package/dist/runtime/server/api/_flows/[name]/runs/[runId]/cancel.post.js +21 -0
  105. package/dist/runtime/server/api/_flows/[name]/runs.get.d.ts +17 -0
  106. package/dist/runtime/server/api/_flows/[name]/runs.get.js +64 -0
  107. package/dist/runtime/server/api/_flows/[name]/start.post.d.ts +2 -0
  108. package/dist/runtime/server/api/_flows/[name]/start.post.js +9 -0
  109. package/dist/runtime/server/api/_flows/index.get.d.ts +7 -0
  110. package/dist/runtime/server/api/_flows/index.get.js +5 -0
  111. package/dist/runtime/server/api/_flows/recent-runs.get.d.ts +15 -0
  112. package/dist/runtime/server/api/_flows/recent-runs.get.js +67 -0
  113. package/dist/runtime/server/api/_flows/ws.d.ts +80 -0
  114. package/dist/runtime/server/api/_flows/ws.js +309 -0
  115. package/dist/runtime/server/api/_queues/[name]/job/[id].get.d.ts +2 -0
  116. package/dist/runtime/server/api/_queues/[name]/job/[id].get.js +14 -0
  117. package/dist/runtime/server/api/_queues/[name]/job/index.get.d.ts +2 -0
  118. package/dist/runtime/server/api/_queues/[name]/job/index.get.js +39 -0
  119. package/dist/runtime/server/api/_queues/index.get.d.ts +2 -0
  120. package/dist/runtime/server/api/_queues/index.get.js +106 -0
  121. package/dist/runtime/server/api/_queues/ws.d.ts +48 -0
  122. package/dist/runtime/server/api/_queues/ws.js +215 -0
  123. package/dist/runtime/server/api/_scheduler/jobs.get.d.ts +19 -0
  124. package/dist/runtime/server/api/_scheduler/jobs.get.js +36 -0
  125. package/dist/runtime/server/api/_triggers/[name]/events.get.d.ts +6 -0
  126. package/dist/runtime/server/api/_triggers/[name]/events.get.js +43 -0
  127. package/dist/runtime/server/api/_triggers/[name]/index.get.d.ts +6 -0
  128. package/dist/runtime/server/api/_triggers/[name]/index.get.js +76 -0
  129. package/dist/runtime/server/api/_triggers/[name].delete.d.ts +7 -0
  130. package/dist/runtime/server/api/_triggers/[name].delete.js +37 -0
  131. package/dist/runtime/server/api/_triggers/[name].patch.d.ts +7 -0
  132. package/dist/runtime/server/api/_triggers/[name].patch.js +117 -0
  133. package/dist/runtime/server/api/_triggers/index.get.d.ts +6 -0
  134. package/dist/runtime/server/api/_triggers/index.get.js +44 -0
  135. package/dist/runtime/server/api/_triggers/index.post.d.ts +7 -0
  136. package/dist/runtime/server/api/_triggers/index.post.js +124 -0
  137. package/dist/runtime/server/api/_triggers/stats.get.d.ts +6 -0
  138. package/dist/runtime/server/api/_triggers/stats.get.js +41 -0
  139. package/dist/runtime/server/api/_triggers/ws.d.ts +74 -0
  140. package/dist/runtime/server/api/_triggers/ws.js +315 -0
  141. package/dist/runtime/server/tsconfig.json +7 -0
  142. package/package.json +8 -8
  143. package/dist/runtime/app/components/FlowScheduleDialog.vue +0 -226
  144. package/dist/runtime/app/components/FlowSchedulesList.vue +0 -99
  145. package/dist/runtime/app/components/FlowStepSelector.vue +0 -238
  146. package/dist/runtime/app/components/QueueConfigDetails.vue +0 -412
  147. package/dist/runtime/app/components/nhealth/component-shell.vue +0 -89
  148. /package/dist/runtime/app/components/{nhealth/component-router.vue → ComponentRouter.vue} +0 -0
  149. /package/dist/runtime/app/components/{FlowNodeCard.d.vue.ts → flow/NodeCard.d.vue.ts} +0 -0
  150. /package/dist/runtime/app/components/{FlowNodeCard.vue → flow/NodeCard.vue} +0 -0
  151. /package/dist/runtime/app/components/{FlowNodeCard.vue.d.ts → flow/NodeCard.vue.d.ts} +0 -0
  152. /package/dist/runtime/app/components/{FlowRunTimeline.d.vue.ts → flow/RunTimeline.d.vue.ts} +0 -0
  153. /package/dist/runtime/app/components/{FlowRunTimeline.vue.d.ts → flow/RunTimeline.vue.d.ts} +0 -0
@@ -8,676 +8,377 @@
8
8
  Flows
9
9
  </h1>
10
10
  </div>
11
- <div class="flex items-center gap-3">
12
- <USelectMenu
13
- v-model="selectedFlow"
14
- :items="(flows || []).map((f) => f.id)"
15
- placeholder="Select a flow..."
16
- class="w-64"
17
- >
18
- <template #leading>
19
- <UIcon
20
- v-if="selectedFlow"
21
- name="i-lucide-git-branch"
22
- class="w-4 h-4 text-gray-500"
23
- />
24
- </template>
25
- </USelectMenu>
26
- </div>
11
+ <LiveIndicator
12
+ :is-connected="flowWs.connected.value"
13
+ :is-reconnecting="flowWs.reconnecting.value"
14
+ />
27
15
  </div>
28
16
  </div>
29
17
 
30
18
  <!-- Main Content -->
31
- <div class="flex-1 min-h-0 overflow-hidden">
32
- <div class="h-full flex gap-px bg-gray-200 dark:bg-gray-800">
33
- <!-- Runs List -->
34
- <div class="w-1/3 min-w-0 flex-shrink-0 bg-white dark:bg-gray-950 flex flex-col min-h-0 overflow-hidden">
35
- <div class="px-4 py-3 min-h-[49px] border-b border-gray-200 dark:border-gray-800 flex items-center justify-between shrink-0">
36
- <h2 class="text-sm font-medium text-gray-900 dark:text-gray-100">
37
- Runs
38
- </h2>
39
- <div class="flex items-center gap-2">
40
- <UButton
41
- v-if="selectedFlow"
42
- icon="i-lucide-play"
43
- size="xs"
44
- color="primary"
45
- variant="soft"
46
- @click="openStartFlowModal"
47
- >
48
- Start
49
- </UButton>
50
- <div
51
- v-if="selectedFlow"
52
- class="flex items-center gap-2 text-xs text-gray-500"
53
- >
54
- <UIcon
55
- name="i-lucide-list"
56
- class="w-3.5 h-3.5"
57
- />
58
- <span>{{ totalRuns }} run{{ totalRuns === 1 ? "" : "s" }}</span>
59
- </div>
60
- <UDropdownMenu
61
- v-if="selectedFlow"
62
- :items="flowActionsItems"
63
- :ui="{ content: 'min-w-48' }"
64
- >
65
- <UButton
66
- icon="i-lucide-more-vertical"
67
- size="xs"
68
- color="neutral"
69
- variant="ghost"
70
- square
71
- />
72
- </UDropdownMenu>
73
- </div>
74
- </div>
75
- <div
76
- v-if="!selectedFlow"
77
- class="flex-1 overflow-y-auto min-h-0"
78
- >
79
- <div class="h-full flex items-center justify-center text-sm text-gray-400 px-4 text-center">
80
- Select a flow to view runs
81
- </div>
19
+ <div class="flex-1 min-h-0 overflow-y-auto">
20
+ <div class="max-w-7xl mx-auto p-6">
21
+ <!-- Stats Overview -->
22
+ <div class="grid grid-cols-2 md:grid-cols-6 gap-4 mb-6">
23
+ <StatCard
24
+ icon="i-lucide-git-branch"
25
+ :count="flows?.length || 0"
26
+ label="Total Flows"
27
+ variant="gray"
28
+ />
29
+ <StatCard
30
+ icon="i-lucide-play"
31
+ :count="totalRuns"
32
+ label="Total Runs"
33
+ variant="blue"
34
+ />
35
+ <StatCard
36
+ icon="i-lucide-check-circle"
37
+ :count="totalSuccess"
38
+ label="Success"
39
+ variant="emerald"
40
+ />
41
+ <StatCard
42
+ icon="i-lucide-x-circle"
43
+ :count="totalFailure"
44
+ label="Failures"
45
+ variant="red"
46
+ />
47
+ <StatCard
48
+ icon="i-lucide-loader"
49
+ :count="totalRunning"
50
+ label="Running"
51
+ variant="purple"
52
+ />
53
+ <StatCard
54
+ icon="i-lucide-pause"
55
+ :count="totalAwaiting"
56
+ label="Awaiting"
57
+ variant="amber"
58
+ />
59
+ </div>
60
+
61
+ <!-- Filters -->
62
+ <div class="mb-4">
63
+ <UInput
64
+ v-model="searchQuery"
65
+ icon="i-lucide-search"
66
+ placeholder="Search flows..."
67
+ size="sm"
68
+ />
69
+ </div>
70
+
71
+ <!-- Flows List -->
72
+ <div
73
+ v-if="!filteredFlows || filteredFlows.length === 0"
74
+ class="bg-white dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-800 p-8 text-center text-gray-500"
75
+ >
76
+ <div v-if="searchQuery">
77
+ <UIcon
78
+ name="i-lucide-search-x"
79
+ class="w-12 h-12 animate-spin mx-auto mb-3 opacity-50"
80
+ />
81
+ <p>No flows match your search</p>
82
+ <UButton
83
+ size="xs"
84
+ color="neutral"
85
+ variant="ghost"
86
+ class="mt-2"
87
+ @click="searchQuery = ''"
88
+ >
89
+ Clear Search
90
+ </UButton>
82
91
  </div>
83
- <div
84
- v-else-if="!runs || runs.length === 0"
85
- class="flex-1 overflow-y-auto min-h-0"
86
- >
87
- <div class="h-full flex items-center justify-center text-sm text-gray-400">
88
- <ClientOnly>
89
- <div class="text-center">
90
- <div v-if="loadingRuns">
91
- Loading runs...
92
- </div>
93
- <div v-else>
94
- No runs yet
95
- </div>
96
- </div>
97
- <template #fallback>
98
- <div class="text-center">
99
- No runs yet
100
- </div>
101
- </template>
102
- </ClientOnly>
103
- </div>
92
+ <div v-else>
93
+ <UIcon
94
+ name="i-lucide-git-branch"
95
+ class="w-12 h-12 mx-auto mb-3 opacity-50"
96
+ />
97
+ <p>No flows found</p>
104
98
  </div>
105
- <div
106
- v-else
107
- ref="runsScrollContainer"
108
- class="flex-1 overflow-y-auto min-h-0 divide-y divide-gray-100 dark:divide-gray-800"
109
- @scroll="handleRunsScroll"
110
- >
99
+ </div>
100
+ <div
101
+ v-else
102
+ class="bg-white dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-800 overflow-hidden"
103
+ >
104
+ <div class="divide-y divide-gray-100 dark:divide-gray-800">
111
105
  <div
112
- v-for="r in runs"
113
- :key="r.id"
114
- class="group"
106
+ v-for="flow in filteredFlows"
107
+ :key="flow.id"
108
+ class="px-6 py-4 hover:bg-gray-50 dark:hover:bg-gray-900/50 transition-colors cursor-pointer"
109
+ @click="openFlow(flow.id)"
115
110
  >
116
- <div
117
- class="px-4 py-3 hover:bg-gray-50 dark:hover:bg-gray-900/50 transition-colors cursor-pointer"
118
- :class="{ 'bg-gray-50 dark:bg-gray-900': selectedRunId === r.id }"
119
- @click="selectRun(r.id)"
120
- >
121
- <div class="flex items-start justify-between gap-3">
122
- <div class="flex-1 min-w-0">
123
- <div class="text-xs font-mono text-gray-900 dark:text-gray-100 truncate">
124
- {{ r.id?.substring(0, 8) }}...{{ r.id?.substring(r.id?.length - 4) }}
111
+ <div class="flex items-start justify-between gap-4">
112
+ <!-- Left: Flow Info -->
113
+ <div class="flex-1 min-w-0">
114
+ <div class="flex items-center gap-2 mb-2">
115
+ <UIcon
116
+ name="i-lucide-git-branch"
117
+ class="w-4 h-4 shrink-0 text-blue-500"
118
+ />
119
+ <h3 class="text-sm font-semibold text-gray-900 dark:text-gray-100 truncate">
120
+ {{ flow.id }}
121
+ </h3>
122
+ <UBadge
123
+ v-if="flow.hasAwait"
124
+ label="await"
125
+ color="purple"
126
+ variant="subtle"
127
+ size="xs"
128
+ >
129
+ <template #leading>
130
+ <UIcon
131
+ name="i-lucide-pause"
132
+ class="w-3 h-3"
133
+ />
134
+ </template>
135
+ </UBadge>
136
+ </div>
137
+
138
+ <div class="flex items-center gap-4 text-xs text-gray-500 dark:text-gray-400">
139
+ <div class="flex items-center gap-1">
140
+ <UIcon
141
+ name="i-lucide-layers"
142
+ class="w-3 h-3"
143
+ />
144
+ <span>{{ getStepCount(flow) }} step{{ getStepCount(flow) === 1 ? "" : "s" }}</span>
125
145
  </div>
126
- <div class="flex items-center gap-3 mt-1.5">
127
- <div class="text-xs text-gray-500">
128
- {{ formatTime(r.createdAt) }}
129
- </div>
130
- <!-- Step progress -->
131
- <div
132
- v-if="r.stepCount > 0"
133
- class="text-xs text-gray-500 flex items-center gap-1"
134
- >
146
+ <div class="flex items-center gap-1">
147
+ <UIcon
148
+ name="i-lucide-bar-chart-3"
149
+ class="w-3 h-3"
150
+ />
151
+ <span>{{ getLevelCount(flow) }} level{{ getLevelCount(flow) === 1 ? "" : "s" }}</span>
152
+ </div>
153
+ <div
154
+ v-if="flow.stats && flow.stats.total > 0"
155
+ class="flex items-center gap-2"
156
+ >
157
+ <div class="flex items-center gap-1">
135
158
  <UIcon
136
- name="i-lucide-list-checks"
159
+ name="i-lucide-play"
137
160
  class="w-3 h-3"
138
161
  />
139
- <span>{{ r.completedSteps }}/{{ r.stepCount }}</span>
162
+ <span>{{ flow.stats.total }}</span>
140
163
  </div>
141
- <!-- Duration (if completed) -->
142
164
  <div
143
- v-if="r.completedAt && r.startedAt"
144
- class="text-xs text-gray-500 flex items-center gap-1"
165
+ v-if="flow.stats.running > 0"
166
+ class="flex items-center gap-1 text-purple-600 dark:text-purple-400"
145
167
  >
146
168
  <UIcon
147
- name="i-lucide-timer"
169
+ name="i-lucide-loader"
148
170
  class="w-3 h-3"
149
171
  />
150
- <span>{{ formatDuration(r.startedAt, r.completedAt) }}</span>
172
+ <span>{{ flow.stats.running }}</span>
151
173
  </div>
152
174
  </div>
153
- </div>
154
- <!-- Status badge -->
155
- <FlowRunStatusBadge
156
- :is-running="r.status === 'running'"
157
- :is-completed="r.status === 'completed'"
158
- :is-failed="r.status === 'failed'"
159
- :is-canceled="r.status === 'canceled'"
160
- :is-stalled="r.status === 'stalled'"
161
- />
162
- </div>
163
- </div>
164
- </div>
165
-
166
- <!-- Loading indicator for infinite scroll -->
167
- <div
168
- v-if="loadingRuns"
169
- class="px-4 py-3 text-center text-xs text-gray-400"
170
- >
171
- <UIcon
172
- name="i-lucide-loader-2"
173
- class="w-4 h-4 animate-spin inline-block"
174
- />
175
- <span class="ml-2">Loading more runs...</span>
176
- </div>
177
-
178
- <!-- End of list indicator -->
179
- <div
180
- v-else-if="!hasMoreRuns && runs.length > 0"
181
- class="px-4 py-3 text-center text-xs text-gray-400"
182
- >
183
- All runs loaded
184
- </div>
185
- </div>
186
-
187
- <!-- Schedules Section -->
188
- <div
189
- v-if="selectedFlow"
190
- class="border-t border-gray-200 dark:border-gray-800 shrink-0"
191
- >
192
- <UAccordion
193
- :items="scheduleAccordionItems"
194
- :ui="{
195
- root: 'w-full',
196
- trigger: 'px-4 py-2 bg-gray-50 dark:bg-gray-900/50',
197
- content: 'max-h-48 overflow-y-auto'
198
- }"
199
- >
200
- <template #item>
201
- <FlowSchedulesList
202
- v-if="selectedFlow"
203
- ref="schedulesListRef"
204
- :flow-name="selectedFlow"
205
- class="px-4 py-3"
206
- @updated="handleSchedulesUpdated"
207
- />
208
- </template>
209
- </UAccordion>
210
- </div>
211
- </div>
212
-
213
- <!-- Main Content Area with Tabs -->
214
- <div class="flex-1 min-w-0 bg-white dark:bg-gray-950 flex flex-col min-h-0 overflow-hidden">
215
- <div class="px-4 py-2.5 border-b border-gray-200 dark:border-gray-800 shrink-0">
216
- <div class="flex items-center justify-between">
217
- <UTabs
218
- v-model="mainTab"
219
- :items="mainTabs"
220
- size="xs"
221
- :ui="{
222
- root: 'gap-0',
223
- trigger: 'px-2 py-0.5'
224
- }"
225
- />
226
- <div class="flex items-center gap-2">
227
- <span
228
- v-if="selectedRunId"
229
- class="text-xs text-gray-500 flex items-center gap-2"
230
- >
231
- <span>Run: {{ selectedRunId.substring(0, 8) }}...</span>
232
- <div
233
- v-if="isReconnecting || isConnected && flowState.isRunning.value"
234
- class="flex items-center gap-1.5"
235
- >
236
175
  <div
237
- class="w-1.5 h-1.5 rounded-full"
238
- :class="isReconnecting ? 'bg-amber-500 animate-pulse' : 'bg-emerald-500 animate-pulse'"
239
- />
240
- <span>{{ isReconnecting ? "Reconnecting" : "Live" }}</span>
176
+ v-if="flow.timeout"
177
+ class="flex items-center gap-1"
178
+ >
179
+ <UIcon
180
+ name="i-lucide-clock"
181
+ class="w-3 h-3"
182
+ />
183
+ <span>{{ formatTimeout(flow.timeout) }} timeout</span>
184
+ </div>
185
+ <!-- Runtime Badges -->
186
+ <div
187
+ v-if="getRuntimes(flow).length > 0"
188
+ class="flex items-center gap-1"
189
+ >
190
+ <UIcon
191
+ name="i-lucide-cpu"
192
+ class="w-3 h-3"
193
+ />
194
+ <span>{{ getRuntimes(flow).join(", ") }}</span>
195
+ </div>
241
196
  </div>
242
- </span>
243
- <span
244
- v-else-if="selectedFlow"
245
- class="text-xs font-mono text-gray-500"
246
- >
247
- {{ selectedFlow }}
248
- </span>
249
- </div>
250
- </div>
251
- </div>
252
- <div class="flex-1 min-h-0">
253
- <!-- Diagram Tab -->
254
- <div
255
- v-if="mainTab === 'diagram'"
256
- class="h-full"
257
- >
258
- <div
259
- v-if="!selectedFlow"
260
- class="h-full flex items-center justify-center text-sm text-gray-400"
261
- >
262
- Select a flow to view diagram
263
- </div>
264
- <FlowDiagram
265
- v-else
266
- :flow="selectedFlowMeta"
267
- :show-controls="true"
268
- :show-background="true"
269
- :step-states="diagramStepStates"
270
- :flow-status="runSnapshot.status"
271
- height-class="h-full"
272
- @node-action="handleNodeAction"
273
- />
274
- </div>
275
-
276
- <!-- Timeline Tab -->
277
- <div
278
- v-else-if="mainTab === 'timeline'"
279
- class="h-full flex gap-px bg-gray-200 dark:bg-gray-800"
280
- >
281
- <!-- Left: Overview -->
282
- <div class="flex-1 min-w-0 max-w-[50%] bg-white dark:bg-gray-950 flex flex-col min-h-0 overflow-hidden">
283
- <div class="flex-1 overflow-y-auto min-h-0">
284
- <FlowRunOverview
285
- :run-status="runSnapshot.status"
286
- :started-at="runSnapshot.startedAt"
287
- :completed-at="runSnapshot.completedAt"
288
- :steps="flowState.stepList.value"
289
- :flow-name="selectedFlow"
290
- :run-id="selectedRunId"
291
- @select-step="handleSelectStep"
292
- @cancel-flow="handleCancelFlow"
293
- />
294
197
  </div>
295
- </div>
296
198
 
297
- <!-- Right: Combined Logs & Events -->
298
- <div class="flex-1 min-w-0 max-w-[50%] bg-white dark:bg-gray-950 flex flex-col min-h-0 overflow-hidden">
299
- <FlowRunTimeline
300
- :events="timeline"
301
- :logs="filteredLogs"
302
- :is-live="isConnected"
303
- @export="exportTimelineJson"
199
+ <!-- Right: Action -->
200
+ <UButton
201
+ icon="i-lucide-arrow-right"
202
+ size="xs"
203
+ color="neutral"
204
+ variant="ghost"
205
+ square
304
206
  />
305
207
  </div>
306
208
  </div>
307
209
  </div>
308
210
  </div>
309
- </div>
310
- </div>
311
211
 
312
- <!-- Start Flow Modal -->
313
- <UModal v-model:open="startFlowModalOpen">
314
- <template #header>
315
- <div class="flex items-center justify-between">
316
- <div>
317
- <h3 class="text-lg font-semibold">
318
- Start Flow Run
319
- </h3>
320
- <p class="text-sm text-gray-500 mt-1">
321
- {{ selectedFlow }}
322
- </p>
323
- </div>
324
- </div>
325
- </template>
326
- <template #body>
327
- <div class="space-y-4">
328
- <div>
329
- <label class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 block">
330
- Input Data (JSON)
331
- </label>
332
- <UTextarea
333
- v-model="flowInputJson"
334
- :rows="12"
335
- placeholder="{\n &quot;key&quot;: &quot;value&quot;\n}"
336
- class="w-full font-mono text-sm"
337
- />
338
- <p
339
- v-if="jsonError"
340
- class="text-xs text-red-500 mt-2"
341
- >
342
- {{ jsonError }}
343
- </p>
344
- </div>
212
+ <!-- Footer Info -->
213
+ <div
214
+ v-if="filteredFlows && filteredFlows.length > 0"
215
+ class="mt-4 text-center text-sm text-gray-500 dark:text-gray-400"
216
+ >
217
+ Showing {{ filteredFlows.length }} flow{{ filteredFlows.length === 1 ? "" : "s" }}
345
218
  </div>
346
- </template>
347
- <template #footer>
348
- <div class="flex justify-end gap-2">
349
- <UButton
350
- color="neutral"
351
- variant="ghost"
352
- @click="startFlowModalOpen = false"
353
- >
354
- Cancel
355
- </UButton>
356
- <UButton
357
- color="primary"
358
- :loading="startingFlow"
359
- :disabled="!!jsonError"
360
- @click="startFlowRun"
361
- >
362
- Start Flow
363
- </UButton>
364
- </div>
365
- </template>
366
- </UModal>
367
-
368
- <!-- Schedule Flow Modal -->
369
- <FlowScheduleDialog
370
- v-if="selectedFlow"
371
- v-model="scheduleModalOpen"
372
- :flow-name="selectedFlow"
373
- @scheduled="handleFlowScheduled"
374
- />
375
-
376
- <!-- Confirm Dialog -->
377
- <ConfirmDialog
378
- v-model:open="confirmDialogOpen"
379
- :title="confirmDialogConfig.title"
380
- :description="confirmDialogConfig.description"
381
- :items="confirmDialogConfig.items"
382
- :warning="confirmDialogConfig.warning"
383
- :loading="clearingHistory"
384
- confirm-label="Clear History"
385
- confirm-color="error"
386
- icon="i-lucide-trash-2"
387
- icon-color="error"
388
- @confirm="confirmDialogConfig.onConfirm"
389
- />
219
+ </div>
220
+ </div>
390
221
  </div>
391
222
  </template>
392
223
 
393
224
  <script setup>
394
- import { ref, computed, watch } from "#imports";
395
- import FlowDiagram from "../../components/FlowDiagram.vue";
396
- import FlowRunOverview from "../../components/FlowRunOverview.vue";
397
- import FlowRunTimeline from "../../components/FlowRunTimeline.vue";
398
- import FlowRunStatusBadge from "../../components/FlowRunStatusBadge.vue";
399
- import FlowSchedulesList from "../../components/FlowSchedulesList.vue";
400
- import FlowScheduleDialog from "../../components/FlowScheduleDialog.vue";
401
- import ConfirmDialog from "../../components/ConfirmDialog.vue";
402
- import {
403
- USelectMenu,
404
- UIcon,
405
- UButton,
406
- UModal,
407
- UTextarea,
408
- UTabs,
409
- UDropdownMenu,
410
- UAccordion
411
- } from "#components";
412
- import { useAnalyzedFlows } from "../../composables/useAnalyzedFlows";
413
- import { useFlowsNavigation } from "../../composables/useFlowsNavigation";
414
- import { useFlowRunsInfinite } from "../../composables/useFlowRunsInfinite";
415
- import { useFlowRunTimeline } from "../../composables/useFlowRunTimeline";
416
- import { useFlowRunsPolling } from "../../composables/useFlowRunsPolling";
417
- const { selectedFlow, selectedRunId } = useFlowsNavigation();
418
- const mainTab = ref("diagram");
419
- const mainTabs = computed(() => [
420
- { label: "Diagram", value: "diagram", icon: "i-lucide-git-branch" },
421
- {
422
- label: "Timeline",
423
- value: "timeline",
424
- icon: "i-lucide-activity",
425
- disabled: !selectedRunId.value
225
+ import { ref, computed, onMounted, onBeforeUnmount } from "#imports";
226
+ import { UButton, UIcon, UBadge, UInput } from "#components";
227
+ import { useComponentRouter } from "../../composables/useComponentRouter";
228
+ import { useFlowWebSocket } from "../../composables/useFlowWebSocket";
229
+ import StatCard from "../../components/StatCard.vue";
230
+ import LiveIndicator from "../../components/LiveIndicator.vue";
231
+ const router = useComponentRouter();
232
+ const flows = ref([]);
233
+ const loading = ref(true);
234
+ const flowWs = useFlowWebSocket();
235
+ async function fetchAnalyzedFlows() {
236
+ try {
237
+ const data = await $fetch("/api/_flows");
238
+ const analyzedFlows = data;
239
+ flows.value = analyzedFlows.map((flow) => ({
240
+ id: flow.id,
241
+ entry: flow.entry,
242
+ steps: flow.analyzed?.steps || flow.steps || {},
243
+ levels: flow.analyzed?.levels || [],
244
+ maxLevel: flow.analyzed?.maxLevel || 0,
245
+ stallTimeout: flow.analyzed?.stallTimeout,
246
+ awaitPatterns: flow.analyzed?.awaitPatterns,
247
+ hasAwait: flow.analyzed?.awaitPatterns?.steps?.length > 0 || false,
248
+ timeout: flow.timeout,
249
+ stats: { total: 0, success: 0, failure: 0, cancel: 0, running: 0, awaiting: 0 }
250
+ }));
251
+ } catch (err) {
252
+ console.error("Error fetching analyzed flows:", err);
426
253
  }
427
- ]);
428
- watch(selectedRunId, (newRunId, oldRunId) => {
429
- if (newRunId && newRunId !== oldRunId) {
430
- mainTab.value = "timeline";
431
- } else if (!newRunId) {
432
- mainTab.value = "diagram";
254
+ }
255
+ function updateFlowStats(data) {
256
+ const flowId = data?.id;
257
+ if (!flowId || !flows.value)
258
+ return;
259
+ const flowIndex = flows.value.findIndex((f) => f.id === flowId);
260
+ if (flowIndex === -1) {
261
+ console.warn("[Flow Stats] Flow not found in list:", flowId);
262
+ return;
433
263
  }
434
- });
435
- const flows = useAnalyzedFlows();
436
- const {
437
- items: runs,
438
- total: totalRuns,
439
- loading: loadingRuns,
440
- hasMore: hasMoreRuns,
441
- loadMore: loadMoreRuns,
442
- refresh: refreshRuns,
443
- checkForNewRuns
444
- } = useFlowRunsInfinite(selectedFlow);
445
- const { flowState, isConnected, isReconnecting } = useFlowRunTimeline(selectedFlow, selectedRunId);
446
- const shouldPoll = computed(() => !!selectedFlow.value);
447
- useFlowRunsPolling(checkForNewRuns, shouldPoll);
448
- const startFlowModalOpen = ref(false);
449
- const scheduleModalOpen = ref(false);
450
- const flowInputJson = ref("{}");
451
- const jsonError = ref("");
452
- const startingFlow = ref(false);
453
- const schedulesListRef = ref();
454
- const clearingHistory = ref(false);
455
- const scheduleAccordionItems = [
456
- {
457
- label: "Schedules",
458
- slot: "item",
459
- defaultOpen: false
264
+ const metadata = data?.metadata;
265
+ if (!metadata) {
266
+ console.warn("[Flow Stats] No metadata in update:", data);
267
+ return;
460
268
  }
461
- ];
462
- const confirmDialogOpen = ref(false);
463
- const confirmDialogConfig = ref({
464
- title: "",
465
- description: "",
466
- items: [],
467
- warning: "",
468
- onConfirm: () => {
269
+ const stats = metadata.stats || {
270
+ total: metadata["stats.total"] || 0,
271
+ success: metadata["stats.success"] || 0,
272
+ failure: metadata["stats.failure"] || 0,
273
+ cancel: metadata["stats.cancel"] || 0,
274
+ running: metadata["stats.running"] || 0,
275
+ awaiting: metadata["stats.awaiting"] || 0
276
+ };
277
+ flows.value[flowIndex] = {
278
+ ...flows.value[flowIndex],
279
+ stats: {
280
+ total: stats.total || 0,
281
+ success: stats.success || 0,
282
+ failure: stats.failure || 0,
283
+ cancel: stats.cancel || 0,
284
+ running: stats.running || 0,
285
+ awaiting: stats.awaiting || 0
286
+ },
287
+ lastRunAt: metadata.lastRunAt,
288
+ lastCompletedAt: metadata.lastCompletedAt
289
+ };
290
+ }
291
+ onMounted(async () => {
292
+ await fetchAnalyzedFlows();
293
+ if (import.meta.client) {
294
+ flowWs.subscribeStats(
295
+ {
296
+ onInitial: (data) => {
297
+ updateFlowStats(data);
298
+ },
299
+ onUpdate: (data) => {
300
+ updateFlowStats(data);
301
+ }
302
+ },
303
+ {
304
+ autoReconnect: true,
305
+ onOpen: () => {
306
+ loading.value = false;
307
+ },
308
+ onError: (err) => {
309
+ console.error("[Flow Stats] Error:", err);
310
+ }
311
+ }
312
+ );
469
313
  }
470
314
  });
471
- const flowActionsItems = computed(() => [[
472
- {
473
- label: "Schedule Flow",
474
- icon: "i-lucide-clock",
475
- onSelect: () => openScheduleModal()
476
- },
477
- {
478
- label: "Clear History",
479
- icon: "i-lucide-trash-2",
480
- disabled: clearingHistory.value,
481
- onSelect: () => confirmClearHistory()
482
- }
483
- ]]);
484
- watch(flowInputJson, (value) => {
485
- try {
486
- JSON.parse(value);
487
- jsonError.value = "";
488
- } catch (err) {
489
- jsonError.value = err instanceof Error ? err.message : "Invalid JSON";
490
- }
315
+ onBeforeUnmount(() => {
316
+ flowWs.unsubscribeStats();
317
+ flowWs.stop();
491
318
  });
492
- const runsScrollContainer = ref(null);
493
- const handleRunsScroll = (event) => {
494
- if (!hasMoreRuns.value || loadingRuns.value) return;
495
- const container = event.target;
496
- const scrollTop = container.scrollTop;
497
- const scrollHeight = container.scrollHeight;
498
- const clientHeight = container.clientHeight;
499
- if (scrollTop + clientHeight >= scrollHeight - 200) {
500
- loadMoreRuns();
501
- }
502
- };
503
- const formatTime = (timestamp) => {
504
- const date = new Date(timestamp);
505
- const now = /* @__PURE__ */ new Date();
506
- const diff = now.getTime() - date.getTime();
507
- const seconds = Math.floor(diff / 1e3);
508
- const minutes = Math.floor(seconds / 60);
509
- const hours = Math.floor(minutes / 60);
510
- const days = Math.floor(hours / 24);
511
- if (days > 0) return `${days}d ago`;
512
- if (hours > 0) return `${hours}h ago`;
513
- if (minutes > 0) return `${minutes}m ago`;
514
- if (seconds > 10) return `${seconds}s ago`;
515
- return "just now";
516
- };
517
- const formatDuration = (start, end) => {
518
- const startTime = new Date(start).getTime();
519
- const endTime = new Date(end).getTime();
520
- const diff = endTime - startTime;
521
- const seconds = Math.floor(diff / 1e3);
522
- const minutes = Math.floor(seconds / 60);
523
- const hours = Math.floor(minutes / 60);
524
- if (hours > 0) {
525
- const remainingMinutes = minutes % 60;
526
- return `${hours}h ${remainingMinutes}m`;
527
- }
528
- if (minutes > 0) {
529
- const remainingSeconds = seconds % 60;
530
- return `${minutes}m ${remainingSeconds}s`;
319
+ const searchQuery = ref("");
320
+ const filteredFlows = computed(() => {
321
+ if (!flows.value)
322
+ return [];
323
+ if (searchQuery.value) {
324
+ const query = searchQuery.value.toLowerCase();
325
+ return flows.value.filter(
326
+ (flow) => flow.id.toLowerCase().includes(query)
327
+ );
531
328
  }
532
- return `${seconds}s`;
533
- };
534
- const runSnapshot = computed(() => {
535
- const state = flowState.state.value;
536
- return {
537
- status: state.status,
538
- startedAt: state.startedAt,
539
- completedAt: state.completedAt,
540
- logsCount: state.logs.length,
541
- lastLogLevel: state.logs.length > 0 ? state.logs[state.logs.length - 1]?.level : void 0
542
- };
329
+ return flows.value;
543
330
  });
544
- const selectedStepKey = ref(null);
545
- const timeline = computed(() => {
546
- const events = flowState.events.value;
547
- if (!selectedStepKey.value) return events;
548
- return events.filter((e) => e.stepName === selectedStepKey.value);
331
+ const totalRuns = computed(() => {
332
+ if (!flows.value)
333
+ return 0;
334
+ return flows.value.reduce((acc, flow) => acc + (flow.stats?.total || 0), 0);
549
335
  });
550
- const filteredLogs = computed(() => {
551
- const logs = flowState.state.value.logs;
552
- if (!selectedStepKey.value) return logs;
553
- return logs.filter((log) => log.stepName === selectedStepKey.value);
336
+ const totalSuccess = computed(() => {
337
+ if (!flows.value)
338
+ return 0;
339
+ return flows.value.reduce((acc, flow) => acc + (flow.stats?.success || 0), 0);
554
340
  });
555
- const selectedFlowMeta = computed(() => {
556
- const id = selectedFlow.value;
557
- if (!id) return null;
558
- return (flows.value || []).find((f) => f?.id === id) || null;
341
+ const totalFailure = computed(() => {
342
+ if (!flows.value)
343
+ return 0;
344
+ return flows.value.reduce((acc, flow) => acc + (flow.stats?.failure || 0), 0);
559
345
  });
560
- const handleSelectStep = (stepKey) => {
561
- selectedStepKey.value = stepKey;
562
- };
563
- const handleCancelFlow = async () => {
564
- if (!selectedFlow.value || !selectedRunId.value) return;
565
- try {
566
- await $fetch(`/api/_flows/${selectedFlow.value}/runs/${selectedRunId.value}/cancel`, {
567
- method: "POST"
568
- });
569
- console.log("Flow canceled successfully");
570
- } catch (error) {
571
- console.error("Failed to cancel flow:", error);
572
- }
573
- };
574
- const diagramStepStates = computed(() => {
575
- if (!selectedRunId.value) return void 0;
576
- return flowState.state.value.steps;
346
+ const totalRunning = computed(() => {
347
+ if (!flows.value)
348
+ return 0;
349
+ return flows.value.reduce((acc, flow) => acc + (flow.stats?.running || 0), 0);
577
350
  });
578
- const selectRun = (runId) => {
579
- selectedRunId.value = runId;
580
- mainTab.value = "timeline";
581
- };
582
- const exportTimelineJson = () => {
583
- const blob = new Blob([JSON.stringify(flowState.events.value, null, 2)], { type: "application/json" });
584
- const url = URL.createObjectURL(blob);
585
- const a = document.createElement("a");
586
- a.href = url;
587
- a.download = `flow-${selectedFlow.value}-${selectedRunId.value}-events-${(/* @__PURE__ */ new Date()).toISOString()}.json`;
588
- document.body.appendChild(a);
589
- a.click();
590
- a.remove();
591
- URL.revokeObjectURL(url);
592
- };
593
- const handleNodeAction = async (payload) => {
594
- const _stepName = payload.id.split(":")[1];
595
- if (!selectedRunId.value) {
596
- console.log("[flows/index] No run selected, showing alert");
597
- alert("Please select a flow run first to view logs or details.");
598
- return;
599
- }
600
- mainTab.value = "timeline";
601
- };
602
- const openStartFlowModal = () => {
603
- flowInputJson.value = "{}";
604
- jsonError.value = "";
605
- startFlowModalOpen.value = true;
606
- };
607
- const openScheduleModal = () => {
608
- scheduleModalOpen.value = true;
609
- };
610
- const handleFlowScheduled = () => {
611
- schedulesListRef.value?.loadSchedules();
612
- };
613
- const handleSchedulesUpdated = () => {
614
- schedulesListRef.value?.loadSchedules();
351
+ const totalAwaiting = computed(() => {
352
+ if (!flows.value)
353
+ return 0;
354
+ return flows.value.reduce((acc, flow) => acc + (flow.stats?.awaiting || 0), 0);
355
+ });
356
+ const getStepCount = (flow) => {
357
+ if (!flow.steps) return 0;
358
+ return Object.keys(flow.steps).length;
615
359
  };
616
- const startFlowRun = async () => {
617
- if (!selectedFlow.value || jsonError.value) return;
618
- try {
619
- startingFlow.value = true;
620
- const input = JSON.parse(flowInputJson.value);
621
- const result = await $fetch(`/api/_flows/${encodeURIComponent(selectedFlow.value)}/start`, {
622
- method: "POST",
623
- body: input
624
- });
625
- startFlowModalOpen.value = false;
626
- flowInputJson.value = "{}";
627
- if (result?.flowId) {
628
- selectedRunId.value = result.flowId;
629
- mainTab.value = "timeline";
630
- }
631
- await refreshRuns();
632
- } catch (err) {
633
- console.error("Failed to start flow:", err);
634
- jsonError.value = err instanceof Error ? err.message : "Failed to start flow";
635
- } finally {
636
- startingFlow.value = false;
637
- }
360
+ const getLevelCount = (flow) => {
361
+ if (!flow.levels) return 0;
362
+ return Array.isArray(flow.levels) ? flow.levels.length : 0;
638
363
  };
639
- const confirmClearHistory = () => {
640
- if (!selectedFlow.value) return;
641
- confirmDialogConfig.value = {
642
- title: "Clear Flow History",
643
- description: `Are you sure you want to clear all history for "${selectedFlow.value}"?`,
644
- items: [
645
- "All flow run events",
646
- "All flow run logs",
647
- "The runs index"
648
- ],
649
- warning: "This action cannot be undone.",
650
- onConfirm: () => {
651
- clearFlowHistory();
364
+ const getRuntimes = (flow) => {
365
+ const runtimes = /* @__PURE__ */ new Set();
366
+ if (!flow.steps) return [];
367
+ for (const step of Object.values(flow.steps)) {
368
+ if (step.runtime) {
369
+ runtimes.add(step.runtime);
652
370
  }
653
- };
654
- confirmDialogOpen.value = true;
655
- };
656
- const clearFlowHistory = async () => {
657
- if (!selectedFlow.value) return;
658
- try {
659
- clearingHistory.value = true;
660
- await $fetch(`/api/_flows/${encodeURIComponent(selectedFlow.value)}/clear-history`, {
661
- method: "DELETE"
662
- });
663
- confirmDialogOpen.value = false;
664
- selectedRunId.value = "";
665
- mainTab.value = "diagram";
666
- await refreshRuns();
667
- console.log(`Successfully cleared history for "${selectedFlow.value}"`);
668
- } catch (err) {
669
- console.error("Failed to clear history:", err);
670
- confirmDialogConfig.value = {
671
- title: "Error Clearing History",
672
- description: `Failed to clear history: ${err instanceof Error ? err.message : "Unknown error"}`,
673
- items: [],
674
- warning: "",
675
- onConfirm: () => {
676
- confirmDialogOpen.value = false;
677
- }
678
- };
679
- } finally {
680
- clearingHistory.value = false;
681
371
  }
372
+ return Array.from(runtimes);
682
373
  };
374
+ function formatTimeout(ms) {
375
+ if (ms < 1e3)
376
+ return `${ms}ms`;
377
+ if (ms < 6e4)
378
+ return `${ms / 1e3}s`;
379
+ return `${ms / 6e4}m`;
380
+ }
381
+ function openFlow(flowId) {
382
+ router.push(`/flows/${flowId}`);
383
+ }
683
384
  </script>