khotan-data 0.0.1 → 0.1.1
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/AGENTS.md +54 -0
- package/README.md +117 -1
- package/dist/cli.js +2869 -0
- package/dist/factory.cjs +3303 -0
- package/dist/factory.cjs.map +1 -0
- package/dist/factory.d.cts +662 -0
- package/dist/factory.d.ts +662 -0
- package/dist/factory.js +3292 -0
- package/dist/factory.js.map +1 -0
- package/dist/plug-client.cjs +99 -0
- package/dist/plug-client.cjs.map +1 -0
- package/dist/plug-client.d.cts +71 -0
- package/dist/plug-client.d.ts +71 -0
- package/dist/plug-client.js +96 -0
- package/dist/plug-client.js.map +1 -0
- package/dist/templates/agent-skill.md +73 -0
- package/dist/templates/agents.md +41 -0
- package/dist/templates/cache.example.ts +11 -0
- package/dist/templates/cache.ts +58 -0
- package/dist/templates/catch.example.ts +36 -0
- package/dist/templates/catch.ts +119 -0
- package/dist/templates/config-page.tsx +20 -0
- package/dist/templates/debug-index-page.tsx +101 -0
- package/dist/templates/debug-page.tsx +48 -0
- package/dist/templates/graph-page.tsx +11 -0
- package/dist/templates/hub.tsx +450 -0
- package/dist/templates/inflow.example.ts +61 -0
- package/dist/templates/inflow.ts +98 -0
- package/dist/templates/khotan-config.ts +49 -0
- package/dist/templates/khotan-route.ts +13 -0
- package/dist/templates/logs-page.tsx +9 -0
- package/dist/templates/logs.tsx +20 -0
- package/dist/templates/mapping-browser.tsx +761 -0
- package/dist/templates/mappings-page.tsx +9 -0
- package/dist/templates/outflow.example.ts +52 -0
- package/dist/templates/outflow.ts +90 -0
- package/dist/templates/pass.example.ts +51 -0
- package/dist/templates/pass.ts +134 -0
- package/dist/templates/plug-debugger.tsx +1185 -0
- package/dist/templates/plug.example.ts +93 -0
- package/dist/templates/plug.ts +806 -0
- package/dist/templates/relay.example.ts +71 -0
- package/dist/templates/relay.ts +104 -0
- package/dist/templates/runs-table.tsx +592 -0
- package/dist/templates/schema.ts +505 -0
- package/dist/templates/skill-dashboard.md +144 -0
- package/dist/templates/skill-plug.md +216 -0
- package/dist/templates/skill-setup.md +161 -0
- package/dist/templates/skill-webhook.md +196 -0
- package/dist/templates/topology-canvas.tsx +1406 -0
- package/dist/templates/var-panel.tsx +276 -0
- package/dist/templates/webhook-events-table.tsx +241 -0
- package/dist/templates/wire-panel.tsx +216 -0
- package/dist/templates/wire.ts +155 -0
- package/package.json +46 -5
|
@@ -0,0 +1,592 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Fragment, useCallback, useEffect, useState } from "react";
|
|
4
|
+
import { RefreshCw } from "lucide-react";
|
|
5
|
+
import { Badge } from "@/components/ui/badge";
|
|
6
|
+
import { Button } from "@/components/ui/button";
|
|
7
|
+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
8
|
+
import { Switch } from "@/components/ui/switch";
|
|
9
|
+
import {
|
|
10
|
+
Table,
|
|
11
|
+
TableBody,
|
|
12
|
+
TableCell,
|
|
13
|
+
TableHead,
|
|
14
|
+
TableHeader,
|
|
15
|
+
TableRow,
|
|
16
|
+
} from "@/components/ui/table";
|
|
17
|
+
|
|
18
|
+
interface RunLogItem {
|
|
19
|
+
id: string;
|
|
20
|
+
runType: string;
|
|
21
|
+
status:
|
|
22
|
+
| "pending"
|
|
23
|
+
| "running"
|
|
24
|
+
| "completed"
|
|
25
|
+
| "partial"
|
|
26
|
+
| "failed"
|
|
27
|
+
| "cancelled";
|
|
28
|
+
workflowRunId: string | null;
|
|
29
|
+
sourceType: "flow" | "webhook" | "unknown";
|
|
30
|
+
sourceName: string | null;
|
|
31
|
+
sourceKind: "catch" | "pass" | null;
|
|
32
|
+
plugName: string | null;
|
|
33
|
+
startedAt: string;
|
|
34
|
+
completedAt: string | null;
|
|
35
|
+
extracted: number;
|
|
36
|
+
transformed: number;
|
|
37
|
+
created: number;
|
|
38
|
+
updated: number;
|
|
39
|
+
deleted: number;
|
|
40
|
+
failed: number;
|
|
41
|
+
error: string | null;
|
|
42
|
+
metadata?: Record<string, unknown> | null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface PageResponse<T> {
|
|
46
|
+
items: T[];
|
|
47
|
+
page: {
|
|
48
|
+
limit: number;
|
|
49
|
+
offset: number;
|
|
50
|
+
hasMore: boolean;
|
|
51
|
+
prevOffset: number;
|
|
52
|
+
nextOffset: number;
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const statusVariant = {
|
|
57
|
+
pending: "outline",
|
|
58
|
+
running: "secondary",
|
|
59
|
+
completed: "default",
|
|
60
|
+
partial: "secondary",
|
|
61
|
+
failed: "destructive",
|
|
62
|
+
cancelled: "outline",
|
|
63
|
+
} as const;
|
|
64
|
+
|
|
65
|
+
const statusLabel = {
|
|
66
|
+
pending: "pending",
|
|
67
|
+
running: "running",
|
|
68
|
+
completed: "completed",
|
|
69
|
+
partial: "partial",
|
|
70
|
+
failed: "failed",
|
|
71
|
+
cancelled: "cancelled",
|
|
72
|
+
} as const;
|
|
73
|
+
|
|
74
|
+
function formatDateTime(value: string | null): string {
|
|
75
|
+
if (!value) return "Never";
|
|
76
|
+
const date = new Date(value);
|
|
77
|
+
if (Number.isNaN(date.getTime())) return value;
|
|
78
|
+
return date
|
|
79
|
+
.toISOString()
|
|
80
|
+
.replace("T", " ")
|
|
81
|
+
.replace(/\.\d{3}Z$/, " UTC");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function formatSource(item: RunLogItem): string {
|
|
85
|
+
if (!item.sourceName) return "Unknown";
|
|
86
|
+
if (item.sourceType !== "webhook" || !item.sourceKind) return item.sourceName;
|
|
87
|
+
return `${item.sourceKind}:${item.sourceName}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function formatCounts(item: RunLogItem): string {
|
|
91
|
+
const parts = [
|
|
92
|
+
item.extracted > 0 ? `extracted ${String(item.extracted)}` : null,
|
|
93
|
+
item.transformed > 0 ? `transformed ${String(item.transformed)}` : null,
|
|
94
|
+
item.created > 0 ? `created ${String(item.created)}` : null,
|
|
95
|
+
item.updated > 0 ? `updated ${String(item.updated)}` : null,
|
|
96
|
+
item.deleted > 0 ? `deleted ${String(item.deleted)}` : null,
|
|
97
|
+
item.failed > 0 ? `failed ${String(item.failed)}` : null,
|
|
98
|
+
].filter(Boolean);
|
|
99
|
+
return parts.length > 0 ? parts.join(" - ") : "-";
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function formatStreamLine(line: string): string {
|
|
103
|
+
try {
|
|
104
|
+
const parsed = JSON.parse(line) as {
|
|
105
|
+
timestamp?: string;
|
|
106
|
+
message?: string;
|
|
107
|
+
type?: string;
|
|
108
|
+
};
|
|
109
|
+
const parsedDate = parsed.timestamp ? new Date(parsed.timestamp) : null;
|
|
110
|
+
const prefix =
|
|
111
|
+
parsedDate && !Number.isNaN(parsedDate.getTime())
|
|
112
|
+
? `[${parsedDate.toISOString().slice(11, 19)} UTC] `
|
|
113
|
+
: "";
|
|
114
|
+
const type = parsed.type ? `${parsed.type}: ` : "";
|
|
115
|
+
return `${prefix}${type}${parsed.message ?? line}`;
|
|
116
|
+
} catch {
|
|
117
|
+
return line;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function RunDetails({
|
|
122
|
+
run,
|
|
123
|
+
streamingEnabled,
|
|
124
|
+
onChanged,
|
|
125
|
+
onStreamInbound,
|
|
126
|
+
}: {
|
|
127
|
+
run: RunLogItem;
|
|
128
|
+
streamingEnabled: boolean;
|
|
129
|
+
onChanged(): void;
|
|
130
|
+
onStreamInbound(): void;
|
|
131
|
+
}) {
|
|
132
|
+
const [detail, setDetail] = useState<Record<string, unknown> | null>(null);
|
|
133
|
+
const [streamLines, setStreamLines] = useState<string[]>([]);
|
|
134
|
+
const [error, setError] = useState<string | null>(null);
|
|
135
|
+
const [busy, setBusy] = useState<"cancel" | "retry" | null>(null);
|
|
136
|
+
const [lastUpdatedAt, setLastUpdatedAt] = useState<string | null>(null);
|
|
137
|
+
|
|
138
|
+
const fetchDetail = useCallback(async (): Promise<
|
|
139
|
+
Record<string, unknown>
|
|
140
|
+
> => {
|
|
141
|
+
const res = await fetch(`/api/khotan/runs/${run.id}`);
|
|
142
|
+
if (!res.ok) throw new Error("Failed to load run detail");
|
|
143
|
+
return (await res.json()) as Record<string, unknown>;
|
|
144
|
+
}, [run.id]);
|
|
145
|
+
|
|
146
|
+
useEffect(() => {
|
|
147
|
+
let cancelled = false;
|
|
148
|
+
|
|
149
|
+
async function loadDetail() {
|
|
150
|
+
try {
|
|
151
|
+
const json = await fetchDetail();
|
|
152
|
+
if (!cancelled) setDetail(json);
|
|
153
|
+
if (!cancelled) setLastUpdatedAt(new Date().toISOString());
|
|
154
|
+
} catch (err) {
|
|
155
|
+
if (!cancelled)
|
|
156
|
+
setError(err instanceof Error ? err.message : "Unknown error");
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
void loadDetail();
|
|
161
|
+
if (!streamingEnabled) {
|
|
162
|
+
return () => {
|
|
163
|
+
cancelled = true;
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const interval = window.setInterval(() => {
|
|
168
|
+
void loadDetail();
|
|
169
|
+
}, 5000);
|
|
170
|
+
return () => {
|
|
171
|
+
cancelled = true;
|
|
172
|
+
window.clearInterval(interval);
|
|
173
|
+
};
|
|
174
|
+
}, [fetchDetail, streamingEnabled]);
|
|
175
|
+
|
|
176
|
+
useEffect(() => {
|
|
177
|
+
if (!run.workflowRunId) return;
|
|
178
|
+
const isLiveRun = run.status === "pending" || run.status === "running";
|
|
179
|
+
if (!streamingEnabled && isLiveRun) return;
|
|
180
|
+
|
|
181
|
+
const controller = new AbortController();
|
|
182
|
+
let buffer = "";
|
|
183
|
+
|
|
184
|
+
async function readStream() {
|
|
185
|
+
try {
|
|
186
|
+
const res = await fetch(
|
|
187
|
+
`/api/khotan/runs/${run.id}/stream?startIndex=-50`,
|
|
188
|
+
{
|
|
189
|
+
signal: controller.signal,
|
|
190
|
+
},
|
|
191
|
+
);
|
|
192
|
+
if (!res.ok || !res.body) return;
|
|
193
|
+
|
|
194
|
+
const reader = res.body.getReader();
|
|
195
|
+
const decoder = new TextDecoder();
|
|
196
|
+
while (true) {
|
|
197
|
+
const { done, value } = await reader.read();
|
|
198
|
+
if (done) break;
|
|
199
|
+
buffer += decoder.decode(value, { stream: true });
|
|
200
|
+
const lines = buffer.split("\n");
|
|
201
|
+
buffer = lines.pop() ?? "";
|
|
202
|
+
const parsed = lines
|
|
203
|
+
.map((line) => line.trim())
|
|
204
|
+
.filter(Boolean)
|
|
205
|
+
.map(formatStreamLine);
|
|
206
|
+
if (parsed.length > 0) {
|
|
207
|
+
setStreamLines((prev) => [...prev, ...parsed].slice(-100));
|
|
208
|
+
setLastUpdatedAt(new Date().toISOString());
|
|
209
|
+
if (streamingEnabled) onStreamInbound();
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
} catch (err) {
|
|
213
|
+
if (!controller.signal.aborted) {
|
|
214
|
+
setError(err instanceof Error ? err.message : "Unknown stream error");
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
void readStream();
|
|
220
|
+
if (streamingEnabled) {
|
|
221
|
+
return () => controller.abort();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const timeout = window.setTimeout(() => controller.abort(), 2000);
|
|
225
|
+
return () => {
|
|
226
|
+
window.clearTimeout(timeout);
|
|
227
|
+
controller.abort();
|
|
228
|
+
};
|
|
229
|
+
}, [
|
|
230
|
+
onStreamInbound,
|
|
231
|
+
run.id,
|
|
232
|
+
run.status,
|
|
233
|
+
run.workflowRunId,
|
|
234
|
+
streamingEnabled,
|
|
235
|
+
]);
|
|
236
|
+
|
|
237
|
+
async function refreshDetail() {
|
|
238
|
+
setError(null);
|
|
239
|
+
try {
|
|
240
|
+
const json = await fetchDetail();
|
|
241
|
+
setDetail(json);
|
|
242
|
+
setLastUpdatedAt(new Date().toISOString());
|
|
243
|
+
} catch (err) {
|
|
244
|
+
setError(err instanceof Error ? err.message : "Unknown error");
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async function postAction(action: "cancel" | "retry") {
|
|
249
|
+
setBusy(action);
|
|
250
|
+
setError(null);
|
|
251
|
+
try {
|
|
252
|
+
const res = await fetch(`/api/khotan/runs/${run.id}/${action}`, {
|
|
253
|
+
method: "POST",
|
|
254
|
+
});
|
|
255
|
+
if (!res.ok) {
|
|
256
|
+
const data = (await res.json().catch(() => ({}))) as { error?: string };
|
|
257
|
+
throw new Error(data.error ?? `Failed to ${action} run`);
|
|
258
|
+
}
|
|
259
|
+
onChanged();
|
|
260
|
+
} catch (err) {
|
|
261
|
+
setError(err instanceof Error ? err.message : "Unknown error");
|
|
262
|
+
} finally {
|
|
263
|
+
setBusy(null);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const workflowStatus =
|
|
268
|
+
typeof detail?.["workflowStatus"] === "string"
|
|
269
|
+
? detail["workflowStatus"]
|
|
270
|
+
: null;
|
|
271
|
+
|
|
272
|
+
return (
|
|
273
|
+
<div className="space-y-3 rounded-md border bg-muted/20 p-3">
|
|
274
|
+
<div className="flex flex-wrap items-center justify-between gap-3">
|
|
275
|
+
<div className="space-y-1 text-sm">
|
|
276
|
+
<div>
|
|
277
|
+
<span className="font-medium">Khotan run:</span>{" "}
|
|
278
|
+
<code className="text-xs">{run.id}</code>
|
|
279
|
+
</div>
|
|
280
|
+
<div>
|
|
281
|
+
<span className="font-medium">Workflow status:</span>{" "}
|
|
282
|
+
{workflowStatus ?? "unknown"}
|
|
283
|
+
</div>
|
|
284
|
+
{run.workflowRunId ? (
|
|
285
|
+
<div>
|
|
286
|
+
<span className="font-medium">Workflow run:</span>{" "}
|
|
287
|
+
<code className="text-xs">{run.workflowRunId}</code>
|
|
288
|
+
</div>
|
|
289
|
+
) : null}
|
|
290
|
+
<div className="text-xs text-muted-foreground">
|
|
291
|
+
Last updated:{" "}
|
|
292
|
+
{lastUpdatedAt ? formatDateTime(lastUpdatedAt) : "Not loaded yet"}
|
|
293
|
+
</div>
|
|
294
|
+
</div>
|
|
295
|
+
<div className="flex items-center gap-2">
|
|
296
|
+
<Button
|
|
297
|
+
variant="outline"
|
|
298
|
+
size="sm"
|
|
299
|
+
disabled={streamingEnabled}
|
|
300
|
+
onClick={() => void refreshDetail()}
|
|
301
|
+
>
|
|
302
|
+
Refresh
|
|
303
|
+
</Button>
|
|
304
|
+
<Button
|
|
305
|
+
variant="outline"
|
|
306
|
+
size="sm"
|
|
307
|
+
disabled={
|
|
308
|
+
!run.workflowRunId || busy !== null || run.status !== "running"
|
|
309
|
+
}
|
|
310
|
+
onClick={() => void postAction("cancel")}
|
|
311
|
+
>
|
|
312
|
+
{busy === "cancel" ? "Cancelling..." : "Cancel"}
|
|
313
|
+
</Button>
|
|
314
|
+
<Button
|
|
315
|
+
variant="outline"
|
|
316
|
+
size="sm"
|
|
317
|
+
disabled={busy !== null || run.sourceType !== "flow"}
|
|
318
|
+
onClick={() => void postAction("retry")}
|
|
319
|
+
>
|
|
320
|
+
{busy === "retry" ? "Retrying..." : "Retry"}
|
|
321
|
+
</Button>
|
|
322
|
+
</div>
|
|
323
|
+
</div>
|
|
324
|
+
|
|
325
|
+
{error ? (
|
|
326
|
+
<div className="rounded-md border border-destructive/30 bg-destructive/5 p-2 text-sm text-destructive">
|
|
327
|
+
{error}
|
|
328
|
+
</div>
|
|
329
|
+
) : null}
|
|
330
|
+
|
|
331
|
+
<div className="rounded-md bg-background p-3">
|
|
332
|
+
<div className="mb-2 text-xs font-medium uppercase text-muted-foreground">
|
|
333
|
+
Workflow stream
|
|
334
|
+
</div>
|
|
335
|
+
{streamLines.length > 0 ? (
|
|
336
|
+
<pre className="max-h-56 overflow-auto whitespace-pre-wrap text-xs">
|
|
337
|
+
{streamLines.join("\n")}
|
|
338
|
+
</pre>
|
|
339
|
+
) : (
|
|
340
|
+
<p className="text-sm text-muted-foreground">
|
|
341
|
+
{streamingEnabled
|
|
342
|
+
? "No stream updates yet. Use sendUpdate() inside Workflow steps to emit progress."
|
|
343
|
+
: run.status === "pending" || run.status === "running"
|
|
344
|
+
? "Streaming is off. Turn it on to follow live Workflow updates."
|
|
345
|
+
: "No stream logs found for this completed Workflow run."}
|
|
346
|
+
</p>
|
|
347
|
+
)}
|
|
348
|
+
{!streamingEnabled && streamLines.length > 0 ? (
|
|
349
|
+
<p className="mt-2 text-xs text-muted-foreground">
|
|
350
|
+
Streaming is off. Showing the last loaded Workflow logs.
|
|
351
|
+
</p>
|
|
352
|
+
) : null}
|
|
353
|
+
</div>
|
|
354
|
+
</div>
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
export function KhotanRunsTable({ pageSize = 10 }: { pageSize?: number } = {}) {
|
|
359
|
+
const [data, setData] = useState<PageResponse<RunLogItem> | null>(null);
|
|
360
|
+
const [offset, setOffset] = useState(0);
|
|
361
|
+
const [loading, setLoading] = useState(true);
|
|
362
|
+
const [error, setError] = useState<string | null>(null);
|
|
363
|
+
const [refreshKey, setRefreshKey] = useState(0);
|
|
364
|
+
const [expandedRunId, setExpandedRunId] = useState<string | null>(null);
|
|
365
|
+
const [streamingEnabled, setStreamingEnabled] = useState(false);
|
|
366
|
+
const [lastUpdatedAt, setLastUpdatedAt] = useState<string | null>(null);
|
|
367
|
+
const [streamPulse, setStreamPulse] = useState(false);
|
|
368
|
+
|
|
369
|
+
const pulseLiveIndicator = useCallback(() => {
|
|
370
|
+
setStreamPulse(true);
|
|
371
|
+
window.setTimeout(() => setStreamPulse(false), 700);
|
|
372
|
+
}, []);
|
|
373
|
+
|
|
374
|
+
useEffect(() => {
|
|
375
|
+
let cancelled = false;
|
|
376
|
+
|
|
377
|
+
async function load() {
|
|
378
|
+
setLoading(true);
|
|
379
|
+
setError(null);
|
|
380
|
+
try {
|
|
381
|
+
const res = await fetch(
|
|
382
|
+
`/api/khotan/runs?limit=${String(pageSize)}&offset=${String(offset)}`,
|
|
383
|
+
);
|
|
384
|
+
if (!res.ok) {
|
|
385
|
+
throw new Error("Failed to load runs");
|
|
386
|
+
}
|
|
387
|
+
const json = (await res.json()) as PageResponse<RunLogItem>;
|
|
388
|
+
if (!cancelled) {
|
|
389
|
+
setData(json);
|
|
390
|
+
setLastUpdatedAt(new Date().toISOString());
|
|
391
|
+
}
|
|
392
|
+
} catch (err) {
|
|
393
|
+
if (!cancelled) {
|
|
394
|
+
setError(err instanceof Error ? err.message : "Unknown error");
|
|
395
|
+
}
|
|
396
|
+
} finally {
|
|
397
|
+
if (!cancelled) {
|
|
398
|
+
setLoading(false);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
void load();
|
|
404
|
+
return () => {
|
|
405
|
+
cancelled = true;
|
|
406
|
+
};
|
|
407
|
+
}, [offset, pageSize, refreshKey]);
|
|
408
|
+
|
|
409
|
+
return (
|
|
410
|
+
<Card>
|
|
411
|
+
<CardHeader className="flex flex-row items-center justify-between gap-4">
|
|
412
|
+
<div>
|
|
413
|
+
<CardTitle>Runs</CardTitle>
|
|
414
|
+
<p className="text-sm text-muted-foreground">
|
|
415
|
+
Recent flow and webhook execution history.
|
|
416
|
+
</p>
|
|
417
|
+
<p className="text-xs text-muted-foreground">
|
|
418
|
+
Last updated:{" "}
|
|
419
|
+
{lastUpdatedAt ? formatDateTime(lastUpdatedAt) : "Not loaded yet"}
|
|
420
|
+
</p>
|
|
421
|
+
</div>
|
|
422
|
+
<div className="flex items-center gap-3">
|
|
423
|
+
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
|
424
|
+
<span className="relative flex h-2.5 w-2.5">
|
|
425
|
+
{streamingEnabled && streamPulse ? (
|
|
426
|
+
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-emerald-400 opacity-75" />
|
|
427
|
+
) : null}
|
|
428
|
+
<span
|
|
429
|
+
className={`relative inline-flex h-2.5 w-2.5 rounded-full ${
|
|
430
|
+
streamingEnabled ? "bg-emerald-500" : "bg-muted-foreground/40"
|
|
431
|
+
}`}
|
|
432
|
+
/>
|
|
433
|
+
</span>
|
|
434
|
+
{streamingEnabled ? "Live" : "Idle"}
|
|
435
|
+
</div>
|
|
436
|
+
<Button
|
|
437
|
+
aria-label="Refresh runs"
|
|
438
|
+
title="Refresh runs"
|
|
439
|
+
variant="outline"
|
|
440
|
+
size="sm"
|
|
441
|
+
onClick={() => setRefreshKey((v) => v + 1)}
|
|
442
|
+
>
|
|
443
|
+
<RefreshCw aria-hidden="true" className="h-4 w-4" />
|
|
444
|
+
</Button>
|
|
445
|
+
<div className="flex items-center gap-2 text-sm">
|
|
446
|
+
<span>Streaming</span>
|
|
447
|
+
<Switch
|
|
448
|
+
checked={streamingEnabled}
|
|
449
|
+
onCheckedChange={setStreamingEnabled}
|
|
450
|
+
aria-label="Toggle run streaming"
|
|
451
|
+
/>
|
|
452
|
+
</div>
|
|
453
|
+
</div>
|
|
454
|
+
</CardHeader>
|
|
455
|
+
<CardContent className="space-y-4">
|
|
456
|
+
{error ? (
|
|
457
|
+
<div className="rounded-md border border-destructive/30 bg-destructive/5 p-3 text-sm text-destructive">
|
|
458
|
+
{error}
|
|
459
|
+
</div>
|
|
460
|
+
) : null}
|
|
461
|
+
|
|
462
|
+
<Table>
|
|
463
|
+
<TableHeader>
|
|
464
|
+
<TableRow>
|
|
465
|
+
<TableHead>Started</TableHead>
|
|
466
|
+
<TableHead>Status</TableHead>
|
|
467
|
+
<TableHead>Source</TableHead>
|
|
468
|
+
<TableHead>Plug</TableHead>
|
|
469
|
+
<TableHead>Run Type</TableHead>
|
|
470
|
+
<TableHead>Counts</TableHead>
|
|
471
|
+
<TableHead>Workflow</TableHead>
|
|
472
|
+
<TableHead />
|
|
473
|
+
</TableRow>
|
|
474
|
+
</TableHeader>
|
|
475
|
+
<TableBody>
|
|
476
|
+
{loading ? (
|
|
477
|
+
<TableRow>
|
|
478
|
+
<TableCell
|
|
479
|
+
colSpan={8}
|
|
480
|
+
className="text-sm text-muted-foreground"
|
|
481
|
+
>
|
|
482
|
+
Loading runs...
|
|
483
|
+
</TableCell>
|
|
484
|
+
</TableRow>
|
|
485
|
+
) : data?.items.length ? (
|
|
486
|
+
data.items.map((item) => (
|
|
487
|
+
<Fragment key={item.id}>
|
|
488
|
+
<TableRow>
|
|
489
|
+
<TableCell className="text-sm text-muted-foreground">
|
|
490
|
+
<div>{formatDateTime(item.startedAt)}</div>
|
|
491
|
+
<div className="text-xs">
|
|
492
|
+
{item.completedAt
|
|
493
|
+
? `completed ${formatDateTime(item.completedAt)}`
|
|
494
|
+
: "in progress"}
|
|
495
|
+
</div>
|
|
496
|
+
</TableCell>
|
|
497
|
+
<TableCell>
|
|
498
|
+
<Badge variant={statusVariant[item.status]}>
|
|
499
|
+
{statusLabel[item.status]}
|
|
500
|
+
</Badge>
|
|
501
|
+
{item.error ? (
|
|
502
|
+
<div
|
|
503
|
+
className="mt-1 max-w-56 truncate text-xs text-destructive"
|
|
504
|
+
title={item.error}
|
|
505
|
+
>
|
|
506
|
+
{item.error}
|
|
507
|
+
</div>
|
|
508
|
+
) : null}
|
|
509
|
+
</TableCell>
|
|
510
|
+
<TableCell className="font-medium">
|
|
511
|
+
{formatSource(item)}
|
|
512
|
+
</TableCell>
|
|
513
|
+
<TableCell className="text-muted-foreground">
|
|
514
|
+
{item.plugName ?? "-"}
|
|
515
|
+
</TableCell>
|
|
516
|
+
<TableCell className="font-mono text-xs">
|
|
517
|
+
{item.runType}
|
|
518
|
+
</TableCell>
|
|
519
|
+
<TableCell className="max-w-64 text-xs text-muted-foreground">
|
|
520
|
+
{formatCounts(item)}
|
|
521
|
+
</TableCell>
|
|
522
|
+
<TableCell className="font-mono text-xs text-muted-foreground">
|
|
523
|
+
{item.workflowRunId ?? "-"}
|
|
524
|
+
</TableCell>
|
|
525
|
+
<TableCell className="text-right">
|
|
526
|
+
<Button
|
|
527
|
+
variant="outline"
|
|
528
|
+
size="sm"
|
|
529
|
+
onClick={() =>
|
|
530
|
+
setExpandedRunId((current) =>
|
|
531
|
+
current === item.id ? null : item.id,
|
|
532
|
+
)
|
|
533
|
+
}
|
|
534
|
+
>
|
|
535
|
+
{expandedRunId === item.id ? "Hide" : "Details"}
|
|
536
|
+
</Button>
|
|
537
|
+
</TableCell>
|
|
538
|
+
</TableRow>
|
|
539
|
+
{expandedRunId === item.id ? (
|
|
540
|
+
<TableRow>
|
|
541
|
+
<TableCell colSpan={8}>
|
|
542
|
+
<RunDetails
|
|
543
|
+
run={item}
|
|
544
|
+
streamingEnabled={streamingEnabled}
|
|
545
|
+
onChanged={() => setRefreshKey((v) => v + 1)}
|
|
546
|
+
onStreamInbound={pulseLiveIndicator}
|
|
547
|
+
/>
|
|
548
|
+
</TableCell>
|
|
549
|
+
</TableRow>
|
|
550
|
+
) : null}
|
|
551
|
+
</Fragment>
|
|
552
|
+
))
|
|
553
|
+
) : (
|
|
554
|
+
<TableRow>
|
|
555
|
+
<TableCell
|
|
556
|
+
colSpan={8}
|
|
557
|
+
className="text-sm text-muted-foreground"
|
|
558
|
+
>
|
|
559
|
+
No runs recorded yet.
|
|
560
|
+
</TableCell>
|
|
561
|
+
</TableRow>
|
|
562
|
+
)}
|
|
563
|
+
</TableBody>
|
|
564
|
+
</Table>
|
|
565
|
+
|
|
566
|
+
<div className="flex items-center justify-between gap-3">
|
|
567
|
+
<p className="text-sm text-muted-foreground">
|
|
568
|
+
Page {Math.floor(offset / pageSize) + 1}
|
|
569
|
+
</p>
|
|
570
|
+
<div className="flex items-center gap-2">
|
|
571
|
+
<Button
|
|
572
|
+
variant="outline"
|
|
573
|
+
size="sm"
|
|
574
|
+
disabled={offset === 0 || loading}
|
|
575
|
+
onClick={() => setOffset(Math.max(offset - pageSize, 0))}
|
|
576
|
+
>
|
|
577
|
+
Previous
|
|
578
|
+
</Button>
|
|
579
|
+
<Button
|
|
580
|
+
variant="outline"
|
|
581
|
+
size="sm"
|
|
582
|
+
disabled={!data?.page.hasMore || loading}
|
|
583
|
+
onClick={() => setOffset(offset + pageSize)}
|
|
584
|
+
>
|
|
585
|
+
Next
|
|
586
|
+
</Button>
|
|
587
|
+
</div>
|
|
588
|
+
</div>
|
|
589
|
+
</CardContent>
|
|
590
|
+
</Card>
|
|
591
|
+
);
|
|
592
|
+
}
|