@agent-native/dispatch 0.19.2 → 0.20.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/actions/provider-api-register.d.ts +5 -5
- package/dist/actions/start-workspace-app-creation.js +1 -1
- package/dist/actions/start-workspace-app-creation.js.map +1 -1
- package/dist/components/admin-navigation.d.ts +2 -1
- package/dist/components/admin-navigation.d.ts.map +1 -1
- package/dist/components/admin-navigation.js +10 -7
- package/dist/components/admin-navigation.js.map +1 -1
- package/dist/components/app-icon.d.ts +9 -0
- package/dist/components/app-icon.d.ts.map +1 -0
- package/dist/components/app-icon.js +85 -0
- package/dist/components/app-icon.js.map +1 -0
- package/dist/components/app-list-row.d.ts +12 -0
- package/dist/components/app-list-row.d.ts.map +1 -0
- package/dist/components/app-list-row.js +11 -0
- package/dist/components/app-list-row.js.map +1 -0
- package/dist/components/app-open-actions.d.ts +17 -0
- package/dist/components/app-open-actions.d.ts.map +1 -0
- package/dist/components/app-open-actions.js +20 -0
- package/dist/components/app-open-actions.js.map +1 -0
- package/dist/components/connected-app-card.d.ts +2 -1
- package/dist/components/connected-app-card.d.ts.map +1 -1
- package/dist/components/connected-app-card.js +8 -8
- package/dist/components/connected-app-card.js.map +1 -1
- package/dist/components/dispatch-control-plane.d.ts.map +1 -1
- package/dist/components/dispatch-control-plane.js +21 -14
- package/dist/components/dispatch-control-plane.js.map +1 -1
- package/dist/components/layout/Layout.d.ts.map +1 -1
- package/dist/components/layout/Layout.js +40 -19
- package/dist/components/layout/Layout.js.map +1 -1
- package/dist/components/layout/workspace-apps-rail.d.ts.map +1 -1
- package/dist/components/layout/workspace-apps-rail.js +10 -10
- package/dist/components/layout/workspace-apps-rail.js.map +1 -1
- package/dist/components/other-apps-section.d.ts +31 -0
- package/dist/components/other-apps-section.d.ts.map +1 -0
- package/dist/components/other-apps-section.js +59 -0
- package/dist/components/other-apps-section.js.map +1 -0
- package/dist/components/workspace-app-card.d.ts.map +1 -1
- package/dist/components/workspace-app-card.js +38 -28
- package/dist/components/workspace-app-card.js.map +1 -1
- package/dist/components/workspace-template-card.d.ts +8 -2
- package/dist/components/workspace-template-card.d.ts.map +1 -1
- package/dist/components/workspace-template-card.js +17 -13
- package/dist/components/workspace-template-card.js.map +1 -1
- package/dist/lib/other-apps.d.ts.map +1 -1
- package/dist/lib/other-apps.js +5 -1
- package/dist/lib/other-apps.js.map +1 -1
- package/dist/lib/workspace-apps.d.ts +1 -0
- package/dist/lib/workspace-apps.d.ts.map +1 -1
- package/dist/lib/workspace-apps.js +3 -0
- package/dist/lib/workspace-apps.js.map +1 -1
- package/dist/routes/index.d.ts.map +1 -1
- package/dist/routes/index.js +0 -2
- package/dist/routes/index.js.map +1 -1
- package/dist/routes/pages/admin._index.d.ts.map +1 -1
- package/dist/routes/pages/admin._index.js +32 -7
- package/dist/routes/pages/admin._index.js.map +1 -1
- package/dist/routes/pages/apps.$appId.d.ts.map +1 -1
- package/dist/routes/pages/apps.$appId.js +62 -13
- package/dist/routes/pages/apps.$appId.js.map +1 -1
- package/dist/routes/pages/apps.d.ts +1 -1
- package/dist/routes/pages/apps.d.ts.map +1 -1
- package/dist/routes/pages/apps.js +21 -30
- package/dist/routes/pages/apps.js.map +1 -1
- package/dist/server/plugins/agent-chat.js +6 -1
- package/dist/server/plugins/agent-chat.js.map +1 -1
- package/package.json +2 -2
- package/src/actions/start-workspace-app-creation.ts +1 -1
- package/src/components/admin-navigation.tsx +21 -16
- package/src/components/app-icon.tsx +149 -0
- package/src/components/app-list-row.tsx +40 -0
- package/src/components/app-open-actions.tsx +111 -0
- package/src/components/connected-app-card.tsx +30 -34
- package/src/components/dispatch-control-plane.spec.tsx +28 -1
- package/src/components/dispatch-control-plane.tsx +122 -48
- package/src/components/layout/Layout.spec.tsx +4 -4
- package/src/components/layout/Layout.tsx +86 -55
- package/src/components/layout/workspace-apps-rail.tsx +20 -21
- package/src/components/other-apps-section.tsx +195 -0
- package/src/components/workspace-app-card.spec.tsx +58 -32
- package/src/components/workspace-app-card.tsx +222 -158
- package/src/components/workspace-template-card.spec.tsx +45 -7
- package/src/components/workspace-template-card.tsx +74 -81
- package/src/lib/other-apps.spec.ts +31 -0
- package/src/lib/other-apps.ts +9 -1
- package/src/lib/workspace-apps.ts +4 -0
- package/src/routes/index.spec.ts +2 -1
- package/src/routes/index.ts +0 -2
- package/src/routes/pages/admin._index.tsx +85 -17
- package/src/routes/pages/apps.$appId.tsx +144 -57
- package/src/routes/pages/apps.tsx +76 -147
- package/src/server/plugins/agent-chat.spec.ts +5 -0
- package/src/server/plugins/agent-chat.ts +6 -1
|
@@ -66,14 +66,23 @@ describe("WorkspaceTemplateCard", () => {
|
|
|
66
66
|
vi.unstubAllGlobals();
|
|
67
67
|
});
|
|
68
68
|
|
|
69
|
-
it("
|
|
69
|
+
it("keeps template cards concise and moves setup context into the add flow", async () => {
|
|
70
70
|
await act(async () => {
|
|
71
71
|
root.render(<WorkspaceTemplateCard template={template} />);
|
|
72
72
|
});
|
|
73
73
|
|
|
74
74
|
expect(container.textContent).toContain("Weekly report");
|
|
75
|
-
expect(container.textContent).toContain("RevOps");
|
|
76
|
-
expect(container.textContent).toContain(
|
|
75
|
+
expect(container.textContent).not.toContain("RevOps");
|
|
76
|
+
expect(container.textContent).not.toContain(
|
|
77
|
+
"Connect your CRM after installing the app.",
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const trigger = Array.from(container.querySelectorAll("button")).find(
|
|
81
|
+
(button) => button.textContent?.includes("Add app"),
|
|
82
|
+
);
|
|
83
|
+
expect(trigger).not.toBeUndefined();
|
|
84
|
+
await act(async () => trigger?.click());
|
|
85
|
+
expect(document.body.textContent).toContain(
|
|
77
86
|
"Connect your CRM after installing the app.",
|
|
78
87
|
);
|
|
79
88
|
|
|
@@ -103,7 +112,7 @@ describe("WorkspaceTemplateCard", () => {
|
|
|
103
112
|
});
|
|
104
113
|
|
|
105
114
|
const trigger = Array.from(container.querySelectorAll("button")).find(
|
|
106
|
-
(button) => button.textContent?.includes("
|
|
115
|
+
(button) => button.textContent?.includes("Add app"),
|
|
107
116
|
);
|
|
108
117
|
expect(trigger).not.toBeUndefined();
|
|
109
118
|
|
|
@@ -144,6 +153,35 @@ describe("WorkspaceTemplateCard", () => {
|
|
|
144
153
|
);
|
|
145
154
|
});
|
|
146
155
|
|
|
156
|
+
it("keeps add app inside the catalog split-button menu", async () => {
|
|
157
|
+
await act(async () => {
|
|
158
|
+
root.render(<WorkspaceTemplateCard template={template} catalog />);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
expect(
|
|
162
|
+
Array.from(container.querySelectorAll("button")).some((button) =>
|
|
163
|
+
button.textContent?.includes("Add app"),
|
|
164
|
+
),
|
|
165
|
+
).toBe(false);
|
|
166
|
+
|
|
167
|
+
const optionsButton = container.querySelector<HTMLButtonElement>(
|
|
168
|
+
'button[aria-label="Open options for Weekly report"]',
|
|
169
|
+
);
|
|
170
|
+
expect(optionsButton).not.toBeNull();
|
|
171
|
+
await act(async () => {
|
|
172
|
+
optionsButton?.dispatchEvent(
|
|
173
|
+
new MouseEvent("pointerdown", { bubbles: true, button: 0 }),
|
|
174
|
+
);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const addItem = Array.from(
|
|
178
|
+
document.querySelectorAll<HTMLElement>('[role="menuitem"]'),
|
|
179
|
+
).find((item) => item.textContent?.includes("Add app"));
|
|
180
|
+
expect(addItem).not.toBeUndefined();
|
|
181
|
+
await act(async () => addItem?.click());
|
|
182
|
+
expect(document.body.textContent).toContain("Choose the URL-safe id");
|
|
183
|
+
});
|
|
184
|
+
|
|
147
185
|
it("accepts the list action envelope and renders installed state", async () => {
|
|
148
186
|
await act(async () => {
|
|
149
187
|
root.render(
|
|
@@ -166,9 +204,9 @@ describe("WorkspaceTemplateCard", () => {
|
|
|
166
204
|
expect(container.textContent).toContain("Curated templates");
|
|
167
205
|
expect(container.textContent).toContain("Installed");
|
|
168
206
|
expect(container.querySelector('a[href^="https://"]')).toBeNull();
|
|
169
|
-
const
|
|
170
|
-
(button) => button.textContent?.includes("
|
|
207
|
+
const addButton = Array.from(container.querySelectorAll("button")).find(
|
|
208
|
+
(button) => button.textContent?.includes("Add app"),
|
|
171
209
|
);
|
|
172
|
-
expect(
|
|
210
|
+
expect(addButton).not.toHaveProperty("disabled", true);
|
|
173
211
|
});
|
|
174
212
|
});
|
|
@@ -2,26 +2,17 @@ import { useActionMutation } from "@agent-native/core/client/hooks";
|
|
|
2
2
|
import {
|
|
3
3
|
IconArrowUpRight,
|
|
4
4
|
IconCircleCheck,
|
|
5
|
-
|
|
6
|
-
IconExternalLink,
|
|
7
|
-
IconFileText,
|
|
5
|
+
IconPlus,
|
|
8
6
|
IconPlugConnected,
|
|
9
7
|
} from "@tabler/icons-react";
|
|
10
8
|
import { useEffect, useMemo, useState, type ReactNode } from "react";
|
|
11
9
|
import { toast } from "sonner";
|
|
12
10
|
|
|
13
11
|
import { cn } from "../lib/utils";
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
12
|
+
import { AppIcon } from "./app-icon";
|
|
13
|
+
import { AppListRow } from "./app-list-row";
|
|
14
|
+
import { AppOpenActions } from "./app-open-actions";
|
|
16
15
|
import { Button } from "./ui/button";
|
|
17
|
-
import {
|
|
18
|
-
Card,
|
|
19
|
-
CardContent,
|
|
20
|
-
CardDescription,
|
|
21
|
-
CardFooter,
|
|
22
|
-
CardHeader,
|
|
23
|
-
CardTitle,
|
|
24
|
-
} from "./ui/card";
|
|
25
16
|
import {
|
|
26
17
|
Dialog,
|
|
27
18
|
DialogContent,
|
|
@@ -43,6 +34,8 @@ export interface CuratedWorkspaceTemplate {
|
|
|
43
34
|
description?: string | null;
|
|
44
35
|
source?: string | null;
|
|
45
36
|
sourceDescription?: string | null;
|
|
37
|
+
icon?: string | null;
|
|
38
|
+
color?: string | null;
|
|
46
39
|
integrationSetup?: string | null;
|
|
47
40
|
setupNote?: string | null;
|
|
48
41
|
installed?: boolean | null;
|
|
@@ -69,6 +62,7 @@ export interface WorkspaceTemplateLabels {
|
|
|
69
62
|
remixError: string;
|
|
70
63
|
appIdRequired: string;
|
|
71
64
|
source: string;
|
|
65
|
+
openApp: string;
|
|
72
66
|
viewLiveApp: string;
|
|
73
67
|
}
|
|
74
68
|
|
|
@@ -78,12 +72,13 @@ const DEFAULT_LABELS: WorkspaceTemplateLabels = {
|
|
|
78
72
|
cancel: "Cancel",
|
|
79
73
|
integrationSetup: "Integration setup",
|
|
80
74
|
installed: "Installed",
|
|
81
|
-
remix: "
|
|
75
|
+
remix: "Add app",
|
|
82
76
|
remixing: "Creating app…",
|
|
83
77
|
remixSuccess: "Template app creation started.",
|
|
84
|
-
remixError: "Could not
|
|
78
|
+
remixError: "Could not add this app",
|
|
85
79
|
appIdRequired: "App ID is required.",
|
|
86
80
|
source: "Source",
|
|
81
|
+
openApp: "Open app",
|
|
87
82
|
viewLiveApp: "View the live app",
|
|
88
83
|
};
|
|
89
84
|
|
|
@@ -92,6 +87,7 @@ export interface WorkspaceTemplateCardProps {
|
|
|
92
87
|
defaultAppId?: string;
|
|
93
88
|
labels?: Partial<WorkspaceTemplateLabels>;
|
|
94
89
|
className?: string;
|
|
90
|
+
catalog?: boolean;
|
|
95
91
|
onRemixSuccess?: (
|
|
96
92
|
result: unknown,
|
|
97
93
|
template: CuratedWorkspaceTemplate,
|
|
@@ -119,7 +115,7 @@ function defaultAppIdFor(
|
|
|
119
115
|
): string {
|
|
120
116
|
if (defaultAppId) return slugifyAppId(defaultAppId);
|
|
121
117
|
const sourceId = slugifyAppId(template.appId || template.name);
|
|
122
|
-
return `${sourceId}-
|
|
118
|
+
return `${sourceId}-app`;
|
|
123
119
|
}
|
|
124
120
|
|
|
125
121
|
function stringifyError(error: unknown): string {
|
|
@@ -139,6 +135,7 @@ export function WorkspaceTemplateCard({
|
|
|
139
135
|
defaultAppId,
|
|
140
136
|
labels: labelOverrides,
|
|
141
137
|
className,
|
|
138
|
+
catalog = false,
|
|
142
139
|
onRemixSuccess,
|
|
143
140
|
}: WorkspaceTemplateCardProps) {
|
|
144
141
|
const labels = useMemo(() => mergeLabels(labelOverrides), [labelOverrides]);
|
|
@@ -189,75 +186,53 @@ export function WorkspaceTemplateCard({
|
|
|
189
186
|
}
|
|
190
187
|
|
|
191
188
|
return (
|
|
192
|
-
<
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
<CardTitle className="truncate text-sm font-semibold">
|
|
206
|
-
{template.name}
|
|
207
|
-
</CardTitle>
|
|
208
|
-
{template.source || template.sourceDescription ? (
|
|
209
|
-
<CardDescription className="mt-1 truncate text-xs">
|
|
210
|
-
<span className="font-medium text-foreground/70">
|
|
211
|
-
{labels.source}:
|
|
212
|
-
</span>{" "}
|
|
213
|
-
{template.sourceDescription || template.source}
|
|
214
|
-
</CardDescription>
|
|
215
|
-
) : null}
|
|
216
|
-
</div>
|
|
217
|
-
</div>
|
|
189
|
+
<AppListRow className={cn("items-center", className)}>
|
|
190
|
+
<AppIcon
|
|
191
|
+
id={template.id || template.templateId || template.name}
|
|
192
|
+
name={template.name}
|
|
193
|
+
icon={template.icon || undefined}
|
|
194
|
+
color={template.color || undefined}
|
|
195
|
+
size="sm"
|
|
196
|
+
/>
|
|
197
|
+
<div className="min-w-0 flex-1">
|
|
198
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
199
|
+
<h3 className="truncate text-sm font-semibold text-foreground">
|
|
200
|
+
{template.name}
|
|
201
|
+
</h3>
|
|
218
202
|
{isInstalled ? (
|
|
219
|
-
<
|
|
220
|
-
variant="outline"
|
|
221
|
-
className="shrink-0 gap-1 border-primary/30 bg-primary/5 text-primary"
|
|
222
|
-
>
|
|
203
|
+
<span className="inline-flex shrink-0 items-center gap-1 text-xs text-primary">
|
|
223
204
|
<IconCircleCheck size={13} />
|
|
224
205
|
{labels.installed}
|
|
225
|
-
</
|
|
206
|
+
</span>
|
|
226
207
|
) : null}
|
|
227
208
|
</div>
|
|
228
|
-
</CardHeader>
|
|
229
|
-
|
|
230
|
-
<CardContent className="flex flex-1 flex-col gap-3 p-4 pt-0">
|
|
231
209
|
{template.description ? (
|
|
232
|
-
<p className="
|
|
210
|
+
<p className="mt-0.5 truncate text-xs text-muted-foreground">
|
|
233
211
|
{template.description}
|
|
234
212
|
</p>
|
|
235
213
|
) : null}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
<Button variant="link" size="sm" className="h-8 px-0" asChild>
|
|
214
|
+
</div>
|
|
215
|
+
<div className="flex shrink-0 items-center gap-2">
|
|
216
|
+
{catalog ? (
|
|
217
|
+
<AppOpenActions
|
|
218
|
+
name={template.name}
|
|
219
|
+
href={liveUrl ?? null}
|
|
220
|
+
target="_blank"
|
|
221
|
+
rel="noreferrer"
|
|
222
|
+
labels={{
|
|
223
|
+
addApp: labels.remix,
|
|
224
|
+
openApp: labels.openApp,
|
|
225
|
+
}}
|
|
226
|
+
onAddApp={() => setOpen(true)}
|
|
227
|
+
/>
|
|
228
|
+
) : liveUrl ? (
|
|
229
|
+
<Button variant="ghost" size="sm" className="shrink-0" asChild>
|
|
253
230
|
<a href={liveUrl} target="_blank" rel="noreferrer">
|
|
254
231
|
{labels.viewLiveApp}
|
|
255
|
-
<
|
|
232
|
+
<IconArrowUpRight />
|
|
256
233
|
</a>
|
|
257
234
|
</Button>
|
|
258
|
-
) :
|
|
259
|
-
<span />
|
|
260
|
-
)}
|
|
235
|
+
) : null}
|
|
261
236
|
|
|
262
237
|
<Dialog
|
|
263
238
|
open={open}
|
|
@@ -265,12 +240,14 @@ export function WorkspaceTemplateCard({
|
|
|
265
240
|
if (!remix.isPending) setOpen(nextOpen);
|
|
266
241
|
}}
|
|
267
242
|
>
|
|
268
|
-
|
|
269
|
-
<
|
|
270
|
-
<
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
243
|
+
{!catalog ? (
|
|
244
|
+
<DialogTrigger asChild>
|
|
245
|
+
<Button type="button" variant="outline" size="sm">
|
|
246
|
+
<IconPlus />
|
|
247
|
+
{labels.remix}
|
|
248
|
+
</Button>
|
|
249
|
+
</DialogTrigger>
|
|
250
|
+
) : null}
|
|
274
251
|
<DialogContent className="max-w-md">
|
|
275
252
|
<DialogHeader>
|
|
276
253
|
<DialogTitle>{template.name}</DialogTitle>
|
|
@@ -297,6 +274,17 @@ export function WorkspaceTemplateCard({
|
|
|
297
274
|
disabled={remix.isPending}
|
|
298
275
|
/>
|
|
299
276
|
</div>
|
|
277
|
+
{setupNote ? (
|
|
278
|
+
<div className="flex items-start gap-2 rounded-lg bg-muted px-3 py-2 text-xs leading-5 text-muted-foreground">
|
|
279
|
+
<IconPlugConnected className="mt-0.5 shrink-0" size={15} />
|
|
280
|
+
<span>
|
|
281
|
+
<span className="font-medium text-foreground">
|
|
282
|
+
{labels.integrationSetup}:
|
|
283
|
+
</span>{" "}
|
|
284
|
+
{setupNote}
|
|
285
|
+
</span>
|
|
286
|
+
</div>
|
|
287
|
+
) : null}
|
|
300
288
|
<DialogFooter>
|
|
301
289
|
<Button
|
|
302
290
|
type="button"
|
|
@@ -314,8 +302,8 @@ export function WorkspaceTemplateCard({
|
|
|
314
302
|
</form>
|
|
315
303
|
</DialogContent>
|
|
316
304
|
</Dialog>
|
|
317
|
-
</
|
|
318
|
-
</
|
|
305
|
+
</div>
|
|
306
|
+
</AppListRow>
|
|
319
307
|
);
|
|
320
308
|
}
|
|
321
309
|
|
|
@@ -326,6 +314,8 @@ export interface WorkspaceTemplatesSectionProps {
|
|
|
326
314
|
labels?: Partial<WorkspaceTemplateLabels>;
|
|
327
315
|
className?: string;
|
|
328
316
|
cardClassName?: string;
|
|
317
|
+
listClassName?: string;
|
|
318
|
+
catalog?: boolean;
|
|
329
319
|
onRemixSuccess?: (
|
|
330
320
|
result: unknown,
|
|
331
321
|
template: CuratedWorkspaceTemplate,
|
|
@@ -345,6 +335,8 @@ export function WorkspaceTemplatesSection({
|
|
|
345
335
|
labels,
|
|
346
336
|
className,
|
|
347
337
|
cardClassName,
|
|
338
|
+
listClassName,
|
|
339
|
+
catalog = false,
|
|
348
340
|
onRemixSuccess,
|
|
349
341
|
}: WorkspaceTemplatesSectionProps) {
|
|
350
342
|
const templates = getTemplateItems(result);
|
|
@@ -354,13 +346,14 @@ export function WorkspaceTemplatesSection({
|
|
|
354
346
|
return (
|
|
355
347
|
<section className={cn("flex flex-col gap-3", className)}>
|
|
356
348
|
{title ? <div className="text-sm font-semibold">{title}</div> : null}
|
|
357
|
-
<div className="
|
|
349
|
+
<div className={cn("overflow-hidden rounded-2xl bg-card", listClassName)}>
|
|
358
350
|
{templates.map((template) => (
|
|
359
351
|
<WorkspaceTemplateCard
|
|
360
352
|
key={templateIdFor(template)}
|
|
361
353
|
template={template}
|
|
362
354
|
defaultAppId={defaultAppId}
|
|
363
355
|
labels={labels}
|
|
356
|
+
catalog={catalog}
|
|
364
357
|
className={cardClassName}
|
|
365
358
|
onRemixSuccess={onRemixSuccess}
|
|
366
359
|
/>
|
|
@@ -73,4 +73,35 @@ describe("filterOtherApps", () => {
|
|
|
73
73
|
},
|
|
74
74
|
]);
|
|
75
75
|
});
|
|
76
|
+
|
|
77
|
+
it("hides the unsupported CRM and Research entries", () => {
|
|
78
|
+
expect(
|
|
79
|
+
filterOtherApps(
|
|
80
|
+
[
|
|
81
|
+
{
|
|
82
|
+
id: "crm",
|
|
83
|
+
name: "CRM",
|
|
84
|
+
url: "https://crm.example.com",
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
id: "research",
|
|
88
|
+
name: "Research",
|
|
89
|
+
url: "https://research.example.com",
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
id: "mail",
|
|
93
|
+
name: "Mail",
|
|
94
|
+
url: "https://mail.agent-native.com",
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
[],
|
|
98
|
+
),
|
|
99
|
+
).toEqual([
|
|
100
|
+
{
|
|
101
|
+
id: "mail",
|
|
102
|
+
name: "Mail",
|
|
103
|
+
url: "https://mail.agent-native.com",
|
|
104
|
+
},
|
|
105
|
+
]);
|
|
106
|
+
});
|
|
76
107
|
});
|
package/src/lib/other-apps.ts
CHANGED
|
@@ -12,6 +12,8 @@ export interface WorkspaceAppId {
|
|
|
12
12
|
isDispatch?: boolean;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
const HIDDEN_OTHER_APP_IDS = new Set(["crm", "research"]);
|
|
16
|
+
|
|
15
17
|
function isHttpUrl(value: string): boolean {
|
|
16
18
|
try {
|
|
17
19
|
const url = new URL(value);
|
|
@@ -34,7 +36,13 @@ export function filterOtherApps(
|
|
|
34
36
|
return connectedApps
|
|
35
37
|
.filter((app) => {
|
|
36
38
|
const id = app.id.trim().toLowerCase();
|
|
37
|
-
if (
|
|
39
|
+
if (
|
|
40
|
+
!id ||
|
|
41
|
+
HIDDEN_OTHER_APP_IDS.has(id) ||
|
|
42
|
+
workspaceAppIds.has(id) ||
|
|
43
|
+
seen.has(id)
|
|
44
|
+
)
|
|
45
|
+
return false;
|
|
38
46
|
if (app.source === "workspace") return false;
|
|
39
47
|
if (!isHttpUrl(app.url)) return false;
|
|
40
48
|
seen.add(id);
|
|
@@ -26,6 +26,10 @@ export interface WorkspaceAppSummary {
|
|
|
26
26
|
archived?: boolean;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
export function workspaceAppRoute(appId: string): string {
|
|
30
|
+
return `/apps/${encodeURIComponent(appId)}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
29
33
|
export function workspaceAppHref(app: WorkspaceAppSummary): string | null {
|
|
30
34
|
if (app.status === "pending") {
|
|
31
35
|
return app.builderUrl
|
package/src/routes/index.spec.ts
CHANGED
|
@@ -31,7 +31,8 @@ describe("Dispatch route registration", () => {
|
|
|
31
31
|
const admin = routes.find((route) => route.path === "admin");
|
|
32
32
|
const adminPaths = admin?.children?.map((route) => route.path) ?? [];
|
|
33
33
|
expect(adminPaths).toContain("operations");
|
|
34
|
-
expect(adminPaths).toContain("apps
|
|
34
|
+
expect(adminPaths).not.toContain("apps");
|
|
35
|
+
expect(adminPaths).not.toContain("apps/:appId");
|
|
35
36
|
expect(adminPaths.indexOf("operations")).toBeGreaterThanOrEqual(0);
|
|
36
37
|
});
|
|
37
38
|
});
|
package/src/routes/index.ts
CHANGED
|
@@ -38,8 +38,6 @@ export const dispatchRoutes: RouteConfig = [
|
|
|
38
38
|
route("overview", "./pages/overview.js"),
|
|
39
39
|
route("admin", "./pages/admin.js", [
|
|
40
40
|
index("./pages/admin._index.js"),
|
|
41
|
-
route("apps", "./pages/apps.js"),
|
|
42
|
-
route("apps/:appId", "./pages/apps.$appId.js"),
|
|
43
41
|
route("new-app", "./pages/new-app.js"),
|
|
44
42
|
route("operations", "./pages/operations.js"),
|
|
45
43
|
route("metrics", "./pages/metrics.js"),
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { useT } from "@agent-native/core/client/i18n";
|
|
2
|
+
import { IconChevronRight } from "@tabler/icons-react";
|
|
3
|
+
import { Link } from "react-router";
|
|
2
4
|
|
|
5
|
+
import {
|
|
6
|
+
useAdminNavGroups,
|
|
7
|
+
type AdminNavGroup,
|
|
8
|
+
} from "../../components/admin-navigation";
|
|
3
9
|
import { DispatchShell } from "../../components/dispatch-shell";
|
|
4
10
|
|
|
5
11
|
export function meta() {
|
|
@@ -8,6 +14,7 @@ export function meta() {
|
|
|
8
14
|
|
|
9
15
|
export default function AdminOverviewRoute() {
|
|
10
16
|
const t = useT();
|
|
17
|
+
const groups = useAdminNavGroups();
|
|
11
18
|
|
|
12
19
|
return (
|
|
13
20
|
<DispatchShell
|
|
@@ -16,24 +23,85 @@ export default function AdminOverviewRoute() {
|
|
|
16
23
|
defaultValue: "Workspace controls and operations",
|
|
17
24
|
})}
|
|
18
25
|
>
|
|
19
|
-
<section className="
|
|
20
|
-
|
|
21
|
-
{
|
|
22
|
-
|
|
23
|
-
})}
|
|
24
|
-
</h2>
|
|
25
|
-
<p className="text-sm leading-6 text-muted-foreground">
|
|
26
|
-
{t("dispatch.pages.adminChooseArea", {
|
|
27
|
-
defaultValue:
|
|
28
|
-
"Manage apps, connections, automations, and operational tools from one place.",
|
|
29
|
-
})}
|
|
30
|
-
</p>
|
|
31
|
-
<p className="text-sm leading-6 text-muted-foreground">
|
|
32
|
-
{t("dispatch.pages.adminChooseAreaHint", {
|
|
33
|
-
defaultValue: "Choose an area from the Admin navigation.",
|
|
34
|
-
})}
|
|
35
|
-
</p>
|
|
26
|
+
<section className="grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
|
|
27
|
+
{groups.map((group, index) => (
|
|
28
|
+
<AdminAreaCard key={group.id} group={group} index={index} />
|
|
29
|
+
))}
|
|
36
30
|
</section>
|
|
37
31
|
</DispatchShell>
|
|
38
32
|
);
|
|
39
33
|
}
|
|
34
|
+
|
|
35
|
+
const GROUP_DESCRIPTIONS: Record<string, string> = {
|
|
36
|
+
workspace: "Apps, resources, and workspace setup",
|
|
37
|
+
operations: "Monitor runs, metrics, and audit history",
|
|
38
|
+
automation: "Control scheduled work and delivery",
|
|
39
|
+
connections: "Manage integrations, secrets, and messaging",
|
|
40
|
+
"agent-platform": "Manage connected agents and dreams",
|
|
41
|
+
"workspace-extensions": "Tools added by this workspace",
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const GROUP_TONES = [
|
|
45
|
+
"bg-primary/10 text-primary",
|
|
46
|
+
"bg-secondary text-secondary-foreground",
|
|
47
|
+
"bg-accent text-accent-foreground",
|
|
48
|
+
"bg-muted text-foreground",
|
|
49
|
+
] as const;
|
|
50
|
+
|
|
51
|
+
function AdminAreaCard({
|
|
52
|
+
group,
|
|
53
|
+
index,
|
|
54
|
+
}: {
|
|
55
|
+
group: AdminNavGroup;
|
|
56
|
+
index: number;
|
|
57
|
+
}) {
|
|
58
|
+
const t = useT();
|
|
59
|
+
const GroupIcon = group.items[0]?.icon;
|
|
60
|
+
const tone = GROUP_TONES[index % GROUP_TONES.length];
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<div className="rounded-2xl border border-transparent bg-card p-4 transition-[border-color,background-color] hover:border-border hover:bg-card/80">
|
|
64
|
+
<div className="flex items-start gap-3">
|
|
65
|
+
<span
|
|
66
|
+
className={`flex size-10 shrink-0 items-center justify-center rounded-xl ${tone}`}
|
|
67
|
+
>
|
|
68
|
+
{GroupIcon ? <GroupIcon size={19} /> : null}
|
|
69
|
+
</span>
|
|
70
|
+
<div className="min-w-0">
|
|
71
|
+
<h2 className="text-sm font-semibold text-foreground">
|
|
72
|
+
{t(group.labelKey, { defaultValue: group.label })}
|
|
73
|
+
</h2>
|
|
74
|
+
<p className="mt-1 text-xs leading-5 text-muted-foreground">
|
|
75
|
+
{GROUP_DESCRIPTIONS[group.id] || "Workspace tools and controls"}
|
|
76
|
+
</p>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<div className="mt-4 space-y-1">
|
|
81
|
+
{group.items
|
|
82
|
+
.filter((item) => item.id !== "admin-overview")
|
|
83
|
+
.map((item) => {
|
|
84
|
+
const ItemIcon = item.icon;
|
|
85
|
+
return (
|
|
86
|
+
<Link
|
|
87
|
+
key={item.id}
|
|
88
|
+
to={item.adminTo ?? item.to}
|
|
89
|
+
className="group flex min-h-9 items-center gap-2 rounded-lg px-2 text-sm text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
|
|
90
|
+
>
|
|
91
|
+
{ItemIcon ? <ItemIcon size={15} className="shrink-0" /> : null}
|
|
92
|
+
<span className="min-w-0 flex-1 truncate">
|
|
93
|
+
{t(`dispatch.nav.${item.id}`, {
|
|
94
|
+
defaultValue: item.label,
|
|
95
|
+
})}
|
|
96
|
+
</span>
|
|
97
|
+
<IconChevronRight
|
|
98
|
+
size={15}
|
|
99
|
+
className="shrink-0 opacity-0 transition-opacity group-hover:opacity-100"
|
|
100
|
+
/>
|
|
101
|
+
</Link>
|
|
102
|
+
);
|
|
103
|
+
})}
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
}
|