@voyantjs/workflows-ui 0.66.0 → 0.68.0

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.
@@ -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,131 @@
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 { Button } from "@voyantjs/ui/components/button";
5
+ import { Card, CardContent } from "@voyantjs/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, 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, messages, rootMessages) {
114
+ if (!lastRun)
115
+ return _jsx("span", { className: "text-muted-foreground", children: messages.lastRunNone });
116
+ const relative = formatRelative(lastRun.startedAt, rootMessages);
117
+ const label = lastRunLabel(lastRun.status, relative, messages);
118
+ return (_jsxs("span", { className: "inline-flex items-center gap-1.5", children: [_jsx(StatusIcon, { status: lastRun.status }), label] }));
119
+ }
120
+ function lastRunLabel(status, relative, messages) {
121
+ switch (status) {
122
+ case "succeeded":
123
+ return messages.lastRunSucceeded(relative);
124
+ case "failed":
125
+ return messages.lastRunFailed(relative);
126
+ case "running":
127
+ return messages.lastRunRunning;
128
+ case "cancelled":
129
+ return messages.lastRunCancelled(relative);
130
+ }
131
+ }
@@ -1 +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,sBAkG9B,CAAA;AAED,eAAO,MAAM,aAAa,wBAAmB,CAAA"}
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,sBAwI9B,CAAA;AAED,eAAO,MAAM,aAAa,wBAAmB,CAAA"}
package/dist/i18n/en.js CHANGED
@@ -92,5 +92,43 @@ export const workflowRunsUiEn = {
92
92
  format: {
93
93
  relativeNow: "now",
94
94
  },
95
+ schedules: {
96
+ title: "Workflow schedules",
97
+ subtitle: "Registered cron, interval, and one-shot schedules.",
98
+ environmentLabel: "Environment",
99
+ versionLabel: "Manifest",
100
+ workflowColumn: "Workflow",
101
+ scheduleColumn: "Schedule",
102
+ nextRunColumn: "Next run",
103
+ lastRunColumn: "Last run",
104
+ statusColumn: "Status",
105
+ actionsColumn: "Actions",
106
+ enabled: "Enabled",
107
+ disabledByRegistration: "Disabled",
108
+ disabledByEnvironment: "Disabled (env filtered)",
109
+ disabledByEnvFlag: "Disabled (env flag)",
110
+ envFlagOff: "Schedules disabled by environment flag — no schedules will fire.",
111
+ envFlagOn: "Schedules enabled.",
112
+ eventDriven: "event-driven",
113
+ cron: (expr, timezone) => `${expr} (${timezone})`,
114
+ every: (interval) => `every ${interval}`,
115
+ at: (timestamp) => `at ${timestamp}`,
116
+ triggerNow: "Trigger now",
117
+ triggering: "Triggering...",
118
+ triggerSuccess: "Run started.",
119
+ triggerFailed: "Trigger failed.",
120
+ refresh: "Refresh",
121
+ loading: "Loading schedules...",
122
+ loadError: "Could not load workflow schedules.",
123
+ empty: "No schedules registered for this environment.",
124
+ inFuture: (relative) => `in ${relative}`,
125
+ inPast: (relative) => `${relative} ago`,
126
+ notScheduled: "—",
127
+ lastRunSucceeded: (relative) => `succeeded ${relative} ago`,
128
+ lastRunFailed: (relative) => `failed ${relative} ago`,
129
+ lastRunRunning: "running",
130
+ lastRunCancelled: (relative) => `cancelled ${relative} ago`,
131
+ lastRunNone: "never run",
132
+ },
95
133
  };
96
134
  export const workflowsUiEn = workflowRunsUiEn;
@@ -82,5 +82,43 @@ export type WorkflowRunsUiMessages = {
82
82
  format: {
83
83
  relativeNow: string;
84
84
  };
85
+ schedules: {
86
+ title: string;
87
+ subtitle: string;
88
+ environmentLabel: string;
89
+ versionLabel: string;
90
+ workflowColumn: string;
91
+ scheduleColumn: string;
92
+ nextRunColumn: string;
93
+ lastRunColumn: string;
94
+ statusColumn: string;
95
+ actionsColumn: string;
96
+ enabled: string;
97
+ disabledByRegistration: string;
98
+ disabledByEnvironment: string;
99
+ disabledByEnvFlag: string;
100
+ envFlagOff: string;
101
+ envFlagOn: string;
102
+ eventDriven: string;
103
+ cron: (expr: string, timezone: string) => string;
104
+ every: (interval: string) => string;
105
+ at: (timestamp: string) => string;
106
+ triggerNow: string;
107
+ triggering: string;
108
+ triggerSuccess: string;
109
+ triggerFailed: string;
110
+ refresh: string;
111
+ loading: string;
112
+ loadError: string;
113
+ empty: string;
114
+ inFuture: (relative: string) => string;
115
+ inPast: (relative: string) => string;
116
+ notScheduled: string;
117
+ lastRunSucceeded: (relative: string) => string;
118
+ lastRunFailed: (relative: string) => string;
119
+ lastRunRunning: string;
120
+ lastRunCancelled: (relative: string) => string;
121
+ lastRunNone: string;
122
+ };
85
123
  };
