mirror of
https://github.com/coder/coder.git
synced 2026-09-21 20:51:01 +08:00
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:
@@ -662,6 +662,7 @@ func ConvertWorkspaceRows(rows []GetWorkspacesRow) []Workspace {
|
||||
TemplateIcon: r.TemplateIcon,
|
||||
TemplateDescription: r.TemplateDescription,
|
||||
NextStartAt: r.NextStartAt,
|
||||
TaskID: r.TaskID,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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={
|
||||
|
||||
Reference in New Issue
Block a user