@agent-native/dispatch 0.5.1 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/app-keys-popover.d.ts.map +1 -1
- package/dist/components/app-keys-popover.js +2 -1
- package/dist/components/app-keys-popover.js.map +1 -1
- package/dist/components/create-app-popover.js +1 -1
- package/dist/components/create-app-popover.js.map +1 -1
- package/dist/components/layout/Layout.js +3 -3
- package/dist/components/layout/Layout.js.map +1 -1
- package/dist/routes/index.d.ts.map +1 -1
- package/dist/routes/index.js +5 -0
- package/dist/routes/index.js.map +1 -1
- package/dist/routes/pages/$appId.d.ts +8 -0
- package/dist/routes/pages/$appId.d.ts.map +1 -0
- package/dist/routes/pages/$appId.js +91 -0
- package/dist/routes/pages/$appId.js.map +1 -0
- package/dist/routes/pages/approval.d.ts.map +1 -1
- package/dist/routes/pages/approval.js +2 -1
- package/dist/routes/pages/approval.js.map +1 -1
- package/dist/routes/pages/apps.$appId.d.ts.map +1 -1
- package/dist/routes/pages/apps.$appId.js +2 -1
- package/dist/routes/pages/apps.$appId.js.map +1 -1
- package/dist/routes/pages/integrations.d.ts +1 -1
- package/dist/routes/pages/integrations.d.ts.map +1 -1
- package/dist/routes/pages/integrations.js +131 -30
- package/dist/routes/pages/integrations.js.map +1 -1
- package/dist/routes/pages/overview.d.ts.map +1 -1
- package/dist/routes/pages/overview.js +10 -1
- package/dist/routes/pages/overview.js.map +1 -1
- package/dist/routes/pages/vault.d.ts.map +1 -1
- package/dist/routes/pages/vault.js +4 -3
- package/dist/routes/pages/vault.js.map +1 -1
- package/dist/routes/pages/workspace.d.ts.map +1 -1
- package/dist/routes/pages/workspace.js +5 -3
- package/dist/routes/pages/workspace.js.map +1 -1
- package/dist/server/plugins/integrations.js +1 -1
- package/dist/server/plugins/integrations.js.map +1 -1
- package/package.json +2 -2
- package/src/components/app-keys-popover.tsx +15 -1
- package/src/components/create-app-popover.tsx +1 -1
- package/src/components/layout/Layout.tsx +3 -3
- package/src/routes/index.ts +5 -0
- package/src/routes/pages/$appId.tsx +178 -0
- package/src/routes/pages/approval.tsx +33 -3
- package/src/routes/pages/apps.$appId.tsx +6 -1
- package/src/routes/pages/integrations.tsx +348 -215
- package/src/routes/pages/overview.tsx +58 -26
- package/src/routes/pages/vault.tsx +25 -12
- package/src/routes/pages/workspace.tsx +21 -3
- package/src/server/plugins/integrations.ts +1 -1
|
@@ -1,277 +1,410 @@
|
|
|
1
|
+
import { useMemo, useState } from "react";
|
|
1
2
|
import { useActionMutation, useActionQuery } from "@agent-native/core/client";
|
|
3
|
+
import { useQueryClient } from "@tanstack/react-query";
|
|
2
4
|
import { toast } from "sonner";
|
|
3
5
|
import {
|
|
4
6
|
IconCheck,
|
|
7
|
+
IconChevronRight,
|
|
5
8
|
IconCircleDashed,
|
|
6
9
|
IconKey,
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
IconWifi,
|
|
10
|
-
IconWifiOff,
|
|
10
|
+
IconLink,
|
|
11
|
+
IconPlugConnected,
|
|
11
12
|
} from "@tabler/icons-react";
|
|
12
13
|
import { DispatchShell } from "@/components/dispatch-shell";
|
|
13
14
|
import { Badge } from "@/components/ui/badge";
|
|
14
15
|
import { Button } from "@/components/ui/button";
|
|
15
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
Collapsible,
|
|
18
|
+
CollapsibleContent,
|
|
19
|
+
CollapsibleTrigger,
|
|
20
|
+
} from "@/components/ui/collapsible";
|
|
21
|
+
import {
|
|
22
|
+
Dialog,
|
|
23
|
+
DialogContent,
|
|
24
|
+
DialogDescription,
|
|
25
|
+
DialogFooter,
|
|
26
|
+
DialogHeader,
|
|
27
|
+
DialogTitle,
|
|
28
|
+
} from "@/components/ui/dialog";
|
|
29
|
+
import { Input } from "@/components/ui/input";
|
|
30
|
+
import { Label } from "@/components/ui/label";
|
|
16
31
|
|
|
17
32
|
export function meta() {
|
|
18
|
-
return [{ title: "
|
|
33
|
+
return [{ title: "Connections — Dispatch" }];
|
|
19
34
|
}
|
|
20
35
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
36
|
+
interface AppRef {
|
|
37
|
+
appId: string;
|
|
38
|
+
appName: string;
|
|
39
|
+
color: string;
|
|
25
40
|
configured: boolean;
|
|
26
41
|
vaultGranted: boolean;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
42
|
+
vaultSecretId?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface Service {
|
|
46
|
+
/** Credential key shared across apps (e.g. `OPENAI_API_KEY`). */
|
|
47
|
+
key: string;
|
|
48
|
+
/** Human label from the first app that declares it (`"OpenAI"`, `"Stripe"`). */
|
|
49
|
+
label: string;
|
|
50
|
+
/** Apps in the workspace that declare this credential. */
|
|
51
|
+
apps: AppRef[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface CatalogApp {
|
|
55
|
+
appId: string;
|
|
56
|
+
appName: string;
|
|
57
|
+
color: string;
|
|
58
|
+
url: string;
|
|
59
|
+
reachable: boolean;
|
|
60
|
+
integrations?: Array<{
|
|
61
|
+
key: string;
|
|
62
|
+
label: string;
|
|
63
|
+
required: boolean;
|
|
64
|
+
configured: boolean;
|
|
65
|
+
vaultGranted: boolean;
|
|
66
|
+
vaultSecretId?: string;
|
|
67
|
+
}>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function inferProviderFromKey(key: string, label: string): string {
|
|
71
|
+
const haystack = `${key} ${label}`.toLowerCase();
|
|
72
|
+
for (const provider of [
|
|
73
|
+
"google",
|
|
74
|
+
"slack",
|
|
75
|
+
"sendgrid",
|
|
76
|
+
"github",
|
|
77
|
+
"stripe",
|
|
78
|
+
"hubspot",
|
|
79
|
+
"jira",
|
|
80
|
+
"bigquery",
|
|
81
|
+
"anthropic",
|
|
82
|
+
"openai",
|
|
83
|
+
]) {
|
|
84
|
+
if (haystack.includes(provider)) return provider;
|
|
60
85
|
}
|
|
61
|
-
return
|
|
62
|
-
<Badge
|
|
63
|
-
variant="secondary"
|
|
64
|
-
className="bg-amber-500/10 text-amber-700 dark:text-amber-400"
|
|
65
|
-
>
|
|
66
|
-
<IconCircleDashed size={12} className="mr-1" />
|
|
67
|
-
Missing
|
|
68
|
-
</Badge>
|
|
69
|
-
);
|
|
86
|
+
return "other";
|
|
70
87
|
}
|
|
71
88
|
|
|
72
|
-
function
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
89
|
+
function ConnectDialog({
|
|
90
|
+
service,
|
|
91
|
+
open,
|
|
92
|
+
onOpenChange,
|
|
93
|
+
}: {
|
|
94
|
+
service: Service;
|
|
95
|
+
open: boolean;
|
|
96
|
+
onOpenChange: (next: boolean) => void;
|
|
97
|
+
}) {
|
|
98
|
+
const [value, setValue] = useState("");
|
|
99
|
+
const qc = useQueryClient();
|
|
100
|
+
|
|
101
|
+
const createSecret = useActionMutation("create-vault-secret", {});
|
|
102
|
+
const createGrant = useActionMutation("create-vault-grant", {});
|
|
103
|
+
const syncToApp = useActionMutation("sync-vault-to-app", {});
|
|
104
|
+
|
|
105
|
+
function reset() {
|
|
106
|
+
setValue("");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function handleSave() {
|
|
110
|
+
const trimmed = value.trim();
|
|
111
|
+
if (!trimmed) {
|
|
112
|
+
toast.error("Enter a value to save");
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
// 1. Create the secret (or get the existing one — server treats key as
|
|
117
|
+
// the unique identifier). The server returns { secret: { id, ... } }.
|
|
118
|
+
const created = await createSecret.mutateAsync({
|
|
119
|
+
credentialKey: service.key,
|
|
120
|
+
name: service.label,
|
|
121
|
+
value: trimmed,
|
|
122
|
+
provider: inferProviderFromKey(service.key, service.label),
|
|
123
|
+
});
|
|
124
|
+
const secretId =
|
|
125
|
+
(created as { secret?: { id?: string } })?.secret?.id ??
|
|
126
|
+
(created as { id?: string })?.id;
|
|
127
|
+
if (!secretId) {
|
|
128
|
+
throw new Error("Secret created but id missing");
|
|
129
|
+
}
|
|
78
130
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
131
|
+
// 2. Grant + sync to every app that declared this credential.
|
|
132
|
+
const targets = service.apps.filter((a) => !a.vaultGranted);
|
|
133
|
+
for (const app of targets) {
|
|
134
|
+
try {
|
|
135
|
+
await createGrant.mutateAsync({
|
|
136
|
+
secretId,
|
|
137
|
+
appId: app.appId,
|
|
138
|
+
});
|
|
139
|
+
} catch (err) {
|
|
140
|
+
console.warn(`grant to ${app.appId} failed`, err);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
for (const app of service.apps) {
|
|
144
|
+
try {
|
|
145
|
+
await syncToApp.mutateAsync({ appId: app.appId });
|
|
146
|
+
} catch (err) {
|
|
147
|
+
console.warn(`sync to ${app.appId} failed`, err);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
qc.invalidateQueries({
|
|
152
|
+
queryKey: ["action", "list-integrations-catalog"],
|
|
153
|
+
});
|
|
154
|
+
toast.success(`Connected ${service.label}`);
|
|
155
|
+
onOpenChange(false);
|
|
156
|
+
reset();
|
|
157
|
+
} catch (err: any) {
|
|
158
|
+
toast.error(err?.message ?? "Failed to save credential");
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const pending =
|
|
163
|
+
createSecret.isPending || createGrant.isPending || syncToApp.isPending;
|
|
83
164
|
|
|
84
165
|
return (
|
|
85
|
-
<
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
166
|
+
<Dialog
|
|
167
|
+
open={open}
|
|
168
|
+
onOpenChange={(next) => {
|
|
169
|
+
if (!next) reset();
|
|
170
|
+
onOpenChange(next);
|
|
171
|
+
}}
|
|
172
|
+
>
|
|
173
|
+
<DialogContent>
|
|
174
|
+
<DialogHeader>
|
|
175
|
+
<DialogTitle>Connect {service.label}</DialogTitle>
|
|
176
|
+
<DialogDescription>
|
|
177
|
+
Used by{" "}
|
|
178
|
+
{service.apps.length === 1
|
|
179
|
+
? service.apps[0].appName
|
|
180
|
+
: `${service.apps.length} apps`}
|
|
181
|
+
. Saved to the workspace vault and synced to every app that needs
|
|
182
|
+
it.
|
|
183
|
+
</DialogDescription>
|
|
184
|
+
</DialogHeader>
|
|
185
|
+
<div className="space-y-3">
|
|
186
|
+
<div>
|
|
187
|
+
<Label className="text-xs text-muted-foreground">Key</Label>
|
|
188
|
+
<div className="font-mono text-sm">{service.key}</div>
|
|
93
189
|
</div>
|
|
94
190
|
<div>
|
|
95
|
-
<
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
)
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
<div className="text-xs text-muted-foreground">{app.appId}</div>
|
|
191
|
+
<Label htmlFor="connector-value">Value</Label>
|
|
192
|
+
<Input
|
|
193
|
+
id="connector-value"
|
|
194
|
+
type="password"
|
|
195
|
+
autoComplete="off"
|
|
196
|
+
value={value}
|
|
197
|
+
onChange={(e) => setValue(e.target.value)}
|
|
198
|
+
placeholder={`Paste your ${service.label} key…`}
|
|
199
|
+
autoFocus
|
|
200
|
+
/>
|
|
106
201
|
</div>
|
|
107
202
|
</div>
|
|
108
|
-
<
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
</
|
|
120
|
-
</
|
|
203
|
+
<DialogFooter>
|
|
204
|
+
<Button
|
|
205
|
+
variant="outline"
|
|
206
|
+
onClick={() => onOpenChange(false)}
|
|
207
|
+
disabled={pending}
|
|
208
|
+
>
|
|
209
|
+
Cancel
|
|
210
|
+
</Button>
|
|
211
|
+
<Button onClick={handleSave} disabled={pending || !value.trim()}>
|
|
212
|
+
{pending ? "Saving…" : "Connect"}
|
|
213
|
+
</Button>
|
|
214
|
+
</DialogFooter>
|
|
215
|
+
</DialogContent>
|
|
216
|
+
</Dialog>
|
|
217
|
+
);
|
|
218
|
+
}
|
|
121
219
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
<span>
|
|
127
|
-
{configuredCount}/{total} configured
|
|
128
|
-
</span>
|
|
129
|
-
<span>{coverage}%</span>
|
|
130
|
-
</div>
|
|
131
|
-
<Progress value={coverage} className="mt-2 h-1.5" />
|
|
220
|
+
function ConnectorCard({ service }: { service: Service }) {
|
|
221
|
+
const [open, setOpen] = useState(false);
|
|
222
|
+
const isConnected = service.apps.some((a) => a.configured);
|
|
223
|
+
const appCount = service.apps.length;
|
|
132
224
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
</span>
|
|
144
|
-
{integration.required && (
|
|
145
|
-
<span className="text-xs text-red-500">required</span>
|
|
146
|
-
)}
|
|
147
|
-
</div>
|
|
148
|
-
<div className="font-mono text-xs text-muted-foreground">
|
|
149
|
-
{integration.key}
|
|
150
|
-
</div>
|
|
151
|
-
</div>
|
|
152
|
-
<StatusBadge
|
|
153
|
-
configured={integration.configured}
|
|
154
|
-
vaultGranted={integration.vaultGranted}
|
|
155
|
-
/>
|
|
156
|
-
</div>
|
|
157
|
-
))}
|
|
158
|
-
</div>
|
|
159
|
-
</>
|
|
160
|
-
) : app.reachable ? (
|
|
161
|
-
<div className="py-4 text-center text-sm text-muted-foreground">
|
|
162
|
-
No declared integrations.
|
|
225
|
+
return (
|
|
226
|
+
<>
|
|
227
|
+
<button
|
|
228
|
+
type="button"
|
|
229
|
+
onClick={() => setOpen(true)}
|
|
230
|
+
className="group flex flex-col items-start gap-2 rounded-2xl border bg-card p-5 text-left transition hover:border-foreground/20 hover:bg-card/80 cursor-pointer"
|
|
231
|
+
>
|
|
232
|
+
<div className="flex w-full items-start justify-between gap-2">
|
|
233
|
+
<div className="flex h-9 w-9 items-center justify-center rounded-xl bg-muted">
|
|
234
|
+
<IconKey size={16} className="text-muted-foreground" />
|
|
163
235
|
</div>
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
236
|
+
{isConnected ? (
|
|
237
|
+
<Badge
|
|
238
|
+
variant="secondary"
|
|
239
|
+
className="bg-green-500/10 text-green-700 dark:text-green-400 gap-1"
|
|
240
|
+
>
|
|
241
|
+
<IconCheck size={12} />
|
|
242
|
+
Connected
|
|
243
|
+
</Badge>
|
|
244
|
+
) : (
|
|
245
|
+
<Badge
|
|
246
|
+
variant="secondary"
|
|
247
|
+
className="bg-amber-500/10 text-amber-700 dark:text-amber-400 gap-1"
|
|
248
|
+
>
|
|
249
|
+
<IconCircleDashed size={12} />
|
|
250
|
+
Connect
|
|
251
|
+
</Badge>
|
|
252
|
+
)}
|
|
253
|
+
</div>
|
|
254
|
+
<div className="min-w-0">
|
|
255
|
+
<div className="text-sm font-semibold text-foreground truncate">
|
|
256
|
+
{service.label}
|
|
257
|
+
</div>
|
|
258
|
+
<div className="font-mono text-xs text-muted-foreground/80 truncate">
|
|
259
|
+
{service.key}
|
|
167
260
|
</div>
|
|
261
|
+
</div>
|
|
262
|
+
<div className="text-xs text-muted-foreground">
|
|
263
|
+
Used by {appCount} {appCount === 1 ? "app" : "apps"}
|
|
264
|
+
</div>
|
|
265
|
+
</button>
|
|
266
|
+
<ConnectDialog service={service} open={open} onOpenChange={setOpen} />
|
|
267
|
+
</>
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function PerAppDetailRow({ app }: { app: CatalogApp }) {
|
|
272
|
+
const total = (app.integrations ?? []).length;
|
|
273
|
+
const ok = (app.integrations ?? []).filter((i) => i.configured).length;
|
|
274
|
+
return (
|
|
275
|
+
<div className="flex items-center justify-between border-t px-4 py-2.5 first:border-t-0">
|
|
276
|
+
<div className="flex items-center gap-2 min-w-0">
|
|
277
|
+
<div
|
|
278
|
+
className="h-5 w-5 rounded text-[10px] font-bold text-white flex items-center justify-center shrink-0"
|
|
279
|
+
style={{ backgroundColor: app.color }}
|
|
280
|
+
>
|
|
281
|
+
{app.appName.charAt(0).toUpperCase()}
|
|
282
|
+
</div>
|
|
283
|
+
<span className="text-sm truncate">{app.appName}</span>
|
|
284
|
+
{!app.reachable && (
|
|
285
|
+
<span className="text-xs text-muted-foreground">offline</span>
|
|
168
286
|
)}
|
|
169
287
|
</div>
|
|
288
|
+
<span className="text-xs text-muted-foreground">
|
|
289
|
+
{total === 0 ? "no integrations" : `${ok}/${total}`}
|
|
290
|
+
</span>
|
|
170
291
|
</div>
|
|
171
292
|
);
|
|
172
293
|
}
|
|
173
294
|
|
|
174
|
-
export default function
|
|
295
|
+
export default function ConnectionsRoute() {
|
|
175
296
|
const { data: catalog, isLoading } = useActionQuery(
|
|
176
297
|
"list-integrations-catalog",
|
|
177
298
|
{},
|
|
178
299
|
);
|
|
300
|
+
const apps = (catalog as CatalogApp[]) || [];
|
|
179
301
|
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
302
|
+
const services = useMemo<Service[]>(() => {
|
|
303
|
+
const map = new Map<string, Service>();
|
|
304
|
+
for (const app of apps) {
|
|
305
|
+
for (const intg of app.integrations ?? []) {
|
|
306
|
+
if (!map.has(intg.key)) {
|
|
307
|
+
map.set(intg.key, {
|
|
308
|
+
key: intg.key,
|
|
309
|
+
label: intg.label,
|
|
310
|
+
apps: [],
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
map.get(intg.key)!.apps.push({
|
|
314
|
+
appId: app.appId,
|
|
315
|
+
appName: app.appName,
|
|
316
|
+
color: app.color,
|
|
317
|
+
configured: intg.configured,
|
|
318
|
+
vaultGranted: intg.vaultGranted,
|
|
319
|
+
vaultSecretId: intg.vaultSecretId,
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return Array.from(map.values()).sort((a, b) =>
|
|
324
|
+
a.label.localeCompare(b.label),
|
|
325
|
+
);
|
|
326
|
+
}, [apps]);
|
|
183
327
|
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
0,
|
|
187
|
-
);
|
|
188
|
-
const configuredIntegrations = apps.reduce(
|
|
189
|
-
(sum: number, a: any) =>
|
|
190
|
-
sum + (a.integrations?.filter((i: any) => i.configured)?.length || 0),
|
|
191
|
-
0,
|
|
192
|
-
);
|
|
328
|
+
const available = services.filter((s) => !s.apps.some((a) => a.configured));
|
|
329
|
+
const connected = services.filter((s) => s.apps.some((a) => a.configured));
|
|
193
330
|
|
|
194
331
|
return (
|
|
195
332
|
<DispatchShell
|
|
196
|
-
title="
|
|
197
|
-
description="
|
|
333
|
+
title="Connections"
|
|
334
|
+
description="Connect services once. Apps that need them pick up the key automatically."
|
|
198
335
|
>
|
|
199
|
-
{
|
|
200
|
-
<div className="
|
|
201
|
-
|
|
202
|
-
<div className="text-sm font-medium text-muted-foreground">
|
|
203
|
-
Apps discovered
|
|
204
|
-
</div>
|
|
205
|
-
<div className="mt-2 text-3xl font-semibold text-foreground">
|
|
206
|
-
{apps.length}
|
|
207
|
-
</div>
|
|
208
|
-
<div className="mt-1 text-xs text-muted-foreground">
|
|
209
|
-
{reachableApps.length} reachable
|
|
210
|
-
</div>
|
|
211
|
-
</div>
|
|
212
|
-
<div className="rounded-2xl border bg-card p-5">
|
|
213
|
-
<div className="text-sm font-medium text-muted-foreground">
|
|
214
|
-
Total integrations
|
|
215
|
-
</div>
|
|
216
|
-
<div className="mt-2 text-3xl font-semibold text-foreground">
|
|
217
|
-
{totalIntegrations}
|
|
218
|
-
</div>
|
|
219
|
-
<div className="mt-1 text-xs text-muted-foreground">
|
|
220
|
-
across all apps
|
|
221
|
-
</div>
|
|
222
|
-
</div>
|
|
223
|
-
<div className="rounded-2xl border bg-card p-5">
|
|
224
|
-
<div className="text-sm font-medium text-muted-foreground">
|
|
225
|
-
Configured
|
|
226
|
-
</div>
|
|
227
|
-
<div className="mt-2 text-3xl font-semibold text-foreground">
|
|
228
|
-
{configuredIntegrations}/{totalIntegrations}
|
|
229
|
-
</div>
|
|
230
|
-
<Progress
|
|
231
|
-
value={
|
|
232
|
-
totalIntegrations > 0
|
|
233
|
-
? Math.round(
|
|
234
|
-
(configuredIntegrations / totalIntegrations) * 100,
|
|
235
|
-
)
|
|
236
|
-
: 0
|
|
237
|
-
}
|
|
238
|
-
className="mt-2 h-1.5"
|
|
239
|
-
/>
|
|
240
|
-
</div>
|
|
336
|
+
{isLoading && services.length === 0 && (
|
|
337
|
+
<div className="rounded-2xl border border-dashed px-6 py-12 text-center text-sm text-muted-foreground">
|
|
338
|
+
Discovering apps and credentials…
|
|
241
339
|
</div>
|
|
242
340
|
)}
|
|
243
341
|
|
|
244
|
-
{isLoading && (
|
|
342
|
+
{!isLoading && services.length === 0 && (
|
|
245
343
|
<div className="rounded-2xl border border-dashed px-6 py-12 text-center text-sm text-muted-foreground">
|
|
246
|
-
|
|
344
|
+
No apps with declared integrations are reachable yet.
|
|
247
345
|
</div>
|
|
248
346
|
)}
|
|
249
347
|
|
|
250
|
-
{
|
|
251
|
-
<
|
|
252
|
-
|
|
253
|
-
<
|
|
254
|
-
|
|
255
|
-
|
|
348
|
+
{available.length > 0 && (
|
|
349
|
+
<section>
|
|
350
|
+
<div className="mb-3 flex items-baseline justify-between">
|
|
351
|
+
<h2 className="text-sm font-medium text-foreground">
|
|
352
|
+
Available to connect
|
|
353
|
+
</h2>
|
|
354
|
+
<span className="text-xs text-muted-foreground">
|
|
355
|
+
{available.length}
|
|
356
|
+
</span>
|
|
357
|
+
</div>
|
|
358
|
+
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
|
|
359
|
+
{available.map((service) => (
|
|
360
|
+
<ConnectorCard key={service.key} service={service} />
|
|
361
|
+
))}
|
|
362
|
+
</div>
|
|
363
|
+
</section>
|
|
256
364
|
)}
|
|
257
365
|
|
|
258
|
-
{
|
|
259
|
-
<
|
|
260
|
-
<
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
366
|
+
{connected.length > 0 && (
|
|
367
|
+
<section>
|
|
368
|
+
<div className="mb-3 mt-2 flex items-baseline justify-between">
|
|
369
|
+
<h2 className="text-sm font-medium text-foreground">Connected</h2>
|
|
370
|
+
<span className="text-xs text-muted-foreground">
|
|
371
|
+
{connected.length}
|
|
372
|
+
</span>
|
|
373
|
+
</div>
|
|
374
|
+
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
|
|
375
|
+
{connected.map((service) => (
|
|
376
|
+
<ConnectorCard key={service.key} service={service} />
|
|
266
377
|
))}
|
|
267
378
|
</div>
|
|
268
|
-
</
|
|
379
|
+
</section>
|
|
269
380
|
)}
|
|
270
381
|
|
|
271
|
-
{
|
|
272
|
-
<
|
|
273
|
-
|
|
274
|
-
|
|
382
|
+
{apps.length > 0 && (
|
|
383
|
+
<Collapsible className="mt-6 rounded-2xl border bg-card">
|
|
384
|
+
<CollapsibleTrigger className="flex w-full items-center justify-between px-4 py-3 text-sm">
|
|
385
|
+
<span className="flex items-center gap-2 text-muted-foreground">
|
|
386
|
+
<IconPlugConnected size={14} />
|
|
387
|
+
Per-app status
|
|
388
|
+
</span>
|
|
389
|
+
<IconChevronRight
|
|
390
|
+
size={14}
|
|
391
|
+
className="text-muted-foreground transition group-data-[state=open]:rotate-90"
|
|
392
|
+
/>
|
|
393
|
+
</CollapsibleTrigger>
|
|
394
|
+
<CollapsibleContent>
|
|
395
|
+
<div className="border-t">
|
|
396
|
+
{apps.map((app) => (
|
|
397
|
+
<PerAppDetailRow key={app.appId} app={app} />
|
|
398
|
+
))}
|
|
399
|
+
</div>
|
|
400
|
+
<div className="flex items-center justify-end gap-1.5 border-t px-4 py-2.5 text-xs text-muted-foreground">
|
|
401
|
+
<IconLink size={12} />
|
|
402
|
+
<a href="/vault" className="hover:underline">
|
|
403
|
+
Open vault for advanced sharing
|
|
404
|
+
</a>
|
|
405
|
+
</div>
|
|
406
|
+
</CollapsibleContent>
|
|
407
|
+
</Collapsible>
|
|
275
408
|
)}
|
|
276
409
|
</DispatchShell>
|
|
277
410
|
);
|