@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,470 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider Connections Page
|
|
3
|
+
*
|
|
4
|
+
* Manages site-wide connections for a specific integration provider.
|
|
5
|
+
* Uses the provider's connectionSchema with DynamicForm for the configuration UI.
|
|
6
|
+
*/
|
|
7
|
+
import { useState, useEffect, useCallback } from "react";
|
|
8
|
+
import { useParams } from "react-router-dom";
|
|
9
|
+
import {
|
|
10
|
+
Plus,
|
|
11
|
+
Settings2,
|
|
12
|
+
Trash2,
|
|
13
|
+
TestTube2,
|
|
14
|
+
CheckCircle2,
|
|
15
|
+
XCircle,
|
|
16
|
+
Loader2,
|
|
17
|
+
} from "lucide-react";
|
|
18
|
+
import {
|
|
19
|
+
PageLayout,
|
|
20
|
+
Card,
|
|
21
|
+
CardContent,
|
|
22
|
+
CardHeader,
|
|
23
|
+
CardTitle,
|
|
24
|
+
Button,
|
|
25
|
+
Dialog,
|
|
26
|
+
DialogContent,
|
|
27
|
+
DialogHeader,
|
|
28
|
+
DialogTitle,
|
|
29
|
+
DialogDescription,
|
|
30
|
+
DialogFooter,
|
|
31
|
+
DynamicIcon,
|
|
32
|
+
EmptyState,
|
|
33
|
+
Table,
|
|
34
|
+
TableBody,
|
|
35
|
+
TableCell,
|
|
36
|
+
TableHead,
|
|
37
|
+
TableHeader,
|
|
38
|
+
TableRow,
|
|
39
|
+
DynamicForm,
|
|
40
|
+
Input,
|
|
41
|
+
Label,
|
|
42
|
+
useToast,
|
|
43
|
+
ConfirmationModal,
|
|
44
|
+
BackLink,
|
|
45
|
+
type LucideIconName,
|
|
46
|
+
} from "@checkstack/ui";
|
|
47
|
+
import { useApi, rpcApiRef } from "@checkstack/frontend-api";
|
|
48
|
+
import { resolveRoute } from "@checkstack/common";
|
|
49
|
+
import {
|
|
50
|
+
IntegrationApi,
|
|
51
|
+
integrationRoutes,
|
|
52
|
+
type IntegrationProviderInfo,
|
|
53
|
+
type ProviderConnectionRedacted,
|
|
54
|
+
} from "@checkstack/integration-common";
|
|
55
|
+
|
|
56
|
+
export const ProviderConnectionsPage = () => {
|
|
57
|
+
const { providerId } = useParams<{ providerId: string }>();
|
|
58
|
+
const rpcApi = useApi(rpcApiRef);
|
|
59
|
+
const client = rpcApi.forPlugin(IntegrationApi);
|
|
60
|
+
const toast = useToast();
|
|
61
|
+
|
|
62
|
+
const [loading, setLoading] = useState(true);
|
|
63
|
+
const [provider, setProvider] = useState<
|
|
64
|
+
IntegrationProviderInfo | undefined
|
|
65
|
+
>();
|
|
66
|
+
const [connections, setConnections] = useState<ProviderConnectionRedacted[]>(
|
|
67
|
+
[]
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
// Dialog states
|
|
71
|
+
const [createDialogOpen, setCreateDialogOpen] = useState(false);
|
|
72
|
+
const [editDialogOpen, setEditDialogOpen] = useState(false);
|
|
73
|
+
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
|
|
74
|
+
const [selectedConnection, setSelectedConnection] = useState<
|
|
75
|
+
ProviderConnectionRedacted | undefined
|
|
76
|
+
>();
|
|
77
|
+
|
|
78
|
+
// Form state
|
|
79
|
+
const [formName, setFormName] = useState("");
|
|
80
|
+
const [formConfig, setFormConfig] = useState<Record<string, unknown>>({});
|
|
81
|
+
const [saving, setSaving] = useState(false);
|
|
82
|
+
|
|
83
|
+
// Test state
|
|
84
|
+
const [testingId, setTestingId] = useState<string | undefined>();
|
|
85
|
+
const [testResults, setTestResults] = useState<
|
|
86
|
+
Record<string, { success: boolean; message?: string }>
|
|
87
|
+
>({});
|
|
88
|
+
|
|
89
|
+
// Form validation state
|
|
90
|
+
const [configValid, setConfigValid] = useState(false);
|
|
91
|
+
|
|
92
|
+
const fetchData = useCallback(async () => {
|
|
93
|
+
if (!providerId) return;
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const [providersResult, connectionsResult] = await Promise.all([
|
|
97
|
+
client.listProviders(),
|
|
98
|
+
client.listConnections({ providerId }),
|
|
99
|
+
]);
|
|
100
|
+
|
|
101
|
+
const foundProvider = providersResult.find(
|
|
102
|
+
(p) => p.qualifiedId === providerId
|
|
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]);
|
|
117
|
+
|
|
118
|
+
const handleCreate = async () => {
|
|
119
|
+
if (!providerId || !formName.trim()) return;
|
|
120
|
+
|
|
121
|
+
setSaving(true);
|
|
122
|
+
try {
|
|
123
|
+
const newConnection = await client.createConnection({
|
|
124
|
+
providerId,
|
|
125
|
+
name: formName.trim(),
|
|
126
|
+
config: formConfig,
|
|
127
|
+
});
|
|
128
|
+
setConnections((prev) => [...prev, newConnection]);
|
|
129
|
+
setCreateDialogOpen(false);
|
|
130
|
+
setFormName("");
|
|
131
|
+
setFormConfig({});
|
|
132
|
+
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
|
+
setSaving(false);
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
// Reset form when creating
|
|
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))
|
|
163
|
+
);
|
|
164
|
+
setEditDialogOpen(false);
|
|
165
|
+
setSelectedConnection(undefined);
|
|
166
|
+
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
|
+
setSaving(false);
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const handleDelete = async () => {
|
|
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)
|
|
182
|
+
);
|
|
183
|
+
setDeleteConfirmOpen(false);
|
|
184
|
+
setSelectedConnection(undefined);
|
|
185
|
+
toast.success("Connection deleted");
|
|
186
|
+
} catch (error) {
|
|
187
|
+
console.error("Failed to delete connection:", error);
|
|
188
|
+
toast.error("Failed to delete connection");
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const handleTest = async (connectionId: string) => {
|
|
193
|
+
setTestingId(connectionId);
|
|
194
|
+
try {
|
|
195
|
+
const result = await client.testConnection({ connectionId });
|
|
196
|
+
setTestResults((prev) => ({
|
|
197
|
+
...prev,
|
|
198
|
+
[connectionId]: result,
|
|
199
|
+
}));
|
|
200
|
+
if (result.success) {
|
|
201
|
+
toast.success(result.message ?? "Connection test successful");
|
|
202
|
+
} else {
|
|
203
|
+
toast.error(result.message ?? "Connection test failed");
|
|
204
|
+
}
|
|
205
|
+
} catch {
|
|
206
|
+
setTestResults((prev) => ({
|
|
207
|
+
...prev,
|
|
208
|
+
[connectionId]: { success: false, message: "Test failed" },
|
|
209
|
+
}));
|
|
210
|
+
toast.error("Connection test failed");
|
|
211
|
+
} finally {
|
|
212
|
+
setTestingId(undefined);
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const openEditDialog = (connection: ProviderConnectionRedacted) => {
|
|
217
|
+
setSelectedConnection(connection);
|
|
218
|
+
setFormName(connection.name);
|
|
219
|
+
setFormConfig(connection.configPreview);
|
|
220
|
+
setConfigValid(true); // Existing connections should have valid config
|
|
221
|
+
setEditDialogOpen(true);
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const openDeleteConfirm = (connection: ProviderConnectionRedacted) => {
|
|
225
|
+
setSelectedConnection(connection);
|
|
226
|
+
setDeleteConfirmOpen(true);
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
if (!providerId) {
|
|
230
|
+
return <PageLayout title="Error">Missing provider ID</PageLayout>;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (!loading && !provider) {
|
|
234
|
+
return (
|
|
235
|
+
<PageLayout title="Provider Not Found">
|
|
236
|
+
<EmptyState
|
|
237
|
+
icon={<Settings2 className="h-12 w-12" />}
|
|
238
|
+
title="Provider not found"
|
|
239
|
+
description={`No provider found with ID: ${providerId}`}
|
|
240
|
+
/>
|
|
241
|
+
</PageLayout>
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (!loading && !provider?.hasConnectionSchema) {
|
|
246
|
+
return (
|
|
247
|
+
<PageLayout title={provider?.displayName ?? "Provider"}>
|
|
248
|
+
<EmptyState
|
|
249
|
+
icon={<Settings2 className="h-12 w-12" />}
|
|
250
|
+
title="No connection management"
|
|
251
|
+
description="This provider does not support site-wide connections"
|
|
252
|
+
/>
|
|
253
|
+
</PageLayout>
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return (
|
|
258
|
+
<PageLayout
|
|
259
|
+
title={`${provider?.displayName ?? "Provider"} Connections`}
|
|
260
|
+
subtitle="Manage site-wide connections for this integration provider"
|
|
261
|
+
loading={loading}
|
|
262
|
+
actions={
|
|
263
|
+
<div className="flex items-center gap-2">
|
|
264
|
+
<BackLink to={resolveRoute(integrationRoutes.routes.list)}>
|
|
265
|
+
Back to Integrations
|
|
266
|
+
</BackLink>
|
|
267
|
+
<Button onClick={openCreateDialog}>
|
|
268
|
+
<Plus className="h-4 w-4 mr-2" />
|
|
269
|
+
New Connection
|
|
270
|
+
</Button>
|
|
271
|
+
</div>
|
|
272
|
+
}
|
|
273
|
+
>
|
|
274
|
+
{connections.length === 0 ? (
|
|
275
|
+
<EmptyState
|
|
276
|
+
icon={
|
|
277
|
+
<DynamicIcon
|
|
278
|
+
name={(provider?.icon ?? "Settings2") as LucideIconName}
|
|
279
|
+
className="h-12 w-12"
|
|
280
|
+
/>
|
|
281
|
+
}
|
|
282
|
+
title="No connections configured"
|
|
283
|
+
description="Create a connection to start using this provider"
|
|
284
|
+
>
|
|
285
|
+
<Button onClick={openCreateDialog} className="mt-4">
|
|
286
|
+
<Plus className="h-4 w-4 mr-2" />
|
|
287
|
+
Create Connection
|
|
288
|
+
</Button>
|
|
289
|
+
</EmptyState>
|
|
290
|
+
) : (
|
|
291
|
+
<Card>
|
|
292
|
+
<CardHeader>
|
|
293
|
+
<CardTitle className="flex items-center gap-2">
|
|
294
|
+
<DynamicIcon
|
|
295
|
+
name={(provider?.icon ?? "Settings2") as LucideIconName}
|
|
296
|
+
className="h-5 w-5"
|
|
297
|
+
/>
|
|
298
|
+
Connections
|
|
299
|
+
</CardTitle>
|
|
300
|
+
</CardHeader>
|
|
301
|
+
<CardContent>
|
|
302
|
+
<Table>
|
|
303
|
+
<TableHeader>
|
|
304
|
+
<TableRow>
|
|
305
|
+
<TableHead>Name</TableHead>
|
|
306
|
+
<TableHead>Created</TableHead>
|
|
307
|
+
<TableHead>Status</TableHead>
|
|
308
|
+
<TableHead className="text-right">Actions</TableHead>
|
|
309
|
+
</TableRow>
|
|
310
|
+
</TableHeader>
|
|
311
|
+
<TableBody>
|
|
312
|
+
{connections.map((conn) => {
|
|
313
|
+
const testResult = testResults[conn.id];
|
|
314
|
+
const isTesting = testingId === conn.id;
|
|
315
|
+
|
|
316
|
+
return (
|
|
317
|
+
<TableRow key={conn.id}>
|
|
318
|
+
<TableCell className="font-medium">{conn.name}</TableCell>
|
|
319
|
+
<TableCell className="text-muted-foreground">
|
|
320
|
+
{new Date(conn.createdAt).toLocaleDateString()}
|
|
321
|
+
</TableCell>
|
|
322
|
+
<TableCell>
|
|
323
|
+
{testResult && (
|
|
324
|
+
<div className="flex items-center gap-1">
|
|
325
|
+
{testResult.success ? (
|
|
326
|
+
<CheckCircle2 className="h-4 w-4 text-green-600" />
|
|
327
|
+
) : (
|
|
328
|
+
<XCircle className="h-4 w-4 text-red-600" />
|
|
329
|
+
)}
|
|
330
|
+
<span className="text-sm">
|
|
331
|
+
{testResult.success ? "Connected" : "Failed"}
|
|
332
|
+
</span>
|
|
333
|
+
</div>
|
|
334
|
+
)}
|
|
335
|
+
</TableCell>
|
|
336
|
+
<TableCell className="text-right">
|
|
337
|
+
<div className="flex items-center justify-end gap-2">
|
|
338
|
+
<Button
|
|
339
|
+
variant="ghost"
|
|
340
|
+
size="sm"
|
|
341
|
+
onClick={() => handleTest(conn.id)}
|
|
342
|
+
disabled={isTesting}
|
|
343
|
+
>
|
|
344
|
+
{isTesting ? (
|
|
345
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
346
|
+
) : (
|
|
347
|
+
<TestTube2 className="h-4 w-4" />
|
|
348
|
+
)}
|
|
349
|
+
</Button>
|
|
350
|
+
<Button
|
|
351
|
+
variant="ghost"
|
|
352
|
+
size="sm"
|
|
353
|
+
onClick={() => openEditDialog(conn)}
|
|
354
|
+
>
|
|
355
|
+
<Settings2 className="h-4 w-4" />
|
|
356
|
+
</Button>
|
|
357
|
+
<Button
|
|
358
|
+
variant="ghost"
|
|
359
|
+
size="sm"
|
|
360
|
+
onClick={() => openDeleteConfirm(conn)}
|
|
361
|
+
>
|
|
362
|
+
<Trash2 className="h-4 w-4 text-destructive" />
|
|
363
|
+
</Button>
|
|
364
|
+
</div>
|
|
365
|
+
</TableCell>
|
|
366
|
+
</TableRow>
|
|
367
|
+
);
|
|
368
|
+
})}
|
|
369
|
+
</TableBody>
|
|
370
|
+
</Table>
|
|
371
|
+
</CardContent>
|
|
372
|
+
</Card>
|
|
373
|
+
)}
|
|
374
|
+
|
|
375
|
+
{/* Create Dialog */}
|
|
376
|
+
<Dialog open={createDialogOpen} onOpenChange={setCreateDialogOpen}>
|
|
377
|
+
<DialogContent className="max-w-lg">
|
|
378
|
+
<DialogHeader>
|
|
379
|
+
<DialogTitle>New Connection</DialogTitle>
|
|
380
|
+
<DialogDescription>
|
|
381
|
+
Create a new {provider?.displayName} connection
|
|
382
|
+
</DialogDescription>
|
|
383
|
+
</DialogHeader>
|
|
384
|
+
<div className="space-y-4 py-4">
|
|
385
|
+
<div className="space-y-2">
|
|
386
|
+
<Label htmlFor="connection-name">Connection Name</Label>
|
|
387
|
+
<Input
|
|
388
|
+
id="connection-name"
|
|
389
|
+
placeholder="e.g., Production Server"
|
|
390
|
+
value={formName}
|
|
391
|
+
onChange={(e) => setFormName(e.target.value)}
|
|
392
|
+
/>
|
|
393
|
+
</div>
|
|
394
|
+
{provider?.connectionSchema && (
|
|
395
|
+
<DynamicForm
|
|
396
|
+
schema={provider.connectionSchema}
|
|
397
|
+
value={formConfig}
|
|
398
|
+
onChange={setFormConfig}
|
|
399
|
+
onValidChange={setConfigValid}
|
|
400
|
+
/>
|
|
401
|
+
)}
|
|
402
|
+
</div>
|
|
403
|
+
<DialogFooter>
|
|
404
|
+
<Button
|
|
405
|
+
variant="outline"
|
|
406
|
+
onClick={() => setCreateDialogOpen(false)}
|
|
407
|
+
>
|
|
408
|
+
Cancel
|
|
409
|
+
</Button>
|
|
410
|
+
<Button
|
|
411
|
+
onClick={handleCreate}
|
|
412
|
+
disabled={!formName.trim() || !configValid || saving}
|
|
413
|
+
>
|
|
414
|
+
{saving && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
|
|
415
|
+
Create
|
|
416
|
+
</Button>
|
|
417
|
+
</DialogFooter>
|
|
418
|
+
</DialogContent>
|
|
419
|
+
</Dialog>
|
|
420
|
+
|
|
421
|
+
{/* Edit Dialog */}
|
|
422
|
+
<Dialog open={editDialogOpen} onOpenChange={setEditDialogOpen}>
|
|
423
|
+
<DialogContent className="max-w-lg">
|
|
424
|
+
<DialogHeader>
|
|
425
|
+
<DialogTitle>Edit Connection</DialogTitle>
|
|
426
|
+
<DialogDescription>Update connection settings</DialogDescription>
|
|
427
|
+
</DialogHeader>
|
|
428
|
+
<div className="space-y-4 py-4">
|
|
429
|
+
<div className="space-y-2">
|
|
430
|
+
<Label htmlFor="edit-connection-name">Connection Name</Label>
|
|
431
|
+
<Input
|
|
432
|
+
id="edit-connection-name"
|
|
433
|
+
value={formName}
|
|
434
|
+
onChange={(e) => setFormName(e.target.value)}
|
|
435
|
+
/>
|
|
436
|
+
</div>
|
|
437
|
+
{provider?.connectionSchema && (
|
|
438
|
+
<DynamicForm
|
|
439
|
+
schema={provider.connectionSchema}
|
|
440
|
+
value={formConfig}
|
|
441
|
+
onChange={setFormConfig}
|
|
442
|
+
onValidChange={setConfigValid}
|
|
443
|
+
/>
|
|
444
|
+
)}
|
|
445
|
+
</div>
|
|
446
|
+
<DialogFooter>
|
|
447
|
+
<Button variant="outline" onClick={() => setEditDialogOpen(false)}>
|
|
448
|
+
Cancel
|
|
449
|
+
</Button>
|
|
450
|
+
<Button onClick={handleUpdate} disabled={!configValid || saving}>
|
|
451
|
+
{saving && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
|
|
452
|
+
Save Changes
|
|
453
|
+
</Button>
|
|
454
|
+
</DialogFooter>
|
|
455
|
+
</DialogContent>
|
|
456
|
+
</Dialog>
|
|
457
|
+
|
|
458
|
+
{/* Delete Confirmation */}
|
|
459
|
+
<ConfirmationModal
|
|
460
|
+
isOpen={deleteConfirmOpen}
|
|
461
|
+
onClose={() => setDeleteConfirmOpen(false)}
|
|
462
|
+
title="Delete Connection"
|
|
463
|
+
message={`Are you sure you want to delete "${selectedConnection?.name}"? This action cannot be undone.`}
|
|
464
|
+
confirmText="Delete"
|
|
465
|
+
variant="danger"
|
|
466
|
+
onConfirm={() => void handleDelete()}
|
|
467
|
+
/>
|
|
468
|
+
</PageLayout>
|
|
469
|
+
);
|
|
470
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registry for provider configuration component extensions.
|
|
3
|
+
* Allows integration providers to register custom React components for
|
|
4
|
+
* subscription configuration instead of using the generic DynamicForm.
|
|
5
|
+
*/
|
|
6
|
+
import type { ComponentType } from "react";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Props passed to custom provider config components.
|
|
10
|
+
* @template TConfig The shape of the provider configuration
|
|
11
|
+
*/
|
|
12
|
+
export interface ProviderConfigProps<
|
|
13
|
+
TConfig extends Record<string, unknown> = Record<string, unknown>
|
|
14
|
+
> {
|
|
15
|
+
/** Current provider configuration values */
|
|
16
|
+
value: TConfig;
|
|
17
|
+
/** Callback to update configuration values */
|
|
18
|
+
onChange: (value: TConfig) => void;
|
|
19
|
+
/** Whether the form is in a saving/loading state */
|
|
20
|
+
isSubmitting?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Extension registration for custom provider configuration components.
|
|
25
|
+
* @template TConfig The shape of the provider configuration
|
|
26
|
+
*/
|
|
27
|
+
export interface ProviderConfigExtension<
|
|
28
|
+
TConfig extends Record<string, unknown> = Record<string, unknown>
|
|
29
|
+
> {
|
|
30
|
+
/** Qualified provider ID this extension applies to (e.g., "integration-jira.jira") */
|
|
31
|
+
providerId: string;
|
|
32
|
+
/**
|
|
33
|
+
* React component to render for this provider's configuration.
|
|
34
|
+
* Will be used instead of the generic DynamicForm.
|
|
35
|
+
*/
|
|
36
|
+
ConfigComponent: ComponentType<ProviderConfigProps<TConfig>>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const registeredExtensions = new Map<string, ProviderConfigExtension>();
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Register a custom configuration component for a provider.
|
|
43
|
+
* @param extension The extension to register
|
|
44
|
+
*/
|
|
45
|
+
export function registerProviderConfigExtension(
|
|
46
|
+
extension: ProviderConfigExtension
|
|
47
|
+
): void {
|
|
48
|
+
registeredExtensions.set(extension.providerId, extension);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Get a registered configuration component for a provider.
|
|
53
|
+
* @param providerId Qualified provider ID (e.g., "integration-jira.jira")
|
|
54
|
+
* @returns The extension if registered, undefined otherwise
|
|
55
|
+
*/
|
|
56
|
+
export function getProviderConfigExtension(
|
|
57
|
+
providerId: string
|
|
58
|
+
): ProviderConfigExtension | undefined {
|
|
59
|
+
return registeredExtensions.get(providerId);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Check if a provider has a custom configuration component registered.
|
|
64
|
+
* @param providerId Qualified provider ID
|
|
65
|
+
*/
|
|
66
|
+
export function hasProviderConfigExtension(providerId: string): boolean {
|
|
67
|
+
return registeredExtensions.has(providerId);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Get all registered provider config extensions.
|
|
72
|
+
*/
|
|
73
|
+
export function getAllProviderConfigExtensions(): ProviderConfigExtension[] {
|
|
74
|
+
return [...registeredExtensions.values()];
|
|
75
|
+
}
|