@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
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
export interface WorkflowRunErrorPayload {
|
|
2
|
+
message: string
|
|
3
|
+
code?: string
|
|
4
|
+
stepName?: string
|
|
5
|
+
stack?: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type WorkflowRunStatus = "running" | "succeeded" | "failed" | "cancelled"
|
|
9
|
+
|
|
10
|
+
export interface WorkflowRun {
|
|
11
|
+
id: string
|
|
12
|
+
workflowName: string
|
|
13
|
+
trigger: string
|
|
14
|
+
correlationId: string | null
|
|
15
|
+
tags: string[]
|
|
16
|
+
status: WorkflowRunStatus
|
|
17
|
+
input: Record<string, unknown> | null
|
|
18
|
+
result: Record<string, unknown> | null
|
|
19
|
+
error: WorkflowRunErrorPayload | null
|
|
20
|
+
parentRunId: string | null
|
|
21
|
+
triggeredByUserId: string | null
|
|
22
|
+
resumeFromStep: string | null
|
|
23
|
+
startedAt: string
|
|
24
|
+
completedAt: string | null
|
|
25
|
+
durationMs: number | null
|
|
26
|
+
createdAt: string
|
|
27
|
+
updatedAt: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type WorkflowRunStepStatus = "running" | "succeeded" | "failed" | "skipped" | "compensated"
|
|
31
|
+
|
|
32
|
+
export interface WorkflowRunStep {
|
|
33
|
+
id: string
|
|
34
|
+
runId: string
|
|
35
|
+
stepName: string
|
|
36
|
+
sequence: number
|
|
37
|
+
status: WorkflowRunStepStatus
|
|
38
|
+
output: Record<string, unknown> | null
|
|
39
|
+
error: WorkflowRunErrorPayload | null
|
|
40
|
+
startedAt: string
|
|
41
|
+
completedAt: string | null
|
|
42
|
+
durationMs: number | null
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface ListWorkflowRunsQuery {
|
|
46
|
+
workflowName?: string
|
|
47
|
+
status?: WorkflowRunStatus
|
|
48
|
+
tag?: string
|
|
49
|
+
parentRunId?: string
|
|
50
|
+
limit?: number
|
|
51
|
+
offset?: number
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface ListWorkflowRunsResponse {
|
|
55
|
+
data: WorkflowRun[]
|
|
56
|
+
total: number
|
|
57
|
+
limit: number
|
|
58
|
+
offset: number
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface WorkflowRunDetailResponse {
|
|
62
|
+
data: { run: WorkflowRun; steps: WorkflowRunStep[] }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface WorkflowRunActionResponse {
|
|
66
|
+
data: {
|
|
67
|
+
runId: string
|
|
68
|
+
parentRunId: string
|
|
69
|
+
resumeFromStep?: string
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface WorkflowRunActionError {
|
|
74
|
+
error: string
|
|
75
|
+
detail?: string
|
|
76
|
+
idempotency?: "safe" | "unsafe" | "resume-only"
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export type WorkflowRunActionResult =
|
|
80
|
+
| { ok: true; data: WorkflowRunActionResponse["data"] }
|
|
81
|
+
| { ok: false; error: WorkflowRunActionError }
|
|
82
|
+
|
|
83
|
+
export interface WorkflowRunsApi {
|
|
84
|
+
listRuns(query: ListWorkflowRunsQuery): Promise<ListWorkflowRunsResponse>
|
|
85
|
+
getRun(id: string): Promise<WorkflowRunDetailResponse>
|
|
86
|
+
rerunRun(id: string, opts?: { confirm?: boolean }): Promise<WorkflowRunActionResult>
|
|
87
|
+
resumeRun(id: string): Promise<WorkflowRunActionResult>
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export type WorkflowRunsFetcher = (input: string, init?: RequestInit) => Promise<Response>
|
|
91
|
+
|
|
92
|
+
export interface WorkflowRunsApiClientOptions {
|
|
93
|
+
apiBase?: string
|
|
94
|
+
baseUrl?: string
|
|
95
|
+
fetcher?: WorkflowRunsFetcher
|
|
96
|
+
credentials?: RequestCredentials
|
|
97
|
+
headers?: HeadersInit
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface WorkflowRunsClientOptions {
|
|
101
|
+
baseUrl: string
|
|
102
|
+
fetcher: WorkflowRunsFetcher
|
|
103
|
+
credentials?: RequestCredentials
|
|
104
|
+
headers?: HeadersInit
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export class WorkflowRunsApiError extends Error {
|
|
108
|
+
readonly status: number
|
|
109
|
+
readonly body: unknown
|
|
110
|
+
|
|
111
|
+
constructor(message: string, status: number, body: unknown) {
|
|
112
|
+
super(message)
|
|
113
|
+
this.name = "WorkflowRunsApiError"
|
|
114
|
+
this.status = status
|
|
115
|
+
this.body = body
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export const defaultWorkflowRunsFetcher: WorkflowRunsFetcher = (input, init) => fetch(input, init)
|
|
120
|
+
|
|
121
|
+
export const workflowRunsQueryKeys = {
|
|
122
|
+
all: ["voyant", "workflow-runs"] as const,
|
|
123
|
+
lists: () => [...workflowRunsQueryKeys.all, "list"] as const,
|
|
124
|
+
list: (query: ListWorkflowRunsQuery) => [...workflowRunsQueryKeys.lists(), query] as const,
|
|
125
|
+
details: () => [...workflowRunsQueryKeys.all, "detail"] as const,
|
|
126
|
+
detail: (id: string) => [...workflowRunsQueryKeys.details(), id] as const,
|
|
127
|
+
} as const
|
|
128
|
+
|
|
129
|
+
export function createWorkflowRunsClientOptions(
|
|
130
|
+
client?: Partial<WorkflowRunsClientOptions>,
|
|
131
|
+
): WorkflowRunsClientOptions {
|
|
132
|
+
return {
|
|
133
|
+
baseUrl: client?.baseUrl ?? "",
|
|
134
|
+
fetcher: client?.fetcher ?? defaultWorkflowRunsFetcher,
|
|
135
|
+
credentials: client?.credentials,
|
|
136
|
+
headers: client?.headers,
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function createWorkflowRunsApiClient(
|
|
141
|
+
options: WorkflowRunsApiClientOptions = {},
|
|
142
|
+
): WorkflowRunsApi {
|
|
143
|
+
const client = createWorkflowRunsClientOptions({
|
|
144
|
+
baseUrl: options.baseUrl ?? options.apiBase ?? "",
|
|
145
|
+
fetcher: options.fetcher,
|
|
146
|
+
credentials: options.credentials ?? "include",
|
|
147
|
+
headers: options.headers,
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
listRuns: (query) => listWorkflowRuns(query, client),
|
|
152
|
+
getRun: (id) => getWorkflowRun(id, client),
|
|
153
|
+
rerunRun: (id, opts) => rerunWorkflowRun({ id, confirm: opts?.confirm }, client),
|
|
154
|
+
resumeRun: (id) => resumeWorkflowRun(id, client),
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export async function listWorkflowRuns(
|
|
159
|
+
query: ListWorkflowRunsQuery = {},
|
|
160
|
+
client: WorkflowRunsClientOptions = createWorkflowRunsClientOptions(),
|
|
161
|
+
): Promise<ListWorkflowRunsResponse> {
|
|
162
|
+
return fetchWorkflowRunsJson(
|
|
163
|
+
`/v1/admin/workflow-runs${buildWorkflowRunsSearch(query)}`,
|
|
164
|
+
{ method: "GET" },
|
|
165
|
+
client,
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export async function getWorkflowRun(
|
|
170
|
+
id: string,
|
|
171
|
+
client: WorkflowRunsClientOptions = createWorkflowRunsClientOptions(),
|
|
172
|
+
): Promise<WorkflowRunDetailResponse> {
|
|
173
|
+
return fetchWorkflowRunsJson(
|
|
174
|
+
`/v1/admin/workflow-runs/${encodeURIComponent(id)}`,
|
|
175
|
+
{ method: "GET" },
|
|
176
|
+
client,
|
|
177
|
+
)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export async function rerunWorkflowRun(
|
|
181
|
+
input: { id: string; confirm?: boolean },
|
|
182
|
+
client: WorkflowRunsClientOptions = createWorkflowRunsClientOptions(),
|
|
183
|
+
): Promise<WorkflowRunActionResult> {
|
|
184
|
+
return runWorkflowRunAction(
|
|
185
|
+
await requestWorkflowRuns(
|
|
186
|
+
`/v1/admin/workflow-runs/${encodeURIComponent(input.id)}/rerun`,
|
|
187
|
+
{
|
|
188
|
+
method: "POST",
|
|
189
|
+
body: JSON.stringify({ confirm: input.confirm ?? false }),
|
|
190
|
+
},
|
|
191
|
+
client,
|
|
192
|
+
),
|
|
193
|
+
)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export async function resumeWorkflowRun(
|
|
197
|
+
id: string,
|
|
198
|
+
client: WorkflowRunsClientOptions = createWorkflowRunsClientOptions(),
|
|
199
|
+
): Promise<WorkflowRunActionResult> {
|
|
200
|
+
return runWorkflowRunAction(
|
|
201
|
+
await requestWorkflowRuns(
|
|
202
|
+
`/v1/admin/workflow-runs/${encodeURIComponent(id)}/resume`,
|
|
203
|
+
{ method: "POST" },
|
|
204
|
+
client,
|
|
205
|
+
),
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function workflowRunIsTerminal(status: WorkflowRunStatus): boolean {
|
|
210
|
+
return status !== "running"
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function fetchWorkflowRunsJson<T>(
|
|
214
|
+
path: string,
|
|
215
|
+
init: RequestInit,
|
|
216
|
+
client: WorkflowRunsClientOptions,
|
|
217
|
+
): Promise<T> {
|
|
218
|
+
const response = await requestWorkflowRuns(path, init, client)
|
|
219
|
+
const body = await safeJson(response)
|
|
220
|
+
if (!response.ok) {
|
|
221
|
+
throw new WorkflowRunsApiError(
|
|
222
|
+
extractErrorMessage(response.status, response.statusText, body),
|
|
223
|
+
response.status,
|
|
224
|
+
body,
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
return body as T
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async function requestWorkflowRuns(
|
|
231
|
+
path: string,
|
|
232
|
+
init: RequestInit,
|
|
233
|
+
client: WorkflowRunsClientOptions,
|
|
234
|
+
): Promise<Response> {
|
|
235
|
+
const headers = new Headers(client.headers)
|
|
236
|
+
for (const [key, value] of new Headers(init.headers).entries()) {
|
|
237
|
+
headers.set(key, value)
|
|
238
|
+
}
|
|
239
|
+
if (init.body !== undefined && !headers.has("Content-Type")) {
|
|
240
|
+
headers.set("Content-Type", "application/json")
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const requestInit: RequestInit = { ...init, headers }
|
|
244
|
+
if (client.credentials !== undefined) {
|
|
245
|
+
requestInit.credentials = client.credentials
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return client.fetcher(joinUrl(client.baseUrl, path), requestInit)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function runWorkflowRunAction(res: Response): Promise<WorkflowRunActionResult> {
|
|
252
|
+
const body = await safeJson(res)
|
|
253
|
+
if (res.ok) {
|
|
254
|
+
return { ok: true, data: (body as WorkflowRunActionResponse).data }
|
|
255
|
+
}
|
|
256
|
+
return { ok: false, error: body as WorkflowRunActionError }
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function buildWorkflowRunsSearch(query: ListWorkflowRunsQuery): string {
|
|
260
|
+
const params = new URLSearchParams()
|
|
261
|
+
if (query.workflowName) params.set("workflowName", query.workflowName)
|
|
262
|
+
if (query.status) params.set("status", query.status)
|
|
263
|
+
if (query.tag) params.set("tag", query.tag)
|
|
264
|
+
if (query.parentRunId) params.set("parentRunId", query.parentRunId)
|
|
265
|
+
if (query.limit !== undefined) params.set("limit", String(query.limit))
|
|
266
|
+
if (query.offset !== undefined) params.set("offset", String(query.offset))
|
|
267
|
+
const search = params.toString()
|
|
268
|
+
return search ? `?${search}` : ""
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async function safeJson(response: Response): Promise<unknown> {
|
|
272
|
+
const text = await response.text()
|
|
273
|
+
if (!text) return undefined
|
|
274
|
+
try {
|
|
275
|
+
return JSON.parse(text)
|
|
276
|
+
} catch {
|
|
277
|
+
return text
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function extractErrorMessage(status: number, statusText: string, body: unknown): string {
|
|
282
|
+
if (typeof body === "object" && body !== null) {
|
|
283
|
+
if ("error" in body && typeof body.error === "string") return body.error
|
|
284
|
+
if ("detail" in body && typeof body.detail === "string") return body.detail
|
|
285
|
+
}
|
|
286
|
+
return `Workflow runs API error: ${status} ${statusText}`
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function joinUrl(baseUrl: string, path: string): string {
|
|
290
|
+
const resolvedBaseUrl = resolveBaseUrl(baseUrl)
|
|
291
|
+
const trimmedBase = resolvedBaseUrl.endsWith("/") ? resolvedBaseUrl.slice(0, -1) : resolvedBaseUrl
|
|
292
|
+
const trimmedPath = path.startsWith("/") ? path : `/${path}`
|
|
293
|
+
return `${trimmedBase}${trimmedPath}`
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function resolveBaseUrl(baseUrl: string): string {
|
|
297
|
+
if (baseUrl.trim()) return baseUrl
|
|
298
|
+
|
|
299
|
+
if (typeof window !== "undefined") {
|
|
300
|
+
return `${window.location.origin}/api`
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return "http://localhost:3300/api"
|
|
304
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { queryOptions, useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
|
4
|
+
import {
|
|
5
|
+
defaultFetcher,
|
|
6
|
+
useVoyantReactContext,
|
|
7
|
+
type VoyantFetcher,
|
|
8
|
+
type VoyantReactContextValue,
|
|
9
|
+
VoyantReactProvider,
|
|
10
|
+
type VoyantReactProviderProps,
|
|
11
|
+
} from "@voyant-travel/react"
|
|
12
|
+
import {
|
|
13
|
+
createWorkflowRunsClientOptions,
|
|
14
|
+
getWorkflowRun,
|
|
15
|
+
type ListWorkflowRunsQuery,
|
|
16
|
+
listWorkflowRuns,
|
|
17
|
+
rerunWorkflowRun,
|
|
18
|
+
resumeWorkflowRun,
|
|
19
|
+
type WorkflowRunsClientOptions,
|
|
20
|
+
workflowRunIsTerminal,
|
|
21
|
+
workflowRunsQueryKeys,
|
|
22
|
+
} from "./workflow-runs-client.js"
|
|
23
|
+
|
|
24
|
+
export * from "./workflow-runs-client.js"
|
|
25
|
+
export {
|
|
26
|
+
defaultFetcher,
|
|
27
|
+
useVoyantReactContext as useVoyantWorkflowsContext,
|
|
28
|
+
type VoyantFetcher,
|
|
29
|
+
type VoyantReactContextValue as VoyantWorkflowsContextValue,
|
|
30
|
+
VoyantReactProvider as VoyantWorkflowsProvider,
|
|
31
|
+
type VoyantReactProviderProps as VoyantWorkflowsProviderProps,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function getWorkflowRunsQueryOptions(
|
|
35
|
+
query: ListWorkflowRunsQuery = {},
|
|
36
|
+
client: WorkflowRunsClientOptions = createWorkflowRunsClientOptions(),
|
|
37
|
+
options: { pollIntervalMs?: number } = {},
|
|
38
|
+
) {
|
|
39
|
+
const pollIntervalMs = options.pollIntervalMs ?? 5000
|
|
40
|
+
|
|
41
|
+
return queryOptions({
|
|
42
|
+
queryKey: workflowRunsQueryKeys.list(query),
|
|
43
|
+
queryFn: () => listWorkflowRuns(query, client),
|
|
44
|
+
refetchInterval: (queryState) => {
|
|
45
|
+
const data = queryState.state.data
|
|
46
|
+
return data?.data.some((run) => !workflowRunIsTerminal(run.status)) ? pollIntervalMs : false
|
|
47
|
+
},
|
|
48
|
+
refetchIntervalInBackground: false,
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function getWorkflowRunQueryOptions(
|
|
53
|
+
id: string | null | undefined,
|
|
54
|
+
client: WorkflowRunsClientOptions = createWorkflowRunsClientOptions(),
|
|
55
|
+
options: { pollIntervalMs?: number } = {},
|
|
56
|
+
) {
|
|
57
|
+
const pollIntervalMs = options.pollIntervalMs ?? 2000
|
|
58
|
+
const runId = id ?? ""
|
|
59
|
+
|
|
60
|
+
return queryOptions({
|
|
61
|
+
queryKey: workflowRunsQueryKeys.detail(runId),
|
|
62
|
+
queryFn: () => getWorkflowRun(runId, client),
|
|
63
|
+
enabled: Boolean(runId),
|
|
64
|
+
refetchInterval: (queryState) => {
|
|
65
|
+
const run = queryState.state.data?.data.run
|
|
66
|
+
return run && !workflowRunIsTerminal(run.status) ? pollIntervalMs : false
|
|
67
|
+
},
|
|
68
|
+
refetchIntervalInBackground: false,
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function useWorkflowRuns(
|
|
73
|
+
query: ListWorkflowRunsQuery = {},
|
|
74
|
+
options: { pollIntervalMs?: number } = {},
|
|
75
|
+
) {
|
|
76
|
+
const client = useVoyantReactContext()
|
|
77
|
+
return useQuery(getWorkflowRunsQueryOptions(query, client, options))
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function useWorkflowRun(
|
|
81
|
+
id: string | null | undefined,
|
|
82
|
+
options: { pollIntervalMs?: number } = {},
|
|
83
|
+
) {
|
|
84
|
+
const client = useVoyantReactContext()
|
|
85
|
+
return useQuery(getWorkflowRunQueryOptions(id, client, options))
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function useRerunMutation() {
|
|
89
|
+
const queryClient = useQueryClient()
|
|
90
|
+
const client = useVoyantReactContext()
|
|
91
|
+
|
|
92
|
+
return useMutation({
|
|
93
|
+
mutationFn: (input: { id: string; confirm?: boolean }) => rerunWorkflowRun(input, client),
|
|
94
|
+
onSuccess: (result) => {
|
|
95
|
+
void queryClient.invalidateQueries({ queryKey: workflowRunsQueryKeys.all })
|
|
96
|
+
if (result.ok) {
|
|
97
|
+
void queryClient.invalidateQueries({
|
|
98
|
+
queryKey: workflowRunsQueryKeys.detail(result.data.parentRunId),
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function useResumeMutation() {
|
|
106
|
+
const queryClient = useQueryClient()
|
|
107
|
+
const client = useVoyantReactContext()
|
|
108
|
+
|
|
109
|
+
return useMutation({
|
|
110
|
+
mutationFn: (id: string) => resumeWorkflowRun(id, client),
|
|
111
|
+
onSuccess: (result) => {
|
|
112
|
+
void queryClient.invalidateQueries({ queryKey: workflowRunsQueryKeys.all })
|
|
113
|
+
if (result.ok) {
|
|
114
|
+
void queryClient.invalidateQueries({
|
|
115
|
+
queryKey: workflowRunsQueryKeys.detail(result.data.parentRunId),
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
})
|
|
120
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// Client for the orchestrator's `/api/schedules/:env` aggregate view.
|
|
2
|
+
// Mirrors `workflow-runs-client.ts` so the workflow-schedules UI can be
|
|
3
|
+
// pointed at any worker that exposes `@voyant-travel/workflows-orchestrator-cloudflare`'s
|
|
4
|
+
// schedules handler.
|
|
5
|
+
|
|
6
|
+
export interface WorkflowScheduleDecl {
|
|
7
|
+
cron?: string
|
|
8
|
+
every?: string | number
|
|
9
|
+
at?: string
|
|
10
|
+
timezone?: string
|
|
11
|
+
enabled?: boolean
|
|
12
|
+
overlap?: "skip" | "queue" | "allow"
|
|
13
|
+
environments?: ("production" | "preview" | "development")[]
|
|
14
|
+
name?: string
|
|
15
|
+
input?: unknown
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface WorkflowScheduleSummary {
|
|
19
|
+
workflowId: string
|
|
20
|
+
scheduleId: string
|
|
21
|
+
schedule: WorkflowScheduleDecl
|
|
22
|
+
/** Epoch millis of the next computed fire, or null when undecidable. */
|
|
23
|
+
nextRunAt: number | null
|
|
24
|
+
enabled: boolean
|
|
25
|
+
disabledReason?: "registration_disabled" | "env_filtered"
|
|
26
|
+
/** Epoch millis of the last scheduler dispatch attempt, when known. */
|
|
27
|
+
lastFireAt?: number | null
|
|
28
|
+
/** Run id produced by the last scheduler dispatch attempt, when known. */
|
|
29
|
+
lastRunId?: string | null
|
|
30
|
+
/** Last scheduler dispatch/lock error, when known. */
|
|
31
|
+
lastError?: string | null
|
|
32
|
+
/** Epoch millis until which this schedule is locked, when known. */
|
|
33
|
+
lockedUntil?: number | null
|
|
34
|
+
/** Epoch millis of the last successful scheduled run, when known. */
|
|
35
|
+
lastSuccessfulRunAt?: number | null
|
|
36
|
+
/** Epoch millis when the persisted scheduler state was last updated. */
|
|
37
|
+
stateUpdatedAt?: number | null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface ListWorkflowSchedulesResponse {
|
|
41
|
+
environment: string
|
|
42
|
+
versionId: string
|
|
43
|
+
schedulesEnabledByEnv?: boolean
|
|
44
|
+
data: WorkflowScheduleSummary[]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface WorkflowSchedulesApi {
|
|
48
|
+
listSchedules(environment: string): Promise<ListWorkflowSchedulesResponse>
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export type WorkflowSchedulesFetcher = (input: string, init?: RequestInit) => Promise<Response>
|
|
52
|
+
|
|
53
|
+
export interface WorkflowSchedulesApiClientOptions {
|
|
54
|
+
/** Base URL for the orchestrator. Defaults to the current origin. */
|
|
55
|
+
apiBase?: string
|
|
56
|
+
baseUrl?: string
|
|
57
|
+
fetcher?: WorkflowSchedulesFetcher
|
|
58
|
+
credentials?: RequestCredentials
|
|
59
|
+
headers?: HeadersInit
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface WorkflowSchedulesClientOptions {
|
|
63
|
+
baseUrl: string
|
|
64
|
+
fetcher: WorkflowSchedulesFetcher
|
|
65
|
+
credentials?: RequestCredentials
|
|
66
|
+
headers?: HeadersInit
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export class WorkflowSchedulesApiError extends Error {
|
|
70
|
+
readonly status: number
|
|
71
|
+
readonly body: unknown
|
|
72
|
+
|
|
73
|
+
constructor(message: string, status: number, body: unknown) {
|
|
74
|
+
super(message)
|
|
75
|
+
this.name = "WorkflowSchedulesApiError"
|
|
76
|
+
this.status = status
|
|
77
|
+
this.body = body
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export const defaultWorkflowSchedulesFetcher: WorkflowSchedulesFetcher = (input, init) =>
|
|
82
|
+
fetch(input, init)
|
|
83
|
+
|
|
84
|
+
export const workflowSchedulesQueryKeys = {
|
|
85
|
+
all: ["voyant", "workflow-schedules"] as const,
|
|
86
|
+
list: (environment: string) => [...workflowSchedulesQueryKeys.all, "list", environment] as const,
|
|
87
|
+
} as const
|
|
88
|
+
|
|
89
|
+
export function createWorkflowSchedulesClientOptions(
|
|
90
|
+
client?: Partial<WorkflowSchedulesClientOptions>,
|
|
91
|
+
): WorkflowSchedulesClientOptions {
|
|
92
|
+
return {
|
|
93
|
+
baseUrl: client?.baseUrl ?? "",
|
|
94
|
+
fetcher: client?.fetcher ?? defaultWorkflowSchedulesFetcher,
|
|
95
|
+
credentials: client?.credentials,
|
|
96
|
+
headers: client?.headers,
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function createWorkflowSchedulesApiClient(
|
|
101
|
+
options: WorkflowSchedulesApiClientOptions = {},
|
|
102
|
+
): WorkflowSchedulesApi {
|
|
103
|
+
const client = createWorkflowSchedulesClientOptions({
|
|
104
|
+
baseUrl: options.baseUrl ?? options.apiBase ?? "",
|
|
105
|
+
fetcher: options.fetcher,
|
|
106
|
+
credentials: options.credentials ?? "include",
|
|
107
|
+
headers: options.headers,
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
listSchedules: (environment) => listWorkflowSchedules(environment, client),
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export async function listWorkflowSchedules(
|
|
116
|
+
environment: string,
|
|
117
|
+
client: WorkflowSchedulesClientOptions = createWorkflowSchedulesClientOptions(),
|
|
118
|
+
): Promise<ListWorkflowSchedulesResponse> {
|
|
119
|
+
const path = `/api/schedules/${encodeURIComponent(environment)}`
|
|
120
|
+
const response = await request(path, { method: "GET" }, client)
|
|
121
|
+
const body = await safeJson(response)
|
|
122
|
+
if (!response.ok) {
|
|
123
|
+
throw new WorkflowSchedulesApiError(
|
|
124
|
+
extractErrorMessage(response.status, response.statusText, body),
|
|
125
|
+
response.status,
|
|
126
|
+
body,
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
return body as ListWorkflowSchedulesResponse
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function request(
|
|
133
|
+
path: string,
|
|
134
|
+
init: RequestInit,
|
|
135
|
+
client: WorkflowSchedulesClientOptions,
|
|
136
|
+
): Promise<Response> {
|
|
137
|
+
const headers = new Headers(client.headers)
|
|
138
|
+
for (const [key, value] of new Headers(init.headers).entries()) {
|
|
139
|
+
headers.set(key, value)
|
|
140
|
+
}
|
|
141
|
+
const requestInit: RequestInit = { ...init, headers }
|
|
142
|
+
if (client.credentials !== undefined) {
|
|
143
|
+
requestInit.credentials = client.credentials
|
|
144
|
+
}
|
|
145
|
+
return client.fetcher(joinUrl(client.baseUrl, path), requestInit)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function safeJson(response: Response): Promise<unknown> {
|
|
149
|
+
const text = await response.text()
|
|
150
|
+
if (!text) return undefined
|
|
151
|
+
try {
|
|
152
|
+
return JSON.parse(text)
|
|
153
|
+
} catch {
|
|
154
|
+
return text
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function extractErrorMessage(status: number, statusText: string, body: unknown): string {
|
|
159
|
+
if (typeof body === "object" && body !== null) {
|
|
160
|
+
if ("error" in body && typeof body.error === "string") return body.error
|
|
161
|
+
if ("message" in body && typeof body.message === "string") return body.message
|
|
162
|
+
}
|
|
163
|
+
return `Workflow schedules API error: ${status} ${statusText}`
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function joinUrl(baseUrl: string, path: string): string {
|
|
167
|
+
const resolved = resolveBaseUrl(baseUrl)
|
|
168
|
+
const trimmedBase = resolved.endsWith("/") ? resolved.slice(0, -1) : resolved
|
|
169
|
+
const trimmedPath = path.startsWith("/") ? path : `/${path}`
|
|
170
|
+
return `${trimmedBase}${trimmedPath}`
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function resolveBaseUrl(baseUrl: string): string {
|
|
174
|
+
if (baseUrl.trim()) return baseUrl
|
|
175
|
+
if (typeof window !== "undefined") return window.location.origin
|
|
176
|
+
return "http://localhost:8787"
|
|
177
|
+
}
|