From 26b5f2ce254c91c1e97085664d35973b075a4413 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Thu, 7 Aug 2025 09:26:33 +0000 Subject: [PATCH] fix(site): fix render crash when no embedded apps are defined for task --- site/src/pages/TaskPage/TaskApps.stories.tsx | 134 ++++++++++++++++ site/src/pages/TaskPage/TaskApps.tsx | 158 +++++++++++-------- 2 files changed, 230 insertions(+), 62 deletions(-) create mode 100644 site/src/pages/TaskPage/TaskApps.stories.tsx diff --git a/site/src/pages/TaskPage/TaskApps.stories.tsx b/site/src/pages/TaskPage/TaskApps.stories.tsx new file mode 100644 index 0000000000000..c3006e2db0a14 --- /dev/null +++ b/site/src/pages/TaskPage/TaskApps.stories.tsx @@ -0,0 +1,134 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import type { WorkspaceApp } from "api/typesGenerated"; +import { + MockTasks, + MockWorkspace, + MockWorkspaceAgent, + MockWorkspaceApp, +} from "testHelpers/entities"; +import { withProxyProvider } from "testHelpers/storybook"; +import { TaskApps } from "./TaskApps"; + +const meta: Meta = { + title: "pages/TaskPage/TaskApps", + component: TaskApps, + decorators: [withProxyProvider()], + parameters: { + layout: "fullscreen", + }, +}; + +export default meta; +type Story = StoryObj; + +const mockAgentNoApps = { + ...MockWorkspaceAgent, + apps: [], +}; + +const mockExternalApp: WorkspaceApp = { + ...MockWorkspaceApp, + external: true, +}; + +const mockEmbeddedApp: WorkspaceApp = { + ...MockWorkspaceApp, + external: false, +}; + +const taskWithNoApps = { + ...MockTasks[0], + workspace: { + ...MockWorkspace, + latest_build: { + ...MockWorkspace.latest_build, + resources: [ + { + ...MockWorkspace.latest_build.resources[0], + agents: [mockAgentNoApps], + }, + ], + }, + }, +}; + +export const NoEmbeddedApps: Story = { + args: { + task: taskWithNoApps, + }, +}; + +export const WithExternalAppsOnly: Story = { + args: { + task: { + ...MockTasks[0], + workspace: { + ...MockWorkspace, + latest_build: { + ...MockWorkspace.latest_build, + resources: [ + { + ...MockWorkspace.latest_build.resources[0], + agents: [ + { + ...MockWorkspaceAgent, + apps: [mockExternalApp], + }, + ], + }, + ], + }, + }, + }, + }, +}; + +export const WithEmbeddedApps: Story = { + args: { + task: { + ...MockTasks[0], + workspace: { + ...MockWorkspace, + latest_build: { + ...MockWorkspace.latest_build, + resources: [ + { + ...MockWorkspace.latest_build.resources[0], + agents: [ + { + ...MockWorkspaceAgent, + apps: [mockEmbeddedApp], + }, + ], + }, + ], + }, + }, + }, + }, +}; + +export const WithMixedApps: Story = { + args: { + task: { + ...MockTasks[0], + workspace: { + ...MockWorkspace, + latest_build: { + ...MockWorkspace.latest_build, + resources: [ + { + ...MockWorkspace.latest_build.resources[0], + agents: [ + { + ...MockWorkspaceAgent, + apps: [mockEmbeddedApp, mockExternalApp], + }, + ], + }, + ], + }, + }, + }, + }, +}; diff --git a/site/src/pages/TaskPage/TaskApps.tsx b/site/src/pages/TaskPage/TaskApps.tsx index 0cccc8c7a01df..1fd31bd3b1481 100644 --- a/site/src/pages/TaskPage/TaskApps.tsx +++ b/site/src/pages/TaskPage/TaskApps.tsx @@ -1,4 +1,4 @@ -import type { WorkspaceApp } from "api/typesGenerated"; +import type { WorkspaceAgent, WorkspaceApp } from "api/typesGenerated"; import { Button } from "components/Button/Button"; import { DropdownMenu, @@ -8,6 +8,7 @@ import { } from "components/DropdownMenu/DropdownMenu"; import { ExternalImage } from "components/ExternalImage/ExternalImage"; import { InfoTooltip } from "components/InfoTooltip/InfoTooltip"; +import { Link } from "components/Link/Link"; import { ChevronDownIcon, LayoutGridIcon } from "lucide-react"; import { useAppLink } from "modules/apps/useAppLink"; import type { Task } from "modules/tasks/tasks"; @@ -15,6 +16,7 @@ import type React from "react"; import { type FC, useState } from "react"; import { Link as RouterLink } from "react-router-dom"; import { cn } from "utils/cn"; +import { docs } from "utils/docs"; import { TaskAppIFrame } from "./TaskAppIframe"; type TaskAppsProps = { @@ -37,25 +39,9 @@ export const TaskApps: FC = ({ task }) => { const embeddedApps = apps.filter((app) => !app.external); const externalApps = apps.filter((app) => app.external); - const [activeAppId, setActiveAppId] = useState(() => { - const appId = embeddedApps[0]?.id; - if (!appId) { - throw new Error("No apps found in task"); - } - return appId; - }); - - const activeApp = apps.find((app) => app.id === activeAppId); - if (!activeApp) { - throw new Error(`Active app with ID ${activeAppId} not found in task`); - } - - const agent = agents.find((a) => - a.apps.some((app) => app.id === activeAppId), + const [activeAppId, setActiveAppId] = useState( + embeddedApps[0]?.id, ); - if (!agent) { - throw new Error(`Agent for app ${activeAppId} not found in task workspace`); - } return (
@@ -76,56 +62,104 @@ export const TaskApps: FC = ({ task }) => { {externalApps.length > 0 && ( -
- - - - - - {externalApps.map((app) => { - const link = useAppLink(app, { - agent, - workspace: task.workspace, - }); - - return ( - - - {app.icon ? ( - - ) : ( - - )} - {link.label} - - - ); - })} - - -
+ )} -
- {embeddedApps.map((app) => { - return ( - - ); - })} -
+ {embeddedApps.length > 0 ? ( +
+ {embeddedApps.map((app) => { + return ( + + ); + })} +
+ ) : ( +
+

+ No embedded apps found. +

+ + + + Learn how to configure apps + {" "} + for your tasks. + +
+ )}
); }; +type TaskExternalAppsDropdownProps = { + task: Task; + agents: WorkspaceAgent[]; + externalApps: WorkspaceApp[]; +}; + +const TaskExternalAppsDropdown: FC = ({ + task, + agents, + externalApps, +}) => { + return ( +
+ + + + + + {externalApps.map((app) => { + const agent = agents.find((agent) => + agent.apps.some((a) => a.id === app.id), + ); + if (!agent) { + throw new Error( + `Agent for app ${app.id} not found in task workspace`, + ); + } + + const link = useAppLink(app, { + agent, + workspace: task.workspace, + }); + + return ( + + + {app.icon ? ( + + ) : ( + + )} + {link.label} + + + ); + })} + + +
+ ); +}; + type TaskAppTabProps = { task: Task; app: WorkspaceApp;