@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.
- package/LICENSE +202 -0
- package/NOTICE +5 -0
- package/dist/index.d.ts +1078 -0
- package/dist/index.js +6407 -0
- package/dist/index.js.map +1 -0
- package/package.json +86 -0
- package/src/components/AnimatedDurablexMark.tsx +35 -0
- package/src/components/AppStatusBadge.tsx +17 -0
- package/src/components/AppTag.tsx +17 -0
- package/src/components/AppsView.tsx +226 -0
- package/src/components/BulkReplayButton.tsx +52 -0
- package/src/components/CursorPager.tsx +50 -0
- package/src/components/DeliveriesSplit.tsx +187 -0
- package/src/components/DeliveryDetail.tsx +188 -0
- package/src/components/DurablexLogo.tsx +12 -0
- package/src/components/EndpointFormDialog.tsx +153 -0
- package/src/components/EndpointRow.tsx +172 -0
- package/src/components/EndpointsTab.tsx +83 -0
- package/src/components/EventsList.tsx +170 -0
- package/src/components/EventsView.tsx +24 -0
- package/src/components/Facts.tsx +14 -0
- package/src/components/FlowControlBadge.tsx +23 -0
- package/src/components/FlowControlSection.tsx +82 -0
- package/src/components/FlowSummary.tsx +47 -0
- package/src/components/FormField.tsx +10 -0
- package/src/components/GlyphBadge.tsx +41 -0
- package/src/components/JsonBlock.tsx +48 -0
- package/src/components/JsonEditor.tsx +91 -0
- package/src/components/LogList.tsx +45 -0
- package/src/components/Meta.tsx +31 -0
- package/src/components/OverviewView.tsx +39 -0
- package/src/components/PayloadTabs.tsx +70 -0
- package/src/components/ReceiverFormDialog.tsx +123 -0
- package/src/components/ReceiversTab.tsx +194 -0
- package/src/components/ReplayRunDialog.tsx +112 -0
- package/src/components/ResumeMark.tsx +38 -0
- package/src/components/RetryFromStepButton.tsx +44 -0
- package/src/components/RunCancelButton.tsx +23 -0
- package/src/components/RunControlHistory.tsx +71 -0
- package/src/components/RunInspector.test.tsx +78 -0
- package/src/components/RunInspector.tsx +297 -0
- package/src/components/RunInspectorActions.tsx +40 -0
- package/src/components/RunPauseButton.tsx +34 -0
- package/src/components/RunnerLiveBadge.tsx +11 -0
- package/src/components/RunsFilterBar.tsx +180 -0
- package/src/components/RunsTable.tsx +110 -0
- package/src/components/RunsTableHead.tsx +19 -0
- package/src/components/RunsTableLoader.tsx +10 -0
- package/src/components/RunsTablePlaceholder.tsx +19 -0
- package/src/components/RunsTableRow.tsx +103 -0
- package/src/components/RunsView.test.tsx +46 -0
- package/src/components/RunsView.tsx +243 -0
- package/src/components/ScheduledBadge.tsx +15 -0
- package/src/components/SecretReveal.tsx +45 -0
- package/src/components/SectionHeader.tsx +10 -0
- package/src/components/StatTileGrid.tsx +71 -0
- package/src/components/StatsTiles.tsx +66 -0
- package/src/components/StatusBadge.tsx +50 -0
- package/src/components/StepFlow.tsx +105 -0
- package/src/components/StepGlyph.tsx +25 -0
- package/src/components/StepInspector.tsx +44 -0
- package/src/components/StepRow.tsx +69 -0
- package/src/components/StepTabsView.tsx +51 -0
- package/src/components/StepTimeline.tsx +87 -0
- package/src/components/TableStatusRows.tsx +54 -0
- package/src/components/TriggerEventDialog.tsx +180 -0
- package/src/components/TriggerEventResult.tsx +61 -0
- package/src/components/WebhookBadges.tsx +69 -0
- package/src/components/WebhookStatusBadge.tsx +25 -0
- package/src/components/WebhooksView.tsx +69 -0
- package/src/components/WorkflowDetail.tsx +149 -0
- package/src/components/WorkflowRunAction.tsx +46 -0
- package/src/components/WorkflowRunDialog.tsx +187 -0
- package/src/components/WorkflowsView.tsx +168 -0
- package/src/components/charts/ChartCard.tsx +19 -0
- package/src/components/charts/RunCharts.tsx +31 -0
- package/src/components/charts/RunLatencyChart.tsx +71 -0
- package/src/components/charts/RunsOverTimeChart.tsx +60 -0
- package/src/components/filters/AppFilter.tsx +65 -0
- package/src/components/filters/FilterDropdown.tsx +33 -0
- package/src/components/filters/FilterDropdownButton.tsx +31 -0
- package/src/components/filters/FilterDropdownItem.tsx +37 -0
- package/src/components/filters/TimeRangeFilter.tsx +43 -0
- package/src/components/filters/TimeZoneFilter.tsx +40 -0
- package/src/components/filters/use-click-outside.ts +18 -0
- package/src/components/filters-pager.test.tsx +94 -0
- package/src/components/marks-geometry.ts +10 -0
- package/src/components/replay-dialog.test.tsx +18 -0
- package/src/components/run-components.test.tsx +126 -0
- package/src/components/run-controls.test.tsx +97 -0
- package/src/hooks/use-confirm-action.ts +19 -0
- package/src/hooks/use-copy.ts +22 -0
- package/src/hooks/use-keyset-pager.ts +34 -0
- package/src/hooks/use-mobile.ts +16 -0
- package/src/index.ts +165 -0
- package/src/lib/app-color.test.ts +32 -0
- package/src/lib/app-color.ts +8 -0
- package/src/lib/control-action.ts +36 -0
- package/src/lib/flow-control.ts +77 -0
- package/src/lib/format.test.ts +102 -0
- package/src/lib/format.ts +45 -0
- package/src/lib/json-highlight.test.ts +36 -0
- package/src/lib/json-highlight.ts +64 -0
- package/src/lib/run-filters.ts +8 -0
- package/src/lib/run-logs.test.ts +80 -0
- package/src/lib/run-logs.ts +34 -0
- package/src/lib/run-progress.test.ts +109 -0
- package/src/lib/run-progress.ts +44 -0
- package/src/lib/run-sort.test.ts +40 -0
- package/src/lib/run-sort.ts +19 -0
- package/src/lib/status-label.test.ts +35 -0
- package/src/lib/status-label.ts +13 -0
- package/src/lib/step-detail.test.ts +122 -0
- package/src/lib/step-detail.ts +35 -0
- package/src/lib/step-display.test.ts +19 -0
- package/src/lib/step-display.ts +13 -0
- package/src/lib/step-timeline.test.ts +89 -0
- package/src/lib/step-timeline.ts +50 -0
- package/src/lib/table.ts +2 -0
- package/src/lib/theme.ts +35 -0
- package/src/lib/time-range.ts +81 -0
- package/src/lib/utils.ts +6 -0
- package/src/lib/webhook-view.test.ts +176 -0
- package/src/lib/webhook-view.ts +113 -0
- package/src/lib/workflow-run.test.ts +55 -0
- package/src/lib/workflow-run.ts +45 -0
- package/src/shell/AppShell.tsx +34 -0
- package/src/shell/Sidebar.tsx +78 -0
- package/src/shell/Topbar.tsx +22 -0
- package/src/styles.css +2204 -0
- package/src/test-utils.tsx +130 -0
- package/src/ui/button.tsx +67 -0
- package/src/ui/chart.tsx +337 -0
- package/src/ui/dialog.tsx +145 -0
- package/src/ui/input.tsx +19 -0
- package/src/ui/resizable.tsx +40 -0
- package/src/ui/separator.tsx +28 -0
- package/src/ui/sheet.tsx +128 -0
- package/src/ui/sidebar.tsx +665 -0
- package/src/ui/skeleton.tsx +15 -0
- package/src/ui/sonner.tsx +35 -0
- package/src/ui/table.tsx +87 -0
- 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
|
+
}
|