@checkstack/integration-frontend 0.1.0 → 0.2.1
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 +71 -0
- package/package.json +1 -1
- package/src/components/CreateSubscriptionDialog.tsx +110 -144
- package/src/pages/DeliveryLogsPage.tsx +35 -27
- package/src/pages/IntegrationsPage.tsx +45 -57
- package/src/pages/ProviderConnectionsPage.tsx +105 -106
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,76 @@
|
|
|
1
1
|
# @checkstack/integration-frontend
|
|
2
2
|
|
|
3
|
+
## 0.2.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [4eed42d]
|
|
8
|
+
- @checkstack/frontend-api@0.3.0
|
|
9
|
+
- @checkstack/ui@0.2.2
|
|
10
|
+
|
|
11
|
+
## 0.2.0
|
|
12
|
+
|
|
13
|
+
### Minor Changes
|
|
14
|
+
|
|
15
|
+
- 7a23261: ## TanStack Query Integration
|
|
16
|
+
|
|
17
|
+
Migrated all frontend components to use `usePluginClient` hook with TanStack Query integration, replacing the legacy `forPlugin()` pattern.
|
|
18
|
+
|
|
19
|
+
### New Features
|
|
20
|
+
|
|
21
|
+
- **`usePluginClient` hook**: Provides type-safe access to plugin APIs with `.useQuery()` and `.useMutation()` methods
|
|
22
|
+
- **Automatic request deduplication**: Multiple components requesting the same data share a single network request
|
|
23
|
+
- **Built-in caching**: Configurable stale time and cache duration per query
|
|
24
|
+
- **Loading/error states**: TanStack Query provides `isLoading`, `error`, `isRefetching` states automatically
|
|
25
|
+
- **Background refetching**: Stale data is automatically refreshed when components mount
|
|
26
|
+
|
|
27
|
+
### Contract Changes
|
|
28
|
+
|
|
29
|
+
All RPC contracts now require `operationType: "query"` or `operationType: "mutation"` metadata:
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
const getItems = proc()
|
|
33
|
+
.meta({ operationType: "query", access: [access.read] })
|
|
34
|
+
.output(z.array(itemSchema))
|
|
35
|
+
.query();
|
|
36
|
+
|
|
37
|
+
const createItem = proc()
|
|
38
|
+
.meta({ operationType: "mutation", access: [access.manage] })
|
|
39
|
+
.input(createItemSchema)
|
|
40
|
+
.output(itemSchema)
|
|
41
|
+
.mutation();
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Migration
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
// Before (forPlugin pattern)
|
|
48
|
+
const api = useApi(myPluginApiRef);
|
|
49
|
+
const [items, setItems] = useState<Item[]>([]);
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
api.getItems().then(setItems);
|
|
52
|
+
}, [api]);
|
|
53
|
+
|
|
54
|
+
// After (usePluginClient pattern)
|
|
55
|
+
const client = usePluginClient(MyPluginApi);
|
|
56
|
+
const { data: items, isLoading } = client.getItems.useQuery({});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Bug Fixes
|
|
60
|
+
|
|
61
|
+
- Fixed `rpc.test.ts` test setup for middleware type inference
|
|
62
|
+
- Fixed `SearchDialog` to use `setQuery` instead of deprecated `search` method
|
|
63
|
+
- Fixed null→undefined warnings in notification and queue frontends
|
|
64
|
+
|
|
65
|
+
### Patch Changes
|
|
66
|
+
|
|
67
|
+
- Updated dependencies [7a23261]
|
|
68
|
+
- @checkstack/frontend-api@0.2.0
|
|
69
|
+
- @checkstack/common@0.3.0
|
|
70
|
+
- @checkstack/integration-common@0.2.0
|
|
71
|
+
- @checkstack/ui@0.2.1
|
|
72
|
+
- @checkstack/signal-frontend@0.0.7
|
|
73
|
+
|
|
3
74
|
## 0.1.0
|
|
4
75
|
|
|
5
76
|
### Minor Changes
|
package/package.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useEffect,
|
|
1
|
+
import { useState, useEffect, useMemo } from "react";
|
|
2
2
|
import { Link } from "react-router-dom";
|
|
3
3
|
import { Trash2, ScrollText } from "lucide-react";
|
|
4
4
|
import {
|
|
@@ -23,15 +23,13 @@ import {
|
|
|
23
23
|
ConfirmationModal,
|
|
24
24
|
type LucideIconName,
|
|
25
25
|
} from "@checkstack/ui";
|
|
26
|
-
import {
|
|
26
|
+
import { usePluginClient } from "@checkstack/frontend-api";
|
|
27
27
|
import { resolveRoute } from "@checkstack/common";
|
|
28
28
|
import {
|
|
29
29
|
IntegrationApi,
|
|
30
30
|
integrationRoutes,
|
|
31
31
|
type WebhookSubscription,
|
|
32
32
|
type IntegrationProviderInfo,
|
|
33
|
-
type IntegrationEventInfo,
|
|
34
|
-
type ProviderConnectionRedacted,
|
|
35
33
|
type PayloadProperty,
|
|
36
34
|
} from "@checkstack/integration-common";
|
|
37
35
|
import { ProviderDocumentation } from "./ProviderDocumentation";
|
|
@@ -60,8 +58,7 @@ export const SubscriptionDialog = ({
|
|
|
60
58
|
onUpdated,
|
|
61
59
|
onDeleted,
|
|
62
60
|
}: SubscriptionDialogProps) => {
|
|
63
|
-
const
|
|
64
|
-
const client = rpcApi.forPlugin(IntegrationApi);
|
|
61
|
+
const client = usePluginClient(IntegrationApi);
|
|
65
62
|
const toast = useToast();
|
|
66
63
|
|
|
67
64
|
// Edit mode detection
|
|
@@ -70,16 +67,11 @@ export const SubscriptionDialog = ({
|
|
|
70
67
|
const [step, setStep] = useState<"provider" | "config">("provider");
|
|
71
68
|
const [selectedProvider, setSelectedProvider] =
|
|
72
69
|
useState<IntegrationProviderInfo>();
|
|
73
|
-
const [events, setEvents] = useState<IntegrationEventInfo[]>([]);
|
|
74
70
|
const [saving, setSaving] = useState(false);
|
|
75
71
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
|
76
72
|
|
|
77
73
|
// Connection state for providers with connectionSchema
|
|
78
|
-
const [connections, setConnections] = useState<ProviderConnectionRedacted[]>(
|
|
79
|
-
[]
|
|
80
|
-
);
|
|
81
74
|
const [selectedConnectionId, setSelectedConnectionId] = useState<string>("");
|
|
82
|
-
const [loadingConnections, setLoadingConnections] = useState(false);
|
|
83
75
|
|
|
84
76
|
// Form state
|
|
85
77
|
const [name, setName] = useState("");
|
|
@@ -88,69 +80,78 @@ export const SubscriptionDialog = ({
|
|
|
88
80
|
{}
|
|
89
81
|
);
|
|
90
82
|
const [selectedEventId, setSelectedEventId] = useState<string>("");
|
|
91
|
-
const [payloadProperties, setPayloadProperties] = useState<PayloadProperty[]>(
|
|
92
|
-
[]
|
|
93
|
-
);
|
|
94
83
|
// Track whether DynamicForm fields are valid (all required fields filled)
|
|
95
84
|
const [providerConfigValid, setProviderConfigValid] = useState(false);
|
|
96
85
|
|
|
97
|
-
//
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
setEvents(result);
|
|
102
|
-
} catch (error) {
|
|
103
|
-
console.error("Failed to fetch events:", error);
|
|
104
|
-
}
|
|
105
|
-
}, [client]);
|
|
106
|
-
|
|
107
|
-
// Fetch connections for providers with connectionSchema
|
|
108
|
-
const fetchConnections = useCallback(
|
|
109
|
-
async (providerId: string) => {
|
|
110
|
-
setLoadingConnections(true);
|
|
111
|
-
try {
|
|
112
|
-
const result = await client.listConnections({ providerId });
|
|
113
|
-
setConnections(result);
|
|
114
|
-
// Auto-select if only one connection
|
|
115
|
-
if (result.length === 1) {
|
|
116
|
-
setSelectedConnectionId(result[0].id);
|
|
117
|
-
}
|
|
118
|
-
} catch (error) {
|
|
119
|
-
console.error("Failed to fetch connections:", error);
|
|
120
|
-
} finally {
|
|
121
|
-
setLoadingConnections(false);
|
|
122
|
-
}
|
|
123
|
-
},
|
|
124
|
-
[client]
|
|
86
|
+
// Queries using hooks
|
|
87
|
+
const { data: events = [] } = client.listEventTypes.useQuery(
|
|
88
|
+
{},
|
|
89
|
+
{ enabled: open }
|
|
125
90
|
);
|
|
126
91
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
92
|
+
const { data: connections = [], isLoading: loadingConnections } =
|
|
93
|
+
client.listConnections.useQuery(
|
|
94
|
+
{ providerId: selectedProvider?.qualifiedId ?? "" },
|
|
95
|
+
{ enabled: open && !!selectedProvider?.hasConnectionSchema }
|
|
96
|
+
);
|
|
132
97
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
98
|
+
const { data: payloadSchemaData } = client.getEventPayloadSchema.useQuery(
|
|
99
|
+
{ eventId: selectedEventId },
|
|
100
|
+
{ enabled: open && !!selectedEventId }
|
|
101
|
+
);
|
|
139
102
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
103
|
+
const payloadProperties: PayloadProperty[] =
|
|
104
|
+
payloadSchemaData?.availableProperties ?? [];
|
|
105
|
+
|
|
106
|
+
// Mutations
|
|
107
|
+
const createMutation = client.createSubscription.useMutation({
|
|
108
|
+
onSuccess: (result) => {
|
|
109
|
+
onCreated?.(result);
|
|
110
|
+
toast.success("Subscription created");
|
|
111
|
+
setSaving(false);
|
|
112
|
+
},
|
|
113
|
+
onError: (error) => {
|
|
114
|
+
toast.error(
|
|
115
|
+
error instanceof Error ? error.message : "Failed to create subscription"
|
|
116
|
+
);
|
|
117
|
+
setSaving(false);
|
|
118
|
+
},
|
|
119
|
+
});
|
|
151
120
|
|
|
152
|
-
|
|
153
|
-
|
|
121
|
+
const updateMutation = client.updateSubscription.useMutation({
|
|
122
|
+
onSuccess: () => {
|
|
123
|
+
toast.success("Subscription updated");
|
|
124
|
+
onUpdated?.(subscription!);
|
|
125
|
+
onOpenChange(false);
|
|
126
|
+
setSaving(false);
|
|
127
|
+
},
|
|
128
|
+
onError: (error) => {
|
|
129
|
+
toast.error(
|
|
130
|
+
error instanceof Error ? error.message : "Failed to update subscription"
|
|
131
|
+
);
|
|
132
|
+
setSaving(false);
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const deleteMutation = client.deleteSubscription.useMutation({
|
|
137
|
+
onSuccess: () => {
|
|
138
|
+
toast.success("Subscription deleted");
|
|
139
|
+
onDeleted?.(subscription!.id);
|
|
140
|
+
onOpenChange(false);
|
|
141
|
+
},
|
|
142
|
+
onError: (error) => {
|
|
143
|
+
toast.error(
|
|
144
|
+
error instanceof Error ? error.message : "Failed to delete subscription"
|
|
145
|
+
);
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// Auto-select if only one connection
|
|
150
|
+
useEffect(() => {
|
|
151
|
+
if (connections.length === 1 && !selectedConnectionId) {
|
|
152
|
+
setSelectedConnectionId(connections[0].id);
|
|
153
|
+
}
|
|
154
|
+
}, [connections, selectedConnectionId]);
|
|
154
155
|
|
|
155
156
|
// Pre-populate form in edit mode
|
|
156
157
|
useEffect(() => {
|
|
@@ -162,9 +163,6 @@ export const SubscriptionDialog = ({
|
|
|
162
163
|
if (provider) {
|
|
163
164
|
setSelectedProvider(provider);
|
|
164
165
|
setStep("config"); // Skip provider selection
|
|
165
|
-
if (provider.hasConnectionSchema) {
|
|
166
|
-
void fetchConnections(provider.qualifiedId);
|
|
167
|
-
}
|
|
168
166
|
}
|
|
169
167
|
// Populate form fields
|
|
170
168
|
setName(subscription.name);
|
|
@@ -177,7 +175,7 @@ export const SubscriptionDialog = ({
|
|
|
177
175
|
setSelectedConnectionId(connId);
|
|
178
176
|
}
|
|
179
177
|
}
|
|
180
|
-
}, [open, subscription, providers
|
|
178
|
+
}, [open, subscription, providers]);
|
|
181
179
|
|
|
182
180
|
// Reset when dialog closes (only in create mode)
|
|
183
181
|
useEffect(() => {
|
|
@@ -188,8 +186,6 @@ export const SubscriptionDialog = ({
|
|
|
188
186
|
setDescription("");
|
|
189
187
|
setProviderConfig({});
|
|
190
188
|
setSelectedEventId("");
|
|
191
|
-
setPayloadProperties([]);
|
|
192
|
-
setConnections([]);
|
|
193
189
|
setSelectedConnectionId("");
|
|
194
190
|
setDeleteDialogOpen(false);
|
|
195
191
|
setProviderConfigValid(false);
|
|
@@ -217,62 +213,39 @@ export const SubscriptionDialog = ({
|
|
|
217
213
|
const handleProviderSelect = (provider: IntegrationProviderInfo) => {
|
|
218
214
|
setSelectedProvider(provider);
|
|
219
215
|
setStep("config");
|
|
220
|
-
// Fetch connections if provider supports them
|
|
221
|
-
if (provider.hasConnectionSchema) {
|
|
222
|
-
void fetchConnections(provider.qualifiedId);
|
|
223
|
-
}
|
|
224
216
|
};
|
|
225
217
|
|
|
226
218
|
// Handle update (edit mode)
|
|
227
|
-
const handleSave =
|
|
219
|
+
const handleSave = () => {
|
|
228
220
|
if (!subscription || !selectedProvider) return;
|
|
229
221
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
eventId
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
});
|
|
249
|
-
toast.success("Subscription updated");
|
|
250
|
-
onUpdated?.(subscription);
|
|
251
|
-
onOpenChange(false);
|
|
252
|
-
} catch (error) {
|
|
253
|
-
console.error("Failed to update subscription:", error);
|
|
254
|
-
toast.error("Failed to update subscription");
|
|
255
|
-
} finally {
|
|
256
|
-
setSaving(false);
|
|
257
|
-
}
|
|
222
|
+
setSaving(true);
|
|
223
|
+
// Include connectionId in providerConfig for providers with connections
|
|
224
|
+
const configWithConnection = selectedProvider.hasConnectionSchema
|
|
225
|
+
? { ...providerConfig, connectionId: selectedConnectionId }
|
|
226
|
+
: providerConfig;
|
|
227
|
+
|
|
228
|
+
updateMutation.mutate({
|
|
229
|
+
id: subscription.id,
|
|
230
|
+
updates: {
|
|
231
|
+
name,
|
|
232
|
+
description: description || undefined,
|
|
233
|
+
providerConfig: configWithConnection,
|
|
234
|
+
eventId:
|
|
235
|
+
selectedEventId === subscription.eventId
|
|
236
|
+
? undefined
|
|
237
|
+
: selectedEventId,
|
|
238
|
+
},
|
|
239
|
+
});
|
|
258
240
|
};
|
|
259
241
|
|
|
260
242
|
// Handle delete
|
|
261
|
-
const handleDelete =
|
|
243
|
+
const handleDelete = () => {
|
|
262
244
|
if (!subscription) return;
|
|
263
|
-
|
|
264
|
-
try {
|
|
265
|
-
await client.deleteSubscription({ id: subscription.id });
|
|
266
|
-
toast.success("Subscription deleted");
|
|
267
|
-
onDeleted?.(subscription.id);
|
|
268
|
-
onOpenChange(false);
|
|
269
|
-
} catch (error) {
|
|
270
|
-
console.error("Failed to delete subscription:", error);
|
|
271
|
-
toast.error("Failed to delete subscription");
|
|
272
|
-
}
|
|
245
|
+
deleteMutation.mutate({ id: subscription.id });
|
|
273
246
|
};
|
|
274
247
|
|
|
275
|
-
const handleCreate =
|
|
248
|
+
const handleCreate = () => {
|
|
276
249
|
if (!selectedProvider) return;
|
|
277
250
|
|
|
278
251
|
// For providers with connections, require a connection to be selected
|
|
@@ -281,32 +254,24 @@ export const SubscriptionDialog = ({
|
|
|
281
254
|
return;
|
|
282
255
|
}
|
|
283
256
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
});
|
|
298
|
-
onCreated?.(result);
|
|
299
|
-
toast.success("Subscription created");
|
|
300
|
-
} catch (error) {
|
|
301
|
-
console.error("Failed to create subscription:", error);
|
|
302
|
-
toast.error(
|
|
303
|
-
error instanceof Error ? error.message : "Failed to create subscription"
|
|
304
|
-
);
|
|
305
|
-
} finally {
|
|
306
|
-
setSaving(false);
|
|
307
|
-
}
|
|
257
|
+
setSaving(true);
|
|
258
|
+
// Include connectionId in providerConfig for providers with connections
|
|
259
|
+
const configWithConnection = selectedProvider.hasConnectionSchema
|
|
260
|
+
? { ...providerConfig, connectionId: selectedConnectionId }
|
|
261
|
+
: providerConfig;
|
|
262
|
+
|
|
263
|
+
createMutation.mutate({
|
|
264
|
+
name,
|
|
265
|
+
description: description || undefined,
|
|
266
|
+
providerId: selectedProvider.qualifiedId,
|
|
267
|
+
providerConfig: configWithConnection,
|
|
268
|
+
eventId: selectedEventId,
|
|
269
|
+
});
|
|
308
270
|
};
|
|
309
271
|
|
|
272
|
+
// Mutation for fetching dynamic dropdown options (called at component level)
|
|
273
|
+
const getOptionsMutation = client.getConnectionOptions.useMutation();
|
|
274
|
+
|
|
310
275
|
// Create optionsResolvers for dynamic dropdown fields (x-options-resolver)
|
|
311
276
|
// Uses a Proxy to handle any resolver name dynamically
|
|
312
277
|
const optionsResolvers = useMemo(() => {
|
|
@@ -319,10 +284,10 @@ export const SubscriptionDialog = ({
|
|
|
319
284
|
{},
|
|
320
285
|
{
|
|
321
286
|
get: (_target, resolverName: string) => {
|
|
322
|
-
// Return a resolver function
|
|
287
|
+
// Return a resolver function that uses mutateAsync from the hook defined above
|
|
323
288
|
return async (formValues: Record<string, unknown>) => {
|
|
324
289
|
try {
|
|
325
|
-
const result = await
|
|
290
|
+
const result = await getOptionsMutation.mutateAsync({
|
|
326
291
|
providerId: selectedProvider.qualifiedId,
|
|
327
292
|
connectionId: selectedConnectionId,
|
|
328
293
|
resolverName,
|
|
@@ -349,7 +314,8 @@ export const SubscriptionDialog = ({
|
|
|
349
314
|
formValues: Record<string, unknown>
|
|
350
315
|
) => Promise<{ value: string; label: string }[]>
|
|
351
316
|
>;
|
|
352
|
-
|
|
317
|
+
// Note: getOptionsMutation intentionally omitted - mutation objects change on every render
|
|
318
|
+
}, [selectedProvider, selectedConnectionId]);
|
|
353
319
|
|
|
354
320
|
return (
|
|
355
321
|
<>
|
|
@@ -21,9 +21,10 @@ import {
|
|
|
21
21
|
TableRow,
|
|
22
22
|
useToast,
|
|
23
23
|
usePagination,
|
|
24
|
+
usePaginationSync,
|
|
24
25
|
BackLink,
|
|
25
26
|
} from "@checkstack/ui";
|
|
26
|
-
import {
|
|
27
|
+
import { usePluginClient } from "@checkstack/frontend-api";
|
|
27
28
|
import { resolveRoute } from "@checkstack/common";
|
|
28
29
|
import {
|
|
29
30
|
IntegrationApi,
|
|
@@ -58,49 +59,56 @@ const statusConfig: Record<
|
|
|
58
59
|
};
|
|
59
60
|
|
|
60
61
|
export const DeliveryLogsPage = () => {
|
|
61
|
-
const
|
|
62
|
-
const client = rpcApi.forPlugin(IntegrationApi);
|
|
62
|
+
const integrationClient = usePluginClient(IntegrationApi);
|
|
63
63
|
const toast = useToast();
|
|
64
64
|
|
|
65
65
|
const [retrying, setRetrying] = useState<string>();
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
67
|
+
// Pagination state
|
|
68
|
+
const pagination = usePagination({ defaultLimit: 20 });
|
|
69
|
+
|
|
70
|
+
// Fetch data with useQuery
|
|
71
|
+
const page = Math.floor(pagination.offset / pagination.limit) + 1;
|
|
72
|
+
const { data, isLoading, refetch } =
|
|
73
|
+
integrationClient.getDeliveryLogs.useQuery({
|
|
74
|
+
page,
|
|
75
|
+
pageSize: pagination.limit,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Sync total from response
|
|
79
|
+
usePaginationSync(pagination, data?.total);
|
|
80
|
+
|
|
81
|
+
const logs = data?.logs ?? [];
|
|
80
82
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const result = await client.retryDelivery({ logId });
|
|
83
|
+
// Retry mutation
|
|
84
|
+
const retryMutation = integrationClient.retryDelivery.useMutation({
|
|
85
|
+
onSuccess: (result) => {
|
|
85
86
|
if (result.success) {
|
|
86
87
|
toast.success("Delivery re-queued");
|
|
87
|
-
|
|
88
|
+
void refetch();
|
|
88
89
|
} else {
|
|
89
90
|
toast.error(result.message ?? "Failed to retry delivery");
|
|
90
91
|
}
|
|
91
|
-
}
|
|
92
|
+
},
|
|
93
|
+
onError: (error) => {
|
|
92
94
|
console.error("Failed to retry delivery:", error);
|
|
93
95
|
toast.error("Failed to retry delivery");
|
|
94
|
-
}
|
|
96
|
+
},
|
|
97
|
+
onSettled: () => {
|
|
95
98
|
setRetrying(undefined);
|
|
96
|
-
}
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const handleRetry = (logId: string) => {
|
|
103
|
+
setRetrying(logId);
|
|
104
|
+
retryMutation.mutate({ logId });
|
|
97
105
|
};
|
|
98
106
|
|
|
99
107
|
return (
|
|
100
108
|
<PageLayout
|
|
101
109
|
title="Delivery Logs"
|
|
102
110
|
subtitle="View and manage webhook delivery attempts"
|
|
103
|
-
loading={
|
|
111
|
+
loading={isLoading}
|
|
104
112
|
actions={
|
|
105
113
|
<BackLink to={resolveRoute(integrationRoutes.routes.list)}>
|
|
106
114
|
Back to Subscriptions
|
|
@@ -115,7 +123,7 @@ export const DeliveryLogsPage = () => {
|
|
|
115
123
|
icon={<FileText className="h-5 w-5" />}
|
|
116
124
|
/>
|
|
117
125
|
|
|
118
|
-
{logs.length === 0 && !
|
|
126
|
+
{logs.length === 0 && !isLoading ? (
|
|
119
127
|
<Card className="p-8">
|
|
120
128
|
<div className="text-center text-muted-foreground">
|
|
121
129
|
No delivery logs found
|
|
@@ -180,7 +188,7 @@ export const DeliveryLogsPage = () => {
|
|
|
180
188
|
<Button
|
|
181
189
|
variant="ghost"
|
|
182
190
|
size="sm"
|
|
183
|
-
onClick={() =>
|
|
191
|
+
onClick={() => handleRetry(log.id)}
|
|
184
192
|
disabled={retrying === log.id}
|
|
185
193
|
>
|
|
186
194
|
<RefreshCw
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useEffect
|
|
1
|
+
import { useState, useEffect } from "react";
|
|
2
2
|
import { Link, useSearchParams } from "react-router-dom";
|
|
3
3
|
import { Plus, Webhook, ArrowRight, Activity } from "lucide-react";
|
|
4
4
|
import {
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
useToast,
|
|
20
20
|
type LucideIconName,
|
|
21
21
|
} from "@checkstack/ui";
|
|
22
|
-
import {
|
|
22
|
+
import { usePluginClient } from "@checkstack/frontend-api";
|
|
23
23
|
import { resolveRoute } from "@checkstack/common";
|
|
24
24
|
import {
|
|
25
25
|
IntegrationApi,
|
|
@@ -30,48 +30,44 @@ import {
|
|
|
30
30
|
import { SubscriptionDialog } from "../components/CreateSubscriptionDialog";
|
|
31
31
|
|
|
32
32
|
export const IntegrationsPage = () => {
|
|
33
|
-
const
|
|
34
|
-
const client = rpcApi.forPlugin(IntegrationApi);
|
|
33
|
+
const client = usePluginClient(IntegrationApi);
|
|
35
34
|
const toast = useToast();
|
|
36
35
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
37
36
|
|
|
38
|
-
const [subscriptions, setSubscriptions] = useState<WebhookSubscription[]>([]);
|
|
39
|
-
const [providers, setProviders] = useState<IntegrationProviderInfo[]>([]);
|
|
40
|
-
const [loading, setLoading] = useState(true);
|
|
41
37
|
const [dialogOpen, setDialogOpen] = useState(false);
|
|
42
38
|
const [selectedSubscription, setSelectedSubscription] =
|
|
43
39
|
useState<WebhookSubscription>();
|
|
44
40
|
|
|
45
|
-
//
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
pending: number;
|
|
52
|
-
}>();
|
|
41
|
+
// Queries using hooks
|
|
42
|
+
const {
|
|
43
|
+
data: subscriptionsData,
|
|
44
|
+
isLoading: subsLoading,
|
|
45
|
+
refetch: refetchSubs,
|
|
46
|
+
} = client.listSubscriptions.useQuery({ page: 1, pageSize: 100 });
|
|
53
47
|
|
|
54
|
-
const
|
|
55
|
-
|
|
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]);
|
|
48
|
+
const { data: providers = [], isLoading: providersLoading } =
|
|
49
|
+
client.listProviders.useQuery({});
|
|
71
50
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
51
|
+
const { data: stats, isLoading: statsLoading } =
|
|
52
|
+
client.getDeliveryStats.useQuery({ hours: 24 });
|
|
53
|
+
|
|
54
|
+
// Mutation for toggling
|
|
55
|
+
const toggleMutation = client.toggleSubscription.useMutation({
|
|
56
|
+
onSuccess: (_result, variables) => {
|
|
57
|
+
toast.success(
|
|
58
|
+
variables.enabled ? "Subscription enabled" : "Subscription disabled"
|
|
59
|
+
);
|
|
60
|
+
void refetchSubs();
|
|
61
|
+
},
|
|
62
|
+
onError: (error) => {
|
|
63
|
+
toast.error(
|
|
64
|
+
error instanceof Error ? error.message : "Failed to toggle subscription"
|
|
65
|
+
);
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const subscriptions = subscriptionsData?.subscriptions ?? [];
|
|
70
|
+
const loading = subsLoading || providersLoading || statsLoading;
|
|
75
71
|
|
|
76
72
|
// Handle ?action=create URL parameter (from command palette)
|
|
77
73
|
useEffect(() => {
|
|
@@ -87,37 +83,29 @@ export const IntegrationsPage = () => {
|
|
|
87
83
|
const getProviderInfo = (
|
|
88
84
|
providerId: string
|
|
89
85
|
): IntegrationProviderInfo | undefined => {
|
|
90
|
-
return providers.find(
|
|
86
|
+
return (providers as IntegrationProviderInfo[]).find(
|
|
87
|
+
(p) => p.qualifiedId === providerId
|
|
88
|
+
);
|
|
91
89
|
};
|
|
92
90
|
|
|
93
|
-
const handleToggle =
|
|
94
|
-
|
|
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
|
-
}
|
|
91
|
+
const handleToggle = (id: string, enabled: boolean) => {
|
|
92
|
+
toggleMutation.mutate({ id, enabled });
|
|
104
93
|
};
|
|
105
94
|
|
|
106
|
-
const handleCreated = (
|
|
107
|
-
|
|
95
|
+
const handleCreated = () => {
|
|
96
|
+
void refetchSubs();
|
|
108
97
|
setDialogOpen(false);
|
|
109
98
|
setSelectedSubscription(undefined);
|
|
110
99
|
};
|
|
111
100
|
|
|
112
101
|
const handleUpdated = () => {
|
|
113
|
-
|
|
114
|
-
void fetchData();
|
|
102
|
+
void refetchSubs();
|
|
115
103
|
setDialogOpen(false);
|
|
116
104
|
setSelectedSubscription(undefined);
|
|
117
105
|
};
|
|
118
106
|
|
|
119
|
-
const handleDeleted = (
|
|
120
|
-
|
|
107
|
+
const handleDeleted = () => {
|
|
108
|
+
void refetchSubs();
|
|
121
109
|
setDialogOpen(false);
|
|
122
110
|
setSelectedSubscription(undefined);
|
|
123
111
|
};
|
|
@@ -277,7 +265,7 @@ export const IntegrationsPage = () => {
|
|
|
277
265
|
size="sm"
|
|
278
266
|
onClick={(e) => {
|
|
279
267
|
e.stopPropagation();
|
|
280
|
-
|
|
268
|
+
handleToggle(sub.id, !sub.enabled);
|
|
281
269
|
}}
|
|
282
270
|
>
|
|
283
271
|
{sub.enabled ? "Disable" : "Enable"}
|
|
@@ -308,7 +296,7 @@ export const IntegrationsPage = () => {
|
|
|
308
296
|
description="Providers handle the delivery of events to external systems"
|
|
309
297
|
/>
|
|
310
298
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
311
|
-
{providers.map((provider) => (
|
|
299
|
+
{(providers as IntegrationProviderInfo[]).map((provider) => (
|
|
312
300
|
<Card key={provider.qualifiedId}>
|
|
313
301
|
<CardContent className="p-4">
|
|
314
302
|
<div className="flex items-center justify-between">
|
|
@@ -344,7 +332,7 @@ export const IntegrationsPage = () => {
|
|
|
344
332
|
</CardContent>
|
|
345
333
|
</Card>
|
|
346
334
|
))}
|
|
347
|
-
{providers.length === 0 && (
|
|
335
|
+
{(providers as IntegrationProviderInfo[]).length === 0 && (
|
|
348
336
|
<Card className="col-span-full">
|
|
349
337
|
<CardContent className="p-4">
|
|
350
338
|
<div className="text-center text-muted-foreground py-4">
|
|
@@ -364,7 +352,7 @@ export const IntegrationsPage = () => {
|
|
|
364
352
|
setDialogOpen(open);
|
|
365
353
|
if (!open) setSelectedSubscription(undefined);
|
|
366
354
|
}}
|
|
367
|
-
providers={providers}
|
|
355
|
+
providers={providers as IntegrationProviderInfo[]}
|
|
368
356
|
subscription={selectedSubscription}
|
|
369
357
|
onCreated={handleCreated}
|
|
370
358
|
onUpdated={handleUpdated}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Manages site-wide connections for a specific integration provider.
|
|
5
5
|
* Uses the provider's connectionSchema with DynamicForm for the configuration UI.
|
|
6
6
|
*/
|
|
7
|
-
import { useState
|
|
7
|
+
import { useState } from "react";
|
|
8
8
|
import { useParams } from "react-router-dom";
|
|
9
9
|
import {
|
|
10
10
|
Plus,
|
|
@@ -44,7 +44,7 @@ import {
|
|
|
44
44
|
BackLink,
|
|
45
45
|
type LucideIconName,
|
|
46
46
|
} from "@checkstack/ui";
|
|
47
|
-
import {
|
|
47
|
+
import { usePluginClient } from "@checkstack/frontend-api";
|
|
48
48
|
import { resolveRoute } from "@checkstack/common";
|
|
49
49
|
import {
|
|
50
50
|
IntegrationApi,
|
|
@@ -55,17 +55,9 @@ import {
|
|
|
55
55
|
|
|
56
56
|
export const ProviderConnectionsPage = () => {
|
|
57
57
|
const { providerId } = useParams<{ providerId: string }>();
|
|
58
|
-
const rpcApi = useApi(rpcApiRef);
|
|
59
|
-
const client = rpcApi.forPlugin(IntegrationApi);
|
|
60
|
-
const toast = useToast();
|
|
61
58
|
|
|
62
|
-
const
|
|
63
|
-
const
|
|
64
|
-
IntegrationProviderInfo | undefined
|
|
65
|
-
>();
|
|
66
|
-
const [connections, setConnections] = useState<ProviderConnectionRedacted[]>(
|
|
67
|
-
[]
|
|
68
|
-
);
|
|
59
|
+
const client = usePluginClient(IntegrationApi);
|
|
60
|
+
const toast = useToast();
|
|
69
61
|
|
|
70
62
|
// Dialog states
|
|
71
63
|
const [createDialogOpen, setCreateDialogOpen] = useState(false);
|
|
@@ -89,128 +81,135 @@ export const ProviderConnectionsPage = () => {
|
|
|
89
81
|
// Form validation state
|
|
90
82
|
const [configValid, setConfigValid] = useState(false);
|
|
91
83
|
|
|
92
|
-
|
|
93
|
-
|
|
84
|
+
// Queries using hooks
|
|
85
|
+
const { data: providers = [], isLoading: providersLoading } =
|
|
86
|
+
client.listProviders.useQuery({});
|
|
94
87
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
);
|
|
104
|
-
setProvider(foundProvider);
|
|
105
|
-
setConnections(connectionsResult);
|
|
106
|
-
} catch (error) {
|
|
107
|
-
console.error("Failed to load connections:", error);
|
|
108
|
-
toast.error("Failed to load connections");
|
|
109
|
-
} finally {
|
|
110
|
-
setLoading(false);
|
|
111
|
-
}
|
|
112
|
-
}, [providerId, client, toast]);
|
|
113
|
-
|
|
114
|
-
useEffect(() => {
|
|
115
|
-
void fetchData();
|
|
116
|
-
}, [fetchData]);
|
|
88
|
+
const {
|
|
89
|
+
data: connections = [],
|
|
90
|
+
isLoading: connectionsLoading,
|
|
91
|
+
refetch: refetchConnections,
|
|
92
|
+
} = client.listConnections.useQuery(
|
|
93
|
+
{ providerId: providerId ?? "" },
|
|
94
|
+
{ enabled: !!providerId }
|
|
95
|
+
);
|
|
117
96
|
|
|
118
|
-
const
|
|
119
|
-
|
|
97
|
+
const loading = providersLoading || connectionsLoading;
|
|
98
|
+
const provider = (providers as IntegrationProviderInfo[]).find(
|
|
99
|
+
(p) => p.qualifiedId === providerId
|
|
100
|
+
);
|
|
120
101
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
name: formName.trim(),
|
|
126
|
-
config: formConfig,
|
|
127
|
-
});
|
|
128
|
-
setConnections((prev) => [...prev, newConnection]);
|
|
102
|
+
// Mutations
|
|
103
|
+
const createMutation = client.createConnection.useMutation({
|
|
104
|
+
onSuccess: () => {
|
|
105
|
+
void refetchConnections();
|
|
129
106
|
setCreateDialogOpen(false);
|
|
130
107
|
setFormName("");
|
|
131
108
|
setFormConfig({});
|
|
132
109
|
toast.success("Connection created successfully");
|
|
133
|
-
} catch (error) {
|
|
134
|
-
console.error("Failed to create connection:", error);
|
|
135
|
-
toast.error("Failed to create connection");
|
|
136
|
-
} finally {
|
|
137
110
|
setSaving(false);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const openCreateDialog = () => {
|
|
143
|
-
setFormName("");
|
|
144
|
-
setFormConfig({});
|
|
145
|
-
setConfigValid(false);
|
|
146
|
-
setCreateDialogOpen(true);
|
|
147
|
-
};
|
|
148
|
-
|
|
149
|
-
const handleUpdate = async () => {
|
|
150
|
-
if (!selectedConnection) return;
|
|
151
|
-
|
|
152
|
-
setSaving(true);
|
|
153
|
-
try {
|
|
154
|
-
const updated = await client.updateConnection({
|
|
155
|
-
connectionId: selectedConnection.id,
|
|
156
|
-
updates: {
|
|
157
|
-
name: formName.trim() || selectedConnection.name,
|
|
158
|
-
config: formConfig,
|
|
159
|
-
},
|
|
160
|
-
});
|
|
161
|
-
setConnections((prev) =>
|
|
162
|
-
prev.map((c) => (c.id === updated.id ? updated : c))
|
|
111
|
+
},
|
|
112
|
+
onError: (error) => {
|
|
113
|
+
toast.error(
|
|
114
|
+
error instanceof Error ? error.message : "Failed to create connection"
|
|
163
115
|
);
|
|
116
|
+
setSaving(false);
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const updateMutation = client.updateConnection.useMutation({
|
|
121
|
+
onSuccess: () => {
|
|
122
|
+
void refetchConnections();
|
|
164
123
|
setEditDialogOpen(false);
|
|
165
124
|
setSelectedConnection(undefined);
|
|
166
125
|
toast.success("Connection updated successfully");
|
|
167
|
-
} catch (error) {
|
|
168
|
-
console.error("Failed to update connection:", error);
|
|
169
|
-
toast.error("Failed to update connection");
|
|
170
|
-
} finally {
|
|
171
126
|
setSaving(false);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
if (!selectedConnection) return;
|
|
177
|
-
|
|
178
|
-
try {
|
|
179
|
-
await client.deleteConnection({ connectionId: selectedConnection.id });
|
|
180
|
-
setConnections((prev) =>
|
|
181
|
-
prev.filter((c) => c.id !== selectedConnection.id)
|
|
127
|
+
},
|
|
128
|
+
onError: (error) => {
|
|
129
|
+
toast.error(
|
|
130
|
+
error instanceof Error ? error.message : "Failed to update connection"
|
|
182
131
|
);
|
|
132
|
+
setSaving(false);
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const deleteMutation = client.deleteConnection.useMutation({
|
|
137
|
+
onSuccess: () => {
|
|
138
|
+
void refetchConnections();
|
|
183
139
|
setDeleteConfirmOpen(false);
|
|
184
140
|
setSelectedConnection(undefined);
|
|
185
141
|
toast.success("Connection deleted");
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
toast.error(
|
|
189
|
-
|
|
190
|
-
|
|
142
|
+
},
|
|
143
|
+
onError: (error) => {
|
|
144
|
+
toast.error(
|
|
145
|
+
error instanceof Error ? error.message : "Failed to delete connection"
|
|
146
|
+
);
|
|
147
|
+
},
|
|
148
|
+
});
|
|
191
149
|
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
try {
|
|
195
|
-
const result = await client.testConnection({ connectionId });
|
|
150
|
+
const testMutation = client.testConnection.useMutation({
|
|
151
|
+
onSuccess: (result, variables) => {
|
|
196
152
|
setTestResults((prev) => ({
|
|
197
153
|
...prev,
|
|
198
|
-
[connectionId]: result,
|
|
154
|
+
[variables.connectionId]: result,
|
|
199
155
|
}));
|
|
200
156
|
if (result.success) {
|
|
201
157
|
toast.success(result.message ?? "Connection test successful");
|
|
202
158
|
} else {
|
|
203
159
|
toast.error(result.message ?? "Connection test failed");
|
|
204
160
|
}
|
|
205
|
-
|
|
161
|
+
setTestingId(undefined);
|
|
162
|
+
},
|
|
163
|
+
onError: (error, variables) => {
|
|
206
164
|
setTestResults((prev) => ({
|
|
207
165
|
...prev,
|
|
208
|
-
[connectionId]: { success: false, message: "Test failed" },
|
|
166
|
+
[variables.connectionId]: { success: false, message: "Test failed" },
|
|
209
167
|
}));
|
|
210
|
-
toast.error(
|
|
211
|
-
|
|
168
|
+
toast.error(
|
|
169
|
+
error instanceof Error ? error.message : "Connection test failed"
|
|
170
|
+
);
|
|
212
171
|
setTestingId(undefined);
|
|
213
|
-
}
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const handleCreate = () => {
|
|
176
|
+
if (!providerId || !formName.trim()) return;
|
|
177
|
+
setSaving(true);
|
|
178
|
+
createMutation.mutate({
|
|
179
|
+
providerId,
|
|
180
|
+
name: formName.trim(),
|
|
181
|
+
config: formConfig,
|
|
182
|
+
});
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
// Reset form when creating
|
|
186
|
+
const openCreateDialog = () => {
|
|
187
|
+
setFormName("");
|
|
188
|
+
setFormConfig({});
|
|
189
|
+
setConfigValid(false);
|
|
190
|
+
setCreateDialogOpen(true);
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const handleUpdate = () => {
|
|
194
|
+
if (!selectedConnection) return;
|
|
195
|
+
setSaving(true);
|
|
196
|
+
updateMutation.mutate({
|
|
197
|
+
connectionId: selectedConnection.id,
|
|
198
|
+
updates: {
|
|
199
|
+
name: formName.trim() || selectedConnection.name,
|
|
200
|
+
config: formConfig,
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const handleDelete = () => {
|
|
206
|
+
if (!selectedConnection) return;
|
|
207
|
+
deleteMutation.mutate({ connectionId: selectedConnection.id });
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const handleTest = (connectionId: string) => {
|
|
211
|
+
setTestingId(connectionId);
|
|
212
|
+
testMutation.mutate({ connectionId });
|
|
214
213
|
};
|
|
215
214
|
|
|
216
215
|
const openEditDialog = (connection: ProviderConnectionRedacted) => {
|
|
@@ -271,7 +270,7 @@ export const ProviderConnectionsPage = () => {
|
|
|
271
270
|
</div>
|
|
272
271
|
}
|
|
273
272
|
>
|
|
274
|
-
{connections.length === 0 ? (
|
|
273
|
+
{(connections as ProviderConnectionRedacted[]).length === 0 ? (
|
|
275
274
|
<EmptyState
|
|
276
275
|
icon={
|
|
277
276
|
<DynamicIcon
|
|
@@ -309,7 +308,7 @@ export const ProviderConnectionsPage = () => {
|
|
|
309
308
|
</TableRow>
|
|
310
309
|
</TableHeader>
|
|
311
310
|
<TableBody>
|
|
312
|
-
{connections.map((conn) => {
|
|
311
|
+
{(connections as ProviderConnectionRedacted[]).map((conn) => {
|
|
313
312
|
const testResult = testResults[conn.id];
|
|
314
313
|
const isTesting = testingId === conn.id;
|
|
315
314
|
|