@checkstack/integration-frontend 0.4.5 → 0.5.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,300 +0,0 @@
1
- import { useState } from "react";
2
- import { useNavigate } from "react-router-dom";
3
- import {
4
- FileText,
5
- RefreshCw,
6
- CheckCircle,
7
- XCircle,
8
- Clock,
9
- AlertCircle,
10
- } from "lucide-react";
11
- import {
12
- PageLayout,
13
- Card,
14
- Button,
15
- Badge,
16
- SectionHeader,
17
- Table,
18
- TableBody,
19
- TableCell,
20
- TableHead,
21
- TableHeader,
22
- TableRow,
23
- useToast,
24
- usePagination,
25
- usePaginationSync,
26
- BackLink,
27
- ResponsiveTable,
28
- MobileCardList,
29
- } from "@checkstack/ui";
30
- import { usePluginClient } from "@checkstack/frontend-api";
31
- import { resolveRoute } from "@checkstack/common";
32
- import {
33
- IntegrationApi,
34
- integrationRoutes,
35
- type DeliveryLog,
36
- type DeliveryStatus,
37
- } from "@checkstack/integration-common";
38
-
39
- const statusConfig: Record<
40
- DeliveryStatus,
41
- {
42
- icon: React.ReactNode;
43
- variant: "success" | "destructive" | "warning" | "secondary";
44
- }
45
- > = {
46
- success: {
47
- icon: <CheckCircle className="h-4 w-4" />,
48
- variant: "success",
49
- },
50
- failed: {
51
- icon: <XCircle className="h-4 w-4" />,
52
- variant: "destructive",
53
- },
54
- retrying: {
55
- icon: <Clock className="h-4 w-4" />,
56
- variant: "warning",
57
- },
58
- pending: {
59
- icon: <AlertCircle className="h-4 w-4" />,
60
- variant: "secondary",
61
- },
62
- };
63
-
64
- export const DeliveryLogsPage = () => {
65
- const integrationClient = usePluginClient(IntegrationApi);
66
- const toast = useToast();
67
- const navigate = useNavigate();
68
-
69
- const [retrying, setRetrying] = useState<string>();
70
-
71
- // Pagination state
72
- const pagination = usePagination({ defaultLimit: 20 });
73
-
74
- // Fetch data with useQuery
75
- const { data, isLoading, refetch } =
76
- integrationClient.getDeliveryLogs.useQuery({
77
- limit: pagination.limit,
78
- offset: pagination.offset,
79
- });
80
-
81
- // Sync total from response
82
- usePaginationSync(pagination, data?.total);
83
-
84
- const logs = data?.items ?? [];
85
-
86
- // Retry mutation
87
- const retryMutation = integrationClient.retryDelivery.useMutation({
88
- onSuccess: (result) => {
89
- if (result.success) {
90
- toast.success("Delivery re-queued");
91
- void refetch();
92
- } else {
93
- toast.error(result.message ?? "Failed to retry delivery");
94
- }
95
- },
96
- onError: () => {
97
- toast.error("Failed to retry delivery");
98
- },
99
- onSettled: () => {
100
- setRetrying(undefined);
101
- },
102
- });
103
-
104
- const handleRetry = (logId: string) => {
105
- setRetrying(logId);
106
- retryMutation.mutate({ logId });
107
- };
108
-
109
- return (
110
- <PageLayout
111
- title="Delivery Logs"
112
- subtitle="View and manage webhook delivery attempts"
113
- icon={FileText}
114
- loading={isLoading}
115
- actions={
116
- <BackLink onClick={() => navigate(resolveRoute(integrationRoutes.routes.list))}>
117
- Back to Subscriptions
118
- </BackLink>
119
- }
120
- >
121
- <div className="space-y-6">
122
- <section>
123
- <SectionHeader
124
- title="Recent Deliveries"
125
- description="All webhook delivery attempts across subscriptions"
126
- icon={<FileText className="h-5 w-5" />}
127
- />
128
-
129
- {logs.length === 0 && !isLoading ? (
130
- <Card className="p-8">
131
- <div className="text-center text-muted-foreground">
132
- No delivery logs found
133
- </div>
134
- </Card>
135
- ) : (
136
- <>
137
- <ResponsiveTable>
138
- <Card>
139
- <Table>
140
- <TableHeader>
141
- <TableRow>
142
- <TableHead>Status</TableHead>
143
- <TableHead>Subscription</TableHead>
144
- <TableHead>Event</TableHead>
145
- <TableHead>Attempts</TableHead>
146
- <TableHead>Created</TableHead>
147
- <TableHead>Error</TableHead>
148
- <TableHead></TableHead>
149
- </TableRow>
150
- </TableHeader>
151
- <TableBody>
152
- {logs.map((log: DeliveryLog) => {
153
- const config = statusConfig[log.status];
154
- return (
155
- <TableRow key={log.id}>
156
- <TableCell>
157
- <Badge
158
- variant={config.variant}
159
- className="flex items-center gap-1 w-fit"
160
- >
161
- {config.icon}
162
- {log.status}
163
- </Badge>
164
- </TableCell>
165
- <TableCell>
166
- <div className="font-medium">
167
- {log.subscriptionName ?? "Unknown"}
168
- </div>
169
- </TableCell>
170
- <TableCell>
171
- <div className="text-sm font-mono">
172
- {log.eventType}
173
- </div>
174
- </TableCell>
175
- <TableCell>{log.attempts}</TableCell>
176
- <TableCell>
177
- <div className="text-sm text-muted-foreground">
178
- {new Date(log.createdAt).toLocaleString()}
179
- </div>
180
- </TableCell>
181
- <TableCell>
182
- {log.errorMessage ? (
183
- <div
184
- className="text-sm text-destructive max-w-[200px] truncate"
185
- title={log.errorMessage}
186
- >
187
- {log.errorMessage}
188
- </div>
189
- ) : undefined}
190
- </TableCell>
191
- <TableCell>
192
- {log.status === "failed" && (
193
- <Button
194
- variant="ghost"
195
- size="sm"
196
- onClick={() => handleRetry(log.id)}
197
- disabled={retrying === log.id}
198
- >
199
- <RefreshCw
200
- className={`h-4 w-4 mr-1 ${
201
- retrying === log.id ? "animate-spin" : ""
202
- }`}
203
- />
204
- Retry
205
- </Button>
206
- )}
207
- </TableCell>
208
- </TableRow>
209
- );
210
- })}
211
- </TableBody>
212
- </Table>
213
- </Card>
214
- </ResponsiveTable>
215
-
216
- <MobileCardList>
217
- {logs.map((log: DeliveryLog) => {
218
- const config = statusConfig[log.status];
219
- return (
220
- <Card key={log.id} className="p-3">
221
- <div className="flex items-start justify-between gap-2">
222
- <span className="font-medium truncate">
223
- {log.subscriptionName ?? "Unknown"}
224
- </span>
225
- <Badge
226
- variant={config.variant}
227
- className="flex items-center gap-1 w-fit shrink-0"
228
- >
229
- {config.icon}
230
- {log.status}
231
- </Badge>
232
- </div>
233
- <div className="mt-1 text-xs text-muted-foreground font-mono break-all">
234
- {log.eventType}
235
- </div>
236
- <div className="mt-1 text-xs text-muted-foreground">
237
- {log.attempts} attempt
238
- {log.attempts === 1 ? "" : "s"} &middot;{" "}
239
- {new Date(log.createdAt).toLocaleString()}
240
- </div>
241
- {log.errorMessage && (
242
- <div
243
- className="mt-2 text-xs text-destructive line-clamp-2"
244
- title={log.errorMessage}
245
- >
246
- {log.errorMessage}
247
- </div>
248
- )}
249
- {log.status === "failed" && (
250
- <div className="mt-3 flex justify-end">
251
- <Button
252
- variant="ghost"
253
- size="sm"
254
- onClick={() => handleRetry(log.id)}
255
- disabled={retrying === log.id}
256
- >
257
- <RefreshCw
258
- className={`h-4 w-4 mr-1 ${
259
- retrying === log.id ? "animate-spin" : ""
260
- }`}
261
- />
262
- Retry
263
- </Button>
264
- </div>
265
- )}
266
- </Card>
267
- );
268
- })}
269
- </MobileCardList>
270
-
271
- {pagination.totalPages > 1 && (
272
- <div className="p-4 border-t flex justify-center gap-2">
273
- <Button
274
- variant="outline"
275
- size="sm"
276
- disabled={!pagination.hasPrev}
277
- onClick={pagination.prevPage}
278
- >
279
- Previous
280
- </Button>
281
- <span className="flex items-center text-sm text-muted-foreground">
282
- Page {pagination.page} of {pagination.totalPages}
283
- </span>
284
- <Button
285
- variant="outline"
286
- size="sm"
287
- disabled={!pagination.hasNext}
288
- onClick={pagination.nextPage}
289
- >
290
- Next
291
- </Button>
292
- </div>
293
- )}
294
- </>
295
- )}
296
- </section>
297
- </div>
298
- </PageLayout>
299
- );
300
- };
@@ -1,378 +0,0 @@
1
- import { useState, useEffect } from "react";
2
- import { Link, useSearchParams } from "react-router-dom";
3
- import {
4
- Plus,
5
- Webhook,
6
- ArrowRight,
7
- Activity,
8
- Link as LinkIcon,
9
- } from "lucide-react";
10
- import {
11
- PageLayout,
12
- Card,
13
- CardContent,
14
- Button,
15
- Badge,
16
- SectionHeader,
17
- DynamicIcon,
18
- EmptyState,
19
- Table,
20
- TableBody,
21
- TableCell,
22
- TableHead,
23
- TableHeader,
24
- TableRow,
25
- useToast,
26
- type LucideIconName,
27
- } from "@checkstack/ui";
28
- import { usePluginClient } from "@checkstack/frontend-api";
29
- import { resolveRoute, extractErrorMessage} from "@checkstack/common";
30
- import {
31
- IntegrationApi,
32
- integrationRoutes,
33
- pluginMetadata as integrationPluginMetadata,
34
- type WebhookSubscription,
35
- type IntegrationProviderInfo,
36
- } from "@checkstack/integration-common";
37
- import { Tip } from "@checkstack/tips-frontend";
38
- import { SubscriptionDialog } from "../components/CreateSubscriptionDialog";
39
-
40
- export const IntegrationsPage = () => {
41
- const client = usePluginClient(IntegrationApi);
42
- const toast = useToast();
43
- const [searchParams, setSearchParams] = useSearchParams();
44
-
45
- const [dialogOpen, setDialogOpen] = useState(false);
46
- const [selectedSubscription, setSelectedSubscription] =
47
- useState<WebhookSubscription>();
48
-
49
- // Queries using hooks
50
- const {
51
- data: subscriptionsData,
52
- isLoading: subsLoading,
53
- refetch: refetchSubs,
54
- } = client.listSubscriptions.useQuery({ limit: 100, offset: 0 });
55
-
56
- const { data: providers = [], isLoading: providersLoading } =
57
- client.listProviders.useQuery({});
58
-
59
- const { data: stats, isLoading: statsLoading } =
60
- client.getDeliveryStats.useQuery({ hours: 24 });
61
-
62
- // Mutation for toggling
63
- const toggleMutation = client.toggleSubscription.useMutation({
64
- onSuccess: (_result, variables) => {
65
- toast.success(
66
- variables.enabled ? "Subscription enabled" : "Subscription disabled",
67
- );
68
- void refetchSubs();
69
- },
70
- onError: (error) => {
71
- toast.error(
72
- extractErrorMessage(error, "Failed to toggle subscription"),
73
- );
74
- },
75
- });
76
-
77
- const subscriptions = subscriptionsData?.items ?? [];
78
- const loading = subsLoading || providersLoading || statsLoading;
79
-
80
- // Handle ?action=create URL parameter (from command palette)
81
- useEffect(() => {
82
- if (searchParams.get("action") === "create") {
83
- setSelectedSubscription(undefined);
84
- setDialogOpen(true);
85
- // Clear the URL param after opening
86
- searchParams.delete("action");
87
- setSearchParams(searchParams, { replace: true });
88
- }
89
- }, [searchParams, setSearchParams]);
90
-
91
- const getProviderInfo = (
92
- providerId: string,
93
- ): IntegrationProviderInfo | undefined => {
94
- return providers.find((p) => p.qualifiedId === providerId);
95
- };
96
-
97
- const handleToggle = (id: string, enabled: boolean) => {
98
- toggleMutation.mutate({ id, enabled });
99
- };
100
-
101
- const handleDialogClose = () => {
102
- void refetchSubs();
103
- setDialogOpen(false);
104
- setSelectedSubscription(undefined);
105
- };
106
-
107
- const openEditDialog = (sub: WebhookSubscription) => {
108
- setSelectedSubscription(sub);
109
- setDialogOpen(true);
110
- };
111
-
112
- const openCreateDialog = () => {
113
- setSelectedSubscription(undefined);
114
- setDialogOpen(true);
115
- };
116
-
117
- return (
118
- <PageLayout
119
- title="Integrations"
120
- subtitle="Configure webhooks to send events to external systems"
121
- icon={LinkIcon}
122
- loading={loading}
123
- actions={
124
- <Tip
125
- plugin={integrationPluginMetadata}
126
- id="subscriptions.create"
127
- title="Send Checkstack events anywhere"
128
- description="A subscription forwards events (incidents, status changes, deployments) to an external system. Pick a connected provider — webhook, Jira, Teams, Slack — and choose which event types it cares about. Failed deliveries retry automatically and show up in the delivery logs."
129
- side="bottom"
130
- align="end"
131
- >
132
- <Button onClick={openCreateDialog}>
133
- <Plus className="h-4 w-4 mr-2" />
134
- New Subscription
135
- </Button>
136
- </Tip>
137
- }
138
- >
139
- <div className="space-y-8">
140
- {/* Stats Overview */}
141
- {stats && (
142
- <section>
143
- <SectionHeader
144
- title="Delivery Activity (24h)"
145
- icon={<Activity className="h-5 w-5" />}
146
- />
147
- <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
148
- <Card>
149
- <CardContent className="p-4">
150
- <div className="text-2xl font-bold">{stats.total}</div>
151
- <div className="text-sm text-muted-foreground">
152
- Total Deliveries
153
- </div>
154
- </CardContent>
155
- </Card>
156
- <Card>
157
- <CardContent className="p-4">
158
- <div className="text-2xl font-bold text-green-600">
159
- {stats.successful}
160
- </div>
161
- <div className="text-sm text-muted-foreground">
162
- Successful
163
- </div>
164
- </CardContent>
165
- </Card>
166
- <Card>
167
- <CardContent className="p-4">
168
- <div className="text-2xl font-bold text-red-600">
169
- {stats.failed}
170
- </div>
171
- <div className="text-sm text-muted-foreground">Failed</div>
172
- </CardContent>
173
- </Card>
174
- <Card>
175
- <CardContent className="p-4">
176
- <div className="text-2xl font-bold text-yellow-600">
177
- {stats.retrying + stats.pending}
178
- </div>
179
- <div className="text-sm text-muted-foreground">
180
- In Progress
181
- </div>
182
- </CardContent>
183
- </Card>
184
- </div>
185
- </section>
186
- )}
187
-
188
- {/* Subscriptions List */}
189
- <section>
190
- <div className="flex items-center justify-between mb-4">
191
- <SectionHeader
192
- title="Webhook Subscriptions"
193
- description="Subscriptions route events to external systems via providers"
194
- icon={<Webhook className="h-5 w-5" />}
195
- />
196
- <Link
197
- to={resolveRoute(integrationRoutes.routes.logs)}
198
- className="text-sm text-primary hover:underline flex items-center gap-1"
199
- >
200
- View Delivery Logs
201
- <ArrowRight className="h-4 w-4" />
202
- </Link>
203
- </div>
204
-
205
- {subscriptions.length === 0 ? (
206
- <EmptyState
207
- icon={<Webhook className="h-12 w-12" />}
208
- title="No webhook subscriptions yet"
209
- description="A subscription forwards events from Checkstack (incidents, status changes, deployments) to external systems — Jira, ServiceNow, your own webhook receiver, anything that speaks HTTP."
210
- steps={[
211
- "Connect at least one provider on the Provider Connections page (Jira, Webhook, Teams, …).",
212
- "Create a subscription that picks one or more event types to forward.",
213
- "Watch the Delivery Logs to confirm payloads are reaching their destination.",
214
- ]}
215
- actions={
216
- <Button onClick={openCreateDialog}>
217
- <Plus className="h-4 w-4 mr-2" />
218
- New subscription
219
- </Button>
220
- }
221
- />
222
- ) : (
223
- <Card>
224
- <Table>
225
- <TableHeader>
226
- <TableRow>
227
- <TableHead>Subscription</TableHead>
228
- <TableHead>Provider</TableHead>
229
- <TableHead>Events</TableHead>
230
- <TableHead>Status</TableHead>
231
- <TableHead></TableHead>
232
- </TableRow>
233
- </TableHeader>
234
- <TableBody>
235
- {subscriptions.map((sub) => {
236
- const provider = getProviderInfo(sub.providerId);
237
- return (
238
- <TableRow
239
- key={sub.id}
240
- className="cursor-pointer"
241
- onClick={() => openEditDialog(sub)}
242
- >
243
- <TableCell>
244
- <div className="flex items-center gap-3">
245
- <div className="p-2 rounded-lg bg-muted">
246
- <DynamicIcon
247
- name={
248
- (provider?.icon as LucideIconName | undefined) ??
249
- "Webhook"
250
- }
251
- className="h-5 w-5 text-muted-foreground"
252
- />
253
- </div>
254
- <div>
255
- <div className="font-medium">{sub.name}</div>
256
- {sub.description && (
257
- <div className="text-sm text-muted-foreground">
258
- {sub.description}
259
- </div>
260
- )}
261
- </div>
262
- </div>
263
- </TableCell>
264
- <TableCell>
265
- {provider?.displayName ?? sub.providerId}
266
- </TableCell>
267
- <TableCell>
268
- <Badge variant="outline">{sub.eventId}</Badge>
269
- </TableCell>
270
- <TableCell>
271
- <Badge
272
- variant={sub.enabled ? "success" : "secondary"}
273
- >
274
- {sub.enabled ? "Active" : "Disabled"}
275
- </Badge>
276
- </TableCell>
277
- <TableCell className="text-right">
278
- <Button
279
- variant="ghost"
280
- size="sm"
281
- onClick={(e) => {
282
- e.stopPropagation();
283
- handleToggle(sub.id, !sub.enabled);
284
- }}
285
- >
286
- {sub.enabled ? "Disable" : "Enable"}
287
- </Button>
288
- </TableCell>
289
- </TableRow>
290
- );
291
- })}
292
- </TableBody>
293
- </Table>
294
- </Card>
295
- )}
296
-
297
- {subscriptions.length === 0 && (
298
- <div className="mt-4 flex justify-center">
299
- <Button onClick={openCreateDialog}>
300
- <Plus className="h-4 w-4 mr-2" />
301
- Create Subscription
302
- </Button>
303
- </div>
304
- )}
305
- </section>
306
-
307
- {/* Providers Overview */}
308
- <section>
309
- <SectionHeader
310
- title="Available Providers"
311
- description="Providers handle the delivery of events to external systems"
312
- />
313
- <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
314
- {providers.map((provider) => (
315
- <Card key={provider.qualifiedId}>
316
- <CardContent className="p-4">
317
- <div className="flex items-center justify-between">
318
- <div className="flex items-center gap-3">
319
- <div className="p-2 rounded-lg bg-muted">
320
- <DynamicIcon
321
- name={(provider.icon as LucideIconName | undefined) ?? "Webhook"}
322
- className="h-6 w-6"
323
- />
324
- </div>
325
- <div>
326
- <div className="font-medium">
327
- {provider.displayName}
328
- </div>
329
- {provider.description && (
330
- <div className="text-sm text-muted-foreground">
331
- {provider.description}
332
- </div>
333
- )}
334
- </div>
335
- </div>
336
- {provider.hasConnectionSchema && (
337
- <Link
338
- to={resolveRoute(integrationRoutes.routes.connections, {
339
- providerId: provider.qualifiedId,
340
- })}
341
- className="text-sm text-primary hover:underline"
342
- >
343
- Connections
344
- </Link>
345
- )}
346
- </div>
347
- </CardContent>
348
- </Card>
349
- ))}
350
- {providers.length === 0 && (
351
- <Card className="col-span-full">
352
- <CardContent className="p-4">
353
- <div className="text-center text-muted-foreground py-4">
354
- No providers registered. Install provider plugins to enable
355
- webhook delivery.
356
- </div>
357
- </CardContent>
358
- </Card>
359
- )}
360
- </div>
361
- </section>
362
- </div>
363
-
364
- <SubscriptionDialog
365
- open={dialogOpen}
366
- onOpenChange={(open) => {
367
- setDialogOpen(open);
368
- if (!open) setSelectedSubscription(undefined);
369
- }}
370
- providers={providers}
371
- subscription={selectedSubscription}
372
- onCreated={handleDialogClose}
373
- onUpdated={handleDialogClose}
374
- onDeleted={handleDialogClose}
375
- />
376
- </PageLayout>
377
- );
378
- };