feat: store and display template creator (#2228)

* design commit

* add owner_id to templates table

* add owner information in apis and ui

* update minWidth for statItem

* rename owner to created_by

* missing refactor to created_by

* handle errors in fetching created_by names
This commit is contained in:
Abhineet Jain
2022-06-10 15:24:21 -04:00
committed by GitHub
parent 46da59a6b5
commit 02d2aea7f2
17 changed files with 135 additions and 24 deletions

View File

@ -88,6 +88,7 @@ func TestDiff(t *testing.T) {
ActiveVersionID: uuid.UUID{3}, ActiveVersionID: uuid.UUID{3},
MaxTtl: int64(time.Hour), MaxTtl: int64(time.Hour),
MinAutostartInterval: int64(time.Minute), MinAutostartInterval: int64(time.Minute),
CreatedBy: uuid.NullUUID{UUID: uuid.UUID{4}, Valid: true},
}, },
exp: audit.Map{ exp: audit.Map{
"id": uuid.UUID{1}.String(), "id": uuid.UUID{1}.String(),
@ -97,6 +98,7 @@ func TestDiff(t *testing.T) {
"active_version_id": uuid.UUID{3}.String(), "active_version_id": uuid.UUID{3}.String(),
"max_ttl": int64(3600000000000), "max_ttl": int64(3600000000000),
"min_autostart_interval": int64(60000000000), "min_autostart_interval": int64(60000000000),
"created_by": uuid.UUID{4}.String(),
}, },
}, },
}) })

View File

