@voyant-travel/workflows-react 0.107.10
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/NOTICE +52 -0
- package/README.md +106 -0
- package/dist/client.d.ts +2 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +1 -0
- 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 +10 -0
- package/dist/components/workflow-run-detail-page.d.ts.map +1 -0
- package/dist/components/workflow-run-detail-page.js +96 -0
- 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 +12 -0
- package/dist/components/workflow-runs-page.d.ts.map +1 -0
- package/dist/components/workflow-runs-page.js +132 -0
- package/dist/components/workflow-schedules-page.d.ts +21 -0
- package/dist/components/workflow-schedules-page.d.ts.map +1 -0
- package/dist/components/workflow-schedules-page.js +144 -0
- package/dist/i18n/en.d.ts +4 -0
- package/dist/i18n/en.d.ts.map +1 -0
- package/dist/i18n/en.js +135 -0
- package/dist/i18n/index.d.ts +5 -0
- package/dist/i18n/index.d.ts.map +1 -0
- package/dist/i18n/index.js +3 -0
- package/dist/i18n/messages.d.ts +125 -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 +4 -0
- package/dist/i18n/ro.d.ts.map +1 -0
- package/dist/i18n/ro.js +137 -0
- package/dist/index.d.ts +56 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/schedules-client.d.ts +2 -0
- package/dist/schedules-client.d.ts.map +1 -0
- package/dist/schedules-client.js +1 -0
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/ui.d.ts +9 -0
- package/dist/ui.d.ts.map +1 -0
- package/dist/ui.js +6 -0
- package/dist/workflow-runs-client.d.ts +124 -0
- package/dist/workflow-runs-client.d.ts.map +1 -0
- package/dist/workflow-runs-client.js +138 -0
- package/dist/workflow-runs.d.ts +36 -0
- package/dist/workflow-runs.d.ts.map +1 -0
- package/dist/workflow-runs.js +70 -0
- package/dist/workflow-schedules-client.d.ts +70 -0
- package/dist/workflow-schedules-client.d.ts.map +1 -0
- package/dist/workflow-schedules-client.js +91 -0
- package/package.json +123 -0
- package/src/client.ts +4 -0
- 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/components/workflow-schedules-page.tsx +398 -0
- package/src/i18n/en.ts +142 -0
- package/src/i18n/index.ts +26 -0
- package/src/i18n/messages.ts +125 -0
- package/src/i18n/provider.tsx +96 -0
- package/src/i18n/ro.ts +144 -0
- package/src/index.ts +94 -0
- package/src/schedules-client.ts +8 -0
- package/src/styles.css +11 -0
- package/src/types.ts +14 -0
- package/src/ui.ts +63 -0
- package/src/workflow-runs-client.ts +304 -0
- package/src/workflow-runs.ts +120 -0
- package/src/workflow-schedules-client.ts +177 -0
package/package.json
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@voyant-travel/workflows-react",
|
|
3
|
+
"version": "0.107.10",
|
|
4
|
+
"description": "React hooks for Voyant Workflows — trigger, subscribe, and stream runs — plus the styled workflow-run admin UI (formerly @voyant-travel/workflows-ui).",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/voyant-travel/voyant.git",
|
|
9
|
+
"directory": "packages/workflows-react"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"sideEffects": false,
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"import": "./dist/index.js",
|
|
17
|
+
"default": "./dist/index.js"
|
|
18
|
+
},
|
|
19
|
+
"./workflow-runs": {
|
|
20
|
+
"types": "./dist/workflow-runs.d.ts",
|
|
21
|
+
"import": "./dist/workflow-runs.js",
|
|
22
|
+
"default": "./dist/workflow-runs.js"
|
|
23
|
+
},
|
|
24
|
+
"./workflow-runs-client": {
|
|
25
|
+
"types": "./dist/workflow-runs-client.d.ts",
|
|
26
|
+
"import": "./dist/workflow-runs-client.js",
|
|
27
|
+
"default": "./dist/workflow-runs-client.js"
|
|
28
|
+
},
|
|
29
|
+
"./workflow-schedules-client": {
|
|
30
|
+
"types": "./dist/workflow-schedules-client.d.ts",
|
|
31
|
+
"import": "./dist/workflow-schedules-client.js",
|
|
32
|
+
"default": "./dist/workflow-schedules-client.js"
|
|
33
|
+
},
|
|
34
|
+
"./ui": {
|
|
35
|
+
"types": "./dist/ui.d.ts",
|
|
36
|
+
"import": "./dist/ui.js",
|
|
37
|
+
"default": "./dist/ui.js"
|
|
38
|
+
},
|
|
39
|
+
"./client": {
|
|
40
|
+
"types": "./dist/client.d.ts",
|
|
41
|
+
"import": "./dist/client.js",
|
|
42
|
+
"default": "./dist/client.js"
|
|
43
|
+
},
|
|
44
|
+
"./styles.css": {
|
|
45
|
+
"default": "./src/styles.css"
|
|
46
|
+
},
|
|
47
|
+
"./i18n": {
|
|
48
|
+
"types": "./dist/i18n/index.d.ts",
|
|
49
|
+
"import": "./dist/i18n/index.js",
|
|
50
|
+
"default": "./dist/i18n/index.js"
|
|
51
|
+
},
|
|
52
|
+
"./i18n/en": {
|
|
53
|
+
"types": "./dist/i18n/en.d.ts",
|
|
54
|
+
"import": "./dist/i18n/en.js",
|
|
55
|
+
"default": "./dist/i18n/en.js"
|
|
56
|
+
},
|
|
57
|
+
"./i18n/ro": {
|
|
58
|
+
"types": "./dist/i18n/ro.d.ts",
|
|
59
|
+
"import": "./dist/i18n/ro.js",
|
|
60
|
+
"default": "./dist/i18n/ro.js"
|
|
61
|
+
},
|
|
62
|
+
"./components/*": {
|
|
63
|
+
"types": "./dist/components/*.d.ts",
|
|
64
|
+
"import": "./dist/components/*.js",
|
|
65
|
+
"default": "./dist/components/*.js"
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
"dependencies": {
|
|
69
|
+
"@voyant-travel/i18n": "^0.106.1",
|
|
70
|
+
"@voyant-travel/react": "^0.104.1",
|
|
71
|
+
"@voyant-travel/workflows": "^0.107.10"
|
|
72
|
+
},
|
|
73
|
+
"peerDependencies": {
|
|
74
|
+
"@tanstack/react-query": "^5.0.0",
|
|
75
|
+
"lucide-react": "^0.475.0 || ^1.0.0",
|
|
76
|
+
"react": "^19.0.0",
|
|
77
|
+
"react-dom": "^19.0.0",
|
|
78
|
+
"@voyant-travel/ui": "^0.106.1"
|
|
79
|
+
},
|
|
80
|
+
"peerDependenciesMeta": {
|
|
81
|
+
"@voyant-travel/ui": {
|
|
82
|
+
"optional": true
|
|
83
|
+
},
|
|
84
|
+
"lucide-react": {
|
|
85
|
+
"optional": true
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
"devDependencies": {
|
|
89
|
+
"@tanstack/react-query": "^5.100.11",
|
|
90
|
+
"@types/react": "^19.2.14",
|
|
91
|
+
"@types/react-dom": "^19.2.3",
|
|
92
|
+
"lucide-react": "^1.7.0",
|
|
93
|
+
"react": "^19.2.4",
|
|
94
|
+
"react-dom": "^19.2.4",
|
|
95
|
+
"typescript": "^6.0.2",
|
|
96
|
+
"vitest": "^4.1.2",
|
|
97
|
+
"@voyant-travel/i18n": "^0.106.1",
|
|
98
|
+
"@voyant-travel/react": "^0.104.1",
|
|
99
|
+
"@voyant-travel/ui": "^0.106.1",
|
|
100
|
+
"@voyant-travel/voyant-typescript-config": "^0.1.0"
|
|
101
|
+
},
|
|
102
|
+
"files": [
|
|
103
|
+
"dist",
|
|
104
|
+
"src",
|
|
105
|
+
"!**/*.test.*",
|
|
106
|
+
"!**/*.spec.*",
|
|
107
|
+
"NOTICE",
|
|
108
|
+
"src/components",
|
|
109
|
+
"src/styles.css"
|
|
110
|
+
],
|
|
111
|
+
"publishConfig": {
|
|
112
|
+
"access": "public"
|
|
113
|
+
},
|
|
114
|
+
"scripts": {
|
|
115
|
+
"build": "tsc -p tsconfig.build.json",
|
|
116
|
+
"clean": "rm -rf dist tsconfig.tsbuildinfo tsconfig.build.tsbuildinfo",
|
|
117
|
+
"typecheck": "tsc --noEmit",
|
|
118
|
+
"lint": "biome check src/",
|
|
119
|
+
"test": "vitest run --passWithNoTests"
|
|
120
|
+
},
|
|
121
|
+
"main": "./dist/index.js",
|
|
122
|
+
"types": "./dist/index.d.ts"
|
|
123
|
+
}
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { Badge } from "@voyant-travel/ui/components/badge"
|
|
4
|
+
import { AlertCircle, CheckCircle2, Clock, Copy, RefreshCw, XCircle } from "lucide-react"
|
|
5
|
+
import { useMemo, useState } from "react"
|
|
6
|
+
|
|
7
|
+
import { useWorkflowRunsUiMessagesOrDefault, type WorkflowRunsUiMessages } from "../i18n/index.js"
|
|
8
|
+
import type { WorkflowRun, WorkflowRunStep, WorkflowRunStepStatus } from "../types.js"
|
|
9
|
+
|
|
10
|
+
export function StatusIcon({ status }: { status: WorkflowRun["status"] }) {
|
|
11
|
+
switch (status) {
|
|
12
|
+
case "succeeded":
|
|
13
|
+
return <CheckCircle2 className="h-4 w-4 text-emerald-500" />
|
|
14
|
+
case "failed":
|
|
15
|
+
return <XCircle className="h-4 w-4 text-destructive" />
|
|
16
|
+
case "cancelled":
|
|
17
|
+
return <AlertCircle className="h-4 w-4 text-amber-500" />
|
|
18
|
+
case "running":
|
|
19
|
+
return <Clock className="h-4 w-4 animate-pulse text-blue-500" />
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function StepStatusIcon({ status }: { status: WorkflowRunStepStatus }) {
|
|
24
|
+
switch (status) {
|
|
25
|
+
case "succeeded":
|
|
26
|
+
return <CheckCircle2 className="h-4 w-4 text-emerald-500" />
|
|
27
|
+
case "failed":
|
|
28
|
+
return <XCircle className="h-4 w-4 text-destructive" />
|
|
29
|
+
case "running":
|
|
30
|
+
return <Clock className="h-4 w-4 animate-pulse text-blue-500" />
|
|
31
|
+
case "skipped":
|
|
32
|
+
return <AlertCircle className="h-4 w-4 text-muted-foreground" />
|
|
33
|
+
case "compensated":
|
|
34
|
+
return <RefreshCw className="h-4 w-4 text-amber-500" />
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function StatusBadge({
|
|
39
|
+
status,
|
|
40
|
+
messages,
|
|
41
|
+
}: {
|
|
42
|
+
status: WorkflowRun["status"]
|
|
43
|
+
messages: WorkflowRunsUiMessages
|
|
44
|
+
}) {
|
|
45
|
+
useWorkflowRunsUiMessagesOrDefault()
|
|
46
|
+
const className = {
|
|
47
|
+
succeeded: "border-emerald-500/40 bg-emerald-500/10 text-emerald-600 dark:text-emerald-300", // i18n-literal-ok: CSS classes
|
|
48
|
+
failed: "border-destructive/40 bg-destructive/10 text-destructive", // i18n-literal-ok: CSS classes
|
|
49
|
+
cancelled: "border-amber-500/40 bg-amber-500/10 text-amber-600 dark:text-amber-300", // i18n-literal-ok: CSS classes
|
|
50
|
+
running: "border-blue-500/40 bg-blue-500/10 text-blue-600 dark:text-blue-300", // i18n-literal-ok: CSS classes
|
|
51
|
+
}[status]
|
|
52
|
+
return (
|
|
53
|
+
<Badge variant="outline" className={`text-[11px] ${className}`}>
|
|
54
|
+
{messages.status[status]}
|
|
55
|
+
</Badge>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function TagChip({ tag }: { tag: string }) {
|
|
60
|
+
const colonIdx = tag.indexOf(":")
|
|
61
|
+
if (colonIdx < 0) {
|
|
62
|
+
return (
|
|
63
|
+
<Badge variant="outline" className="font-mono text-[10px]">
|
|
64
|
+
{tag}
|
|
65
|
+
</Badge>
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
const key = tag.slice(0, colonIdx)
|
|
69
|
+
const value = tag.slice(colonIdx + 1)
|
|
70
|
+
return (
|
|
71
|
+
<Badge variant="outline" className="gap-1 font-mono text-[10px]" title={tag}>
|
|
72
|
+
<span className="text-muted-foreground">{key}</span>
|
|
73
|
+
<span className="max-w-[18ch] truncate">{value}</span>
|
|
74
|
+
</Badge>
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function CopyableId({
|
|
79
|
+
id,
|
|
80
|
+
copiedLabel,
|
|
81
|
+
className,
|
|
82
|
+
}: {
|
|
83
|
+
id: string
|
|
84
|
+
copiedLabel: string
|
|
85
|
+
className?: string
|
|
86
|
+
}) {
|
|
87
|
+
const [copied, setCopied] = useState(false)
|
|
88
|
+
const onCopy = async () => {
|
|
89
|
+
try {
|
|
90
|
+
await navigator.clipboard.writeText(id)
|
|
91
|
+
setCopied(true)
|
|
92
|
+
setTimeout(() => setCopied(false), 1500)
|
|
93
|
+
} catch {
|
|
94
|
+
/* clipboard blocked */
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return (
|
|
98
|
+
<button
|
|
99
|
+
type="button"
|
|
100
|
+
onClick={onCopy}
|
|
101
|
+
className={`group inline-flex items-center gap-1.5 rounded border bg-muted/40 px-2 py-1 font-mono text-[11px] hover:bg-muted ${
|
|
102
|
+
className ?? ""
|
|
103
|
+
}`}
|
|
104
|
+
title={id}
|
|
105
|
+
>
|
|
106
|
+
<span className="max-w-[16ch] truncate">{id}</span>
|
|
107
|
+
<Copy className="h-3 w-3 opacity-60 group-hover:opacity-100" />
|
|
108
|
+
{copied ? <span className="text-emerald-500 text-[10px]">{copiedLabel}</span> : null}
|
|
109
|
+
</button>
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function PayloadBlock({
|
|
114
|
+
title,
|
|
115
|
+
value,
|
|
116
|
+
messages,
|
|
117
|
+
hideTitle = false,
|
|
118
|
+
}: {
|
|
119
|
+
title: string
|
|
120
|
+
value: Record<string, unknown>
|
|
121
|
+
messages: WorkflowRunsUiMessages
|
|
122
|
+
hideTitle?: boolean
|
|
123
|
+
}) {
|
|
124
|
+
const [copied, setCopied] = useState(false)
|
|
125
|
+
const json = useMemo(() => JSON.stringify(value, null, 2), [value])
|
|
126
|
+
|
|
127
|
+
const onCopy = async () => {
|
|
128
|
+
try {
|
|
129
|
+
await navigator.clipboard.writeText(json)
|
|
130
|
+
setCopied(true)
|
|
131
|
+
setTimeout(() => setCopied(false), 1500)
|
|
132
|
+
} catch {
|
|
133
|
+
/* clipboard blocked */
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<div className="space-y-1">
|
|
139
|
+
{hideTitle ? null : (
|
|
140
|
+
<div className="flex items-center justify-between gap-2">
|
|
141
|
+
<span className="text-muted-foreground text-xs">{title}</span>
|
|
142
|
+
<button
|
|
143
|
+
type="button"
|
|
144
|
+
onClick={onCopy}
|
|
145
|
+
className="flex items-center gap-1 text-muted-foreground text-xs hover:text-foreground"
|
|
146
|
+
>
|
|
147
|
+
<Copy className="h-3 w-3" />
|
|
148
|
+
{copied ? messages.detail.copied : messages.detail.copy}
|
|
149
|
+
</button>
|
|
150
|
+
</div>
|
|
151
|
+
)}
|
|
152
|
+
<pre className="max-h-[24rem] overflow-auto rounded bg-muted/40 p-3 font-mono text-[11px] leading-relaxed">
|
|
153
|
+
{json}
|
|
154
|
+
</pre>
|
|
155
|
+
</div>
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function formatRelative(iso: string, messages: WorkflowRunsUiMessages): string {
|
|
160
|
+
const seconds = Math.round((Date.now() - new Date(iso).getTime()) / 1000)
|
|
161
|
+
if (Math.abs(seconds) < 5) return messages.format.relativeNow
|
|
162
|
+
const formatter = new Intl.RelativeTimeFormat(undefined, { numeric: "auto" })
|
|
163
|
+
if (Math.abs(seconds) < 60) return formatter.format(-seconds, "second")
|
|
164
|
+
const minutes = Math.round(seconds / 60)
|
|
165
|
+
if (Math.abs(minutes) < 60) return formatter.format(-minutes, "minute")
|
|
166
|
+
const hours = Math.round(minutes / 60)
|
|
167
|
+
if (Math.abs(hours) < 24) return formatter.format(-hours, "hour")
|
|
168
|
+
const days = Math.round(hours / 24)
|
|
169
|
+
return formatter.format(-days, "day")
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function formatDuration(ms: number): string {
|
|
173
|
+
if (ms < 1000) return `${ms}ms`
|
|
174
|
+
if (ms < 60_000) return `${(ms / 1000).toFixed(2)}s`
|
|
175
|
+
const minutes = Math.floor(ms / 60_000)
|
|
176
|
+
const seconds = Math.round((ms % 60_000) / 1000)
|
|
177
|
+
return `${minutes}m ${seconds}s`
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function runHasLiveStatus(run: WorkflowRun | WorkflowRunStep): boolean {
|
|
181
|
+
return run.status === "running"
|
|
182
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { Button } from "@voyant-travel/ui/components/button"
|
|
4
|
+
import { Card, CardContent, CardHeader, CardTitle } from "@voyant-travel/ui/components/card"
|
|
5
|
+
import { AlertTriangle, Play, RotateCw } from "lucide-react"
|
|
6
|
+
import { useCallback, useState } from "react"
|
|
7
|
+
|
|
8
|
+
import { useWorkflowRunsUiMessagesOrDefault } from "../i18n/index.js"
|
|
9
|
+
import type { WorkflowRun, WorkflowRunActionError, WorkflowRunsApi } from "../types.js"
|
|
10
|
+
import { runHasLiveStatus } from "./common.js"
|
|
11
|
+
|
|
12
|
+
export function WorkflowRunActionsCard({
|
|
13
|
+
api,
|
|
14
|
+
run,
|
|
15
|
+
onOpenRun,
|
|
16
|
+
}: {
|
|
17
|
+
api: WorkflowRunsApi
|
|
18
|
+
run: WorkflowRun
|
|
19
|
+
onOpenRun?: (id: string) => void
|
|
20
|
+
}) {
|
|
21
|
+
const messages = useWorkflowRunsUiMessagesOrDefault()
|
|
22
|
+
const [busy, setBusy] = useState<"rerun" | "resume" | null>(null)
|
|
23
|
+
const [feedback, setFeedback] = useState<{ kind: "info" | "error"; message: string } | null>(null)
|
|
24
|
+
const [confirmOpen, setConfirmOpen] = useState(false)
|
|
25
|
+
|
|
26
|
+
const explainError = useCallback(
|
|
27
|
+
(err: WorkflowRunActionError): string => {
|
|
28
|
+
if (err.error === "runner_not_registered") return messages.actions.runnerMissing
|
|
29
|
+
if (err.error === "rerun_blocked") return err.detail ?? messages.actions.rerunBlocked
|
|
30
|
+
if (err.error === "incomplete_prior_step") {
|
|
31
|
+
return err.detail ?? messages.actions.incompletePriorStep
|
|
32
|
+
}
|
|
33
|
+
return err.detail ?? err.error ?? messages.actions.actionFailed
|
|
34
|
+
},
|
|
35
|
+
[messages],
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
const doRerun = async (confirm: boolean): Promise<void> => {
|
|
39
|
+
setBusy("rerun")
|
|
40
|
+
setFeedback(null)
|
|
41
|
+
try {
|
|
42
|
+
const result = await api.rerunRun(run.id, { confirm })
|
|
43
|
+
if (result.ok) {
|
|
44
|
+
setFeedback({ kind: "info", message: messages.actions.rerunStarted })
|
|
45
|
+
onOpenRun?.(result.data.runId)
|
|
46
|
+
setConfirmOpen(false)
|
|
47
|
+
} else if (result.error.error === "confirmation_required") {
|
|
48
|
+
setConfirmOpen(true)
|
|
49
|
+
} else {
|
|
50
|
+
setFeedback({ kind: "error", message: explainError(result.error) })
|
|
51
|
+
}
|
|
52
|
+
} finally {
|
|
53
|
+
setBusy(null)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const doResume = async (): Promise<void> => {
|
|
58
|
+
setBusy("resume")
|
|
59
|
+
setFeedback(null)
|
|
60
|
+
try {
|
|
61
|
+
const result = await api.resumeRun(run.id)
|
|
62
|
+
if (result.ok) {
|
|
63
|
+
setFeedback({
|
|
64
|
+
kind: "info",
|
|
65
|
+
message: messages.actions.resumeStarted(result.data.resumeFromStep ?? ""),
|
|
66
|
+
})
|
|
67
|
+
onOpenRun?.(result.data.runId)
|
|
68
|
+
} else {
|
|
69
|
+
setFeedback({ kind: "error", message: explainError(result.error) })
|
|
70
|
+
}
|
|
71
|
+
} finally {
|
|
72
|
+
setBusy(null)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const canResume = run.status === "failed"
|
|
77
|
+
const isRunning = runHasLiveStatus(run)
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<Card>
|
|
81
|
+
<CardContent className="flex flex-wrap items-center gap-2 pt-4">
|
|
82
|
+
<Button
|
|
83
|
+
variant="default"
|
|
84
|
+
size="sm"
|
|
85
|
+
onClick={() => void doRerun(false)}
|
|
86
|
+
disabled={busy !== null || isRunning}
|
|
87
|
+
title={isRunning ? messages.actions.waitForCompletion : messages.actions.rerunDescription}
|
|
88
|
+
>
|
|
89
|
+
<RotateCw className={`mr-1.5 h-3.5 w-3.5 ${busy === "rerun" ? "animate-spin" : ""}`} />
|
|
90
|
+
{busy === "rerun" ? messages.actions.rerunBusy : messages.actions.rerun}
|
|
91
|
+
</Button>
|
|
92
|
+
<Button
|
|
93
|
+
variant="secondary"
|
|
94
|
+
size="sm"
|
|
95
|
+
onClick={() => void doResume()}
|
|
96
|
+
disabled={busy !== null || !canResume}
|
|
97
|
+
title={
|
|
98
|
+
canResume ? messages.actions.resumeDescription : messages.actions.resumeUnavailable
|
|
99
|
+
}
|
|
100
|
+
>
|
|
101
|
+
<Play className={`mr-1.5 h-3.5 w-3.5 ${busy === "resume" ? "animate-pulse" : ""}`} />
|
|
102
|
+
{busy === "resume" ? messages.actions.resumeBusy : messages.actions.resume}
|
|
103
|
+
</Button>
|
|
104
|
+
{feedback ? (
|
|
105
|
+
<span
|
|
106
|
+
className={`ml-auto text-xs ${
|
|
107
|
+
feedback.kind === "error" ? "text-destructive" : "text-muted-foreground"
|
|
108
|
+
}`}
|
|
109
|
+
>
|
|
110
|
+
{feedback.message}
|
|
111
|
+
</span>
|
|
112
|
+
) : null}
|
|
113
|
+
</CardContent>
|
|
114
|
+
{confirmOpen ? (
|
|
115
|
+
<ConfirmRerunDialog
|
|
116
|
+
onCancel={() => setConfirmOpen(false)}
|
|
117
|
+
onConfirm={() => void doRerun(true)}
|
|
118
|
+
busy={busy === "rerun"}
|
|
119
|
+
/>
|
|
120
|
+
) : null}
|
|
121
|
+
</Card>
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function ConfirmRerunDialog({
|
|
126
|
+
onCancel,
|
|
127
|
+
onConfirm,
|
|
128
|
+
busy,
|
|
129
|
+
}: {
|
|
130
|
+
onCancel: () => void
|
|
131
|
+
onConfirm: () => void
|
|
132
|
+
busy: boolean
|
|
133
|
+
}) {
|
|
134
|
+
const messages = useWorkflowRunsUiMessagesOrDefault()
|
|
135
|
+
return (
|
|
136
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4">
|
|
137
|
+
<Card className="w-full max-w-md border-amber-500/40">
|
|
138
|
+
<CardHeader className="pb-3">
|
|
139
|
+
<CardTitle className="flex items-center gap-2 text-base">
|
|
140
|
+
<AlertTriangle className="h-4 w-4 text-amber-500" />
|
|
141
|
+
{messages.actions.confirmTitle}
|
|
142
|
+
</CardTitle>
|
|
143
|
+
</CardHeader>
|
|
144
|
+
<CardContent className="space-y-3 text-sm">
|
|
145
|
+
<p>{messages.actions.confirmBody}</p>
|
|
146
|
+
<p className="text-muted-foreground text-xs">{messages.actions.confirmTip}</p>
|
|
147
|
+
<div className="flex justify-end gap-2 pt-2">
|
|
148
|
+
<Button variant="ghost" size="sm" onClick={onCancel} disabled={busy}>
|
|
149
|
+
{messages.actions.cancel}
|
|
150
|
+
</Button>
|
|
151
|
+
<Button variant="default" size="sm" onClick={onConfirm} disabled={busy}>
|
|
152
|
+
<RotateCw className={`mr-1.5 h-3.5 w-3.5 ${busy ? "animate-spin" : ""}`} />
|
|
153
|
+
{messages.actions.rerunAnyway}
|
|
154
|
+
</Button>
|
|
155
|
+
</div>
|
|
156
|
+
</CardContent>
|
|
157
|
+
</Card>
|
|
158
|
+
</div>
|
|
159
|
+
)
|
|
160
|
+
}
|