@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.
Files changed (81) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +52 -0
  3. package/README.md +106 -0
  4. package/dist/client.d.ts +2 -0
  5. package/dist/client.d.ts.map +1 -0
  6. package/dist/client.js +1 -0
  7. package/dist/components/common.d.ts +30 -0
  8. package/dist/components/common.d.ts.map +1 -0
  9. package/dist/components/common.js +108 -0
  10. package/dist/components/workflow-run-actions.d.ts +7 -0
  11. package/dist/components/workflow-run-actions.d.ts.map +1 -0
  12. package/dist/components/workflow-run-actions.js +72 -0
  13. package/dist/components/workflow-run-detail-page.d.ts +10 -0
  14. package/dist/components/workflow-run-detail-page.d.ts.map +1 -0
  15. package/dist/components/workflow-run-detail-page.js +96 -0
  16. package/dist/components/workflow-runs-filters.d.ts +32 -0
  17. package/dist/components/workflow-runs-filters.d.ts.map +1 -0
  18. package/dist/components/workflow-runs-filters.js +97 -0
  19. package/dist/components/workflow-runs-page.d.ts +12 -0
  20. package/dist/components/workflow-runs-page.d.ts.map +1 -0
  21. package/dist/components/workflow-runs-page.js +132 -0
  22. package/dist/components/workflow-schedules-page.d.ts +21 -0
  23. package/dist/components/workflow-schedules-page.d.ts.map +1 -0
  24. package/dist/components/workflow-schedules-page.js +144 -0
  25. package/dist/i18n/en.d.ts +4 -0
  26. package/dist/i18n/en.d.ts.map +1 -0
  27. package/dist/i18n/en.js +135 -0
  28. package/dist/i18n/index.d.ts +5 -0
  29. package/dist/i18n/index.d.ts.map +1 -0
  30. package/dist/i18n/index.js +3 -0
  31. package/dist/i18n/messages.d.ts +125 -0
  32. package/dist/i18n/messages.d.ts.map +1 -0
  33. package/dist/i18n/messages.js +1 -0
  34. package/dist/i18n/provider.d.ts +26 -0
  35. package/dist/i18n/provider.d.ts.map +1 -0
  36. package/dist/i18n/provider.js +44 -0
  37. package/dist/i18n/ro.d.ts +4 -0
  38. package/dist/i18n/ro.d.ts.map +1 -0
  39. package/dist/i18n/ro.js +137 -0
  40. package/dist/index.d.ts +56 -0
  41. package/dist/index.d.ts.map +1 -0
  42. package/dist/index.js +21 -0
  43. package/dist/schedules-client.d.ts +2 -0
  44. package/dist/schedules-client.d.ts.map +1 -0
  45. package/dist/schedules-client.js +1 -0
  46. package/dist/types.d.ts +2 -0
  47. package/dist/types.d.ts.map +1 -0
  48. package/dist/types.js +1 -0
  49. package/dist/ui.d.ts +9 -0
  50. package/dist/ui.d.ts.map +1 -0
  51. package/dist/ui.js +6 -0
  52. package/dist/workflow-runs-client.d.ts +124 -0
  53. package/dist/workflow-runs-client.d.ts.map +1 -0
  54. package/dist/workflow-runs-client.js +138 -0
  55. package/dist/workflow-runs.d.ts +36 -0
  56. package/dist/workflow-runs.d.ts.map +1 -0
  57. package/dist/workflow-runs.js +70 -0
  58. package/dist/workflow-schedules-client.d.ts +70 -0
  59. package/dist/workflow-schedules-client.d.ts.map +1 -0
  60. package/dist/workflow-schedules-client.js +91 -0
  61. package/package.json +123 -0
  62. package/src/client.ts +4 -0
  63. package/src/components/common.tsx +182 -0
  64. package/src/components/workflow-run-actions.tsx +160 -0
  65. package/src/components/workflow-run-detail-page.tsx +393 -0
  66. package/src/components/workflow-runs-filters.tsx +349 -0
  67. package/src/components/workflow-runs-page.tsx +357 -0
  68. package/src/components/workflow-schedules-page.tsx +398 -0
  69. package/src/i18n/en.ts +142 -0
  70. package/src/i18n/index.ts +26 -0
  71. package/src/i18n/messages.ts +125 -0
  72. package/src/i18n/provider.tsx +96 -0
  73. package/src/i18n/ro.ts +144 -0
  74. package/src/index.ts +94 -0
  75. package/src/schedules-client.ts +8 -0
  76. package/src/styles.css +11 -0
  77. package/src/types.ts +14 -0
  78. package/src/ui.ts +63 -0
  79. package/src/workflow-runs-client.ts +304 -0
  80. package/src/workflow-runs.ts +120 -0
  81. 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,4 @@
1
+ export {
2
+ createWorkflowRunsApiClient,
3
+ type WorkflowRunsApiClientOptions,
4
+ } from "./workflow-runs-client.js"
@@ -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
+ }