@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,71 @@
1
+ import type { StatusFilter } from "../lib/run-filters";
2
+ import { ResumeMark } from "./ResumeMark";
3
+
4
+ export interface StatTile {
5
+ label: string;
6
+ value: number;
7
+ sub: string;
8
+ token: string;
9
+ mark?: boolean;
10
+ trend?: "up" | "down";
11
+ // When set, the tile is a button that filters the runs to this status (and shows
12
+ // an active state while that filter is on). Tiles without it stay static.
13
+ status?: StatusFilter;
14
+ active?: boolean;
15
+ }
16
+
17
+ export function StatTileGrid({
18
+ tiles,
19
+ onSelect,
20
+ }: {
21
+ tiles: StatTile[];
22
+ onSelect?: (status: StatusFilter) => void;
23
+ }) {
24
+ return (
25
+ <div className="stats">
26
+ {tiles.map((t) => {
27
+ const inner = (
28
+ <>
29
+ <div className="stat-top">
30
+ {t.mark ? (
31
+ <ResumeMark variant="run" size={12} className="stat-mark" />
32
+ ) : (
33
+ <i className="stat-dot" style={{ background: `var(${t.token})` }} />
34
+ )}
35
+ <span className="stat-label">{t.label}</span>
36
+ </div>
37
+ <div className="stat-val">{t.value}</div>
38
+ <div className="stat-sub">
39
+ <span className={t.trend ?? ""}>{t.sub}</span>
40
+ </div>
41
+ </>
42
+ );
43
+ if (t.status && onSelect) {
44
+ const status = t.status;
45
+ return (
46
+ <button
47
+ type="button"
48
+ className="stat stat-btn focusable"
49
+ key={t.label}
50
+ data-active={t.active ? "1" : "0"}
51
+ style={t.active ? { boxShadow: `inset 0 -2px 0 var(${t.token})` } : undefined}
52
+ title={
53
+ t.active
54
+ ? `Clear the ${t.label.toLowerCase()} filter`
55
+ : `Show ${t.label.toLowerCase()} runs`
56
+ }
57
+ onClick={() => onSelect(status)}
58
+ >
59
+ {inner}
60
+ </button>
61
+ );
62
+ }
63
+ return (
64
+ <div className="stat" key={t.label}>
65
+ {inner}
66
+ </div>
67
+ );
68
+ })}
69
+ </div>
70
+ );
71
+ }
@@ -0,0 +1,66 @@
1
+ import type { RunStats } from "@durablex/react";
2
+ import type { StatusFilter } from "../lib/run-filters";
3
+ import { StatTileGrid } from "./StatTileGrid";
4
+
5
+ const EMPTY: RunStats = {
6
+ total: 0,
7
+ active: 0,
8
+ queued: 0,
9
+ running: 0,
10
+ succeeded: 0,
11
+ failed: 0,
12
+ successRate: 100,
13
+ };
14
+
15
+ export function StatsTiles({
16
+ stats,
17
+ rangeLabel,
18
+ activeStatus,
19
+ onFilterStatus,
20
+ }: {
21
+ stats?: RunStats;
22
+ rangeLabel?: string;
23
+ activeStatus?: StatusFilter;
24
+ onFilterStatus?: (status: StatusFilter) => void;
25
+ }) {
26
+ const s = stats ?? EMPTY;
27
+
28
+ return (
29
+ <StatTileGrid
30
+ onSelect={onFilterStatus}
31
+ tiles={[
32
+ {
33
+ label: "Recent",
34
+ value: s.total,
35
+ sub: rangeLabel ?? "recent",
36
+ token: "--muted-foreground",
37
+ },
38
+ {
39
+ label: "Active",
40
+ value: s.active,
41
+ sub: "in progress",
42
+ token: "--st-running-fg",
43
+ mark: true,
44
+ },
45
+ {
46
+ label: "Succeeded",
47
+ value: s.succeeded,
48
+ sub: `${s.successRate}% success`,
49
+ token: "--st-succeeded-fg",
50
+ trend: "up",
51
+ status: "succeeded",
52
+ active: activeStatus === "succeeded",
53
+ },
54
+ {
55
+ label: "Failed",
56
+ value: s.failed,
57
+ sub: s.failed ? "need attention" : "all clear",
58
+ token: "--st-failed-fg",
59
+ trend: s.failed ? "down" : undefined,
60
+ status: "failed",
61
+ active: activeStatus === "failed",
62
+ },
63
+ ]}
64
+ />
65
+ );
66
+ }
@@ -0,0 +1,50 @@
1
+ import type { RunStatus, StepStatus } from "@durablex/react";
2
+ import { cn } from "../lib/utils";
3
+ import { ResumeMark } from "./ResumeMark";
4
+ import { STATUS_LABELS } from "../lib/status-label";
5
+
6
+ type Status = RunStatus | StepStatus;
7
+
8
+ const LIVE_STATUSES: ReadonlySet<Status> = new Set(["running", "waiting"]);
9
+
10
+ export function StatusBadge({
11
+ status,
12
+ small,
13
+ label,
14
+ className,
15
+ }: {
16
+ status: Status;
17
+ small?: boolean;
18
+ label?: string;
19
+ className?: string;
20
+ }) {
21
+ const live = LIVE_STATUSES.has(status);
22
+ return (
23
+ <span
24
+ className={cn(
25
+ "inline-flex items-center gap-1.5 border border-transparent font-medium leading-none whitespace-nowrap",
26
+ small ? "h-[17px] px-1.5 text-[10px]" : "h-[19px] px-1.5 text-[11px]",
27
+ status === "skipped" && "border-dashed",
28
+ className,
29
+ )}
30
+ style={{
31
+ backgroundColor: `var(--st-${status}-bg)`,
32
+ color: `var(--st-${status}-fg)`,
33
+ borderColor:
34
+ status === "skipped"
35
+ ? "color-mix(in oklch, var(--st-skipped-fg) 40%, transparent)"
36
+ : undefined,
37
+ }}
38
+ >
39
+ {live ? (
40
+ <ResumeMark variant="run" size={small ? 12 : 13} className="badge-mark" />
41
+ ) : (
42
+ <span
43
+ className="size-[6px] shrink-0 rounded-full"
44
+ style={{ backgroundColor: `var(--st-${status}-fg)` }}
45
+ />
46
+ )}
47
+ {label ?? STATUS_LABELS[status] ?? status}
48
+ </span>
49
+ );
50
+ }
@@ -0,0 +1,105 @@
1
+ import { ChevronRight, TriangleAlert } from "lucide-react";
2
+ import { Fragment, type ReactNode, useState } from "react";
3
+ import type { Step } from "@durablex/react";
4
+ import { formatDuration } from "../lib/format";
5
+ import { NO_LOGS, type RunLogs, stepLogKey } from "../lib/run-logs";
6
+ import { STATUS_LABELS } from "../lib/status-label";
7
+ import { initialStepSelection } from "../lib/step-detail";
8
+ import { stepDisplayStatus } from "../lib/step-display";
9
+ import { StepGlyph } from "./StepGlyph";
10
+ import { StepInspector } from "./StepInspector";
11
+
12
+ function FlowNode({
13
+ step,
14
+ selected,
15
+ runPaused,
16
+ onSelect,
17
+ }: {
18
+ step: Step;
19
+ selected: boolean;
20
+ runPaused: boolean;
21
+ onSelect(): void;
22
+ }) {
23
+ const display = stepDisplayStatus(step.status, runPaused);
24
+ return (
25
+ <button
26
+ type="button"
27
+ className={"flow-node focusable" + (selected ? " sel" : "")}
28
+ data-status={display}
29
+ aria-pressed={selected}
30
+ onClick={onSelect}
31
+ >
32
+ <span className="fn-rail" />
33
+ <span className="fn-node">
34
+ <StepGlyph status={display} className="fn-gly" />
35
+ </span>
36
+ <span className="fn-name">{step.name}</span>
37
+ {step.attempt > 1 && <span className="fn-att">&times;{step.attempt}</span>}
38
+ {display !== "succeeded" && (
39
+ <span className="fn-status">{STATUS_LABELS[display] ?? display}</span>
40
+ )}
41
+ {step.error && <TriangleAlert className="fn-errflag" />}
42
+ <span className="fn-dur">{formatDuration(step.durationMs)}</span>
43
+ </button>
44
+ );
45
+ }
46
+
47
+ export function StepFlow({
48
+ steps,
49
+ workflowName,
50
+ runPaused,
51
+ logs,
52
+ renderStepRetry,
53
+ }: {
54
+ steps: Step[];
55
+ workflowName: string;
56
+ runPaused: boolean;
57
+ logs: RunLogs;
58
+ renderStepRetry?: (stepName: string) => ReactNode;
59
+ }) {
60
+ const [sel, setSel] = useState(() => initialStepSelection(steps));
61
+ const step = steps[sel] ?? steps[0];
62
+ if (!step) return null;
63
+
64
+ return (
65
+ <div className="stepflow">
66
+ <div className="flow-canvas">
67
+ <div className="flow-trigger">
68
+ <span className="flow-trigger-dot" />
69
+ <span>trigger</span>
70
+ <span className="flow-trigger-wf">{workflowName}</span>
71
+ </div>
72
+ {steps.map((s, i) => {
73
+ const prev = steps[i - 1];
74
+ return (
75
+ <Fragment key={`${s.index}-${s.attempt}`}>
76
+ <div
77
+ className="flow-edge"
78
+ data-status={prev ? prev.status : "trigger"}
79
+ aria-hidden="true"
80
+ >
81
+ <span className="fe-line" />
82
+ <ChevronRight className="fe-arrow" />
83
+ </div>
84
+ <FlowNode
85
+ step={s}
86
+ selected={i === sel}
87
+ runPaused={runPaused}
88
+ onSelect={() => setSel(i)}
89
+ />
90
+ </Fragment>
91
+ );
92
+ })}
93
+ </div>
94
+ <StepInspector
95
+ key={sel}
96
+ step={step}
97
+ logs={logs.byStep.get(stepLogKey(step.name, step.attempt)) ?? NO_LOGS}
98
+ index={sel}
99
+ total={steps.length}
100
+ runPaused={runPaused}
101
+ renderStepRetry={renderStepRetry}
102
+ />
103
+ </div>
104
+ );
105
+ }
@@ -0,0 +1,25 @@
1
+ import { Check, CircleDot, Clock, Loader2, Pause, X } from "lucide-react";
2
+ import type { ComponentType } from "react";
3
+ import { cn } from "../lib/utils";
4
+ import type { StepDisplayStatus } from "../lib/step-display";
5
+
6
+ const GLYPH: Record<StepDisplayStatus, ComponentType<{ className?: string }>> = {
7
+ succeeded: Check,
8
+ failed: X,
9
+ cancelled: X,
10
+ running: Loader2,
11
+ waiting: Clock,
12
+ skipped: CircleDot,
13
+ paused: Pause,
14
+ };
15
+
16
+ export function StepGlyph({
17
+ status,
18
+ className,
19
+ }: {
20
+ status: StepDisplayStatus;
21
+ className?: string;
22
+ }) {
23
+ const Glyph = GLYPH[status] ?? CircleDot;
24
+ return <Glyph className={cn(className, status === "running" && "animate-spin")} />;
25
+ }
@@ -0,0 +1,44 @@
1
+ import { type ReactNode, useState } from "react";
2
+ import type { LogFrame, Step } from "@durablex/react";
3
+ import { formatDuration } from "../lib/format";
4
+ import { stepTabs, type StepTab } from "../lib/step-detail";
5
+ import { stepDisplayStatus } from "../lib/step-display";
6
+ import { StepTabsView } from "./StepTabsView";
7
+
8
+ // Keyed by selection in the parent so the tab resets on step change without a
9
+ // setState-in-effect.
10
+ export function StepInspector({
11
+ step,
12
+ logs,
13
+ index,
14
+ total,
15
+ runPaused,
16
+ renderStepRetry,
17
+ }: {
18
+ step: Step;
19
+ logs: LogFrame[];
20
+ index: number;
21
+ total: number;
22
+ runPaused: boolean;
23
+ renderStepRetry?: (stepName: string) => ReactNode;
24
+ }) {
25
+ const tabs = stepTabs(step, logs.length > 0);
26
+ const [tab, setTab] = useState<StepTab | null>(tabs[0] ?? null);
27
+ return (
28
+ <div className="flow-inspect">
29
+ <div className="fi-head">
30
+ <span className="fi-dot" data-status={stepDisplayStatus(step.status, runPaused)} />
31
+ <span className="fi-name">{step.name}</span>
32
+ <span className="fi-meta">
33
+ step {index + 1}/{total} &middot; {formatDuration(step.durationMs)}
34
+ </span>
35
+ {renderStepRetry?.(step.name)}
36
+ </div>
37
+ {tabs.length > 0 ? (
38
+ <StepTabsView step={step} logs={logs} tabs={tabs} tab={tab} onTab={setTab} />
39
+ ) : (
40
+ <div className="fi-empty">No output, error, or logs recorded for this step.</div>
41
+ )}
42
+ </div>
43
+ );
44
+ }
@@ -0,0 +1,69 @@
1
+ import { ChevronRight } from "lucide-react";
2
+ import { type ReactNode, useState } from "react";
3
+ import type { LogFrame, Step } from "@durablex/react";
4
+ import { formatDuration } from "../lib/format";
5
+ import { STATUS_LABELS } from "../lib/status-label";
6
+ import { stepTabs, type StepTab } from "../lib/step-detail";
7
+ import { stepDisplayStatus } from "../lib/step-display";
8
+ import { StepGlyph } from "./StepGlyph";
9
+ import { StepTabsView } from "./StepTabsView";
10
+
11
+ export function StepRow({
12
+ step,
13
+ index,
14
+ current,
15
+ runPaused,
16
+ logs,
17
+ renderStepRetry,
18
+ }: {
19
+ step: Step;
20
+ index: number;
21
+ current: boolean;
22
+ runPaused: boolean;
23
+ logs: LogFrame[];
24
+ renderStepRetry?: (stepName: string) => ReactNode;
25
+ }) {
26
+ const tabs = stepTabs(step, logs.length > 0);
27
+ const retry = renderStepRetry?.(step.name);
28
+ const expandable = tabs.length > 0 || retry != null;
29
+ const [open, setOpen] = useState(step.status === "failed");
30
+ const [tab, setTab] = useState<StepTab | null>(tabs[0] ?? null);
31
+ const display = stepDisplayStatus(step.status, runPaused);
32
+
33
+ return (
34
+ <div
35
+ className={"step" + (open ? " open" : "")}
36
+ data-status={display}
37
+ data-current={current ? "1" : undefined}
38
+ >
39
+ <button
40
+ type="button"
41
+ className={"step-row focusable" + (expandable ? "" : " static")}
42
+ aria-expanded={expandable ? open : undefined}
43
+ onClick={() => expandable && setOpen(!open)}
44
+ >
45
+ <span className="step-node">
46
+ <StepGlyph status={display} className="step-gly" />
47
+ </span>
48
+ <span className="step-ix">{index}</span>
49
+ <span className="step-name">{step.name}</span>
50
+ <span className="step-meta">
51
+ {display !== "succeeded" && (
52
+ <span className="step-status">{STATUS_LABELS[display] ?? display}</span>
53
+ )}
54
+ {step.attempt > 1 && <span className="step-att">&times;{step.attempt}</span>}
55
+ <span className="step-dur">{formatDuration(step.durationMs)}</span>
56
+ {expandable ? <ChevronRight className="step-chev" /> : <span className="step-chev-sp" />}
57
+ </span>
58
+ </button>
59
+ {open && expandable && (
60
+ <div className="step-body">
61
+ {tabs.length > 0 && (
62
+ <StepTabsView step={step} logs={logs} tabs={tabs} tab={tab} onTab={setTab} />
63
+ )}
64
+ {retry && <div className="step-actions">{retry}</div>}
65
+ </div>
66
+ )}
67
+ </div>
68
+ );
69
+ }
@@ -0,0 +1,51 @@
1
+ import { TriangleAlert } from "lucide-react";
2
+ import type { LogFrame, Step } from "@durablex/react";
3
+ import { STEP_TAB_LABEL, stepErrorText, type StepTab } from "../lib/step-detail";
4
+ import { JsonBlock } from "./JsonBlock";
5
+ import { LogList } from "./LogList";
6
+
7
+ export function StepTabsView({
8
+ step,
9
+ logs,
10
+ tabs,
11
+ tab,
12
+ onTab,
13
+ }: {
14
+ step: Step;
15
+ logs: LogFrame[];
16
+ tabs: StepTab[];
17
+ tab: StepTab | null;
18
+ onTab(t: StepTab): void;
19
+ }) {
20
+ return (
21
+ <>
22
+ <div className="step-tabs" role="tablist">
23
+ {tabs.map((t) => (
24
+ <button
25
+ key={t}
26
+ type="button"
27
+ role="tab"
28
+ aria-selected={tab === t}
29
+ className="step-tab"
30
+ data-kind={t}
31
+ onClick={() => onTab(t)}
32
+ >
33
+ {t === "error" && <TriangleAlert className="step-tab-ico" />}
34
+ {STEP_TAB_LABEL[t]}
35
+ </button>
36
+ ))}
37
+ </div>
38
+ <div className="step-pane">
39
+ {tab === "error" ? (
40
+ <pre className="errbox">{stepErrorText(step)}</pre>
41
+ ) : tab === "logs" ? (
42
+ <LogList logs={logs} />
43
+ ) : tab === "input" ? (
44
+ <JsonBlock value={step.input} />
45
+ ) : (
46
+ <JsonBlock value={step.output} />
47
+ )}
48
+ </div>
49
+ </>
50
+ );
51
+ }
@@ -0,0 +1,87 @@
1
+ import { type ReactNode, useEffect, useMemo, useState } from "react";
2
+ import type { Step } from "@durablex/react";
3
+ import { formatDuration } from "../lib/format";
4
+ import { NO_LOGS, type RunLogs, stepLogKey } from "../lib/run-logs";
5
+ import { initialStepSelection } from "../lib/step-detail";
6
+ import { stepDisplayStatus } from "../lib/step-display";
7
+ import { buildTimelineLayout } from "../lib/step-timeline";
8
+ import { StepInspector } from "./StepInspector";
9
+
10
+ export function StepTimeline({
11
+ steps,
12
+ runPaused,
13
+ logs,
14
+ renderStepRetry,
15
+ }: {
16
+ steps: Step[];
17
+ runPaused: boolean;
18
+ logs: RunLogs;
19
+ renderStepRetry?: (stepName: string) => ReactNode;
20
+ }) {
21
+ const [sel, setSel] = useState(() => initialStepSelection(steps));
22
+ // An unfinished step's bar extends to "now"; tick it while any step is in
23
+ // flight so the bar grows, and leave it frozen for a terminal or paused run.
24
+ const ticking = !runPaused && steps.some((s) => s.status === "running" || s.status === "waiting");
25
+ const [now, setNow] = useState(() => Date.now());
26
+ useEffect(() => {
27
+ if (!ticking) return;
28
+ const id = setInterval(() => setNow(Date.now()), 1000);
29
+ return () => clearInterval(id);
30
+ }, [ticking]);
31
+
32
+ const layout = useMemo(() => buildTimelineLayout(steps, now), [steps, now]);
33
+ const step = steps[sel] ?? steps[0];
34
+ if (!step) return null;
35
+
36
+ // Axis ticks are cumulative offsets from the run's start, so every label carries a
37
+ // leading "+" (the first reads "+0ms"), keeping the offset convention uniform.
38
+ const ticks = [0, 0.25, 0.5, 0.75, 1].map(
39
+ (f) => `+${formatDuration(Math.round(layout.totalMs * f))}`,
40
+ );
41
+
42
+ return (
43
+ <div className="steptl">
44
+ <div className="tl-canvas">
45
+ <div className="tl-axis" aria-hidden="true">
46
+ <span className="tl-axis-scale">
47
+ {ticks.map((label, i) => (
48
+ <span key={i}>{label}</span>
49
+ ))}
50
+ </span>
51
+ </div>
52
+ {layout.lanes.map((lane, i) => (
53
+ <button
54
+ key={`${lane.step.index}-${lane.step.attempt}`}
55
+ type="button"
56
+ className={"tl-row focusable" + (i === sel ? " sel" : "")}
57
+ aria-pressed={i === sel}
58
+ onClick={() => setSel(i)}
59
+ >
60
+ <span className="tl-label">{lane.step.name}</span>
61
+ <span className="tl-track">
62
+ {lane.leftPct === null ? (
63
+ <span className="tl-skip">not started</span>
64
+ ) : (
65
+ <span
66
+ className="tl-bar"
67
+ data-status={stepDisplayStatus(lane.step.status, runPaused)}
68
+ style={{ left: `${lane.leftPct}%`, width: `${lane.widthPct}%` }}
69
+ />
70
+ )}
71
+ </span>
72
+ <span className="tl-dur tnum">{formatDuration(lane.step.durationMs)}</span>
73
+ </button>
74
+ ))}
75
+ </div>
76
+ <StepInspector
77
+ key={sel}
78
+ step={step}
79
+ logs={logs.byStep.get(stepLogKey(step.name, step.attempt)) ?? NO_LOGS}
80
+ index={sel}
81
+ total={steps.length}
82
+ runPaused={runPaused}
83
+ renderStepRetry={renderStepRetry}
84
+ />
85
+ </div>
86
+ );
87
+ }
@@ -0,0 +1,54 @@
1
+ import { Loader2 } from "lucide-react";
2
+ import { TableCell, TableRow } from "../ui/table";
3
+
4
+ interface TableStatusRowsProps {
5
+ colSpan: number;
6
+ isLoading: boolean;
7
+ hasData: boolean;
8
+ isError: boolean;
9
+ error: Error | null;
10
+ emptyMessage: string;
11
+ errorFallback: string;
12
+ }
13
+
14
+ export function TableStatusRows({
15
+ colSpan,
16
+ isLoading,
17
+ hasData,
18
+ isError,
19
+ error,
20
+ emptyMessage,
21
+ errorFallback,
22
+ }: TableStatusRowsProps) {
23
+ if (isLoading && !hasData) {
24
+ return (
25
+ <TableRow>
26
+ <TableCell colSpan={colSpan} className="py-8 text-center">
27
+ <Loader2 className="text-muted-foreground mx-auto h-4 w-4 animate-spin" />
28
+ </TableCell>
29
+ </TableRow>
30
+ );
31
+ }
32
+
33
+ if (isError) {
34
+ return (
35
+ <TableRow>
36
+ <TableCell colSpan={colSpan} className="text-destructive py-6 text-center text-sm">
37
+ {error?.message ?? errorFallback}
38
+ </TableCell>
39
+ </TableRow>
40
+ );
41
+ }
42
+
43
+ if (!hasData) {
44
+ return (
45
+ <TableRow>
46
+ <TableCell colSpan={colSpan} className="text-muted-foreground py-6 text-center text-sm">
47
+ {emptyMessage}
48
+ </TableCell>
49
+ </TableRow>
50
+ );
51
+ }
52
+
53
+ return null;
54
+ }