86
124
  //# sourceMappingURL=messages.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../../src/i18n/messages.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../../src/i18n/messages.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"ro.d.ts","sourceRoot":"","sources":["../../src/i18n/ro.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAA;AAE3D,eAAO,MAAM,gBAAgB,EAAE,sBAoG9B,CAAA;AAED,eAAO,MAAM,aAAa,wBAAmB,CAAA"}
1
+ {"version":3,"file":"ro.d.ts","sourceRoot":"","sources":["../../src/i18n/ro.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAA;AAE3D,eAAO,MAAM,gBAAgB,EAAE,sBA0I9B,CAAA;AAED,eAAO,MAAM,aAAa,wBAAmB,CAAA"}
package/dist/i18n/ro.js CHANGED
@@ -94,5 +94,43 @@ export const workflowRunsUiRo = {
94
94
  format: {
95
95
  relativeNow: "acum",
96
96
  },
97
+ schedules: {
98
+ title: "Programari workflow",
99
+ subtitle: "Programari cron, interval si one-shot inregistrate.",
100
+ environmentLabel: "Mediu",
101
+ versionLabel: "Manifest",
102
+ workflowColumn: "Workflow",
103
+ scheduleColumn: "Programare",
104
+ nextRunColumn: "Urmatoarea rulare",
105
+ lastRunColumn: "Ultima rulare",
106
+ statusColumn: "Status",
107
+ actionsColumn: "Actiuni",
108
+ enabled: "Activ",
109
+ disabledByRegistration: "Dezactivat",
110
+ disabledByEnvironment: "Dezactivat (filtrat dupa mediu)",
111
+ disabledByEnvFlag: "Dezactivat (flag de mediu)",
112
+ envFlagOff: "Programarile sunt dezactivate de flag-ul de mediu — nimic nu va rula.",
113
+ envFlagOn: "Programarile sunt active.",
114
+ eventDriven: "declansat de eveniment",
115
+ cron: (expr, timezone) => `${expr} (${timezone})`,
116
+ every: (interval) => `la fiecare ${interval}`,
117
+ at: (timestamp) => `la ${timestamp}`,
118
+ triggerNow: "Declanseaza acum",
119
+ triggering: "Se declanseaza...",
120
+ triggerSuccess: "Rulare pornita.",
121
+ triggerFailed: "Declansarea a esuat.",
122
+ refresh: "Reincarca",
123
+ loading: "Se incarca programarile...",
124
+ loadError: "Programarile nu au putut fi incarcate.",
125
+ empty: "Nu exista programari inregistrate pentru acest mediu.",
126
+ inFuture: (relative) => `in ${relative}`,
127
+ inPast: (relative) => `acum ${relative}`,
128
+ notScheduled: "—",
129
+ lastRunSucceeded: (relative) => `reusita acum ${relative}`,
130
+ lastRunFailed: (relative) => `esuata acum ${relative}`,
131
+ lastRunRunning: "in rulare",
132
+ lastRunCancelled: (relative) => `anulata acum ${relative}`,
133
+ lastRunNone: "nu a rulat niciodata",
134
+ },
97
135
  };
98
136
  export const workflowsUiRo = workflowRunsUiRo;
package/dist/index.d.ts CHANGED
@@ -2,6 +2,8 @@ export type { WorkflowRunsApiClientOptions } from "./client.js";
2
2
  export { createWorkflowRunsApiClient } from "./client.js";
3
3
  export { WorkflowRunDetailPage, type WorkflowRunDetailPageProps, } from "./components/workflow-run-detail-page.js";
4
4
  export { WorkflowRunsPage, type WorkflowRunsPageProps, WorkflowRunsPageSkeleton, } from "./components/workflow-runs-page.js";
5
+ export { WorkflowSchedulesPage, type WorkflowSchedulesPageProps, } from "./components/workflow-schedules-page.js";
5
6
  export { getWorkflowRunsUiI18n, getWorkflowRunsUiI18n as getWorkflowsUiI18n, resolveWorkflowRunsUiMessages, resolveWorkflowRunsUiMessages as resolveWorkflowsUiMessages, useWorkflowRunsUiI18n, useWorkflowRunsUiI18n as useWorkflowsUiI18n, useWorkflowRunsUiI18nOrDefault, useWorkflowRunsUiI18nOrDefault as useWorkflowsUiI18nOrDefault, useWorkflowRunsUiMessages, useWorkflowRunsUiMessages as useWorkflowsUiMessages, useWorkflowRunsUiMessagesOrDefault, useWorkflowRunsUiMessagesOrDefault as useWorkflowsUiMessagesOrDefault, type WorkflowRunsUiMessageOverrides, type WorkflowRunsUiMessageOverrides as WorkflowsUiMessageOverrides, type WorkflowRunsUiMessages, type WorkflowRunsUiMessages as WorkflowsUiMessages, WorkflowRunsUiMessagesProvider, WorkflowRunsUiMessagesProvider as WorkflowsUiMessagesProvider, workflowRunsUiEn, workflowRunsUiEn as workflowsUiEn, workflowRunsUiMessageDefinitions, workflowRunsUiMessageDefinitions as workflowsUiMessageDefinitions, workflowRunsUiRo, workflowRunsUiRo as workflowsUiRo, } from "./i18n/index.js";
7
+ export { createWorkflowSchedulesApiClient, type ListWorkflowSchedulesResponse, type WorkflowScheduleDecl, type WorkflowScheduleSummary, type WorkflowSchedulesApi, type WorkflowSchedulesApiClientOptions, } from "./schedules-client.js";
6
8
  export type { ListWorkflowRunsQuery, ListWorkflowRunsResponse, WorkflowRun, WorkflowRunActionError, WorkflowRunActionResponse, WorkflowRunActionResult, WorkflowRunDetailResponse, WorkflowRunErrorPayload, WorkflowRunStatus, WorkflowRunStep, WorkflowRunStepStatus, WorkflowRunsApi, } from "./types.js";
7
9
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,4BAA4B,EAAE,MAAM,aAAa,CAAA;AAC/D,OAAO,EAAE,2BAA2B,EAAE,MAAM,aAAa,CAAA;AACzD,OAAO,EACL,qBAAqB,EACrB,KAAK,0BAA0B,GAChC,MAAM,0CAA0C,CAAA;AACjD,OAAO,EACL,gBAAgB,EAChB,KAAK,qBAAqB,EAC1B,wBAAwB,GACzB,MAAM,oCAAoC,CAAA;AAC3C,OAAO,EACL,qBAAqB,EACrB,qBAAqB,IAAI,kBAAkB,EAC3C,6BAA6B,EAC7B,6BAA6B,IAAI,0BAA0B,EAC3D,qBAAqB,EACrB,qBAAqB,IAAI,kBAAkB,EAC3C,8BAA8B,EAC9B,8BAA8B,IAAI,2BAA2B,EAC7D,yBAAyB,EACzB,yBAAyB,IAAI,sBAAsB,EACnD,kCAAkC,EAClC,kCAAkC,IAAI,+BAA+B,EACrE,KAAK,8BAA8B,EACnC,KAAK,8BAA8B,IAAI,2BAA2B,EAClE,KAAK,sBAAsB,EAC3B,KAAK,sBAAsB,IAAI,mBAAmB,EAClD,8BAA8B,EAC9B,8BAA8B,IAAI,2BAA2B,EAC7D,gBAAgB,EAChB,gBAAgB,IAAI,aAAa,EACjC,gCAAgC,EAChC,gCAAgC,IAAI,6BAA6B,EACjE,gBAAgB,EAChB,gBAAgB,IAAI,aAAa,GAClC,MAAM,iBAAiB,CAAA;AACxB,YAAY,EACV,qBAAqB,EACrB,wBAAwB,EACxB,WAAW,EACX,sBAAsB,EACtB,yBAAyB,EACzB,uBAAuB,EACvB,yBAAyB,EACzB,uBAAuB,EACvB,iBAAiB,EACjB,eAAe,EACf,qBAAqB,EACrB,eAAe,GAChB,MAAM,YAAY,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,4BAA4B,EAAE,MAAM,aAAa,CAAA;AAC/D,OAAO,EAAE,2BAA2B,EAAE,MAAM,aAAa,CAAA;AACzD,OAAO,EACL,qBAAqB,EACrB,KAAK,0BAA0B,GAChC,MAAM,0CAA0C,CAAA;AACjD,OAAO,EACL,gBAAgB,EAChB,KAAK,qBAAqB,EAC1B,wBAAwB,GACzB,MAAM,oCAAoC,CAAA;AAC3C,OAAO,EACL,qBAAqB,EACrB,KAAK,0BAA0B,GAChC,MAAM,yCAAyC,CAAA;AAChD,OAAO,EACL,qBAAqB,EACrB,qBAAqB,IAAI,kBAAkB,EAC3C,6BAA6B,EAC7B,6BAA6B,IAAI,0BAA0B,EAC3D,qBAAqB,EACrB,qBAAqB,IAAI,kBAAkB,EAC3C,8BAA8B,EAC9B,8BAA8B,IAAI,2BAA2B,EAC7D,yBAAyB,EACzB,yBAAyB,IAAI,sBAAsB,EACnD,kCAAkC,EAClC,kCAAkC,IAAI,+BAA+B,EACrE,KAAK,8BAA8B,EACnC,KAAK,8BAA8B,IAAI,2BAA2B,EAClE,KAAK,sBAAsB,EAC3B,KAAK,sBAAsB,IAAI,mBAAmB,EAClD,8BAA8B,EAC9B,8BAA8B,IAAI,2BAA2B,EAC7D,gBAAgB,EAChB,gBAAgB,IAAI,aAAa,EACjC,gCAAgC,EAChC,gCAAgC,IAAI,6BAA6B,EACjE,gBAAgB,EAChB,gBAAgB,IAAI,aAAa,GAClC,MAAM,iBAAiB,CAAA;AACxB,OAAO,EACL,gCAAgC,EAChC,KAAK,6BAA6B,EAClC,KAAK,oBAAoB,EACzB,KAAK,uBAAuB,EAC5B,KAAK,oBAAoB,EACzB,KAAK,iCAAiC,GACvC,MAAM,uBAAuB,CAAA;AAC9B,YAAY,EACV,qBAAqB,EACrB,wBAAwB,EACxB,WAAW,EACX,sBAAsB,EACtB,yBAAyB,EACzB,uBAAuB,EACvB,yBAAyB,EACzB,uBAAuB,EACvB,iBAAiB,EACjB,eAAe,EACf,qBAAqB,EACrB,eAAe,GAChB,MAAM,YAAY,CAAA"}
package/dist/index.js CHANGED
@@ -1,4 +1,6 @@
1
1
  export { createWorkflowRunsApiClient } from "./client.js";
2
2
  export { WorkflowRunDetailPage, } from "./components/workflow-run-detail-page.js";
3
3
  export { WorkflowRunsPage, WorkflowRunsPageSkeleton, } from "./components/workflow-runs-page.js";
4
+ export { WorkflowSchedulesPage, } from "./components/workflow-schedules-page.js";
4
5
  export { getWorkflowRunsUiI18n, getWorkflowRunsUiI18n as getWorkflowsUiI18n, resolveWorkflowRunsUiMessages, resolveWorkflowRunsUiMessages as resolveWorkflowsUiMessages, useWorkflowRunsUiI18n, useWorkflowRunsUiI18n as useWorkflowsUiI18n, useWorkflowRunsUiI18nOrDefault, useWorkflowRunsUiI18nOrDefault as useWorkflowsUiI18nOrDefault, useWorkflowRunsUiMessages, useWorkflowRunsUiMessages as useWorkflowsUiMessages, useWorkflowRunsUiMessagesOrDefault, useWorkflowRunsUiMessagesOrDefault as useWorkflowsUiMessagesOrDefault, WorkflowRunsUiMessagesProvider, WorkflowRunsUiMessagesProvider as WorkflowsUiMessagesProvider, workflowRunsUiEn, workflowRunsUiEn as workflowsUiEn, workflowRunsUiMessageDefinitions, workflowRunsUiMessageDefinitions as workflowsUiMessageDefinitions, workflowRunsUiRo, workflowRunsUiRo as workflowsUiRo, } from "./i18n/index.js";
6
+ export { createWorkflowSchedulesApiClient, } from "./schedules-client.js";
@@ -0,0 +1,2 @@
1
+ export { createWorkflowSchedulesApiClient, type ListWorkflowSchedulesResponse, type WorkflowScheduleDecl, type WorkflowScheduleSummary, type WorkflowSchedulesApi, type WorkflowSchedulesApiClientOptions, } from "@voyantjs/workflows-react/workflow-schedules-client";
2
+ //# sourceMappingURL=schedules-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schedules-client.d.ts","sourceRoot":"","sources":["../src/schedules-client.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gCAAgC,EAChC,KAAK,6BAA6B,EAClC,KAAK,oBAAoB,EACzB,KAAK,uBAAuB,EAC5B,KAAK,oBAAoB,EACzB,KAAK,iCAAiC,GACvC,MAAM,qDAAqD,CAAA"}
@@ -0,0 +1 @@
1
+ export { createWorkflowSchedulesApiClient, } from "@voyantjs/workflows-react/workflow-schedules-client";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voyantjs/workflows-ui",
3
- "version": "0.66.0",
3
+ "version": "0.68.0",
4
4
  "description": "Importable React admin UI for Voyant workflow run observability.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -46,14 +46,14 @@
46
46
  }
