@agent-native/dispatch 0.19.1 → 0.20.0
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 +9 -9
- 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/package.json +3 -3
- 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
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { IconChevronDown, IconPlus } from "@tabler/icons-react";
|
|
2
|
+
|
|
3
|
+
import { Button } from "./ui/button";
|
|
4
|
+
import {
|
|
5
|
+
DropdownMenu,
|
|
6
|
+
DropdownMenuContent,
|
|
7
|
+
DropdownMenuItem,
|
|
8
|
+
DropdownMenuTrigger,
|
|
9
|
+
} from "./ui/dropdown-menu";
|
|
10
|
+
|
|
11
|
+
export interface AppOpenActionLabels {
|
|
12
|
+
addApp: string;
|
|
13
|
+
openApp: string;
|
|
14
|
+
openInline: string;
|
|
15
|
+
openInNewTab: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const DEFAULT_LABELS: AppOpenActionLabels = {
|
|
19
|
+
addApp: "Add app",
|
|
20
|
+
openApp: "Open app",
|
|
21
|
+
openInline: "Open inline",
|
|
22
|
+
openInNewTab: "Open in new tab",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export function AppOpenActions({
|
|
26
|
+
name,
|
|
27
|
+
href,
|
|
28
|
+
target,
|
|
29
|
+
rel,
|
|
30
|
+
labels: labelOverrides,
|
|
31
|
+
onAddApp,
|
|
32
|
+
showInlineOption = false,
|
|
33
|
+
showNewTabOption = false,
|
|
34
|
+
}: {
|
|
35
|
+
name: string;
|
|
36
|
+
href: string | null;
|
|
37
|
+
target?: "_blank";
|
|
38
|
+
rel?: string;
|
|
39
|
+
labels?: Partial<AppOpenActionLabels>;
|
|
40
|
+
onAddApp?: () => void;
|
|
41
|
+
showInlineOption?: boolean;
|
|
42
|
+
showNewTabOption?: boolean;
|
|
43
|
+
}) {
|
|
44
|
+
const labels = { ...DEFAULT_LABELS, ...labelOverrides };
|
|
45
|
+
const hasMenu =
|
|
46
|
+
Boolean(onAddApp) ||
|
|
47
|
+
(Boolean(href) && (showInlineOption || showNewTabOption));
|
|
48
|
+
|
|
49
|
+
if (!href && !hasMenu) {
|
|
50
|
+
return (
|
|
51
|
+
<Button size="sm" variant="outline" disabled>
|
|
52
|
+
{labels.openApp}
|
|
53
|
+
</Button>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div className="flex shrink-0 overflow-hidden rounded-md border border-border focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2">
|
|
59
|
+
<Button
|
|
60
|
+
asChild={Boolean(href)}
|
|
61
|
+
size="sm"
|
|
62
|
+
variant="outline"
|
|
63
|
+
className="rounded-none border-0 border-e border-border"
|
|
64
|
+
disabled={!href}
|
|
65
|
+
>
|
|
66
|
+
{href ? (
|
|
67
|
+
<a href={href} target={target} rel={rel}>
|
|
68
|
+
{labels.openApp}
|
|
69
|
+
</a>
|
|
70
|
+
) : (
|
|
71
|
+
<span>{labels.openApp}</span>
|
|
72
|
+
)}
|
|
73
|
+
</Button>
|
|
74
|
+
{hasMenu ? (
|
|
75
|
+
<DropdownMenu>
|
|
76
|
+
<DropdownMenuTrigger asChild>
|
|
77
|
+
<Button
|
|
78
|
+
type="button"
|
|
79
|
+
size="sm"
|
|
80
|
+
variant="outline"
|
|
81
|
+
className="size-9 rounded-none border-0 p-0"
|
|
82
|
+
aria-label={`Open options for ${name}`}
|
|
83
|
+
>
|
|
84
|
+
<IconChevronDown size={15} />
|
|
85
|
+
</Button>
|
|
86
|
+
</DropdownMenuTrigger>
|
|
87
|
+
<DropdownMenuContent align="end" className="min-w-40">
|
|
88
|
+
{onAddApp ? (
|
|
89
|
+
<DropdownMenuItem onSelect={onAddApp}>
|
|
90
|
+
<IconPlus size={14} className="mr-2" />
|
|
91
|
+
{labels.addApp}
|
|
92
|
+
</DropdownMenuItem>
|
|
93
|
+
) : null}
|
|
94
|
+
{showInlineOption && href ? (
|
|
95
|
+
<DropdownMenuItem asChild>
|
|
96
|
+
<a href={href}>{labels.openInline}</a>
|
|
97
|
+
</DropdownMenuItem>
|
|
98
|
+
) : null}
|
|
99
|
+
{showNewTabOption && href ? (
|
|
100
|
+
<DropdownMenuItem asChild>
|
|
101
|
+
<a href={href} target="_blank" rel="noreferrer">
|
|
102
|
+
{labels.openInNewTab}
|
|
103
|
+
</a>
|
|
104
|
+
</DropdownMenuItem>
|
|
105
|
+
) : null}
|
|
106
|
+
</DropdownMenuContent>
|
|
107
|
+
</DropdownMenu>
|
|
108
|
+
) : null}
|
|
109
|
+
</div>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
@@ -1,42 +1,38 @@
|
|
|
1
|
-
import { useT } from "@agent-native/core/client/i18n";
|
|
2
1
|
import { withBuilderUtmTrackingParams } from "@agent-native/core/shared/builder-link-tracking";
|
|
3
|
-
import { IconArrowUpRight } from "@tabler/icons-react";
|
|
4
2
|
|
|
5
3
|
import type { ConnectedAppSummary } from "../lib/other-apps";
|
|
4
|
+
import { AppIcon } from "./app-icon";
|
|
5
|
+
import { AppListRow } from "./app-list-row";
|
|
6
|
+
import { AppOpenActions } from "./app-open-actions";
|
|
6
7
|
|
|
7
|
-
export function ConnectedAppCard({
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
export function ConnectedAppCard({
|
|
9
|
+
app,
|
|
10
|
+
className,
|
|
11
|
+
}: {
|
|
12
|
+
app: ConnectedAppSummary;
|
|
13
|
+
className?: string;
|
|
14
|
+
}) {
|
|
10
15
|
return (
|
|
11
|
-
<
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
className="group flex min-h-[116px] items-start gap-3 rounded-xl bg-card/40 p-4 transition-[background-color] hover:bg-accent/15 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
19
|
-
>
|
|
20
|
-
<span className="flex size-8 shrink-0 items-center justify-center rounded-md bg-muted text-xs font-semibold text-muted-foreground">
|
|
21
|
-
{app.name.charAt(0).toUpperCase()}
|
|
22
|
-
</span>
|
|
23
|
-
<span className="min-w-0 flex-1">
|
|
24
|
-
<span className="flex items-start justify-between gap-3">
|
|
25
|
-
<span className="truncate text-sm font-semibold text-foreground">
|
|
26
|
-
{app.name}
|
|
27
|
-
</span>
|
|
28
|
-
<IconArrowUpRight
|
|
29
|
-
size={15}
|
|
30
|
-
className="shrink-0 text-muted-foreground transition-transform group-hover:-translate-y-0.5 group-hover:translate-x-0.5"
|
|
31
|
-
/>
|
|
32
|
-
</span>
|
|
33
|
-
<span className="mt-2 line-clamp-2 block text-[13px] leading-5 text-muted-foreground">
|
|
16
|
+
<AppListRow className={className}>
|
|
17
|
+
<AppIcon id={app.id} name={app.name} color={app.color} size="sm" />
|
|
18
|
+
<div className="min-w-0 flex-1">
|
|
19
|
+
<div className="truncate text-sm font-semibold text-foreground">
|
|
20
|
+
{app.name}
|
|
21
|
+
</div>
|
|
22
|
+
<div className="mt-0.5 truncate text-xs text-muted-foreground">
|
|
34
23
|
{app.description || app.url}
|
|
35
|
-
</
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
<AppOpenActions
|
|
27
|
+
name={app.name}
|
|
28
|
+
href={withBuilderUtmTrackingParams(app.url, {
|
|
29
|
+
campaign: "product",
|
|
30
|
+
content: "dispatch_app",
|
|
31
|
+
})}
|
|
32
|
+
target="_blank"
|
|
33
|
+
rel="noopener noreferrer"
|
|
34
|
+
showNewTabOption
|
|
35
|
+
/>
|
|
36
|
+
</AppListRow>
|
|
41
37
|
);
|
|
42
38
|
}
|
|
@@ -12,6 +12,7 @@ const clientState = vi.hoisted(() => ({
|
|
|
12
12
|
promptComposerProps: null as Record<string, unknown> | null,
|
|
13
13
|
workspaceApps: [] as Array<Record<string, unknown>>,
|
|
14
14
|
connectedApps: [] as Array<Record<string, unknown>>,
|
|
15
|
+
curatedTemplates: [] as Array<Record<string, unknown>>,
|
|
15
16
|
useChatModels: vi.fn(() => ({
|
|
16
17
|
availableModels: [],
|
|
17
18
|
defaultModel: "auto",
|
|
@@ -56,7 +57,9 @@ vi.mock("@agent-native/core/client/hooks", () => ({
|
|
|
56
57
|
data:
|
|
57
58
|
name === "list-connected-agents"
|
|
58
59
|
? clientState.connectedApps
|
|
59
|
-
:
|
|
60
|
+
: name === "list-curated-workspace-templates"
|
|
61
|
+
? clientState.curatedTemplates
|
|
62
|
+
: clientState.workspaceApps,
|
|
60
63
|
isLoading: false,
|
|
61
64
|
isError: false,
|
|
62
65
|
error: null,
|
|
@@ -89,6 +92,7 @@ describe("DispatchControlPlane", () => {
|
|
|
89
92
|
clientState.promptComposerProps = null;
|
|
90
93
|
clientState.workspaceApps = [];
|
|
91
94
|
clientState.connectedApps = [];
|
|
95
|
+
clientState.curatedTemplates = [];
|
|
92
96
|
clientState.useChatModels.mockClear();
|
|
93
97
|
container = document.createElement("div");
|
|
94
98
|
document.body.appendChild(container);
|
|
@@ -195,6 +199,22 @@ describe("DispatchControlPlane", () => {
|
|
|
195
199
|
url: "https://duplicate.example.com",
|
|
196
200
|
},
|
|
197
201
|
];
|
|
202
|
+
clientState.curatedTemplates = [
|
|
203
|
+
{
|
|
204
|
+
id: "mail",
|
|
205
|
+
name: "Mail",
|
|
206
|
+
description: "Email client",
|
|
207
|
+
liveUrl: "https://mail.agent-native.com",
|
|
208
|
+
installed: false,
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
id: "analytics",
|
|
212
|
+
name: "Analytics",
|
|
213
|
+
description: "Workspace insights",
|
|
214
|
+
liveUrl: "https://analytics.agent-native.com",
|
|
215
|
+
installed: false,
|
|
216
|
+
},
|
|
217
|
+
];
|
|
198
218
|
|
|
199
219
|
await act(async () => {
|
|
200
220
|
root.render(
|
|
@@ -209,8 +229,15 @@ describe("DispatchControlPlane", () => {
|
|
|
209
229
|
expect(container.textContent).toContain("Onboarding");
|
|
210
230
|
expect(container.textContent).toContain("Mail");
|
|
211
231
|
expect(container.textContent).toContain("Clips");
|
|
232
|
+
expect(container.textContent).toContain("Analytics");
|
|
233
|
+
expect(container.textContent).toContain("Your apps");
|
|
234
|
+
expect(container.textContent).toContain("Other apps");
|
|
235
|
+
expect(container.textContent?.indexOf("Your apps")).toBeLessThan(
|
|
236
|
+
container.textContent?.indexOf("Other apps") ?? -1,
|
|
237
|
+
);
|
|
212
238
|
expect(container.textContent).not.toContain("Archived app");
|
|
213
239
|
expect(container.textContent).not.toContain("Duplicate onboarding");
|
|
240
|
+
expect(container.textContent).not.toContain("CRM");
|
|
214
241
|
expect(
|
|
215
242
|
Array.from(container.querySelectorAll("a")).filter((anchor) =>
|
|
216
243
|
anchor.getAttribute("href")?.includes("onboarding"),
|
|
@@ -10,18 +10,25 @@ import {
|
|
|
10
10
|
IconArrowUpRight,
|
|
11
11
|
IconChevronDown,
|
|
12
12
|
IconClockHour4,
|
|
13
|
+
IconApps,
|
|
13
14
|
} from "@tabler/icons-react";
|
|
14
15
|
import type { ReactNode } from "react";
|
|
15
16
|
import { useState } from "react";
|
|
16
17
|
import { Link, useNavigate } from "react-router";
|
|
17
18
|
|
|
18
|
-
import {
|
|
19
|
+
import type { ConnectedAppSummary } from "../lib/other-apps";
|
|
19
20
|
import { submitOverviewPrompt } from "../lib/overview-chat";
|
|
21
|
+
import { cn } from "../lib/utils";
|
|
20
22
|
import type { WorkspaceAppSummary } from "../lib/workspace-apps";
|
|
21
23
|
import { ActionQueryError } from "./action-query-error";
|
|
22
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
APP_LIST_GRID_CLASS,
|
|
26
|
+
AppList,
|
|
27
|
+
APP_LIST_GRID_ROW_CLASS,
|
|
28
|
+
} from "./app-list-row";
|
|
23
29
|
import { CreateAppPopover } from "./create-app-popover";
|
|
24
30
|
import { useSetPageTitle } from "./layout/HeaderActions";
|
|
31
|
+
import { OtherAppsSection } from "./other-apps-section";
|
|
25
32
|
import { Button } from "./ui/button";
|
|
26
33
|
import {
|
|
27
34
|
Collapsible,
|
|
@@ -30,6 +37,7 @@ import {
|
|
|
30
37
|
} from "./ui/collapsible";
|
|
31
38
|
import { Skeleton } from "./ui/skeleton";
|
|
32
39
|
import { WorkspaceAppCard } from "./workspace-app-card";
|
|
40
|
+
import type { CuratedWorkspaceTemplatesResult } from "./workspace-template-card";
|
|
33
41
|
|
|
34
42
|
function SectionHeader({
|
|
35
43
|
title,
|
|
@@ -48,6 +56,26 @@ function SectionHeader({
|
|
|
48
56
|
);
|
|
49
57
|
}
|
|
50
58
|
|
|
59
|
+
function AppGroupHeader({
|
|
60
|
+
title,
|
|
61
|
+
description,
|
|
62
|
+
}: {
|
|
63
|
+
title: string;
|
|
64
|
+
description: string;
|
|
65
|
+
}) {
|
|
66
|
+
return (
|
|
67
|
+
<div className="flex min-w-0 items-start gap-2">
|
|
68
|
+
<IconApps size={16} className="mt-0.5 shrink-0 text-muted-foreground" />
|
|
69
|
+
<div className="min-w-0">
|
|
70
|
+
<h3 className="truncate text-sm font-semibold text-foreground">
|
|
71
|
+
{title}
|
|
72
|
+
</h3>
|
|
73
|
+
<p className="mt-0.5 text-xs text-muted-foreground">{description}</p>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
51
79
|
function CommandPanel() {
|
|
52
80
|
const t = useT();
|
|
53
81
|
const {
|
|
@@ -143,29 +171,33 @@ function CommandPanel() {
|
|
|
143
171
|
function AppsPanel({
|
|
144
172
|
apps,
|
|
145
173
|
isLoading,
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
174
|
+
connectedApps,
|
|
175
|
+
connectedAppsError,
|
|
176
|
+
connectedAppsLoading,
|
|
177
|
+
onRetryConnectedApps,
|
|
178
|
+
curatedTemplates,
|
|
179
|
+
curatedTemplatesError,
|
|
180
|
+
curatedTemplatesLoading,
|
|
181
|
+
onRetryCuratedTemplates,
|
|
150
182
|
}: {
|
|
151
183
|
apps: WorkspaceAppSummary[];
|
|
152
184
|
isLoading: boolean;
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
185
|
+
connectedApps: ConnectedAppSummary[];
|
|
186
|
+
connectedAppsError?: Error | null;
|
|
187
|
+
connectedAppsLoading: boolean;
|
|
188
|
+
onRetryConnectedApps: () => void;
|
|
189
|
+
curatedTemplates?: CuratedWorkspaceTemplatesResult;
|
|
190
|
+
curatedTemplatesError?: Error | null;
|
|
191
|
+
curatedTemplatesLoading: boolean;
|
|
192
|
+
onRetryCuratedTemplates: () => void;
|
|
157
193
|
}) {
|
|
158
194
|
const t = useT();
|
|
159
195
|
const [showPending, setShowPending] = useState(false);
|
|
160
196
|
const visibleApps = apps.filter((app) => !app.isDispatch && !app.archived);
|
|
161
197
|
const activeApps = visibleApps.filter((app) => app.status !== "pending");
|
|
162
198
|
const pendingApps = visibleApps.filter((app) => app.status === "pending");
|
|
163
|
-
const hasActiveApps = activeApps.length > 0 || otherApps.length > 0;
|
|
164
199
|
const showSkeletons =
|
|
165
|
-
|
|
166
|
-
activeApps.length === 0 &&
|
|
167
|
-
otherApps.length === 0 &&
|
|
168
|
-
pendingApps.length === 0;
|
|
200
|
+
isLoading && activeApps.length === 0 && pendingApps.length === 0;
|
|
169
201
|
|
|
170
202
|
return (
|
|
171
203
|
<section className="flex flex-col gap-3">
|
|
@@ -181,30 +213,45 @@ function AppsPanel({
|
|
|
181
213
|
}
|
|
182
214
|
/>
|
|
183
215
|
{showSkeletons ? (
|
|
184
|
-
<
|
|
185
|
-
{Array.from({ length: 4 }).map((_, index) => (
|
|
186
|
-
<div key={index} className="rounded-xl bg-card/40 p-4">
|
|
187
|
-
<Skeleton className="h-4 w-32" />
|
|
188
|
-
<Skeleton className="mt-3 h-3 w-24" />
|
|
189
|
-
<Skeleton className="mt-3 h-3 w-full" />
|
|
190
|
-
</div>
|
|
191
|
-
))}
|
|
192
|
-
</div>
|
|
193
|
-
) : hasActiveApps ? (
|
|
194
|
-
<div className="grid gap-3 md:grid-cols-2">
|
|
195
|
-
{activeApps.map((app) => (
|
|
196
|
-
<WorkspaceAppCard key={app.id} app={app} className="min-h-32" />
|
|
197
|
-
))}
|
|
198
|
-
{otherApps.map((app) => (
|
|
199
|
-
<ConnectedAppCard key={app.id} app={app} />
|
|
200
|
-
))}
|
|
201
|
-
</div>
|
|
216
|
+
<OverviewAppsSkeleton />
|
|
202
217
|
) : (
|
|
203
|
-
|
|
218
|
+
<>
|
|
219
|
+
{activeApps.length > 0 ? (
|
|
220
|
+
<div className="space-y-3">
|
|
221
|
+
<AppGroupHeader
|
|
222
|
+
title={t("dispatch.pages.yourApps", {
|
|
223
|
+
defaultValue: "Your apps",
|
|
224
|
+
})}
|
|
225
|
+
description={t("dispatch.pages.activeCount", {
|
|
226
|
+
count: activeApps.length,
|
|
227
|
+
})}
|
|
228
|
+
/>
|
|
229
|
+
<AppList className={APP_LIST_GRID_CLASS}>
|
|
230
|
+
{activeApps.map((app) => (
|
|
231
|
+
<WorkspaceAppCard
|
|
232
|
+
key={app.id}
|
|
233
|
+
app={app}
|
|
234
|
+
className={APP_LIST_GRID_ROW_CLASS}
|
|
235
|
+
/>
|
|
236
|
+
))}
|
|
237
|
+
</AppList>
|
|
238
|
+
</div>
|
|
239
|
+
) : (
|
|
240
|
+
<CreateAppPopover />
|
|
241
|
+
)}
|
|
242
|
+
<OtherAppsSection
|
|
243
|
+
templates={curatedTemplates}
|
|
244
|
+
connectedApps={connectedApps}
|
|
245
|
+
workspaceApps={apps}
|
|
246
|
+
templatesLoading={curatedTemplatesLoading}
|
|
247
|
+
connectedAppsLoading={connectedAppsLoading}
|
|
248
|
+
templatesError={curatedTemplatesError}
|
|
249
|
+
connectedAppsError={connectedAppsError}
|
|
250
|
+
onRetryTemplates={onRetryCuratedTemplates}
|
|
251
|
+
onRetryConnectedApps={onRetryConnectedApps}
|
|
252
|
+
/>
|
|
253
|
+
</>
|
|
204
254
|
)}
|
|
205
|
-
{otherAppsError ? (
|
|
206
|
-
<ActionQueryError error={otherAppsError} onRetry={onRetryOtherApps} />
|
|
207
|
-
) : null}
|
|
208
255
|
{pendingApps.length > 0 ? (
|
|
209
256
|
<Collapsible open={showPending} onOpenChange={setShowPending}>
|
|
210
257
|
<div className="space-y-3 border-t pt-3">
|
|
@@ -243,15 +290,15 @@ function AppsPanel({
|
|
|
243
290
|
</CollapsibleTrigger>
|
|
244
291
|
</div>
|
|
245
292
|
<CollapsibleContent>
|
|
246
|
-
<
|
|
293
|
+
<AppList className={APP_LIST_GRID_CLASS}>
|
|
247
294
|
{pendingApps.map((app) => (
|
|
248
295
|
<WorkspaceAppCard
|
|
249
296
|
key={app.id}
|
|
250
297
|
app={app}
|
|
251
|
-
className=
|
|
298
|
+
className={APP_LIST_GRID_ROW_CLASS}
|
|
252
299
|
/>
|
|
253
300
|
))}
|
|
254
|
-
</
|
|
301
|
+
</AppList>
|
|
255
302
|
</CollapsibleContent>
|
|
256
303
|
</div>
|
|
257
304
|
</Collapsible>
|
|
@@ -260,6 +307,29 @@ function AppsPanel({
|
|
|
260
307
|
);
|
|
261
308
|
}
|
|
262
309
|
|
|
310
|
+
function OverviewAppsSkeleton() {
|
|
311
|
+
return (
|
|
312
|
+
<AppList className={APP_LIST_GRID_CLASS}>
|
|
313
|
+
{Array.from({ length: 4 }).map((_, index) => (
|
|
314
|
+
<div
|
|
315
|
+
key={index}
|
|
316
|
+
className={cn(
|
|
317
|
+
"flex min-w-0 items-center gap-3 border-b px-4 py-3.5 last:border-b-0",
|
|
318
|
+
APP_LIST_GRID_ROW_CLASS,
|
|
319
|
+
)}
|
|
320
|
+
>
|
|
321
|
+
<Skeleton className="size-8 shrink-0 rounded-xl" />
|
|
322
|
+
<div className="min-w-0 flex-1 space-y-2">
|
|
323
|
+
<Skeleton className="h-4 w-32" />
|
|
324
|
+
<Skeleton className="h-3 w-2/3" />
|
|
325
|
+
</div>
|
|
326
|
+
<Skeleton className="h-9 w-24 shrink-0 rounded-md" />
|
|
327
|
+
</div>
|
|
328
|
+
))}
|
|
329
|
+
</AppList>
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
263
333
|
export function DispatchControlPlane() {
|
|
264
334
|
useSetPageTitle(<span aria-hidden />);
|
|
265
335
|
const appsQuery = useActionQuery<WorkspaceAppSummary[]>(
|
|
@@ -270,11 +340,11 @@ export function DispatchControlPlane() {
|
|
|
270
340
|
"list-connected-agents",
|
|
271
341
|
{},
|
|
272
342
|
);
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
workspaceApps ?? [],
|
|
343
|
+
const curatedTemplatesQuery = useActionQuery<CuratedWorkspaceTemplatesResult>(
|
|
344
|
+
"list-curated-workspace-templates",
|
|
345
|
+
{},
|
|
277
346
|
);
|
|
347
|
+
const { data: workspaceApps = [], isLoading: appsLoading } = appsQuery;
|
|
278
348
|
|
|
279
349
|
return (
|
|
280
350
|
<div className="flex flex-col gap-8">
|
|
@@ -288,10 +358,14 @@ export function DispatchControlPlane() {
|
|
|
288
358
|
<AppsPanel
|
|
289
359
|
apps={workspaceApps ?? []}
|
|
290
360
|
isLoading={appsLoading}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
361
|
+
connectedApps={connectedAppsQuery.data ?? []}
|
|
362
|
+
connectedAppsError={connectedAppsQuery.error}
|
|
363
|
+
connectedAppsLoading={connectedAppsQuery.isLoading}
|
|
364
|
+
onRetryConnectedApps={() => void connectedAppsQuery.refetch()}
|
|
365
|
+
curatedTemplates={curatedTemplatesQuery.data}
|
|
366
|
+
curatedTemplatesError={curatedTemplatesQuery.error}
|
|
367
|
+
curatedTemplatesLoading={curatedTemplatesQuery.isLoading}
|
|
368
|
+
onRetryCuratedTemplates={() => void curatedTemplatesQuery.refetch()}
|
|
295
369
|
/>
|
|
296
370
|
)}
|
|
297
371
|
</div>
|
|
@@ -173,7 +173,7 @@ describe("Dispatch NavContent", () => {
|
|
|
173
173
|
expect(lists[1].className).toContain("gap-1");
|
|
174
174
|
expect(lists[1].querySelector('a[href="/admin"]')).not.toBeNull();
|
|
175
175
|
expect(lists[1].querySelector('a[href="/settings"]')).not.toBeNull();
|
|
176
|
-
expect(lists[0].querySelector("a")?.className).toContain("
|
|
176
|
+
expect(lists[0].querySelector("a")?.className).toContain("size-9");
|
|
177
177
|
});
|
|
178
178
|
|
|
179
179
|
it("keeps management routes out of the primary navigation", async () => {
|
|
@@ -218,7 +218,7 @@ describe("Dispatch NavContent", () => {
|
|
|
218
218
|
expect(shell?.querySelector('a[href="/admin/apps"]')).toBeNull();
|
|
219
219
|
});
|
|
220
220
|
|
|
221
|
-
it("shows ready workspace apps as
|
|
221
|
+
it("shows ready workspace apps as host links and highlights the active app", async () => {
|
|
222
222
|
clientState.workspaceApps = [
|
|
223
223
|
{ id: "calendar", name: "Calendar", path: "/calendar", status: "ready" },
|
|
224
224
|
{ id: "pending", name: "Pending", path: "/pending", status: "pending" },
|
|
@@ -226,7 +226,7 @@ describe("Dispatch NavContent", () => {
|
|
|
226
226
|
|
|
227
227
|
await act(async () => {
|
|
228
228
|
root.render(
|
|
229
|
-
<MemoryRouter initialEntries={["/calendar
|
|
229
|
+
<MemoryRouter initialEntries={["/apps/calendar"]}>
|
|
230
230
|
<TooltipProvider>
|
|
231
231
|
<NavContent />
|
|
232
232
|
</TooltipProvider>
|
|
@@ -239,7 +239,7 @@ describe("Dispatch NavContent", () => {
|
|
|
239
239
|
expect(rail?.textContent).toContain("Calendar");
|
|
240
240
|
expect(rail?.textContent).not.toContain("Pending");
|
|
241
241
|
|
|
242
|
-
const calendarLink = rail?.querySelector('a[href="/calendar"]');
|
|
242
|
+
const calendarLink = rail?.querySelector('a[href="/apps/calendar"]');
|
|
243
243
|
expect(calendarLink).not.toBeNull();
|
|
244
244
|
expect(calendarLink?.getAttribute("aria-current")).toBe("page");
|
|
245
245
|
});
|