@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.
- package/CHANGELOG.md +95 -0
- package/package.json +30 -0
- package/src/components/CreateSubscriptionDialog.tsx +649 -0
- package/src/components/IntegrationMenuItem.tsx +35 -0
- package/src/components/ProviderDocumentation.tsx +137 -0
- package/src/index.tsx +60 -0
- package/src/pages/DeliveryLogsPage.tsx +229 -0
- package/src/pages/IntegrationsPage.tsx +375 -0
- package/src/pages/ProviderConnectionsPage.tsx +470 -0
- package/src/provider-config-registry.ts +75 -0
- package/tsconfig.json +6 -0
|
@@ -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
|
+
};
|