Skip to content

Commit 2a9afc7

Browse files
authored
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 #20802
1 parent 2840fdc commit 2a9afc7

File tree

4 files changed

+81
-0
lines changed

4 files changed

+81
-0
lines changed

coderd/database/modelmethods.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -662,6 +662,7 @@ func ConvertWorkspaceRows(rows []GetWorkspacesRow) []Workspace {
662662
TemplateIcon: r.TemplateIcon,
663663
TemplateDescription: r.TemplateDescription,
664664
NextStartAt: r.NextStartAt,
665+
TaskID: r.TaskID,
665666
}
666667
}
667668

coderd/workspaces_test.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4794,6 +4794,64 @@ func TestWorkspaceFilterHasAITask(t *testing.T) {
47944794
require.Len(t, res.Workspaces, 4)
47954795
}
47964796

4797+
func TestWorkspaceListTasks(t *testing.T) {
4798+
t.Parallel()
4799+
4800+
ctx := testutil.Context(t, testutil.WaitShort)
4801+
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
4802+
user := coderdtest.CreateFirstUser(t, client)
4803+
expClient := codersdk.NewExperimentalClient(client)
4804+
4805+
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
4806+
Parse: echo.ParseComplete,
4807+
ProvisionApply: echo.ApplyComplete,
4808+
ProvisionPlan: []*proto.Response{
4809+
{Type: &proto.Response_Plan{Plan: &proto.PlanComplete{
4810+
HasAiTasks: true,
4811+
}}},
4812+
},
4813+
})
4814+
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
4815+
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
4816+
4817+
// Given: a regular user workspace
4818+
workspaceWithoutTask, err := client.CreateUserWorkspace(ctx, codersdk.Me, codersdk.CreateWorkspaceRequest{
4819+
TemplateID: template.ID,
4820+
Name: "user-workspace",
4821+
})
4822+
require.NoError(t, err)
4823+
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspaceWithoutTask.LatestBuild.ID)
4824+
4825+
// Given: a workspace associated with a task
4826+
task, err := expClient.CreateTask(ctx, codersdk.Me, codersdk.CreateTaskRequest{
4827+
TemplateVersionID: template.ActiveVersionID,
4828+
Input: "Some task prompt",
4829+
})
4830+
require.NoError(t, err)
4831+
assert.True(t, task.WorkspaceID.Valid)
4832+
workspaceWithTask, err := client.Workspace(ctx, task.WorkspaceID.UUID)
4833+
require.NoError(t, err)
4834+
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspaceWithTask.LatestBuild.ID)
4835+
assert.NotEmpty(t, task.Name)
4836+
assert.Equal(t, template.ID, task.TemplateID)
4837+
4838+
// When: listing the workspaces
4839+
workspaces, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{})
4840+
require.NoError(t, err)
4841+
4842+
assert.Equal(t, workspaces.Count, 2)
4843+
4844+
// Then: verify TaskID is only set for task workspaces
4845+
for _, workspace := range workspaces.Workspaces {
4846+
if workspace.ID == workspaceWithoutTask.ID {
4847+
assert.False(t, workspace.TaskID.Valid)
4848+
} else if workspace.ID == workspaceWithTask.ID {
4849+
assert.True(t, workspace.TaskID.Valid)
4850+
assert.Equal(t, task.ID, workspace.TaskID.UUID)
4851+
}
4852+
}
4853+
}
4854+
47974855
func TestWorkspaceAppUpsertRestart(t *testing.T) {
47984856
t.Parallel()
47994857

site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
MockBuildInfo,
33
MockOrganization,
44
MockPendingProvisionerJob,
5+
MockTaskWorkspace,
56
MockTemplate,
67
MockUserOwner,
78
MockWorkspace,
@@ -381,3 +382,18 @@ export const ShowOrganizations: Story = {
381382
expect(accessibleTableCell).toBeDefined();
382383
},
383384
};
385+
386+
export const ShowWorkspaceTasks: Story = {
387+
args: {
388+
workspaces: [
389+
{
390+
...MockWorkspace,
391+
name: "regular-user-workspace",
392+
},
393+
{
394+
...MockTaskWorkspace,
395+
name: "task-workspace",
396+
},
397+
],
398+
},
399+
};

site/src/pages/WorkspacesPage/WorkspacesTable.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import type {
1717
import { Avatar } from "components/Avatar/Avatar";
1818
import { AvatarData } from "components/Avatar/AvatarData";
1919
import { AvatarDataSkeleton } from "components/Avatar/AvatarDataSkeleton";
20+
import { Badge } from "components/Badge/Badge";
2021
import { Button } from "components/Button/Button";
2122
import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog";
2223
import { ExternalImage } from "components/ExternalImage/ExternalImage";
@@ -207,6 +208,11 @@ export const WorkspacesTable: FC<WorkspacesTableProps> = ({
207208
{workspace.outdated && (
208209
<WorkspaceOutdatedTooltip workspace={workspace} />
209210
)}
211+
{workspace.task_id && (
212+
<Badge size="xs" variant="default">
213+
Task
214+
</Badge>
215+
)}
210216
</Stack>
211217
}
212218
subtitle={

0 commit comments

Comments
 (0)