47
47
  },
48
48
  "dependencies": {
49
- "@voyantjs/i18n": "0.66.0",
50
- "@voyantjs/workflows-react": "0.66.0"
49
+ "@voyantjs/i18n": "0.68.0",
50
+ "@voyantjs/workflows-react": "0.68.0"
51
51
  },
52
52
  "peerDependencies": {
53
53
  "lucide-react": "^0.475.0 || ^1.0.0",
54
54
  "react": "^19.0.0",
55
55
  "react-dom": "^19.0.0",
56
- "@voyantjs/ui": "0.66.0"
56
+ "@voyantjs/ui": "0.68.0"
57
57
  },
58
58
  "devDependencies": {
59
59
  "@types/react": "^19.2.14",
@@ -63,9 +63,9 @@
63
63
  "react-dom": "^19.2.4",
64
64
  "typescript": "^6.0.2",
65
65
  "vitest": "^4.1.2",
66
- "@voyantjs/i18n": "0.66.0",
67
- "@voyantjs/ui": "0.66.0",
68
- "@voyantjs/voyant-typescript-config": "0.1.0"
66
+ "@voyantjs/ui": "0.68.0",
67
+ "@voyantjs/voyant-typescript-config": "0.1.0",
68
+ "@voyantjs/i18n": "0.68.0"
69
69
  },
