@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,137 @@
1
+ import { useState } from "react";
2
+ import {
3
+ Card,
4
+ Button,
5
+ Badge,
6
+ Table,
7
+ TableBody,
8
+ TableCell,
9
+ TableHead,
10
+ TableHeader,
11
+ TableRow,
12
+ } from "@checkstack/ui";
13
+ import { ChevronDown, ChevronUp, ExternalLink, FileJson } from "lucide-react";
14
+ import type { IntegrationProviderInfo } from "@checkstack/integration-common";
15
+
16
+ interface ProviderDocumentationProps {
17
+ provider: IntegrationProviderInfo;
18
+ }
19
+
20
+ /**
21
+ * Displays provider documentation in a collapsible section.
22
+ * Shows setup guide, example payload, headers, and external docs link.
23
+ */
24
+ export const ProviderDocumentation = ({
25
+ provider,
26
+ }: ProviderDocumentationProps) => {
27
+ const { documentation } = provider;
28
+ const [isExpanded, setIsExpanded] = useState(false);
29
+
30
+ // Don't render if no documentation defined
31
+ if (!documentation) {
32
+ return <></>;
33
+ }
34
+
35
+ // Check if there's any actual content to display
36
+ const hasContent =
37
+ documentation.setupGuide ??
38
+ documentation.examplePayload ??
39
+ documentation.headers?.length ??
40
+ documentation.externalDocsUrl;
41
+
42
+ if (!hasContent) {
43
+ return <></>;
44
+ }
45
+
46
+ return (
47
+ <div className="border rounded-md">
48
+ <button
49
+ type="button"
50
+ onClick={() => setIsExpanded(!isExpanded)}
51
+ className="w-full flex items-center justify-between p-3 hover:bg-muted/50 transition-colors"
52
+ >
53
+ <div className="flex items-center gap-2">
54
+ <FileJson className="h-4 w-4 text-muted-foreground" />
55
+ <span className="text-sm font-medium">Documentation</span>
56
+ <Badge variant="secondary" className="text-xs">
57
+ {isExpanded ? "Hide" : "Show"}
58
+ </Badge>
59
+ </div>
60
+ {isExpanded ? (
61
+ <ChevronUp className="h-4 w-4 text-muted-foreground" />
62
+ ) : (
63
+ <ChevronDown className="h-4 w-4 text-muted-foreground" />
64
+ )}
65
+ </button>
66
+
67
+ {isExpanded && (
68
+ <div className="px-3 pb-3 space-y-4">
69
+ {/* Setup Guide */}
70
+ {documentation.setupGuide && (
71
+ <div>
72
+ <h4 className="text-sm font-medium mb-2">Setup Guide</h4>
73
+ <div className="bg-muted/50 p-3 rounded-md text-sm whitespace-pre-wrap">
74
+ {documentation.setupGuide}
75
+ </div>
76
+ </div>
77
+ )}
78
+
79
+ {/* Example Payload */}
80
+ {documentation.examplePayload && (
81
+ <div>
82
+ <h4 className="text-sm font-medium mb-2">Example Payload</h4>
83
+ <pre className="bg-muted p-3 rounded-md text-xs overflow-x-auto">
84
+ <code>{documentation.examplePayload}</code>
85
+ </pre>
86
+ </div>
87
+ )}
88
+
89
+ {/* Headers */}
90
+ {documentation.headers && documentation.headers.length > 0 && (
91
+ <div>
92
+ <h4 className="text-sm font-medium mb-2">HTTP Headers</h4>
93
+ <Card>
94
+ <Table>
95
+ <TableHeader>
96
+ <TableRow>
97
+ <TableHead className="w-1/3">Header</TableHead>
98
+ <TableHead>Description</TableHead>
99
+ </TableRow>
100
+ </TableHeader>
101
+ <TableBody>
102
+ {documentation.headers.map((header) => (
103
+ <TableRow key={header.name}>
104
+ <TableCell className="font-mono text-sm">
105
+ {header.name}
106
+ </TableCell>
107
+ <TableCell className="text-sm text-muted-foreground">
108
+ {header.description}
109
+ </TableCell>
110
+ </TableRow>
111
+ ))}
112
+ </TableBody>
113
+ </Table>
114
+ </Card>
115
+ </div>
116
+ )}
117
+
118
+ {/* External Docs Link */}
119
+ {documentation.externalDocsUrl && (
120
+ <div>
121
+ <Button
122
+ variant="outline"
123
+ size="sm"
124
+ onClick={() =>
125
+ window.open(documentation.externalDocsUrl, "_blank")
126
+ }
127
+ >
128
+ <ExternalLink className="h-4 w-4 mr-2" />
129
+ View Full Documentation
130
+ </Button>
131
+ </div>
132
+ )}
133
+ </div>
134
+ )}
135
+ </div>
136
+ );
137
+ };
package/src/index.tsx ADDED
@@ -0,0 +1,60 @@
1
+ import {
2
+ createFrontendPlugin,
3
+ createSlotExtension,
4
+ UserMenuItemsSlot,
5
+ } from "@checkstack/frontend-api";
6
+ import {
7
+ integrationRoutes,
8
+ pluginMetadata,
9
+ permissions,
10
+ } from "@checkstack/integration-common";
11
+ import { IntegrationsPage } from "./pages/IntegrationsPage";
12
+ import { DeliveryLogsPage } from "./pages/DeliveryLogsPage";
13
+ import { ProviderConnectionsPage } from "./pages/ProviderConnectionsPage";
14
+ import { IntegrationMenuItem } from "./components/IntegrationMenuItem";
15
+
16
+ export const integrationPlugin = createFrontendPlugin({
17
+ metadata: pluginMetadata,
18
+ routes: [
19
+ {
20
+ route: integrationRoutes.routes.list,
21
+ element: <IntegrationsPage />,
22
+ permission: permissions.integrationManage,
23
+ },
24
+ {
25
+ route: integrationRoutes.routes.logs,
26
+ element: <DeliveryLogsPage />,
27
+ permission: permissions.integrationManage,
28
+ },
29
+ {
30
+ route: integrationRoutes.routes.deliveryLogs,
31
+ element: <DeliveryLogsPage />,
32
+ permission: permissions.integrationManage,
33
+ },
34
+ {
35
+ route: integrationRoutes.routes.connections,
36
+ element: <ProviderConnectionsPage />,
37
+ permission: permissions.integrationManage,
38
+ },
39
+ ],
40
+ extensions: [
41
+ createSlotExtension(UserMenuItemsSlot, {
42
+ id: "integration.user-menu.link",
43
+ component: IntegrationMenuItem,
44
+ }),
45
+ ],
46
+ });
47
+
48
+ export default integrationPlugin;
49
+
50
+ // Re-export registry and types for providers to register custom config components
51
+ export {
52
+ registerProviderConfigExtension,
53
+ getProviderConfigExtension,
54
+ hasProviderConfigExtension,
55
+ } from "./provider-config-registry";
56
+
57
+ export type {
58
+ ProviderConfigProps,
59
+ ProviderConfigExtension,
60
+ } from "./provider-config-registry";
@@ -0,0 +1,229 @@
1
+ import { useState } from "react";
2
+ import {
3
+ FileText,
4
+ RefreshCw,
5
+ CheckCircle,
6
+ XCircle,
7
+ Clock,
8
+ AlertCircle,
9
+ } from "lucide-react";
10
+ import {
11
+ PageLayout,
12
+ Card,
13
+ Button,
14
+ Badge,
15
+ SectionHeader,
16
+ Table,
17
+ TableBody,
18
+ TableCell,
19
+ TableHead,
20
+ TableHeader,
21
+ TableRow,
22
+ useToast,
23
+ usePagination,
24
+ BackLink,
25
+ } from "@checkstack/ui";
26
+ import { useApi, rpcApiRef } from "@checkstack/frontend-api";
27
+ import { resolveRoute } from "@checkstack/common";
28
+ import {
29
+ IntegrationApi,
30
+ integrationRoutes,
31
+ type DeliveryLog,
32
+ type DeliveryStatus,
33
+ } from "@checkstack/integration-common";
34
+
35
+ const statusConfig: Record<
36
+ DeliveryStatus,
37
+ {
38
+ icon: React.ReactNode;
39
+ variant: "success" | "destructive" | "warning" | "secondary";
40
+ }
41
+ > = {
42
+ success: {
43
+ icon: <CheckCircle className="h-4 w-4" />,
44
+ variant: "success",
45
+ },
46
+ failed: {
47
+ icon: <XCircle className="h-4 w-4" />,
48
+ variant: "destructive",
49
+ },
50
+ retrying: {
51
+ icon: <Clock className="h-4 w-4" />,
52
+ variant: "warning",
53
+ },
54
+ pending: {
55
+ icon: <AlertCircle className="h-4 w-4" />,
56
+ variant: "secondary",
57
+ },
58
+ };
59
+
60
+ export const DeliveryLogsPage = () => {
61
+ const rpcApi = useApi(rpcApiRef);
62
+ const client = rpcApi.forPlugin(IntegrationApi);
63
+ const toast = useToast();
64
+
65
+ const [retrying, setRetrying] = useState<string>();
66
+
67
+ const {
68
+ items: logs,
69
+ loading,
70
+ pagination,
71
+ } = usePagination({
72
+ fetchFn: async ({ limit, offset }) => {
73
+ const page = Math.floor(offset / limit) + 1;
74
+ return client.getDeliveryLogs({ page, pageSize: limit });
75
+ },
76
+ getItems: (response) => response.logs,
77
+ getTotal: (response) => response.total,
78
+ defaultLimit: 20,
79
+ });
80
+
81
+ const handleRetry = async (logId: string) => {
82
+ try {
83
+ setRetrying(logId);
84
+ const result = await client.retryDelivery({ logId });
85
+ if (result.success) {
86
+ toast.success("Delivery re-queued");
87
+ pagination.refetch();
88
+ } else {
89
+ toast.error(result.message ?? "Failed to retry delivery");
90
+ }
91
+ } catch (error) {
92
+ console.error("Failed to retry delivery:", error);
93
+ toast.error("Failed to retry delivery");
94
+ } finally {
95
+ setRetrying(undefined);
96
+ }
97
+ };
98
+
99
+ return (
100
+ <PageLayout
101
+ title="Delivery Logs"
102
+ subtitle="View and manage webhook delivery attempts"
103
+ loading={loading}
104
+ actions={
105
+ <BackLink to={resolveRoute(integrationRoutes.routes.list)}>
106
+ Back to Subscriptions
107
+ </BackLink>
108
+ }
109
+ >
110
+ <div className="space-y-6">
111
+ <section>
112
+ <SectionHeader
113
+ title="Recent Deliveries"
114
+ description="All webhook delivery attempts across subscriptions"
115
+ icon={<FileText className="h-5 w-5" />}
116
+ />
117
+
118
+ {logs.length === 0 && !loading ? (
119
+ <Card className="p-8">
120
+ <div className="text-center text-muted-foreground">
121
+ No delivery logs found
122
+ </div>
123
+ </Card>
124
+ ) : (
125
+ <Card>
126
+ <Table>
127
+ <TableHeader>
128
+ <TableRow>
129
+ <TableHead>Status</TableHead>
130
+ <TableHead>Subscription</TableHead>
131
+ <TableHead>Event</TableHead>
132
+ <TableHead>Attempts</TableHead>
133
+ <TableHead>Created</TableHead>
134
+ <TableHead>Error</TableHead>
135
+ <TableHead></TableHead>
136
+ </TableRow>
137
+ </TableHeader>
138
+ <TableBody>
139
+ {logs.map((log: DeliveryLog) => {
140
+ const config = statusConfig[log.status];
141
+ return (
142
+ <TableRow key={log.id}>
143
+ <TableCell>
144
+ <Badge
145
+ variant={config.variant}
146
+ className="flex items-center gap-1 w-fit"
147
+ >
148
+ {config.icon}
149
+ {log.status}
150
+ </Badge>
151
+ </TableCell>
152
+ <TableCell>
153
+ <div className="font-medium">
154
+ {log.subscriptionName ?? "Unknown"}
155
+ </div>
156
+ </TableCell>
157
+ <TableCell>
158
+ <div className="text-sm font-mono">
159
+ {log.eventType}
160
+ </div>
161
+ </TableCell>
162
+ <TableCell>{log.attempts}</TableCell>
163
+ <TableCell>
164
+ <div className="text-sm text-muted-foreground">
165
+ {new Date(log.createdAt).toLocaleString()}
166
+ </div>
167
+ </TableCell>
168
+ <TableCell>
169
+ {log.errorMessage ? (
170
+ <div
171
+ className="text-sm text-destructive max-w-[200px] truncate"
172
+ title={log.errorMessage}
173
+ >
174
+ {log.errorMessage}
175
+ </div>
176
+ ) : undefined}
177
+ </TableCell>
178
+ <TableCell>
179
+ {log.status === "failed" && (
180
+ <Button
181
+ variant="ghost"
182
+ size="sm"
183
+ onClick={() => void handleRetry(log.id)}
184
+ disabled={retrying === log.id}
185
+ >
186
+ <RefreshCw
187
+ className={`h-4 w-4 mr-1 ${
188
+ retrying === log.id ? "animate-spin" : ""
189
+ }`}
190
+ />
191
+ Retry
192
+ </Button>
193
+ )}
194
+ </TableCell>
195
+ </TableRow>
196
+ );
197
+ })}
198
+ </TableBody>
199
+ </Table>
200
+ {pagination.totalPages > 1 && (
201
+ <div className="p-4 border-t flex justify-center gap-2">
202
+ <Button
203
+ variant="outline"
204
+ size="sm"
205
+ disabled={!pagination.hasPrev}
206
+ onClick={pagination.prevPage}
207
+ >
208
+ Previous
209
+ </Button>
210
+ <span className="flex items-center text-sm text-muted-foreground">
211
+ Page {pagination.page} of {pagination.totalPages}
212
+ </span>
213
+ <Button
214
+ variant="outline"
215
+ size="sm"
216
+ disabled={!pagination.hasNext}
217
+ onClick={pagination.nextPage}
218
+ >
219
+ Next
220
+ </Button>
221
+ </div>
222
+ )}
223
+ </Card>
224
+ )}
225
+ </section>
226
+ </div>
227
+ </PageLayout>
228
+ );
229
+ };