@tangle-network/sandbox-ui 0.116.7 → 0.116.9
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/README.md +3 -0
- package/dist/{WorkflowGraph-B-NsdYz_.js → WorkflowGraph-CzWs8P7y.js} +1 -1
- package/dist/{dashboard-_-b7laAR.js → dashboard-DwdEQoqK.js} +460 -384
- package/dist/dashboard.d.ts +1 -1
- package/dist/dashboard.js +1 -1
- package/dist/globals.css +56 -33
- package/dist/{index-D82GWl1N.d.ts → index-CGMpKgB8.d.ts} +6 -3
- package/dist/{index-D_G-wF6u.d.ts → index-z_zNuZHm.d.ts} +4 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/integrations.d.ts +198 -55
- package/dist/integrations.js +1026 -406
- package/dist/{provider-logo-DjqmConF.js → provider-logo-BiuiO_c2.js} +2 -1
- package/dist/styles.css +56 -33
- package/dist/{workbench-DknSt1S8.js → workbench-DXTqeu9h.js} +17 -3
- package/dist/workbench.d.ts +1 -1
- package/dist/workbench.js +1 -1
- package/dist/workflows.js +1 -1
- package/package.json +1 -1
package/dist/integrations.js
CHANGED
|
@@ -1,9 +1,363 @@
|
|
|
1
|
-
import { n as normalizeProviderId, r as providerLogoCandidates, t as ProviderIcon } from "./provider-logo-
|
|
2
|
-
import { Button,
|
|
3
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { n as normalizeProviderId, r as providerLogoCandidates, t as ProviderIcon } from "./provider-logo-BiuiO_c2.js";
|
|
2
|
+
import { Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, EmptyState } from "@tangle-network/ui/primitives";
|
|
3
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
4
4
|
import * as React$1 from "react";
|
|
5
5
|
import { cn, focusFieldWithin, focusRing } from "@tangle-network/ui/utils";
|
|
6
6
|
import { Check, ExternalLink, MoreVertical, Search, Unplug } from "lucide-react";
|
|
7
|
+
//#region src/integrations/integrations-catalog.tsx
|
|
8
|
+
const TONES = {
|
|
9
|
+
neutral: "text-muted-foreground",
|
|
10
|
+
success: "text-[var(--surface-success-text)]",
|
|
11
|
+
warning: "text-[var(--surface-warning-text)]",
|
|
12
|
+
error: "text-destructive"
|
|
13
|
+
};
|
|
14
|
+
function catalogRowActive(row) {
|
|
15
|
+
return row.kind === "app" ? row.installedCount > 0 : row.connections.length > 0;
|
|
16
|
+
}
|
|
17
|
+
/** A controlled selector shared by the catalog and detail; never picks an account. */
|
|
18
|
+
function ConnectionSelector({ connections, value, onChange, label }) {
|
|
19
|
+
const selected = connections.some((connection) => connection.id === value) ? value : "";
|
|
20
|
+
return /* @__PURE__ */ jsxs("select", {
|
|
21
|
+
"aria-label": label,
|
|
22
|
+
value: selected ?? "",
|
|
23
|
+
onChange: (event) => onChange(event.target.value || null),
|
|
24
|
+
className: cn("h-8 w-full min-w-0 truncate rounded-md border border-border bg-background px-2 text-sm text-foreground", focusRing),
|
|
25
|
+
children: [/* @__PURE__ */ jsx("option", {
|
|
26
|
+
value: "",
|
|
27
|
+
children: "Select an account"
|
|
28
|
+
}), connections.map((connection) => /* @__PURE__ */ jsx("option", {
|
|
29
|
+
value: connection.id,
|
|
30
|
+
children: connection.accountDisplay ? `${connection.accountDisplay} · ${connection.id}` : connection.id
|
|
31
|
+
}, connection.id))]
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
function ConnectionMenu({ row, connection, onManage, onDisconnect, compact }) {
|
|
35
|
+
const manage = connection.capabilities.manage && (connection.manageHref || onManage);
|
|
36
|
+
const disconnect = connection.capabilities.disconnect && onDisconnect;
|
|
37
|
+
const actions = connection.actions ?? [];
|
|
38
|
+
if (!manage && !disconnect && actions.length === 0) return null;
|
|
39
|
+
return /* @__PURE__ */ jsxs(DropdownMenu, { children: [/* @__PURE__ */ jsx(DropdownMenuTrigger, {
|
|
40
|
+
asChild: true,
|
|
41
|
+
children: /* @__PURE__ */ jsx("button", {
|
|
42
|
+
type: "button",
|
|
43
|
+
"data-testid": `menu-${row.providerId}`,
|
|
44
|
+
"aria-label": `More actions for ${row.title}`,
|
|
45
|
+
title: "More actions",
|
|
46
|
+
className: cn("relative z-20 flex shrink-0 items-center justify-center rounded-md transition-colors", compact ? "h-6 w-6 text-muted-foreground/70 hover:bg-background hover:text-foreground focus-visible:bg-background focus-visible:text-foreground" : "h-7 w-7 text-muted-foreground hover:bg-accent hover:text-foreground", focusRing),
|
|
47
|
+
children: /* @__PURE__ */ jsx(MoreVertical, { className: compact ? "h-3.5 w-3.5" : "h-4 w-4" })
|
|
48
|
+
})
|
|
49
|
+
}), /* @__PURE__ */ jsxs(DropdownMenuContent, {
|
|
50
|
+
side: "right",
|
|
51
|
+
align: "start",
|
|
52
|
+
className: "min-w-[160px] rounded-md",
|
|
53
|
+
children: [
|
|
54
|
+
manage && (connection.manageHref ? /* @__PURE__ */ jsx(DropdownMenuItem, {
|
|
55
|
+
asChild: true,
|
|
56
|
+
children: /* @__PURE__ */ jsxs("a", {
|
|
57
|
+
href: connection.manageHref,
|
|
58
|
+
target: connection.manageInNewWindow ? "_blank" : void 0,
|
|
59
|
+
rel: connection.manageInNewWindow ? "noopener noreferrer" : void 0,
|
|
60
|
+
"data-testid": `manage-item-${row.providerId}`,
|
|
61
|
+
children: [/* @__PURE__ */ jsx(ExternalLink, { className: "mr-2 h-4 w-4" }), " Manage"]
|
|
62
|
+
})
|
|
63
|
+
}) : /* @__PURE__ */ jsxs(DropdownMenuItem, {
|
|
64
|
+
"data-testid": `manage-item-${row.providerId}`,
|
|
65
|
+
onSelect: () => onManage?.(connection, row),
|
|
66
|
+
children: [/* @__PURE__ */ jsx(ExternalLink, { className: "mr-2 h-4 w-4" }), " Manage"]
|
|
67
|
+
})),
|
|
68
|
+
actions.map((action) => /* @__PURE__ */ jsx(DropdownMenuItem, {
|
|
69
|
+
disabled: action.disabled,
|
|
70
|
+
onSelect: action.onSelect,
|
|
71
|
+
children: action.label
|
|
72
|
+
}, action.id)),
|
|
73
|
+
disconnect && (manage || actions.length > 0) ? /* @__PURE__ */ jsx(DropdownMenuSeparator, {}) : null,
|
|
74
|
+
disconnect ? /* @__PURE__ */ jsxs(DropdownMenuItem, {
|
|
75
|
+
"data-testid": `disconnect-${row.providerId}`,
|
|
76
|
+
className: "text-destructive focus:text-destructive",
|
|
77
|
+
onSelect: () => onDisconnect?.(connection, row),
|
|
78
|
+
children: [/* @__PURE__ */ jsx(Unplug, { className: "mr-2 h-4 w-4" }), " Disconnect"]
|
|
79
|
+
}) : null
|
|
80
|
+
]
|
|
81
|
+
})] });
|
|
82
|
+
}
|
|
83
|
+
/** Platform's catalog presentation, with app-owned state, destinations and effects. */
|
|
84
|
+
function IntegrationsCatalog({ rows, query, onQueryChange, categoryFilter = "", onCategoryFilterChange, sort, onSortChange, onSelectConnection, onConnect, onManage, onDisconnect, onRequestIntegration, onRetry, loading = false, error, actionError, connectError, busyProviderId, reserveConnectionContext = false, skeletonCount, emptyCatalogLabel = "No integrations are available to connect yet.", title, description, layout = "cards", className }) {
|
|
85
|
+
const compact = layout === "tiles";
|
|
86
|
+
const categories = [...new Set(rows.flatMap((row) => row.category ? [row.category] : []))].sort();
|
|
87
|
+
const q = query.trim().toLowerCase();
|
|
88
|
+
const filtered = rows.filter((row) => (!categoryFilter || row.category === categoryFilter) && (!q || `${row.title} ${row.providerId} ${row.description ?? ""}`.toLowerCase().includes(q)));
|
|
89
|
+
const hiddenActive = rows.filter((row) => catalogRowActive(row) && !filtered.includes(row)).length;
|
|
90
|
+
const state = error ? "error" : loading && rows.length === 0 ? "loading" : rows.length === 0 ? "empty" : "loaded";
|
|
91
|
+
const gridClass = compact ? "grid grid-cols-3 gap-3 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-6" : "grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-3";
|
|
92
|
+
const boxClass = cn(compact ? "flex min-w-0 aspect-square flex-col items-center justify-center gap-2 rounded-xl border p-3 text-center" : "flex min-w-0 min-h-[200px] flex-col gap-3 rounded-xl border p-4 text-left", compact && reserveConnectionContext && "w-full min-h-[136px]");
|
|
93
|
+
const headerClass = cn("flex w-full min-w-0 gap-2", compact ? "flex-col items-center" : "items-start gap-3");
|
|
94
|
+
const clearFilters = () => {
|
|
95
|
+
onQueryChange("");
|
|
96
|
+
onCategoryFilterChange?.("");
|
|
97
|
+
};
|
|
98
|
+
return /* @__PURE__ */ jsxs("section", {
|
|
99
|
+
className: cn("w-full min-w-0 space-y-4", className),
|
|
100
|
+
"aria-label": title ?? "Integrations",
|
|
101
|
+
"aria-busy": loading,
|
|
102
|
+
"data-testid": state === "loading" ? "integrations-skeleton" : "integrations-panel",
|
|
103
|
+
"data-catalog-state": state,
|
|
104
|
+
children: [
|
|
105
|
+
title ? /* @__PURE__ */ jsxs("header", { children: [/* @__PURE__ */ jsx("h2", {
|
|
106
|
+
className: "text-lg font-semibold",
|
|
107
|
+
children: title
|
|
108
|
+
}), description ? /* @__PURE__ */ jsx("p", {
|
|
109
|
+
className: "mt-1 text-sm text-muted-foreground",
|
|
110
|
+
children: description
|
|
111
|
+
}) : null] }) : null,
|
|
112
|
+
/* @__PURE__ */ jsxs("div", {
|
|
113
|
+
className: "flex min-w-0 flex-col gap-2 sm:flex-row sm:flex-wrap sm:items-center sm:justify-between",
|
|
114
|
+
children: [
|
|
115
|
+
/* @__PURE__ */ jsxs("div", {
|
|
116
|
+
className: cn("flex min-w-0 flex-1 items-center gap-2 rounded-lg border bg-card px-3 py-2", focusFieldWithin),
|
|
117
|
+
children: [/* @__PURE__ */ jsx(Search, { className: "h-4 w-4 shrink-0 text-muted-foreground" }), /* @__PURE__ */ jsx("input", {
|
|
118
|
+
type: "text",
|
|
119
|
+
value: query,
|
|
120
|
+
onChange: (event) => onQueryChange(event.target.value),
|
|
121
|
+
"aria-label": "Search integrations",
|
|
122
|
+
placeholder: "Search integrations...",
|
|
123
|
+
autoFocus: compact,
|
|
124
|
+
"data-testid": "integration-search",
|
|
125
|
+
className: "min-w-0 flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground"
|
|
126
|
+
})]
|
|
127
|
+
}),
|
|
128
|
+
onCategoryFilterChange ? /* @__PURE__ */ jsxs("select", {
|
|
129
|
+
"aria-label": "Filter by category",
|
|
130
|
+
value: categoryFilter,
|
|
131
|
+
onChange: (event) => onCategoryFilterChange(event.target.value),
|
|
132
|
+
className: cn("h-10 min-w-0 rounded-lg border bg-card px-3 text-sm", focusRing),
|
|
133
|
+
children: [
|
|
134
|
+
/* @__PURE__ */ jsx("option", {
|
|
135
|
+
value: "",
|
|
136
|
+
children: "All categories"
|
|
137
|
+
}),
|
|
138
|
+
categoryFilter && !categories.includes(categoryFilter) ? /* @__PURE__ */ jsx("option", {
|
|
139
|
+
value: categoryFilter,
|
|
140
|
+
children: categoryFilter
|
|
141
|
+
}) : null,
|
|
142
|
+
categories.map((category) => /* @__PURE__ */ jsx("option", {
|
|
143
|
+
value: category,
|
|
144
|
+
children: category
|
|
145
|
+
}, category))
|
|
146
|
+
]
|
|
147
|
+
}) : null,
|
|
148
|
+
sort && onSortChange ? /* @__PURE__ */ jsx("div", {
|
|
149
|
+
role: "group",
|
|
150
|
+
"aria-label": "Sort integrations",
|
|
151
|
+
className: "flex shrink-0 items-center gap-1 rounded-lg border border-border bg-card p-1",
|
|
152
|
+
children: [["featured", "Featured"], ["alpha", "A–Z"]].map(([value, label]) => /* @__PURE__ */ jsx("button", {
|
|
153
|
+
type: "button",
|
|
154
|
+
"aria-pressed": sort === value,
|
|
155
|
+
onClick: () => onSortChange(value),
|
|
156
|
+
"data-testid": `sort-${value}`,
|
|
157
|
+
className: cn("rounded-md px-2.5 py-1 text-xs font-medium transition-colors", focusRing, sort === value ? "bg-accent text-foreground" : "text-muted-foreground hover:text-foreground"),
|
|
158
|
+
children: label
|
|
159
|
+
}, value))
|
|
160
|
+
}) : null,
|
|
161
|
+
onRequestIntegration ? /* @__PURE__ */ jsx(Button, {
|
|
162
|
+
variant: "outline",
|
|
163
|
+
onClick: () => onRequestIntegration(query.trim()),
|
|
164
|
+
children: "Request an integration"
|
|
165
|
+
}) : null
|
|
166
|
+
]
|
|
167
|
+
}),
|
|
168
|
+
actionError ? /* @__PURE__ */ jsx("p", {
|
|
169
|
+
role: "alert",
|
|
170
|
+
className: "text-sm text-destructive",
|
|
171
|
+
children: actionError
|
|
172
|
+
}) : null,
|
|
173
|
+
connectError ? /* @__PURE__ */ jsx("p", {
|
|
174
|
+
role: "alert",
|
|
175
|
+
"data-testid": "integration-connect-error",
|
|
176
|
+
className: "rounded-lg border border-destructive/40 bg-destructive/10 px-3 py-2 text-sm text-destructive",
|
|
177
|
+
children: connectError
|
|
178
|
+
}) : null,
|
|
179
|
+
(q || categoryFilter) && state === "loaded" ? /* @__PURE__ */ jsxs("span", {
|
|
180
|
+
role: "status",
|
|
181
|
+
className: "sr-only",
|
|
182
|
+
children: [filtered.length, " matching integrations"]
|
|
183
|
+
}) : null,
|
|
184
|
+
/* @__PURE__ */ jsx("div", {
|
|
185
|
+
className: gridClass,
|
|
186
|
+
"data-testid": "integrations-catalog-grid",
|
|
187
|
+
children: state === "loading" ? Array.from({ length: skeletonCount ?? (compact ? 12 : 6) }, (_, i) => /* @__PURE__ */ jsxs("div", {
|
|
188
|
+
"aria-hidden": "true",
|
|
189
|
+
className: cn(boxClass, "animate-pulse border-border bg-muted/40"),
|
|
190
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
191
|
+
className: headerClass,
|
|
192
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
193
|
+
className: "shrink-0 rounded-2xl bg-muted",
|
|
194
|
+
style: {
|
|
195
|
+
width: 48,
|
|
196
|
+
height: 48
|
|
197
|
+
}
|
|
198
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
199
|
+
className: "flex h-8 w-full items-center justify-center",
|
|
200
|
+
children: /* @__PURE__ */ jsx("div", { className: "h-2.5 w-3/4 rounded bg-muted" })
|
|
201
|
+
})]
|
|
202
|
+
}), !compact ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("div", { className: "h-8 w-full rounded bg-muted" }), /* @__PURE__ */ jsx("div", { className: "mt-auto h-9 border-t border-border" })] }) : null]
|
|
203
|
+
}, i)) : state === "error" ? /* @__PURE__ */ jsxs("div", {
|
|
204
|
+
role: "alert",
|
|
205
|
+
className: "col-span-full flex min-h-[200px] min-w-0 flex-col items-center justify-center gap-3 rounded-xl border border-destructive/40 bg-card p-6",
|
|
206
|
+
children: [/* @__PURE__ */ jsx("p", {
|
|
207
|
+
className: "break-words text-sm text-destructive",
|
|
208
|
+
children: error
|
|
209
|
+
}), onRetry ? /* @__PURE__ */ jsx(Button, {
|
|
210
|
+
variant: "outline",
|
|
211
|
+
onClick: onRetry,
|
|
212
|
+
children: "Retry"
|
|
213
|
+
}) : null]
|
|
214
|
+
}) : filtered.length === 0 ? /* @__PURE__ */ jsxs("div", {
|
|
215
|
+
className: "col-span-full min-h-[200px] min-w-0 rounded-xl border border-border bg-card p-4",
|
|
216
|
+
children: [/* @__PURE__ */ jsx(EmptyState, {
|
|
217
|
+
title: state === "empty" ? "No integrations" : "No matches",
|
|
218
|
+
description: state === "empty" ? emptyCatalogLabel : `No integrations match "${query.trim()}".`
|
|
219
|
+
}), q || categoryFilter ? /* @__PURE__ */ jsxs("div", {
|
|
220
|
+
className: "flex flex-col items-center gap-2",
|
|
221
|
+
children: [hiddenActive > 0 ? /* @__PURE__ */ jsxs("p", {
|
|
222
|
+
className: "text-sm text-muted-foreground",
|
|
223
|
+
children: [hiddenActive, " active integrations are hidden by your filters."]
|
|
224
|
+
}) : null, /* @__PURE__ */ jsx(Button, {
|
|
225
|
+
variant: "outline",
|
|
226
|
+
onClick: clearFilters,
|
|
227
|
+
children: "Clear filters"
|
|
228
|
+
})]
|
|
229
|
+
}) : null]
|
|
230
|
+
}) : filtered.map((row) => {
|
|
231
|
+
const provider = row.kind === "provider" ? row : void 0;
|
|
232
|
+
const connection = provider?.connections.find((item) => item.id === provider.selectedConnectionId);
|
|
233
|
+
const active = catalogRowActive(row);
|
|
234
|
+
const canConnect = provider?.canConnect && onConnect;
|
|
235
|
+
const connecting = busyProviderId === row.providerId;
|
|
236
|
+
const wholeButton = !active && !!canConnect && !row.to;
|
|
237
|
+
const Box = wholeButton ? "button" : "div";
|
|
238
|
+
const manageHref = connection?.capabilities.manage ? connection.manageHref : void 0;
|
|
239
|
+
const header = /* @__PURE__ */ jsxs("div", {
|
|
240
|
+
className: headerClass,
|
|
241
|
+
children: [/* @__PURE__ */ jsx(ProviderIcon, {
|
|
242
|
+
id: row.kind === "app" ? row.logoProviderId : row.providerId,
|
|
243
|
+
iconUrl: row.iconUrl,
|
|
244
|
+
displayName: row.title,
|
|
245
|
+
size: 48,
|
|
246
|
+
className: "rounded-2xl"
|
|
247
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
248
|
+
className: "flex h-8 w-full min-w-0 flex-col justify-center leading-4",
|
|
249
|
+
children: [row.to ? /* @__PURE__ */ jsx("a", {
|
|
250
|
+
href: row.to,
|
|
251
|
+
className: cn("relative z-20 truncate text-sm font-medium hover:underline", focusRing),
|
|
252
|
+
children: row.title
|
|
253
|
+
}) : /* @__PURE__ */ jsx("span", {
|
|
254
|
+
className: cn("w-full font-medium text-foreground", compact ? "text-xs" : "text-sm", connection?.accountDisplay ? "truncate" : "line-clamp-2"),
|
|
255
|
+
children: row.title
|
|
256
|
+
}), connection?.accountDisplay ? /* @__PURE__ */ jsx("span", {
|
|
257
|
+
className: "w-full truncate text-[11px] leading-4 text-muted-foreground",
|
|
258
|
+
"data-testid": `account-${row.providerId}`,
|
|
259
|
+
children: connection.accountDisplay
|
|
260
|
+
}) : null]
|
|
261
|
+
})]
|
|
262
|
+
});
|
|
263
|
+
return /* @__PURE__ */ jsxs(Box, {
|
|
264
|
+
type: wholeButton ? "button" : void 0,
|
|
265
|
+
"data-testid": `integration-${row.providerId}`,
|
|
266
|
+
"data-connected": active ? "true" : "false",
|
|
267
|
+
"data-connecting": connecting ? "true" : void 0,
|
|
268
|
+
"aria-busy": connecting || void 0,
|
|
269
|
+
disabled: wholeButton ? connecting : void 0,
|
|
270
|
+
onClick: wholeButton ? () => provider && onConnect?.(provider) : void 0,
|
|
271
|
+
title: connection?.accountDisplay ? `${row.title} — ${connection.accountDisplay}` : row.description ?? row.title,
|
|
272
|
+
className: cn(boxClass, "group relative", compact && active ? "border-[var(--surface-success-border)] bg-[var(--surface-success-bg)]" : "border-border bg-card", wholeButton && cn("transition-all hover:border-[var(--border-strong)] hover:bg-accent/40 hover:shadow-sm", focusRing), connection?.detail && compact && "w-full min-h-[136px]"),
|
|
273
|
+
children: [
|
|
274
|
+
compact && manageHref ? /* @__PURE__ */ jsx("a", {
|
|
275
|
+
href: manageHref,
|
|
276
|
+
target: connection?.manageInNewWindow ? "_blank" : void 0,
|
|
277
|
+
rel: connection?.manageInNewWindow ? "noopener noreferrer" : void 0,
|
|
278
|
+
"aria-label": `Manage ${row.title}${connection?.manageInNewWindow ? " (opens in new window)" : ""}`,
|
|
279
|
+
"data-testid": `manage-${row.providerId}`,
|
|
280
|
+
className: cn("absolute inset-0 z-10 rounded-xl", focusRing)
|
|
281
|
+
}) : null,
|
|
282
|
+
compact && active ? /* @__PURE__ */ jsx("span", {
|
|
283
|
+
className: "pointer-events-none absolute left-2 top-2 flex h-4 w-4 items-center justify-center rounded-full bg-[var(--surface-success-text)] text-white",
|
|
284
|
+
children: /* @__PURE__ */ jsx(Check, {
|
|
285
|
+
className: "h-3 w-3",
|
|
286
|
+
strokeWidth: 3
|
|
287
|
+
})
|
|
288
|
+
}) : null,
|
|
289
|
+
header,
|
|
290
|
+
provider && (provider.connections.length > 1 || active && !connection) ? /* @__PURE__ */ jsx("div", {
|
|
291
|
+
className: "relative z-20 w-full min-w-0",
|
|
292
|
+
children: /* @__PURE__ */ jsx(ConnectionSelector, {
|
|
293
|
+
connections: provider.connections,
|
|
294
|
+
value: provider.selectedConnectionId,
|
|
295
|
+
onChange: (id) => onSelectConnection(row.providerId, id),
|
|
296
|
+
label: `Account for ${row.title}`
|
|
297
|
+
})
|
|
298
|
+
}) : null,
|
|
299
|
+
connection?.detail ? /* @__PURE__ */ jsx("span", {
|
|
300
|
+
className: "w-full truncate text-[11px] leading-4 text-muted-foreground",
|
|
301
|
+
title: connection.detail,
|
|
302
|
+
children: connection.detail
|
|
303
|
+
}) : null,
|
|
304
|
+
!compact ? /* @__PURE__ */ jsx("p", {
|
|
305
|
+
className: cn("min-h-8 text-sm", TONES[connection?.statusTone ?? "neutral"]),
|
|
306
|
+
children: row.kind === "app" ? row.installedCount ? `Installed · ${row.installedCount} ${row.installedCount === 1 ? "account" : "accounts"}` : "Not installed" : connection?.statusLabel ?? (active ? `${provider?.connections.length} accounts · select one to manage` : "Not connected")
|
|
307
|
+
}) : null,
|
|
308
|
+
!compact ? /* @__PURE__ */ jsxs("div", {
|
|
309
|
+
className: "mt-auto flex min-h-9 items-center gap-2 border-t border-border pt-3",
|
|
310
|
+
children: [row.kind === "app" ? /* @__PURE__ */ jsx("a", {
|
|
311
|
+
href: row.to,
|
|
312
|
+
className: cn("text-sm font-medium hover:underline", focusRing),
|
|
313
|
+
children: row.installedCount ? "Manage" : "Set up"
|
|
314
|
+
}) : !active && canConnect ? wholeButton ? /* @__PURE__ */ jsx("span", {
|
|
315
|
+
className: "text-sm font-medium",
|
|
316
|
+
children: connecting ? "Connecting…" : "Connect"
|
|
317
|
+
}) : /* @__PURE__ */ jsx(Button, {
|
|
318
|
+
variant: "outline",
|
|
319
|
+
disabled: connecting,
|
|
320
|
+
onClick: () => onConnect?.(row),
|
|
321
|
+
children: connecting ? "Connecting…" : "Connect"
|
|
322
|
+
}) : manageHref ? /* @__PURE__ */ jsx("a", {
|
|
323
|
+
href: manageHref,
|
|
324
|
+
target: connection?.manageInNewWindow ? "_blank" : void 0,
|
|
325
|
+
rel: connection?.manageInNewWindow ? "noopener noreferrer" : void 0,
|
|
326
|
+
"data-testid": `manage-${row.providerId}`,
|
|
327
|
+
className: cn("text-sm font-medium hover:underline", focusRing),
|
|
328
|
+
children: "Manage"
|
|
329
|
+
}) : connection?.capabilities.manage && onManage ? /* @__PURE__ */ jsx(Button, {
|
|
330
|
+
variant: "outline",
|
|
331
|
+
onClick: () => onManage(connection, row),
|
|
332
|
+
children: "Manage"
|
|
333
|
+
}) : null, provider && connection ? /* @__PURE__ */ jsx("div", {
|
|
334
|
+
className: "ml-auto",
|
|
335
|
+
children: /* @__PURE__ */ jsx(ConnectionMenu, {
|
|
336
|
+
compact,
|
|
337
|
+
row: provider,
|
|
338
|
+
connection,
|
|
339
|
+
onManage,
|
|
340
|
+
onDisconnect
|
|
341
|
+
}, connection.id)
|
|
342
|
+
}) : null]
|
|
343
|
+
}) : provider && connection ? /* @__PURE__ */ jsx("div", {
|
|
344
|
+
className: "absolute right-1.5 top-1.5 z-20",
|
|
345
|
+
children: /* @__PURE__ */ jsx(ConnectionMenu, {
|
|
346
|
+
compact,
|
|
347
|
+
row: provider,
|
|
348
|
+
connection,
|
|
349
|
+
onManage,
|
|
350
|
+
onDisconnect
|
|
351
|
+
}, connection.id)
|
|
352
|
+
}) : null
|
|
353
|
+
]
|
|
354
|
+
}, `${row.kind}:${row.providerId}`);
|
|
355
|
+
})
|
|
356
|
+
})
|
|
357
|
+
]
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
//#endregion
|
|
7
361
|
//#region src/integrations/integrations-panel.tsx
|
|
8
362
|
const DEFAULT_FEATURED_IDS = [
|
|
9
363
|
"gmail",
|
|
@@ -43,463 +397,713 @@ const DEFAULT_FEATURED_IDS = [
|
|
|
43
397
|
function defaultConnectorOf(provider) {
|
|
44
398
|
return provider.connectors?.[0]?.connectorId ?? provider.providerId;
|
|
45
399
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const TILE_BOX_CLASS = "flex min-w-0 aspect-square flex-col items-center justify-center gap-2 rounded-xl border p-3 text-center";
|
|
49
|
-
/**
|
|
50
|
-
* Search + sort row. Rendered by the skeleton AND the loaded panel from this
|
|
51
|
-
* one component, which is the whole point: the row is ~74px tall with a 16px
|
|
52
|
-
* gap under it, and a skeleton that omitted it under-reserved the page by that
|
|
53
|
-
* much on every single load — the panel visibly dropped when data arrived.
|
|
54
|
-
* Height parity here is structural, not a matched pair of magic numbers.
|
|
55
|
-
*
|
|
56
|
-
* The controls stay live while loading. They drive local state only (`query`,
|
|
57
|
-
* `sort`), so typing during the fetch is applied the moment the catalog lands
|
|
58
|
-
* instead of being thrown away — and keeping the same input element mounted
|
|
59
|
-
* across the transition is what preserves its autofocus.
|
|
60
|
-
*/
|
|
61
|
-
function IntegrationsToolbar({ query, onQueryChange, sort, onSortChange }) {
|
|
62
|
-
return /* @__PURE__ */ jsxs("div", {
|
|
63
|
-
className: "flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between",
|
|
64
|
-
children: [/* @__PURE__ */ jsxs("div", {
|
|
65
|
-
className: `flex flex-1 items-center gap-2 rounded-lg border bg-card px-3 py-2 ${focusFieldWithin}`,
|
|
66
|
-
children: [/* @__PURE__ */ jsx(Search, { className: "h-4 w-4 shrink-0 text-muted-foreground" }), /* @__PURE__ */ jsx("input", {
|
|
67
|
-
type: "text",
|
|
68
|
-
value: query,
|
|
69
|
-
onChange: (e) => onQueryChange(e.target.value),
|
|
70
|
-
"aria-label": "Search integrations",
|
|
71
|
-
placeholder: "Search integrations...",
|
|
72
|
-
autoFocus: true,
|
|
73
|
-
"data-testid": "integration-search",
|
|
74
|
-
className: "flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground"
|
|
75
|
-
})]
|
|
76
|
-
}), /* @__PURE__ */ jsx("div", {
|
|
77
|
-
role: "group",
|
|
78
|
-
"aria-label": "Sort integrations",
|
|
79
|
-
className: "flex shrink-0 items-center gap-1 rounded-lg border border-border bg-card p-1",
|
|
80
|
-
children: [["featured", "Featured"], ["alpha", "A–Z"]].map(([value, label]) => /* @__PURE__ */ jsx("button", {
|
|
81
|
-
type: "button",
|
|
82
|
-
"aria-pressed": sort === value,
|
|
83
|
-
onClick: () => onSortChange(value),
|
|
84
|
-
"data-testid": `sort-${value}`,
|
|
85
|
-
className: cn("rounded-md px-2.5 py-1 text-xs font-medium transition-colors", focusRing, sort === value ? "bg-accent text-foreground" : "text-muted-foreground hover:text-foreground"),
|
|
86
|
-
children: label
|
|
87
|
-
}, value))
|
|
88
|
-
})]
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
const LOGO_SIZE = 48;
|
|
92
|
-
function buildConnectionIndex(connections) {
|
|
93
|
-
const index = /* @__PURE__ */ new Map();
|
|
94
|
-
for (const conn of connections) {
|
|
95
|
-
if (conn.status === "revoked") continue;
|
|
96
|
-
index.set(`${conn.providerId}:${conn.connectorId ?? conn.providerId}`, conn);
|
|
97
|
-
}
|
|
98
|
-
return index;
|
|
400
|
+
function displayNameOf(provider) {
|
|
401
|
+
return provider.displayName ?? provider.title ?? provider.providerId.replace(/[-_]/g, " ");
|
|
99
402
|
}
|
|
100
|
-
function makeFeaturedRank(
|
|
403
|
+
function makeFeaturedRank(ids) {
|
|
101
404
|
const rank = /* @__PURE__ */ new Map();
|
|
102
|
-
|
|
103
|
-
const norm = normalizeProviderId(id);
|
|
405
|
+
ids.forEach((id, i) => {
|
|
104
406
|
if (!rank.has(id)) rank.set(id, i);
|
|
105
|
-
|
|
407
|
+
const normalized = normalizeProviderId(id);
|
|
408
|
+
if (!rank.has(normalized)) rank.set(normalized, i);
|
|
106
409
|
});
|
|
107
410
|
return rank;
|
|
108
411
|
}
|
|
109
412
|
function rankOf(provider, rank) {
|
|
110
413
|
const raw = provider.providerId.toLowerCase();
|
|
111
|
-
|
|
112
|
-
if (direct !== void 0) return direct;
|
|
113
|
-
return rank.get(normalizeProviderId(raw)) ?? Number.MAX_SAFE_INTEGER;
|
|
114
|
-
}
|
|
115
|
-
function displayNameOf(provider) {
|
|
116
|
-
return provider.displayName ?? provider.title ?? provider.providerId.replace(/[-_]/g, " ");
|
|
117
|
-
}
|
|
118
|
-
function matchesQuery(provider, q) {
|
|
119
|
-
return `${displayNameOf(provider)} ${provider.providerId} ${provider.description ?? ""}`.toLowerCase().includes(q);
|
|
414
|
+
return rank.get(raw) ?? rank.get(normalizeProviderId(raw)) ?? Number.MAX_SAFE_INTEGER;
|
|
120
415
|
}
|
|
121
|
-
|
|
122
|
-
|
|
416
|
+
/** Compatibility facade: legacy effects/sorting, one controlled catalog renderer. */
|
|
417
|
+
function IntegrationsPanel({ catalog, connections, healthByConnectionId, isLoading, error, onConnect, onDisconnect, getManageHref, onManage, getConnectionContext, getConnectionActions, emptyCatalogLabel = "No integrations available yet.", featuredIds = DEFAULT_FEATURED_IDS, defaultSort = "featured", skeletonCount = 12, className }) {
|
|
418
|
+
const [query, setQuery] = React$1.useState("");
|
|
419
|
+
const [sort, setSort] = React$1.useState(defaultSort);
|
|
420
|
+
const [selected, setSelected] = React$1.useState({});
|
|
123
421
|
const [actionError, setActionError] = React$1.useState(null);
|
|
124
422
|
const [pendingAction, setPendingAction] = React$1.useState(false);
|
|
423
|
+
const [connectingId, setConnectingId] = React$1.useState(null);
|
|
424
|
+
const [connectError, setConnectError] = React$1.useState(null);
|
|
425
|
+
const activeConnect = React$1.useRef(0);
|
|
426
|
+
const [disconnectTarget, setDisconnectTarget] = React$1.useState(null);
|
|
427
|
+
const [isDisconnecting, setIsDisconnecting] = React$1.useState(false);
|
|
428
|
+
const [disconnectError, setDisconnectError] = React$1.useState(null);
|
|
429
|
+
const activeDisconnect = React$1.useRef(0);
|
|
430
|
+
React$1.useEffect(() => () => {
|
|
431
|
+
activeConnect.current += 1;
|
|
432
|
+
activeDisconnect.current += 1;
|
|
433
|
+
}, []);
|
|
125
434
|
const runConnectionAction = async (action) => {
|
|
126
435
|
setActionError(null);
|
|
127
436
|
setPendingAction(true);
|
|
128
437
|
try {
|
|
129
438
|
await action.onSelect();
|
|
130
|
-
} catch (
|
|
131
|
-
setActionError(
|
|
439
|
+
} catch (failure) {
|
|
440
|
+
setActionError(failure instanceof Error ? failure.message : "Could not update the connection.");
|
|
132
441
|
} finally {
|
|
133
442
|
setPendingAction(false);
|
|
134
443
|
}
|
|
135
444
|
};
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
const [disconnectTarget, setDisconnectTarget] = React$1.useState(null);
|
|
139
|
-
const [isDisconnecting, setIsDisconnecting] = React$1.useState(false);
|
|
140
|
-
const [disconnectError, setDisconnectError] = React$1.useState(null);
|
|
141
|
-
const activeDisconnect = React$1.useRef(0);
|
|
142
|
-
const connectionIndex = React$1.useMemo(() => buildConnectionIndex(connections), [connections]);
|
|
143
|
-
const featuredRank = React$1.useMemo(() => makeFeaturedRank(featuredIds), [featuredIds]);
|
|
144
|
-
const visible = React$1.useMemo(() => {
|
|
145
|
-
const q = query.trim().toLowerCase();
|
|
146
|
-
const sorted = [...q ? catalog.filter((p) => matchesQuery(p, q)) : catalog];
|
|
147
|
-
sorted.sort((a, b) => {
|
|
148
|
-
if (sort === "featured") {
|
|
149
|
-
const ra = rankOf(a, featuredRank);
|
|
150
|
-
const rb = rankOf(b, featuredRank);
|
|
151
|
-
if (ra !== rb) return ra - rb;
|
|
152
|
-
}
|
|
153
|
-
return displayNameOf(a).localeCompare(displayNameOf(b));
|
|
154
|
-
});
|
|
155
|
-
return sorted;
|
|
156
|
-
}, [
|
|
157
|
-
catalog,
|
|
158
|
-
query,
|
|
159
|
-
sort,
|
|
160
|
-
featuredRank
|
|
161
|
-
]);
|
|
162
|
-
const closeDisconnect = React$1.useCallback(() => {
|
|
163
|
-
activeDisconnect.current += 1;
|
|
164
|
-
setIsDisconnecting(false);
|
|
165
|
-
setDisconnectTarget(null);
|
|
166
|
-
setDisconnectError(null);
|
|
167
|
-
}, []);
|
|
168
|
-
const [connectingId, setConnectingId] = React$1.useState(null);
|
|
169
|
-
const [connectError, setConnectError] = React$1.useState(null);
|
|
170
|
-
const activeConnect = React$1.useRef(0);
|
|
171
|
-
const startConnect = React$1.useCallback(async (input) => {
|
|
172
|
-
const reqId = activeConnect.current += 1;
|
|
445
|
+
const startConnect = async (provider) => {
|
|
446
|
+
const request = ++activeConnect.current;
|
|
173
447
|
setConnectError(null);
|
|
174
448
|
let pending;
|
|
175
449
|
try {
|
|
176
|
-
pending = onConnect(
|
|
177
|
-
|
|
178
|
-
|
|
450
|
+
pending = onConnect({
|
|
451
|
+
providerId: provider.providerId,
|
|
452
|
+
connectorId: defaultConnectorOf(provider)
|
|
453
|
+
});
|
|
454
|
+
} catch (failure) {
|
|
455
|
+
setConnectError(failure instanceof Error ? failure.message : "Failed to start the connection.");
|
|
179
456
|
return;
|
|
180
457
|
}
|
|
181
458
|
if (typeof pending?.then !== "function") return;
|
|
182
|
-
setConnectingId(
|
|
459
|
+
setConnectingId(provider.providerId);
|
|
183
460
|
try {
|
|
184
461
|
await pending;
|
|
185
|
-
} catch (
|
|
186
|
-
if (activeConnect.current
|
|
187
|
-
setConnectError(e instanceof Error ? e.message : "Failed to start the connection.");
|
|
462
|
+
} catch (failure) {
|
|
463
|
+
if (activeConnect.current === request) setConnectError(failure instanceof Error ? failure.message : "Failed to start the connection.");
|
|
188
464
|
} finally {
|
|
189
|
-
if (activeConnect.current ===
|
|
465
|
+
if (activeConnect.current === request) setConnectingId(null);
|
|
190
466
|
}
|
|
191
|
-
}
|
|
192
|
-
const
|
|
467
|
+
};
|
|
468
|
+
const closeDisconnect = React$1.useCallback(() => {
|
|
469
|
+
activeDisconnect.current += 1;
|
|
470
|
+
setIsDisconnecting(false);
|
|
471
|
+
setDisconnectTarget(null);
|
|
472
|
+
setDisconnectError(null);
|
|
473
|
+
}, []);
|
|
474
|
+
const confirmDisconnect = async () => {
|
|
193
475
|
if (!disconnectTarget) return;
|
|
194
|
-
const
|
|
476
|
+
const request = ++activeDisconnect.current;
|
|
195
477
|
setIsDisconnecting(true);
|
|
196
478
|
setDisconnectError(null);
|
|
197
479
|
try {
|
|
198
|
-
await
|
|
199
|
-
if (activeDisconnect.current
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
if (activeDisconnect.current !== reqId) return;
|
|
203
|
-
setDisconnectError(e instanceof Error ? e.message : "Failed to disconnect.");
|
|
480
|
+
await onDisconnect(disconnectTarget.connection.id);
|
|
481
|
+
if (activeDisconnect.current === request) setDisconnectTarget(null);
|
|
482
|
+
} catch (failure) {
|
|
483
|
+
if (activeDisconnect.current === request) setDisconnectError(failure instanceof Error ? failure.message : "Failed to disconnect.");
|
|
204
484
|
} finally {
|
|
205
|
-
if (activeDisconnect.current ===
|
|
485
|
+
if (activeDisconnect.current === request) setIsDisconnecting(false);
|
|
206
486
|
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
487
|
+
};
|
|
488
|
+
const rank = makeFeaturedRank(featuredIds);
|
|
489
|
+
const rows = [...catalog].sort((a, b) => {
|
|
490
|
+
if (sort === "featured") {
|
|
491
|
+
const delta = rankOf(a, rank) - rankOf(b, rank);
|
|
492
|
+
if (delta) return delta;
|
|
493
|
+
}
|
|
494
|
+
return displayNameOf(a).localeCompare(displayNameOf(b));
|
|
495
|
+
}).map((provider) => {
|
|
496
|
+
const live = connections.filter((connection) => connection.providerId === provider.providerId && connection.status !== "revoked");
|
|
497
|
+
const selectedId = Object.hasOwn(selected, provider.providerId) ? selected[provider.providerId] ?? null : live.length === 1 ? live[0].id : null;
|
|
498
|
+
return {
|
|
499
|
+
kind: "provider",
|
|
500
|
+
providerId: provider.providerId,
|
|
501
|
+
title: displayNameOf(provider),
|
|
502
|
+
description: provider.description,
|
|
503
|
+
iconUrl: provider.iconUrl,
|
|
504
|
+
canConnect: true,
|
|
505
|
+
selectedConnectionId: live.some((connection) => connection.id === selectedId) ? selectedId : null,
|
|
506
|
+
connections: live.map((connection) => {
|
|
507
|
+
const health = healthByConnectionId?.[connection.id];
|
|
508
|
+
const manageHref = getManageHref?.(connection);
|
|
509
|
+
return {
|
|
510
|
+
id: connection.id,
|
|
511
|
+
accountDisplay: connection.accountDisplay,
|
|
512
|
+
statusLabel: health?.message ?? connection.status.replace(/_/g, " "),
|
|
513
|
+
detail: getConnectionContext?.(connection),
|
|
514
|
+
manageHref,
|
|
515
|
+
manageInNewWindow: true,
|
|
516
|
+
capabilities: {
|
|
517
|
+
manage: Boolean(manageHref || onManage),
|
|
518
|
+
disconnect: true
|
|
519
|
+
},
|
|
520
|
+
actions: (getConnectionActions?.(connection) ?? []).map((action) => ({
|
|
521
|
+
id: action.id,
|
|
522
|
+
label: action.label,
|
|
523
|
+
disabled: action.disabled || pendingAction,
|
|
524
|
+
onSelect: () => {
|
|
525
|
+
runConnectionAction(action);
|
|
526
|
+
}
|
|
527
|
+
}))
|
|
528
|
+
};
|
|
215
529
|
})
|
|
216
|
-
}
|
|
530
|
+
};
|
|
217
531
|
});
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
532
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(IntegrationsCatalog, {
|
|
533
|
+
rows,
|
|
534
|
+
query,
|
|
535
|
+
onQueryChange: setQuery,
|
|
536
|
+
sort,
|
|
537
|
+
onSortChange: setSort,
|
|
538
|
+
layout: "tiles",
|
|
539
|
+
loading: isLoading,
|
|
540
|
+
reserveConnectionContext: Boolean(getConnectionContext),
|
|
541
|
+
error: error ? `Failed to load integrations: ${error.message}` : null,
|
|
542
|
+
actionError,
|
|
543
|
+
connectError,
|
|
544
|
+
busyProviderId: connectingId,
|
|
545
|
+
skeletonCount,
|
|
546
|
+
emptyCatalogLabel,
|
|
547
|
+
className,
|
|
548
|
+
onSelectConnection: (providerId, id) => setSelected((current) => ({
|
|
549
|
+
...current,
|
|
550
|
+
[providerId]: id
|
|
551
|
+
})),
|
|
552
|
+
onConnect: (row) => {
|
|
553
|
+
const provider = catalog.find((item) => item.providerId === row.providerId);
|
|
554
|
+
if (provider) startConnect(provider);
|
|
555
|
+
},
|
|
556
|
+
onManage: onManage ? (connection, row) => {
|
|
557
|
+
runConnectionAction({
|
|
558
|
+
id: "manage",
|
|
559
|
+
label: "Manage",
|
|
560
|
+
onSelect: () => onManage({
|
|
561
|
+
connectionId: connection.id,
|
|
562
|
+
providerId: row.providerId
|
|
563
|
+
})
|
|
564
|
+
});
|
|
565
|
+
} : void 0,
|
|
566
|
+
onDisconnect: (display, row) => {
|
|
567
|
+
const connection = connections.find((item) => item.id === display.id && item.providerId === row.providerId && item.status !== "revoked");
|
|
568
|
+
if (connection) setDisconnectTarget({
|
|
569
|
+
connection,
|
|
570
|
+
name: row.title
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
}), /* @__PURE__ */ jsx(Dialog, {
|
|
574
|
+
open: !!disconnectTarget,
|
|
575
|
+
onOpenChange: (open) => {
|
|
576
|
+
if (!open) closeDisconnect();
|
|
577
|
+
},
|
|
578
|
+
children: /* @__PURE__ */ jsxs(DialogContent, {
|
|
579
|
+
className: "max-w-sm",
|
|
580
|
+
onCloseAutoFocus: (event) => event.preventDefault(),
|
|
581
|
+
children: [
|
|
582
|
+
/* @__PURE__ */ jsxs(DialogHeader, { children: [/* @__PURE__ */ jsxs(DialogTitle, { children: [
|
|
583
|
+
"Disconnect ",
|
|
584
|
+
disconnectTarget?.name ?? "integration",
|
|
585
|
+
"?"
|
|
586
|
+
] }), /* @__PURE__ */ jsxs(DialogDescription, { children: [
|
|
587
|
+
"This removes Sandbox's access to your ",
|
|
588
|
+
disconnectTarget?.name ?? "this",
|
|
589
|
+
" account",
|
|
590
|
+
disconnectTarget?.connection.accountDisplay ? ` (${disconnectTarget.connection.accountDisplay})` : "",
|
|
591
|
+
". You can reconnect anytime."
|
|
592
|
+
] })] }),
|
|
593
|
+
disconnectError ? /* @__PURE__ */ jsx("p", {
|
|
594
|
+
className: "text-sm text-destructive",
|
|
595
|
+
role: "alert",
|
|
596
|
+
children: disconnectError
|
|
597
|
+
}) : null,
|
|
598
|
+
/* @__PURE__ */ jsxs(DialogFooter, { children: [/* @__PURE__ */ jsx(Button, {
|
|
599
|
+
variant: "outline",
|
|
600
|
+
onClick: closeDisconnect,
|
|
601
|
+
"data-testid": "cancel-disconnect",
|
|
602
|
+
children: "Cancel"
|
|
603
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
604
|
+
variant: "destructive",
|
|
605
|
+
loading: isDisconnecting,
|
|
606
|
+
onClick: confirmDisconnect,
|
|
607
|
+
"data-testid": "confirm-disconnect",
|
|
608
|
+
children: "Disconnect"
|
|
609
|
+
})] })
|
|
610
|
+
]
|
|
611
|
+
})
|
|
612
|
+
})] });
|
|
613
|
+
}
|
|
614
|
+
//#endregion
|
|
615
|
+
//#region src/integrations/connection-detail.tsx
|
|
616
|
+
/** Platform's permission layout, without its fetching, defaults or persistence. */
|
|
617
|
+
function IntegrationConnectionDetail({ provider, connections, selectedConnectionId, onSelectConnection, detailsByConnectionId, loading, error, backHref, description, onRetry, onTestConnection, onDisconnect, onDecisionChange, onResetDecision, className }) {
|
|
618
|
+
const connection = connections.find((item) => item.id === selectedConnectionId);
|
|
619
|
+
const details = connection && Object.hasOwn(detailsByConnectionId, connection.id) ? detailsByConnectionId[connection.id] : void 0;
|
|
620
|
+
const busy = !!details?.busy;
|
|
621
|
+
const groups = details?.permissionGroups;
|
|
622
|
+
const failure = error ?? details?.error;
|
|
623
|
+
const renderRows = (group) => /* @__PURE__ */ jsx("ul", {
|
|
624
|
+
className: "divide-y divide-border",
|
|
625
|
+
children: group.rows.map((row) => {
|
|
626
|
+
const known = row.decision !== null && row.decisionOptions.some((option) => option.value === row.decision);
|
|
627
|
+
const editable = connection?.capabilities.editPermissions && onDecisionChange;
|
|
628
|
+
return /* @__PURE__ */ jsxs("li", {
|
|
629
|
+
className: "flex min-w-0 flex-col gap-3 p-4 sm:flex-row sm:items-center sm:justify-between",
|
|
630
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
631
|
+
className: "min-w-0",
|
|
632
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
633
|
+
className: "flex min-w-0 flex-wrap items-center gap-2",
|
|
634
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
635
|
+
className: cn("rounded border border-border px-2 py-0.5 text-xs", row.riskTone === "error" ? "text-destructive" : "text-muted-foreground"),
|
|
636
|
+
children: row.riskLabel
|
|
637
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
638
|
+
className: "min-w-0 break-words text-sm font-medium",
|
|
639
|
+
children: row.title
|
|
640
|
+
})]
|
|
641
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
642
|
+
className: "mt-1 break-all font-mono text-xs text-muted-foreground",
|
|
643
|
+
children: row.actionPath
|
|
644
|
+
})]
|
|
645
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
646
|
+
className: "flex flex-wrap items-center gap-3 sm:shrink-0",
|
|
647
|
+
children: [
|
|
648
|
+
/* @__PURE__ */ jsx("span", {
|
|
649
|
+
className: "text-xs text-muted-foreground",
|
|
650
|
+
children: row.sourceLabel
|
|
651
|
+
}),
|
|
652
|
+
editable ? /* @__PURE__ */ jsxs("select", {
|
|
653
|
+
"aria-label": `Decision for ${row.actionPath}`,
|
|
654
|
+
value: known ? row.decision : "",
|
|
655
|
+
disabled: busy || row.disabled,
|
|
656
|
+
onChange: (event) => {
|
|
657
|
+
if (connection && row.decisionOptions.some((option) => option.value === event.target.value)) onDecisionChange?.(connection.id, row.actionPath, event.target.value);
|
|
658
|
+
},
|
|
659
|
+
className: cn("h-9 rounded-md border border-border bg-background px-2 text-sm", focusRing),
|
|
660
|
+
children: [!known ? /* @__PURE__ */ jsx("option", {
|
|
661
|
+
value: "",
|
|
662
|
+
disabled: true,
|
|
663
|
+
children: "Not available"
|
|
664
|
+
}) : null, row.decisionOptions.map((option) => /* @__PURE__ */ jsx("option", {
|
|
665
|
+
value: option.value,
|
|
666
|
+
children: option.label
|
|
667
|
+
}, option.value))]
|
|
668
|
+
}) : /* @__PURE__ */ jsx("span", {
|
|
669
|
+
className: "text-sm",
|
|
670
|
+
children: known ? row.decisionOptions.find((option) => option.value === row.decision)?.label : "Not available"
|
|
671
|
+
}),
|
|
672
|
+
row.canReset && connection?.capabilities.resetPermissions && onResetDecision ? /* @__PURE__ */ jsx(Button, {
|
|
673
|
+
variant: "outline",
|
|
674
|
+
disabled: busy || row.disabled,
|
|
675
|
+
"aria-label": `Reset ${row.actionPath}`,
|
|
676
|
+
onClick: () => onResetDecision(connection.id, row.actionPath),
|
|
677
|
+
children: "Reset"
|
|
678
|
+
}) : null
|
|
679
|
+
]
|
|
240
680
|
})]
|
|
241
|
-
},
|
|
242
|
-
})
|
|
243
|
-
});
|
|
244
|
-
if (catalog.length === 0) return /* @__PURE__ */ jsx(EmptyState, {
|
|
245
|
-
title: "No integrations",
|
|
246
|
-
description: emptyCatalogLabel,
|
|
247
|
-
className
|
|
681
|
+
}, row.actionPath);
|
|
682
|
+
})
|
|
248
683
|
});
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
"data-testid": "
|
|
684
|
+
return /* @__PURE__ */ jsxs("section", {
|
|
685
|
+
className: cn("w-full min-w-0 space-y-4", className),
|
|
686
|
+
"aria-label": `${provider.title} connection settings`,
|
|
687
|
+
"data-testid": "integration-connection-detail",
|
|
688
|
+
"aria-busy": loading || details?.loading || void 0,
|
|
253
689
|
children: [
|
|
254
|
-
/* @__PURE__ */ jsx(
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
onSortChange: setSort
|
|
259
|
-
}),
|
|
260
|
-
actionError ? /* @__PURE__ */ jsx("p", {
|
|
261
|
-
role: "alert",
|
|
262
|
-
className: "text-sm text-destructive",
|
|
263
|
-
children: actionError
|
|
690
|
+
backHref ? /* @__PURE__ */ jsx("a", {
|
|
691
|
+
href: backHref,
|
|
692
|
+
className: cn("inline-flex text-sm text-muted-foreground hover:text-foreground", focusRing),
|
|
693
|
+
children: "← Integrations"
|
|
264
694
|
}) : null,
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
695
|
+
/* @__PURE__ */ jsxs("header", {
|
|
696
|
+
className: "flex min-w-0 items-start gap-3",
|
|
697
|
+
children: [/* @__PURE__ */ jsx(ProviderIcon, {
|
|
698
|
+
id: provider.providerId,
|
|
699
|
+
iconUrl: provider.iconUrl,
|
|
700
|
+
displayName: provider.title,
|
|
701
|
+
size: 48
|
|
702
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
703
|
+
className: "min-w-0",
|
|
704
|
+
children: [/* @__PURE__ */ jsxs("h2", {
|
|
705
|
+
className: "break-words text-xl font-semibold",
|
|
706
|
+
children: [provider.title, " connection settings"]
|
|
707
|
+
}), description ? /* @__PURE__ */ jsx("p", {
|
|
708
|
+
className: "mt-1 text-sm text-muted-foreground",
|
|
709
|
+
children: description
|
|
710
|
+
}) : null]
|
|
711
|
+
})]
|
|
712
|
+
}),
|
|
713
|
+
/* @__PURE__ */ jsx(ConnectionSelector, {
|
|
714
|
+
connections,
|
|
715
|
+
value: selectedConnectionId,
|
|
716
|
+
onChange: onSelectConnection,
|
|
717
|
+
label: `Account for ${provider.title}`
|
|
718
|
+
}),
|
|
719
|
+
/* @__PURE__ */ jsxs("div", {
|
|
720
|
+
className: "min-h-[200px] min-w-0 space-y-4",
|
|
721
|
+
children: [failure ? /* @__PURE__ */ jsxs("div", {
|
|
722
|
+
role: "alert",
|
|
723
|
+
className: "rounded-xl border border-destructive/40 bg-card p-4 text-sm text-destructive",
|
|
724
|
+
children: [/* @__PURE__ */ jsx("p", { children: failure }), onRetry ? /* @__PURE__ */ jsx(Button, {
|
|
725
|
+
variant: "outline",
|
|
726
|
+
onClick: () => onRetry(connection?.id ?? null),
|
|
727
|
+
children: "Retry"
|
|
728
|
+
}) : null]
|
|
729
|
+
}) : null, loading && !connection ? /* @__PURE__ */ jsx("p", {
|
|
730
|
+
role: "status",
|
|
731
|
+
className: "rounded-xl border border-border bg-card p-6 text-sm text-muted-foreground",
|
|
732
|
+
children: "Loading connections…"
|
|
733
|
+
}) : !connection ? /* @__PURE__ */ jsx("p", {
|
|
734
|
+
className: "rounded-xl border border-border bg-card p-6 text-sm text-muted-foreground",
|
|
735
|
+
children: connections.length ? "Select a connected account to view its settings." : "No connected accounts are available."
|
|
736
|
+
}) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
737
|
+
/* @__PURE__ */ jsxs("div", {
|
|
738
|
+
className: "flex flex-wrap items-start justify-between gap-3 rounded-xl border border-border bg-card p-4",
|
|
739
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
740
|
+
className: "min-w-0",
|
|
290
741
|
children: [
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
rel: "noopener noreferrer",
|
|
295
|
-
"aria-label": `Manage ${name} (opens in new window)`,
|
|
296
|
-
"data-testid": `manage-${provider.providerId}`,
|
|
297
|
-
className: `absolute inset-0 z-10 rounded-xl ${focusRing}`
|
|
298
|
-
}) : null,
|
|
299
|
-
/* @__PURE__ */ jsx("span", {
|
|
300
|
-
className: "pointer-events-none absolute left-2 top-2 flex h-4 w-4 items-center justify-center rounded-full bg-[var(--surface-success-text)] text-white",
|
|
301
|
-
children: /* @__PURE__ */ jsx(Check, {
|
|
302
|
-
className: "h-3 w-3",
|
|
303
|
-
strokeWidth: 3
|
|
304
|
-
})
|
|
742
|
+
/* @__PURE__ */ jsx("p", {
|
|
743
|
+
className: "break-words text-sm font-medium",
|
|
744
|
+
children: connection.accountDisplay ?? connection.id
|
|
305
745
|
}),
|
|
306
|
-
/* @__PURE__ */
|
|
307
|
-
className: "
|
|
308
|
-
children:
|
|
309
|
-
id: provider.providerId,
|
|
310
|
-
iconUrl: provider.iconUrl,
|
|
311
|
-
displayName: displayNameOf(provider),
|
|
312
|
-
size: LOGO_SIZE,
|
|
313
|
-
className: "rounded-2xl"
|
|
314
|
-
}), /* @__PURE__ */ jsxs("div", {
|
|
315
|
-
className: "flex h-8 w-full flex-col justify-center leading-4",
|
|
316
|
-
children: [/* @__PURE__ */ jsx("span", {
|
|
317
|
-
className: cn("w-full text-xs font-medium text-foreground", live.accountDisplay ? "truncate" : "line-clamp-2"),
|
|
318
|
-
children: name
|
|
319
|
-
}), live.accountDisplay ? /* @__PURE__ */ jsx("span", {
|
|
320
|
-
className: "w-full truncate text-[11px] leading-4 text-muted-foreground",
|
|
321
|
-
"data-testid": `account-${provider.providerId}`,
|
|
322
|
-
children: live.accountDisplay
|
|
323
|
-
}) : null]
|
|
324
|
-
})]
|
|
746
|
+
/* @__PURE__ */ jsx("p", {
|
|
747
|
+
className: "text-sm text-muted-foreground",
|
|
748
|
+
children: connection.statusLabel
|
|
325
749
|
}),
|
|
326
|
-
|
|
327
|
-
className: "
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
}) : null,
|
|
331
|
-
/* @__PURE__ */ jsxs(DropdownMenu, { children: [/* @__PURE__ */ jsx(DropdownMenuTrigger, {
|
|
332
|
-
asChild: true,
|
|
333
|
-
children: /* @__PURE__ */ jsx("button", {
|
|
334
|
-
type: "button",
|
|
335
|
-
"data-testid": `menu-${provider.providerId}`,
|
|
336
|
-
"aria-label": `More actions for ${name}`,
|
|
337
|
-
title: "More actions",
|
|
338
|
-
className: `absolute right-1.5 top-1.5 z-20 flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground/70 transition-colors hover:bg-background hover:text-foreground focus-visible:bg-background focus-visible:text-foreground ${focusRing}`,
|
|
339
|
-
children: /* @__PURE__ */ jsx(MoreVertical, { className: "h-3.5 w-3.5" })
|
|
340
|
-
})
|
|
341
|
-
}), /* @__PURE__ */ jsxs(DropdownMenuContent, {
|
|
342
|
-
side: "right",
|
|
343
|
-
align: "start",
|
|
344
|
-
className: "min-w-[160px] rounded-md",
|
|
345
|
-
children: [
|
|
346
|
-
manageHref ? /* @__PURE__ */ jsx(DropdownMenuItem, {
|
|
347
|
-
asChild: true,
|
|
348
|
-
children: /* @__PURE__ */ jsxs("a", {
|
|
349
|
-
href: manageHref,
|
|
350
|
-
target: "_blank",
|
|
351
|
-
rel: "noopener noreferrer",
|
|
352
|
-
"data-testid": `manage-item-${provider.providerId}`,
|
|
353
|
-
children: [/* @__PURE__ */ jsx(ExternalLink, { className: "mr-2 h-4 w-4" }), " Manage"]
|
|
354
|
-
})
|
|
355
|
-
}) : onManage ? /* @__PURE__ */ jsxs(DropdownMenuItem, {
|
|
356
|
-
"data-testid": `manage-item-${provider.providerId}`,
|
|
357
|
-
onClick: () => onManage({
|
|
358
|
-
connectionId: live.id,
|
|
359
|
-
providerId: provider.providerId
|
|
360
|
-
}),
|
|
361
|
-
children: [/* @__PURE__ */ jsx(ExternalLink, { className: "mr-2 h-4 w-4" }), " Manage"]
|
|
362
|
-
}) : null,
|
|
363
|
-
connectionActions.map((action) => /* @__PURE__ */ jsx(DropdownMenuItem, {
|
|
364
|
-
disabled: action.disabled || pendingAction,
|
|
365
|
-
onSelect: () => {
|
|
366
|
-
runConnectionAction(action);
|
|
367
|
-
},
|
|
368
|
-
children: action.label
|
|
369
|
-
}, action.id)),
|
|
370
|
-
canManage || connectionActions.length > 0 ? /* @__PURE__ */ jsx(DropdownMenuSeparator, {}) : null,
|
|
371
|
-
/* @__PURE__ */ jsxs(DropdownMenuItem, {
|
|
372
|
-
"data-testid": `disconnect-${provider.providerId}`,
|
|
373
|
-
className: "text-destructive focus:text-destructive",
|
|
374
|
-
onClick: () => setDisconnectTarget({
|
|
375
|
-
connectionId: live.id,
|
|
376
|
-
name,
|
|
377
|
-
accountDisplay: live.accountDisplay
|
|
378
|
-
}),
|
|
379
|
-
children: [/* @__PURE__ */ jsx(Unplug, { className: "mr-2 h-4 w-4" }), " Disconnect"]
|
|
380
|
-
})
|
|
381
|
-
]
|
|
382
|
-
})] })
|
|
750
|
+
connection.detail ? /* @__PURE__ */ jsx("p", {
|
|
751
|
+
className: "mt-1 text-sm text-muted-foreground",
|
|
752
|
+
children: connection.detail
|
|
753
|
+
}) : null
|
|
383
754
|
]
|
|
384
|
-
},
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
});
|
|
398
|
-
},
|
|
399
|
-
title: provider.description ? provider.description : `Connect ${name}`,
|
|
400
|
-
className: cn(tileBoxClass, "group border-border bg-card transition-all", "hover:border-[var(--border-strong)] hover:bg-accent/40 hover:shadow-sm", focusRing),
|
|
401
|
-
children: [/* @__PURE__ */ jsx(ProviderIcon, {
|
|
402
|
-
id: provider.providerId,
|
|
403
|
-
iconUrl: provider.iconUrl,
|
|
404
|
-
displayName: displayNameOf(provider),
|
|
405
|
-
size: LOGO_SIZE,
|
|
406
|
-
className: "rounded-2xl"
|
|
407
|
-
}), /* @__PURE__ */ jsx("span", {
|
|
408
|
-
className: "line-clamp-2 h-8 w-full text-xs font-medium leading-4 text-foreground",
|
|
409
|
-
children: name
|
|
755
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
756
|
+
className: "flex flex-wrap gap-2",
|
|
757
|
+
children: [connection.capabilities.test && onTestConnection ? /* @__PURE__ */ jsx(Button, {
|
|
758
|
+
variant: "outline",
|
|
759
|
+
disabled: busy,
|
|
760
|
+
onClick: () => onTestConnection(connection.id),
|
|
761
|
+
children: "Test connection"
|
|
762
|
+
}) : null, connection.capabilities.disconnect && onDisconnect ? /* @__PURE__ */ jsx(Button, {
|
|
763
|
+
variant: "destructive",
|
|
764
|
+
disabled: busy,
|
|
765
|
+
onClick: () => onDisconnect(connection.id),
|
|
766
|
+
children: "Disconnect"
|
|
767
|
+
}) : null]
|
|
410
768
|
})]
|
|
411
|
-
},
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
769
|
+
}),
|
|
770
|
+
details?.testResult ? /* @__PURE__ */ jsx("p", {
|
|
771
|
+
role: details.testResult.tone === "error" ? "alert" : "status",
|
|
772
|
+
className: cn("text-sm", details.testResult.tone === "error" ? "text-destructive" : "text-muted-foreground"),
|
|
773
|
+
children: details.testResult.message
|
|
774
|
+
}) : null,
|
|
775
|
+
details?.loading ? /* @__PURE__ */ jsx("p", {
|
|
776
|
+
role: "status",
|
|
777
|
+
className: "rounded-xl border border-border bg-card p-6 text-sm text-muted-foreground",
|
|
778
|
+
children: "Loading connection settings…"
|
|
779
|
+
}) : !groups ? /* @__PURE__ */ jsx("p", {
|
|
780
|
+
className: "rounded-xl border border-border bg-card p-6 text-sm text-muted-foreground",
|
|
781
|
+
children: "Connection settings are not available yet."
|
|
782
|
+
}) : groups.every((group) => group.rows.length === 0) ? /* @__PURE__ */ jsx("p", {
|
|
783
|
+
className: "rounded-xl border border-border bg-card p-6 text-sm text-muted-foreground",
|
|
784
|
+
children: "No actions are available for this connection."
|
|
785
|
+
}) : groups.map((group) => group.collapsed ? /* @__PURE__ */ jsxs("details", {
|
|
786
|
+
className: "min-w-0 rounded-xl border border-border bg-card",
|
|
787
|
+
children: [
|
|
788
|
+
/* @__PURE__ */ jsxs("summary", {
|
|
789
|
+
className: cn("cursor-pointer rounded-xl p-4 font-medium", focusRing),
|
|
790
|
+
children: [
|
|
791
|
+
group.title,
|
|
792
|
+
" (",
|
|
793
|
+
group.rows.length,
|
|
794
|
+
")"
|
|
795
|
+
]
|
|
796
|
+
}),
|
|
797
|
+
group.description ? /* @__PURE__ */ jsx("p", {
|
|
798
|
+
className: "px-4 pb-4 text-sm text-muted-foreground",
|
|
799
|
+
children: group.description
|
|
800
|
+
}) : null,
|
|
801
|
+
renderRows(group)
|
|
802
|
+
]
|
|
803
|
+
}, group.id) : /* @__PURE__ */ jsxs("section", {
|
|
804
|
+
className: "min-w-0 rounded-xl border border-border bg-card",
|
|
805
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
806
|
+
className: "border-b border-border p-4",
|
|
807
|
+
children: [/* @__PURE__ */ jsx("h3", {
|
|
808
|
+
className: "font-medium",
|
|
809
|
+
children: group.title
|
|
810
|
+
}), group.description ? /* @__PURE__ */ jsx("p", {
|
|
811
|
+
className: "mt-1 text-sm text-muted-foreground",
|
|
812
|
+
children: group.description
|
|
813
|
+
}) : null]
|
|
814
|
+
}), renderRows(group)]
|
|
815
|
+
}, group.id))
|
|
816
|
+
] })]
|
|
817
|
+
}, connection?.id ?? "unselected")
|
|
455
818
|
]
|
|
456
819
|
});
|
|
457
820
|
}
|
|
458
821
|
//#endregion
|
|
822
|
+
//#region src/integrations/connection-dialogs.tsx
|
|
823
|
+
const fieldClass = cn("h-10 w-full min-w-0 rounded-md border border-border bg-background px-3 text-sm disabled:opacity-50", focusRing);
|
|
824
|
+
function ConnectionDialog({ children, complete, submitLabel, ...props }) {
|
|
825
|
+
const opener = React$1.useRef(null);
|
|
826
|
+
const errorId = React$1.useId();
|
|
827
|
+
return /* @__PURE__ */ jsx(Dialog, {
|
|
828
|
+
open: props.open,
|
|
829
|
+
onOpenChange: props.onOpenChange,
|
|
830
|
+
children: /* @__PURE__ */ jsxs(DialogContent, {
|
|
831
|
+
className: "max-h-[90dvh] max-w-md overflow-y-auto",
|
|
832
|
+
onOpenAutoFocus: () => {
|
|
833
|
+
opener.current = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
834
|
+
},
|
|
835
|
+
onCloseAutoFocus: (event) => {
|
|
836
|
+
event.preventDefault();
|
|
837
|
+
if (opener.current?.isConnected) opener.current.focus();
|
|
838
|
+
},
|
|
839
|
+
onEscapeKeyDown: (event) => event.stopPropagation(),
|
|
840
|
+
children: [/* @__PURE__ */ jsxs(DialogHeader, { children: [/* @__PURE__ */ jsxs(DialogTitle, { children: ["Connect ", props.title] }), /* @__PURE__ */ jsx(DialogDescription, { children: props.description ?? "Enter the connection details requested by this application." })] }), /* @__PURE__ */ jsxs("form", {
|
|
841
|
+
className: "min-w-0 space-y-4",
|
|
842
|
+
"aria-busy": props.busy || void 0,
|
|
843
|
+
"aria-describedby": props.error ? errorId : void 0,
|
|
844
|
+
onSubmit: (event) => {
|
|
845
|
+
event.preventDefault();
|
|
846
|
+
if (complete && !props.busy && props.canSubmit !== false) props.onSubmit();
|
|
847
|
+
},
|
|
848
|
+
children: [
|
|
849
|
+
children,
|
|
850
|
+
props.error ? /* @__PURE__ */ jsx("p", {
|
|
851
|
+
id: errorId,
|
|
852
|
+
role: "alert",
|
|
853
|
+
className: "text-sm text-destructive",
|
|
854
|
+
children: props.error
|
|
855
|
+
}) : null,
|
|
856
|
+
/* @__PURE__ */ jsxs(DialogFooter, { children: [/* @__PURE__ */ jsx(Button, {
|
|
857
|
+
type: "button",
|
|
858
|
+
variant: "outline",
|
|
859
|
+
onClick: () => props.onOpenChange(false),
|
|
860
|
+
children: "Cancel"
|
|
861
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
862
|
+
type: "submit",
|
|
863
|
+
disabled: !complete || props.busy || props.canSubmit === false,
|
|
864
|
+
children: props.busy ? "Connecting…" : submitLabel
|
|
865
|
+
})] })
|
|
866
|
+
]
|
|
867
|
+
})]
|
|
868
|
+
})
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
function ApiKeyField({ value, onValueChange, busy, error }) {
|
|
872
|
+
const id = React$1.useId();
|
|
873
|
+
const [revealed, setRevealed] = React$1.useState(false);
|
|
874
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
875
|
+
className: "space-y-2",
|
|
876
|
+
children: [/* @__PURE__ */ jsx("label", {
|
|
877
|
+
htmlFor: id,
|
|
878
|
+
className: "block text-sm font-medium",
|
|
879
|
+
children: "API key"
|
|
880
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
881
|
+
className: "flex min-w-0 gap-2",
|
|
882
|
+
children: [/* @__PURE__ */ jsx("input", {
|
|
883
|
+
id,
|
|
884
|
+
type: revealed ? "text" : "password",
|
|
885
|
+
value,
|
|
886
|
+
required: true,
|
|
887
|
+
onChange: (event) => onValueChange(event.target.value),
|
|
888
|
+
disabled: busy,
|
|
889
|
+
autoComplete: "off",
|
|
890
|
+
autoCapitalize: "none",
|
|
891
|
+
spellCheck: false,
|
|
892
|
+
"aria-invalid": !!error || void 0,
|
|
893
|
+
className: cn(fieldClass, "font-mono")
|
|
894
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
895
|
+
type: "button",
|
|
896
|
+
variant: "outline",
|
|
897
|
+
"aria-label": revealed ? "Hide API key" : "Show API key",
|
|
898
|
+
"aria-pressed": revealed,
|
|
899
|
+
onClick: () => setRevealed((current) => !current),
|
|
900
|
+
disabled: busy,
|
|
901
|
+
children: revealed ? "Hide" : "Show"
|
|
902
|
+
})]
|
|
903
|
+
})]
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
/** Key bytes remain in owner-controlled value/onValueChange; no request or storage. */
|
|
907
|
+
function ApiKeyConnectDialog(props) {
|
|
908
|
+
return /* @__PURE__ */ jsxs(ConnectionDialog, {
|
|
909
|
+
...props,
|
|
910
|
+
complete: props.value.trim().length > 0,
|
|
911
|
+
submitLabel: "Connect",
|
|
912
|
+
children: [/* @__PURE__ */ jsx(ApiKeyField, {
|
|
913
|
+
value: props.value,
|
|
914
|
+
onValueChange: props.onValueChange,
|
|
915
|
+
busy: props.busy,
|
|
916
|
+
error: props.error
|
|
917
|
+
}, props.providerId), props.children]
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
function ParameterFields({ parameters, values, onValueChange, busy, error }) {
|
|
921
|
+
const prefix = React$1.useId();
|
|
922
|
+
return /* @__PURE__ */ jsx(Fragment, { children: parameters.map((parameter) => {
|
|
923
|
+
const id = `${prefix}-${parameter.key}`;
|
|
924
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
925
|
+
className: "space-y-2",
|
|
926
|
+
children: [
|
|
927
|
+
/* @__PURE__ */ jsx("label", {
|
|
928
|
+
htmlFor: id,
|
|
929
|
+
className: "block text-sm font-medium",
|
|
930
|
+
children: parameter.label
|
|
931
|
+
}),
|
|
932
|
+
/* @__PURE__ */ jsx("input", {
|
|
933
|
+
id,
|
|
934
|
+
type: "text",
|
|
935
|
+
value: values[parameter.key] ?? "",
|
|
936
|
+
onChange: (event) => onValueChange(parameter.key, event.target.value),
|
|
937
|
+
required: parameter.required,
|
|
938
|
+
disabled: busy,
|
|
939
|
+
autoComplete: "off",
|
|
940
|
+
autoCapitalize: "none",
|
|
941
|
+
spellCheck: false,
|
|
942
|
+
placeholder: parameter.placeholder,
|
|
943
|
+
"aria-invalid": !!error || void 0,
|
|
944
|
+
"aria-describedby": parameter.description ? `${id}-hint` : void 0,
|
|
945
|
+
className: fieldClass
|
|
946
|
+
}),
|
|
947
|
+
parameter.description ? /* @__PURE__ */ jsx("p", {
|
|
948
|
+
id: `${id}-hint`,
|
|
949
|
+
className: "text-xs text-muted-foreground",
|
|
950
|
+
children: parameter.description
|
|
951
|
+
}) : null
|
|
952
|
+
]
|
|
953
|
+
}, parameter.key);
|
|
954
|
+
}) });
|
|
955
|
+
}
|
|
956
|
+
/** Parameter schema/values and all auth behavior belong to the owner. */
|
|
957
|
+
function OAuthConnectionParameterDialog(props) {
|
|
958
|
+
const complete = props.parameters.every((parameter) => !parameter.required || !!props.values[parameter.key]?.trim());
|
|
959
|
+
return /* @__PURE__ */ jsxs(ConnectionDialog, {
|
|
960
|
+
...props,
|
|
961
|
+
complete,
|
|
962
|
+
submitLabel: "Continue",
|
|
963
|
+
children: [/* @__PURE__ */ jsx(ParameterFields, { ...props }, props.providerId), props.children]
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
//#endregion
|
|
459
967
|
//#region src/integrations/use-integrations.ts
|
|
460
968
|
function unwrap(json) {
|
|
461
969
|
if (json && typeof json === "object" && "data" in json && json.data !== void 0) return json.data;
|
|
462
970
|
return json;
|
|
463
971
|
}
|
|
464
|
-
function
|
|
465
|
-
|
|
466
|
-
|
|
972
|
+
function cancellationError() {
|
|
973
|
+
return Object.assign(/* @__PURE__ */ new Error("Integration lifetime ended"), { name: "AbortError" });
|
|
974
|
+
}
|
|
975
|
+
function assertActive(signal) {
|
|
976
|
+
if (signal.aborted) throw cancellationError();
|
|
977
|
+
}
|
|
978
|
+
/** Also settle when a custom fetch or response body ignores AbortSignal. */
|
|
979
|
+
function whileActive(signal, start) {
|
|
980
|
+
if (signal.aborted) return Promise.reject(cancellationError());
|
|
981
|
+
return new Promise((resolve, reject) => {
|
|
982
|
+
const onAbort = () => reject(cancellationError());
|
|
983
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
984
|
+
Promise.resolve().then(() => {
|
|
985
|
+
assertActive(signal);
|
|
986
|
+
return start();
|
|
987
|
+
}).then((value) => {
|
|
988
|
+
signal.removeEventListener("abort", onAbort);
|
|
989
|
+
if (signal.aborted) reject(cancellationError());
|
|
990
|
+
else resolve(value);
|
|
991
|
+
}, (err) => {
|
|
992
|
+
signal.removeEventListener("abort", onAbort);
|
|
993
|
+
reject(signal.aborted ? cancellationError() : err);
|
|
994
|
+
});
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
function useIntegrations({ apiBaseUrl, fetchImpl, autoLoad = true, refreshOnReturn = false }) {
|
|
998
|
+
const availableFetch = fetchImpl ?? (typeof fetch === "function" ? fetch : null);
|
|
999
|
+
if (!availableFetch) throw new Error("useIntegrations: fetch is not available in this environment");
|
|
1000
|
+
const fetcher = availableFetch;
|
|
467
1001
|
const base = apiBaseUrl.replace(/\/+$/, "");
|
|
468
1002
|
const [catalog, setCatalog] = React$1.useState([]);
|
|
469
1003
|
const [connections, setConnections] = React$1.useState([]);
|
|
470
1004
|
const [healthByConnectionId, setHealthByConnectionId] = React$1.useState({});
|
|
471
1005
|
const [isLoading, setIsLoading] = React$1.useState(autoLoad);
|
|
472
1006
|
const [error, setError] = React$1.useState(null);
|
|
473
|
-
const
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
const catalogJson = unwrap(await catalogRes.json());
|
|
481
|
-
const providers = catalogJson?.catalog?.providers ?? catalogJson?.providers ?? [];
|
|
482
|
-
setCatalog(providers);
|
|
483
|
-
const connectionsJson = unwrap(await connectionsRes.json());
|
|
484
|
-
setConnections(connectionsJson?.connections ?? []);
|
|
1007
|
+
const scope = React$1.useMemo(() => ({ current: null }), [base, fetcher]);
|
|
1008
|
+
React$1.useEffect(() => {
|
|
1009
|
+
const controller = new AbortController();
|
|
1010
|
+
const { signal } = controller;
|
|
1011
|
+
let running = null;
|
|
1012
|
+
let pending = false;
|
|
1013
|
+
async function load() {
|
|
485
1014
|
try {
|
|
486
|
-
const
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
1015
|
+
const [catalogRes, connectionsRes] = await whileActive(signal, () => Promise.all([fetcher(`${base}/catalog`, {
|
|
1016
|
+
credentials: "include",
|
|
1017
|
+
signal
|
|
1018
|
+
}), fetcher(`${base}/connections`, {
|
|
1019
|
+
credentials: "include",
|
|
1020
|
+
signal
|
|
1021
|
+
})]));
|
|
1022
|
+
if (!catalogRes.ok) throw new Error(`Failed to load integration catalog (${catalogRes.status})`);
|
|
1023
|
+
if (!connectionsRes.ok) throw new Error(`Failed to load integration connections (${connectionsRes.status})`);
|
|
1024
|
+
const [catalogBody, connectionsBody] = await whileActive(signal, () => Promise.all([catalogRes.json(), connectionsRes.json()]));
|
|
1025
|
+
const catalogJson = unwrap(catalogBody);
|
|
1026
|
+
const connectionsJson = unwrap(connectionsBody);
|
|
1027
|
+
assertActive(signal);
|
|
1028
|
+
setCatalog(catalogJson?.catalog?.providers ?? catalogJson?.providers ?? []);
|
|
1029
|
+
setConnections(connectionsJson?.connections ?? []);
|
|
1030
|
+
try {
|
|
1031
|
+
const healthRes = await whileActive(signal, () => fetcher(`${base}/healthchecks`, {
|
|
1032
|
+
credentials: "include",
|
|
1033
|
+
signal
|
|
1034
|
+
}));
|
|
1035
|
+
if (healthRes.ok) {
|
|
1036
|
+
const healthJson = unwrap(await whileActive(signal, () => healthRes.json()));
|
|
1037
|
+
const map = {};
|
|
1038
|
+
for (const h of healthJson?.healthchecks ?? []) map[h.connectionId] = h;
|
|
1039
|
+
assertActive(signal);
|
|
1040
|
+
setHealthByConnectionId(map);
|
|
1041
|
+
}
|
|
1042
|
+
} catch {}
|
|
1043
|
+
} catch (err) {
|
|
1044
|
+
if (!signal.aborted) setError(err instanceof Error ? err : new Error(String(err)));
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
function scheduleRefresh() {
|
|
1048
|
+
if (signal.aborted) return Promise.resolve();
|
|
1049
|
+
pending = true;
|
|
1050
|
+
if (!running) {
|
|
1051
|
+
setIsLoading(true);
|
|
1052
|
+
running = Promise.resolve().then(async () => {
|
|
1053
|
+
try {
|
|
1054
|
+
while (pending && !signal.aborted) {
|
|
1055
|
+
pending = false;
|
|
1056
|
+
setError(null);
|
|
1057
|
+
await load();
|
|
1058
|
+
}
|
|
1059
|
+
} finally {
|
|
1060
|
+
running = null;
|
|
1061
|
+
if (!signal.aborted) setIsLoading(false);
|
|
1062
|
+
}
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
return running;
|
|
498
1066
|
}
|
|
499
|
-
|
|
1067
|
+
scope.current = {
|
|
1068
|
+
signal,
|
|
1069
|
+
refresh: scheduleRefresh
|
|
1070
|
+
};
|
|
1071
|
+
setCatalog([]);
|
|
1072
|
+
setConnections([]);
|
|
1073
|
+
setHealthByConnectionId({});
|
|
1074
|
+
setError(null);
|
|
1075
|
+
setIsLoading(false);
|
|
1076
|
+
return () => {
|
|
1077
|
+
scope.current = null;
|
|
1078
|
+
pending = false;
|
|
1079
|
+
controller.abort();
|
|
1080
|
+
};
|
|
1081
|
+
}, [
|
|
1082
|
+
base,
|
|
1083
|
+
fetcher,
|
|
1084
|
+
scope
|
|
1085
|
+
]);
|
|
1086
|
+
const refresh = React$1.useCallback(() => scope.current?.refresh() ?? Promise.resolve(), [scope]);
|
|
500
1087
|
React$1.useEffect(() => {
|
|
501
1088
|
if (autoLoad) refresh();
|
|
502
1089
|
}, [autoLoad, refresh]);
|
|
1090
|
+
React$1.useEffect(() => {
|
|
1091
|
+
if (!refreshOnReturn || typeof window === "undefined" || typeof document === "undefined") return;
|
|
1092
|
+
const onReturn = () => {
|
|
1093
|
+
if (document.visibilityState === "visible") refresh();
|
|
1094
|
+
};
|
|
1095
|
+
const onPageShow = (event) => {
|
|
1096
|
+
if (event.persisted) onReturn();
|
|
1097
|
+
};
|
|
1098
|
+
window.addEventListener("focus", onReturn);
|
|
1099
|
+
document.addEventListener("visibilitychange", onReturn);
|
|
1100
|
+
window.addEventListener("pageshow", onPageShow);
|
|
1101
|
+
return () => {
|
|
1102
|
+
window.removeEventListener("focus", onReturn);
|
|
1103
|
+
document.removeEventListener("visibilitychange", onReturn);
|
|
1104
|
+
window.removeEventListener("pageshow", onPageShow);
|
|
1105
|
+
};
|
|
1106
|
+
}, [refreshOnReturn, refresh]);
|
|
503
1107
|
return {
|
|
504
1108
|
catalog,
|
|
505
1109
|
connections,
|
|
@@ -508,36 +1112,52 @@ function useIntegrations({ apiBaseUrl, fetchImpl, autoLoad = true }) {
|
|
|
508
1112
|
error,
|
|
509
1113
|
refresh,
|
|
510
1114
|
connect: React$1.useCallback(async (input) => {
|
|
511
|
-
const
|
|
1115
|
+
const active = scope.current;
|
|
1116
|
+
if (!active) throw cancellationError();
|
|
1117
|
+
const { signal } = active;
|
|
1118
|
+
const res = await whileActive(signal, () => fetcher(`${base}/auth/start`, {
|
|
512
1119
|
method: "POST",
|
|
513
1120
|
credentials: "include",
|
|
514
1121
|
headers: { "content-type": "application/json" },
|
|
515
|
-
body: JSON.stringify(input)
|
|
516
|
-
|
|
1122
|
+
body: JSON.stringify(input),
|
|
1123
|
+
signal
|
|
1124
|
+
}));
|
|
517
1125
|
if (!res.ok) {
|
|
518
|
-
const text = await res.text().catch(() => "");
|
|
1126
|
+
const text = await whileActive(signal, () => res.text().catch(() => ""));
|
|
1127
|
+
assertActive(signal);
|
|
519
1128
|
throw new Error(`Failed to start OAuth (${res.status}): ${text}`);
|
|
520
1129
|
}
|
|
521
|
-
const json = unwrap(await res.json());
|
|
1130
|
+
const json = unwrap(await whileActive(signal, () => res.json()));
|
|
1131
|
+
assertActive(signal);
|
|
522
1132
|
if (!json?.authorizationUrl) throw new Error("Platform did not return an authorizationUrl");
|
|
523
1133
|
window.location.href = json.authorizationUrl;
|
|
524
|
-
}, [
|
|
1134
|
+
}, [
|
|
1135
|
+
base,
|
|
1136
|
+
fetcher,
|
|
1137
|
+
scope
|
|
1138
|
+
]),
|
|
525
1139
|
disconnect: React$1.useCallback(async (connectionId) => {
|
|
526
|
-
const
|
|
1140
|
+
const active = scope.current;
|
|
1141
|
+
if (!active) throw cancellationError();
|
|
1142
|
+
const { signal } = active;
|
|
1143
|
+
const res = await whileActive(signal, () => fetcher(`${base}/connections/${encodeURIComponent(connectionId)}`, {
|
|
527
1144
|
method: "DELETE",
|
|
528
|
-
credentials: "include"
|
|
529
|
-
|
|
1145
|
+
credentials: "include",
|
|
1146
|
+
signal
|
|
1147
|
+
}));
|
|
530
1148
|
if (!res.ok) {
|
|
531
|
-
const text = await res.text().catch(() => "");
|
|
1149
|
+
const text = await whileActive(signal, () => res.text().catch(() => ""));
|
|
1150
|
+
assertActive(signal);
|
|
532
1151
|
throw new Error(`Failed to revoke connection (${res.status}): ${text}`);
|
|
533
1152
|
}
|
|
534
|
-
await refresh
|
|
1153
|
+
await whileActive(signal, active.refresh);
|
|
1154
|
+
assertActive(signal);
|
|
535
1155
|
}, [
|
|
536
1156
|
base,
|
|
537
1157
|
fetcher,
|
|
538
|
-
|
|
1158
|
+
scope
|
|
539
1159
|
])
|
|
540
1160
|
};
|
|
541
1161
|
}
|
|
542
1162
|
//#endregion
|
|
543
|
-
export { IntegrationsPanel, ProviderIcon, providerLogoCandidates, useIntegrations };
|
|
1163
|
+
export { ApiKeyConnectDialog, ApiKeyConnectDialog as ApiKeyConnectModal, IntegrationConnectionDetail, IntegrationsCatalog, IntegrationsPanel, OAuthConnectionParameterDialog, OAuthConnectionParameterDialog as OAuthConnectionParameterModal, ProviderIcon, catalogRowActive, providerLogoCandidates, useIntegrations };
|