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:
Marcin Tojek
2022-11-16 15:50:32 +01:00
committed by GitHub
parent c1ecc91aab
commit 32927b1a24
10 changed files with 106 additions and 32 deletions

View File

@ -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,
} }
} }

View File

@ -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) {

View File

@ -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 {

View File

@ -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

View File

@ -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}>

View File

@ -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>

View File

@ -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)
}) })
}) })

View File

@ -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,

View File

@ -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)
})
})
}) })

View File

@ -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
}