From 1de952b556ad3b36baee84b5c5fbb994b199e4f7 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Fri, 19 Dec 2025 11:33:07 +0000 Subject: [PATCH] fix(site): allow updating workspace in TaskPage (#21316) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Relates to #20925 This PR modifies TaskPage to update an outdated workspace instead of starting it. Before, starting an outdated workspace where the template required the active version would fail with the error "cannot use non-active version: rbac: forbidden". For the case of a dormant workspace, I deemed it safe enough to simply unset dormancy on an attempted start (ref: https://github.com/coder/coder/pull/21306). However, automatically updating a workspace is a more risky option, so I instead elected to give the user the option of updating their workspace using the existing tooltip. **Note:** I made a change to the `WorkspaceOutdatedTooltip` components to allow it to have children so that the tooltip could trigger over a wider element instead of just the info icon. ``` <🤖 AI Disclaimer>I got some help from Gemini 3 Flash in "Ask" mode. ``` --- .../WorkspaceOutdatedTooltip.tsx | 30 ++++++++---- site/src/pages/TaskPage/TaskPage.stories.tsx | 47 ++++++++++++++++++- site/src/pages/TaskPage/TaskPage.tsx | 12 +++++ 3 files changed, 79 insertions(+), 10 deletions(-) diff --git a/site/src/modules/workspaces/WorkspaceOutdatedTooltip/WorkspaceOutdatedTooltip.tsx b/site/src/modules/workspaces/WorkspaceOutdatedTooltip/WorkspaceOutdatedTooltip.tsx index 6de6f2c40d..5f6a415220 100644 --- a/site/src/modules/workspaces/WorkspaceOutdatedTooltip/WorkspaceOutdatedTooltip.tsx +++ b/site/src/modules/workspaces/WorkspaceOutdatedTooltip/WorkspaceOutdatedTooltip.tsx @@ -13,10 +13,11 @@ import { HelpTooltipLinksGroup, HelpTooltipText, HelpTooltipTitle, + HelpTooltipTrigger, } from "components/HelpTooltip/HelpTooltip"; import { InfoIcon, RotateCcwIcon } from "lucide-react"; import { linkToTemplate, useLinks } from "modules/navigation"; -import { type FC, useState } from "react"; +import { type FC, type ReactNode, useState } from "react"; import { useQuery } from "react-query"; import { useWorkspaceUpdate, @@ -25,20 +26,31 @@ import { interface WorkspaceOutdatedTooltipProps { workspace: Workspace; + children?: ReactNode; } -export const WorkspaceOutdatedTooltip: FC = ( - props, -) => { +export const WorkspaceOutdatedTooltip: FC = ({ + workspace, + children, +}) => { const [isOpen, setIsOpen] = useState(false); return ( - - - Outdated info - - + {children ? ( + + + + {children} + + + ) : ( + + + Outdated info + + )} + ); }; diff --git a/site/src/pages/TaskPage/TaskPage.stories.tsx b/site/src/pages/TaskPage/TaskPage.stories.tsx index cfe4c90a65..581b331b7b 100644 --- a/site/src/pages/TaskPage/TaskPage.stories.tsx +++ b/site/src/pages/TaskPage/TaskPage.stories.tsx @@ -19,6 +19,7 @@ import { } from "testHelpers/entities"; import { withAuthProvider, + withDashboardProvider, withGlobalSnackbar, withProxyProvider, withWebSocket, @@ -65,7 +66,7 @@ const MockVSCodeApp: WorkspaceApp = { const meta: Meta = { title: "pages/TaskPage", component: TaskPage, - decorators: [withProxyProvider(), withAuthProvider], + decorators: [withProxyProvider(), withAuthProvider, withDashboardProvider], beforeEach: () => { spyOn(API, "getTasks").mockResolvedValue(MockTasks); }, @@ -402,6 +403,50 @@ export const MainAppHealthy: Story = mainAppHealthStory("healthy"); export const MainAppInitializing: Story = mainAppHealthStory("initializing"); export const MainAppUnhealthy: Story = mainAppHealthStory("unhealthy"); +export const OutdatedWorkspace: Story = { + // Given: an 'outdated' workspace (that is, the latest build does not use template's active version) + parameters: { + queries: [ + { + key: ["tasks", { owner: MockTask.owner_name }], + data: [MockTask], + }, + { + key: ["tasks", MockTask.owner_name, MockTask.id], + data: MockTask, + }, + { + key: [ + "workspace", + MockTask.owner_name, + MockTask.workspace_name, + "settings", + ], + data: { + ...MockStoppedWorkspace, + outdated: true, + }, + }, + { + key: [ + "workspaceBuilds", + MockStoppedWorkspace.latest_build.id, + "parameters", + ], + data: [], + }, + ], + }, + // Then: a tooltip should be displayed prompting the user to update the workspace. + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const outdatedTooltip = await canvas.findByTestId( + "workspace-outdated-tooltip", + ); + expect(outdatedTooltip).toBeVisible(); + }, +}; + export const Active: Story = { decorators: [withProxyProvider()], beforeEach: () => { diff --git a/site/src/pages/TaskPage/TaskPage.tsx b/site/src/pages/TaskPage/TaskPage.tsx index 56da12468c..0808fe851d 100644 --- a/site/src/pages/TaskPage/TaskPage.tsx +++ b/site/src/pages/TaskPage/TaskPage.tsx @@ -26,6 +26,7 @@ import { getAllAppsWithAgent } from "modules/tasks/apps"; import { TasksSidebar } from "modules/tasks/TasksSidebar/TasksSidebar"; import { WorkspaceErrorDialog } from "modules/workspaces/ErrorDialog/WorkspaceErrorDialog"; import { WorkspaceBuildLogs } from "modules/workspaces/WorkspaceBuildLogs/WorkspaceBuildLogs"; +import { WorkspaceOutdatedTooltip } from "modules/workspaces/WorkspaceOutdatedTooltip/WorkspaceOutdatedTooltip"; import { type FC, type PropsWithChildren, @@ -275,9 +276,20 @@ const WorkspaceNotRunning: FC = ({ Apps and previous statuses are not available + {workspace.outdated && ( +
+ + You can update your task workspace to a newer version + +
+ )}