@ -72,6 +72,7 @@ var AuditableResources = auditMap(map[any]map[string]Action{
"description": ActionTrack, "description": ActionTrack,
"max_ttl": ActionTrack, "max_ttl": ActionTrack,
"min_autostart_interval": ActionTrack, "min_autostart_interval": ActionTrack,
"created_by": ActionTrack,
}, },
&database.TemplateVersion{}: { &database.TemplateVersion{}: {
"id": ActionTrack, "id": ActionTrack,

View File

@ -1341,6 +1341,7 @@ func (q *fakeQuerier) InsertTemplate(_ context.Context, arg database.InsertTempl
Description: arg.Description, Description: arg.Description,
MaxTtl: arg.MaxTtl, MaxTtl: arg.MaxTtl,
MinAutostartInterval: arg.MinAutostartInterval, MinAutostartInterval: arg.MinAutostartInterval,
CreatedBy: arg.CreatedBy,
} }
q.templates = append(q.templates, template) q.templates = append(q.templates, template)
return template, nil return template, nil

View File

@ -248,7 +248,8 @@ CREATE TABLE templates (
active_version_id uuid NOT NULL, active_version_id uuid NOT NULL,
description character varying(128) DEFAULT ''::character varying NOT NULL, description character varying(128) DEFAULT ''::character varying NOT NULL,
max_ttl bigint DEFAULT '604800000000000'::bigint NOT NULL, max_ttl bigint DEFAULT '604800000000000'::bigint NOT NULL,
min_autostart_interval bigint DEFAULT '3600000000000'::bigint NOT NULL min_autostart_interval bigint DEFAULT '3600000000000'::bigint NOT NULL,
created_by uuid
); );
CREATE TABLE users ( CREATE TABLE users (
@ -476,6 +477,9 @@ ALTER TABLE ONLY template_versions
ALTER TABLE ONLY template_versions ALTER TABLE ONLY template_versions
ADD CONSTRAINT template_versions_template_id_fkey FOREIGN KEY (template_id) REFERENCES templates(id) ON DELETE CASCADE; ADD CONSTRAINT template_versions_template_id_fkey FOREIGN KEY (template_id) REFERENCES templates(id) ON DELETE CASCADE;
ALTER TABLE ONLY templates
ADD CONSTRAINT templates_created_by_fkey FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE RESTRICT;
ALTER TABLE ONLY templates ALTER TABLE ONLY templates
ADD CONSTRAINT templates_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; ADD CONSTRAINT templates_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE;

View File

@ -0,0 +1 @@
ALTER TABLE ONLY templates DROP COLUMN IF EXISTS created_by;

View File

@ -0,0 +1 @@
ALTER TABLE ONLY templates ADD COLUMN IF NOT EXISTS created_by uuid REFERENCES users (id) ON DELETE RESTRICT;

View File

@ -438,6 +438,7 @@ type Template struct {
Description string `db:"description" json:"description"` Description string `db:"description" json:"description"`
MaxTtl int64 `db:"max_ttl" json:"max_ttl"` MaxTtl int64 `db:"max_ttl" json:"max_ttl"`
MinAutostartInterval int64 `db:"min_autostart_interval" json:"min_autostart_interval"` MinAutostartInterval int64 `db:"min_autostart_interval" json:"min_autostart_interval"`
CreatedBy uuid.NullUUID `db:"created_by" json:"created_by"`
} }
type TemplateVersion struct { type TemplateVersion struct {

View File

@ -1603,7 +1603,7 @@ func (q *sqlQuerier) UpdateProvisionerJobWithCompleteByID(ctx context.Context, a
const getTemplateByID = `-- name: GetTemplateByID :one const getTemplateByID = `-- name: GetTemplateByID :one
SELECT SELECT
id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, max_ttl, min_autostart_interval id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, max_ttl, min_autostart_interval, created_by
FROM FROM
templates templates
WHERE WHERE
@ -1627,13 +1627,14 @@ func (q *sqlQuerier) GetTemplateByID(ctx context.Context, id uuid.UUID) (Templat
&i.Description, &i.Description,
&i.MaxTtl, &i.MaxTtl,
&i.MinAutostartInterval, &i.MinAutostartInterval,
&i.CreatedBy,
) )
return i, err return i, err
} }
const getTemplateByOrganizationAndName = `-- name: GetTemplateByOrganizationAndName :one const getTemplateByOrganizationAndName = `-- name: GetTemplateByOrganizationAndName :one
SELECT SELECT
id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, max_ttl, min_autostart_interval id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, max_ttl, min_autostart_interval, created_by
FROM FROM
templates templates
WHERE WHERE
@ -1665,13 +1666,14 @@ func (q *sqlQuerier) GetTemplateByOrganizationAndName(ctx context.Context, arg G
&i.Description, &i.Description,
&i.MaxTtl, &i.MaxTtl,
&i.MinAutostartInterval, &i.MinAutostartInterval,
&i.CreatedBy,
) )
return i, err return i, err
} }
const getTemplatesByIDs = `-- name: GetTemplatesByIDs :many const getTemplatesByIDs = `-- name: GetTemplatesByIDs :many
SELECT SELECT
id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, max_ttl, min_autostart_interval id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, max_ttl, min_autostart_interval, created_by
FROM FROM
templates templates
WHERE WHERE
@ -1699,6 +1701,7 @@ func (q *sqlQuerier) GetTemplatesByIDs(ctx context.Context, ids []uuid.UUID) ([]
&i.Description, &i.Description,
&i.MaxTtl, &i.MaxTtl,
&i.MinAutostartInterval, &i.MinAutostartInterval,
&i.CreatedBy,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@ -1715,7 +1718,7 @@ func (q *sqlQuerier) GetTemplatesByIDs(ctx context.Context, ids []uuid.UUID) ([]
const getTemplatesByOrganization = `-- name: GetTemplatesByOrganization :many const getTemplatesByOrganization = `-- name: GetTemplatesByOrganization :many
SELECT SELECT
id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, max_ttl, min_autostart_interval id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, max_ttl, min_autostart_interval, created_by
FROM FROM
templates templates
WHERE WHERE
@ -1749,6 +1752,7 @@ func (q *sqlQuerier) GetTemplatesByOrganization(ctx context.Context, arg GetTemp
&i.Description, &i.Description,
&i.MaxTtl, &i.MaxTtl,
&i.MinAutostartInterval, &i.MinAutostartInterval,
&i.CreatedBy,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@ -1775,10 +1779,11 @@ INSERT INTO
active_version_id, active_version_id,
description, description,
max_ttl, max_ttl,
min_autostart_interval min_autostart_interval,
created_by
) )
VALUES VALUES
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, max_ttl, min_autostart_interval ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) RETURNING id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, max_ttl, min_autostart_interval, created_by
` `
type InsertTemplateParams struct { type InsertTemplateParams struct {
@ -1792,6 +1797,7 @@ type InsertTemplateParams struct {
Description string `db:"description" json:"description"` Description string `db:"description" json:"description"`
MaxTtl int64 `db:"max_ttl" json:"max_ttl"` MaxTtl int64 `db:"max_ttl" json:"max_ttl"`
MinAutostartInterval int64 `db:"min_autostart_interval" json:"min_autostart_interval"` MinAutostartInterval int64 `db:"min_autostart_interval" json:"min_autostart_interval"`
CreatedBy uuid.NullUUID `db:"created_by" json:"created_by"`
} }
func (q *sqlQuerier) InsertTemplate(ctx context.Context, arg InsertTemplateParams) (Template, error) { func (q *sqlQuerier) InsertTemplate(ctx context.Context, arg InsertTemplateParams) (Template, error) {
@ -1806,6 +1812,7 @@ func (q *sqlQuerier) InsertTemplate(ctx context.Context, arg InsertTemplateParam
arg.Description, arg.Description,
arg.MaxTtl, arg.MaxTtl,
arg.MinAutostartInterval, arg.MinAutostartInterval,
arg.CreatedBy,
) )
var i Template var i Template
err := row.Scan( err := row.Scan(
@ -1820,6 +1827,7 @@ func (q *sqlQuerier) InsertTemplate(ctx context.Context, arg InsertTemplateParam
&i.Description, &i.Description,
&i.MaxTtl, &i.MaxTtl,
&i.MinAutostartInterval, &i.MinAutostartInterval,
&i.CreatedBy,
) )
return i, err return i, err
} }
@ -1873,7 +1881,7 @@ SET
WHERE WHERE
id = $1 id = $1
RETURNING RETURNING
id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, max_ttl, min_autostart_interval id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, max_ttl, min_autostart_interval, created_by
` `
type UpdateTemplateMetaByIDParams struct { type UpdateTemplateMetaByIDParams struct {

View File

@ -49,10 +49,11 @@ INSERT INTO
active_version_id, active_version_id,
description, description,
max_ttl, max_ttl,
min_autostart_interval min_autostart_interval,
created_by
) )
VALUES VALUES
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING *; ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) RETURNING *;
-- name: UpdateTemplateActiveVersionByID :exec -- name: UpdateTemplateActiveVersionByID :exec
UPDATE UPDATE

