@checkstack/integration-frontend 0.4.5 → 0.5.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 +154 -0
- package/package.json +8 -8
- package/src/components/IntegrationMenuItem.tsx +14 -9
- package/src/index.tsx +9 -14
- package/src/pages/IntegrationsLandingPage.tsx +116 -0
- package/src/components/CreateSubscriptionDialog.tsx +0 -652
- package/src/pages/DeliveryLogsPage.tsx +0 -300
- package/src/pages/IntegrationsPage.tsx +0 -378
|
@@ -1,652 +0,0 @@
|
|
|
1
|
-
import { useState, useEffect, useMemo } from "react";
|
|
2
|
-
import { Link } from "react-router-dom";
|
|
3
|
-
import { Trash2, ScrollText } from "lucide-react";
|
|
4
|
-
import {
|
|
5
|
-
Dialog,
|
|
6
|
-
DialogContent,
|
|
7
|
-
DialogDescription,
|
|
8
|
-
DialogHeader,
|
|
9
|
-
DialogTitle,
|
|
10
|
-
DialogFooter,
|
|
11
|
-
Button,
|
|
12
|
-
Input,
|
|
13
|
-
Textarea,
|
|
14
|
-
DynamicForm,
|
|
15
|
-
DynamicIcon,
|
|
16
|
-
integrationScriptContext,
|
|
17
|
-
type JsonSchemaProperty,
|
|
18
|
-
useToast,
|
|
19
|
-
Select,
|
|
20
|
-
SelectContent,
|
|
21
|
-
SelectItem,
|
|
22
|
-
SelectTrigger,
|
|
23
|
-
SelectValue,
|
|
24
|
-
Label,
|
|
25
|
-
ConfirmationModal,
|
|
26
|
-
toastError,
|
|
27
|
-
type LucideIconName,
|
|
28
|
-
} from "@checkstack/ui";
|
|
29
|
-
import { usePluginClient } from "@checkstack/frontend-api";
|
|
30
|
-
import { resolveRoute } from "@checkstack/common";
|
|
31
|
-
import {
|
|
32
|
-
IntegrationApi,
|
|
33
|
-
integrationRoutes,
|
|
34
|
-
type WebhookSubscription,
|
|
35
|
-
type IntegrationProviderInfo,
|
|
36
|
-
type PayloadProperty,
|
|
37
|
-
} from "@checkstack/integration-common";
|
|
38
|
-
import { ProviderDocumentation } from "./ProviderDocumentation";
|
|
39
|
-
import { getProviderConfigExtension } from "../provider-config-registry";
|
|
40
|
-
|
|
41
|
-
interface SubscriptionDialogProps {
|
|
42
|
-
open: boolean;
|
|
43
|
-
onOpenChange: (open: boolean) => void;
|
|
44
|
-
providers: IntegrationProviderInfo[];
|
|
45
|
-
/** Existing subscription for edit mode */
|
|
46
|
-
subscription?: WebhookSubscription;
|
|
47
|
-
/** Called when a new subscription is created */
|
|
48
|
-
onCreated?: (subscription: WebhookSubscription) => void;
|
|
49
|
-
/** Called when an existing subscription is updated */
|
|
50
|
-
onUpdated?: (subscription: WebhookSubscription) => void;
|
|
51
|
-
/** Called when an existing subscription is deleted */
|
|
52
|
-
onDeleted?: (id: string) => void;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export const SubscriptionDialog = ({
|
|
56
|
-
open,
|
|
57
|
-
onOpenChange,
|
|
58
|
-
providers,
|
|
59
|
-
subscription,
|
|
60
|
-
onCreated,
|
|
61
|
-
onUpdated,
|
|
62
|
-
onDeleted,
|
|
63
|
-
}: SubscriptionDialogProps) => {
|
|
64
|
-
const client = usePluginClient(IntegrationApi);
|
|
65
|
-
const toast = useToast();
|
|
66
|
-
|
|
67
|
-
// Edit mode detection
|
|
68
|
-
const isEditMode = !!subscription;
|
|
69
|
-
|
|
70
|
-
const [step, setStep] = useState<"provider" | "config">("provider");
|
|
71
|
-
const [selectedProvider, setSelectedProvider] =
|
|
72
|
-
useState<IntegrationProviderInfo>();
|
|
73
|
-
const [saving, setSaving] = useState(false);
|
|
74
|
-
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
|
75
|
-
|
|
76
|
-
// Connection state for providers with connectionSchema
|
|
77
|
-
const [selectedConnectionId, setSelectedConnectionId] = useState<string>("");
|
|
78
|
-
|
|
79
|
-
// Form state
|
|
80
|
-
const [name, setName] = useState("");
|
|
81
|
-
const [description, setDescription] = useState("");
|
|
82
|
-
const [providerConfig, setProviderConfig] = useState<Record<string, unknown>>(
|
|
83
|
-
{},
|
|
84
|
-
);
|
|
85
|
-
const [selectedEventId, setSelectedEventId] = useState<string>("");
|
|
86
|
-
// Track whether DynamicForm fields are valid (all required fields filled)
|
|
87
|
-
const [providerConfigValid, setProviderConfigValid] = useState(false);
|
|
88
|
-
|
|
89
|
-
// Queries using hooks
|
|
90
|
-
const { data: events = [] } = client.listEventTypes.useQuery(
|
|
91
|
-
{},
|
|
92
|
-
{ enabled: open },
|
|
93
|
-
);
|
|
94
|
-
|
|
95
|
-
const { data: connections = [], isLoading: loadingConnections } =
|
|
96
|
-
client.listConnections.useQuery(
|
|
97
|
-
{ providerId: selectedProvider?.qualifiedId ?? "" },
|
|
98
|
-
{ enabled: open && !!selectedProvider?.hasConnectionSchema },
|
|
99
|
-
);
|
|
100
|
-
|
|
101
|
-
const { data: payloadSchemaData } = client.getEventPayloadSchema.useQuery(
|
|
102
|
-
{ eventId: selectedEventId },
|
|
103
|
-
{ enabled: open && !!selectedEventId },
|
|
104
|
-
);
|
|
105
|
-
|
|
106
|
-
const payloadProperties: PayloadProperty[] =
|
|
107
|
-
payloadSchemaData?.availableProperties ?? [];
|
|
108
|
-
|
|
109
|
-
// Build the editor IntelliSense + starter-template + shell-env-var
|
|
110
|
-
// bundle for this event. When the payload schema isn't available yet,
|
|
111
|
-
// we still get the result-shape virtual module and the platform-injected
|
|
112
|
-
// env vars (EVENT_ID, DELIVERY_ID, ...) — just no per-payload-field
|
|
113
|
-
// PAYLOAD_* hints. Recomputes only when the schema actually changes.
|
|
114
|
-
const editorScriptContext = useMemo(
|
|
115
|
-
() =>
|
|
116
|
-
integrationScriptContext({
|
|
117
|
-
eventPayloadSchema: payloadSchemaData?.payloadSchema as
|
|
118
|
-
| JsonSchemaProperty
|
|
119
|
-
| undefined,
|
|
120
|
-
}),
|
|
121
|
-
[payloadSchemaData?.payloadSchema],
|
|
122
|
-
);
|
|
123
|
-
|
|
124
|
-
// Mutations
|
|
125
|
-
const createMutation = client.createSubscription.useMutation({
|
|
126
|
-
onSuccess: (result) => {
|
|
127
|
-
onCreated?.(result);
|
|
128
|
-
toast.success("Subscription created");
|
|
129
|
-
setSaving(false);
|
|
130
|
-
},
|
|
131
|
-
onError: (error) => {
|
|
132
|
-
toastError(toast, "Failed to create subscription", error);
|
|
133
|
-
setSaving(false);
|
|
134
|
-
},
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
const updateMutation = client.updateSubscription.useMutation({
|
|
138
|
-
onSuccess: () => {
|
|
139
|
-
toast.success("Subscription updated");
|
|
140
|
-
onUpdated?.(subscription!);
|
|
141
|
-
onOpenChange(false);
|
|
142
|
-
setSaving(false);
|
|
143
|
-
},
|
|
144
|
-
onError: (error) => {
|
|
145
|
-
toastError(toast, "Failed to update subscription", error);
|
|
146
|
-
setSaving(false);
|
|
147
|
-
},
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
const deleteMutation = client.deleteSubscription.useMutation({
|
|
151
|
-
onSuccess: () => {
|
|
152
|
-
toast.success("Subscription deleted");
|
|
153
|
-
onDeleted?.(subscription!.id);
|
|
154
|
-
onOpenChange(false);
|
|
155
|
-
},
|
|
156
|
-
onError: (error) => {
|
|
157
|
-
toastError(toast, "Failed to delete subscription", error);
|
|
158
|
-
},
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
// Auto-select if only one connection
|
|
162
|
-
useEffect(() => {
|
|
163
|
-
if (connections.length === 1 && !selectedConnectionId) {
|
|
164
|
-
setSelectedConnectionId(connections[0].id);
|
|
165
|
-
}
|
|
166
|
-
}, [connections, selectedConnectionId]);
|
|
167
|
-
|
|
168
|
-
// Pre-populate form in edit mode
|
|
169
|
-
useEffect(() => {
|
|
170
|
-
if (open && subscription) {
|
|
171
|
-
// Find the provider for this subscription
|
|
172
|
-
const provider = providers.find(
|
|
173
|
-
(p) => p.qualifiedId === subscription.providerId,
|
|
174
|
-
);
|
|
175
|
-
if (provider) {
|
|
176
|
-
setSelectedProvider(provider);
|
|
177
|
-
setStep("config"); // Skip provider selection
|
|
178
|
-
}
|
|
179
|
-
// Populate form fields
|
|
180
|
-
setName(subscription.name);
|
|
181
|
-
setDescription(subscription.description ?? "");
|
|
182
|
-
setProviderConfig(subscription.providerConfig);
|
|
183
|
-
setSelectedEventId(subscription.eventId);
|
|
184
|
-
// Set connection ID from config
|
|
185
|
-
const connId = subscription.providerConfig.connectionId;
|
|
186
|
-
if (typeof connId === "string") {
|
|
187
|
-
setSelectedConnectionId(connId);
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
}, [open, subscription, providers]);
|
|
191
|
-
|
|
192
|
-
// Reset when dialog closes (only in create mode)
|
|
193
|
-
useEffect(() => {
|
|
194
|
-
if (!open && !subscription) {
|
|
195
|
-
setStep("provider");
|
|
196
|
-
setSelectedProvider(undefined);
|
|
197
|
-
setName("");
|
|
198
|
-
setDescription("");
|
|
199
|
-
setProviderConfig({});
|
|
200
|
-
setSelectedEventId("");
|
|
201
|
-
setSelectedConnectionId("");
|
|
202
|
-
setDeleteDialogOpen(false);
|
|
203
|
-
setProviderConfigValid(false);
|
|
204
|
-
}
|
|
205
|
-
}, [open, subscription]);
|
|
206
|
-
|
|
207
|
-
// For providers with custom config components or no configSchema,
|
|
208
|
-
// DynamicForm won't report validity, so assume valid
|
|
209
|
-
useEffect(() => {
|
|
210
|
-
if (!selectedProvider) return;
|
|
211
|
-
|
|
212
|
-
const hasCustomConfig = getProviderConfigExtension(
|
|
213
|
-
selectedProvider.qualifiedId,
|
|
214
|
-
);
|
|
215
|
-
const hasNoSchema =
|
|
216
|
-
!selectedProvider.configSchema ||
|
|
217
|
-
!selectedProvider.configSchema.properties ||
|
|
218
|
-
Object.keys(selectedProvider.configSchema.properties).length === 0;
|
|
219
|
-
|
|
220
|
-
if (hasCustomConfig || hasNoSchema) {
|
|
221
|
-
setProviderConfigValid(true);
|
|
222
|
-
}
|
|
223
|
-
}, [selectedProvider]);
|
|
224
|
-
|
|
225
|
-
const handleProviderSelect = (provider: IntegrationProviderInfo) => {
|
|
226
|
-
setSelectedProvider(provider);
|
|
227
|
-
setStep("config");
|
|
228
|
-
};
|
|
229
|
-
|
|
230
|
-
// Handle update (edit mode)
|
|
231
|
-
const handleSave = () => {
|
|
232
|
-
if (!subscription || !selectedProvider) return;
|
|
233
|
-
|
|
234
|
-
setSaving(true);
|
|
235
|
-
// Include connectionId in providerConfig for providers with connections
|
|
236
|
-
const configWithConnection = selectedProvider.hasConnectionSchema
|
|
237
|
-
? { ...providerConfig, connectionId: selectedConnectionId }
|
|
238
|
-
: providerConfig;
|
|
239
|
-
|
|
240
|
-
updateMutation.mutate({
|
|
241
|
-
id: subscription.id,
|
|
242
|
-
updates: {
|
|
243
|
-
name,
|
|
244
|
-
description: description || undefined,
|
|
245
|
-
providerConfig: configWithConnection,
|
|
246
|
-
eventId:
|
|
247
|
-
selectedEventId === subscription.eventId
|
|
248
|
-
? undefined
|
|
249
|
-
: selectedEventId,
|
|
250
|
-
},
|
|
251
|
-
});
|
|
252
|
-
};
|
|
253
|
-
|
|
254
|
-
// Handle delete
|
|
255
|
-
const handleDelete = () => {
|
|
256
|
-
if (!subscription) return;
|
|
257
|
-
deleteMutation.mutate({ id: subscription.id });
|
|
258
|
-
};
|
|
259
|
-
|
|
260
|
-
const handleCreate = () => {
|
|
261
|
-
if (!selectedProvider) return;
|
|
262
|
-
|
|
263
|
-
// For providers with connections, require a connection to be selected
|
|
264
|
-
if (selectedProvider.hasConnectionSchema && !selectedConnectionId) {
|
|
265
|
-
toast.error("Please select a connection");
|
|
266
|
-
return;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
setSaving(true);
|
|
270
|
-
// Include connectionId in providerConfig for providers with connections
|
|
271
|
-
const configWithConnection = selectedProvider.hasConnectionSchema
|
|
272
|
-
? { ...providerConfig, connectionId: selectedConnectionId }
|
|
273
|
-
: providerConfig;
|
|
274
|
-
|
|
275
|
-
createMutation.mutate({
|
|
276
|
-
name,
|
|
277
|
-
description: description || undefined,
|
|
278
|
-
providerId: selectedProvider.qualifiedId,
|
|
279
|
-
providerConfig: configWithConnection,
|
|
280
|
-
eventId: selectedEventId,
|
|
281
|
-
});
|
|
282
|
-
};
|
|
283
|
-
|
|
284
|
-
// Mutation for fetching dynamic dropdown options (called at component level)
|
|
285
|
-
const getOptionsMutation = client.getConnectionOptions.useMutation();
|
|
286
|
-
|
|
287
|
-
// Create optionsResolvers for dynamic dropdown fields (x-options-resolver)
|
|
288
|
-
// Uses a Proxy to handle any resolver name dynamically
|
|
289
|
-
const optionsResolvers = useMemo(() => {
|
|
290
|
-
if (!selectedProvider || !selectedConnectionId) {
|
|
291
|
-
return;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
// Create a Proxy that handles any resolver name
|
|
295
|
-
return new Proxy(
|
|
296
|
-
{},
|
|
297
|
-
{
|
|
298
|
-
get: (_target, resolverName: string) => {
|
|
299
|
-
// Return a resolver function that uses mutateAsync from the hook defined above
|
|
300
|
-
return async (formValues: Record<string, unknown>) => {
|
|
301
|
-
try {
|
|
302
|
-
const result = await getOptionsMutation.mutateAsync({
|
|
303
|
-
providerId: selectedProvider.qualifiedId,
|
|
304
|
-
connectionId: selectedConnectionId,
|
|
305
|
-
resolverName,
|
|
306
|
-
context: formValues,
|
|
307
|
-
});
|
|
308
|
-
return result.map((opt) => ({
|
|
309
|
-
value: opt.value,
|
|
310
|
-
label: opt.label,
|
|
311
|
-
}));
|
|
312
|
-
} catch {
|
|
313
|
-
return [];
|
|
314
|
-
}
|
|
315
|
-
};
|
|
316
|
-
},
|
|
317
|
-
has: () => true, // All resolver names are valid
|
|
318
|
-
},
|
|
319
|
-
) as Record<
|
|
320
|
-
string,
|
|
321
|
-
(
|
|
322
|
-
formValues: Record<string, unknown>,
|
|
323
|
-
) => Promise<{ value: string; label: string }[]>
|
|
324
|
-
>;
|
|
325
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps -- getOptionsMutation is a TanStack mutation object that changes identity every render; including it would destroy the Proxy on each render, breaking in-flight requests
|
|
326
|
-
}, [selectedProvider, selectedConnectionId]);
|
|
327
|
-
|
|
328
|
-
return (
|
|
329
|
-
<>
|
|
330
|
-
<Dialog
|
|
331
|
-
open={open}
|
|
332
|
-
onOpenChange={(isOpen) => {
|
|
333
|
-
// Don't close the dialog if the delete confirmation is open
|
|
334
|
-
if (!isOpen && deleteDialogOpen) return;
|
|
335
|
-
onOpenChange(isOpen);
|
|
336
|
-
}}
|
|
337
|
-
>
|
|
338
|
-
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
|
|
339
|
-
<DialogHeader>
|
|
340
|
-
<DialogTitle>
|
|
341
|
-
{isEditMode
|
|
342
|
-
? `Edit ${selectedProvider?.displayName ?? "Subscription"}`
|
|
343
|
-
: step === "provider"
|
|
344
|
-
? "Select Provider"
|
|
345
|
-
: `Configure ${
|
|
346
|
-
selectedProvider?.displayName ?? "Subscription"
|
|
347
|
-
}`}
|
|
348
|
-
</DialogTitle>
|
|
349
|
-
<DialogDescription className="sr-only">
|
|
350
|
-
{isEditMode
|
|
351
|
-
? "Edit the settings for this integration subscription"
|
|
352
|
-
: step === "provider"
|
|
353
|
-
? "Choose a provider for your integration subscription"
|
|
354
|
-
: "Configure the subscription settings"}
|
|
355
|
-
</DialogDescription>
|
|
356
|
-
</DialogHeader>
|
|
357
|
-
|
|
358
|
-
{step === "provider" ? (
|
|
359
|
-
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 py-4">
|
|
360
|
-
{providers.length === 0 ? (
|
|
361
|
-
<div className="col-span-full text-center text-muted-foreground py-8">
|
|
362
|
-
No providers available. Install provider plugins to enable
|
|
363
|
-
webhook delivery.
|
|
364
|
-
</div>
|
|
365
|
-
) : (
|
|
366
|
-
providers.map((provider) => (
|
|
367
|
-
<button
|
|
368
|
-
key={provider.qualifiedId}
|
|
369
|
-
onClick={() => handleProviderSelect(provider)}
|
|
370
|
-
className="flex items-center gap-4 p-4 border rounded-lg hover:bg-muted transition-colors text-left"
|
|
371
|
-
>
|
|
372
|
-
<div className="p-3 rounded-lg bg-muted">
|
|
373
|
-
<DynamicIcon
|
|
374
|
-
name={(provider.icon ?? "Webhook") as LucideIconName}
|
|
375
|
-
className="h-6 w-6"
|
|
376
|
-
/>
|
|
377
|
-
</div>
|
|
378
|
-
<div>
|
|
379
|
-
<div className="font-medium">{provider.displayName}</div>
|
|
380
|
-
{provider.description && (
|
|
381
|
-
<div className="text-sm text-muted-foreground">
|
|
382
|
-
{provider.description}
|
|
383
|
-
</div>
|
|
384
|
-
)}
|
|
385
|
-
</div>
|
|
386
|
-
</button>
|
|
387
|
-
))
|
|
388
|
-
)}
|
|
389
|
-
</div>
|
|
390
|
-
) : (
|
|
391
|
-
<div className="space-y-6 py-4">
|
|
392
|
-
{/* Basic Info */}
|
|
393
|
-
<div className="space-y-4">
|
|
394
|
-
<div>
|
|
395
|
-
<label className="block text-sm font-medium mb-1">
|
|
396
|
-
Name <span className="text-destructive">*</span>
|
|
397
|
-
</label>
|
|
398
|
-
<Input
|
|
399
|
-
type="text"
|
|
400
|
-
value={name}
|
|
401
|
-
onChange={(e) => setName(e.target.value)}
|
|
402
|
-
placeholder="My Webhook"
|
|
403
|
-
/>
|
|
404
|
-
</div>
|
|
405
|
-
<div>
|
|
406
|
-
<label className="block text-sm font-medium mb-1">
|
|
407
|
-
Description
|
|
408
|
-
</label>
|
|
409
|
-
<Textarea
|
|
410
|
-
value={description}
|
|
411
|
-
onChange={(e) => setDescription(e.target.value)}
|
|
412
|
-
placeholder="Optional description"
|
|
413
|
-
rows={2}
|
|
414
|
-
/>
|
|
415
|
-
</div>
|
|
416
|
-
</div>
|
|
417
|
-
|
|
418
|
-
{/* Event Selection (required) */}
|
|
419
|
-
<div>
|
|
420
|
-
<Label className="mb-2">
|
|
421
|
-
Event <span className="text-destructive">*</span>
|
|
422
|
-
</Label>
|
|
423
|
-
<Select
|
|
424
|
-
value={selectedEventId}
|
|
425
|
-
onValueChange={setSelectedEventId}
|
|
426
|
-
>
|
|
427
|
-
<SelectTrigger>
|
|
428
|
-
<SelectValue placeholder="Select an event" />
|
|
429
|
-
</SelectTrigger>
|
|
430
|
-
<SelectContent>
|
|
431
|
-
{events.map((event) => (
|
|
432
|
-
<SelectItem key={event.eventId} value={event.eventId}>
|
|
433
|
-
<div>
|
|
434
|
-
<div>{event.displayName}</div>
|
|
435
|
-
{event.description && (
|
|
436
|
-
<div className="text-xs text-muted-foreground">
|
|
437
|
-
{event.description}
|
|
438
|
-
</div>
|
|
439
|
-
)}
|
|
440
|
-
</div>
|
|
441
|
-
</SelectItem>
|
|
442
|
-
))}
|
|
443
|
-
</SelectContent>
|
|
444
|
-
</Select>
|
|
445
|
-
{events.length === 0 && (
|
|
446
|
-
<div className="text-muted-foreground text-sm mt-2">
|
|
447
|
-
No events registered. Plugins will register events.
|
|
448
|
-
</div>
|
|
449
|
-
)}
|
|
450
|
-
</div>
|
|
451
|
-
|
|
452
|
-
{/* Connection Selection (for providers with connectionSchema) */}
|
|
453
|
-
{selectedProvider?.hasConnectionSchema && (
|
|
454
|
-
<div>
|
|
455
|
-
<Label className="mb-2">
|
|
456
|
-
Connection <span className="text-destructive">*</span>
|
|
457
|
-
</Label>
|
|
458
|
-
{loadingConnections ? (
|
|
459
|
-
<div className="text-sm text-muted-foreground py-2">
|
|
460
|
-
Loading connections...
|
|
461
|
-
</div>
|
|
462
|
-
) : connections.length === 0 ? (
|
|
463
|
-
<div className="border rounded-md p-4 bg-muted/50">
|
|
464
|
-
<p className="text-sm text-muted-foreground mb-2">
|
|
465
|
-
No connections configured for this provider.
|
|
466
|
-
</p>
|
|
467
|
-
<Button variant="outline" size="sm" asChild>
|
|
468
|
-
<Link
|
|
469
|
-
to={resolveRoute(
|
|
470
|
-
integrationRoutes.routes.connections,
|
|
471
|
-
{
|
|
472
|
-
providerId: selectedProvider.qualifiedId,
|
|
473
|
-
},
|
|
474
|
-
)}
|
|
475
|
-
>
|
|
476
|
-
Configure Connections
|
|
477
|
-
</Link>
|
|
478
|
-
</Button>
|
|
479
|
-
</div>
|
|
480
|
-
) : (
|
|
481
|
-
<Select
|
|
482
|
-
value={selectedConnectionId}
|
|
483
|
-
onValueChange={setSelectedConnectionId}
|
|
484
|
-
>
|
|
485
|
-
<SelectTrigger>
|
|
486
|
-
<SelectValue placeholder="Select a connection" />
|
|
487
|
-
</SelectTrigger>
|
|
488
|
-
<SelectContent>
|
|
489
|
-
{connections.map((conn) => (
|
|
490
|
-
<SelectItem key={conn.id} value={conn.id}>
|
|
491
|
-
{conn.name}
|
|
492
|
-
</SelectItem>
|
|
493
|
-
))}
|
|
494
|
-
</SelectContent>
|
|
495
|
-
</Select>
|
|
496
|
-
)}
|
|
497
|
-
</div>
|
|
498
|
-
)}
|
|
499
|
-
|
|
500
|
-
{/* Provider Config - only show when event is selected */}
|
|
501
|
-
{selectedProvider &&
|
|
502
|
-
(() => {
|
|
503
|
-
// Check if provider has a custom config component
|
|
504
|
-
const extension = getProviderConfigExtension(
|
|
505
|
-
selectedProvider.qualifiedId,
|
|
506
|
-
);
|
|
507
|
-
|
|
508
|
-
// Require event selection before showing config with template support
|
|
509
|
-
if (!selectedEventId) {
|
|
510
|
-
// Only show message if provider has configuration options
|
|
511
|
-
if (extension || selectedProvider.configSchema) {
|
|
512
|
-
return (
|
|
513
|
-
<div className="border rounded-md p-4 bg-muted/50">
|
|
514
|
-
<p className="text-sm text-muted-foreground">
|
|
515
|
-
Select an event above to configure provider options
|
|
516
|
-
with template support.
|
|
517
|
-
</p>
|
|
518
|
-
</div>
|
|
519
|
-
);
|
|
520
|
-
}
|
|
521
|
-
return <></>;
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
if (extension) {
|
|
525
|
-
// Render custom component
|
|
526
|
-
const CustomConfig = extension.ConfigComponent;
|
|
527
|
-
return (
|
|
528
|
-
<div>
|
|
529
|
-
<label className="block text-sm font-medium mb-2">
|
|
530
|
-
Provider Configuration
|
|
531
|
-
</label>
|
|
532
|
-
<div className="border rounded-md p-4">
|
|
533
|
-
<CustomConfig
|
|
534
|
-
value={providerConfig}
|
|
535
|
-
onChange={setProviderConfig}
|
|
536
|
-
isSubmitting={saving}
|
|
537
|
-
/>
|
|
538
|
-
</div>
|
|
539
|
-
</div>
|
|
540
|
-
);
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
// Fall back to DynamicForm for providers without custom component
|
|
544
|
-
if (selectedProvider.configSchema) {
|
|
545
|
-
return (
|
|
546
|
-
<div>
|
|
547
|
-
<label className="block text-sm font-medium mb-2">
|
|
548
|
-
Provider Configuration
|
|
549
|
-
</label>
|
|
550
|
-
<div className="border rounded-md p-4">
|
|
551
|
-
<DynamicForm
|
|
552
|
-
schema={selectedProvider.configSchema}
|
|
553
|
-
value={providerConfig}
|
|
554
|
-
onChange={setProviderConfig}
|
|
555
|
-
onValidChange={setProviderConfigValid}
|
|
556
|
-
optionsResolvers={optionsResolvers}
|
|
557
|
-
templateProperties={payloadProperties}
|
|
558
|
-
typeDefinitions={editorScriptContext.typeDefinitions}
|
|
559
|
-
shellEnvVars={editorScriptContext.shellEnvVars}
|
|
560
|
-
starterTemplates={
|
|
561
|
-
editorScriptContext.starterTemplates
|
|
562
|
-
}
|
|
563
|
-
/>
|
|
564
|
-
</div>
|
|
565
|
-
</div>
|
|
566
|
-
);
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
return <></>;
|
|
570
|
-
})()}
|
|
571
|
-
|
|
572
|
-
{/* Provider Documentation */}
|
|
573
|
-
{selectedProvider && (
|
|
574
|
-
<ProviderDocumentation provider={selectedProvider} />
|
|
575
|
-
)}
|
|
576
|
-
</div>
|
|
577
|
-
)}
|
|
578
|
-
|
|
579
|
-
<DialogFooter className="flex-col sm:flex-row gap-2">
|
|
580
|
-
{/* Left side: Delete and View Logs in edit mode */}
|
|
581
|
-
{isEditMode && (
|
|
582
|
-
<div className="flex gap-2 mr-auto">
|
|
583
|
-
<Button
|
|
584
|
-
variant="destructive"
|
|
585
|
-
onClick={() => setDeleteDialogOpen(true)}
|
|
586
|
-
>
|
|
587
|
-
<Trash2 className="h-4 w-4 mr-2" />
|
|
588
|
-
Delete
|
|
589
|
-
</Button>
|
|
590
|
-
<Link
|
|
591
|
-
to={resolveRoute(integrationRoutes.routes.deliveryLogs, {
|
|
592
|
-
subscriptionId: subscription.id,
|
|
593
|
-
})}
|
|
594
|
-
>
|
|
595
|
-
<Button variant="outline">
|
|
596
|
-
<ScrollText className="h-4 w-4 mr-2" />
|
|
597
|
-
View Logs
|
|
598
|
-
</Button>
|
|
599
|
-
</Link>
|
|
600
|
-
</div>
|
|
601
|
-
)}
|
|
602
|
-
|
|
603
|
-
{/* Right side: Cancel, Back, Create/Save */}
|
|
604
|
-
{step === "config" && !isEditMode && (
|
|
605
|
-
<Button variant="outline" onClick={() => setStep("provider")}>
|
|
606
|
-
Back
|
|
607
|
-
</Button>
|
|
608
|
-
)}
|
|
609
|
-
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
|
610
|
-
Cancel
|
|
611
|
-
</Button>
|
|
612
|
-
{step === "config" && (
|
|
613
|
-
<Button
|
|
614
|
-
onClick={() =>
|
|
615
|
-
void (isEditMode ? handleSave() : handleCreate())
|
|
616
|
-
}
|
|
617
|
-
disabled={
|
|
618
|
-
!name.trim() ||
|
|
619
|
-
!selectedEventId ||
|
|
620
|
-
!providerConfigValid ||
|
|
621
|
-
saving
|
|
622
|
-
}
|
|
623
|
-
>
|
|
624
|
-
{saving
|
|
625
|
-
? isEditMode
|
|
626
|
-
? "Saving..."
|
|
627
|
-
: "Creating..."
|
|
628
|
-
: isEditMode
|
|
629
|
-
? "Save Changes"
|
|
630
|
-
: "Create Subscription"}
|
|
631
|
-
</Button>
|
|
632
|
-
)}
|
|
633
|
-
</DialogFooter>
|
|
634
|
-
</DialogContent>
|
|
635
|
-
</Dialog>
|
|
636
|
-
|
|
637
|
-
{/* Delete Confirmation Modal - rendered outside Dialog to fix z-index */}
|
|
638
|
-
<ConfirmationModal
|
|
639
|
-
isOpen={deleteDialogOpen}
|
|
640
|
-
onClose={() => setDeleteDialogOpen(false)}
|
|
641
|
-
title="Delete Subscription"
|
|
642
|
-
message={`Are you sure you want to delete "${subscription?.name}"? This action cannot be undone.`}
|
|
643
|
-
confirmText="Delete"
|
|
644
|
-
variant="danger"
|
|
645
|
-
onConfirm={() => void handleDelete()}
|
|
646
|
-
/>
|
|
647
|
-
</>
|
|
648
|
-
);
|
|
649
|
-
};
|
|
650
|
-
|
|
651
|
-
// Export with original name for backwards compatibility
|
|
652
|
-
export const CreateSubscriptionDialog = SubscriptionDialog;
|