@voyantjs/workflows-ui 0.37.1 → 0.38.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 (48) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +2 -3
  3. package/dist/client.d.ts +1 -1
  4. package/dist/client.d.ts.map +1 -1
  5. package/dist/client.js +1 -1
  6. package/dist/components/common.d.ts +30 -0
  7. package/dist/components/common.d.ts.map +1 -0
  8. package/dist/components/common.js +108 -0
  9. package/dist/components/workflow-run-actions.d.ts +7 -0
  10. package/dist/components/workflow-run-actions.d.ts.map +1 -0
  11. package/dist/components/workflow-run-actions.js +72 -0
  12. package/dist/components/workflow-run-detail-page.d.ts +9 -1
  13. package/dist/components/workflow-run-detail-page.d.ts.map +1 -1
  14. package/dist/components/workflow-run-detail-page.js +96 -1
  15. package/dist/components/workflow-runs-filters.d.ts +32 -0
  16. package/dist/components/workflow-runs-filters.d.ts.map +1 -0
  17. package/dist/components/workflow-runs-filters.js +97 -0
  18. package/dist/components/workflow-runs-page.d.ts +11 -1
  19. package/dist/components/workflow-runs-page.d.ts.map +1 -1
  20. package/dist/components/workflow-runs-page.js +132 -1
  21. package/dist/i18n/en.d.ts +3 -2
  22. package/dist/i18n/en.d.ts.map +1 -1
  23. package/dist/i18n/en.js +96 -2
  24. package/dist/i18n/index.d.ts +4 -2
  25. package/dist/i18n/index.d.ts.map +1 -1
  26. package/dist/i18n/index.js +3 -2
  27. package/dist/i18n/messages.d.ts +86 -0
  28. package/dist/i18n/messages.d.ts.map +1 -0
  29. package/dist/i18n/messages.js +1 -0
  30. package/dist/i18n/provider.d.ts +26 -0
  31. package/dist/i18n/provider.d.ts.map +1 -0
  32. package/dist/i18n/provider.js +44 -0
  33. package/dist/i18n/ro.d.ts +3 -2
  34. package/dist/i18n/ro.d.ts.map +1 -1
  35. package/dist/i18n/ro.js +98 -2
  36. package/dist/index.d.ts +6 -2
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +4 -2
  39. package/dist/types.d.ts +2 -0
  40. package/dist/types.d.ts.map +1 -0
  41. package/dist/types.js +1 -0
  42. package/package.json +55 -68
  43. package/src/components/common.tsx +182 -0
  44. package/src/components/workflow-run-actions.tsx +160 -0
  45. package/src/components/workflow-run-detail-page.tsx +393 -0
  46. package/src/components/workflow-runs-filters.tsx +349 -0
  47. package/src/components/workflow-runs-page.tsx +357 -0
  48. package/src/styles.css +5 -3
