@voyantjs/workflows-react 0.106.0 → 0.107.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.md +43 -0
  2. package/dist/client.d.ts +2 -0
  3. package/dist/client.d.ts.map +1 -0
  4. package/dist/client.js +1 -0
  5. package/dist/components/common.d.ts +30 -0
  6. package/dist/components/common.d.ts.map +1 -0
  7. package/dist/components/common.js +108 -0
  8. package/dist/components/workflow-run-actions.d.ts +7 -0
  9. package/dist/components/workflow-run-actions.d.ts.map +1 -0
  10. package/dist/components/workflow-run-actions.js +72 -0
  11. package/dist/components/workflow-run-detail-page.d.ts +10 -0
  12. package/dist/components/workflow-run-detail-page.d.ts.map +1 -0
  13. package/dist/components/workflow-run-detail-page.js +96 -0
  14. package/dist/components/workflow-runs-filters.d.ts +32 -0
  15. package/dist/components/workflow-runs-filters.d.ts.map +1 -0
  16. package/dist/components/workflow-runs-filters.js +97 -0
  17. package/dist/components/workflow-runs-page.d.ts +12 -0
  18. package/dist/components/workflow-runs-page.d.ts.map +1 -0
  19. package/dist/components/workflow-runs-page.js +132 -0
  20. package/dist/components/workflow-schedules-page.d.ts +21 -0
  21. package/dist/components/workflow-schedules-page.d.ts.map +1 -0
  22. package/dist/components/workflow-schedules-page.js +144 -0
  23. package/dist/i18n/en.d.ts +4 -0
  24. package/dist/i18n/en.d.ts.map +1 -0
  25. package/dist/i18n/en.js +135 -0
  26. package/dist/i18n/index.d.ts +5 -0
  27. package/dist/i18n/index.d.ts.map +1 -0
  28. package/dist/i18n/index.js +3 -0
  29. package/dist/i18n/messages.d.ts +125 -0
  30. package/dist/i18n/messages.d.ts.map +1 -0
  31. package/dist/i18n/messages.js +1 -0
  32. package/dist/i18n/provider.d.ts +26 -0
  33. package/dist/i18n/provider.d.ts.map +1 -0
  34. package/dist/i18n/provider.js +44 -0
  35. package/dist/i18n/ro.d.ts +4 -0
  36. package/dist/i18n/ro.d.ts.map +1 -0
  37. package/dist/i18n/ro.js +137 -0
  38. package/dist/schedules-client.d.ts +2 -0
  39. package/dist/schedules-client.d.ts.map +1 -0
  40. package/dist/schedules-client.js +1 -0
  41. package/dist/types.d.ts +2 -0
  42. package/dist/types.d.ts.map +1 -0
  43. package/dist/types.js +1 -0
  44. package/dist/ui.d.ts +9 -0
  45. package/dist/ui.d.ts.map +1 -0
  46. package/dist/ui.js +6 -0
  47. package/package.json +69 -21
  48. package/src/client.ts +4 -0
  49. package/src/components/common.tsx +182 -0
  50. package/src/components/workflow-run-actions.tsx +160 -0
  51. package/src/components/workflow-run-detail-page.tsx +393 -0
  52. package/src/components/workflow-runs-filters.tsx +349 -0
  53. package/src/components/workflow-runs-page.tsx +357 -0
  54. package/src/components/workflow-schedules-page.tsx +398 -0
  55. package/src/i18n/en.ts +142 -0
  56. package/src/i18n/index.ts +26 -0
  57. package/src/i18n/messages.ts +125 -0
  58. package/src/i18n/provider.tsx +96 -0
  59. package/src/i18n/ro.ts +144 -0
  60. package/src/schedules-client.ts +8 -0
  61. package/src/styles.css +11 -0
  62. package/src/types.ts +14 -0
  63. package/src/ui.ts +63 -0
package/README.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # @voyantjs/workflows-react
2
2
 
3
+ The workflows client tier: headless hooks and API clients plus the styled
4
+ workflow-run admin UI (formerly `@voyantjs/workflows-ui`).
5
+
6
+ Headless consumers import from the root, `./workflow-runs`,
7
+ `./workflow-runs-client`, or `./workflow-schedules-client` — these pull no
8
+ styling peers. Styled surfaces live under `./ui`, `./components/*`, `./client`,
9
+ `./i18n`, and `./styles.css`, whose heavier peers (`@voyantjs/ui`,
10
+ `lucide-react`) are optional and only needed when you import those subpaths.
11
+
3
12
  React hooks for triggering Voyant Workflows and subscribing to runs in
