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)