@@ -0,0 +1,393 @@
1
+ "use client"
2
+
3
+ import { Badge } from "@voyantjs/ui/components/badge"
4
+ import { Card, CardContent, CardHeader, CardTitle } from "@voyantjs/ui/components/card"
5
+ import { ChevronDown, ChevronRight, Link2 } from "lucide-react"
6
+ import { useEffect, useMemo, useState } from "react"
7
+
8
+ import { useWorkflowRunsUiMessagesOrDefault } from "../i18n/index.js"
9
+ import type {
10
+ WorkflowRun,
11
+ WorkflowRunErrorPayload,
12
+ WorkflowRunStep,
13
+ WorkflowRunsApi,
14
+ } from "../types.js"
15
+ import {
16
+ CopyableId,
17
+ formatDuration,
18
+ formatRelative,
19
+ PayloadBlock,
20
+ StatusBadge,
21
+ StatusIcon,
22
+ StepStatusIcon,
23
+ TagChip,
24
+ } from "./common.js"
25
+ import { WorkflowRunActionsCard } from "./workflow-run-actions.js"
26
+
27
+ export interface WorkflowRunDetailPageProps {
28
+ api: WorkflowRunsApi
29
+ runId: string
30
+ onOpenRun?: (id: string) => void
31
+ pollIntervalMs?: number
32
+ className?: string
33
+ }
34
+
35
+ export function WorkflowRunDetailPage({
36
+ api,
37
+ runId,
38
+ onOpenRun,
39
+ pollIntervalMs = 3000,
40
+ className,
41
+ }: WorkflowRunDetailPageProps) {
42
+ const messages = useWorkflowRunsUiMessagesOrDefault()
43
+ const [run, setRun] = useState<WorkflowRun | null>(null)
44
+ const [steps, setSteps] = useState<WorkflowRunStep[]>([])
45
+ const [error, setError] = useState<string | null>(null)
46
+ const [reruns, setReruns] = useState<WorkflowRun[]>([])
47
+
48
+ useEffect(() => {
49
+ let cancelled = false
50
+ const refresh = async () => {
51
+ try {
52
+ const [detail, children] = await Promise.all([
53
+ api.getRun(runId),
54
+ api.listRuns({ parentRunId: runId, limit: 20 }).catch(() => ({ data: [] })),
55
+ ])
56
+ if (cancelled) return
57
+ setRun(detail.data.run)
58
+ setSteps(detail.data.steps)
59
+ setReruns(children.data)
60
+ setError(null)
61
+ } catch (err) {
62
+ if (!cancelled) setError(err instanceof Error ? err.message : String(err))
63
+ }
64
+ }
65
+ void refresh()
66
+ const interval = setInterval(() => void refresh(), pollIntervalMs)
67
+ return () => {
68
+ cancelled = true
69
+ clearInterval(interval)
70
+ }
71
+ }, [api, pollIntervalMs, runId])
72
+
73
+ const duplicateRunError = useMemo(() => {
74
+ if (!run?.error) return false
75
+ return steps.some((step) => step.error?.message === run.error?.message)
76
+ }, [run?.error, steps])
77
+
78
+ if (error) return <ErrorMessage message={error} />
79
+ if (!run) {
80
+ return (
81
+ <Card className={className}>
82
+ <CardContent className="pt-4 text-muted-foreground text-sm">
83
+ {messages.page.loading}
84
+ </CardContent>
85
+ </Card>
86
+ )
87
+ }
88
+
89
+ return (
90
+ <div className={`space-y-4 ${className ?? ""}`}>
91
+ <RunHeaderCard run={run} onOpenRun={onOpenRun} />
92
+ <WorkflowRunActionsCard api={api} run={run} onOpenRun={onOpenRun} />
93
+ {reruns.length > 0 ? <RerunsListCard reruns={reruns} onOpenRun={onOpenRun} /> : null}
94
+ <StepsCard steps={steps} />
95
+ {run.input ? <PayloadCard title={messages.detail.input} value={run.input} /> : null}
96
+ {run.result ? <PayloadCard title={messages.detail.result} value={run.result} /> : null}
97
+ {run.error && !duplicateRunError ? (
98
+ <ErrorCard title={messages.detail.runError} error={run.error} />
99
+ ) : null}
100
+ </div>
101
+ )
102
+ }
103
+
104
+ function RunHeaderCard({ run, onOpenRun }: { run: WorkflowRun; onOpenRun?: (id: string) => void }) {
105
+ const messages = useWorkflowRunsUiMessagesOrDefault()
106
+ return (
107
+ <Card>
108
+ <CardHeader className="space-y-3 pb-4">
109
+ <div className="flex flex-wrap items-center gap-3">
110
+ <StatusIcon status={run.status} />
111
+ <CardTitle className="font-semibold text-lg">{run.workflowName}</CardTitle>
112
+ <StatusBadge status={run.status} messages={messages} />
113
+ {run.durationMs != null ? (
114
+ <Badge variant="outline" className="font-mono text-xs">
115
+ {formatDuration(run.durationMs)}
116
+ </Badge>
117
+ ) : null}
118
+ {run.resumeFromStep ? (
119
+ <Badge variant="outline" className="font-mono text-xs">
120
+ {messages.detail.resumedAt(run.resumeFromStep)}
121
+ </Badge>
122
+ ) : null}
123
+ <CopyableId id={run.id} copiedLabel={messages.detail.copied} className="ml-auto" />
124
+ </div>
125
+ <div className="text-muted-foreground text-sm">
126
+ <span>
127
+ {messages.detail.started} {new Date(run.startedAt).toLocaleString()}
128
+ </span>
129
+ {run.completedAt ? (
130
+ <span>
131
+ {" · "}
132
+ {messages.detail.finished} {new Date(run.completedAt).toLocaleString()}
133
+ </span>
134
+ ) : null}
135
+ </div>
136
+ </CardHeader>
137
+ <CardContent className="space-y-3 pt-0">
138
+ <dl className="grid grid-cols-1 gap-x-6 gap-y-2 text-sm sm:grid-cols-2">
139
+ <DefRow label={messages.detail.trigger} value={run.trigger} mono />
140
+ {run.correlationId ? (
141
+ <DefRow label={messages.detail.correlation} value={run.correlationId} mono />
142
+ ) : null}
143
+ {run.parentRunId ? (
144
+ <LinkedRunRow
145
+ label={messages.detail.parent}
146
+ runId={run.parentRunId}
147
+ onOpenRun={onOpenRun}
148
+ />
149
+ ) : null}
150
+ {run.triggeredByUserId ? (
151
+ <DefRow label={messages.detail.triggeredBy} value={run.triggeredByUserId} mono />
152
+ ) : null}
153
+ </dl>
154
+ {run.tags.length > 0 ? (
155
+ <div>
156
+ <div className="mb-1.5 text-muted-foreground text-xs uppercase tracking-wide">
157
+ {messages.detail.tags}
158
+ </div>
159
+ <div className="flex flex-wrap gap-1">
160
+ {run.tags.map((tag) => (
161
+ <TagChip key={tag} tag={tag} />
162
+ ))}
163
+ </div>
164
+ </div>
165
+ ) : null}
166
+ </CardContent>
167
+ </Card>
168
+ )
169
+ }
170
+
171
+ function LinkedRunRow({
172
+ label,
173
+ runId,
174
+ onOpenRun,
175
+ }: {
176
+ label: string
177
+ runId: string
178
+ onOpenRun?: (id: string) => void
179
+ }) {
180
+ return (
181
+ <div className="flex items-baseline gap-2">
182
+ <dt className="shrink-0 text-muted-foreground text-xs uppercase tracking-wide">{label}</dt>
183
+ <dd className="min-w-0 truncate">
184
+ <button
185
+ type="button"
186
+ onClick={() => onOpenRun?.(runId)}
187
+ className="inline-flex items-center gap-1.5 truncate font-mono text-xs hover:underline"
188
+ title={runId}
189
+ >
190
+ <Link2 className="h-3 w-3 opacity-60" />
191
+ {runId}
192
+ </button>
193
+ </dd>
194
+ </div>
195
+ )
196
+ }
197
+
198
+ function DefRow({ label, value, mono }: { label: string; value: string; mono?: boolean }) {
199
+ return (
200
+ <div className="flex items-baseline gap-2">
201
+ <dt className="shrink-0 text-muted-foreground text-xs uppercase tracking-wide">{label}</dt>
202
+ <dd
203
+ className={
204
+ mono ? "min-w-0 truncate font-mono text-xs" : "min-w-0 truncate text-sm" // i18n-literal-ok: CSS classes
205
+ }
206
+ >
207
+ {value}
208
+ </dd>
209
+ </div>
210
+ )
211
+ }
212
+
213
+ function RerunsListCard({
214
+ reruns,
215
+ onOpenRun,
216
+ }: {
217
+ reruns: WorkflowRun[]
218
+ onOpenRun?: (id: string) => void
219
+ }) {
220
+ const messages = useWorkflowRunsUiMessagesOrDefault()
221
+ return (
222
+ <Card>
223
+ <CardHeader className="pb-3">
224
+ <CardTitle className="flex items-center gap-2 text-sm">
225
+ <Link2 className="h-3.5 w-3.5 text-muted-foreground" />
226
+ {messages.detail.reruns}
227
+ <span className="font-normal text-muted-foreground text-xs">{`(${reruns.length})`}</span>
228
+ </CardTitle>
229
+ </CardHeader>
230
+ <CardContent>
231
+ <ul className="space-y-1">
232
+ {reruns.map((run) => (
233
+ <li key={run.id}>
234
+ <button
235
+ type="button"
236
+ onClick={() => onOpenRun?.(run.id)}
237
+ className="flex w-full items-center gap-2 rounded p-2 text-left text-sm hover:bg-muted/40"
238
+ >
239
+ <StatusIcon status={run.status} />
240
+ <span className="font-mono text-xs">{run.id}</span>
241
+ <span className="text-muted-foreground text-xs">{run.trigger}</span>
242
+ {run.resumeFromStep ? (
243
+ <Badge variant="outline" className="font-mono text-[10px]">
244
+ {messages.detail.resumedAt(run.resumeFromStep)}
245
+ </Badge>
246
+ ) : null}
247
+ <span className="ml-auto whitespace-nowrap text-muted-foreground text-xs">
248
+ {formatRelative(run.startedAt, messages)}
249
+ </span>
250
+ </button>
251
+ </li>
252
+ ))}
253
+ </ul>
254
+ </CardContent>
255
+ </Card>
256
+ )
257
+ }
258
+
259
+ function StepsCard({ steps }: { steps: WorkflowRunStep[] }) {
260
+ const messages = useWorkflowRunsUiMessagesOrDefault()
261
+ return (
262
+ <Card>
263
+ <CardHeader className="pb-3">
264
+ <CardTitle className="text-sm">{messages.detail.steps}</CardTitle>
265
+ </CardHeader>
266
+ <CardContent>
267
+ {steps.length === 0 ? (
268
+ <p className="text-muted-foreground text-sm">{messages.detail.noSteps}</p>
269
+ ) : (
270
+ <ol className="space-y-2">
271
+ {steps.map((step) => (
272
+ <StepRow key={step.id} step={step} />
273
+ ))}
274
+ </ol>
275
+ )}
276
+ </CardContent>
277
+ </Card>
278
+ )
279
+ }
280
+
281
+ function StepRow({ step }: { step: WorkflowRunStep }) {
282
+ const messages = useWorkflowRunsUiMessagesOrDefault()
283
+ const [open, setOpen] = useState(step.status === "failed")
284
+ const hasDetail = step.output !== null || step.error !== null
285
+ return (
286
+ <li className="rounded-md border">
287
+ <button
288
+ type="button"
289
+ className="flex w-full items-center gap-2 p-3 text-left text-sm hover:bg-muted/30"
290
+ onClick={() => hasDetail && setOpen((prev) => !prev)}
291
+ disabled={!hasDetail}
292
+ >
293
+ <StepStatusIcon status={step.status} />
294
+ <span className="font-medium">{`${step.sequence}. ${step.stepName}`}</span>
295
+ {step.error ? (
296
+ <span className="truncate text-destructive text-xs">{step.error.message}</span>
297
+ ) : null}
298
+ <span className="ml-auto whitespace-nowrap text-muted-foreground text-xs">
299
+ {step.durationMs != null
300
+ ? formatDuration(step.durationMs)
301
+ : messages.detail.durationUnavailable}
302
+ </span>
303
+ {hasDetail ? (
304
+ open ? (
305
+ <ChevronDown className="h-3.5 w-3.5 text-muted-foreground" />
306
+ ) : (
307
+ <ChevronRight className="h-3.5 w-3.5 text-muted-foreground" />
308
+ )
309
+ ) : null}
310
+ </button>
311
+ {open && hasDetail ? (
312
+ <div className="space-y-3 border-t p-3">
313
+ {step.error ? <ErrorBlock error={step.error} /> : null}
314
+ {step.output ? (
315
+ <PayloadBlock title={messages.detail.output} value={step.output} messages={messages} />
316
+ ) : null}
317
+ </div>
318
+ ) : null}
319
+ </li>
320
+ )
321
+ }
322
+
323
+ function PayloadCard({ title, value }: { title: string; value: Record<string, unknown> }) {
324
+ const messages = useWorkflowRunsUiMessagesOrDefault()
325
+ return (
326
+ <Card>
327
+ <CardHeader className="pb-3">
328
+ <CardTitle className="text-sm">{title}</CardTitle>
329
+ </CardHeader>
330
+ <CardContent>
331
+ <PayloadBlock title={title} value={value} messages={messages} hideTitle />
332
+ </CardContent>
333
+ </Card>
334
+ )
335
+ }
336
+
337
+ function ErrorCard({ title, error }: { title: string; error: WorkflowRunErrorPayload }) {
338
+ return (
339
+ <Card className="border-destructive/40">
340
+ <CardHeader className="pb-3">
341
+ <CardTitle className="text-destructive text-sm">{title}</CardTitle>
342
+ </CardHeader>
343
+ <CardContent>
344
+ <ErrorBlock error={error} />
345
+ </CardContent>
346
+ </Card>
347
+ )
348
+ }
349
+
350
+ function ErrorBlock({ error }: { error: WorkflowRunErrorPayload }) {
351
+ const messages = useWorkflowRunsUiMessagesOrDefault()
352
+ const [stackOpen, setStackOpen] = useState(false)
353
+ return (
354
+ <div className="space-y-2">
355
+ <div className="rounded border border-destructive/30 bg-destructive/5 p-3 text-sm">
356
+ <div className="font-medium text-destructive">{error.message}</div>
357
+ {error.code || error.stepName ? (
358
+ <div className="mt-1 flex flex-wrap gap-2 text-muted-foreground text-xs">
359
+ {error.code ? <span>{`${messages.detail.code} ${error.code}`}</span> : null}
360
+ {error.stepName ? <span>{`${messages.detail.step} ${error.stepName}`}</span> : null}
361
+ </div>
362
+ ) : null}
363
+ </div>
364
+ {error.stack ? (
365
+ <details
366
+ open={stackOpen}
367
+ onToggle={(event) => setStackOpen((event.target as HTMLDetailsElement).open)}
368
+ className="rounded border bg-muted/30"
369
+ >
370
+ <summary className="flex cursor-pointer select-none items-center gap-1.5 px-3 py-2 text-muted-foreground text-xs hover:text-foreground">
371
+ {stackOpen ? (
372
+ <ChevronDown className="h-3.5 w-3.5" />
373
+ ) : (
374
+ <ChevronRight className="h-3.5 w-3.5" />
375
+ )}
376
+ {messages.detail.stackTrace}
377
+ </summary>
378
+ <pre className="overflow-x-auto whitespace-pre p-3 pt-0 font-mono text-[11px] leading-relaxed text-muted-foreground">
379
+ {error.stack}
380
+ </pre>
381
+ </details>
382
+ ) : null}
383
+ </div>
384
+ )
385
+ }
386
+
387
+ function ErrorMessage({ message }: { message: string }) {
388
+ return (
389
+ <Card className="border-destructive/40">
390
+ <CardContent className="pt-4 text-destructive text-sm">{message}</CardContent>
391
+ </Card>
392
+ )
393
+ }