@checkstack/integration-frontend 0.0.2

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.
@@ -0,0 +1,375 @@
1
+ import { useState, useEffect, useCallback } from "react";
2
+ import { Link, useSearchParams } from "react-router-dom";
3
+ import { Plus, Webhook, ArrowRight, Activity } from "lucide-react";
4
+ import {
5
+ PageLayout,
6
+ Card,
7
+ CardContent,
8
+ Button,
9
+ Badge,
10
+ SectionHeader,
11
+ DynamicIcon,
12
+ EmptyState,
13
+ Table,
14
+ TableBody,
15
+ TableCell,
16
+ TableHead,
17
+ TableHeader,
18
+ TableRow,
19
+ useToast,
20
+ type LucideIconName,
21
+ } from "@checkstack/ui";
22
+ import { useApi, rpcApiRef } from "@checkstack/frontend-api";
23
+ import { resolveRoute } from "@checkstack/common";
24
+ import {
25
+ IntegrationApi,
26
+ integrationRoutes,
27
+ type WebhookSubscription,
28
+ type IntegrationProviderInfo,
29
+ } from "@checkstack/integration-common";
30
+ import { SubscriptionDialog } from "../components/CreateSubscriptionDialog";
31
+
32
+ export const IntegrationsPage = () => {
33
+ const rpcApi = useApi(rpcApiRef);
34
+ const client = rpcApi.forPlugin(IntegrationApi);
35
+ const toast = useToast();
36
+ const [searchParams, setSearchParams] = useSearchParams();
37
+
38
+ const [subscriptions, setSubscriptions] = useState<WebhookSubscription[]>([]);
39
+ const [providers, setProviders] = useState<IntegrationProviderInfo[]>([]);
40
+ const [loading, setLoading] = useState(true);
41
+ const [dialogOpen, setDialogOpen] = useState(false);
42
+ const [selectedSubscription, setSelectedSubscription] =
43
+ useState<WebhookSubscription>();
44
+
45
+ // Stats state
46
+ const [stats, setStats] = useState<{
47
+ total: number;
48
+ successful: number;
49
+ failed: number;
50
+ retrying: number;
51
+ pending: number;
52
+ }>();
53
+
54
+ const fetchData = useCallback(async () => {
55
+ try {
56
+ const [subsResult, providersResult, statsResult] = await Promise.all([
57
+ client.listSubscriptions({ page: 1, pageSize: 100 }),
58
+ client.listProviders(),
59
+ client.getDeliveryStats({ hours: 24 }),
60
+ ]);
61
+ setSubscriptions(subsResult.subscriptions);
62
+ setProviders(providersResult);
63
+ setStats(statsResult);
64
+ } catch (error) {
65
+ console.error("Failed to load integrations data:", error);
66
+ toast.error("Failed to load integrations data");
67
+ } finally {
68
+ setLoading(false);
69
+ }
70
+ }, [client, toast]);
71
+
72
+ useEffect(() => {
73
+ void fetchData();
74
+ }, [fetchData]);
75
+
76
+ // Handle ?action=create URL parameter (from command palette)
77
+ useEffect(() => {
78
+ if (searchParams.get("action") === "create") {
79
+ setSelectedSubscription(undefined);
80
+ setDialogOpen(true);
81
+ // Clear the URL param after opening
82
+ searchParams.delete("action");
83
+ setSearchParams(searchParams, { replace: true });
84
+ }
85
+ }, [searchParams, setSearchParams]);
86
+
87
+ const getProviderInfo = (
88
+ providerId: string
89
+ ): IntegrationProviderInfo | undefined => {
90
+ return providers.find((p) => p.qualifiedId === providerId);
91
+ };
92
+
93
+ const handleToggle = async (id: string, enabled: boolean) => {
94
+ try {
95
+ await client.toggleSubscription({ id, enabled });
96
+ setSubscriptions((prev) =>
97
+ prev.map((s) => (s.id === id ? { ...s, enabled } : s))
98
+ );
99
+ toast.success(enabled ? "Subscription enabled" : "Subscription disabled");
100
+ } catch (error) {
101
+ console.error("Failed to toggle subscription:", error);
102
+ toast.error("Failed to toggle subscription");
103
+ }
104
+ };
105
+
106
+ const handleCreated = (newSub: WebhookSubscription) => {
107
+ setSubscriptions((prev) => [newSub, ...prev]);
108
+ setDialogOpen(false);
109
+ setSelectedSubscription(undefined);
110
+ };
111
+
112
+ const handleUpdated = () => {
113
+ // Refresh data after update
114
+ void fetchData();
115
+ setDialogOpen(false);
116
+ setSelectedSubscription(undefined);
117
+ };
118
+
119
+ const handleDeleted = (id: string) => {
120
+ setSubscriptions((prev) => prev.filter((s) => s.id !== id));
121
+ setDialogOpen(false);
122
+ setSelectedSubscription(undefined);
123
+ };
124
+
125
+ const openEditDialog = (sub: WebhookSubscription) => {
126
+ setSelectedSubscription(sub);
127
+ setDialogOpen(true);
128
+ };
129
+
130
+ const openCreateDialog = () => {
131
+ setSelectedSubscription(undefined);
132
+ setDialogOpen(true);
133
+ };
134
+
135
+ return (
136
+ <PageLayout
137
+ title="Integrations"
138
+ subtitle="Configure webhooks to send events to external systems"
139
+ loading={loading}
140
+ actions={
141
+ <Button onClick={openCreateDialog}>
142
+ <Plus className="h-4 w-4 mr-2" />
143
+ New Subscription
144
+ </Button>
145
+ }
146
+ >
147
+ <div className="space-y-8">
148
+ {/* Stats Overview */}
149
+ {stats && (
150
+ <section>
151
+ <SectionHeader
152
+ title="Delivery Activity (24h)"
153
+ icon={<Activity className="h-5 w-5" />}
154
+ />
155
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
156
+ <Card>
157
+ <CardContent className="p-4">
158
+ <div className="text-2xl font-bold">{stats.total}</div>
159
+ <div className="text-sm text-muted-foreground">
160
+ Total Deliveries
161
+ </div>
162
+ </CardContent>
163
+ </Card>
164
+ <Card>
165
+ <CardContent className="p-4">
166
+ <div className="text-2xl font-bold text-green-600">
167
+ {stats.successful}
168
+ </div>
169
+ <div className="text-sm text-muted-foreground">
170
+ Successful
171
+ </div>
172
+ </CardContent>
173
+ </Card>
174
+ <Card>
175
+ <CardContent className="p-4">
176
+ <div className="text-2xl font-bold text-red-600">
177
+ {stats.failed}
178
+ </div>
179
+ <div className="text-sm text-muted-foreground">Failed</div>
180
+ </CardContent>
181
+ </Card>
182
+ <Card>
183
+ <CardContent className="p-4">
184
+ <div className="text-2xl font-bold text-yellow-600">
185
+ {stats.retrying + stats.pending}
186
+ </div>
187
+ <div className="text-sm text-muted-foreground">
188
+ In Progress
189
+ </div>
190
+ </CardContent>
191
+ </Card>
192
+ </div>
193
+ </section>
194
+ )}
195
+
196
+ {/* Subscriptions List */}
197
+ <section>
198
+ <div className="flex items-center justify-between mb-4">
199
+ <SectionHeader
200
+ title="Webhook Subscriptions"
201
+ description="Subscriptions route events to external systems via providers"
202
+ icon={<Webhook className="h-5 w-5" />}
203
+ />
204
+ <Link
205
+ to={resolveRoute(integrationRoutes.routes.logs)}
206
+ className="text-sm text-primary hover:underline flex items-center gap-1"
207
+ >
208
+ View Delivery Logs
209
+ <ArrowRight className="h-4 w-4" />
210
+ </Link>
211
+ </div>
212
+
213
+ {subscriptions.length === 0 ? (
214
+ <EmptyState
215
+ icon={<Webhook className="h-12 w-12" />}
216
+ title="No webhook subscriptions"
217
+ description="Create a subscription to start routing events to external systems"
218
+ />
219
+ ) : (
220
+ <Card>
221
+ <Table>
222
+ <TableHeader>
223
+ <TableRow>
224
+ <TableHead>Subscription</TableHead>
225
+ <TableHead>Provider</TableHead>
226
+ <TableHead>Events</TableHead>
227
+ <TableHead>Status</TableHead>
228
+ <TableHead></TableHead>
229
+ </TableRow>
230
+ </TableHeader>
231
+ <TableBody>
232
+ {subscriptions.map((sub) => {
233
+ const provider = getProviderInfo(sub.providerId);
234
+ return (
235
+ <TableRow
236
+ key={sub.id}
237
+ className="cursor-pointer"
238
+ onClick={() => openEditDialog(sub)}
239
+ >
240
+ <TableCell>
241
+ <div className="flex items-center gap-3">
242
+ <div className="p-2 rounded-lg bg-muted">
243
+ <DynamicIcon
244
+ name={
245
+ (provider?.icon ??
246
+ "Webhook") as LucideIconName
247
+ }
248
+ className="h-5 w-5 text-muted-foreground"
249
+ />
250
+ </div>
251
+ <div>
252
+ <div className="font-medium">{sub.name}</div>
253
+ {sub.description && (
254
+ <div className="text-sm text-muted-foreground">
255
+ {sub.description}
256
+ </div>
257
+ )}
258
+ </div>
259
+ </div>
260
+ </TableCell>
261
+ <TableCell>
262
+ {provider?.displayName ?? sub.providerId}
263
+ </TableCell>
264
+ <TableCell>
265
+ <Badge variant="outline">{sub.eventId}</Badge>
266
+ </TableCell>
267
+ <TableCell>
268
+ <Badge
269
+ variant={sub.enabled ? "success" : "secondary"}
270
+ >
271
+ {sub.enabled ? "Active" : "Disabled"}
272
+ </Badge>
273
+ </TableCell>
274
+ <TableCell className="text-right">
275
+ <Button
276
+ variant="ghost"
277
+ size="sm"
278
+ onClick={(e) => {
279
+ e.stopPropagation();
280
+ void handleToggle(sub.id, !sub.enabled);
281
+ }}
282
+ >
283
+ {sub.enabled ? "Disable" : "Enable"}
284
+ </Button>
285
+ </TableCell>
286
+ </TableRow>
287
+ );
288
+ })}
289
+ </TableBody>
290
+ </Table>
291
+ </Card>
292
+ )}
293
+
294
+ {subscriptions.length === 0 && (
295
+ <div className="mt-4 flex justify-center">
296
+ <Button onClick={openCreateDialog}>
297
+ <Plus className="h-4 w-4 mr-2" />
298
+ Create Subscription
299
+ </Button>
300
+ </div>
301
+ )}
302
+ </section>
303
+
304
+ {/* Providers Overview */}
305
+ <section>
306
+ <SectionHeader
307
+ title="Available Providers"
308
+ description="Providers handle the delivery of events to external systems"
309
+ />
310
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
311
+ {providers.map((provider) => (
312
+ <Card key={provider.qualifiedId}>
313
+ <CardContent className="p-4">
314
+ <div className="flex items-center justify-between">
315
+ <div className="flex items-center gap-3">
316
+ <div className="p-2 rounded-lg bg-muted">
317
+ <DynamicIcon
318
+ name={(provider.icon ?? "Webhook") as LucideIconName}
319
+ className="h-6 w-6"
320
+ />
321
+ </div>
322
+ <div>
323
+ <div className="font-medium">
324
+ {provider.displayName}
325
+ </div>
326
+ {provider.description && (
327
+ <div className="text-sm text-muted-foreground">
328
+ {provider.description}
329
+ </div>
330
+ )}
331
+ </div>
332
+ </div>
333
+ {provider.hasConnectionSchema && (
334
+ <Link
335
+ to={resolveRoute(integrationRoutes.routes.connections, {
336
+ providerId: provider.qualifiedId,
337
+ })}
338
+ className="text-sm text-primary hover:underline"
339
+ >
340
+ Connections
341
+ </Link>
342
+ )}
343
+ </div>
344
+ </CardContent>
345
+ </Card>
346
+ ))}
347
+ {providers.length === 0 && (
348
+ <Card className="col-span-full">
349
+ <CardContent className="p-4">
350
+ <div className="text-center text-muted-foreground py-4">
351
+ No providers registered. Install provider plugins to enable
352
+ webhook delivery.
353
+ </div>
354
+ </CardContent>
355
+ </Card>
356
+ )}
357
+ </div>
358
+ </section>
359
+ </div>
360
+
361
+ <SubscriptionDialog
362
+ open={dialogOpen}
363
+ onOpenChange={(open) => {
364
+ setDialogOpen(open);
365
+ if (!open) setSelectedSubscription(undefined);
366
+ }}
367
+ providers={providers}
368
+ subscription={selectedSubscription}
369
+ onCreated={handleCreated}
370
+ onUpdated={handleUpdated}
371
+ onDeleted={handleDeleted}
372
+ />
373
+ </PageLayout>
374
+ );
375
+ };