@durablex/react-ui 0.1.0-beta.3

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 (143) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +5 -0
  3. package/dist/index.d.ts +1078 -0
  4. package/dist/index.js +6407 -0
  5. package/dist/index.js.map +1 -0
  6. package/package.json +86 -0
  7. package/src/components/AnimatedDurablexMark.tsx +35 -0
  8. package/src/components/AppStatusBadge.tsx +17 -0
  9. package/src/components/AppTag.tsx +17 -0
  10. package/src/components/AppsView.tsx +226 -0
  11. package/src/components/BulkReplayButton.tsx +52 -0
  12. package/src/components/CursorPager.tsx +50 -0
  13. package/src/components/DeliveriesSplit.tsx +187 -0
  14. package/src/components/DeliveryDetail.tsx +188 -0
  15. package/src/components/DurablexLogo.tsx +12 -0
  16. package/src/components/EndpointFormDialog.tsx +153 -0
  17. package/src/components/EndpointRow.tsx +172 -0
  18. package/src/components/EndpointsTab.tsx +83 -0
  19. package/src/components/EventsList.tsx +170 -0
  20. package/src/components/EventsView.tsx +24 -0
  21. package/src/components/Facts.tsx +14 -0
  22. package/src/components/FlowControlBadge.tsx +23 -0
  23. package/src/components/FlowControlSection.tsx +82 -0
  24. package/src/components/FlowSummary.tsx +47 -0
  25. package/src/components/FormField.tsx +10 -0
  26. package/src/components/GlyphBadge.tsx +41 -0
  27. package/src/components/JsonBlock.tsx +48 -0
  28. package/src/components/JsonEditor.tsx +91 -0
  29. package/src/components/LogList.tsx +45 -0
  30. package/src/components/Meta.tsx +31 -0
  31. package/src/components/OverviewView.tsx +39 -0
  32. package/src/components/PayloadTabs.tsx +70 -0
  33. package/src/components/ReceiverFormDialog.tsx +123 -0
  34. package/src/components/ReceiversTab.tsx +194 -0
  35. package/src/components/ReplayRunDialog.tsx +112 -0
  36. package/src/components/ResumeMark.tsx +38 -0
  37. package/src/components/RetryFromStepButton.tsx +44 -0
  38. package/src/components/RunCancelButton.tsx +23 -0
  39. package/src/components/RunControlHistory.tsx +71 -0
  40. package/src/components/RunInspector.test.tsx +78 -0
  41. package/src/components/RunInspector.tsx +297 -0
  42. package/src/components/RunInspectorActions.tsx +40 -0
  43. package/src/components/RunPauseButton.tsx +34 -0
  44. package/src/components/RunnerLiveBadge.tsx +11 -0
  45. package/src/components/RunsFilterBar.tsx +180 -0
  46. package/src/components/RunsTable.tsx +110 -0
  47. package/src/components/RunsTableHead.tsx +19 -0
  48. package/src/components/RunsTableLoader.tsx +10 -0
  49. package/src/components/RunsTablePlaceholder.tsx +19 -0
  50. package/src/components/RunsTableRow.tsx +103 -0
  51. package/src/components/RunsView.test.tsx +46 -0
  52. package/src/components/RunsView.tsx +243 -0
  53. package/src/components/ScheduledBadge.tsx +15 -0
  54. package/src/components/SecretReveal.tsx +45 -0
  55. package/src/components/SectionHeader.tsx +10 -0
  56. package/src/components/StatTileGrid.tsx +71 -0
  57. package/src/components/StatsTiles.tsx +66 -0
  58. package/src/components/StatusBadge.tsx +50 -0
  59. package/src/components/StepFlow.tsx +105 -0
  60. package/src/components/StepGlyph.tsx +25 -0
  61. package/src/components/StepInspector.tsx +44 -0
  62. package/src/components/StepRow.tsx +69 -0
  63. package/src/components/StepTabsView.tsx +51 -0
  64. package/src/components/StepTimeline.tsx +87 -0
  65. package/src/components/TableStatusRows.tsx +54 -0
  66. package/src/components/TriggerEventDialog.tsx +180 -0
  67. package/src/components/TriggerEventResult.tsx +61 -0
  68. package/src/components/WebhookBadges.tsx +69 -0
  69. package/src/components/WebhookStatusBadge.tsx +25 -0
  70. package/src/components/WebhooksView.tsx +69 -0
  71. package/src/components/WorkflowDetail.tsx +149 -0
  72. package/src/components/WorkflowRunAction.tsx +46 -0
  73. package/src/components/WorkflowRunDialog.tsx +187 -0
  74. package/src/components/WorkflowsView.tsx +168 -0
  75. package/src/components/charts/ChartCard.tsx +19 -0
  76. package/src/components/charts/RunCharts.tsx +31 -0
  77. package/src/components/charts/RunLatencyChart.tsx +71 -0
  78. package/src/components/charts/RunsOverTimeChart.tsx +60 -0
  79. package/src/components/filters/AppFilter.tsx +65 -0
  80. package/src/components/filters/FilterDropdown.tsx +33 -0
  81. package/src/components/filters/FilterDropdownButton.tsx +31 -0
  82. package/src/components/filters/FilterDropdownItem.tsx +37 -0
  83. package/src/components/filters/TimeRangeFilter.tsx +43 -0
  84. package/src/components/filters/TimeZoneFilter.tsx +40 -0
  85. package/src/components/filters/use-click-outside.ts +18 -0
  86. package/src/components/filters-pager.test.tsx +94 -0
  87. package/src/components/marks-geometry.ts +10 -0
  88. package/src/components/replay-dialog.test.tsx +18 -0
  89. package/src/components/run-components.test.tsx +126 -0
  90. package/src/components/run-controls.test.tsx +97 -0
  91. package/src/hooks/use-confirm-action.ts +19 -0
  92. package/src/hooks/use-copy.ts +22 -0
  93. package/src/hooks/use-keyset-pager.ts +34 -0
  94. package/src/hooks/use-mobile.ts +16 -0
  95. package/src/index.ts +165 -0
  96. package/src/lib/app-color.test.ts +32 -0
  97. package/src/lib/app-color.ts +8 -0
  98. package/src/lib/control-action.ts +36 -0
  99. package/src/lib/flow-control.ts +77 -0
  100. package/src/lib/format.test.ts +102 -0
  101. package/src/lib/format.ts +45 -0
  102. package/src/lib/json-highlight.test.ts +36 -0
  103. package/src/lib/json-highlight.ts +64 -0
  104. package/src/lib/run-filters.ts +8 -0
  105. package/src/lib/run-logs.test.ts +80 -0
  106. package/src/lib/run-logs.ts +34 -0
  107. package/src/lib/run-progress.test.ts +109 -0
  108. package/src/lib/run-progress.ts +44 -0
  109. package/src/lib/run-sort.test.ts +40 -0
  110. package/src/lib/run-sort.ts +19 -0
  111. package/src/lib/status-label.test.ts +35 -0
  112. package/src/lib/status-label.ts +13 -0
  113. package/src/lib/step-detail.test.ts +122 -0
  114. package/src/lib/step-detail.ts +35 -0
  115. package/src/lib/step-display.test.ts +19 -0
  116. package/src/lib/step-display.ts +13 -0
  117. package/src/lib/step-timeline.test.ts +89 -0
  118. package/src/lib/step-timeline.ts +50 -0
  119. package/src/lib/table.ts +2 -0
  120. package/src/lib/theme.ts +35 -0
  121. package/src/lib/time-range.ts +81 -0
  122. package/src/lib/utils.ts +6 -0
  123. package/src/lib/webhook-view.test.ts +176 -0
  124. package/src/lib/webhook-view.ts +113 -0
  125. package/src/lib/workflow-run.test.ts +55 -0
  126. package/src/lib/workflow-run.ts +45 -0
  127. package/src/shell/AppShell.tsx +34 -0
  128. package/src/shell/Sidebar.tsx +78 -0
  129. package/src/shell/Topbar.tsx +22 -0
  130. package/src/styles.css +2204 -0
  131. package/src/test-utils.tsx +130 -0
  132. package/src/ui/button.tsx +67 -0
  133. package/src/ui/chart.tsx +337 -0
  134. package/src/ui/dialog.tsx +145 -0
  135. package/src/ui/input.tsx +19 -0
  136. package/src/ui/resizable.tsx +40 -0
  137. package/src/ui/separator.tsx +28 -0
  138. package/src/ui/sheet.tsx +128 -0
  139. package/src/ui/sidebar.tsx +665 -0
  140. package/src/ui/skeleton.tsx +15 -0
  141. package/src/ui/sonner.tsx +35 -0
  142. package/src/ui/table.tsx +87 -0
  143. package/src/ui/tooltip.tsx +51 -0
