Skip to content

Commit ffbd583

Browse files
fix(site): fix render crash when no embedded apps are defined for task (#19215)
Fixes #19101 We now gracefully handle the scenario where there are no embedded apps defined for a task.
1 parent 6ba5521 commit ffbd583

File tree

2 files changed

+230
-62
lines changed

2 files changed

+230
-62
lines changed
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import type { Meta, StoryObj } from "@storybook/react";
2+
import type { WorkspaceApp } from "api/typesGenerated";
3+
import {
4+
MockTasks,
5+
MockWorkspace,
6+
MockWorkspaceAgent,
7+
MockWorkspaceApp,
8+
} from "testHelpers/entities";
9+
import { withProxyProvider } from "testHelpers/storybook";
10+
import { TaskApps } from "./TaskApps";
11+
12+
const meta: Meta<typeof TaskApps> = {
13+
title: "pages/TaskPage/TaskApps",
14+
component: TaskApps,
15+
decorators: [withProxyProvider()],
16+
parameters: {
17+
layout: "fullscreen",
18+
},
19+
};
20+
21+
export default meta;
22+
type Story = StoryObj<typeof TaskApps>;
23+
24+
const mockAgentNoApps = {
25+
...MockWorkspaceAgent,
26+
apps: [],
27+
};
28+
29+
const mockExternalApp: WorkspaceApp = {
30+
...MockWorkspaceApp,
31+
external: true,
32+
};
33+
34+
const mockEmbeddedApp: WorkspaceApp = {
35+
...MockWorkspaceApp,
36+
external: false,
37+
};
38+
39+
const taskWithNoApps = {
40+
...MockTasks[0],
41+
workspace: {
42+
...MockWorkspace,
43+
latest_build: {
44+
...MockWorkspace.latest_build,
45+
resources: [
46+
{
47+
...MockWorkspace.latest_build.resources[0],
48+
agents: [mockAgentNoApps],
49+
},
50+
],
51+
},
52+
},
53+
};
54+
55+
export const NoEmbeddedApps: Story = {
56+
args: {
57+
task: taskWithNoApps,
58+
},
59+
};
60+
61+
export const WithExternalAppsOnly: Story = {
62+
args: {
63+
task: {
64+
...MockTasks[0],
65+
workspace: {
66+
...MockWorkspace,
67+
latest_build: {
68+
...MockWorkspace.latest_build,
69+
resources: [
70+
{
71+
...MockWorkspace.latest_build.resources[0],
72+
agents: [
73+
{
74+
...MockWorkspaceAgent,
75+
apps: [mockExternalApp],
76+
},
77+
],
78+
},
79+
],
80+
},
81+
},
82+
},
83+
},
84+
};
85+
86+
export const WithEmbeddedApps: Story = {
87+
args: {
88+
task: {
89+
...MockTasks[0],
90+
workspace: {
91+
...MockWorkspace,
92+
latest_build: {
93+
...MockWorkspace.latest_build,
94+
resources: [
95+
{
96+
...MockWorkspace.latest_build.resources[0],
97+
agents: [
98+
{
99+
...MockWorkspaceAgent,
100+
apps: [mockEmbeddedApp],
101+
},
102+
],
103+
},
104+
],
105+
},
106+
},
107+
},
108+
},
109+
};
110+
111+
export const WithMixedApps: Story = {
112+
args: {
113+
task: {
114+
...MockTasks[0],
115+
workspace: {
116+
...MockWorkspace,
117+
latest_build: {
118+
...MockWorkspace.latest_build,
119+
resources: [
120+
{
121+
...MockWorkspace.latest_build.resources[0],
122+
agents: [
123+
{
124+
...MockWorkspaceAgent,
125+
apps: [mockEmbeddedApp, mockExternalApp],
126+
},
127+
],
128+
},
129+
],
130+
},
131+
},
132+
},
133+
},
134+
};

site/src/pages/TaskPage/TaskApps.tsx

Lines changed: 96 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { WorkspaceApp } from "api/typesGenerated";
1+
import type { WorkspaceAgent, WorkspaceApp } from "api/typesGenerated";
22
import { Button } from "components/Button/Button";
33
import {
44
DropdownMenu,
@@ -8,13 +8,15 @@ import {
88
} from "components/DropdownMenu/DropdownMenu";
99
import { ExternalImage } from "components/ExternalImage/ExternalImage";
1010
import { InfoTooltip } from "components/InfoTooltip/InfoTooltip";
11+
import { Link } from "components/Link/Link";
1112
import { ChevronDownIcon, LayoutGridIcon } from "lucide-react";
1213
import { useAppLink } from "modules/apps/useAppLink";
1314
import type { Task } from "modules/tasks/tasks";
1415
import type React from "react";
1516
import { type FC, useState } from "react";
1617
import { Link as RouterLink } from "react-router-dom";
1718
import { cn } from "utils/cn";
19+
import { docs } from "utils/docs";
1820
import { TaskAppIFrame } from "./TaskAppIframe";
1921

2022
type TaskAppsProps = {
@@ -37,25 +39,9 @@ export const TaskApps: FC<TaskAppsProps> = ({ task }) => {
3739
const embeddedApps = apps.filter((app) => !app.external);
3840
const externalApps = apps.filter((app) => app.external);
3941

40-
const [activeAppId, setActiveAppId] = useState<string>(() => {
41-
const appId = embeddedApps[0]?.id;
42-
if (!appId) {
43-
throw new Error("No apps found in task");
44-
}
45-
return appId;
46-
});
47-
48-
const activeApp = apps.find((app) => app.id === activeAppId);
49-
if (!activeApp) {
50-
throw new Error(`Active app with ID ${activeAppId} not found in task`);
51-
}
52-
53-
const agent = agents.find((a) =>
54-
a.apps.some((app) => app.id === activeAppId),
42+
const [activeAppId, setActiveAppId] = useState<string | undefined>(
43+
embeddedApps[0]?.id,
5544
);
56-
if (!agent) {
57-
throw new Error(`Agent for app ${activeAppId} not found in task workspace`);
58-
}
5945

6046
return (
6147
<main className="flex flex-col">
@@ -76,56 +62,104 @@ export const TaskApps: FC<TaskAppsProps> = ({ task }) => {
7662
</div>
7763

7864
{externalApps.length > 0 && (
79-
<div className="ml-auto">
80-
<DropdownMenu>
81-
<DropdownMenuTrigger asChild>
82-
<Button size="sm" variant="subtle">
83-
Open locally
84-
<ChevronDownIcon />
85-
</Button>
86-
</DropdownMenuTrigger>
87-
<DropdownMenuContent>
88-
{externalApps.map((app) => {
89-
const link = useAppLink(app, {
90-
agent,
91-
workspace: task.workspace,
92-
});
93-
94-
return (
95-
<DropdownMenuItem key={app.id} asChild>
96-
<RouterLink to={link.href}>
97-
{app.icon ? (
98-
<ExternalImage src={app.icon} />
99-
) : (
100-
<LayoutGridIcon />
101-
)}
102-
{link.label}
103-
</RouterLink>
104-
</DropdownMenuItem>
105-
);
106-
})}
107-
</DropdownMenuContent>
108-
</DropdownMenu>
109-
</div>
65+
<TaskExternalAppsDropdown
66+
task={task}
67+
agents={agents}
68+
externalApps={externalApps}
69+
/>
11070
)}
11171
</div>
11272

113-
<div className="flex-1">
114-
{embeddedApps.map((app) => {
115-
return (
116-
<TaskAppIFrame
117-
key={app.id}
118-
active={activeAppId === app.id}
119-
app={app}
120-
task={task}
121-
/>
122-
);
123-
})}
124-
</div>
73+
{embeddedApps.length > 0 ? (
74+
<div className="flex-1">
75+
{embeddedApps.map((app) => {
76+
return (
77+
<TaskAppIFrame
78+
key={app.id}
79+
active={activeAppId === app.id}
80+
app={app}
81+
task={task}
82+
/>
83+
);
84+
})}
85+
</div>
86+
) : (
87+
<div className="mx-auto my-auto flex flex-col items-center">
88+
<h3 className="font-medium text-content-primary text-base">
89+
No embedded apps found.
90+
</h3>
91+
92+
<span className="text-content-secondary text-sm">
93+
<Link
94+
href={docs("/ai-coder/tasks")}
95+
target="_blank"
96+
rel="noreferrer"
97+
>
98+
Learn how to configure apps
99+
</Link>{" "}
100+
for your tasks.
101+
</span>
102+
</div>
103+
)}
125104
</main>
126105
);
127106
};
128107

108+
type TaskExternalAppsDropdownProps = {
109+
task: Task;
110+
agents: WorkspaceAgent[];
111+
externalApps: WorkspaceApp[];
112+
};
113+
114+
const TaskExternalAppsDropdown: FC<TaskExternalAppsDropdownProps> = ({
115+
task,
116+
agents,
117+
externalApps,
118+
}) => {
119+
return (
120+
<div className="ml-auto">
121+
<DropdownMenu>
122+
<DropdownMenuTrigger asChild>
123+
<Button size="sm" variant="subtle">
124+
Open locally
125+
<ChevronDownIcon />
126+
</Button>
127+
</DropdownMenuTrigger>
128+
<DropdownMenuContent>
129+
{externalApps.map((app) => {
130+
const agent = agents.find((agent) =>
131+
agent.apps.some((a) => a.id === app.id),
132+
);
133+
if (!agent) {
134+
throw new Error(
135+
`Agent for app ${app.id} not found in task workspace`,
136+
);
137+
}
138+
139+
const link = useAppLink(app, {
140+
agent,
141+
workspace: task.workspace,
142+
});
143+
144+
return (
145+
<DropdownMenuItem key={app.id} asChild>
146+
<RouterLink to={link.href}>
147+
{app.icon ? (
148+
<ExternalImage src={app.icon} />
149+
) : (
150+
<LayoutGridIcon />
151+
)}
152+
{link.label}
153+
</RouterLink>
154+
</DropdownMenuItem>
155+
);
156+
})}
157+
</DropdownMenuContent>
158+
</DropdownMenu>
159+
</div>
160+
);
161+
};
162+
129163
type TaskAppTabProps = {
130164
task: Task;
131165
app: WorkspaceApp;

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy