khotan-data 0.1.1 → 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.
@@ -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<string | null>(null);
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 res = await fetch(
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 instanceof Error ? err.message : "Unknown error");
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
- <div className="rounded-md border border-destructive/30 bg-destructive/5 p-3 text-sm text-destructive">
135
- {error}
136
- </div>
131
+ <ApiErrorState
132
+ error={error}
133
+ onRetry={() => setRefreshKey((v) => v + 1)}
134
+ compact
135
+ />
137
136
  ) : null}
138
137
 
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 ? (
138
+ {error ? null : (
139
+ <Table>
140
+ <TableHeader>
152
141
  <TableRow>
153
- <TableCell
154
- colSpan={6}
155
- className="text-sm text-muted-foreground"
156
- >
157
- Loading webhook events...
158
- </TableCell>
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
- ) : 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>
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
- <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>
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
- <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>
211
+ )}
212
+ </TableBody>
213
+ </Table>
214
+ )}
214
215
 
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>
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
- </div>
240
+ )}
238
241
  </CardContent>
239
242
  </Card>
240
243
  );
@@ -4,6 +4,7 @@ import { useEffect, useState, useCallback } from "react";
4
4
  import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
5
5
  import { Badge } from "@/components/ui/badge";
6
6
  import { Button } from "@/components/ui/button";
7
+ import { khotanFetch, ApiErrorState } from "./api-state";
7
8
 
8
9
  // ============================================================================
9
10
  // Wire Panel — UI for managing webhook subscriptions
@@ -45,21 +46,23 @@ export function WirePanel({
45
46
  const [loading, setLoading] = useState(true);
46
47
  const [acting, setActing] = useState(false);
47
48
  const [error, setError] = useState<string | null>(null);
49
+ const [loadError, setLoadError] = useState<unknown>(null);
48
50
 
49
51
  const fetchWire = useCallback(async () => {
52
+ setLoading(true);
53
+ setLoadError(null);
50
54
  try {
51
- const res = await fetch(`${basePath}/wires/${plugName}`);
52
- if (!res.ok) {
53
- setConfigured(false);
54
- setWire(null);
55
- return;
56
- }
57
- const data = await res.json();
55
+ const data = await khotanFetch<{
56
+ configured?: boolean;
57
+ wire?: WireRecord | null;
58
+ }>(`${basePath}/wires/${plugName}`);
58
59
  setConfigured(data.configured ?? false);
59
60
  setWire(data.wire ?? null);
60
61
  setError(null);
61
- } catch {
62
+ } catch (err) {
62
63
  setConfigured(false);
64
+ setWire(null);
65
+ setLoadError(err);
63
66
  } finally {
64
67
  setLoading(false);
65
68
  }
@@ -136,6 +139,25 @@ export function WirePanel({
136
139
  );
137
140
  }
138
141
 
142
+ if (loadError) {
143
+ return (
144
+ <Card>
145
+ <CardHeader className="pb-2">
146
+ <CardTitle className="text-sm font-medium capitalize">
147
+ {displayName} Wire
148
+ </CardTitle>
149
+ </CardHeader>
150
+ <CardContent>
151
+ <ApiErrorState
152
+ error={loadError}
153
+ onRetry={() => void fetchWire()}
154
+ compact
155
+ />
156
+ </CardContent>
157
+ </Card>
158
+ );
159
+ }
160
+
139
161
  if (!configured) return null;
140
162
 
141
163
  const isActive = wire?.status === "active";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "khotan-data",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Data primitives for TypeScript — ETL pipelines, transforms, and Drizzle Postgres integration.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",