@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,96 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Badge } from "@voyant-travel/ui/components/badge";
|
|
4
|
+
import { Card, CardContent, CardHeader, CardTitle } from "@voyant-travel/ui/components/card";
|
|
5
|
+
import { ChevronDown, ChevronRight, Link2 } from "lucide-react";
|
|
6
|
+
import { useEffect, useMemo, useState } from "react";
|
|
7
|
+
import { useWorkflowRunsUiMessagesOrDefault } from "../i18n/index.js";
|
|
8
|
+
import { CopyableId, formatDuration, formatRelative, PayloadBlock, StatusBadge, StatusIcon, StepStatusIcon, TagChip, } from "./common.js";
|
|
9
|
+
import { WorkflowRunActionsCard } from "./workflow-run-actions.js";
|
|
10
|
+
export function WorkflowRunDetailPage({ api, runId, onOpenRun, pollIntervalMs = 3000, className, }) {
|
|
11
|
+
const messages = useWorkflowRunsUiMessagesOrDefault();
|
|
12
|
+
const [run, setRun] = useState(null);
|
|
13
|
+
const [steps, setSteps] = useState([]);
|
|
14
|
+
const [error, setError] = useState(null);
|
|
15
|
+
const [reruns, setReruns] = useState([]);
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
let cancelled = false;
|
|
18
|
+
const refresh = async () => {
|
|
19
|
+
try {
|
|
20
|
+
const [detail, children] = await Promise.all([
|
|
21
|
+
api.getRun(runId),
|
|
22
|
+
api.listRuns({ parentRunId: runId, limit: 20 }).catch(() => ({ data: [] })),
|
|
23
|
+
]);
|
|
24
|
+
if (cancelled)
|
|
25
|
+
return;
|
|
26
|
+
setRun(detail.data.run);
|
|
27
|
+
setSteps(detail.data.steps);
|
|
28
|
+
setReruns(children.data);
|
|
29
|
+
setError(null);
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
if (!cancelled)
|
|
33
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
void refresh();
|
|
37
|
+
const interval = setInterval(() => void refresh(), pollIntervalMs);
|
|
38
|
+
return () => {
|
|
39
|
+
cancelled = true;
|
|
40
|
+
clearInterval(interval);
|
|
41
|
+
};
|
|
42
|
+
}, [api, pollIntervalMs, runId]);
|
|
43
|
+
const duplicateRunError = useMemo(() => {
|
|
44
|
+
if (!run?.error)
|
|
45
|
+
return false;
|
|
46
|
+
return steps.some((step) => step.error?.message === run.error?.message);
|
|
47
|
+
}, [run?.error, steps]);
|
|
48
|
+
if (error)
|
|
49
|
+
return _jsx(ErrorMessage, { message: error });
|
|
50
|
+
if (!run) {
|
|
51
|
+
return (_jsx(Card, { className: className, children: _jsx(CardContent, { className: "pt-4 text-muted-foreground text-sm", children: messages.page.loading }) }));
|
|
52
|
+
}
|
|
53
|
+
return (_jsxs("div", { className: `space-y-4 ${className ?? ""}`, children: [_jsx(RunHeaderCard, { run: run, onOpenRun: onOpenRun }), _jsx(WorkflowRunActionsCard, { api: api, run: run, onOpenRun: onOpenRun }), reruns.length > 0 ? _jsx(RerunsListCard, { reruns: reruns, onOpenRun: onOpenRun }) : null, _jsx(StepsCard, { steps: steps }), run.input ? _jsx(PayloadCard, { title: messages.detail.input, value: run.input }) : null, run.result ? _jsx(PayloadCard, { title: messages.detail.result, value: run.result }) : null, run.error && !duplicateRunError ? (_jsx(ErrorCard, { title: messages.detail.runError, error: run.error })) : null] }));
|
|
54
|
+
}
|
|
55
|
+
function RunHeaderCard({ run, onOpenRun }) {
|
|
56
|
+
const messages = useWorkflowRunsUiMessagesOrDefault();
|
|
57
|
+
return (_jsxs(Card, { children: [_jsxs(CardHeader, { className: "space-y-3 pb-4", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-3", children: [_jsx(StatusIcon, { status: run.status }), _jsx(CardTitle, { className: "font-semibold text-lg", children: run.workflowName }), _jsx(StatusBadge, { status: run.status, messages: messages }), run.durationMs != null ? (_jsx(Badge, { variant: "outline", className: "font-mono text-xs", children: formatDuration(run.durationMs) })) : null, run.resumeFromStep ? (_jsx(Badge, { variant: "outline", className: "font-mono text-xs", children: messages.detail.resumedAt(run.resumeFromStep) })) : null, _jsx(CopyableId, { id: run.id, copiedLabel: messages.detail.copied, className: "ml-auto" })] }), _jsxs("div", { className: "text-muted-foreground text-sm", children: [_jsxs("span", { children: [messages.detail.started, " ", new Date(run.startedAt).toLocaleString()] }), run.completedAt ? (_jsxs("span", { children: [" · ", messages.detail.finished, " ", new Date(run.completedAt).toLocaleString()] })) : null] })] }), _jsxs(CardContent, { className: "space-y-3 pt-0", children: [_jsxs("dl", { className: "grid grid-cols-1 gap-x-6 gap-y-2 text-sm sm:grid-cols-2", children: [_jsx(DefRow, { label: messages.detail.trigger, value: run.trigger, mono: true }), run.correlationId ? (_jsx(DefRow, { label: messages.detail.correlation, value: run.correlationId, mono: true })) : null, run.parentRunId ? (_jsx(LinkedRunRow, { label: messages.detail.parent, runId: run.parentRunId, onOpenRun: onOpenRun })) : null, run.triggeredByUserId ? (_jsx(DefRow, { label: messages.detail.triggeredBy, value: run.triggeredByUserId, mono: true })) : null] }), run.tags.length > 0 ? (_jsxs("div", { children: [_jsx("div", { className: "mb-1.5 text-muted-foreground text-xs uppercase tracking-wide", children: messages.detail.tags }), _jsx("div", { className: "flex flex-wrap gap-1", children: run.tags.map((tag) => (_jsx(TagChip, { tag: tag }, tag))) })] })) : null] })] }));
|
|
58
|
+
}
|
|
59
|
+
function LinkedRunRow({ label, runId, onOpenRun, }) {
|
|
60
|
+
return (_jsxs("div", { className: "flex items-baseline gap-2", children: [_jsx("dt", { className: "shrink-0 text-muted-foreground text-xs uppercase tracking-wide", children: label }), _jsx("dd", { className: "min-w-0 truncate", children: _jsxs("button", { type: "button", onClick: () => onOpenRun?.(runId), className: "inline-flex items-center gap-1.5 truncate font-mono text-xs hover:underline", title: runId, children: [_jsx(Link2, { className: "h-3 w-3 opacity-60" }), runId] }) })] }));
|
|
61
|
+
}
|
|
62
|
+
function DefRow({ label, value, mono }) {
|
|
63
|
+
return (_jsxs("div", { className: "flex items-baseline gap-2", children: [_jsx("dt", { className: "shrink-0 text-muted-foreground text-xs uppercase tracking-wide", children: label }), _jsx("dd", { className: mono ? "min-w-0 truncate font-mono text-xs" : "min-w-0 truncate text-sm" // i18n-literal-ok: CSS classes
|
|
64
|
+
, children: value })] }));
|
|
65
|
+
}
|
|
66
|
+
function RerunsListCard({ reruns, onOpenRun, }) {
|
|
67
|
+
const messages = useWorkflowRunsUiMessagesOrDefault();
|
|
68
|
+
return (_jsxs(Card, { children: [_jsx(CardHeader, { className: "pb-3", children: _jsxs(CardTitle, { className: "flex items-center gap-2 text-sm", children: [_jsx(Link2, { className: "h-3.5 w-3.5 text-muted-foreground" }), messages.detail.reruns, _jsx("span", { className: "font-normal text-muted-foreground text-xs", children: `(${reruns.length})` })] }) }), _jsx(CardContent, { children: _jsx("ul", { className: "space-y-1", children: reruns.map((run) => (_jsx("li", { children: _jsxs("button", { type: "button", onClick: () => onOpenRun?.(run.id), className: "flex w-full items-center gap-2 rounded p-2 text-left text-sm hover:bg-muted/40", children: [_jsx(StatusIcon, { status: run.status }), _jsx("span", { className: "font-mono text-xs", children: run.id }), _jsx("span", { className: "text-muted-foreground text-xs", children: run.trigger }), run.resumeFromStep ? (_jsx(Badge, { variant: "outline", className: "font-mono text-[10px]", children: messages.detail.resumedAt(run.resumeFromStep) })) : null, _jsx("span", { className: "ml-auto whitespace-nowrap text-muted-foreground text-xs", children: formatRelative(run.startedAt, messages) })] }) }, run.id))) }) })] }));
|
|
69
|
+
}
|
|
70
|
+
function StepsCard({ steps }) {
|
|
71
|
+
const messages = useWorkflowRunsUiMessagesOrDefault();
|
|
72
|
+
return (_jsxs(Card, { children: [_jsx(CardHeader, { className: "pb-3", children: _jsx(CardTitle, { className: "text-sm", children: messages.detail.steps }) }), _jsx(CardContent, { children: steps.length === 0 ? (_jsx("p", { className: "text-muted-foreground text-sm", children: messages.detail.noSteps })) : (_jsx("ol", { className: "space-y-2", children: steps.map((step) => (_jsx(StepRow, { step: step }, step.id))) })) })] }));
|
|
73
|
+
}
|
|
74
|
+
function StepRow({ step }) {
|
|
75
|
+
const messages = useWorkflowRunsUiMessagesOrDefault();
|
|
76
|
+
const [open, setOpen] = useState(step.status === "failed");
|
|
77
|
+
const hasDetail = step.output !== null || step.error !== null;
|
|
78
|
+
return (_jsxs("li", { className: "rounded-md border", children: [_jsxs("button", { type: "button", className: "flex w-full items-center gap-2 p-3 text-left text-sm hover:bg-muted/30", onClick: () => hasDetail && setOpen((prev) => !prev), disabled: !hasDetail, children: [_jsx(StepStatusIcon, { status: step.status }), _jsx("span", { className: "font-medium", children: `${step.sequence}. ${step.stepName}` }), step.error ? (_jsx("span", { className: "truncate text-destructive text-xs", children: step.error.message })) : null, _jsx("span", { className: "ml-auto whitespace-nowrap text-muted-foreground text-xs", children: step.durationMs != null
|
|
79
|
+
? formatDuration(step.durationMs)
|
|
80
|
+
: messages.detail.durationUnavailable }), hasDetail ? (open ? (_jsx(ChevronDown, { className: "h-3.5 w-3.5 text-muted-foreground" })) : (_jsx(ChevronRight, { className: "h-3.5 w-3.5 text-muted-foreground" }))) : null] }), open && hasDetail ? (_jsxs("div", { className: "space-y-3 border-t p-3", children: [step.error ? _jsx(ErrorBlock, { error: step.error }) : null, step.output ? (_jsx(PayloadBlock, { title: messages.detail.output, value: step.output, messages: messages })) : null] })) : null] }));
|
|
81
|
+
}
|
|
82
|
+
function PayloadCard({ title, value }) {
|
|
83
|
+
const messages = useWorkflowRunsUiMessagesOrDefault();
|
|
84
|
+
return (_jsxs(Card, { children: [_jsx(CardHeader, { className: "pb-3", children: _jsx(CardTitle, { className: "text-sm", children: title }) }), _jsx(CardContent, { children: _jsx(PayloadBlock, { title: title, value: value, messages: messages, hideTitle: true }) })] }));
|
|
85
|
+
}
|
|
86
|
+
function ErrorCard({ title, error }) {
|
|
87
|
+
return (_jsxs(Card, { className: "border-destructive/40", children: [_jsx(CardHeader, { className: "pb-3", children: _jsx(CardTitle, { className: "text-destructive text-sm", children: title }) }), _jsx(CardContent, { children: _jsx(ErrorBlock, { error: error }) })] }));
|
|
88
|
+
}
|
|
89
|
+
function ErrorBlock({ error }) {
|
|
90
|
+
const messages = useWorkflowRunsUiMessagesOrDefault();
|
|
91
|
+
const [stackOpen, setStackOpen] = useState(false);
|
|
92
|
+
return (_jsxs("div", { className: "space-y-2", children: [_jsxs("div", { className: "rounded border border-destructive/30 bg-destructive/5 p-3 text-sm", children: [_jsx("div", { className: "font-medium text-destructive", children: error.message }), error.code || error.stepName ? (_jsxs("div", { className: "mt-1 flex flex-wrap gap-2 text-muted-foreground text-xs", children: [error.code ? _jsx("span", { children: `${messages.detail.code} ${error.code}` }) : null, error.stepName ? _jsx("span", { children: `${messages.detail.step} ${error.stepName}` }) : null] })) : null] }), error.stack ? (_jsxs("details", { open: stackOpen, onToggle: (event) => setStackOpen(event.target.open), className: "rounded border bg-muted/30", children: [_jsxs("summary", { className: "flex cursor-pointer select-none items-center gap-1.5 px-3 py-2 text-muted-foreground text-xs hover:text-foreground", children: [stackOpen ? (_jsx(ChevronDown, { className: "h-3.5 w-3.5" })) : (_jsx(ChevronRight, { className: "h-3.5 w-3.5" })), messages.detail.stackTrace] }), _jsx("pre", { className: "overflow-x-auto whitespace-pre p-3 pt-0 font-mono text-[11px] leading-relaxed text-muted-foreground", children: error.stack })] })) : null] }));
|
|
93
|
+
}
|
|
94
|
+
function ErrorMessage({ message }) {
|
|
95
|
+
return (_jsx(Card, { className: "border-destructive/40", children: _jsx(CardContent, { className: "pt-4 text-destructive text-sm", children: message }) }));
|
|
96
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ListWorkflowRunsQuery, WorkflowRun, WorkflowRunStatus } from "../types.js";
|
|
2
|
+
export declare const STATUS_OPTIONS: WorkflowRunStatus[];
|
|
3
|
+
export declare const TIME_RANGES: readonly ["15m", "1h", "24h", "7d", "all"];
|
|
4
|
+
export type TimeRange = (typeof TIME_RANGES)[number];
|
|
5
|
+
export type WorkflowOption = {
|
|
6
|
+
name: string;
|
|
7
|
+
count: number;
|
|
8
|
+
};
|
|
9
|
+
export declare function WorkflowRunsFilters({ filters, workflowOptions, tagOptions, statusFilters, tagFilters, searchQuery, timeRange, onChange, onToggleStatus, onAddTagFilter, onRemoveTagFilter, onSearchChange, onTimeRangeChange, onClear, }: {
|
|
10
|
+
filters: ListWorkflowRunsQuery;
|
|
11
|
+
workflowOptions: WorkflowOption[];
|
|
12
|
+
tagOptions: string[];
|
|
13
|
+
statusFilters: WorkflowRunStatus[];
|
|
14
|
+
tagFilters: string[];
|
|
15
|
+
searchQuery: string;
|
|
16
|
+
timeRange: TimeRange;
|
|
17
|
+
onChange: (next: ListWorkflowRunsQuery) => void;
|
|
18
|
+
onToggleStatus: (status: WorkflowRunStatus) => void;
|
|
19
|
+
onAddTagFilter: (tag: string) => void;
|
|
20
|
+
onRemoveTagFilter: (tag: string) => void;
|
|
21
|
+
onSearchChange: (value: string) => void;
|
|
22
|
+
onTimeRangeChange: (value: TimeRange) => void;
|
|
23
|
+
onClear: () => void;
|
|
24
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
25
|
+
export declare function buildFilterOptions(runs: WorkflowRun[], selectedWorkflow?: string): {
|
|
26
|
+
workflows: {
|
|
27
|
+
name: string;
|
|
28
|
+
count: number;
|
|
29
|
+
}[];
|
|
30
|
+
tags: string[];
|
|
31
|
+
};
|
|
32
|
+
//# sourceMappingURL=workflow-runs-filters.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workflow-runs-filters.d.ts","sourceRoot":"","sources":["../../src/components/workflow-runs-filters.tsx"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,qBAAqB,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAGxF,eAAO,MAAM,cAAc,EAAE,iBAAiB,EAAoD,CAAA;AAClG,eAAO,MAAM,WAAW,4CAA6C,CAAA;AAErE,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,CAAC,CAAA;AAEpD,MAAM,MAAM,cAAc,GAAG;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;CACd,CAAA;AAED,wBAAgB,mBAAmB,CAAC,EAClC,OAAO,EACP,eAAe,EACf,UAAU,EACV,aAAa,EACb,UAAU,EACV,WAAW,EACX,SAAS,EACT,QAAQ,EACR,cAAc,EACd,cAAc,EACd,iBAAiB,EACjB,cAAc,EACd,iBAAiB,EACjB,OAAO,GACR,EAAE;IACD,OAAO,EAAE,qBAAqB,CAAA;IAC9B,eAAe,EAAE,cAAc,EAAE,CAAA;IACjC,UAAU,EAAE,MAAM,EAAE,CAAA;IACpB,aAAa,EAAE,iBAAiB,EAAE,CAAA;IAClC,UAAU,EAAE,MAAM,EAAE,CAAA;IACpB,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,SAAS,CAAA;IACpB,QAAQ,EAAE,CAAC,IAAI,EAAE,qBAAqB,KAAK,IAAI,CAAA;IAC/C,cAAc,EAAE,CAAC,MAAM,EAAE,iBAAiB,KAAK,IAAI,CAAA;IACnD,cAAc,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;IACrC,iBAAiB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;IACxC,cAAc,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACvC,iBAAiB,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,CAAA;IAC7C,OAAO,EAAE,MAAM,IAAI,CAAA;CACpB,2CA0GA;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,WAAW,EAAE,EAAE,gBAAgB,CAAC,EAAE,MAAM;;;;;;EAkBhF"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Button } from "@voyant-travel/ui/components/button";
|
|
4
|
+
import { Card, CardContent, CardHeader, CardTitle } from "@voyant-travel/ui/components/card";
|
|
5
|
+
import { Combobox, ComboboxCollection, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList, } from "@voyant-travel/ui/components/combobox";
|
|
6
|
+
import { Input } from "@voyant-travel/ui/components/input";
|
|
7
|
+
import { CheckCircle2, Clock, Search, X, XCircle } from "lucide-react";
|
|
8
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
9
|
+
import { useWorkflowRunsUiMessagesOrDefault } from "../i18n/index.js";
|
|
10
|
+
import { TagChip } from "./common.js";
|
|
11
|
+
export const STATUS_OPTIONS = ["running", "failed", "succeeded", "cancelled"];
|
|
12
|
+
export const TIME_RANGES = ["15m", "1h", "24h", "7d", "all"];
|
|
13
|
+
export function WorkflowRunsFilters({ filters, workflowOptions, tagOptions, statusFilters, tagFilters, searchQuery, timeRange, onChange, onToggleStatus, onAddTagFilter, onRemoveTagFilter, onSearchChange, onTimeRangeChange, onClear, }) {
|
|
14
|
+
const messages = useWorkflowRunsUiMessagesOrDefault();
|
|
15
|
+
const searchInputRef = useRef(null);
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
const onKeyDown = (event) => {
|
|
18
|
+
if (!(event.metaKey || event.ctrlKey) || event.key.toLowerCase() !== "k")
|
|
19
|
+
return;
|
|
20
|
+
event.preventDefault();
|
|
21
|
+
searchInputRef.current?.focus();
|
|
22
|
+
};
|
|
23
|
+
globalThis.addEventListener("keydown", onKeyDown);
|
|
24
|
+
return () => globalThis.removeEventListener("keydown", onKeyDown);
|
|
25
|
+
}, []);
|
|
26
|
+
return (_jsxs(Card, { children: [_jsx(CardHeader, { className: "pb-3", children: _jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsx(CardTitle, { className: "text-sm", children: messages.page.filterTitle }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: onClear, children: messages.page.clearFilters })] }) }), _jsxs(CardContent, { className: "space-y-4", children: [_jsx(Field, { label: messages.page.searchLabel, children: _jsxs("div", { className: "relative", children: [_jsx(Search, { className: "absolute top-1/2 left-2.5 h-4 w-4 -translate-y-1/2 text-muted-foreground" }), _jsx(Input, { ref: searchInputRef, className: "pl-8", placeholder: messages.page.searchPlaceholder, value: searchQuery, onChange: (event) => onSearchChange(event.target.value) })] }) }), _jsx(Field, { label: messages.page.workflowLabel, children: _jsx(WorkflowCombobox, { value: filters.workflowName ?? null, options: workflowOptions, placeholder: messages.page.workflowPlaceholder, emptyLabel: messages.page.workflowEmpty, onChange: (workflowName) => onChange({ ...filters, workflowName: workflowName ?? undefined }) }) }), _jsx(Field, { label: messages.page.statusLabel, children: _jsxs("div", { className: "flex flex-wrap gap-1.5", children: [_jsx(Button, { type: "button", variant: statusFilters.length === 0 ? "default" : "outline", size: "sm", onClick: () => {
|
|
27
|
+
for (const status of statusFilters)
|
|
28
|
+
onToggleStatus(status);
|
|
29
|
+
}, children: messages.page.anyStatus }), STATUS_OPTIONS.map((status) => (_jsxs(Button, { type: "button", variant: statusFilters.includes(status) ? "default" : "outline", size: "sm", onClick: () => onToggleStatus(status), "aria-pressed": statusFilters.includes(status), children: [_jsx(StatusGlyph, { status: status }), messages.status[status]] }, status)))] }) }), _jsx(Field, { label: messages.page.timeRangeLabel, children: _jsx("div", { className: "flex flex-wrap gap-1.5", children: TIME_RANGES.map((range) => (_jsx(Button, { type: "button", variant: timeRange === range ? "default" : "outline", size: "sm", onClick: () => onTimeRangeChange(range), "aria-pressed": timeRange === range, children: messages.page.timeRanges[range] }, range))) }) }), _jsx(Field, { label: messages.page.tagLabel, children: _jsx(TagFilterBuilder, { tagOptions: tagOptions, tagFilters: tagFilters, placeholder: messages.page.tagPlaceholder, emptyLabel: messages.page.tagEmpty, addLabel: messages.page.addTag, removeLabel: messages.page.removeTag, onAdd: onAddTagFilter, onRemove: onRemoveTagFilter }) })] })] }));
|
|
30
|
+
}
|
|
31
|
+
export function buildFilterOptions(runs, selectedWorkflow) {
|
|
32
|
+
const workflowCounts = new Map();
|
|
33
|
+
const tags = new Set();
|
|
34
|
+
for (const run of runs) {
|
|
35
|
+
workflowCounts.set(run.workflowName, (workflowCounts.get(run.workflowName) ?? 0) + 1);
|
|
36
|
+
for (const tag of run.tags)
|
|
37
|
+
tags.add(tag);
|
|
38
|
+
}
|
|
39
|
+
if (selectedWorkflow && !workflowCounts.has(selectedWorkflow)) {
|
|
40
|
+
workflowCounts.set(selectedWorkflow, 0);
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
workflows: Array.from(workflowCounts.entries())
|
|
44
|
+
.map(([name, count]) => ({ name, count }))
|
|
45
|
+
.sort((a, b) => b.count - a.count || a.name.localeCompare(b.name)),
|
|
46
|
+
tags: Array.from(tags).sort((a, b) => a.localeCompare(b)),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function WorkflowCombobox({ value, options, placeholder, emptyLabel, onChange, }) {
|
|
50
|
+
const itemMap = useMemo(() => new Map(options.map((item) => [item.name, item])), [options]);
|
|
51
|
+
const selectedLabel = value ?? "";
|
|
52
|
+
const [inputValue, setInputValue] = useState(selectedLabel);
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
setInputValue(selectedLabel);
|
|
55
|
+
}, [selectedLabel]);
|
|
56
|
+
return (_jsxs(Combobox, { items: options.map((item) => item.name), value: value, inputValue: inputValue, autoHighlight: true, itemToStringValue: (item) => String(item), onInputValueChange: (next) => {
|
|
57
|
+
setInputValue(next);
|
|
58
|
+
if (!next)
|
|
59
|
+
onChange(null);
|
|
60
|
+
}, onValueChange: (next) => {
|
|
61
|
+
const nextValue = next ?? null;
|
|
62
|
+
onChange(nextValue);
|
|
63
|
+
setInputValue(nextValue ?? "");
|
|
64
|
+
}, children: [_jsx(ComboboxInput, { placeholder: placeholder, showClear: !!value, className: "w-full" }), _jsxs(ComboboxContent, { children: [_jsx(ComboboxEmpty, { children: emptyLabel }), _jsx(ComboboxList, { children: _jsx(ComboboxCollection, { children: (name) => {
|
|
65
|
+
const option = itemMap.get(String(name));
|
|
66
|
+
if (!option)
|
|
67
|
+
return null;
|
|
68
|
+
return (_jsx(ComboboxItem, { value: option.name, children: _jsxs("div", { className: "flex min-w-0 flex-1 items-center justify-between gap-3", children: [_jsx("span", { className: "truncate font-medium", children: option.name }), _jsx("span", { className: "text-muted-foreground text-xs", children: option.count })] }) }, option.name));
|
|
69
|
+
} }) })] })] }));
|
|
70
|
+
}
|
|
71
|
+
function TagFilterBuilder({ tagOptions, tagFilters, placeholder, emptyLabel, addLabel, removeLabel, onAdd, onRemove, }) {
|
|
72
|
+
const [inputValue, setInputValue] = useState("");
|
|
73
|
+
const submit = () => {
|
|
74
|
+
const next = inputValue.trim();
|
|
75
|
+
if (!next)
|
|
76
|
+
return;
|
|
77
|
+
onAdd(next);
|
|
78
|
+
setInputValue("");
|
|
79
|
+
};
|
|
80
|
+
return (_jsxs("div", { className: "space-y-2", children: [_jsxs("div", { className: "grid grid-cols-[1fr_auto] gap-2", children: [_jsxs(Combobox, { items: tagOptions, value: null, inputValue: inputValue, autoHighlight: true, itemToStringValue: (item) => String(item), onInputValueChange: setInputValue, onValueChange: (next) => {
|
|
81
|
+
const tag = next ?? "";
|
|
82
|
+
if (tag) {
|
|
83
|
+
onAdd(tag);
|
|
84
|
+
setInputValue("");
|
|
85
|
+
}
|
|
86
|
+
}, children: [_jsx(ComboboxInput, { placeholder: placeholder, className: "w-full" }), _jsxs(ComboboxContent, { children: [_jsx(ComboboxEmpty, { children: emptyLabel }), _jsx(ComboboxList, { children: _jsx(ComboboxCollection, { children: (tag) => (_jsx(ComboboxItem, { value: String(tag), children: _jsx(TagChip, { tag: String(tag) }) }, String(tag))) }) })] })] }), _jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: submit, children: addLabel })] }), tagFilters.length > 0 ? (_jsx("div", { className: "flex flex-wrap gap-1.5", children: tagFilters.map((tag) => (_jsxs("button", { type: "button", className: "inline-flex items-center gap-1 rounded-full border px-2 py-0.5 font-mono text-[10px] hover:bg-muted", onClick: () => onRemove(tag), "aria-label": `${removeLabel}: ${tag}`, children: [_jsx(TagChip, { tag: tag }), _jsx(X, { className: "h-3 w-3", "aria-hidden": "true" })] }, tag))) })) : null] }));
|
|
87
|
+
}
|
|
88
|
+
function Field({ label, children }) {
|
|
89
|
+
return (_jsxs("div", { className: "space-y-1", children: [_jsx("span", { className: "text-muted-foreground text-xs", children: label }), children] }));
|
|
90
|
+
}
|
|
91
|
+
function StatusGlyph({ status }) {
|
|
92
|
+
if (status === "succeeded")
|
|
93
|
+
return _jsx(CheckCircle2, { "data-icon": "inline-start", "aria-hidden": "true" });
|
|
94
|
+
if (status === "failed")
|
|
95
|
+
return _jsx(XCircle, { "data-icon": "inline-start", "aria-hidden": "true" });
|
|
96
|
+
return _jsx(Clock, { "data-icon": "inline-start", "aria-hidden": "true" });
|
|
97
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ListWorkflowRunsQuery, WorkflowRunsApi } from "../types.js";
|
|
2
|
+
export interface WorkflowRunsPageProps {
|
|
3
|
+
api: WorkflowRunsApi;
|
|
4
|
+
selectedRunId?: string | null;
|
|
5
|
+
onOpenRun?: (id: string) => void;
|
|
6
|
+
initialFilters?: ListWorkflowRunsQuery;
|
|
7
|
+
pollIntervalMs?: number;
|
|
8
|
+
className?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function WorkflowRunsPage({ api, selectedRunId, onOpenRun, initialFilters, pollIntervalMs, className, }: WorkflowRunsPageProps): import("react/jsx-runtime").JSX.Element;
|
|
11
|
+
export declare function WorkflowRunsPageSkeleton(): import("react/jsx-runtime").JSX.Element;
|
|
12
|
+
//# sourceMappingURL=workflow-runs-page.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workflow-runs-page.d.ts","sourceRoot":"","sources":["../../src/components/workflow-runs-page.tsx"],"names":[],"mappings":"AAQA,OAAO,KAAK,EACV,qBAAqB,EAGrB,eAAe,EAChB,MAAM,aAAa,CAAA;AAKpB,MAAM,WAAW,qBAAqB;IACpC,GAAG,EAAE,eAAe,CAAA;IACpB,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,SAAS,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAA;IAChC,cAAc,CAAC,EAAE,qBAAqB,CAAA;IACtC,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,gBAAgB,CAAC,EAC/B,GAAG,EACH,aAAa,EACb,SAAS,EACT,cAAc,EACd,cAAqB,EACrB,SAAS,GACV,EAAE,qBAAqB,2CAuKvB;AAED,wBAAgB,wBAAwB,4CAqBvC"}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Button } from "@voyant-travel/ui/components/button";
|
|
4
|
+
import { Card, CardContent } from "@voyant-travel/ui/components/card";
|
|
5
|
+
import { Clock, Workflow } from "lucide-react";
|
|
6
|
+
import { useEffect, useMemo, useState } from "react";
|
|
7
|
+
import { useWorkflowRunsUiMessagesOrDefault } from "../i18n/index.js";
|
|
8
|
+
import { formatDuration, formatRelative, StatusBadge, StatusIcon, TagChip } from "./common.js";
|
|
9
|
+
import { WorkflowRunDetailPage } from "./workflow-run-detail-page.js";
|
|
10
|
+
import { buildFilterOptions, WorkflowRunsFilters } from "./workflow-runs-filters.js";
|
|
11
|
+
export function WorkflowRunsPage({ api, selectedRunId, onOpenRun, initialFilters, pollIntervalMs = 5000, className, }) {
|
|
12
|
+
const messages = useWorkflowRunsUiMessagesOrDefault();
|
|
13
|
+
const [filters, setFilters] = useState(initialFilters ?? { limit: 50 });
|
|
14
|
+
const [statusFilters, setStatusFilters] = useState(initialFilters?.status ? [initialFilters.status] : []);
|
|
15
|
+
const [tagFilters, setTagFilters] = useState(initialFilters?.tag ? [initialFilters.tag] : []);
|
|
16
|
+
const [searchQuery, setSearchQuery] = useState("");
|
|
17
|
+
const [timeRange, setTimeRange] = useState("24h");
|
|
18
|
+
const [live, setLive] = useState(false);
|
|
19
|
+
const [runs, setRuns] = useState([]);
|
|
20
|
+
const [error, setError] = useState(null);
|
|
21
|
+
const [loading, setLoading] = useState(false);
|
|
22
|
+
const [localSelectedRunId, setLocalSelectedRunId] = useState(null);
|
|
23
|
+
const activeRunId = selectedRunId !== undefined ? selectedRunId : localSelectedRunId;
|
|
24
|
+
const serverFilters = useMemo(() => ({
|
|
25
|
+
...filters,
|
|
26
|
+
status: statusFilters.length === 1 ? statusFilters[0] : undefined,
|
|
27
|
+
tag: tagFilters.length === 1 ? tagFilters[0] : undefined,
|
|
28
|
+
}), [filters, statusFilters, tagFilters]);
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
let cancelled = false;
|
|
31
|
+
const refresh = async () => {
|
|
32
|
+
if (typeof document !== "undefined" && document.hidden)
|
|
33
|
+
return;
|
|
34
|
+
setLoading(true);
|
|
35
|
+
try {
|
|
36
|
+
const res = await api.listRuns(serverFilters);
|
|
37
|
+
if (!cancelled) {
|
|
38
|
+
setRuns(res.data);
|
|
39
|
+
setError(null);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
if (!cancelled)
|
|
44
|
+
setError(err instanceof Error ? err.message : messages.page.loadError);
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
if (!cancelled)
|
|
48
|
+
setLoading(false);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
void refresh();
|
|
52
|
+
const interval = setInterval(() => void refresh(), live ? 1000 : pollIntervalMs);
|
|
53
|
+
return () => {
|
|
54
|
+
cancelled = true;
|
|
55
|
+
clearInterval(interval);
|
|
56
|
+
};
|
|
57
|
+
}, [api, live, messages.page.loadError, pollIntervalMs, serverFilters]);
|
|
58
|
+
const filterOptions = useMemo(() => buildFilterOptions(runs, filters.workflowName), [filters.workflowName, runs]);
|
|
59
|
+
const filteredRuns = useMemo(() => filterRuns({ runs, statusFilters, tagFilters, searchQuery, timeRange }), [runs, searchQuery, statusFilters, tagFilters, timeRange]);
|
|
60
|
+
const openRun = (id) => {
|
|
61
|
+
setLocalSelectedRunId(id);
|
|
62
|
+
onOpenRun?.(id);
|
|
63
|
+
};
|
|
64
|
+
const toggleStatus = (status) => {
|
|
65
|
+
setStatusFilters((current) => current.includes(status) ? current.filter((item) => item !== status) : [...current, status]);
|
|
66
|
+
};
|
|
67
|
+
const addTagFilter = (tag) => {
|
|
68
|
+
const trimmed = tag.trim();
|
|
69
|
+
if (!trimmed)
|
|
70
|
+
return;
|
|
71
|
+
setTagFilters((current) => (current.includes(trimmed) ? current : [...current, trimmed]));
|
|
72
|
+
};
|
|
73
|
+
const removeTagFilter = (tag) => setTagFilters((current) => current.filter((item) => item !== tag));
|
|
74
|
+
return (_jsxs("div", { className: `flex min-h-screen flex-col bg-background ${className ?? ""}`, children: [_jsx("header", { className: "sticky top-0 z-10 border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60", children: _jsxs("div", { className: "container mx-auto flex items-center gap-3 px-4 py-3", children: [_jsx(Workflow, { className: "h-5 w-5" }), _jsxs("div", { className: "min-w-0", children: [_jsx("h1", { className: "font-semibold text-base", children: messages.page.title }), _jsx("p", { className: "text-muted-foreground text-xs", children: messages.page.subtitle })] }), _jsxs("div", { className: "ml-auto flex items-center gap-2", children: [_jsxs(Button, { type: "button", variant: live ? "default" : "outline", size: "sm", onClick: () => setLive((value) => !value), "aria-pressed": live, children: [_jsx(Clock, { "data-icon": "inline-start", "aria-hidden": "true" }), messages.page.live] }), _jsx("span", { className: "text-muted-foreground text-xs", children: messages.page.filteredRunCount(filteredRuns.length, runs.length) })] })] }) }), _jsxs("main", { className: "container mx-auto flex flex-1 flex-col gap-6 px-4 py-6 md:flex-row", children: [_jsxs("aside", { className: "space-y-3 md:w-96 md:shrink-0", children: [_jsx(WorkflowRunsFilters, { filters: filters, workflowOptions: filterOptions.workflows, tagOptions: filterOptions.tags, statusFilters: statusFilters, tagFilters: tagFilters, searchQuery: searchQuery, timeRange: timeRange, onChange: setFilters, onToggleStatus: toggleStatus, onAddTagFilter: addTagFilter, onRemoveTagFilter: removeTagFilter, onSearchChange: setSearchQuery, onTimeRangeChange: setTimeRange, onClear: () => {
|
|
75
|
+
setFilters({ limit: filters.limit ?? 50 });
|
|
76
|
+
setStatusFilters([]);
|
|
77
|
+
setTagFilters([]);
|
|
78
|
+
setSearchQuery("");
|
|
79
|
+
setTimeRange("24h");
|
|
80
|
+
} }), error ? (_jsx(Card, { className: "border-destructive/40", children: _jsx(CardContent, { className: "pt-4 text-destructive text-sm", children: error }) })) : null, _jsxs("div", { className: "space-y-2", children: [filteredRuns.length === 0 && !loading ? (_jsx(Card, { children: _jsx(CardContent, { className: "pt-4 text-muted-foreground text-sm", children: messages.page.empty }) })) : null, filteredRuns.map((run) => (_jsx(RunListItem, { run: run, selected: activeRunId === run.id, activeTagFilters: tagFilters, activeStatusFilters: statusFilters, onSelect: () => openRun(run.id), onToggleStatus: toggleStatus, onToggleTag: (tag) => tagFilters.includes(tag) ? removeTagFilter(tag) : addTagFilter(tag) }, run.id)))] })] }), _jsx("section", { className: "min-w-0 flex-1", children: activeRunId ? (_jsx(WorkflowRunDetailPage, { api: api, runId: activeRunId, onOpenRun: openRun })) : (_jsx(SelectPrompt, {})) })] })] }));
|
|
81
|
+
}
|
|
82
|
+
export function WorkflowRunsPageSkeleton() {
|
|
83
|
+
const messages = useWorkflowRunsUiMessagesOrDefault();
|
|
84
|
+
return (_jsxs("div", { className: "flex min-h-screen flex-col bg-background", children: [_jsx("header", { className: "border-b px-4 py-3", children: _jsx("div", { className: "h-5 w-40 rounded bg-muted", role: "status", "aria-label": messages.page.loading }) }), _jsxs("main", { className: "container mx-auto flex flex-1 flex-col gap-6 px-4 py-6 md:flex-row", children: [_jsxs("aside", { className: "space-y-3 md:w-96 md:shrink-0", children: [_jsx("div", { className: "h-56 rounded-md bg-muted" }), _jsx("div", { className: "h-20 rounded-md bg-muted" }), _jsx("div", { className: "h-20 rounded-md bg-muted" })] }), _jsx("section", { className: "min-h-[24rem] flex-1 rounded-md bg-muted" })] })] }));
|
|
85
|
+
}
|
|
86
|
+
function RunListItem({ run, selected, activeTagFilters, activeStatusFilters, onSelect, onToggleStatus, onToggleTag, }) {
|
|
87
|
+
const messages = useWorkflowRunsUiMessagesOrDefault();
|
|
88
|
+
return (_jsxs("div", { className: `rounded-md border bg-card p-3 text-sm transition-colors hover:bg-muted/50 ${selected ? "border-primary bg-primary/5" : "" // i18n-literal-ok: CSS classes
|
|
89
|
+
}`, children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsxs("button", { type: "button", onClick: onSelect, className: "flex min-w-0 flex-1 items-center gap-2 text-left", children: [_jsx(StatusIcon, { status: run.status }), _jsx("span", { className: "truncate font-medium", children: run.workflowName }), _jsx("span", { className: "ml-auto whitespace-nowrap text-muted-foreground text-xs", children: formatRelative(run.startedAt, messages) })] }), _jsx("button", { type: "button", onClick: () => onToggleStatus(run.status), "aria-pressed": activeStatusFilters.includes(run.status), children: _jsx(StatusBadge, { status: run.status, messages: messages }) })] }), run.durationMs != null ? (_jsx("div", { className: "mt-1 text-muted-foreground text-xs", children: formatDuration(run.durationMs) })) : null, run.tags.length > 0 ? (_jsxs("div", { className: "mt-2 flex flex-wrap gap-1", children: [run.tags.slice(0, 3).map((tag) => (_jsx("button", { type: "button", onClick: () => onToggleTag(tag), "aria-pressed": activeTagFilters.includes(tag), className: activeTagFilters.includes(tag) ? "rounded-full ring-2 ring-primary/40" : undefined, children: _jsx(TagChip, { tag: tag }) }, tag))), run.tags.length > 3 ? (_jsx("span", { className: "text-muted-foreground text-xs", children: `+${run.tags.length - 3}` })) : null] })) : null] }));
|
|
90
|
+
}
|
|
91
|
+
function SelectPrompt() {
|
|
92
|
+
const messages = useWorkflowRunsUiMessagesOrDefault();
|
|
93
|
+
return (_jsx(Card, { children: _jsx(CardContent, { className: "flex min-h-[24rem] items-center justify-center text-muted-foreground text-sm", children: messages.page.selectPrompt }) }));
|
|
94
|
+
}
|
|
95
|
+
function filterRuns({ runs, statusFilters, tagFilters, searchQuery, timeRange, }) {
|
|
96
|
+
const search = searchQuery.trim().toLowerCase();
|
|
97
|
+
const cutoff = rangeCutoff(timeRange);
|
|
98
|
+
return runs.filter((run) => {
|
|
99
|
+
if (statusFilters.length > 0 && !statusFilters.includes(run.status))
|
|
100
|
+
return false;
|
|
101
|
+
if (tagFilters.length > 0 && !tagFilters.every((tag) => run.tags.includes(tag)))
|
|
102
|
+
return false;
|
|
103
|
+
if (cutoff && new Date(run.startedAt).getTime() < cutoff)
|
|
104
|
+
return false;
|
|
105
|
+
if (!search)
|
|
106
|
+
return true;
|
|
107
|
+
return runSearchText(run).includes(search);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
function rangeCutoff(range) {
|
|
111
|
+
if (range === "all")
|
|
112
|
+
return null;
|
|
113
|
+
const minutes = range === "15m" ? 15 : range === "1h" ? 60 : range === "24h" ? 24 * 60 : 7 * 24 * 60;
|
|
114
|
+
return Date.now() - minutes * 60_000;
|
|
115
|
+
}
|
|
116
|
+
function runSearchText(run) {
|
|
117
|
+
return [
|
|
118
|
+
run.id,
|
|
119
|
+
run.workflowName,
|
|
120
|
+
run.trigger,
|
|
121
|
+
run.correlationId,
|
|
122
|
+
run.status,
|
|
123
|
+
...run.tags,
|
|
124
|
+
run.error?.message,
|
|
125
|
+
run.error?.code,
|
|
126
|
+
run.input ? JSON.stringify(run.input) : null,
|
|
127
|
+
run.result ? JSON.stringify(run.result) : null,
|
|
128
|
+
]
|
|
129
|
+
.filter(Boolean)
|
|
130
|
+
.join(" ")
|
|
131
|
+
.toLowerCase();
|
|
132
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { WorkflowSchedulesApi } from "../schedules-client.js";
|
|
2
|
+
import type { WorkflowRunsApi } from "../types.js";
|
|
3
|
+
export interface WorkflowSchedulesPageProps {
|
|
4
|
+
/** Schedules API — backed by `/api/schedules/:env`. */
|
|
5
|
+
schedulesApi: WorkflowSchedulesApi;
|
|
6
|
+
/** Optional runs API — when provided, the page joins each row with the most recent matching run. */
|
|
7
|
+
runsApi?: WorkflowRunsApi;
|
|
8
|
+
/**
|
|
9
|
+
* Optional trigger callback. When provided, each row renders a
|
|
10
|
+
* "Trigger now" button that calls this with the workflow id + the
|
|
11
|
+
* schedule's recorded `input` payload (if any).
|
|
12
|
+
*/
|
|
13
|
+
onTriggerNow?: (workflowId: string, input: unknown) => Promise<void>;
|
|
14
|
+
/** Manifest environment to inspect. Defaults to "production". */
|
|
15
|
+
environment?: "production" | "preview" | "development";
|
|
16
|
+
/** Auto-refresh interval (ms). Defaults to 30s. Pass 0 to disable. */
|
|
17
|
+
pollIntervalMs?: number;
|
|
18
|
+
className?: string;
|
|
19
|
+
}
|
|
20
|
+
export declare function WorkflowSchedulesPage({ schedulesApi, runsApi, onTriggerNow, environment, pollIntervalMs, className, }: WorkflowSchedulesPageProps): import("react/jsx-runtime").JSX.Element;
|
|
21
|
+
//# sourceMappingURL=workflow-schedules-page.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workflow-schedules-page.d.ts","sourceRoot":"","sources":["../../src/components/workflow-schedules-page.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,EAIV,oBAAoB,EACrB,MAAM,wBAAwB,CAAA;AAC/B,OAAO,KAAK,EAAkC,eAAe,EAAE,MAAM,aAAa,CAAA;AAKlF,MAAM,WAAW,0BAA0B;IACzC,uDAAuD;IACvD,YAAY,EAAE,oBAAoB,CAAA;IAClC,oGAAoG;IACpG,OAAO,CAAC,EAAE,eAAe,CAAA;IACzB;;;;OAIG;IACH,YAAY,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IACpE,iEAAiE;IACjE,WAAW,CAAC,EAAE,YAAY,GAAG,SAAS,GAAG,aAAa,CAAA;IACtD,sEAAsE;IACtE,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,qBAAqB,CAAC,EACpC,YAAY,EACZ,OAAO,EACP,YAAY,EACZ,WAA0B,EAC1B,cAAuB,EACvB,SAAS,GACV,EAAE,0BAA0B,2CA0L5B"}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Badge } from "@voyant-travel/ui/components/badge";
|
|
4
|
+
import { Button } from "@voyant-travel/ui/components/button";
|
|
5
|
+
import { Card, CardContent } from "@voyant-travel/ui/components/card";
|
|
6
|
+
import { AlertTriangle, CalendarClock, RefreshCw } from "lucide-react";
|
|
7
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
8
|
+
import { useWorkflowRunsUiMessagesOrDefault } from "../i18n/index.js";
|
|
9
|
+
import { formatRelative, StatusIcon } from "./common.js";
|
|
10
|
+
export function WorkflowSchedulesPage({ schedulesApi, runsApi, onTriggerNow, environment = "production", pollIntervalMs = 30_000, className, }) {
|
|
11
|
+
const rootMessages = useWorkflowRunsUiMessagesOrDefault();
|
|
12
|
+
const messages = rootMessages.schedules;
|
|
13
|
+
const [response, setResponse] = useState(null);
|
|
14
|
+
const [lastRuns, setLastRuns] = useState({});
|
|
15
|
+
const [loading, setLoading] = useState(false);
|
|
16
|
+
const [error, setError] = useState(null);
|
|
17
|
+
const [triggering, setTriggering] = useState({});
|
|
18
|
+
const [triggerNotice, setTriggerNotice] = useState(null);
|
|
19
|
+
const refresh = useCallback(async () => {
|
|
20
|
+
setLoading(true);
|
|
21
|
+
try {
|
|
22
|
+
const next = await schedulesApi.listSchedules(environment);
|
|
23
|
+
setResponse(next);
|
|
24
|
+
setError(null);
|
|
25
|
+
if (runsApi) {
|
|
26
|
+
const uniqueIds = Array.from(new Set(next.data.map((entry) => entry.workflowId)));
|
|
27
|
+
const results = await Promise.all(uniqueIds.map(async (workflowId) => {
|
|
28
|
+
try {
|
|
29
|
+
const runs = await runsApi.listRuns({ workflowName: workflowId, limit: 1 });
|
|
30
|
+
return [workflowId, runs.data[0] ?? null];
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return [workflowId, null];
|
|
34
|
+
}
|
|
35
|
+
}));
|
|
36
|
+
setLastRuns(Object.fromEntries(results));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
setError(err instanceof Error ? err.message : messages.loadError);
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
setLoading(false);
|
|
44
|
+
}
|
|
45
|
+
}, [environment, messages.loadError, runsApi, schedulesApi]);
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
void refresh();
|
|
48
|
+
if (!pollIntervalMs)
|
|
49
|
+
return;
|
|
50
|
+
const interval = setInterval(() => void refresh(), pollIntervalMs);
|
|
51
|
+
return () => clearInterval(interval);
|
|
52
|
+
}, [pollIntervalMs, refresh]);
|
|
53
|
+
const showEnvFlag = response?.schedulesEnabledByEnv !== undefined;
|
|
54
|
+
const envFlagOn = response?.schedulesEnabledByEnv === true;
|
|
55
|
+
const triggerRow = useCallback(async (entry) => {
|
|
56
|
+
if (!onTriggerNow)
|
|
57
|
+
return;
|
|
58
|
+
setTriggering((prev) => ({ ...prev, [entry.scheduleId]: true }));
|
|
59
|
+
setTriggerNotice(null);
|
|
60
|
+
try {
|
|
61
|
+
await onTriggerNow(entry.workflowId, entry.schedule.input);
|
|
62
|
+
setTriggerNotice({ kind: "success", text: messages.triggerSuccess });
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
setTriggerNotice({
|
|
66
|
+
kind: "error",
|
|
67
|
+
text: err instanceof Error ? err.message : messages.triggerFailed,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
setTriggering((prev) => ({ ...prev, [entry.scheduleId]: false }));
|
|
72
|
+
}
|
|
73
|
+
}, [messages.triggerFailed, messages.triggerSuccess, onTriggerNow]);
|
|
74
|
+
const rows = useMemo(() => response?.data ?? [], [response]);
|
|
75
|
+
return (_jsxs("div", { className: `flex min-h-screen flex-col bg-background ${className ?? ""}`, children: [_jsx("header", { className: "sticky top-0 z-10 border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60", children: _jsxs("div", { className: "container mx-auto flex flex-wrap items-center gap-3 px-4 py-3", children: [_jsx(CalendarClock, { className: "h-5 w-5" }), _jsxs("div", { className: "min-w-0", children: [_jsx("h1", { className: "font-semibold text-base", children: messages.title }), _jsx("p", { className: "text-muted-foreground text-xs", children: messages.subtitle })] }), _jsxs("div", { className: "ml-auto flex items-center gap-2 text-muted-foreground text-xs", children: [_jsxs("span", { children: [messages.environmentLabel, ": ", _jsx("span", { className: "font-mono", children: environment })] }), response?.versionId ? (_jsxs("span", { children: [messages.versionLabel, ":", " ", _jsx("span", { className: "font-mono", children: response.versionId.slice(0, 12) })] })) : null, _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => void refresh(), disabled: loading, children: [_jsx(RefreshCw, { className: `h-3.5 w-3.5 ${loading ? "animate-spin" : ""}`, "aria-hidden": "true" }), messages.refresh] })] })] }) }), _jsxs("main", { className: "container mx-auto flex flex-1 flex-col gap-4 px-4 py-6", children: [showEnvFlag && !envFlagOn ? (_jsx(Card, { className: "border-amber-500/40 bg-amber-500/5", children: _jsxs(CardContent, { className: "flex items-center gap-2 pt-4 text-amber-700 text-sm dark:text-amber-300", children: [_jsx(AlertTriangle, { className: "h-4 w-4", "aria-hidden": "true" }), messages.envFlagOff] }) })) : null, triggerNotice ? (_jsx(Card, { className: triggerNotice.kind === "success"
|
|
76
|
+
? "border-emerald-500/40 bg-emerald-500/5"
|
|
77
|
+
: "border-destructive/40 bg-destructive/5", children: _jsx(CardContent, { className: `pt-4 text-sm ${triggerNotice.kind === "success"
|
|
78
|
+
? "text-emerald-600 dark:text-emerald-300"
|
|
79
|
+
: "text-destructive"}`, children: triggerNotice.text }) })) : null, error ? (_jsx(Card, { className: "border-destructive/40", children: _jsx(CardContent, { className: "pt-4 text-destructive text-sm", children: error }) })) : null, !error && rows.length === 0 && !loading ? (_jsx(Card, { children: _jsx(CardContent, { className: "pt-4 text-muted-foreground text-sm", children: messages.empty }) })) : null, rows.length > 0 ? (_jsx("div", { className: "overflow-hidden rounded-md border", children: _jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { className: "bg-muted/40 text-left text-xs uppercase", children: _jsxs("tr", { children: [_jsx("th", { className: "px-3 py-2 font-medium", children: messages.workflowColumn }), _jsx("th", { className: "px-3 py-2 font-medium", children: messages.scheduleColumn }), _jsx("th", { className: "px-3 py-2 font-medium", children: messages.nextRunColumn }), _jsx("th", { className: "px-3 py-2 font-medium", children: messages.lastRunColumn }), _jsx("th", { className: "px-3 py-2 font-medium", children: messages.statusColumn }), onTriggerNow ? (_jsx("th", { className: "px-3 py-2 font-medium", children: messages.actionsColumn })) : null] }) }), _jsx("tbody", { children: rows.map((row) => (_jsx(ScheduleRow, { row: row, lastRun: lastRuns[row.workflowId] ?? null, triggering: !!triggering[row.scheduleId], onTriggerNow: onTriggerNow ? () => void triggerRow(row) : undefined, envFlagDisabled: showEnvFlag && !envFlagOn, rootMessages: rootMessages }, row.scheduleId))) })] }) })) : null] })] }));
|
|
80
|
+
}
|
|
81
|
+
function ScheduleRow({ row, lastRun, triggering, onTriggerNow, envFlagDisabled, rootMessages, }) {
|
|
82
|
+
const messages = rootMessages.schedules;
|
|
83
|
+
return (_jsxs("tr", { className: "border-t", children: [_jsx("td", { className: "px-3 py-2 font-mono text-xs", children: row.workflowId }), _jsx("td", { className: "px-3 py-2 font-mono text-xs", children: formatScheduleDecl(row.schedule, messages) }), _jsx("td", { className: "px-3 py-2 text-xs text-muted-foreground", children: formatNextRun(row, messages, rootMessages) }), _jsx("td", { className: "px-3 py-2 text-xs", children: formatLastRun(lastRun, row, messages, rootMessages) }), _jsx("td", { className: "px-3 py-2", children: _jsx(StatusPill, { row: row, envFlagDisabled: envFlagDisabled, messages: messages }) }), onTriggerNow ? (_jsx("td", { className: "px-3 py-2", children: _jsx(Button, { type: "button", size: "sm", variant: "outline", onClick: onTriggerNow, disabled: triggering, children: triggering ? messages.triggering : messages.triggerNow }) })) : null] }));
|
|
84
|
+
}
|
|
85
|
+
function StatusPill({ row, envFlagDisabled, messages, }) {
|
|
86
|
+
if (envFlagDisabled) {
|
|
87
|
+
return (_jsx(Badge, { variant: "outline", className: "border-muted-foreground/30 text-muted-foreground", children: messages.disabledByEnvFlag }));
|
|
88
|
+
}
|
|
89
|
+
if (row.disabledReason === "registration_disabled") {
|
|
90
|
+
return (_jsx(Badge, { variant: "outline", className: "border-muted-foreground/30 text-muted-foreground", children: messages.disabledByRegistration }));
|
|
91
|
+
}
|
|
92
|
+
if (row.disabledReason === "env_filtered") {
|
|
93
|
+
return (_jsx(Badge, { variant: "outline", className: "border-muted-foreground/30 text-muted-foreground", children: messages.disabledByEnvironment }));
|
|
94
|
+
}
|
|
95
|
+
return (_jsx(Badge, { variant: "outline", className: "border-emerald-500/40 bg-emerald-500/10 text-emerald-600 dark:text-emerald-300", children: messages.enabled }));
|
|
96
|
+
}
|
|
97
|
+
function formatScheduleDecl(decl, messages) {
|
|
98
|
+
if (decl.cron)
|
|
99
|
+
return messages.cron(decl.cron, decl.timezone ?? "UTC");
|
|
100
|
+
if (decl.every !== undefined)
|
|
101
|
+
return messages.every(String(decl.every));
|
|
102
|
+
if (decl.at)
|
|
103
|
+
return messages.at(decl.at);
|
|
104
|
+
return messages.eventDriven;
|
|
105
|
+
}
|
|
106
|
+
function formatNextRun(row, messages, rootMessages) {
|
|
107
|
+
if (!row.enabled || row.nextRunAt === null)
|
|
108
|
+
return messages.notScheduled;
|
|
109
|
+
const delta = row.nextRunAt - Date.now();
|
|
110
|
+
const relative = formatRelative(new Date(row.nextRunAt).toISOString(), rootMessages);
|
|
111
|
+
return delta >= 0 ? messages.inFuture(relative) : messages.inPast(relative);
|
|
112
|
+
}
|
|
113
|
+
function formatLastRun(lastRun, row, messages, rootMessages) {
|
|
114
|
+
if (lastRun) {
|
|
115
|
+
const relative = formatRelative(lastRun.startedAt, rootMessages);
|
|
116
|
+
const label = lastRunLabel(lastRun.status, relative, messages);
|
|
117
|
+
return (_jsxs("span", { className: "inline-flex items-center gap-1.5", children: [_jsx(StatusIcon, { status: lastRun.status }), label] }));
|
|
118
|
+
}
|
|
119
|
+
if (row.lastError && row.lastFireAt !== undefined && row.lastFireAt !== null) {
|
|
120
|
+
const relative = formatRelative(new Date(row.lastFireAt).toISOString(), rootMessages);
|
|
121
|
+
return (_jsxs("span", { className: "inline-flex items-center gap-1.5", title: row.lastError, children: [_jsx(StatusIcon, { status: "failed" }), messages.lastRunFailed(relative)] }));
|
|
122
|
+
}
|
|
123
|
+
if (row.lastSuccessfulRunAt !== undefined && row.lastSuccessfulRunAt !== null) {
|
|
124
|
+
const relative = formatRelative(new Date(row.lastSuccessfulRunAt).toISOString(), rootMessages);
|
|
125
|
+
return (_jsxs("span", { className: "inline-flex items-center gap-1.5", children: [_jsx(StatusIcon, { status: "succeeded" }), messages.lastRunSucceeded(relative)] }));
|
|
126
|
+
}
|
|
127
|
+
if (row.lastFireAt !== undefined && row.lastFireAt !== null) {
|
|
128
|
+
const relative = formatRelative(new Date(row.lastFireAt).toISOString(), rootMessages);
|
|
129
|
+
return (_jsxs("span", { className: "inline-flex items-center gap-1.5", children: [_jsx(CalendarClock, { className: "h-3.5 w-3.5 text-muted-foreground", "aria-hidden": "true" }), messages.lastFireRecorded(relative)] }));
|
|
130
|
+
}
|
|
131
|
+
return _jsx("span", { className: "text-muted-foreground", children: messages.lastRunNone });
|
|
132
|
+
}
|
|
133
|
+
function lastRunLabel(status, relative, messages) {
|
|
134
|
+
switch (status) {
|
|
135
|
+
case "succeeded":
|
|
136
|
+
return messages.lastRunSucceeded(relative);
|
|
137
|
+
case "failed":
|
|
138
|
+
return messages.lastRunFailed(relative);
|
|
139
|
+
case "running":
|
|
140
|
+
return messages.lastRunRunning;
|
|
141
|
+
case "cancelled":
|
|
142
|
+
return messages.lastRunCancelled(relative);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"en.d.ts","sourceRoot":"","sources":["../../src/i18n/en.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAA;AAE3D,eAAO,MAAM,gBAAgB,EAAE,sBAyI9B,CAAA;AAED,eAAO,MAAM,aAAa,wBAAmB,CAAA"}
|