fix(site): allow updating workspace in TaskPage (#21316)

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.</🤖 AI Disclaimer>
```
This commit is contained in:
Cian Johnston
2025-12-19 11:33:07 +00:00
committed by GitHub
parent 73253df6bf
commit 1de952b556
3 changed files with 79 additions and 10 deletions
@@ -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<WorkspaceOutdatedTooltipProps> = (
props,
) => {
export const WorkspaceOutdatedTooltip: FC<WorkspaceOutdatedTooltipProps> = ({
workspace,
children,
}) => {
const [isOpen, setIsOpen] = useState(false);
return (
<HelpTooltip open={isOpen} onOpenChange={setIsOpen}>
<HelpTooltipIconTrigger size="small" hoverEffect={false}>
<InfoIcon css={styles.icon} />
<span className="sr-only">Outdated info</span>
</HelpTooltipIconTrigger>
<WorkspaceOutdatedTooltipContent isOpen={isOpen} {...props} />
{children ? (
<HelpTooltipTrigger asChild>
<span className="flex items-center gap-1.5 cursor-help">
<InfoIcon css={styles.icon} size={14} />
<span>{children}</span>
</span>
</HelpTooltipTrigger>
) : (
<HelpTooltipIconTrigger size="small" hoverEffect={false}>
<InfoIcon css={styles.icon} />
<span className="sr-only">Outdated info</span>
</HelpTooltipIconTrigger>
)}
<WorkspaceOutdatedTooltipContent isOpen={isOpen} workspace={workspace} />
</HelpTooltip>
);
};
+46 -1
View File
@@ -19,6 +19,7 @@ import {
} from "testHelpers/entities";
import {
withAuthProvider,
withDashboardProvider,
withGlobalSnackbar,
withProxyProvider,
withWebSocket,
@@ -65,7 +66,7 @@ const MockVSCodeApp: WorkspaceApp = {
const meta: Meta<typeof TaskPage> = {
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: () => {
+12
View File
@@ -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<WorkspaceNotRunningProps> = ({
<span className="text-content-secondary text-sm">
Apps and previous statuses are not available
</span>
{workspace.outdated && (
<div
data-testid="workspace-outdated-tooltip"
className="flex items-center gap-1.5 mt-1 text-content-secondary text-sm"
>
<WorkspaceOutdatedTooltip workspace={workspace}>
You can update your task workspace to a newer version
</WorkspaceOutdatedTooltip>
</div>
)}
<div className="flex flex-row mt-4 gap-4">
<Button
size="sm"
data-testid="task-start-workspace"
disabled={isWaitingForStart}
onClick={() => {
mutateStartWorkspace.mutate({