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,276 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState, useCallback } from "react";
|
|
4
|
+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
5
|
+
import { Badge } from "@/components/ui/badge";
|
|
6
|
+
import { Button } from "@/components/ui/button";
|
|
7
|
+
import { Input } from "@/components/ui/input";
|
|
8
|
+
import { Label } from "@/components/ui/label";
|
|
9
|
+
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// Var Panel — UI for managing plug variables
|
|
12
|
+
// Generated by khotan CLI · https://github.com/khotan-data
|
|
13
|
+
//
|
|
14
|
+
// This file is yours. Restyle, extend, or embed it however you like.
|
|
15
|
+
// It uses shadcn/ui primitives and fetches from /api/khotan/variables.
|
|
16
|
+
// ============================================================================
|
|
17
|
+
|
|
18
|
+
interface VarField {
|
|
19
|
+
key: string;
|
|
20
|
+
label: string;
|
|
21
|
+
type: "text" | "password" | "url";
|
|
22
|
+
secret?: boolean;
|
|
23
|
+
hidden?: boolean;
|
|
24
|
+
required?: boolean;
|
|
25
|
+
placeholder?: string;
|
|
26
|
+
defaultValue?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface VarPanelProps {
|
|
30
|
+
plugName: string;
|
|
31
|
+
label?: string;
|
|
32
|
+
basePath?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function VarPanel({
|
|
36
|
+
plugName,
|
|
37
|
+
label,
|
|
38
|
+
basePath = "/api/khotan",
|
|
39
|
+
}: VarPanelProps) {
|
|
40
|
+
const displayName = label ?? plugName;
|
|
41
|
+
const [fields, setFields] = useState<VarField[]>([]);
|
|
42
|
+
const [values, setValues] = useState<Record<string, string>>({});
|
|
43
|
+
const [formValues, setFormValues] = useState<Record<string, string>>({});
|
|
44
|
+
const [configured, setConfigured] = useState(false);
|
|
45
|
+
const [loading, setLoading] = useState(true);
|
|
46
|
+
const [saving, setSaving] = useState(false);
|
|
47
|
+
const [error, setError] = useState<string | null>(null);
|
|
48
|
+
const [success, setSuccess] = useState<string | null>(null);
|
|
49
|
+
const [editing, setEditing] = useState(false);
|
|
50
|
+
const [visibleSecrets, setVisibleSecrets] = useState<Record<string, boolean>>(
|
|
51
|
+
{},
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const fetchVariables = useCallback(async () => {
|
|
55
|
+
try {
|
|
56
|
+
const res = await fetch(`${basePath}/variables/${plugName}`);
|
|
57
|
+
if (!res.ok) {
|
|
58
|
+
setFields([]);
|
|
59
|
+
setConfigured(false);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const data = await res.json();
|
|
63
|
+
setFields((data.fields ?? []).filter((f: VarField) => !f.hidden));
|
|
64
|
+
setValues(data.values ?? {});
|
|
65
|
+
setConfigured(data.configured ?? false);
|
|
66
|
+
setFormValues(data.configured ? data.values : {});
|
|
67
|
+
setError(null);
|
|
68
|
+
} catch {
|
|
69
|
+
setError("Failed to load variables");
|
|
70
|
+
} finally {
|
|
71
|
+
setLoading(false);
|
|
72
|
+
}
|
|
73
|
+
}, [basePath, plugName]);
|
|
74
|
+
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
fetchVariables();
|
|
77
|
+
}, [fetchVariables]);
|
|
78
|
+
|
|
79
|
+
async function handleSave() {
|
|
80
|
+
setSaving(true);
|
|
81
|
+
setError(null);
|
|
82
|
+
setSuccess(null);
|
|
83
|
+
try {
|
|
84
|
+
const res = await fetch(`${basePath}/variables/${plugName}`, {
|
|
85
|
+
method: "POST",
|
|
86
|
+
headers: { "Content-Type": "application/json" },
|
|
87
|
+
body: JSON.stringify(formValues),
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
if (!res.ok) {
|
|
91
|
+
const data = await res.json();
|
|
92
|
+
setError(data.error ?? "Failed to save variables");
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
setSuccess("Variables saved");
|
|
97
|
+
setEditing(false);
|
|
98
|
+
await fetchVariables();
|
|
99
|
+
} catch (e) {
|
|
100
|
+
setError(e instanceof Error ? e.message : "Network error");
|
|
101
|
+
} finally {
|
|
102
|
+
setSaving(false);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function handleClear() {
|
|
107
|
+
setSaving(true);
|
|
108
|
+
setError(null);
|
|
109
|
+
setSuccess(null);
|
|
110
|
+
try {
|
|
111
|
+
const res = await fetch(`${basePath}/variables/${plugName}`, {
|
|
112
|
+
method: "DELETE",
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
if (!res.ok && res.status !== 204) {
|
|
116
|
+
const data = await res.json();
|
|
117
|
+
setError(data.error ?? "Failed to clear variables");
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
setSuccess("Variables cleared");
|
|
122
|
+
setFormValues({});
|
|
123
|
+
setEditing(false);
|
|
124
|
+
await fetchVariables();
|
|
125
|
+
} catch (e) {
|
|
126
|
+
setError(e instanceof Error ? e.message : "Network error");
|
|
127
|
+
} finally {
|
|
128
|
+
setSaving(false);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (loading) {
|
|
133
|
+
return (
|
|
134
|
+
<Card>
|
|
135
|
+
<CardHeader>
|
|
136
|
+
<CardTitle className="text-sm font-medium capitalize">
|
|
137
|
+
{displayName} Variables
|
|
138
|
+
</CardTitle>
|
|
139
|
+
</CardHeader>
|
|
140
|
+
<CardContent>
|
|
141
|
+
<div className="h-4 w-2/3 animate-pulse rounded bg-muted" />
|
|
142
|
+
</CardContent>
|
|
143
|
+
</Card>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (fields.length === 0) return null;
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
<Card>
|
|
151
|
+
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
152
|
+
<CardTitle className="text-sm font-medium capitalize">
|
|
153
|
+
{displayName} Variables
|
|
154
|
+
</CardTitle>
|
|
155
|
+
<Badge variant={configured ? "default" : "outline"}>
|
|
156
|
+
{configured ? "Configured" : "Not Configured"}
|
|
157
|
+
</Badge>
|
|
158
|
+
</CardHeader>
|
|
159
|
+
<CardContent className="space-y-4">
|
|
160
|
+
{error && <p className="text-sm text-destructive">{error}</p>}
|
|
161
|
+
{success && <p className="text-sm text-green-600">{success}</p>}
|
|
162
|
+
|
|
163
|
+
{!editing && configured ? (
|
|
164
|
+
<div className="space-y-3">
|
|
165
|
+
<div className="text-xs text-muted-foreground space-y-1">
|
|
166
|
+
{fields.map((field) => (
|
|
167
|
+
<p key={field.key}>
|
|
168
|
+
<span className="font-medium">{field.label}:</span>{" "}
|
|
169
|
+
<code className="bg-muted px-1 py-0.5 rounded text-[11px]">
|
|
170
|
+
{field.secret ? "••••••••" : (values[field.key] ?? "—")}
|
|
171
|
+
</code>
|
|
172
|
+
</p>
|
|
173
|
+
))}
|
|
174
|
+
</div>
|
|
175
|
+
<div className="flex gap-2">
|
|
176
|
+
<Button
|
|
177
|
+
size="sm"
|
|
178
|
+
variant="outline"
|
|
179
|
+
onClick={() => {
|
|
180
|
+
setEditing(true);
|
|
181
|
+
const prefilled: Record<string, string> = {};
|
|
182
|
+
for (const field of fields) {
|
|
183
|
+
if (!field.secret && values[field.key]) {
|
|
184
|
+
prefilled[field.key] = values[field.key];
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
setFormValues(prefilled);
|
|
188
|
+
setVisibleSecrets({});
|
|
189
|
+
setSuccess(null);
|
|
190
|
+
}}
|
|
191
|
+
>
|
|
192
|
+
Update
|
|
193
|
+
</Button>
|
|
194
|
+
<Button
|
|
195
|
+
size="sm"
|
|
196
|
+
variant="destructive"
|
|
197
|
+
onClick={handleClear}
|
|
198
|
+
disabled={saving}
|
|
199
|
+
>
|
|
200
|
+
{saving ? "Clearing..." : "Clear"}
|
|
201
|
+
</Button>
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
) : (
|
|
205
|
+
<div className="space-y-3">
|
|
206
|
+
{fields.map((field) => (
|
|
207
|
+
<div key={field.key} className="space-y-1">
|
|
208
|
+
<Label
|
|
209
|
+
htmlFor={`var-${plugName}-${field.key}`}
|
|
210
|
+
className="text-xs"
|
|
211
|
+
>
|
|
212
|
+
{field.label}
|
|
213
|
+
{field.required !== false && (
|
|
214
|
+
<span className="text-destructive ml-0.5">*</span>
|
|
215
|
+
)}
|
|
216
|
+
</Label>
|
|
217
|
+
<div className="relative">
|
|
218
|
+
<Input
|
|
219
|
+
id={`var-${plugName}-${field.key}`}
|
|
220
|
+
type={
|
|
221
|
+
field.secret && !visibleSecrets[field.key]
|
|
222
|
+
? "password"
|
|
223
|
+
: "text"
|
|
224
|
+
}
|
|
225
|
+
placeholder={
|
|
226
|
+
field.placeholder ??
|
|
227
|
+
(field.secret ? "Enter value..." : "")
|
|
228
|
+
}
|
|
229
|
+
value={formValues[field.key] ?? ""}
|
|
230
|
+
onChange={(e) =>
|
|
231
|
+
setFormValues((prev) => ({
|
|
232
|
+
...prev,
|
|
233
|
+
[field.key]: e.target.value,
|
|
234
|
+
}))
|
|
235
|
+
}
|
|
236
|
+
/>
|
|
237
|
+
{field.secret && (
|
|
238
|
+
<button
|
|
239
|
+
type="button"
|
|
240
|
+
className="absolute right-2 top-1/2 -translate-y-1/2 text-xs text-muted-foreground hover:text-foreground"
|
|
241
|
+
onClick={() =>
|
|
242
|
+
setVisibleSecrets((prev) => ({
|
|
243
|
+
...prev,
|
|
244
|
+
[field.key]: !prev[field.key],
|
|
245
|
+
}))
|
|
246
|
+
}
|
|
247
|
+
>
|
|
248
|
+
{visibleSecrets[field.key] ? "Hide" : "Show"}
|
|
249
|
+
</button>
|
|
250
|
+
)}
|
|
251
|
+
</div>
|
|
252
|
+
</div>
|
|
253
|
+
))}
|
|
254
|
+
<div className="flex gap-2">
|
|
255
|
+
<Button size="sm" onClick={handleSave} disabled={saving}>
|
|
256
|
+
{saving ? "Saving..." : "Save Variables"}
|
|
257
|
+
</Button>
|
|
258
|
+
{editing && (
|
|
259
|
+
<Button
|
|
260
|
+
size="sm"
|
|
261
|
+
variant="outline"
|
|
262
|
+
onClick={() => {
|
|
263
|
+
setEditing(false);
|
|
264
|
+
setSuccess(null);
|
|
265
|
+
}}
|
|
266
|
+
>
|
|
267
|
+
Cancel
|
|
268
|
+
</Button>
|
|
269
|
+
)}
|
|
270
|
+
</div>
|
|
271
|
+
</div>
|
|
272
|
+
)}
|
|
273
|
+
</CardContent>
|
|
274
|
+
</Card>
|
|
275
|
+
);
|
|
276
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { Badge } from "@/components/ui/badge";
|
|
5
|
+
import { Button } from "@/components/ui/button";
|
|
6
|
+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
7
|
+
import {
|
|
8
|
+
Table,
|
|
9
|
+
TableBody,
|
|
10
|
+
TableCell,
|
|
11
|
+
TableHead,
|
|
12
|
+
TableHeader,
|
|
13
|
+
TableRow,
|
|
14
|
+
} from "@/components/ui/table";
|
|
15
|
+
|
|
16
|
+
interface WebhookEventItem {
|
|
17
|
+
id: string;
|
|
18
|
+
khotanRunId: string;
|
|
19
|
+
eventType: string;
|
|
20
|
+
payload: Record<string, unknown>;
|
|
21
|
+
headers: Record<string, string>;
|
|
22
|
+
receivedAt: string;
|
|
23
|
+
handlerName: string | null;
|
|
24
|
+
handlerType: "catch" | "pass" | null;
|
|
25
|
+
plugName: string | null;
|
|
26
|
+
workflowRunId: string | null;
|
|
27
|
+
runStatus:
|
|
28
|
+
| "pending"
|
|
29
|
+
| "running"
|
|
30
|
+
| "completed"
|
|
31
|
+
| "partial"
|
|
32
|
+
| "failed"
|
|
33
|
+
| "cancelled"
|
|
34
|
+
| null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface PageResponse<T> {
|
|
38
|
+
items: T[];
|
|
39
|
+
page: {
|
|
40
|
+
limit: number;
|
|
41
|
+
offset: number;
|
|
42
|
+
hasMore: boolean;
|
|
43
|
+
prevOffset: number;
|
|
44
|
+
nextOffset: number;
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const statusVariant = {
|
|
49
|
+
pending: "outline",
|
|
50
|
+
running: "secondary",
|
|
51
|
+
completed: "default",
|
|
52
|
+
partial: "secondary",
|
|
53
|
+
failed: "destructive",
|
|
54
|
+
cancelled: "outline",
|
|
55
|
+
} as const;
|
|
56
|
+
|
|
57
|
+
function formatDateTime(value: string): string {
|
|
58
|
+
const date = new Date(value);
|
|
59
|
+
if (Number.isNaN(date.getTime())) return value;
|
|
60
|
+
return date
|
|
61
|
+
.toISOString()
|
|
62
|
+
.replace("T", " ")
|
|
63
|
+
.replace(/\.\d{3}Z$/, " UTC");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function formatHandler(item: WebhookEventItem): string {
|
|
67
|
+
if (!item.handlerName) return "Unknown";
|
|
68
|
+
if (!item.handlerType) return item.handlerName;
|
|
69
|
+
return `${item.handlerType}:${item.handlerName}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function KhotanWebhookEventsTable({
|
|
73
|
+
pageSize = 10,
|
|
74
|
+
}: { pageSize?: number } = {}) {
|
|
75
|
+
const [data, setData] = useState<PageResponse<WebhookEventItem> | null>(null);
|
|
76
|
+
const [offset, setOffset] = useState(0);
|
|
77
|
+
const [loading, setLoading] = useState(true);
|
|
78
|
+
const [error, setError] = useState<string | null>(null);
|
|
79
|
+
const [refreshKey, setRefreshKey] = useState(0);
|
|
80
|
+
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
let cancelled = false;
|
|
83
|
+
|
|
84
|
+
async function load() {
|
|
85
|
+
setLoading(true);
|
|
86
|
+
setError(null);
|
|
87
|
+
try {
|
|
88
|
+
const res = await fetch(
|
|
89
|
+
`/api/khotan/webhook-events?limit=${String(pageSize)}&offset=${String(offset)}`,
|
|
90
|
+
);
|
|
91
|
+
if (!res.ok) {
|
|
92
|
+
throw new Error("Failed to load webhook events");
|
|
93
|
+
}
|
|
94
|
+
const json = (await res.json()) as PageResponse<WebhookEventItem>;
|
|
95
|
+
if (!cancelled) {
|
|
96
|
+
setData(json);
|
|
97
|
+
}
|
|
98
|
+
} catch (err) {
|
|
99
|
+
if (!cancelled) {
|
|
100
|
+
setError(err instanceof Error ? err.message : "Unknown error");
|
|
101
|
+
}
|
|
102
|
+
} finally {
|
|
103
|
+
if (!cancelled) {
|
|
104
|
+
setLoading(false);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
void load();
|
|
110
|
+
return () => {
|
|
111
|
+
cancelled = true;
|
|
112
|
+
};
|
|
113
|
+
}, [offset, pageSize, refreshKey]);
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<Card>
|
|
117
|
+
<CardHeader className="flex flex-row items-center justify-between gap-4">
|
|
118
|
+
<div>
|
|
119
|
+
<CardTitle>Webhook Events</CardTitle>
|
|
120
|
+
<p className="text-sm text-muted-foreground">
|
|
121
|
+
Recent inbound events captured by Khotan before workflow execution.
|
|
122
|
+
</p>
|
|
123
|
+
</div>
|
|
124
|
+
<Button
|
|
125
|
+
variant="outline"
|
|
126
|
+
size="sm"
|
|
127
|
+
onClick={() => setRefreshKey((v) => v + 1)}
|
|
128
|
+
>
|
|
129
|
+
Refresh
|
|
130
|
+
</Button>
|
|
131
|
+
</CardHeader>
|
|
132
|
+
<CardContent className="space-y-4">
|
|
133
|
+
{error ? (
|
|
134
|
+
<div className="rounded-md border border-destructive/30 bg-destructive/5 p-3 text-sm text-destructive">
|
|
135
|
+
{error}
|
|
136
|
+
</div>
|
|
137
|
+
) : null}
|
|
138
|
+
|
|
139
|
+
<Table>
|
|
140
|
+
<TableHeader>
|
|
141
|
+
<TableRow>
|
|
142
|
+
<TableHead>Received</TableHead>
|
|
143
|
+
<TableHead>Event</TableHead>
|
|
144
|
+
<TableHead>Handler</TableHead>
|
|
145
|
+
<TableHead>Plug</TableHead>
|
|
146
|
+
<TableHead>Run</TableHead>
|
|
147
|
+
<TableHead>Payload</TableHead>
|
|
148
|
+
</TableRow>
|
|
149
|
+
</TableHeader>
|
|
150
|
+
<TableBody>
|
|
151
|
+
{loading ? (
|
|
152
|
+
<TableRow>
|
|
153
|
+
<TableCell
|
|
154
|
+
colSpan={6}
|
|
155
|
+
className="text-sm text-muted-foreground"
|
|
156
|
+
>
|
|
157
|
+
Loading webhook events...
|
|
158
|
+
</TableCell>
|
|
159
|
+
</TableRow>
|
|
160
|
+
) : data?.items.length ? (
|
|
161
|
+
data.items.map((item) => (
|
|
162
|
+
<TableRow key={item.id}>
|
|
163
|
+
<TableCell className="text-sm text-muted-foreground">
|
|
164
|
+
{formatDateTime(item.receivedAt)}
|
|
165
|
+
</TableCell>
|
|
166
|
+
<TableCell className="font-medium">
|
|
167
|
+
{item.eventType}
|
|
168
|
+
</TableCell>
|
|
169
|
+
<TableCell className="text-sm text-muted-foreground">
|
|
170
|
+
{formatHandler(item)}
|
|
171
|
+
</TableCell>
|
|
172
|
+
<TableCell className="text-muted-foreground">
|
|
173
|
+
{item.plugName ?? "-"}
|
|
174
|
+
</TableCell>
|
|
175
|
+
<TableCell className="space-y-1 text-xs">
|
|
176
|
+
<div className="font-mono text-muted-foreground">
|
|
177
|
+
{item.khotanRunId}
|
|
178
|
+
</div>
|
|
179
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
180
|
+
{item.runStatus ? (
|
|
181
|
+
<Badge variant={statusVariant[item.runStatus]}>
|
|
182
|
+
{item.runStatus}
|
|
183
|
+
</Badge>
|
|
184
|
+
) : null}
|
|
185
|
+
<span className="font-mono text-muted-foreground">
|
|
186
|
+
{item.workflowRunId ?? "-"}
|
|
187
|
+
</span>
|
|
188
|
+
</div>
|
|
189
|
+
</TableCell>
|
|
190
|
+
<TableCell className="max-w-80">
|
|
191
|
+
<details>
|
|
192
|
+
<summary className="cursor-pointer text-sm text-primary">
|
|
193
|
+
View payload
|
|
194
|
+
</summary>
|
|
195
|
+
<pre className="mt-2 max-h-64 overflow-auto rounded-md bg-muted p-3 text-xs">
|
|
196
|
+
{JSON.stringify(item.payload, null, 2)}
|
|
197
|
+
</pre>
|
|
198
|
+
</details>
|
|
199
|
+
</TableCell>
|
|
200
|
+
</TableRow>
|
|
201
|
+
))
|
|
202
|
+
) : (
|
|
203
|
+
<TableRow>
|
|
204
|
+
<TableCell
|
|
205
|
+
colSpan={6}
|
|
206
|
+
className="text-sm text-muted-foreground"
|
|
207
|
+
>
|
|
208
|
+
No webhook events recorded yet.
|
|
209
|
+
</TableCell>
|
|
210
|
+
</TableRow>
|
|
211
|
+
)}
|
|
212
|
+
</TableBody>
|
|
213
|
+
</Table>
|
|
214
|
+
|
|
215
|
+
<div className="flex items-center justify-between gap-3">
|
|
216
|
+
<p className="text-sm text-muted-foreground">
|
|
217
|
+
Page {Math.floor(offset / pageSize) + 1}
|
|
218
|
+
</p>
|
|
219
|
+
<div className="flex items-center gap-2">
|
|
220
|
+
<Button
|
|
221
|
+
variant="outline"
|
|
222
|
+
size="sm"
|
|
223
|
+
disabled={offset === 0 || loading}
|
|
224
|
+
onClick={() => setOffset(Math.max(offset - pageSize, 0))}
|
|
225
|
+
>
|
|
226
|
+
Previous
|
|
227
|
+
</Button>
|
|
228
|
+
<Button
|
|
229
|
+
variant="outline"
|
|
230
|
+
size="sm"
|
|
231
|
+
disabled={!data?.page.hasMore || loading}
|
|
232
|
+
onClick={() => setOffset(offset + pageSize)}
|
|
233
|
+
>
|
|
234
|
+
Next
|
|
235
|
+
</Button>
|
|
236
|
+
</div>
|
|
237
|
+
</div>
|
|
238
|
+
</CardContent>
|
|
239
|
+
</Card>
|
|
240
|
+
);
|
|
241
|
+
}
|