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