@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,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
|
+
};
|