View File

@ -1,6 +1,7 @@
package coderd package coderd
import ( import (
"context"
"database/sql" "database/sql"
"errors" "errors"
"fmt" "fmt"
@ -49,7 +50,16 @@ func (api *API) template(rw http.ResponseWriter, r *http.Request) {
count = uint32(workspaceCounts[0].Count) count = uint32(workspaceCounts[0].Count)
} }
httpapi.Write(rw, http.StatusOK, convertTemplate(template, count)) createdByNameMap, err := getCreatedByNamesByTemplateIDs(r.Context(), api.Database, []database.Template{template})
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: "Internal error fetching creator name.",
Detail: err.Error(),
})
return
}
httpapi.Write(rw, http.StatusOK, convertTemplate(template, count, createdByNameMap[template.ID.String()]))
} }
func (api *API) deleteTemplate(rw http.ResponseWriter, r *http.Request) { func (api *API) deleteTemplate(rw http.ResponseWriter, r *http.Request) {
@ -97,6 +107,7 @@ func (api *API) deleteTemplate(rw http.ResponseWriter, r *http.Request) {
func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Request) { func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Request) {
var createTemplate codersdk.CreateTemplateRequest var createTemplate codersdk.CreateTemplateRequest
organization := httpmw.OrganizationParam(r) organization := httpmw.OrganizationParam(r)
apiKey := httpmw.APIKey(r)
if !api.Authorize(rw, r, rbac.ActionCreate, rbac.ResourceTemplate.InOrg(organization.ID)) { if !api.Authorize(rw, r, rbac.ActionCreate, rbac.ResourceTemplate.InOrg(organization.ID)) {
return return
} }
@ -175,6 +186,10 @@ func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Reque
Description: createTemplate.Description, Description: createTemplate.Description,
MaxTtl: int64(maxTTL), MaxTtl: int64(maxTTL),
MinAutostartInterval: int64(minAutostartInterval), MinAutostartInterval: int64(minAutostartInterval),
CreatedBy: uuid.NullUUID{
UUID: apiKey.UserID,
Valid: true,
},
}) })
if err != nil { if err != nil {
return xerrors.Errorf("insert template: %s", err) return xerrors.Errorf("insert template: %s", err)
@ -208,7 +223,12 @@ func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Reque
} }
} }
template = convertTemplate(dbTemplate, 0) createdByNameMap, err := getCreatedByNamesByTemplateIDs(r.Context(), db, []database.Template{dbTemplate})
if err != nil {
return xerrors.Errorf("get creator name: %w", err)
}
template = convertTemplate(dbTemplate, 0, createdByNameMap[dbTemplate.ID.String()])
return nil return nil
}) })
if err != nil { if err != nil {
@ -258,7 +278,16 @@ func (api *API) templatesByOrganization(rw http.ResponseWriter, r *http.Request)
return return
} }
httpapi.Write(rw, http.StatusOK, convertTemplates(templates, workspaceCounts)) createdByNameMap, err := getCreatedByNamesByTemplateIDs(r.Context(), api.Database, templates)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: "Internal error fetching creator names.",
Detail: err.Error(),
})
return
}
httpapi.Write(rw, http.StatusOK, convertTemplates(templates, workspaceCounts, createdByNameMap))
} }
func (api *API) templateByOrganizationAndName(rw http.ResponseWriter, r *http.Request) { func (api *API) templateByOrganizationAndName(rw http.ResponseWriter, r *http.Request) {
@ -304,7 +333,16 @@ func (api *API) templateByOrganizationAndName(rw http.ResponseWriter, r *http.Re
count = uint32(workspaceCounts[0].Count) count = uint32(workspaceCounts[0].Count)
} }
httpapi.Write(rw, http.StatusOK, convertTemplate(template, count)) createdByNameMap, err := getCreatedByNamesByTemplateIDs(r.Context(), api.Database, []database.Template{template})
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: "Internal error fetching creator name.",
Detail: err.Error(),
})
return
}
httpapi.Write(rw, http.StatusOK, convertTemplate(template, count, createdByNameMap[template.ID.String()]))
} }
func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) { func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
@ -400,10 +438,35 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
return return
} }
httpapi.Write(rw, http.StatusOK, convertTemplate(updated, count)) createdByNameMap, err := getCreatedByNamesByTemplateIDs(r.Context(), api.Database, []database.Template{updated})
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: "Internal error fetching creator name.",
Detail: err.Error(),
})
return
}
httpapi.Write(rw, http.StatusOK, convertTemplate(updated, count, createdByNameMap[updated.ID.String()]))
} }
func convertTemplates(templates []database.Template, workspaceCounts []database.GetWorkspaceOwnerCountsByTemplateIDsRow) []codersdk.Template { func getCreatedByNamesByTemplateIDs(ctx context.Context, db database.Store, templates []database.Template) (map[string]string, error) {
creators := make(map[string]string, len(templates))
for _, template := range templates {
if template.CreatedBy.Valid {
creator, err := db.GetUserByID(ctx, template.CreatedBy.UUID)
if err != nil {
return map[string]string{}, err
}
creators[template.ID.String()] = creator.Username
} else {
creators[template.ID.String()] = ""
}
}
return creators, nil
}
func convertTemplates(templates []database.Template, workspaceCounts []database.GetWorkspaceOwnerCountsByTemplateIDsRow, createdByNameMap map[string]string) []codersdk.Template {
apiTemplates := make([]codersdk.Template, 0, len(templates)) apiTemplates := make([]codersdk.Template, 0, len(templates))
for _, template := range templates { for _, template := range templates {
found := false found := false
@ -411,18 +474,18 @@ func convertTemplates(templates []database.Template, workspaceCounts []database.
if workspaceCount.TemplateID.String() != template.ID.String() { if workspaceCount.TemplateID.String() != template.ID.String() {
continue continue
} }
apiTemplates = append(apiTemplates, convertTemplate(template, uint32(workspaceCount.Count))) apiTemplates = append(apiTemplates, convertTemplate(template, uint32(workspaceCount.Count), createdByNameMap[template.ID.String()]))
found = true found = true
break break
} }
if !found { if !found {
apiTemplates = append(apiTemplates, convertTemplate(template, uint32(0))) apiTemplates = append(apiTemplates, convertTemplate(template, uint32(0), createdByNameMap[template.ID.String()]))
} }
} }
return apiTemplates return apiTemplates
} }
func convertTemplate(template database.Template, workspaceOwnerCount uint32) codersdk.Template { func convertTemplate(template database.Template, workspaceOwnerCount uint32, createdByName string) codersdk.Template {
return codersdk.Template{ return codersdk.Template{
ID: template.ID, ID: template.ID,
CreatedAt: template.CreatedAt, CreatedAt: template.CreatedAt,
@ -435,5 +498,7 @@ func convertTemplate(template database.Template, workspaceOwnerCount uint32) cod
Description: template.Description, Description: template.Description,
MaxTTLMillis: time.Duration(template.MaxTtl).Milliseconds(), MaxTTLMillis: time.Duration(template.MaxTtl).Milliseconds(),
MinAutostartIntervalMillis: time.Duration(template.MinAutostartInterval).Milliseconds(), MinAutostartIntervalMillis: time.Duration(template.MinAutostartInterval).Milliseconds(),
CreatedByID: template.CreatedBy,
CreatedByName: createdByName,
} }
} }

