@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,180 @@
|
|
|
1
|
+
import { useMemo, useState, type ReactNode } from "react";
|
|
2
|
+
import { ZapIcon } from "lucide-react";
|
|
3
|
+
import {
|
|
4
|
+
useTriggerEvent,
|
|
5
|
+
useWorkflows,
|
|
6
|
+
type SendEventResult,
|
|
7
|
+
type WorkflowDef,
|
|
8
|
+
} from "@durablex/react";
|
|
9
|
+
import {
|
|
10
|
+
Dialog,
|
|
11
|
+
DialogContent,
|
|
12
|
+
DialogDescription,
|
|
13
|
+
DialogFooter,
|
|
14
|
+
DialogHeader,
|
|
15
|
+
DialogTitle,
|
|
16
|
+
DialogTrigger,
|
|
17
|
+
} from "../ui/dialog";
|
|
18
|
+
import { Button } from "../ui/button";
|
|
19
|
+
import { Input } from "../ui/input";
|
|
20
|
+
import { parseJson } from "../lib/json-highlight";
|
|
21
|
+
import { ALL_FILTER } from "../lib/run-filters";
|
|
22
|
+
import { AppFilter } from "./filters/AppFilter";
|
|
23
|
+
import { JsonEditor } from "./JsonEditor";
|
|
24
|
+
import { TriggerEventResult } from "./TriggerEventResult";
|
|
25
|
+
|
|
26
|
+
export function TriggerEventDialog({ onOpenRun }: { onOpenRun?: (runId: string) => void }) {
|
|
27
|
+
const [open, setOpen] = useState(false);
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
31
|
+
<DialogTrigger asChild>
|
|
32
|
+
<Button size="sm" variant="outline">
|
|
33
|
+
<ZapIcon /> Trigger event
|
|
34
|
+
</Button>
|
|
35
|
+
</DialogTrigger>
|
|
36
|
+
<DialogContent className="sm:max-w-md">
|
|
37
|
+
{open && <TriggerForm onClose={() => setOpen(false)} onOpenRun={onOpenRun} />}
|
|
38
|
+
</DialogContent>
|
|
39
|
+
</Dialog>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Concrete (non-wildcard) event names a workflow can be triggered by, for the
|
|
44
|
+
// event-name autocomplete - a workflow with no triggers fires on its own name.
|
|
45
|
+
function knownEventNames(workflows: WorkflowDef[] | undefined): string[] {
|
|
46
|
+
const names = new Set<string>();
|
|
47
|
+
for (const w of workflows ?? []) {
|
|
48
|
+
const triggers = w.triggers ?? [];
|
|
49
|
+
if (triggers.length === 0) names.add(w.name);
|
|
50
|
+
for (const t of triggers) {
|
|
51
|
+
if (t.event && !t.event.endsWith("*")) names.add(t.event);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return [...names].sort();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function TriggerForm({
|
|
58
|
+
onClose,
|
|
59
|
+
onOpenRun,
|
|
60
|
+
}: {
|
|
61
|
+
onClose: () => void;
|
|
62
|
+
onOpenRun?: (runId: string) => void;
|
|
63
|
+
}) {
|
|
64
|
+
const { data: workflows } = useWorkflows();
|
|
65
|
+
const eventNames = useMemo(() => knownEventNames(workflows), [workflows]);
|
|
66
|
+
|
|
67
|
+
const [name, setName] = useState("");
|
|
68
|
+
const [app, setApp] = useState<string>(ALL_FILTER);
|
|
69
|
+
const [payload, setPayload] = useState("{}");
|
|
70
|
+
const [result, setResult] = useState<SendEventResult | null>(null);
|
|
71
|
+
const fire = useTriggerEvent();
|
|
72
|
+
|
|
73
|
+
const parsed = useMemo(() => parseJson(payload), [payload]);
|
|
74
|
+
const canSubmit = name.trim() !== "" && parsed.ok && !fire.isPending;
|
|
75
|
+
|
|
76
|
+
function submit() {
|
|
77
|
+
if (!parsed.ok) return;
|
|
78
|
+
fire.mutate(
|
|
79
|
+
{ name: name.trim(), app: app === ALL_FILTER ? undefined : app, data: parsed.value },
|
|
80
|
+
{ onSuccess: setResult },
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<>
|
|
86
|
+
<DialogHeader>
|
|
87
|
+
<DialogTitle>Trigger event</DialogTitle>
|
|
88
|
+
<DialogDescription>
|
|
89
|
+
Fire an event into the engine. Every workflow whose trigger matches the name runs.
|
|
90
|
+
</DialogDescription>
|
|
91
|
+
</DialogHeader>
|
|
92
|
+
|
|
93
|
+
<div className="flex flex-col gap-3">
|
|
94
|
+
<Field label="Event name" htmlFor="trigger-name">
|
|
95
|
+
<Input
|
|
96
|
+
id="trigger-name"
|
|
97
|
+
list="trigger-event-names"
|
|
98
|
+
value={name}
|
|
99
|
+
onChange={(e) => setName(e.target.value)}
|
|
100
|
+
placeholder="order.placed"
|
|
101
|
+
autoFocus
|
|
102
|
+
/>
|
|
103
|
+
<datalist id="trigger-event-names">
|
|
104
|
+
{eventNames.map((n) => (
|
|
105
|
+
<option key={n} value={n} />
|
|
106
|
+
))}
|
|
107
|
+
</datalist>
|
|
108
|
+
</Field>
|
|
109
|
+
|
|
110
|
+
<div className="flex flex-col gap-1.5">
|
|
111
|
+
<span className="text-xs font-medium">App</span>
|
|
112
|
+
<div>
|
|
113
|
+
<AppFilter app={app} onApp={setApp} />
|
|
114
|
+
</div>
|
|
115
|
+
<span className="text-muted-foreground text-[11px]">
|
|
116
|
+
"All apps" fires the event namespace-wide.
|
|
117
|
+
</span>
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
<Field label="Payload" htmlFor="trigger-payload" hint="JSON">
|
|
121
|
+
<JsonEditor
|
|
122
|
+
value={payload}
|
|
123
|
+
onChange={setPayload}
|
|
124
|
+
invalid={!parsed.ok}
|
|
125
|
+
ariaLabel="Event payload"
|
|
126
|
+
/>
|
|
127
|
+
{!parsed.ok ? <span className="text-xs text-destructive">{parsed.error}</span> : null}
|
|
128
|
+
</Field>
|
|
129
|
+
|
|
130
|
+
{result ? (
|
|
131
|
+
<TriggerEventResult
|
|
132
|
+
result={result}
|
|
133
|
+
onOpenRun={
|
|
134
|
+
onOpenRun
|
|
135
|
+
? (id) => {
|
|
136
|
+
onClose();
|
|
137
|
+
onOpenRun(id);
|
|
138
|
+
}
|
|
139
|
+
: undefined
|
|
140
|
+
}
|
|
141
|
+
/>
|
|
142
|
+
) : null}
|
|
143
|
+
{fire.isError ? (
|
|
144
|
+
<span className="text-xs text-destructive">
|
|
145
|
+
Could not fire event:{" "}
|
|
146
|
+
{fire.error instanceof Error ? fire.error.message : "unknown error"}
|
|
147
|
+
</span>
|
|
148
|
+
) : null}
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
<DialogFooter showCloseButton>
|
|
152
|
+
<Button disabled={!canSubmit} onClick={submit}>
|
|
153
|
+
{fire.isPending ? "Firing..." : "Fire event"}
|
|
154
|
+
</Button>
|
|
155
|
+
</DialogFooter>
|
|
156
|
+
</>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function Field({
|
|
161
|
+
label,
|
|
162
|
+
htmlFor,
|
|
163
|
+
hint,
|
|
164
|
+
children,
|
|
165
|
+
}: {
|
|
166
|
+
label: string;
|
|
167
|
+
htmlFor: string;
|
|
168
|
+
hint?: string;
|
|
169
|
+
children: ReactNode;
|
|
170
|
+
}) {
|
|
171
|
+
return (
|
|
172
|
+
<div className="flex flex-col gap-1.5">
|
|
173
|
+
<label htmlFor={htmlFor} className="flex items-center gap-1.5 text-xs font-medium">
|
|
174
|
+
{label}
|
|
175
|
+
{hint ? <span className="font-normal text-muted-foreground">{hint}</span> : null}
|
|
176
|
+
</label>
|
|
177
|
+
{children}
|
|
178
|
+
</div>
|
|
179
|
+
);
|
|
180
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { SendEventResult, TriggeredOutcome } from "@durablex/react";
|
|
2
|
+
|
|
3
|
+
function triggerStatus(o: TriggeredOutcome): string {
|
|
4
|
+
if (o.dropped) return "dropped";
|
|
5
|
+
if (o.deduped) return "deduped";
|
|
6
|
+
if (o.debounced) return "debounced";
|
|
7
|
+
if (o.batched) return "batched";
|
|
8
|
+
if (o.skipped) return "skipped";
|
|
9
|
+
return "started";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function TriggerRow({
|
|
13
|
+
t,
|
|
14
|
+
onOpenRun,
|
|
15
|
+
}: {
|
|
16
|
+
t: TriggeredOutcome;
|
|
17
|
+
onOpenRun?: (runId: string) => void;
|
|
18
|
+
}) {
|
|
19
|
+
const status = triggerStatus(t);
|
|
20
|
+
const runId = t.runId;
|
|
21
|
+
return (
|
|
22
|
+
<li className="flex items-center justify-between gap-2 font-mono">
|
|
23
|
+
<span className="truncate">{t.workflow}</span>
|
|
24
|
+
{runId && onOpenRun ? (
|
|
25
|
+
<button
|
|
26
|
+
type="button"
|
|
27
|
+
className="text-primary hover:underline"
|
|
28
|
+
onClick={() => onOpenRun(runId)}
|
|
29
|
+
>
|
|
30
|
+
{status}
|
|
31
|
+
</button>
|
|
32
|
+
) : (
|
|
33
|
+
<span className="text-muted-foreground">{status}</span>
|
|
34
|
+
)}
|
|
35
|
+
</li>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function TriggerEventResult({
|
|
40
|
+
result,
|
|
41
|
+
onOpenRun,
|
|
42
|
+
}: {
|
|
43
|
+
result: SendEventResult;
|
|
44
|
+
onOpenRun?: (runId: string) => void;
|
|
45
|
+
}) {
|
|
46
|
+
const triggered = result.triggered ?? [];
|
|
47
|
+
return (
|
|
48
|
+
<div className="rounded-lg border border-border bg-muted/40 p-2.5 text-xs">
|
|
49
|
+
<div className="font-medium text-foreground">
|
|
50
|
+
Event accepted · woke {result.woke} {result.woke === 1 ? "run" : "runs"}
|
|
51
|
+
</div>
|
|
52
|
+
{triggered.length > 0 ? (
|
|
53
|
+
<ul className="mt-1.5 flex flex-col gap-1">
|
|
54
|
+
{triggered.map((t, i) => (
|
|
55
|
+
<TriggerRow key={`${t.workflow}-${i}`} t={t} onOpenRun={onOpenRun} />
|
|
56
|
+
))}
|
|
57
|
+
</ul>
|
|
58
|
+
) : null}
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { cn } from "../lib/utils";
|
|
2
|
+
import {
|
|
3
|
+
codePillClass,
|
|
4
|
+
DELIVERY_VIEW_LABEL,
|
|
5
|
+
DELIVERY_VIEW_TOKEN,
|
|
6
|
+
type DeliveryView,
|
|
7
|
+
type EndpointHealth,
|
|
8
|
+
} from "../lib/webhook-view";
|
|
9
|
+
import { ResumeMark } from "./ResumeMark";
|
|
10
|
+
|
|
11
|
+
function StatusPill({
|
|
12
|
+
token,
|
|
13
|
+
label,
|
|
14
|
+
small,
|
|
15
|
+
live,
|
|
16
|
+
}: {
|
|
17
|
+
token: string;
|
|
18
|
+
label: string;
|
|
19
|
+
small?: boolean;
|
|
20
|
+
live?: boolean;
|
|
21
|
+
}) {
|
|
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
|
+
)}
|
|
28
|
+
style={{ backgroundColor: `var(--st-${token}-bg)`, color: `var(--st-${token}-fg)` }}
|
|
29
|
+
>
|
|
30
|
+
{live ? <ResumeMark size={small ? 12 : 13} variant="run" /> : <i className="bdot" />}
|
|
31
|
+
{label}
|
|
32
|
+
</span>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function DeliveryBadge({ view, small }: { view: DeliveryView; small?: boolean }) {
|
|
37
|
+
return (
|
|
38
|
+
<StatusPill
|
|
39
|
+
token={DELIVERY_VIEW_TOKEN[view]}
|
|
40
|
+
label={DELIVERY_VIEW_LABEL[view]}
|
|
41
|
+
small={small}
|
|
42
|
+
live={view === "retrying"}
|
|
43
|
+
/>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const HEALTH_TOKEN: Record<EndpointHealth, string> = {
|
|
48
|
+
active: "succeeded",
|
|
49
|
+
failing: "failed",
|
|
50
|
+
disabled: "cancelled",
|
|
51
|
+
};
|
|
52
|
+
const HEALTH_LABEL: Record<EndpointHealth, string> = {
|
|
53
|
+
active: "Active",
|
|
54
|
+
failing: "Failing",
|
|
55
|
+
disabled: "Disabled",
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export function EndpointBadge({ health, small }: { health: EndpointHealth; small?: boolean }) {
|
|
59
|
+
return <StatusPill token={HEALTH_TOKEN[health]} label={HEALTH_LABEL[health]} small={small} />;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function CodePill({ code }: { code?: number }) {
|
|
63
|
+
const k = codePillClass(code);
|
|
64
|
+
return <span className={`wh-code ${k}`}>{k === "none" ? "-" : code}</span>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function EventChip({ name }: { name: string }) {
|
|
68
|
+
return <span className="wh-evchip">{name === "*" ? "all events" : name}</span>;
|
|
69
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { DeliveryStatus } from "@durablex/react";
|
|
2
|
+
import { cn } from "../lib/utils";
|
|
3
|
+
|
|
4
|
+
// failed = awaiting retry, exhausted = retries spent, dead = a non-retryable response.
|
|
5
|
+
const STATUS_CLASS: Record<DeliveryStatus, string> = {
|
|
6
|
+
pending: "text-muted-foreground border-border",
|
|
7
|
+
delivering: "border-sky-500/40 text-sky-600 dark:text-sky-400",
|
|
8
|
+
succeeded: "border-emerald-500/40 text-emerald-600 dark:text-emerald-400",
|
|
9
|
+
failed: "border-amber-500/40 text-amber-600 dark:text-amber-400",
|
|
10
|
+
exhausted: "border-red-500/40 text-red-600 dark:text-red-400",
|
|
11
|
+
dead: "border-red-500/40 text-red-600 dark:text-red-400",
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function WebhookStatusBadge({ status }: { status: DeliveryStatus }) {
|
|
15
|
+
return (
|
|
16
|
+
<span
|
|
17
|
+
className={cn(
|
|
18
|
+
"inline-flex border px-1.5 py-0.5 font-mono text-[10px] leading-none",
|
|
19
|
+
STATUS_CLASS[status],
|
|
20
|
+
)}
|
|
21
|
+
>
|
|
22
|
+
{status}
|
|
23
|
+
</span>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Globe, Inbox, Send } from "lucide-react";
|
|
2
|
+
import type { ComponentType } from "react";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { DeliveriesSplit } from "./DeliveriesSplit";
|
|
5
|
+
import { EndpointsTab } from "./EndpointsTab";
|
|
6
|
+
import { ReceiversTab } from "./ReceiversTab";
|
|
7
|
+
import { useEndpoints, useReceivers } from "@durablex/react";
|
|
8
|
+
import type { WebhookTab } from "../lib/webhook-view";
|
|
9
|
+
|
|
10
|
+
const TABS = [
|
|
11
|
+
{ key: "deliveries", label: "Deliveries", Icon: Send },
|
|
12
|
+
{ key: "endpoints", label: "Endpoints", Icon: Globe },
|
|
13
|
+
{ key: "receivers", label: "Receivers", Icon: Inbox },
|
|
14
|
+
] as const;
|
|
15
|
+
|
|
16
|
+
export function WebhooksView({ onOpenRun }: { onOpenRun?: (runId: string) => void }) {
|
|
17
|
+
const [tab, setTab] = useState<WebhookTab>("deliveries");
|
|
18
|
+
const endpoints = useEndpoints().data?.length;
|
|
19
|
+
const receivers = useReceivers().data?.length;
|
|
20
|
+
const counts: Record<WebhookTab, number | undefined> = {
|
|
21
|
+
deliveries: undefined,
|
|
22
|
+
endpoints,
|
|
23
|
+
receivers,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div className="wh-root">
|
|
28
|
+
<div className="wh-subnav" role="tablist">
|
|
29
|
+
{TABS.map(({ key, label, Icon }) => (
|
|
30
|
+
<WebhookTab
|
|
31
|
+
key={key}
|
|
32
|
+
label={label}
|
|
33
|
+
Icon={Icon}
|
|
34
|
+
count={counts[key]}
|
|
35
|
+
selected={tab === key}
|
|
36
|
+
onSelect={() => setTab(key)}
|
|
37
|
+
/>
|
|
38
|
+
))}
|
|
39
|
+
</div>
|
|
40
|
+
<div className="wh-body">
|
|
41
|
+
{tab === "deliveries" && <DeliveriesSplit onOpenRun={onOpenRun} />}
|
|
42
|
+
{tab === "endpoints" && <EndpointsTab />}
|
|
43
|
+
{tab === "receivers" && <ReceiversTab />}
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function WebhookTab({
|
|
50
|
+
label,
|
|
51
|
+
Icon,
|
|
52
|
+
count,
|
|
53
|
+
selected,
|
|
54
|
+
onSelect,
|
|
55
|
+
}: {
|
|
56
|
+
label: string;
|
|
57
|
+
Icon: ComponentType<{ className?: string }>;
|
|
58
|
+
count?: number;
|
|
59
|
+
selected: boolean;
|
|
60
|
+
onSelect(): void;
|
|
61
|
+
}) {
|
|
62
|
+
return (
|
|
63
|
+
<button type="button" className="wh-tab" role="tab" aria-selected={selected} onClick={onSelect}>
|
|
64
|
+
<Icon className="wh-tab-ico" />
|
|
65
|
+
<span>{label}</span>
|
|
66
|
+
{count != null && <span className="wh-count tabular-nums">{count}</span>}
|
|
67
|
+
</button>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { X } from "lucide-react";
|
|
2
|
+
import { useState, type ReactNode } from "react";
|
|
3
|
+
import { useRuns, useRunStats, useRunTimeSeries, type WorkflowDef } from "@durablex/react";
|
|
4
|
+
import { CursorPager } from "./CursorPager";
|
|
5
|
+
import { Meta } from "./Meta";
|
|
6
|
+
import { RunsTable } from "./RunsTable";
|
|
7
|
+
import { StatsTiles } from "./StatsTiles";
|
|
8
|
+
import { SectionHeader } from "./SectionHeader";
|
|
9
|
+
import { AppTag } from "./AppTag";
|
|
10
|
+
import { ScheduledBadge } from "./ScheduledBadge";
|
|
11
|
+
import { RunCharts } from "./charts/RunCharts";
|
|
12
|
+
import { FlowControlSection } from "./FlowControlSection";
|
|
13
|
+
import { TimeRangeFilter } from "./filters/TimeRangeFilter";
|
|
14
|
+
import { useKeysetPager } from "../hooks/use-keyset-pager";
|
|
15
|
+
import { hasFlowControl } from "../lib/flow-control";
|
|
16
|
+
import { formatNextFire, formatTime } from "../lib/format";
|
|
17
|
+
import { DEFAULT_RUN_SORT, type RunSortKey, toggleRunSort } from "../lib/run-sort";
|
|
18
|
+
import { DEFAULT_TIME_RANGE, seriesBucketSeconds, timeLabel, windowSince } from "../lib/time-range";
|
|
19
|
+
|
|
20
|
+
const RUNS_PAGE_SIZE = 30;
|
|
21
|
+
|
|
22
|
+
export function WorkflowDetail({
|
|
23
|
+
workflow,
|
|
24
|
+
onClose,
|
|
25
|
+
onOpenRun,
|
|
26
|
+
renderRunAction,
|
|
27
|
+
}: {
|
|
28
|
+
workflow: WorkflowDef;
|
|
29
|
+
onClose(): void;
|
|
30
|
+
onOpenRun(runId: string): void;
|
|
31
|
+
renderRunAction?: (workflow: WorkflowDef) => ReactNode;
|
|
32
|
+
}) {
|
|
33
|
+
const [time, setTime] = useState(DEFAULT_TIME_RANGE);
|
|
34
|
+
const [sort, setSort] = useState(DEFAULT_RUN_SORT);
|
|
35
|
+
const onSort = (key: RunSortKey) => setSort((prev) => toggleRunSort(prev, key));
|
|
36
|
+
|
|
37
|
+
const since = windowSince(time);
|
|
38
|
+
const bucket = seriesBucketSeconds(time);
|
|
39
|
+
const scope = { app: workflow.app, workflow: workflow.name };
|
|
40
|
+
|
|
41
|
+
// Reset to the newest page whenever the time range or sort changes.
|
|
42
|
+
const { cursor, canNewer, range, goOlder, goNewer } = useKeysetPager(
|
|
43
|
+
`${time}|${sort.key}-${sort.dir}`,
|
|
44
|
+
RUNS_PAGE_SIZE,
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const stats = useRunStats({ ...scope, since });
|
|
48
|
+
// Concurrency in-flight/queued are point-in-time, so they read an unwindowed count
|
|
49
|
+
// (a run executing longer than the selected range still counts as in flight).
|
|
50
|
+
const liveStats = useRunStats(scope);
|
|
51
|
+
const { data: series, isLoading: seriesLoading } = useRunTimeSeries({ ...scope, since, bucket });
|
|
52
|
+
const runs = useRuns({
|
|
53
|
+
...scope,
|
|
54
|
+
since,
|
|
55
|
+
sort: sort.key,
|
|
56
|
+
dir: sort.dir,
|
|
57
|
+
limit: RUNS_PAGE_SIZE,
|
|
58
|
+
cursor,
|
|
59
|
+
});
|
|
60
|
+
const rows = runs.data?.runs ?? [];
|
|
61
|
+
const nextCursor = runs.data?.nextCursor ?? null;
|
|
62
|
+
const { start: rangeStart, end: rangeEnd } = range(rows.length);
|
|
63
|
+
const schedule = workflow.scheduled ? workflow.schedules?.[0] : undefined;
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<div className="run-panel">
|
|
67
|
+
<div className="panel-head">
|
|
68
|
+
<div className="ph-top">
|
|
69
|
+
<h2>
|
|
70
|
+
<span className="h2-name">{workflow.name}</span>
|
|
71
|
+
</h2>
|
|
72
|
+
<div className="ph-actions">
|
|
73
|
+
{renderRunAction?.(workflow)}
|
|
74
|
+
<button
|
|
75
|
+
type="button"
|
|
76
|
+
className="iconbtn focusable"
|
|
77
|
+
aria-label="Close"
|
|
78
|
+
onClick={onClose}
|
|
79
|
+
>
|
|
80
|
+
<X />
|
|
81
|
+
</button>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
<div className="ph-status">
|
|
85
|
+
<AppTag app={workflow.app} />
|
|
86
|
+
{schedule && (
|
|
87
|
+
<span className="step-cell">
|
|
88
|
+
<ScheduledBadge compact />
|
|
89
|
+
<span className="font-mono text-xs">{schedule.cron}</span>
|
|
90
|
+
</span>
|
|
91
|
+
)}
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<div className="panel-body">
|
|
96
|
+
<div className="metagrid">
|
|
97
|
+
<Meta label="App" value={workflow.app} />
|
|
98
|
+
<Meta label="Max attempts" value={String(workflow.maxAttempts)} />
|
|
99
|
+
<Meta label="Backoff" value={workflow.backoff} />
|
|
100
|
+
<Meta label="Schedule" value={schedule ? schedule.cron : "Not scheduled"} />
|
|
101
|
+
{schedule && <Meta label="Next fire" value={formatNextFire(schedule.nextFireAt)} />}
|
|
102
|
+
<Meta label="Registered" value={formatTime(workflow.registeredAt)} />
|
|
103
|
+
<Meta label="Updated" value={formatTime(workflow.updatedAt)} />
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
{hasFlowControl(workflow.flowControl) && workflow.flowControl && (
|
|
107
|
+
<FlowControlSection
|
|
108
|
+
fc={workflow.flowControl}
|
|
109
|
+
app={workflow.app}
|
|
110
|
+
workflow={workflow.name}
|
|
111
|
+
stats={liveStats.data}
|
|
112
|
+
/>
|
|
113
|
+
)}
|
|
114
|
+
|
|
115
|
+
<StatsTiles stats={stats.data} rangeLabel={timeLabel(time)} />
|
|
116
|
+
<div className="filterbar">
|
|
117
|
+
<TimeRangeFilter time={time} onTime={setTime} />
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
<RunCharts series={series} loading={seriesLoading} />
|
|
121
|
+
|
|
122
|
+
<SectionHeader>Runs</SectionHeader>
|
|
123
|
+
<RunsTable
|
|
124
|
+
rows={rows}
|
|
125
|
+
selectedId={null}
|
|
126
|
+
onSelect={onOpenRun}
|
|
127
|
+
sort={sort}
|
|
128
|
+
onSort={onSort}
|
|
129
|
+
loading={runs.isLoading}
|
|
130
|
+
isError={runs.isError}
|
|
131
|
+
error={runs.error}
|
|
132
|
+
emptyTitle="No runs yet"
|
|
133
|
+
emptyMessage="This workflow has no runs in the selected window."
|
|
134
|
+
hideColumns={["workflow", "app"]}
|
|
135
|
+
/>
|
|
136
|
+
{!runs.isLoading && rows.length > 0 && (
|
|
137
|
+
<CursorPager
|
|
138
|
+
rangeStart={rangeStart}
|
|
139
|
+
rangeEnd={rangeEnd}
|
|
140
|
+
canNewer={canNewer}
|
|
141
|
+
canOlder={nextCursor != null}
|
|
142
|
+
onNewer={goNewer}
|
|
143
|
+
onOlder={() => goOlder(nextCursor)}
|
|
144
|
+
/>
|
|
145
|
+
)}
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { Play } from "lucide-react";
|
|
3
|
+
import type { WorkflowDef } from "@durablex/react";
|
|
4
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
|
|
5
|
+
import { workflowRunPlan } from "../lib/workflow-run";
|
|
6
|
+
import { WorkflowRunDialog } from "./WorkflowRunDialog";
|
|
7
|
+
|
|
8
|
+
export function WorkflowRunAction({
|
|
9
|
+
workflow,
|
|
10
|
+
onOpenRun,
|
|
11
|
+
}: {
|
|
12
|
+
workflow: WorkflowDef;
|
|
13
|
+
onOpenRun(runId: string): void;
|
|
14
|
+
}) {
|
|
15
|
+
const [open, setOpen] = useState(false);
|
|
16
|
+
|
|
17
|
+
if (!workflowRunPlan(workflow).runnable) {
|
|
18
|
+
return (
|
|
19
|
+
<Tooltip>
|
|
20
|
+
<TooltipTrigger asChild>
|
|
21
|
+
<span className="btn" aria-disabled>
|
|
22
|
+
<Play /> Run
|
|
23
|
+
</span>
|
|
24
|
+
</TooltipTrigger>
|
|
25
|
+
<TooltipContent>
|
|
26
|
+
This workflow only runs on a schedule, so there is no event to fire. Manual fire-now lands
|
|
27
|
+
in a later release.
|
|
28
|
+
</TooltipContent>
|
|
29
|
+
</Tooltip>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<>
|
|
35
|
+
<button type="button" className="btn focusable" onClick={() => setOpen(true)}>
|
|
36
|
+
<Play /> Run
|
|
37
|
+
</button>
|
|
38
|
+
<WorkflowRunDialog
|
|
39
|
+
workflow={workflow}
|
|
40
|
+
open={open}
|
|
41
|
+
onOpenChange={setOpen}
|
|
42
|
+
onOpenRun={onOpenRun}
|
|
43
|
+
/>
|
|
44
|
+
</>
|
|
45
|
+
);
|
|
46
|
+
}
|