70
70
  "files": [
71
71
  "dist",
@@ -0,0 +1,368 @@
1
+ "use client"
2
+
3
+ import { Badge } from "@voyantjs/ui/components/badge"
4
+ import { Button } from "@voyantjs/ui/components/button"
5
+ import { Card, CardContent } from "@voyantjs/ui/components/card"
6
+ import { AlertTriangle, CalendarClock, RefreshCw } from "lucide-react"
7
+ import { useCallback, useEffect, useMemo, useState } from "react"
8
+
9
+ import { useWorkflowRunsUiMessagesOrDefault, type WorkflowRunsUiMessages } from "../i18n/index.js"
10
+ import type {
11
+ ListWorkflowSchedulesResponse,
12
+ WorkflowScheduleDecl,
13
+ WorkflowScheduleSummary,
14
+ WorkflowSchedulesApi,
15
+ } from "../schedules-client.js"
16
+ import type { WorkflowRun, WorkflowRunStatus, WorkflowRunsApi } from "../types.js"
17
+ import { formatRelative, StatusIcon } from "./common.js"
18
+
19
+ type SchedulesMessages = WorkflowRunsUiMessages["schedules"]
20
+
21
+ export interface WorkflowSchedulesPageProps {
22
+ /** Schedules API — backed by `/api/schedules/:env`. */
23
+ schedulesApi: WorkflowSchedulesApi
24
+ /** Optional runs API — when provided, the page joins each row with the most recent matching run. */
25
+ runsApi?: WorkflowRunsApi
26
+ /**
27
+ * Optional trigger callback. When provided, each row renders a
28
+ * "Trigger now" button that calls this with the workflow id + the
29
+ * schedule's recorded `input` payload (if any).
30
+ */
31
+ onTriggerNow?: (workflowId: string, input: unknown) => Promise<void>
32
+ /** Manifest environment to inspect. Defaults to "production". */
33
+ environment?: "production" | "preview" | "development"
34
+ /** Auto-refresh interval (ms). Defaults to 30s. Pass 0 to disable. */
35
+ pollIntervalMs?: number
36
+ className?: string
37
+ }
38
+
39
+ export function WorkflowSchedulesPage({
40
+ schedulesApi,
41
+ runsApi,
42
+ onTriggerNow,
43
+ environment = "production",
44
+ pollIntervalMs = 30_000,
45
+ className,
46
+ }: WorkflowSchedulesPageProps) {
47
+ const rootMessages = useWorkflowRunsUiMessagesOrDefault()
48
+ const messages = rootMessages.schedules
49
+ const [response, setResponse] = useState<ListWorkflowSchedulesResponse | null>(null)
50
+ const [lastRuns, setLastRuns] = useState<Record<string, WorkflowRun | null>>({})
51
+ const [loading, setLoading] = useState(false)
52
+ const [error, setError] = useState<string | null>(null)
53
+ const [triggering, setTriggering] = useState<Record<string, boolean>>({})
54
+ const [triggerNotice, setTriggerNotice] = useState<{
55
+ kind: "success" | "error"
56
+ text: string
57
+ } | null>(null)
58
+
59
+ const refresh = useCallback(async () => {
60
+ setLoading(true)
61
+ try {
62
+ const next = await schedulesApi.listSchedules(environment)
63
+ setResponse(next)
64
+ setError(null)
65
+ if (runsApi) {
66
+ const uniqueIds = Array.from(new Set(next.data.map((entry) => entry.workflowId)))
67
+ const results = await Promise.all(
68
+ uniqueIds.map(async (workflowId) => {
69
+ try {
70
+ const runs = await runsApi.listRuns({ workflowName: workflowId, limit: 1 })
71
+ return [workflowId, runs.data[0] ?? null] as const
72
+ } catch {
73
+ return [workflowId, null] as const
74
+ }
75
+ }),
76
+ )
77
+ setLastRuns(Object.fromEntries(results))
78
+ }
79
+ } catch (err) {
80
+ setError(err instanceof Error ? err.message : messages.loadError)
81
+ } finally {
82
+ setLoading(false)
83
+ }
84
+ }, [environment, messages.loadError, runsApi, schedulesApi])
85
+
86
+ useEffect(() => {
87
+ void refresh()
88
+ if (!pollIntervalMs) return
89
+ const interval = setInterval(() => void refresh(), pollIntervalMs)
90
+ return () => clearInterval(interval)
91
+ }, [pollIntervalMs, refresh])
92
+
93
+ const showEnvFlag = response?.schedulesEnabledByEnv !== undefined
94
+ const envFlagOn = response?.schedulesEnabledByEnv === true
95
+
96
+ const triggerRow = useCallback(
97
+ async (entry: WorkflowScheduleSummary) => {
98
+ if (!onTriggerNow) return
99
+ setTriggering((prev) => ({ ...prev, [entry.scheduleId]: true }))
100
+ setTriggerNotice(null)
101
+ try {
102
+ await onTriggerNow(entry.workflowId, entry.schedule.input)
103
+ setTriggerNotice({ kind: "success", text: messages.triggerSuccess })
104
+ } catch (err) {
105
+ setTriggerNotice({
106
+ kind: "error",
107
+ text: err instanceof Error ? err.message : messages.triggerFailed,
108
+ })
109
+ } finally {
110
+ setTriggering((prev) => ({ ...prev, [entry.scheduleId]: false }))
111
+ }
112
+ },
113
+ [messages.triggerFailed, messages.triggerSuccess, onTriggerNow],
114
+ )
115
+
116
+ const rows = useMemo(() => response?.data ?? [], [response])
117
+
118
+ return (
119
+ <div className={`flex min-h-screen flex-col bg-background ${className ?? ""}`}>
120
+ <header className="sticky top-0 z-10 border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
121
+ <div className="container mx-auto flex flex-wrap items-center gap-3 px-4 py-3">
122
+ <CalendarClock className="h-5 w-5" />
123
+ <div className="min-w-0">
124
+ <h1 className="font-semibold text-base">{messages.title}</h1>
125
+ <p className="text-muted-foreground text-xs">{messages.subtitle}</p>
126
+ </div>
127
+ <div className="ml-auto flex items-center gap-2 text-muted-foreground text-xs">
128
+ <span>
129
+ {messages.environmentLabel}: <span className="font-mono">{environment}</span>
130
+ </span>
131
+ {response?.versionId ? (
132
+ <span>
133
+ {messages.versionLabel}:{" "}
134
+ <span className="font-mono">{response.versionId.slice(0, 12)}</span>
135
+ </span>
136
+ ) : null}
137
+ <Button
138
+ type="button"
139
+ variant="outline"
140
+ size="sm"
141
+ onClick={() => void refresh()}
142
+ disabled={loading}
143
+ >
144
+ <RefreshCw
145
+ className={`h-3.5 w-3.5 ${loading ? "animate-spin" : ""}`}
146
+ aria-hidden="true"
147
+ />
148
+ {messages.refresh}
149
+ </Button>
150
+ </div>
151
+ </div>
152
+ </header>
153
+
154
+ <main className="container mx-auto flex flex-1 flex-col gap-4 px-4 py-6">
155
+ {showEnvFlag && !envFlagOn ? (
156
+ <Card className="border-amber-500/40 bg-amber-500/5">
157
+ <CardContent className="flex items-center gap-2 pt-4 text-amber-700 text-sm dark:text-amber-300">
158
+ <AlertTriangle className="h-4 w-4" aria-hidden="true" />
159
+ {messages.envFlagOff}
160
+ </CardContent>
161
+ </Card>
162
+ ) : null}
163
+
164
+ {triggerNotice ? (
165
+ <Card
166
+ className={
167
+ triggerNotice.kind === "success"
168
+ ? "border-emerald-500/40 bg-emerald-500/5"
169
+ : "border-destructive/40 bg-destructive/5"
170
+ }
171
+ >
172
+ <CardContent
173
+ className={`pt-4 text-sm ${
174
+ triggerNotice.kind === "success"
175
+ ? "text-emerald-600 dark:text-emerald-300"
176
+ : "text-destructive"
177
+ }`}
178
+ >
179
+ {triggerNotice.text}
180
+ </CardContent>
181
+ </Card>
182
+ ) : null}
183
+
184
+ {error ? (
185
+ <Card className="border-destructive/40">
186
+ <CardContent className="pt-4 text-destructive text-sm">{error}</CardContent>
187
+ </Card>
188
+ ) : null}
189
+
190
+ {!error && rows.length === 0 && !loading ? (
191
+ <Card>
192
+ <CardContent className="pt-4 text-muted-foreground text-sm">
193
+ {messages.empty}
194
+ </CardContent>
195
+ </Card>
196
+ ) : null}
197
+
198
+ {rows.length > 0 ? (
199
+ <div className="overflow-hidden rounded-md border">
200
+ <table className="w-full text-sm">
201
+ <thead className="bg-muted/40 text-left text-xs uppercase">
202
+ <tr>
203
+ <th className="px-3 py-2 font-medium">{messages.workflowColumn}</th>
204
+ <th className="px-3 py-2 font-medium">{messages.scheduleColumn}</th>
205
+ <th className="px-3 py-2 font-medium">{messages.nextRunColumn}</th>
206
+ <th className="px-3 py-2 font-medium">{messages.lastRunColumn}</th>
207
+ <th className="px-3 py-2 font-medium">{messages.statusColumn}</th>
208
+ {onTriggerNow ? (
209
+ <th className="px-3 py-2 font-medium">{messages.actionsColumn}</th>
210
+ ) : null}
211
+ </tr>
212
+ </thead>
213
+ <tbody>
214
+ {rows.map((row) => (
215
+ <ScheduleRow
216
+ key={row.scheduleId}
217
+ row={row}
218
+ lastRun={lastRuns[row.workflowId] ?? null}
219
+ triggering={!!triggering[row.scheduleId]}
220
+ onTriggerNow={onTriggerNow ? () => void triggerRow(row) : undefined}
221
+ envFlagDisabled={showEnvFlag && !envFlagOn}
222
+ rootMessages={rootMessages}
223
+ />
224
+ ))}
225
+ </tbody>
226
+ </table>
227
+ </div>
228
+ ) : null}
229
+ </main>
230
+ </div>
231
+ )
232
+ }
233
+
234
+ function ScheduleRow({
235
+ row,
236
+ lastRun,
237
+ triggering,
238
+ onTriggerNow,
239
+ envFlagDisabled,
240
+ rootMessages,
241
+ }: {
242
+ row: WorkflowScheduleSummary
243
+ lastRun: WorkflowRun | null
244
+ triggering: boolean
245
+ onTriggerNow?: () => void
246
+ envFlagDisabled: boolean
247
+ rootMessages: WorkflowRunsUiMessages
248
+ }) {
249
+ const messages = rootMessages.schedules
250
+
251
+ return (
252
+ <tr className="border-t">
253
+ <td className="px-3 py-2 font-mono text-xs">{row.workflowId}</td>
254
+ <td className="px-3 py-2 font-mono text-xs">{formatScheduleDecl(row.schedule, messages)}</td>
255
+ <td className="px-3 py-2 text-xs text-muted-foreground">
256
+ {formatNextRun(row, messages, rootMessages)}
257
+ </td>
258
+ <td className="px-3 py-2 text-xs">{formatLastRun(lastRun, messages, rootMessages)}</td>
259
+ <td className="px-3 py-2">
260
+ <StatusPill row={row} envFlagDisabled={envFlagDisabled} messages={messages} />
261
+ </td>
262
+ {onTriggerNow ? (
263
+ <td className="px-3 py-2">
264
+ <Button
265
+ type="button"
266
+ size="sm"
267
+ variant="outline"
268
+ onClick={onTriggerNow}
269
+ disabled={triggering}
270
+ >
271
+ {triggering ? messages.triggering : messages.triggerNow}
272
+ </Button>
273
+ </td>
274
+ ) : null}
275
+ </tr>
276
+ )
277
+ }
278
+
279
+ function StatusPill({
280
+ row,
281
+ envFlagDisabled,
282
+ messages,
283
+ }: {
284
+ row: WorkflowScheduleSummary
285
+ envFlagDisabled: boolean
286
+ messages: SchedulesMessages
287
+ }) {
288
+ if (envFlagDisabled) {
289
+ return (
290
+ <Badge variant="outline" className="border-muted-foreground/30 text-muted-foreground">
291
+ {messages.disabledByEnvFlag}
292
+ </Badge>
293
+ )
294
+ }
295
+ if (row.disabledReason === "registration_disabled") {
296
+ return (
297
+ <Badge variant="outline" className="border-muted-foreground/30 text-muted-foreground">
298
+ {messages.disabledByRegistration}
299
+ </Badge>
300
+ )
301
+ }
302
+ if (row.disabledReason === "env_filtered") {
303
+ return (
304
+ <Badge variant="outline" className="border-muted-foreground/30 text-muted-foreground">
305
+ {messages.disabledByEnvironment}
306
+ </Badge>
307
+ )
308
+ }
309
+ return (
310
+ <Badge
311
+ variant="outline"
312
+ className="border-emerald-500/40 bg-emerald-500/10 text-emerald-600 dark:text-emerald-300"
313
+ >
314
+ {messages.enabled}
315
+ </Badge>
316
+ )
317
+ }
318
+
319
+ function formatScheduleDecl(decl: WorkflowScheduleDecl, messages: SchedulesMessages): string {
320
+ if (decl.cron) return messages.cron(decl.cron, decl.timezone ?? "UTC")
321
+ if (decl.every !== undefined) return messages.every(String(decl.every))
322
+ if (decl.at) return messages.at(decl.at)
323
+ return messages.eventDriven
324
+ }
325
+
326
+ function formatNextRun(
327
+ row: WorkflowScheduleSummary,
328
+ messages: SchedulesMessages,
329
+ rootMessages: WorkflowRunsUiMessages,
330
+ ): string {
331
+ if (!row.enabled || row.nextRunAt === null) return messages.notScheduled
332
+ const delta = row.nextRunAt - Date.now()
333
+ const relative = formatRelative(new Date(row.nextRunAt).toISOString(), rootMessages)
334
+ return delta >= 0 ? messages.inFuture(relative) : messages.inPast(relative)
335
+ }
336
+
337
+ function formatLastRun(
338
+ lastRun: WorkflowRun | null,
339
+ messages: SchedulesMessages,
340
+ rootMessages: WorkflowRunsUiMessages,
341
+ ) {
342
+ if (!lastRun) return <span className="text-muted-foreground">{messages.lastRunNone}</span>
343
+ const relative = formatRelative(lastRun.startedAt, rootMessages)
344
+ const label = lastRunLabel(lastRun.status, relative, messages)
345
+ return (
346
+ <span className="inline-flex items-center gap-1.5">
347
+ <StatusIcon status={lastRun.status} />
348
+ {label}
349
+ </span>
350
+ )
351
+ }
352
+
353
+ function lastRunLabel(
354
+ status: WorkflowRunStatus,
355
+ relative: string,
356
+ messages: SchedulesMessages,
357
+ ): string {
358
+ switch (status) {
359
+ case "succeeded":
360
+ return messages.lastRunSucceeded(relative)
361
+ case "failed":
362
+ return messages.lastRunFailed(relative)
363
+ case "running":
364
+ return messages.lastRunRunning
365
+ case "cancelled":
366
+ return messages.lastRunCancelled(relative)
367
+ }
368
+ }