View File

@ -25,6 +25,8 @@ type Template struct {
Description string `json:"description"` Description string `json:"description"`
MaxTTLMillis int64 `json:"max_ttl_ms"` MaxTTLMillis int64 `json:"max_ttl_ms"`
MinAutostartIntervalMillis int64 `json:"min_autostart_interval_ms"` MinAutostartIntervalMillis int64 `json:"min_autostart_interval_ms"`
CreatedByID uuid.NullUUID `json:"created_by_id"`
CreatedByName string `json:"created_by_name"`
} }
type UpdateActiveTemplateVersion struct { type UpdateActiveTemplateVersion struct {

View File

@ -247,6 +247,8 @@ export interface Template {
readonly description: string readonly description: string
readonly max_ttl_ms: number readonly max_ttl_ms: number
readonly min_autostart_interval_ms: number readonly min_autostart_interval_ms: number
readonly created_by_id?: string
readonly created_by_name: string
} }
// From codersdk/templateversions.go:14:6 // From codersdk/templateversions.go:14:6
@ -276,12 +278,12 @@ export interface TemplateVersionParameter {
readonly default_source_value: boolean readonly default_source_value: boolean
} }
// From codersdk/templates.go:98:6 // From codersdk/templates.go:100:6
export interface TemplateVersionsByTemplateRequest extends Pagination { export interface TemplateVersionsByTemplateRequest extends Pagination {
readonly template_id: string readonly template_id: string
} }
// From codersdk/templates.go:30:6 // From codersdk/templates.go:32:6
export interface UpdateActiveTemplateVersion { export interface UpdateActiveTemplateVersion {
readonly id: string readonly id: string
} }
@ -291,7 +293,7 @@ export interface UpdateRoles {
readonly roles: string[] readonly roles: string[]
} }
// From codersdk/templates.go:34:6 // From codersdk/templates.go:36:6
export interface UpdateTemplateMeta { export interface UpdateTemplateMeta {
readonly description?: string readonly description?: string
readonly max_ttl_ms?: number readonly max_ttl_ms?: number

View File

@ -23,3 +23,12 @@ UsedByMany.args = {
}, },
activeVersion: Mocks.MockTemplateVersion, activeVersion: Mocks.MockTemplateVersion,
} }
export const UnknownCreator = Template.bind({})
UnknownCreator.args = {
template: {
...Mocks.MockTemplate,
created_by_name: "",
},
activeVersion: Mocks.MockTemplateVersion,
}