4
13
  real time. The package also includes admin workflow-run observability hooks for
5
14
  the `/v1/admin/workflow-runs` routes exposed by `@voyantjs/workflow-runs`.
@@ -62,3 +71,37 @@ export function WorkflowsRoute() {
62
71
  `useWorkflowRuns` polls every 5 seconds only while the visible list contains a
63
72
  running workflow. `useWorkflowRun` polls every 2 seconds while the selected run
64
73
  is running. Both stop polling in background tabs.
74
+
75
+ ## UI components
76
+
77
+ Importable React UI for the workflow run admin API exposed by
78
+ `@voyantjs/workflow-runs`.
79
+
80
+ ```tsx
81
+ import "@voyantjs/workflows-react/styles.css"
82
+
83
+ import { createWorkflowRunsApiClient, WorkflowRunsPage } from "@voyantjs/workflows-react/ui"
84
+
85
+ const workflowsApi = createWorkflowRunsApiClient({
86
+ apiBase: "/api",
87
+ })
88
+
89
+ export function WorkflowsRoute() {
90
+ return <WorkflowRunsPage api={workflowsApi} />
91
+ }
92
+ ```
93
+
94
+ The `./ui` subpath exports `WorkflowRunsPage` and `WorkflowRunDetailPage`.
95
+ Route-owning apps can wire deep links by controlling the selected run id:
96
+
97
+ ```tsx
98
+ <WorkflowRunsPage
99
+ api={workflowsApi}
100
+ selectedRunId={params.id}
101
+ onOpenRun={(id) => navigate({ to: "/workflows/$id", params: { id } })}
102
+ />
103
+ ```
104
+
105
+ `@voyantjs/workflows-react/ui` is the public workflows-facing import path for
106
+ these surfaces. Older `@voyantjs/workflows-react/ui` and `@voyantjs/workflow-runs-ui`
107
+ imports should migrate here.
@@ -0,0 +1,2 @@
1
+ export { createWorkflowRunsApiClient, type WorkflowRunsApiClientOptions, } from "./workflow-runs-client.js";
2
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,2BAA2B,EAC3B,KAAK,4BAA4B,GAClC,MAAM,2BAA2B,CAAA"}
package/dist/client.js ADDED
@@ -0,0 +1 @@
1
+ export { createWorkflowRunsApiClient, } from "./workflow-runs-client.js";
@@ -0,0 +1,30 @@
1
+ import { type WorkflowRunsUiMessages } from "../i18n/index.js";
2
+ import type { WorkflowRun, WorkflowRunStep, WorkflowRunStepStatus } from "../types.js";
3
+ export declare function StatusIcon({ status }: {
4
+ status: WorkflowRun["status"];
5
+ }): import("react/jsx-runtime").JSX.Element;
6
+ export declare function StepStatusIcon({ status }: {
7
+ status: WorkflowRunStepStatus;
8
+ }): import("react/jsx-runtime").JSX.Element;
9
+ export declare function StatusBadge({ status, messages, }: {
10
+ status: WorkflowRun["status"];
11
+ messages: WorkflowRunsUiMessages;
12
+ }): import("react/jsx-runtime").JSX.Element;
13
+ export declare function TagChip({ tag }: {
14
+ tag: string;
15
+ }): import("react/jsx-runtime").JSX.Element;
16
+ export declare function CopyableId({ id, copiedLabel, className, }: {
17
+ id: string;
18
+ copiedLabel: string;
19
+ className?: string;
20
+ }): import("react/jsx-runtime").JSX.Element;
21
+ export declare function PayloadBlock({ title, value, messages, hideTitle, }: {
22
+ title: string;
23
+ value: Record<string, unknown>;
24
+ messages: WorkflowRunsUiMessages;
25
+ hideTitle?: boolean;
26
+ }): import("react/jsx-runtime").JSX.Element;
27
+ export declare function formatRelative(iso: string, messages: WorkflowRunsUiMessages): string;
28
+ export declare function formatDuration(ms: number): string;
29
+ export declare function runHasLiveStatus(run: WorkflowRun | WorkflowRunStep): boolean;
30
+ //# sourceMappingURL=common.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../../src/components/common.tsx"],"names":[],"mappings":"AAMA,OAAO,EAAsC,KAAK,sBAAsB,EAAE,MAAM,kBAAkB,CAAA;AAClG,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAA;AAEtF,wBAAgB,UAAU,CAAC,EAAE,MAAM,EAAE,EAAE;IAAE,MAAM,EAAE,WAAW,CAAC,QAAQ,CAAC,CAAA;CAAE,2CAWvE;AAED,wBAAgB,cAAc,CAAC,EAAE,MAAM,EAAE,EAAE;IAAE,MAAM,EAAE,qBAAqB,CAAA;CAAE,2CAa3E;AAED,wBAAgB,WAAW,CAAC,EAC1B,MAAM,EACN,QAAQ,GACT,EAAE;IACD,MAAM,EAAE,WAAW,CAAC,QAAQ,CAAC,CAAA;IAC7B,QAAQ,EAAE,sBAAsB,CAAA;CACjC,2CAaA;AAED,wBAAgB,OAAO,CAAC,EAAE,GAAG,EAAE,EAAE;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,2CAiB/C;AAED,wBAAgB,UAAU,CAAC,EACzB,EAAE,EACF,WAAW,EACX,SAAS,GACV,EAAE;IACD,EAAE,EAAE,MAAM,CAAA;IACV,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,2CAyBA;AAED,wBAAgB,YAAY,CAAC,EAC3B,KAAK,EACL,KAAK,EACL,QAAQ,EACR,SAAiB,GAClB,EAAE;IACD,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC9B,QAAQ,EAAE,sBAAsB,CAAA;IAChC,SAAS,CAAC,EAAE,OAAO,CAAA;CACpB,2CAkCA;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,sBAAsB,GAAG,MAAM,CAWpF;AAED,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAMjD;AAED,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,WAAW,GAAG,eAAe,GAAG,OAAO,CAE5E"}
@@ -0,0 +1,108 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Badge } from "@voyantjs/ui/components/badge";
4
+ import { AlertCircle, CheckCircle2, Clock, Copy, RefreshCw, XCircle } from "lucide-react";
5
+ import { useMemo, useState } from "react";
6
+ import { useWorkflowRunsUiMessagesOrDefault } from "../i18n/index.js";
7
+ export function StatusIcon({ status }) {
8
+ switch (status) {
9
+ case "succeeded":
10
+ return _jsx(CheckCircle2, { className: "h-4 w-4 text-emerald-500" });
11
+ case "failed":
12
+ return _jsx(XCircle, { className: "h-4 w-4 text-destructive" });
13
+ case "cancelled":
14
+ return _jsx(AlertCircle, { className: "h-4 w-4 text-amber-500" });
15
+ case "running":
16
+ return _jsx(Clock, { className: "h-4 w-4 animate-pulse text-blue-500" });
17
+ }
18
+ }
19
+ export function StepStatusIcon({ status }) {
20
+ switch (status) {
21
+ case "succeeded":
22
+ return _jsx(CheckCircle2, { className: "h-4 w-4 text-emerald-500" });
23
+ case "failed":
24
+ return _jsx(XCircle, { className: "h-4 w-4 text-destructive" });
25
+ case "running":
26
+ return _jsx(Clock, { className: "h-4 w-4 animate-pulse text-blue-500" });
27
+ case "skipped":
28
+ return _jsx(AlertCircle, { className: "h-4 w-4 text-muted-foreground" });
29
+ case "compensated":
30
+ return _jsx(RefreshCw, { className: "h-4 w-4 text-amber-500" });
31
+ }
32
+ }
33
+ export function StatusBadge({ status, messages, }) {
34
+ useWorkflowRunsUiMessagesOrDefault();
35
+ const className = {
36
+ succeeded: "border-emerald-500/40 bg-emerald-500/10 text-emerald-600 dark:text-emerald-300", // i18n-literal-ok: CSS classes
37
+ failed: "border-destructive/40 bg-destructive/10 text-destructive", // i18n-literal-ok: CSS classes
38
+ cancelled: "border-amber-500/40 bg-amber-500/10 text-amber-600 dark:text-amber-300", // i18n-literal-ok: CSS classes
39
+ running: "border-blue-500/40 bg-blue-500/10 text-blue-600 dark:text-blue-300", // i18n-literal-ok: CSS classes
40
+ }[status];
41
+ return (_jsx(Badge, { variant: "outline", className: `text-[11px] ${className}`, children: messages.status[status] }));
42
+ }
43
+ export function TagChip({ tag }) {
44
+ const colonIdx = tag.indexOf(":");
45
+ if (colonIdx < 0) {
46
+ return (_jsx(Badge, { variant: "outline", className: "font-mono text-[10px]", children: tag }));
47
+ }
48
+ const key = tag.slice(0, colonIdx);
49
+ const value = tag.slice(colonIdx + 1);
50
+ return (_jsxs(Badge, { variant: "outline", className: "gap-1 font-mono text-[10px]", title: tag, children: [_jsx("span", { className: "text-muted-foreground", children: key }), _jsx("span", { className: "max-w-[18ch] truncate", children: value })] }));
51
+ }
52
+ export function CopyableId({ id, copiedLabel, className, }) {
53
+ const [copied, setCopied] = useState(false);
54
+ const onCopy = async () => {
55
+ try {
56
+ await navigator.clipboard.writeText(id);
57
+ setCopied(true);
58
+ setTimeout(() => setCopied(false), 1500);
59
+ }
60
+ catch {
61
+ /* clipboard blocked */
62
+ }
63
+ };
64
+ return (_jsxs("button", { type: "button", onClick: onCopy, 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 ${className ?? ""}`, title: id, children: [_jsx("span", { className: "max-w-[16ch] truncate", children: id }), _jsx(Copy, { className: "h-3 w-3 opacity-60 group-hover:opacity-100" }), copied ? _jsx("span", { className: "text-emerald-500 text-[10px]", children: copiedLabel }) : null] }));
65
+ }
66
+ export function PayloadBlock({ title, value, messages, hideTitle = false, }) {
67
+ const [copied, setCopied] = useState(false);
68
+ const json = useMemo(() => JSON.stringify(value, null, 2), [value]);
69
+ const onCopy = async () => {
70
+ try {
71
+ await navigator.clipboard.writeText(json);
72
+ setCopied(true);
73
+ setTimeout(() => setCopied(false), 1500);
74
+ }
75
+ catch {
76
+ /* clipboard blocked */
77
+ }
78
+ };
79
+ return (_jsxs("div", { className: "space-y-1", children: [hideTitle ? null : (_jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsx("span", { className: "text-muted-foreground text-xs", children: title }), _jsxs("button", { type: "button", onClick: onCopy, className: "flex items-center gap-1 text-muted-foreground text-xs hover:text-foreground", children: [_jsx(Copy, { className: "h-3 w-3" }), copied ? messages.detail.copied : messages.detail.copy] })] })), _jsx("pre", { className: "max-h-[24rem] overflow-auto rounded bg-muted/40 p-3 font-mono text-[11px] leading-relaxed", children: json })] }));
80
+ }
81
+ export function formatRelative(iso, messages) {
82
+ const seconds = Math.round((Date.now() - new Date(iso).getTime()) / 1000);
83
+ if (Math.abs(seconds) < 5)
84
+ return messages.format.relativeNow;
85
+ const formatter = new Intl.RelativeTimeFormat(undefined, { numeric: "auto" });
86
+ if (Math.abs(seconds) < 60)
87
+ return formatter.format(-seconds, "second");
88
+ const minutes = Math.round(seconds / 60);
89
+ if (Math.abs(minutes) < 60)
90
+ return formatter.format(-minutes, "minute");
91
+ const hours = Math.round(minutes / 60);
92
+ if (Math.abs(hours) < 24)
93
+ return formatter.format(-hours, "hour");
94
+ const days = Math.round(hours / 24);
95
+ return formatter.format(-days, "day");
96
+ }
97
+ export function formatDuration(ms) {
98
+ if (ms < 1000)
99
+ return `${ms}ms`;
100
+ if (ms < 60_000)
101
+ return `${(ms / 1000).toFixed(2)}s`;
102
+ const minutes = Math.floor(ms / 60_000);
103
+ const seconds = Math.round((ms % 60_000) / 1000);
104
+ return `${minutes}m ${seconds}s`;
105
+ }
106
+ export function runHasLiveStatus(run) {
107
+ return run.status === "running";
108
+ }
@@ -0,0 +1,7 @@
1
+ import type { WorkflowRun, WorkflowRunsApi } from "../types.js";
2
+ export declare function WorkflowRunActionsCard({ api, run, onOpenRun, }: {
3
+ api: WorkflowRunsApi;
4
+ run: WorkflowRun;
5
+ onOpenRun?: (id: string) => void;
6
+ }): import("react/jsx-runtime").JSX.Element;
7
+ //# sourceMappingURL=workflow-run-actions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workflow-run-actions.d.ts","sourceRoot":"","sources":["../../src/components/workflow-run-actions.tsx"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,WAAW,EAA0B,eAAe,EAAE,MAAM,aAAa,CAAA;AAGvF,wBAAgB,sBAAsB,CAAC,EACrC,GAAG,EACH,GAAG,EACH,SAAS,GACV,EAAE;IACD,GAAG,EAAE,eAAe,CAAA;IACpB,GAAG,EAAE,WAAW,CAAA;IAChB,SAAS,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAA;CACjC,2CAuGA"}
@@ -0,0 +1,72 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Button } from "@voyantjs/ui/components/button";
4
+ import { Card, CardContent, CardHeader, CardTitle } from "@voyantjs/ui/components/card";
5
+ import { AlertTriangle, Play, RotateCw } from "lucide-react";
6
+ import { useCallback, useState } from "react";
7
+ import { useWorkflowRunsUiMessagesOrDefault } from "../i18n/index.js";
8
+ import { runHasLiveStatus } from "./common.js";
9
+ export function WorkflowRunActionsCard({ api, run, onOpenRun, }) {
10
+ const messages = useWorkflowRunsUiMessagesOrDefault();
11
+ const [busy, setBusy] = useState(null);
12
+ const [feedback, setFeedback] = useState(null);
13
+ const [confirmOpen, setConfirmOpen] = useState(false);
14
+ const explainError = useCallback((err) => {
15
+ if (err.error === "runner_not_registered")
16
+ return messages.actions.runnerMissing;
17
+ if (err.error === "rerun_blocked")
18
+ return err.detail ?? messages.actions.rerunBlocked;
19
+ if (err.error === "incomplete_prior_step") {
20
+ return err.detail ?? messages.actions.incompletePriorStep;
21
+ }
22
+ return err.detail ?? err.error ?? messages.actions.actionFailed;
23
+ }, [messages]);
24
+ const doRerun = async (confirm) => {
25
+ setBusy("rerun");
26
+ setFeedback(null);
27
+ try {
28
+ const result = await api.rerunRun(run.id, { confirm });
29
+ if (result.ok) {
30
+ setFeedback({ kind: "info", message: messages.actions.rerunStarted });
31
+ onOpenRun?.(result.data.runId);
32
+ setConfirmOpen(false);
33
+ }
34
+ else if (result.error.error === "confirmation_required") {
35
+ setConfirmOpen(true);
36
+ }
37
+ else {
38
+ setFeedback({ kind: "error", message: explainError(result.error) });
39
+ }
40
+ }
41
+ finally {
42
+ setBusy(null);
43
+ }
44
+ };
45
+ const doResume = async () => {
46
+ setBusy("resume");
47
+ setFeedback(null);
48
+ try {
49
+ const result = await api.resumeRun(run.id);
50
+ if (result.ok) {
51
+ setFeedback({
52
+ kind: "info",
53
+ message: messages.actions.resumeStarted(result.data.resumeFromStep ?? ""),
54
+ });
55
+ onOpenRun?.(result.data.runId);
56
+ }
57
+ else {
58
+ setFeedback({ kind: "error", message: explainError(result.error) });
59
+ }
60
+ }
61
+ finally {
62
+ setBusy(null);
63
+ }
64
+ };
65
+ const canResume = run.status === "failed";
66
+ const isRunning = runHasLiveStatus(run);
67
+ return (_jsxs(Card, { children: [_jsxs(CardContent, { className: "flex flex-wrap items-center gap-2 pt-4", children: [_jsxs(Button, { variant: "default", size: "sm", onClick: () => void doRerun(false), disabled: busy !== null || isRunning, title: isRunning ? messages.actions.waitForCompletion : messages.actions.rerunDescription, children: [_jsx(RotateCw, { className: `mr-1.5 h-3.5 w-3.5 ${busy === "rerun" ? "animate-spin" : ""}` }), busy === "rerun" ? messages.actions.rerunBusy : messages.actions.rerun] }), _jsxs(Button, { variant: "secondary", size: "sm", onClick: () => void doResume(), disabled: busy !== null || !canResume, title: canResume ? messages.actions.resumeDescription : messages.actions.resumeUnavailable, children: [_jsx(Play, { className: `mr-1.5 h-3.5 w-3.5 ${busy === "resume" ? "animate-pulse" : ""}` }), busy === "resume" ? messages.actions.resumeBusy : messages.actions.resume] }), feedback ? (_jsx("span", { className: `ml-auto text-xs ${feedback.kind === "error" ? "text-destructive" : "text-muted-foreground"}`, children: feedback.message })) : null] }), confirmOpen ? (_jsx(ConfirmRerunDialog, { onCancel: () => setConfirmOpen(false), onConfirm: () => void doRerun(true), busy: busy === "rerun" })) : null] }));
68
+ }
69
+ function ConfirmRerunDialog({ onCancel, onConfirm, busy, }) {
70
+ const messages = useWorkflowRunsUiMessagesOrDefault();
71
+ return (_jsx("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4", children: _jsxs(Card, { className: "w-full max-w-md border-amber-500/40", children: [_jsx(CardHeader, { className: "pb-3", children: _jsxs(CardTitle, { className: "flex items-center gap-2 text-base", children: [_jsx(AlertTriangle, { className: "h-4 w-4 text-amber-500" }), messages.actions.confirmTitle] }) }), _jsxs(CardContent, { className: "space-y-3 text-sm", children: [_jsx("p", { children: messages.actions.confirmBody }), _jsx("p", { className: "text-muted-foreground text-xs", children: messages.actions.confirmTip }), _jsxs("div", { className: "flex justify-end gap-2 pt-2", children: [_jsx(Button, { variant: "ghost", size: "sm", onClick: onCancel, disabled: busy, children: messages.actions.cancel }), _jsxs(Button, { variant: "default", size: "sm", onClick: onConfirm, disabled: busy, children: [_jsx(RotateCw, { className: `mr-1.5 h-3.5 w-3.5 ${busy ? "animate-spin" : ""}` }), messages.actions.rerunAnyway] })] })] })] }) }));
72
+ }
@@ -0,0 +1,10 @@
1
+ import type { WorkflowRunsApi } from "../types.js";
2
+ export interface WorkflowRunDetailPageProps {
3
+ api: WorkflowRunsApi;
4
+ runId: string;
5
+ onOpenRun?: (id: string) => void;
6
+ pollIntervalMs?: number;
7
+ className?: string;
8
+ }
9
+ export declare function WorkflowRunDetailPage({ api, runId, onOpenRun, pollIntervalMs, className, }: WorkflowRunDetailPageProps): import("react/jsx-runtime").JSX.Element;
10
+ //# sourceMappingURL=workflow-run-detail-page.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workflow-run-detail-page.d.ts","sourceRoot":"","sources":["../../src/components/workflow-run-detail-page.tsx"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAIV,eAAe,EAChB,MAAM,aAAa,CAAA;AAapB,MAAM,WAAW,0BAA0B;IACzC,GAAG,EAAE,eAAe,CAAA;IACpB,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAA;IAChC,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,qBAAqB,CAAC,EACpC,GAAG,EACH,KAAK,EACL,SAAS,EACT,cAAqB,EACrB,SAAS,GACV,EAAE,0BAA0B,2CA6D5B"}
@@ -0,0 +1,96 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Badge } from "@voyantjs/ui/components/badge";
4
+ import { Card, CardContent, CardHeader, CardTitle } from "@voyantjs/ui/components/card";
5
+ import { ChevronDown, ChevronRight, Link2 } from "lucide-react";
6
+ import { useEffect, useMemo, useState } from "react";
7
+ 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 "@voyantjs/ui/components/button";
4
+ import { Card, CardContent, CardHeader, CardTitle } from "@voyantjs/ui/components/card";
5
+ import { Combobox, ComboboxCollection, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList, } from "@voyantjs/ui/components/combobox";
6
+ import { Input } from "@voyantjs/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"}