@@ -0,0 +1,78 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { screen } from "@testing-library/react";
3
+ import { RunInspector } from "./RunInspector";
4
+ import { fakeClient, makeRun, makeStep, renderWithClient } from "../test-utils";
5
+
6
+ const noop = () => {};
7
+
8
+ describe("RunInspector", () => {
9
+ it("renders a loaded run: workflow name, id, and its steps", async () => {
10
+ const client = fakeClient({
11
+ run: makeRun({ id: "run_42", workflowName: "sendEmail", status: "succeeded" }),
12
+ steps: [makeStep({ name: "validate" }), makeStep({ name: "deliver", index: 1 })],
13
+ });
14
+ renderWithClient(<RunInspector runId="run_42" onClose={noop} />, client);
15
+
16
+ expect(await screen.findByText("sendEmail")).toBeTruthy();
17
+ expect(screen.getByText("validate")).toBeTruthy();
18
+ expect(screen.getByText("deliver")).toBeTruthy();
19
+ expect(screen.getAllByText("run_42").length).toBeGreaterThan(0);
20
+ });
21
+
22
+ it("shows the error message when the run fails to load", async () => {
23
+ const client = fakeClient({ getError: new Error("run not found") });
24
+ renderWithClient(<RunInspector runId="ghost" onClose={noop} />, client);
25
+
26
+ expect(await screen.findByText("run not found")).toBeTruthy();
27
+ });
28
+
29
+ it("offers a step retry on a TERMINAL run (slot fed runId + stepName)", async () => {
30
+ const renderStepRetry = vi.fn((_runId: string, stepName: string) => (
31
+ <span>retry:{stepName}</span>
32
+ ));
33
+ const client = fakeClient({
34
+ run: makeRun({ id: "run_1", status: "failed" }),
35
+ steps: [makeStep({ name: "validate", status: "failed", error: { message: "boom" } })],
36
+ });
37
+ renderWithClient(
38
+ <RunInspector runId="run_1" onClose={noop} renderStepRetry={renderStepRetry} />,
39
+ client,
40
+ );
41
+
42
+ await screen.findByText("sendEmail");
43
+ expect(renderStepRetry).toHaveBeenCalledWith("run_1", "validate");
44
+ });
45
+
46
+ it("does NOT offer a step retry on an ACTIVE run", async () => {
47
+ const renderStepRetry = vi.fn(() => <span>retry</span>);
48
+ const client = fakeClient({
49
+ run: makeRun({ id: "run_1", status: "running" }),
50
+ steps: [makeStep({ name: "validate", status: "running" })],
51
+ });
52
+ renderWithClient(
53
+ <RunInspector runId="run_1" onClose={noop} renderStepRetry={renderStepRetry} />,
54
+ client,
55
+ );
56
+
57
+ await screen.findByText("sendEmail");
58
+ expect(renderStepRetry).not.toHaveBeenCalled();
59
+ });
60
+
61
+ it("feeds renderActions the run and renderControlHistory the run id", async () => {
62
+ const renderActions = vi.fn((run: { status: string }) => <span>act:{run.status}</span>);
63
+ const renderControlHistory = vi.fn((id: string) => <span>hist:{id}</span>);
64
+ const client = fakeClient({ run: makeRun({ id: "run_7", status: "succeeded" }) });
65
+ renderWithClient(
66
+ <RunInspector
67
+ runId="run_7"
68
+ onClose={noop}
69
+ renderActions={renderActions}
70
+ renderControlHistory={renderControlHistory}
71
+ />,
72
+ client,
73
+ );
74
+
75
+ expect(await screen.findByText("act:succeeded")).toBeTruthy();
76
+ expect(screen.getByText("hist:run_7")).toBeTruthy();
77
+ });
78
+ });
@@ -0,0 +1,297 @@
1
+ import { BarChartHorizontalBig, Check, Copy, Loader2, Workflow, X } from "lucide-react";
2
+ import { type ReactNode, useMemo, useState } from "react";
3
+ import {
4
+ ACTIVE_STATUSES,
5
+ type Run,
6
+ type Step,
7
+ TERMINAL_STATUSES,
8
+ useRun,
9
+ useRunStats,
10
+ useRunStream,
11
+ useRunSteps,
12
+ useWorkflows,
13
+ } from "@durablex/react";
14
+ import { useCopyToClipboard } from "../hooks/use-copy";
15
+ import { formatDuration, formatNextFire, formatTime } from "../lib/format";
16
+ import { groupLogs, NO_LOGS, type RunLogs, stepLogKey } from "../lib/run-logs";
17
+ import { stepProgress } from "../lib/run-progress";
18
+ import { Meta } from "./Meta";
19
+ import { PayloadTabs } from "./PayloadTabs";
20
+ import { StatusBadge } from "./StatusBadge";
21
+ import { StepFlow } from "./StepFlow";
22
+ import { StepRow } from "./StepRow";
23
+ import { StepTimeline } from "./StepTimeline";
24
+
25
+ type StepsView = "list" | "flow" | "timeline";
26
+
27
+ export interface RunInspectorProps {
28
+ runId: string;
29
+ onClose(): void;
30
+ onFilterByEvent?: (eventId: string, eventName?: string) => void;
31
+ onOpenRun?: (runId: string) => void;
32
+ onShowReplays?: (runId: string) => void;
33
+ // Control actions stay in the consumer; the kit feeds each slot the data it needs.
34
+ // renderStepRetry is offered only on a terminal run, so the consumer never gates it.
35
+ renderActions?: (run: Run) => ReactNode;
36
+ renderControlHistory?: (runId: string) => ReactNode;
37
+ renderStepRetry?: (runId: string, stepName: string) => ReactNode;
38
+ }
39
+
40
+ export function RunInspector({
41
+ runId,
42
+ onClose,
43
+ onFilterByEvent,
44
+ onOpenRun,
45
+ onShowReplays,
46
+ renderActions,
47
+ renderControlHistory,
48
+ renderStepRetry,
49
+ }: RunInspectorProps) {
50
+ const run = useRun(runId);
51
+ const status = run.data?.status;
52
+ const isActive = status !== undefined && ACTIVE_STATUSES.has(status);
53
+ const isPaused = status === "paused";
54
+ const retryRunId = status !== undefined && TERMINAL_STATUSES.has(status) ? runId : undefined;
55
+ const steps = useRunSteps(runId, isActive);
56
+ const frames = useRunStream(runId);
57
+ const logs = useMemo(() => groupLogs(frames), [frames]);
58
+ const { data: workflows } = useWorkflows();
59
+ const replayCount = useRunStats({ replayOf: runId }, { poll: false }).data?.total ?? 0;
60
+ const [view, setView] = useState<StepsView>("list");
61
+
62
+ const stepRetry =
63
+ retryRunId && renderStepRetry
64
+ ? (stepName: string) => renderStepRetry(retryRunId, stepName)
65
+ : undefined;
66
+
67
+ if (run.isLoading) {
68
+ return (
69
+ <div className="run-panel">
70
+ <div className="steps-loading">
71
+ <Loader2 className="size-4 animate-spin" />
72
+ </div>
73
+ </div>
74
+ );
75
+ }
76
+
77
+ if (run.isError || !run.data) {
78
+ return (
79
+ <div className="run-panel">
80
+ <div className="fi-empty">
81
+ {run.error instanceof Error ? run.error.message : "Run not found."}
82
+ </div>
83
+ </div>
84
+ );
85
+ }
86
+
87
+ const r = run.data;
88
+ const rows = steps.data ?? [];
89
+ const progress = stepProgress(r, rows);
90
+ const wf = workflows?.find((w) => w.name === r.workflowName && w.app === r.app);
91
+ const scheduled = r.triggerKind === "cron";
92
+ const sourceEventId = r.eventId;
93
+ const replayOf = r.replayOf;
94
+ const nextFire = scheduled ? wf?.schedules?.[0]?.nextFireAt : undefined;
95
+
96
+ return (
97
+ <div className="run-panel">
98
+ <div className="panel-head">
99
+ <div className="ph-top">
100
+ <h2>
101
+ <span className="h2-name">{r.workflowName}</span>
102
+ </h2>
103
+ <div className="ph-actions">
104
+ {renderActions?.(r)}
105
+ <button
106
+ type="button"
107
+ className="iconbtn focusable"
108
+ aria-label="Close"
109
+ onClick={onClose}
110
+ >
111
+ <X />
112
+ </button>
113
+ </div>
114
+ </div>
115
+ <div className="ph-status">
116
+ <StatusBadge status={r.status} />
117
+ <span className="runid">
118
+ <span className="runid-text">{r.id}</span>
119
+ <CopyId id={r.id} />
120
+ </span>
121
+ </div>
122
+ </div>
123
+
124
+ <div className="panel-body">
125
+ <div className="metagrid">
126
+ <Meta label="Run id" value={r.id} />
127
+ <Meta label="App" value={r.app} />
128
+ {r.executedBy && <Meta label="Runner" value={r.executedBy} />}
129
+ <Meta label="Started" value={formatTime(r.startedAt)} />
130
+ <Meta label="Duration" value={formatDuration(r.durationMs)} />
131
+ <Meta label="Step" value={`${progress.ix}/${progress.total}`} />
132
+ <Meta label="Attempt" value={String(r.attempt)} />
133
+ {sourceEventId ? (
134
+ <Meta
135
+ label="Trigger"
136
+ value={r.eventName ?? "event"}
137
+ title="Show all runs from this event"
138
+ onClick={() => onFilterByEvent?.(sourceEventId, r.eventName)}
139
+ />
140
+ ) : (
141
+ <Meta label="Trigger" value={scheduled ? "Scheduled (cron)" : "Event"} />
142
+ )}
143
+ {nextFire && <Meta label="Next fire" value={formatNextFire(nextFire)} />}
144
+ {replayOf && (
145
+ <Meta
146
+ label="Replay of"
147
+ value={replayOf}
148
+ title="Open the run this was replayed from"
149
+ onClick={() => onOpenRun?.(replayOf)}
150
+ />
151
+ )}
152
+ {replayCount > 0 && (
153
+ <Meta
154
+ label="Replayed into"
155
+ value={`${replayCount} run${replayCount === 1 ? "" : "s"}`}
156
+ title="Show the runs replayed from this one"
157
+ onClick={() => onShowReplays?.(r.id)}
158
+ />
159
+ )}
160
+ <Meta label="Errors" value={String(r.errorCount)} error={r.errorCount > 0} />
161
+ <Meta label="Ended" value={formatTime(r.endedAt)} />
162
+ </div>
163
+
164
+ <PayloadTabs run={r} logs={logs.root} />
165
+
166
+ {renderControlHistory?.(r.id)}
167
+
168
+ <div className="steps-head">
169
+ <span className="sec-title">Steps</span>
170
+ <span className="steps-prog tnum">
171
+ {progress.ix}
172
+ <span className="sp-sep">/</span>
173
+ {progress.total}
174
+ </span>
175
+ <div className="steps-seg" role="tablist">
176
+ <button
177
+ type="button"
178
+ className="seg-btn"
179
+ role="tab"
180
+ aria-selected={view === "list"}
181
+ onClick={() => setView("list")}
182
+ >
183
+ List
184
+ </button>
185
+ <button
186
+ type="button"
187
+ className="seg-btn"
188
+ role="tab"
189
+ aria-selected={view === "flow"}
190
+ onClick={() => setView("flow")}
191
+ >
192
+ <Workflow className="seg-ico" />
193
+ Flow
194
+ </button>
195
+ <button
196
+ type="button"
197
+ className="seg-btn"
198
+ role="tab"
199
+ aria-selected={view === "timeline"}
200
+ onClick={() => setView("timeline")}
201
+ >
202
+ <BarChartHorizontalBig className="seg-ico" />
203
+ Timeline
204
+ </button>
205
+ </div>
206
+ </div>
207
+
208
+ <RunStepsPanel
209
+ loading={steps.isLoading}
210
+ steps={rows}
211
+ view={view}
212
+ runPaused={isPaused}
213
+ logs={logs}
214
+ workflowName={r.workflowName}
215
+ runId={r.id}
216
+ renderStepRetry={stepRetry}
217
+ />
218
+ </div>
219
+ </div>
220
+ );
221
+ }
222
+
223
+ function RunStepsPanel({
224
+ loading,
225
+ steps,
226
+ view,
227
+ runPaused,
228
+ logs,
229
+ workflowName,
230
+ runId,
231
+ renderStepRetry,
232
+ }: {
233
+ loading: boolean;
234
+ steps: Step[];
235
+ view: StepsView;
236
+ runPaused: boolean;
237
+ logs: RunLogs;
238
+ workflowName: string;
239
+ runId: string;
240
+ renderStepRetry?: (stepName: string) => ReactNode;
241
+ }) {
242
+ if (loading) {
243
+ return (
244
+ <div className="steps-loading">
245
+ <Loader2 className="size-3.5 animate-spin" />
246
+ </div>
247
+ );
248
+ }
249
+ if (steps.length === 0) return <div className="steps-empty">No steps recorded yet.</div>;
250
+ if (view === "flow") {
251
+ return (
252
+ <StepFlow
253
+ key={runId}
254
+ steps={steps}
255
+ workflowName={workflowName}
256
+ runPaused={runPaused}
257
+ logs={logs}
258
+ renderStepRetry={renderStepRetry}
259
+ />
260
+ );
261
+ }
262
+ if (view === "timeline") {
263
+ return (
264
+ <StepTimeline
265
+ key={runId}
266
+ steps={steps}
267
+ runPaused={runPaused}
268
+ logs={logs}
269
+ renderStepRetry={renderStepRetry}
270
+ />
271
+ );
272
+ }
273
+ return (
274
+ <div className="steplist">
275
+ {steps.map((s) => (
276
+ <StepRow
277
+ key={`${s.index}-${s.attempt}`}
278
+ step={s}
279
+ index={s.index + 1}
280
+ current={s.status === "running" || s.status === "waiting"}
281
+ runPaused={runPaused}
282
+ logs={logs.byStep.get(stepLogKey(s.name, s.attempt)) ?? NO_LOGS}
283
+ renderStepRetry={renderStepRetry}
284
+ />
285
+ ))}
286
+ </div>
287
+ );
288
+ }
289
+
290
+ function CopyId({ id }: { id: string }) {
291
+ const { copied, copy } = useCopyToClipboard();
292
+ return (
293
+ <button type="button" className="copy" title="Copy run id" onClick={() => copy(id)}>
294
+ {copied ? <Check className="size-3" /> : <Copy className="size-3" />}
295
+ </button>
296
+ );
297
+ }
@@ -0,0 +1,40 @@
1
+ import { RotateCcw } from "lucide-react";
2
+ import { useState } from "react";
3
+ import { ACTIVE_STATUSES, type Run } from "@durablex/react";
4
+ import { RunCancelButton } from "./RunCancelButton";
5
+ import { RunPauseButton } from "./RunPauseButton";
6
+ import { ReplayRunDialog } from "./ReplayRunDialog";
7
+
8
+ // The run inspector's header controls: a live run can be paused/cancelled, a
9
+ // terminal run replayed. onOpenRun is the navigation the replay toast uses to
10
+ // jump to the forked run.
11
+ export function RunInspectorActions({
12
+ run,
13
+ onOpenRun,
14
+ }: {
15
+ run: Run;
16
+ onOpenRun?: (runId: string) => void;
17
+ }) {
18
+ const isLive = ACTIVE_STATUSES.has(run.status) || run.status === "paused";
19
+ if (isLive) {
20
+ return (
21
+ <>
22
+ <RunPauseButton id={run.id} status={run.status} />
23
+ <RunCancelButton id={run.id} />
24
+ </>
25
+ );
26
+ }
27
+ return <ReplayButton run={run} onOpenRun={onOpenRun} />;
28
+ }
29
+
30
+ function ReplayButton({ run, onOpenRun }: { run: Run; onOpenRun?: (runId: string) => void }) {
31
+ const [open, setOpen] = useState(false);
32
+ return (
33
+ <>
34
+ <button type="button" className="btn focusable" onClick={() => setOpen(true)}>
35
+ <RotateCcw /> Replay
36
+ </button>
37
+ <ReplayRunDialog run={run} open={open} onOpenChange={setOpen} onOpenRun={onOpenRun} />
38
+ </>
39
+ );
40
+ }
@@ -0,0 +1,34 @@
1
+ import { Loader2, Pause, Play } from "lucide-react";
2
+ import { usePauseRun, useResumeRun, type RunStatus } from "@durablex/react";
3
+
4
+ // Reversible and cheap, so unlike Cancel these fire on a single click with no confirm.
5
+ export function RunPauseButton({ id, status }: { id: string; status: RunStatus }) {
6
+ const pause = usePauseRun();
7
+ const resume = useResumeRun();
8
+
9
+ if (status === "paused") {
10
+ return (
11
+ <button
12
+ type="button"
13
+ className="btn focusable"
14
+ title="Resume - re-queues the run; it continues from the next step."
15
+ disabled={resume.isPending}
16
+ onClick={() => resume.mutate(id)}
17
+ >
18
+ {resume.isPending ? <Loader2 className="animate-spin" /> : <Play />} Resume
19
+ </button>
20
+ );
21
+ }
22
+
23
+ return (
24
+ <button
25
+ type="button"
26
+ className="btn focusable"
27
+ title="Pause - halts the run after the current step finishes."
28
+ disabled={pause.isPending}
29
+ onClick={() => pause.mutate(id)}
30
+ >
31
+ {pause.isPending ? <Loader2 className="animate-spin" /> : <Pause />} Pause
32
+ </button>
33
+ );
34
+ }
@@ -0,0 +1,11 @@
1
+ import { StatusBadge } from "./StatusBadge";
2
+
3
+ const RUNNER_BADGE = {
4
+ live: { status: "succeeded", label: "Live" },
5
+ stale: { status: "cancelled", label: "Stale" },
6
+ } as const;
7
+
8
+ export function RunnerLiveBadge({ live, small }: { live: boolean; small?: boolean }) {
9
+ const m = live ? RUNNER_BADGE.live : RUNNER_BADGE.stale;
10
+ return <StatusBadge status={m.status} label={m.label} small={small} />;
11
+ }
@@ -0,0 +1,180 @@
1
+ import { Braces, Search, X } from "lucide-react";
2
+ import type { ReactNode } from "react";
3
+ import { ALL_FILTER, type RunTypeFilter, type StatusFilter } from "../lib/run-filters";
4
+ import { AppFilter } from "./filters/AppFilter";
5
+ import { FilterDropdown } from "./filters/FilterDropdown";
6
+ import { FilterDropdownButton } from "./filters/FilterDropdownButton";
7
+ import { FilterDropdownItem } from "./filters/FilterDropdownItem";
8
+ import { TimeRangeFilter } from "./filters/TimeRangeFilter";
9
+
10
+ const STATUS_OPTIONS: { value: StatusFilter; label: string; color?: string }[] = [
11
+ { value: ALL_FILTER, label: "All statuses" },
12
+ { value: "queued", label: "Queued", color: "var(--st-queued-fg)" },
13
+ { value: "running", label: "Running", color: "var(--st-running-fg)" },
14
+ { value: "paused", label: "Paused", color: "var(--st-paused-fg)" },
15
+ { value: "succeeded", label: "Succeeded", color: "var(--st-succeeded-fg)" },
16
+ { value: "failed", label: "Failed", color: "var(--st-failed-fg)" },
17
+ { value: "cancelled", label: "Cancelled", color: "var(--st-cancelled-fg)" },
18
+ ];
19
+
20
+ const RUNTYPE_OPTIONS: { value: RunTypeFilter; label: string }[] = [
21
+ { value: ALL_FILTER, label: "All run types" },
22
+ { value: "triggered", label: "Event-triggered" },
23
+ { value: "scheduled", label: "Scheduled (cron)" },
24
+ ];
25
+
26
+ interface RunsFilterBarProps {
27
+ query: string;
28
+ onQuery(v: string): void;
29
+ deep: boolean;
30
+ onDeep(v: boolean): void;
31
+ time: string;
32
+ onTime(v: string): void;
33
+ status: StatusFilter;
34
+ onStatus(v: StatusFilter): void;
35
+ app: string;
36
+ onApp(v: string): void;
37
+ runType: RunTypeFilter;
38
+ onRunType(v: RunTypeFilter): void;
39
+ shown: number;
40
+ action?: ReactNode;
41
+ }
42
+
43
+ export function RunsFilterBar({
44
+ query,
45
+ onQuery,
46
+ deep,
47
+ onDeep,
48
+ time,
49
+ onTime,
50
+ status,
51
+ onStatus,
52
+ app,
53
+ onApp,
54
+ runType,
55
+ onRunType,
56
+ shown,
57
+ action,
58
+ }: RunsFilterBarProps) {
59
+ const statusOpt = STATUS_OPTIONS.find((o) => o.value === status);
60
+ const runTypeOpt = RUNTYPE_OPTIONS.find((o) => o.value === runType);
61
+ const activeCount =
62
+ (status !== ALL_FILTER ? 1 : 0) +
63
+ (app !== ALL_FILTER ? 1 : 0) +
64
+ (runType !== ALL_FILTER ? 1 : 0) +
65
+ (query ? 1 : 0) +
66
+ (deep ? 1 : 0);
67
+ const clearAll = () => {
68
+ onStatus(ALL_FILTER);
69
+ onApp(ALL_FILTER);
70
+ onRunType(ALL_FILTER);
71
+ onQuery("");
72
+ onDeep(false);
73
+ };
74
+
75
+ return (
76
+ <div className="filterbar">
77
+ <div className="fb-search-field">
78
+ <Search />
79
+ <input
80
+ placeholder={deep ? "Search input & result…" : "Search runs, ids, apps…"}
81
+ value={query}
82
+ onChange={(e) => onQuery(e.target.value)}
83
+ onKeyDown={(e) => {
84
+ if (e.key === "Escape") onQuery("");
85
+ }}
86
+ />
87
+ {query && (
88
+ <span className="x" title="Clear search" onClick={() => onQuery("")}>
89
+ <X style={{ width: 12, height: 12 }} />
90
+ </span>
91
+ )}
92
+ </div>
93
+
94
+ <button
95
+ type="button"
96
+ className="chip"
97
+ data-on={deep ? "1" : "0"}
98
+ title="Also search run input, result, and error content"
99
+ onClick={() => onDeep(!deep)}
100
+ >
101
+ <Braces /> Payloads
102
+ </button>
103
+
104
+ <TimeRangeFilter time={time} onTime={onTime} />
105
+
106
+ <FilterDropdown
107
+ width={180}
108
+ trigger={(open, toggle) => (
109
+ <FilterDropdownButton
110
+ open={open}
111
+ toggle={toggle}
112
+ keyLabel="Status"
113
+ value={statusOpt?.label === "All statuses" ? "All" : (statusOpt?.label ?? "All")}
114
+ dot={statusOpt?.color ?? null}
115
+ />
116
+ )}
117
+ >
118
+ {(close) =>
119
+ STATUS_OPTIONS.map((o) => (
120
+ <FilterDropdownItem
121
+ key={o.value}
122
+ active={status === o.value}
123
+ dot={o.color ?? null}
124
+ label={o.label}
125
+ onClick={() => {
126
+ onStatus(o.value);
127
+ close();
128
+ }}
129
+ />
130
+ ))
131
+ }
132
+ </FilterDropdown>
133
+
134
+ <AppFilter app={app} onApp={onApp} />
135
+
136
+ <FilterDropdown
137
+ width={180}
138
+ trigger={(open, toggle) => (
139
+ <FilterDropdownButton
140
+ open={open}
141
+ toggle={toggle}
142
+ keyLabel="Run type"
143
+ value={
144
+ runType === ALL_FILTER ? "All" : (runTypeOpt?.label.replace(/ \(.*\)/, "") ?? "All")
145
+ }
146
+ />
147
+ )}
148
+ >
149
+ {(close) =>
150
+ RUNTYPE_OPTIONS.map((o) => (
151
+ <FilterDropdownItem
152
+ key={o.value}
153
+ active={runType === o.value}
154
+ label={o.label}
155
+ onClick={() => {
156
+ onRunType(o.value);
157
+ close();
158
+ }}
159
+ />
160
+ ))
161
+ }
162
+ </FilterDropdown>
163
+
164
+ {activeCount > 0 && (
165
+ <button
166
+ type="button"
167
+ className="fb-btn focusable"
168
+ title="Clear all filters"
169
+ onClick={clearAll}
170
+ >
171
+ <X style={{ width: 12, height: 12 }} /> Clear
172
+ </button>
173
+ )}
174
+
175
+ <span className="fb-spacer" />
176
+ {action}
177
+ <span className="fb-meta">{shown} shown</span>
178
+ </div>
179
+ );
180
+ }