mirror of
https://github.com/coder/coder.git
synced 2025-07-15 22:20:27 +00:00
feat: show template.display_name on Workspace pages (#5082)
* feat: expose template.display_name via Workspaces endpoint * Fix: MockWorkspace * UI: Workspace stats and row * Show template.display_name on pages * Fix: address PR comments * Add helper function: getDisplayWorkspaceTemplateName
This commit is contained in:
@ -1011,20 +1011,21 @@ func convertWorkspace(
|
|||||||
|
|
||||||
ttlMillis := convertWorkspaceTTLMillis(workspace.Ttl)
|
ttlMillis := convertWorkspaceTTLMillis(workspace.Ttl)
|
||||||
return codersdk.Workspace{
|
return codersdk.Workspace{
|
||||||
ID: workspace.ID,
|
ID: workspace.ID,
|
||||||
CreatedAt: workspace.CreatedAt,
|
CreatedAt: workspace.CreatedAt,
|
||||||
UpdatedAt: workspace.UpdatedAt,
|
UpdatedAt: workspace.UpdatedAt,
|
||||||
OwnerID: workspace.OwnerID,
|
OwnerID: workspace.OwnerID,
|
||||||
OwnerName: owner.Username,
|
OwnerName: owner.Username,
|
||||||
TemplateID: workspace.TemplateID,
|
TemplateID: workspace.TemplateID,
|
||||||
LatestBuild: workspaceBuild,
|
LatestBuild: workspaceBuild,
|
||||||
TemplateName: template.Name,
|
TemplateName: template.Name,
|
||||||
TemplateIcon: template.Icon,
|
TemplateIcon: template.Icon,
|
||||||
Outdated: workspaceBuild.TemplateVersionID.String() != template.ActiveVersionID.String(),
|
TemplateDisplayName: template.DisplayName,
|
||||||
Name: workspace.Name,
|
Outdated: workspaceBuild.TemplateVersionID.String() != template.ActiveVersionID.String(),
|
||||||
AutostartSchedule: autostartSchedule,
|
Name: workspace.Name,
|
||||||
TTLMillis: ttlMillis,
|
AutostartSchedule: autostartSchedule,
|
||||||
LastUsedAt: workspace.LastUsedAt,
|
TTLMillis: ttlMillis,
|
||||||
|
LastUsedAt: workspace.LastUsedAt,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -115,6 +115,36 @@ func TestWorkspace(t *testing.T) {
|
|||||||
})
|
})
|
||||||
require.Error(t, err, "workspace rename should have failed")
|
require.Error(t, err, "workspace rename should have failed")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("TemplateProperties", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
||||||
|
user := coderdtest.CreateFirstUser(t, client)
|
||||||
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||||
|
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
|
||||||
|
|
||||||
|
const templateIcon = "/img/icon.svg"
|
||||||
|
const templateDisplayName = "This is template"
|
||||||
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
|
||||||
|
ctr.Icon = templateIcon
|
||||||
|
ctr.DisplayName = templateDisplayName
|
||||||
|
})
|
||||||
|
require.NotEmpty(t, template.Name)
|
||||||
|
require.NotEmpty(t, template.DisplayName)
|
||||||
|
require.NotEmpty(t, template.Icon)
|
||||||
|
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
ws, err := client.Workspace(ctx, workspace.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, user.UserID, ws.LatestBuild.InitiatorID)
|
||||||
|
assert.Equal(t, codersdk.BuildReasonInitiator, ws.LatestBuild.Reason)
|
||||||
|
assert.Equal(t, template.Name, ws.TemplateName)
|
||||||
|
assert.Equal(t, templateIcon, ws.TemplateIcon)
|
||||||
|
assert.Equal(t, templateDisplayName, ws.TemplateDisplayName)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAdminViewAllWorkspaces(t *testing.T) {
|
func TestAdminViewAllWorkspaces(t *testing.T) {
|
||||||
|
@ -17,20 +17,21 @@ import (
|
|||||||
// Workspace is a deployment of a template. It references a specific
|
// Workspace is a deployment of a template. It references a specific
|
||||||
// version and can be updated.
|
// version and can be updated.
|
||||||
type Workspace struct {
|
type Workspace struct {
|
||||||
ID uuid.UUID `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
OwnerID uuid.UUID `json:"owner_id"`
|
OwnerID uuid.UUID `json:"owner_id"`
|
||||||
OwnerName string `json:"owner_name"`
|
OwnerName string `json:"owner_name"`
|
||||||
TemplateID uuid.UUID `json:"template_id"`
|
TemplateID uuid.UUID `json:"template_id"`
|
||||||
TemplateName string `json:"template_name"`
|
TemplateName string `json:"template_name"`
|
||||||
TemplateIcon string `json:"template_icon"`
|
TemplateDisplayName string `json:"template_display_name"`
|
||||||
LatestBuild WorkspaceBuild `json:"latest_build"`
|
TemplateIcon string `json:"template_icon"`
|
||||||
Outdated bool `json:"outdated"`
|
LatestBuild WorkspaceBuild `json:"latest_build"`
|
||||||
Name string `json:"name"`
|
Outdated bool `json:"outdated"`
|
||||||
AutostartSchedule *string `json:"autostart_schedule,omitempty"`
|
Name string `json:"name"`
|
||||||
TTLMillis *int64 `json:"ttl_ms,omitempty"`
|
AutostartSchedule *string `json:"autostart_schedule,omitempty"`
|
||||||
LastUsedAt time.Time `json:"last_used_at"`
|
TTLMillis *int64 `json:"ttl_ms,omitempty"`
|
||||||
|
LastUsedAt time.Time `json:"last_used_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type WorkspacesRequest struct {
|
type WorkspacesRequest struct {
|
||||||
|
@ -788,6 +788,7 @@ export interface Workspace {
|
|||||||
readonly owner_name: string
|
readonly owner_name: string
|
||||||
readonly template_id: string
|
readonly template_id: string
|
||||||
readonly template_name: string
|
readonly template_name: string
|
||||||
|
readonly template_display_name: string
|
||||||
readonly template_icon: string
|
readonly template_icon: string
|
||||||
readonly latest_build: WorkspaceBuild
|
readonly latest_build: WorkspaceBuild
|
||||||
readonly outdated: boolean
|
readonly outdated: boolean
|
||||||
|
@ -5,7 +5,10 @@ import { FC } from "react"
|
|||||||
import { Link as RouterLink } from "react-router-dom"
|
import { Link as RouterLink } from "react-router-dom"
|
||||||
import { combineClasses } from "util/combineClasses"
|
import { combineClasses } from "util/combineClasses"
|
||||||
import { createDayString } from "util/createDayString"
|
import { createDayString } from "util/createDayString"
|
||||||
import { getDisplayWorkspaceBuildInitiatedBy } from "util/workspace"
|
import {
|
||||||
|
getDisplayWorkspaceBuildInitiatedBy,
|
||||||
|
getDisplayWorkspaceTemplateName,
|
||||||
|
} from "util/workspace"
|
||||||
import { Workspace } from "../../api/typesGenerated"
|
import { Workspace } from "../../api/typesGenerated"
|
||||||
|
|
||||||
const Language = {
|
const Language = {
|
||||||
@ -36,6 +39,7 @@ export const WorkspaceStats: FC<WorkspaceStatsProps> = ({
|
|||||||
const initiatedBy = getDisplayWorkspaceBuildInitiatedBy(
|
const initiatedBy = getDisplayWorkspaceBuildInitiatedBy(
|
||||||
workspace.latest_build,
|
workspace.latest_build,
|
||||||
)
|
)
|
||||||
|
const displayTemplateName = getDisplayWorkspaceTemplateName(workspace)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.stats} aria-label={Language.workspaceDetails}>
|
<div className={styles.stats} aria-label={Language.workspaceDetails}>
|
||||||
@ -46,7 +50,7 @@ export const WorkspaceStats: FC<WorkspaceStatsProps> = ({
|
|||||||
to={`/templates/${workspace.template_name}`}
|
to={`/templates/${workspace.template_name}`}
|
||||||
className={combineClasses([styles.statsValue, styles.link])}
|
className={combineClasses([styles.statsValue, styles.link])}
|
||||||
>
|
>
|
||||||
{workspace.template_name}
|
{displayTemplateName}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.statItem}>
|
<div className={styles.statItem}>
|
||||||
|
@ -7,6 +7,7 @@ import { AvatarData } from "components/AvatarData/AvatarData"
|
|||||||
import { WorkspaceStatusBadge } from "components/WorkspaceStatusBadge/WorkspaceStatusBadge"
|
import { WorkspaceStatusBadge } from "components/WorkspaceStatusBadge/WorkspaceStatusBadge"
|
||||||
import { FC } from "react"
|
import { FC } from "react"
|
||||||
import { useNavigate } from "react-router-dom"
|
import { useNavigate } from "react-router-dom"
|
||||||
|
import { getDisplayWorkspaceTemplateName } from "util/workspace"
|
||||||
import { WorkspaceItemMachineRef } from "../../xServices/workspaces/workspacesXService"
|
import { WorkspaceItemMachineRef } from "../../xServices/workspaces/workspacesXService"
|
||||||
import { LastUsed } from "../LastUsed/LastUsed"
|
import { LastUsed } from "../LastUsed/LastUsed"
|
||||||
import {
|
import {
|
||||||
@ -32,6 +33,7 @@ export const WorkspacesRow: FC<
|
|||||||
const workspacePageLink = `/@${workspace.owner_name}/${workspace.name}`
|
const workspacePageLink = `/@${workspace.owner_name}/${workspace.name}`
|
||||||
const hasTemplateIcon =
|
const hasTemplateIcon =
|
||||||
workspace.template_icon && workspace.template_icon !== ""
|
workspace.template_icon && workspace.template_icon !== ""
|
||||||
|
const displayTemplateName = getDisplayWorkspaceTemplateName(workspace)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRow
|
<TableRow
|
||||||
@ -61,7 +63,7 @@ export const WorkspacesRow: FC<
|
|||||||
</TableCellLink>
|
</TableCellLink>
|
||||||
|
|
||||||
<TableCellLink to={workspacePageLink}>
|
<TableCellLink to={workspacePageLink}>
|
||||||
<TableCellDataPrimary>{workspace.template_name}</TableCellDataPrimary>
|
<TableCellDataPrimary>{displayTemplateName}</TableCellDataPrimary>
|
||||||
</TableCellLink>
|
</TableCellLink>
|
||||||
<TableCellLink to={workspacePageLink}>
|
<TableCellLink to={workspacePageLink}>
|
||||||
<TableCellData>
|
<TableCellData>
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
import { screen, waitFor } from "@testing-library/react"
|
import { screen, waitFor } from "@testing-library/react"
|
||||||
import { rest } from "msw"
|
import { rest } from "msw"
|
||||||
import * as CreateDayString from "util/createDayString"
|
import * as CreateDayString from "util/createDayString"
|
||||||
import { MockWorkspace } from "../../testHelpers/entities"
|
import {
|
||||||
|
MockWorkspace,
|
||||||
|
MockWorkspacesResponse,
|
||||||
|
} from "../../testHelpers/entities"
|
||||||
import { history, render } from "../../testHelpers/renderHelpers"
|
import { history, render } from "../../testHelpers/renderHelpers"
|
||||||
import { server } from "../../testHelpers/server"
|
import { server } from "../../testHelpers/server"
|
||||||
import WorkspacesPage from "./WorkspacesPage"
|
import WorkspacesPage from "./WorkspacesPage"
|
||||||
@ -54,5 +57,9 @@ describe("WorkspacesPage", () => {
|
|||||||
{ timeout: 2000 },
|
{ timeout: 2000 },
|
||||||
)
|
)
|
||||||
await screen.findByText(`${MockWorkspace.name}1`)
|
await screen.findByText(`${MockWorkspace.name}1`)
|
||||||
|
const templateDisplayNames = await screen.findAllByText(
|
||||||
|
`${MockWorkspace.template_display_name}`,
|
||||||
|
)
|
||||||
|
expect(templateDisplayNames).toHaveLength(MockWorkspacesResponse.count)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -434,6 +434,7 @@ export const MockWorkspace: TypesGen.Workspace = {
|
|||||||
template_id: MockTemplate.id,
|
template_id: MockTemplate.id,
|
||||||
template_name: MockTemplate.name,
|
template_name: MockTemplate.name,
|
||||||
template_icon: MockTemplate.icon,
|
template_icon: MockTemplate.icon,
|
||||||
|
template_display_name: MockTemplate.display_name,
|
||||||
outdated: false,
|
outdated: false,
|
||||||
owner_id: MockUser.id,
|
owner_id: MockUser.id,
|
||||||
owner_name: MockUser.username,
|
owner_name: MockUser.username,
|
||||||
|
@ -5,6 +5,7 @@ import {
|
|||||||
defaultWorkspaceExtension,
|
defaultWorkspaceExtension,
|
||||||
getDisplayVersionStatus,
|
getDisplayVersionStatus,
|
||||||
getDisplayWorkspaceBuildInitiatedBy,
|
getDisplayWorkspaceBuildInitiatedBy,
|
||||||
|
getDisplayWorkspaceTemplateName,
|
||||||
isWorkspaceOn,
|
isWorkspaceOn,
|
||||||
} from "./workspace"
|
} from "./workspace"
|
||||||
|
|
||||||
@ -120,4 +121,22 @@ describe("util > workspace", () => {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("getDisplayWorkspaceTemplateName", () => {
|
||||||
|
it("display name is not set", async () => {
|
||||||
|
const workspace: TypesGen.Workspace = {
|
||||||
|
...Mocks.MockWorkspace,
|
||||||
|
template_display_name: "",
|
||||||
|
}
|
||||||
|
const displayed = getDisplayWorkspaceTemplateName(workspace)
|
||||||
|
expect(displayed).toEqual(workspace.template_name)
|
||||||
|
})
|
||||||
|
it("display name is set", async () => {
|
||||||
|
const workspace: TypesGen.Workspace = {
|
||||||
|
...Mocks.MockWorkspace,
|
||||||
|
}
|
||||||
|
const displayed = getDisplayWorkspaceTemplateName(workspace)
|
||||||
|
expect(displayed).toEqual(workspace.template_display_name)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -182,3 +182,11 @@ export const getFaviconByStatus = (
|
|||||||
return "favicon"
|
return "favicon"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getDisplayWorkspaceTemplateName = (
|
||||||
|
workspace: TypesGen.Workspace,
|
||||||
|
): string => {
|
||||||
|
return workspace.template_display_name.length > 0
|
||||||
|
? workspace.template_display_name
|
||||||
|
: workspace.template_name
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user