@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.
- package/LICENSE +201 -0
- package/README.md +2 -3
- package/dist/client.d.ts +1 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +1 -1
- package/dist/components/common.d.ts +30 -0
- package/dist/components/common.d.ts.map +1 -0
- package/dist/components/common.js +108 -0
- package/dist/components/workflow-run-actions.d.ts +7 -0
- package/dist/components/workflow-run-actions.d.ts.map +1 -0
- package/dist/components/workflow-run-actions.js +72 -0
- package/dist/components/workflow-run-detail-page.d.ts +9 -1
- package/dist/components/workflow-run-detail-page.d.ts.map +1 -1
- package/dist/components/workflow-run-detail-page.js +96 -1
- package/dist/components/workflow-runs-filters.d.ts +32 -0
- package/dist/components/workflow-runs-filters.d.ts.map +1 -0
- package/dist/components/workflow-runs-filters.js +97 -0
- package/dist/components/workflow-runs-page.d.ts +11 -1
- package/dist/components/workflow-runs-page.d.ts.map +1 -1
- package/dist/components/workflow-runs-page.js +132 -1
- package/dist/i18n/en.d.ts +3 -2
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +96 -2
- package/dist/i18n/index.d.ts +4 -2
- package/dist/i18n/index.d.ts.map +1 -1
- package/dist/i18n/index.js +3 -2
- package/dist/i18n/messages.d.ts +86 -0
- package/dist/i18n/messages.d.ts.map +1 -0
- package/dist/i18n/messages.js +1 -0
- package/dist/i18n/provider.d.ts +26 -0
- package/dist/i18n/provider.d.ts.map +1 -0
- package/dist/i18n/provider.js +44 -0
- package/dist/i18n/ro.d.ts +3 -2
- package/dist/i18n/ro.d.ts.map +1 -1
- package/dist/i18n/ro.js +98 -2
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/package.json +55 -68
- package/src/components/common.tsx +182 -0
- package/src/components/workflow-run-actions.tsx +160 -0
- package/src/components/workflow-run-detail-page.tsx +393 -0
- package/src/components/workflow-runs-filters.tsx +349 -0
- package/src/components/workflow-runs-page.tsx +357 -0
- 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
|
+
}
|