View File

@ -13,6 +13,8 @@ const Language = {
lastUpdateLabel: "Last updated", lastUpdateLabel: "Last updated",
userPlural: "users", userPlural: "users",
userSingular: "user", userSingular: "user",
createdByLabel: "Created by",
defaultTemplateCreator: "<unknown>",
} }
export interface TemplateStatsProps { export interface TemplateStatsProps {
@ -45,6 +47,11 @@ export const TemplateStats: FC<TemplateStatsProps> = ({ template, activeVersion
{dayjs().to(dayjs(template.updated_at))} {dayjs().to(dayjs(template.updated_at))}
</span> </span>
</div> </div>
<div className={styles.statsDivider} />
<div className={styles.statItem}>
<span className={styles.statsLabel}>{Language.createdByLabel}</span>
<span className={styles.statsValue}>{template.created_by_name || Language.defaultTemplateCreator}</span>
</div>
</div> </div>
) )
} }
@ -63,7 +70,7 @@ const useStyles = makeStyles((theme) => ({
}, },
statItem: { statItem: {
minWidth: theme.spacing(20), minWidth: "20%",
padding: theme.spacing(2), padding: theme.spacing(2),
paddingTop: theme.spacing(1.75), paddingTop: theme.spacing(1.75),
}, },

View File

@ -69,7 +69,7 @@ const useStyles = makeStyles((theme) => ({
}, },
statItem: { statItem: {
minWidth: theme.spacing(20), minWidth: "16%",
padding: theme.spacing(2), padding: theme.spacing(2),
paddingTop: theme.spacing(1.75), paddingTop: theme.spacing(1.75),
}, },

View File

@ -49,6 +49,8 @@ export const Language = {
templateTooltipTitle: "What is template?", templateTooltipTitle: "What is template?",
templateTooltipText: "With templates you can create a common configuration for your workspaces using Terraform.", templateTooltipText: "With templates you can create a common configuration for your workspaces using Terraform.",
templateTooltipLink: "Manage templates", templateTooltipLink: "Manage templates",
createdByLabel: "Created by",
defaultTemplateCreator: "<unknown>",
} }
const TemplateHelpTooltip: React.FC = () => { const TemplateHelpTooltip: React.FC = () => {
@ -95,6 +97,7 @@ export const TemplatesPageView: FC<TemplatesPageViewProps> = (props) => {
<TableCell>{Language.nameLabel}</TableCell> <TableCell>{Language.nameLabel}</TableCell>
<TableCell>{Language.usedByLabel}</TableCell> <TableCell>{Language.usedByLabel}</TableCell>
<TableCell>{Language.lastUpdatedLabel}</TableCell> <TableCell>{Language.lastUpdatedLabel}</TableCell>
<TableCell>{Language.createdByLabel}</TableCell>
<TableCell width="1%"></TableCell> <TableCell width="1%"></TableCell>
</TableRow> </TableRow>
</TableHead> </TableHead>
@ -137,6 +140,7 @@ export const TemplatesPageView: FC<TemplatesPageViewProps> = (props) => {
<TableCell>{Language.developerCount(template.workspace_owner_count)}</TableCell> <TableCell>{Language.developerCount(template.workspace_owner_count)}</TableCell>
<TableCell data-chromatic="ignore">{dayjs().to(dayjs(template.updated_at))}</TableCell> <TableCell data-chromatic="ignore">{dayjs().to(dayjs(template.updated_at))}</TableCell>
<TableCell>{template.created_by_name || Language.defaultTemplateCreator}</TableCell>
<TableCell> <TableCell>
<div className={styles.arrowCell}> <div className={styles.arrowCell}>
<KeyboardArrowRight className={styles.arrowRight} /> <KeyboardArrowRight className={styles.arrowRight} />

View File

@ -114,6 +114,8 @@ export const MockTemplate: TypesGen.Template = {
description: "This is a test description.", description: "This is a test description.",
max_ttl_ms: 604800000, max_ttl_ms: 604800000,
min_autostart_interval_ms: 3600000, min_autostart_interval_ms: 3600000,
created_by_id: "test-creator-id",
created_by_name: "test_creator",
} }
export const MockWorkspaceAutostartDisabled: TypesGen.UpdateWorkspaceAutostartRequest = { export const MockWorkspaceAutostartDisabled: TypesGen.UpdateWorkspaceAutostartRequest = {