feat: associate task icon with workspaces (#20834)

## Problem

Workspaces associated with tasks were not visually distinguishable in
the workspaces list view. Additionally, the list workspaces endpoint was
not returning the `task_id` field.

<img width="2784" height="864" alt="Screenshot 2025-11-20 at 10 32 22"
src="https://github.com/user-attachments/assets/60704f16-3c66-4553-9215-f10654998a38"
/>

## Changes

- Fix `ConvertWorkspaceRows` to include `task_id` in the list workspaces
endpoint response
- Add "Task" icon to the workspace list view for workspaces associated
with tasks
- Add test to verify `task_id` is correctly returned by the list
workspaces endpoint
- Add Storybook story to showcase the Task icon in the workspace list

Closes https://github.com/coder/coder/issues/20802
This commit is contained in:
Susana Ferreira
2025-11-21 11:47:10 +00:00
committed by GitHub
parent 2840fdcb54
commit 2a9afc77de
4 changed files with 81 additions and 0 deletions
+1
View File
@@ -662,6 +662,7 @@ func ConvertWorkspaceRows(rows []GetWorkspacesRow) []Workspace {
TemplateIcon: r.TemplateIcon,
TemplateDescription: r.TemplateDescription,
NextStartAt: r.NextStartAt,
TaskID: r.TaskID,
}
}
+58
View File
@@ -4794,6 +4794,64 @@ func TestWorkspaceFilterHasAITask(t *testing.T) {
require.Len(t, res.Workspaces, 4)
}
func TestWorkspaceListTasks(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitShort)
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
expClient := codersdk.NewExperimentalClient(client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: echo.ApplyComplete,
ProvisionPlan: []*proto.Response{
{Type: &proto.Response_Plan{Plan: &proto.PlanComplete{
HasAiTasks: true,
}}},
},
})
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
// Given: a regular user workspace
workspaceWithoutTask, err := client.CreateUserWorkspace(ctx, codersdk.Me, codersdk.CreateWorkspaceRequest{
TemplateID: template.ID,
Name: "user-workspace",
})
require.NoError(t, err)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspaceWithoutTask.LatestBuild.ID)
// Given: a workspace associated with a task
task, err := expClient.CreateTask(ctx, codersdk.Me, codersdk.CreateTaskRequest{
TemplateVersionID: template.ActiveVersionID,
Input: "Some task prompt",
})
require.NoError(t, err)
assert.True(t, task.WorkspaceID.Valid)
workspaceWithTask, err := client.Workspace(ctx, task.WorkspaceID.UUID)
require.NoError(t, err)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspaceWithTask.LatestBuild.ID)
assert.NotEmpty(t, task.Name)
assert.Equal(t, template.ID, task.TemplateID)
// When: listing the workspaces
workspaces, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{})
require.NoError(t, err)
assert.Equal(t, workspaces.Count, 2)
// Then: verify TaskID is only set for task workspaces
for _, workspace := range workspaces.Workspaces {
if workspace.ID == workspaceWithoutTask.ID {
assert.False(t, workspace.TaskID.Valid)
} else if workspace.ID == workspaceWithTask.ID {
assert.True(t, workspace.TaskID.Valid)
assert.Equal(t, task.ID, workspace.TaskID.UUID)
}
}
}
func TestWorkspaceAppUpsertRestart(t *testing.T) {
t.Parallel()
@@ -2,6 +2,7 @@ import {
MockBuildInfo,
MockOrganization,
MockPendingProvisionerJob,
MockTaskWorkspace,
MockTemplate,
MockUserOwner,
MockWorkspace,
@@ -381,3 +382,18 @@ export const ShowOrganizations: Story = {
expect(accessibleTableCell).toBeDefined();
},
};
export const ShowWorkspaceTasks: Story = {
args: {
workspaces: [
{
...MockWorkspace,
name: "regular-user-workspace",
},
{
...MockTaskWorkspace,
name: "task-workspace",
},
],
},
};
@@ -17,6 +17,7 @@ import type {
import { Avatar } from "components/Avatar/Avatar";
import { AvatarData } from "components/Avatar/AvatarData";
import { AvatarDataSkeleton } from "components/Avatar/AvatarDataSkeleton";
import { Badge } from "components/Badge/Badge";
import { Button } from "components/Button/Button";
import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog";
import { ExternalImage } from "components/ExternalImage/ExternalImage";
@@ -207,6 +208,11 @@ export const WorkspacesTable: FC<WorkspacesTableProps> = ({
{workspace.outdated && (
<WorkspaceOutdatedTooltip workspace={workspace} />
)}
{workspace.task_id && (
<Badge size="xs" variant="default">
Task
</Badge>
)}
</Stack>
}
subtitle={