khotan-data 0.1.0 → 0.2.0
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/README.md +89 -6
- package/dist/cli.js +405 -35
- package/dist/factory.cjs +1160 -106
- package/dist/factory.cjs.map +1 -1
- package/dist/factory.d.cts +262 -38
- package/dist/factory.d.ts +262 -38
- package/dist/factory.js +1158 -108
- package/dist/factory.js.map +1 -1
- package/dist/templates/api-state.tsx +249 -0
- package/dist/templates/cache.example.ts +11 -0
- package/dist/templates/cache.ts +58 -0
- package/dist/templates/catch.ts +13 -1
- package/dist/templates/debug-index-page.tsx +56 -36
- package/dist/templates/hub.tsx +9 -23
- package/dist/templates/inflow.ts +5 -6
- package/dist/templates/khotan-config.ts +30 -4
- package/dist/templates/mapping-browser.tsx +773 -0
- package/dist/templates/mappings-page.tsx +9 -0
- package/dist/templates/outflow.ts +5 -5
- package/dist/templates/pass.ts +10 -0
- package/dist/templates/plug-debugger.tsx +15 -7
- package/dist/templates/relay.example.ts +11 -1
- package/dist/templates/relay.ts +16 -7
- package/dist/templates/runs-table.tsx +133 -130
- package/dist/templates/schema.ts +81 -0
- package/dist/templates/skill-plug.md +38 -15
- package/dist/templates/skill-setup.md +80 -3
- package/dist/templates/topology-canvas.tsx +19 -30
- package/dist/templates/var-panel.tsx +33 -10
- package/dist/templates/webhook-events-table.tsx +105 -102
- package/dist/templates/wire-panel.tsx +30 -8
- package/package.json +1 -1
|
@@ -98,28 +98,39 @@ export const myPlug = plug({
|
|
|
98
98
|
|
|
99
99
|
Endpoints power the plug debugger UI, `khotan plug --compare`, and typed clients.
|
|
100
100
|
|
|
101
|
-
##
|
|
101
|
+
## Preferred Pattern
|
|
102
102
|
|
|
103
|
-
|
|
103
|
+
Keep each integration in a single app-owned plug file when possible:
|
|
104
104
|
|
|
105
105
|
```typescript
|
|
106
|
-
import {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
responses: { 200: z.object({ data: z.array(ProductSchema), total: z.number() }) },
|
|
114
|
-
},
|
|
106
|
+
import { z } from "zod";
|
|
107
|
+
import { plug, basic } from "./plug";
|
|
108
|
+
|
|
109
|
+
const ProductSchema = z.object({
|
|
110
|
+
id: z.string(),
|
|
111
|
+
sku: z.string(),
|
|
112
|
+
name: z.string(),
|
|
115
113
|
});
|
|
116
114
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
115
|
+
export type Product = z.infer<typeof ProductSchema>;
|
|
116
|
+
|
|
117
|
+
export const myPlug = plug({
|
|
118
|
+
name: "my-service",
|
|
119
|
+
baseUrl: "https://api.example.com",
|
|
120
|
+
auth: basic(process.env.API_USER!, process.env.API_KEY!),
|
|
121
|
+
endpoints: {
|
|
122
|
+
listProducts: {
|
|
123
|
+
method: "GET",
|
|
124
|
+
path: "/products",
|
|
125
|
+
query: z.object({ page: z.number().optional(), limit: z.number().optional() }),
|
|
126
|
+
responses: { 200: z.array(ProductSchema) },
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
});
|
|
121
130
|
```
|
|
122
131
|
|
|
132
|
+
This keeps the runtime plug, debugger metadata, `khotan plug --compare`, and any exported types in one place.
|
|
133
|
+
|
|
123
134
|
## Hooks
|
|
124
135
|
|
|
125
136
|
```typescript
|
|
@@ -179,6 +190,18 @@ npx khotan plug myPlug --endpoint listProducts --compare # Check schema
|
|
|
179
190
|
|
|
180
191
|
Set `KHOTAN_DEBUG=1` for verbose `[khotan:auth]` and `[khotan:request]` console logs.
|
|
181
192
|
|
|
193
|
+
### Recommended Plug Workflow
|
|
194
|
+
|
|
195
|
+
1. Create the plug file and auth/hook setup.
|
|
196
|
+
2. Add a small set of typed endpoints directly on the plug (`listProducts`, `getProduct`, etc).
|
|
197
|
+
3. Run the app with `KHOTAN_DEBUG=1`.
|
|
198
|
+
4. Use `npx khotan plug myPlug --info` to confirm the endpoints are visible to the debugger.
|
|
199
|
+
5. Use `npx khotan plug myPlug --endpoint listProducts --compare` against the live API.
|
|
200
|
+
6. Tighten schemas until the compare output matches the real payload shape you care about.
|
|
201
|
+
7. Only then build inflows, relays, outflows, or webhook handlers on top of those endpoints.
|
|
202
|
+
|
|
203
|
+
The package does not paginate or delta-sync for you automatically inside user flows. Your app code decides which typed endpoints to call, what page size to use, when to stop, and how to implement full, test, partial, backfill, reconcile, or delta runs.
|
|
204
|
+
|
|
182
205
|
## Managing Vars
|
|
183
206
|
|
|
184
207
|
Use the CLI to inspect and update stored plug variables:
|
|
@@ -42,8 +42,8 @@ import { stripeChargesInflow } from "./flows/stripe-charges";
|
|
|
42
42
|
const khotanData = khotan({
|
|
43
43
|
adapter: drizzleAdapter(db),
|
|
44
44
|
resources: [
|
|
45
|
-
{ name: "products", connectField: "sku" },
|
|
46
|
-
{ name: "orders", connectField: "order_number" },
|
|
45
|
+
{ name: "products", mapping: { connectField: "sku" } },
|
|
46
|
+
{ name: "orders", mapping: { connectField: "order_number" } },
|
|
47
47
|
],
|
|
48
48
|
plugs: [
|
|
49
49
|
{
|
|
@@ -90,8 +90,44 @@ The schema command auto-detects your Drizzle schema directory, updates `drizzle.
|
|
|
90
90
|
|----------|----------|---------|
|
|
91
91
|
| `DATABASE_URL` | Yes | Postgres connection (used by Drizzle) |
|
|
92
92
|
| `KHOTAN_SECRET` | For variables | AES-256-GCM key for encrypting plug vars |
|
|
93
|
-
| `KHOTAN_DEBUG` | For debugging | Enables `/debug/*` routes and the `plug` CLI (`probe` alias) |
|
|
93
|
+
| `KHOTAN_DEBUG` | For debugging | Enables `/debug/*` routes and the `plug` CLI (`probe` alias). Automatically disabled when `NODE_ENV=production` |
|
|
94
94
|
| `KHOTAN_WEBHOOK_URL` | For webhooks | Public URL for wire callbacks |
|
|
95
|
+
| `CRON_SECRET` | For production cron | Protects the built-in `/api/khotan/cron` dispatcher route. The route fails closed in production when this is unset |
|
|
96
|
+
|
|
97
|
+
## Securing the Management API
|
|
98
|
+
|
|
99
|
+
The management API (`/api/khotan/*`) and the Hub dashboard expose plug
|
|
100
|
+
credentials and operational controls. **They are public unless you gate them.**
|
|
101
|
+
|
|
102
|
+
Pass an `authorize` hook to `khotan({ ... })`. It receives the raw `Request` and
|
|
103
|
+
returns `true` to allow the request or `false` to reject it with `401`. It
|
|
104
|
+
composes directly with session libraries like better-auth:
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
import { khotan, drizzleAdapter } from "khotan-data/factory";
|
|
108
|
+
import { auth } from "@/lib/auth";
|
|
109
|
+
import { db } from "@/db";
|
|
110
|
+
|
|
111
|
+
const khotanData = khotan({
|
|
112
|
+
adapter: drizzleAdapter(db),
|
|
113
|
+
authorize: async (request) => {
|
|
114
|
+
const session = await auth.api.getSession({ headers: request.headers });
|
|
115
|
+
return Boolean(session?.user); // or: session?.user?.role === "admin"
|
|
116
|
+
},
|
|
117
|
+
plugs: [/* ... */],
|
|
118
|
+
});
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Notes:
|
|
122
|
+
- `authorize` is **not** a replacement for `KHOTAN_SECRET` — that key only
|
|
123
|
+
encrypts credentials at rest, it does not authenticate requests.
|
|
124
|
+
- Inbound webhooks (`POST /webhook/:plug`, verified per-plug via `onVerify`),
|
|
125
|
+
the cron dispatcher (`CRON_SECRET`), and debug routes (`KHOTAN_DEBUG`,
|
|
126
|
+
non-production only) are exempt from `authorize` automatically.
|
|
127
|
+
- Also protect the Hub dashboard page (e.g. `/config`) with your app's
|
|
128
|
+
middleware — `authorize` only guards the API, not your React pages.
|
|
129
|
+
- Without `authorize`, khotan logs a startup warning. Always configure it
|
|
130
|
+
before deploying.
|
|
95
131
|
|
|
96
132
|
## Next.js Config
|
|
97
133
|
|
|
@@ -111,6 +147,47 @@ curl http://localhost:3000/api/khotan/flows # Should list flows
|
|
|
111
147
|
curl http://localhost:3000/api/khotan/resources # Should list resources
|
|
112
148
|
```
|
|
113
149
|
|
|
150
|
+
## Scheduled Flows On Vercel
|
|
151
|
+
|
|
152
|
+
Khotan flow `schedule` values are runtime source-of-truth metadata. On Vercel, prefer a single dispatcher CRON instead of defining one platform CRON per flow.
|
|
153
|
+
|
|
154
|
+
Add one entry to `vercel.json`:
|
|
155
|
+
|
|
156
|
+
```json
|
|
157
|
+
{
|
|
158
|
+
"crons": [
|
|
159
|
+
{ "path": "/api/khotan/cron", "schedule": "* * * * *" }
|
|
160
|
+
]
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Then define schedules only on your flows in `{outputDir}/khotan.ts`:
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
{
|
|
168
|
+
name: "products-inflow",
|
|
169
|
+
type: "inflow",
|
|
170
|
+
schedule: "0 * * * *",
|
|
171
|
+
resource: "products",
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
The dispatcher route evaluates which flows are due on each tick and starts them through the normal run-tracking path. If `CRON_SECRET` is set, Vercel should call the route with `Authorization: Bearer <CRON_SECRET>`.
|
|
176
|
+
|
|
177
|
+
## Typical Build Order
|
|
178
|
+
|
|
179
|
+
After init and schema setup, the usual path to a working sync is:
|
|
180
|
+
|
|
181
|
+
1. Add or author a plug file for the external service.
|
|
182
|
+
2. Define a few typed endpoints directly on the plug with Zod response schemas.
|
|
183
|
+
3. Start the app with `KHOTAN_DEBUG=1`.
|
|
184
|
+
4. Verify the plug is visible with `npx khotan plug --list` and `npx khotan plug myPlug --info`.
|
|
185
|
+
5. Hit live endpoints with `npx khotan plug myPlug --endpoint listProducts --compare` until the schemas match the real API shape you intend to use.
|
|
186
|
+
6. Register the plug in `{outputDir}/khotan.ts` with resources and flows.
|
|
187
|
+
7. Only after endpoint verification, build inflows, relays, outflows, or webhook handlers on top of those live-checked endpoints.
|
|
188
|
+
|
|
189
|
+
This keeps sync logic grounded in real API payloads before you write pagination, mapping, or transformation code.
|
|
190
|
+
|
|
114
191
|
## Troubleshooting
|
|
115
192
|
|
|
116
193
|
- **Empty plug list**: Factory upserts on first request — hit any endpoint first, then check `/plugs`
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
type NodeProps,
|
|
19
19
|
} from "@xyflow/react";
|
|
20
20
|
import "@xyflow/react/dist/style.css";
|
|
21
|
+
import { khotanFetch, ApiErrorState } from "./api-state";
|
|
21
22
|
import { Badge } from "@/components/ui/badge";
|
|
22
23
|
import {
|
|
23
24
|
Card,
|
|
@@ -1056,8 +1057,9 @@ function TopologyCanvasInner() {
|
|
|
1056
1057
|
const [nodes, setNodes, onNodesChange] = useNodesState<TopologyNodeData>([]);
|
|
1057
1058
|
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
|
1058
1059
|
const [loading, setLoading] = useState(true);
|
|
1059
|
-
const [error, setError] = useState<
|
|
1060
|
+
const [error, setError] = useState<unknown>(null);
|
|
1060
1061
|
const [lastUpdatedAt, setLastUpdatedAt] = useState<string | null>(null);
|
|
1062
|
+
const [refreshKey, setRefreshKey] = useState(0);
|
|
1061
1063
|
|
|
1062
1064
|
useEffect(() => {
|
|
1063
1065
|
let cancelled = false;
|
|
@@ -1068,30 +1070,22 @@ function TopologyCanvasInner() {
|
|
|
1068
1070
|
setError(null);
|
|
1069
1071
|
}
|
|
1070
1072
|
|
|
1071
|
-
const [
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1073
|
+
const [plugs, flows, runsRaw] = await Promise.all([
|
|
1074
|
+
khotanFetch<PlugRecord[]>("/api/khotan/plugs"),
|
|
1075
|
+
khotanFetch<FlowRecord[]>("/api/khotan/flows"),
|
|
1076
|
+
khotanFetch<unknown>("/api/khotan/runs?limit=100"),
|
|
1075
1077
|
]);
|
|
1076
1078
|
|
|
1077
|
-
|
|
1078
|
-
throw new Error("Failed to load topology data from /api/khotan");
|
|
1079
|
-
}
|
|
1080
|
-
|
|
1081
|
-
const plugs = (await plugsRes.json()) as PlugRecord[];
|
|
1082
|
-
const flows = (await flowsRes.json()) as FlowRecord[];
|
|
1083
|
-
const runs = normalizeRuns(await runsRes.json());
|
|
1079
|
+
const runs = normalizeRuns(runsRaw);
|
|
1084
1080
|
|
|
1085
1081
|
const webhookGroups = await Promise.all(
|
|
1086
1082
|
plugs.map(async (plug) => {
|
|
1087
1083
|
try {
|
|
1088
|
-
const
|
|
1084
|
+
const handlers = await khotanFetch<
|
|
1085
|
+
Array<Omit<WebhookHandlerRecord, "plugName">>
|
|
1086
|
+
>(
|
|
1089
1087
|
`/api/khotan/webhook-handlers/${encodeURIComponent(plug.name)}`,
|
|
1090
1088
|
);
|
|
1091
|
-
if (!res.ok) return [] as WebhookHandlerRecord[];
|
|
1092
|
-
const handlers = (await res.json()) as Array<
|
|
1093
|
-
Omit<WebhookHandlerRecord, "plugName">
|
|
1094
|
-
>;
|
|
1095
1089
|
return handlers.map((handler) => ({
|
|
1096
1090
|
...handler,
|
|
1097
1091
|
plugName: plug.name,
|
|
@@ -1113,11 +1107,7 @@ function TopologyCanvasInner() {
|
|
|
1113
1107
|
setLastUpdatedAt(new Date().toISOString());
|
|
1114
1108
|
} catch (loadError) {
|
|
1115
1109
|
if (!cancelled) {
|
|
1116
|
-
setError(
|
|
1117
|
-
loadError instanceof Error
|
|
1118
|
-
? loadError.message
|
|
1119
|
-
: "Unknown topology load failure",
|
|
1120
|
-
);
|
|
1110
|
+
setError(loadError);
|
|
1121
1111
|
}
|
|
1122
1112
|
} finally {
|
|
1123
1113
|
if (!cancelled) {
|
|
@@ -1135,7 +1125,7 @@ function TopologyCanvasInner() {
|
|
|
1135
1125
|
cancelled = true;
|
|
1136
1126
|
window.clearInterval(interval);
|
|
1137
1127
|
};
|
|
1138
|
-
}, []);
|
|
1128
|
+
}, [refreshKey]);
|
|
1139
1129
|
|
|
1140
1130
|
const model = useMemo(() => {
|
|
1141
1131
|
return snapshot ? buildTopologyModel(snapshot) : null;
|
|
@@ -1180,19 +1170,18 @@ function TopologyCanvasInner() {
|
|
|
1180
1170
|
|
|
1181
1171
|
if (error) {
|
|
1182
1172
|
return (
|
|
1183
|
-
<Card className="border-
|
|
1173
|
+
<Card className="border-white/70 bg-white/80 shadow-xl backdrop-blur">
|
|
1184
1174
|
<CardHeader>
|
|
1185
1175
|
<CardTitle>Topology Canvas</CardTitle>
|
|
1186
1176
|
<CardDescription>
|
|
1187
1177
|
Could not load the graph from the local Khotan API.
|
|
1188
1178
|
</CardDescription>
|
|
1189
1179
|
</CardHeader>
|
|
1190
|
-
<CardContent
|
|
1191
|
-
<
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
</p>
|
|
1180
|
+
<CardContent>
|
|
1181
|
+
<ApiErrorState
|
|
1182
|
+
error={error}
|
|
1183
|
+
onRetry={() => setRefreshKey((v) => v + 1)}
|
|
1184
|
+
/>
|
|
1196
1185
|
</CardContent>
|
|
1197
1186
|
</Card>
|
|
1198
1187
|
);
|
|
@@ -6,6 +6,7 @@ import { Badge } from "@/components/ui/badge";
|
|
|
6
6
|
import { Button } from "@/components/ui/button";
|
|
7
7
|
import { Input } from "@/components/ui/input";
|
|
8
8
|
import { Label } from "@/components/ui/label";
|
|
9
|
+
import { khotanFetch, ApiErrorState } from "./api-state";
|
|
9
10
|
|
|
10
11
|
// ============================================================================
|
|
11
12
|
// Var Panel — UI for managing plug variables
|
|
@@ -44,6 +45,7 @@ export function VarPanel({
|
|
|
44
45
|
const [configured, setConfigured] = useState(false);
|
|
45
46
|
const [loading, setLoading] = useState(true);
|
|
46
47
|
const [saving, setSaving] = useState(false);
|
|
48
|
+
const [loadError, setLoadError] = useState<unknown>(null);
|
|
47
49
|
const [error, setError] = useState<string | null>(null);
|
|
48
50
|
const [success, setSuccess] = useState<string | null>(null);
|
|
49
51
|
const [editing, setEditing] = useState(false);
|
|
@@ -52,21 +54,23 @@ export function VarPanel({
|
|
|
52
54
|
);
|
|
53
55
|
|
|
54
56
|
const fetchVariables = useCallback(async () => {
|
|
57
|
+
setLoading(true);
|
|
58
|
+
setLoadError(null);
|
|
55
59
|
try {
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
const data = await res.json();
|
|
60
|
+
const data = await khotanFetch<{
|
|
61
|
+
fields?: VarField[];
|
|
62
|
+
values?: Record<string, string>;
|
|
63
|
+
configured?: boolean;
|
|
64
|
+
}>(`${basePath}/variables/${plugName}`);
|
|
63
65
|
setFields((data.fields ?? []).filter((f: VarField) => !f.hidden));
|
|
64
66
|
setValues(data.values ?? {});
|
|
65
67
|
setConfigured(data.configured ?? false);
|
|
66
|
-
setFormValues(data.configured ? data.values : {});
|
|
68
|
+
setFormValues(data.configured ? (data.values ?? {}) : {});
|
|
67
69
|
setError(null);
|
|
68
|
-
} catch {
|
|
69
|
-
|
|
70
|
+
} catch (err) {
|
|
71
|
+
setFields([]);
|
|
72
|
+
setConfigured(false);
|
|
73
|
+
setLoadError(err);
|
|
70
74
|
} finally {
|
|
71
75
|
setLoading(false);
|
|
72
76
|
}
|
|
@@ -144,6 +148,25 @@ export function VarPanel({
|
|
|
144
148
|
);
|
|
145
149
|
}
|
|
146
150
|
|
|
151
|
+
if (loadError) {
|
|
152
|
+
return (
|
|
153
|
+
<Card>
|
|
154
|
+
<CardHeader className="pb-2">
|
|
155
|
+
<CardTitle className="text-sm font-medium capitalize">
|
|
156
|
+
{displayName} Variables
|
|
157
|
+
</CardTitle>
|
|
158
|
+
</CardHeader>
|
|
159
|
+
<CardContent>
|
|
160
|
+
<ApiErrorState
|
|
161
|
+
error={loadError}
|
|
162
|
+
onRetry={() => void fetchVariables()}
|
|
163
|
+
compact
|
|
164
|
+
/>
|
|
165
|
+
</CardContent>
|
|
166
|
+
</Card>
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
147
170
|
if (fields.length === 0) return null;
|
|
148
171
|
|
|
149
172
|
return (
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { useEffect, useState } from "react";
|
|
4
|
+
import { khotanFetch, ApiErrorState } from "./api-state";
|
|
4
5
|
import { Badge } from "@/components/ui/badge";
|
|
5
6
|
import { Button } from "@/components/ui/button";
|
|
6
7
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
@@ -75,7 +76,7 @@ export function KhotanWebhookEventsTable({
|
|
|
75
76
|
const [data, setData] = useState<PageResponse<WebhookEventItem> | null>(null);
|
|
76
77
|
const [offset, setOffset] = useState(0);
|
|
77
78
|
const [loading, setLoading] = useState(true);
|
|
78
|
-
const [error, setError] = useState<
|
|
79
|
+
const [error, setError] = useState<unknown>(null);
|
|
79
80
|
const [refreshKey, setRefreshKey] = useState(0);
|
|
80
81
|
|
|
81
82
|
useEffect(() => {
|
|
@@ -85,19 +86,15 @@ export function KhotanWebhookEventsTable({
|
|
|
85
86
|
setLoading(true);
|
|
86
87
|
setError(null);
|
|
87
88
|
try {
|
|
88
|
-
const
|
|
89
|
+
const json = await khotanFetch<PageResponse<WebhookEventItem>>(
|
|
89
90
|
`/api/khotan/webhook-events?limit=${String(pageSize)}&offset=${String(offset)}`,
|
|
90
91
|
);
|
|
91
|
-
if (!res.ok) {
|
|
92
|
-
throw new Error("Failed to load webhook events");
|
|
93
|
-
}
|
|
94
|
-
const json = (await res.json()) as PageResponse<WebhookEventItem>;
|
|
95
92
|
if (!cancelled) {
|
|
96
93
|
setData(json);
|
|
97
94
|
}
|
|
98
95
|
} catch (err) {
|
|
99
96
|
if (!cancelled) {
|
|
100
|
-
setError(err
|
|
97
|
+
setError(err);
|
|
101
98
|
}
|
|
102
99
|
} finally {
|
|
103
100
|
if (!cancelled) {
|
|
@@ -131,110 +128,116 @@ export function KhotanWebhookEventsTable({
|
|
|
131
128
|
</CardHeader>
|
|
132
129
|
<CardContent className="space-y-4">
|
|
133
130
|
{error ? (
|
|
134
|
-
<
|
|
135
|
-
{error}
|
|
136
|
-
|
|
131
|
+
<ApiErrorState
|
|
132
|
+
error={error}
|
|
133
|
+
onRetry={() => setRefreshKey((v) => v + 1)}
|
|
134
|
+
compact
|
|
135
|
+
/>
|
|
137
136
|
) : null}
|
|
138
137
|
|
|
139
|
-
|
|
140
|
-
<
|
|
141
|
-
<
|
|
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 ? (
|
|
138
|
+
{error ? null : (
|
|
139
|
+
<Table>
|
|
140
|
+
<TableHeader>
|
|
152
141
|
<TableRow>
|
|
153
|
-
<
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
>
|
|
157
|
-
|
|
158
|
-
</
|
|
142
|
+
<TableHead>Received</TableHead>
|
|
143
|
+
<TableHead>Event</TableHead>
|
|
144
|
+
<TableHead>Handler</TableHead>
|
|
145
|
+
<TableHead>Plug</TableHead>
|
|
146
|
+
<TableHead>Run</TableHead>
|
|
147
|
+
<TableHead>Payload</TableHead>
|
|
159
148
|
</TableRow>
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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>
|
|
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...
|
|
189
158
|
</TableCell>
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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.
|
|
199
209
|
</TableCell>
|
|
200
210
|
</TableRow>
|
|
201
|
-
)
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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>
|
|
211
|
+
)}
|
|
212
|
+
</TableBody>
|
|
213
|
+
</Table>
|
|
214
|
+
)}
|
|
214
215
|
|
|
215
|
-
|
|
216
|
-
<
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
<
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
216
|
+
{error ? null : (
|
|
217
|
+
<div className="flex items-center justify-between gap-3">
|
|
218
|
+
<p className="text-sm text-muted-foreground">
|
|
219
|
+
Page {Math.floor(offset / pageSize) + 1}
|
|
220
|
+
</p>
|
|
221
|
+
<div className="flex items-center gap-2">
|
|
222
|
+
<Button
|
|
223
|
+
variant="outline"
|
|
224
|
+
size="sm"
|
|
225
|
+
disabled={offset === 0 || loading}
|
|
226
|
+
onClick={() => setOffset(Math.max(offset - pageSize, 0))}
|
|
227
|
+
>
|
|
228
|
+
Previous
|
|
229
|
+
</Button>
|
|
230
|
+
<Button
|
|
231
|
+
variant="outline"
|
|
232
|
+
size="sm"
|
|
233
|
+
disabled={!data?.page.hasMore || loading}
|
|
234
|
+
onClick={() => setOffset(offset + pageSize)}
|
|
235
|
+
>
|
|
236
|
+
Next
|
|
237
|
+
</Button>
|
|
238
|
+
</div>
|
|
236
239
|
</div>
|
|
237
|
-
|
|
240
|
+
)}
|
|
238
241
|
</CardContent>
|
|
239
242
|
</Card>
|
|
240
243
|
);
|