diff --git a/cli/templateedit.go b/cli/templateedit.go index 0a84f04f69283..166c29793b9cc 100644 --- a/cli/templateedit.go +++ b/cli/templateedit.go @@ -15,6 +15,7 @@ func templateEdit() *cobra.Command { var ( name string description string + icon string maxTTL time.Duration minAutostartInterval time.Duration ) @@ -41,6 +42,7 @@ func templateEdit() *cobra.Command { req := codersdk.UpdateTemplateMeta{ Name: name, Description: description, + Icon: icon, MaxTTLMillis: maxTTL.Milliseconds(), MinAutostartIntervalMillis: minAutostartInterval.Milliseconds(), } @@ -56,6 +58,7 @@ func templateEdit() *cobra.Command { cmd.Flags().StringVarP(&name, "name", "", "", "Edit the template name") cmd.Flags().StringVarP(&description, "description", "", "", "Edit the template description") + cmd.Flags().StringVarP(&icon, "icon", "", "", "Edit the template icon path") cmd.Flags().DurationVarP(&maxTTL, "max-ttl", "", 0, "Edit the template maximum time before shutdown") cmd.Flags().DurationVarP(&minAutostartInterval, "min-autostart-interval", "", 0, "Edit the template minimum autostart interval") cliui.AllowSkipPrompt(cmd) diff --git a/cli/templateedit_test.go b/cli/templateedit_test.go index 6acaabcbbf64d..24c38d9d9b534 100644 --- a/cli/templateedit_test.go +++ b/cli/templateedit_test.go @@ -25,6 +25,7 @@ func TestTemplateEdit(t *testing.T) { _ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) { ctr.Description = "original description" + ctr.Icon = "/icons/default-icon.png" ctr.MaxTTLMillis = ptr.Ref(24 * time.Hour.Milliseconds()) ctr.MinAutostartIntervalMillis = ptr.Ref(time.Hour.Milliseconds()) }) @@ -32,6 +33,7 @@ func TestTemplateEdit(t *testing.T) { // Test the cli command. name := "new-template-name" desc := "lorem ipsum dolor sit amet et cetera" + icon := "/icons/new-icon.png" maxTTL := 12 * time.Hour minAutostartInterval := time.Minute cmdArgs := []string{ @@ -40,6 +42,7 @@ func TestTemplateEdit(t *testing.T) { template.Name, "--name", name, "--description", desc, + "--icon", icon, "--max-ttl", maxTTL.String(), "--min-autostart-interval", minAutostartInterval.String(), } @@ -55,6 +58,7 @@ func TestTemplateEdit(t *testing.T) { require.NoError(t, err) assert.Equal(t, name, updated.Name) assert.Equal(t, desc, updated.Description) + assert.Equal(t, icon, updated.Icon) assert.Equal(t, maxTTL.Milliseconds(), updated.MaxTTLMillis) assert.Equal(t, minAutostartInterval.Milliseconds(), updated.MinAutostartIntervalMillis) }) @@ -67,6 +71,7 @@ func TestTemplateEdit(t *testing.T) { _ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) { ctr.Description = "original description" + ctr.Icon = "/icons/default-icon.png" ctr.MaxTTLMillis = ptr.Ref(24 * time.Hour.Milliseconds()) ctr.MinAutostartIntervalMillis = ptr.Ref(time.Hour.Milliseconds()) }) @@ -78,6 +83,7 @@ func TestTemplateEdit(t *testing.T) { template.Name, "--name", template.Name, "--description", template.Description, + "--icon", template.Icon, "--max-ttl", (time.Duration(template.MaxTTLMillis) * time.Millisecond).String(), "--min-autostart-interval", (time.Duration(template.MinAutostartIntervalMillis) * time.Millisecond).String(), } @@ -93,6 +99,7 @@ func TestTemplateEdit(t *testing.T) { require.NoError(t, err) assert.Equal(t, template.Name, updated.Name) assert.Equal(t, template.Description, updated.Description) + assert.Equal(t, template.Icon, updated.Icon) assert.Equal(t, template.MaxTTLMillis, updated.MaxTTLMillis) assert.Equal(t, template.MinAutostartIntervalMillis, updated.MinAutostartIntervalMillis) }) diff --git a/coderd/audit/table.go b/coderd/audit/table.go index 6a44bdc88b653..865d28831c073 100644 --- a/coderd/audit/table.go +++ b/coderd/audit/table.go @@ -70,6 +70,7 @@ var AuditableResources = auditMap(map[any]map[string]Action{ "provisioner": ActionTrack, "active_version_id": ActionTrack, "description": ActionTrack, + "icon": ActionTrack, "max_ttl": ActionTrack, "min_autostart_interval": ActionTrack, "created_by": ActionTrack, diff --git a/coderd/database/databasefake/databasefake.go b/coderd/database/databasefake/databasefake.go index bf88994256452..1e39e0a2fa54b 100644 --- a/coderd/database/databasefake/databasefake.go +++ b/coderd/database/databasefake/databasefake.go @@ -883,6 +883,7 @@ func (q *fakeQuerier) UpdateTemplateMetaByID(_ context.Context, arg database.Upd tpl.UpdatedAt = database.Now() tpl.Name = arg.Name tpl.Description = arg.Description + tpl.Icon = arg.Icon tpl.MaxTtl = arg.MaxTtl tpl.MinAutostartInterval = arg.MinAutostartInterval q.templates[idx] = tpl diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 1db9442d93543..7ca153c7f562d 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -260,7 +260,8 @@ CREATE TABLE templates ( description character varying(128) DEFAULT ''::character varying NOT NULL, max_ttl bigint DEFAULT '604800000000000'::bigint NOT NULL, min_autostart_interval bigint DEFAULT '3600000000000'::bigint NOT NULL, - created_by uuid NOT NULL + created_by uuid NOT NULL, + icon character varying(256) DEFAULT ''::character varying NOT NULL ); CREATE TABLE user_links ( diff --git a/coderd/database/migrations/000036_template_icon.down.sql b/coderd/database/migrations/000036_template_icon.down.sql new file mode 100644 index 0000000000000..c03f75e789b72 --- /dev/null +++ b/coderd/database/migrations/000036_template_icon.down.sql @@ -0,0 +1 @@ +ALTER TABLE templates DROP COLUMN icon; diff --git a/coderd/database/migrations/000036_template_icon.up.sql b/coderd/database/migrations/000036_template_icon.up.sql new file mode 100644 index 0000000000000..fe61612c861d6 --- /dev/null +++ b/coderd/database/migrations/000036_template_icon.up.sql @@ -0,0 +1 @@ +ALTER TABLE templates ADD COLUMN icon VARCHAR(256) NOT NULL DEFAULT ''; diff --git a/coderd/database/models.go b/coderd/database/models.go index 25ed8eedf3190..68c4414664f02 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -464,6 +464,7 @@ type Template struct { MaxTtl int64 `db:"max_ttl" json:"max_ttl"` MinAutostartInterval int64 `db:"min_autostart_interval" json:"min_autostart_interval"` CreatedBy uuid.UUID `db:"created_by" json:"created_by"` + Icon string `db:"icon" json:"icon"` } type TemplateVersion struct { diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index b4fc10691f4fe..490b888479817 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -1786,7 +1786,7 @@ func (q *sqlQuerier) InsertDeploymentID(ctx context.Context, value string) error const getTemplateByID = `-- name: GetTemplateByID :one SELECT - id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, max_ttl, min_autostart_interval, created_by + id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, max_ttl, min_autostart_interval, created_by, icon FROM templates WHERE @@ -1811,13 +1811,14 @@ func (q *sqlQuerier) GetTemplateByID(ctx context.Context, id uuid.UUID) (Templat &i.MaxTtl, &i.MinAutostartInterval, &i.CreatedBy, + &i.Icon, ) return i, err } const getTemplateByOrganizationAndName = `-- name: GetTemplateByOrganizationAndName :one SELECT - id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, max_ttl, min_autostart_interval, created_by + id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, max_ttl, min_autostart_interval, created_by, icon FROM templates WHERE @@ -1850,12 +1851,13 @@ func (q *sqlQuerier) GetTemplateByOrganizationAndName(ctx context.Context, arg G &i.MaxTtl, &i.MinAutostartInterval, &i.CreatedBy, + &i.Icon, ) return i, err } const getTemplates = `-- name: GetTemplates :many -SELECT id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, max_ttl, min_autostart_interval, created_by FROM templates +SELECT id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, max_ttl, min_autostart_interval, created_by, icon FROM templates ORDER BY (name, id) ASC ` @@ -1881,6 +1883,7 @@ func (q *sqlQuerier) GetTemplates(ctx context.Context) ([]Template, error) { &i.MaxTtl, &i.MinAutostartInterval, &i.CreatedBy, + &i.Icon, ); err != nil { return nil, err } @@ -1897,7 +1900,7 @@ func (q *sqlQuerier) GetTemplates(ctx context.Context) ([]Template, error) { const getTemplatesWithFilter = `-- name: GetTemplatesWithFilter :many SELECT - id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, max_ttl, min_autostart_interval, created_by + id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, max_ttl, min_autostart_interval, created_by, icon FROM templates WHERE @@ -1958,6 +1961,7 @@ func (q *sqlQuerier) GetTemplatesWithFilter(ctx context.Context, arg GetTemplate &i.MaxTtl, &i.MinAutostartInterval, &i.CreatedBy, + &i.Icon, ); err != nil { return nil, err } @@ -1985,10 +1989,11 @@ INSERT INTO description, max_ttl, min_autostart_interval, - created_by + created_by, + icon ) VALUES - ($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 + ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, max_ttl, min_autostart_interval, created_by, icon ` type InsertTemplateParams struct { @@ -2003,6 +2008,7 @@ type InsertTemplateParams struct { MaxTtl int64 `db:"max_ttl" json:"max_ttl"` MinAutostartInterval int64 `db:"min_autostart_interval" json:"min_autostart_interval"` CreatedBy uuid.UUID `db:"created_by" json:"created_by"` + Icon string `db:"icon" json:"icon"` } func (q *sqlQuerier) InsertTemplate(ctx context.Context, arg InsertTemplateParams) (Template, error) { @@ -2018,6 +2024,7 @@ func (q *sqlQuerier) InsertTemplate(ctx context.Context, arg InsertTemplateParam arg.MaxTtl, arg.MinAutostartInterval, arg.CreatedBy, + arg.Icon, ) var i Template err := row.Scan( @@ -2033,6 +2040,7 @@ func (q *sqlQuerier) InsertTemplate(ctx context.Context, arg InsertTemplateParam &i.MaxTtl, &i.MinAutostartInterval, &i.CreatedBy, + &i.Icon, ) return i, err } @@ -2087,11 +2095,12 @@ SET description = $3, max_ttl = $4, min_autostart_interval = $5, - name = $6 + name = $6, + icon = $7 WHERE id = $1 RETURNING - id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, max_ttl, min_autostart_interval, created_by + id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, max_ttl, min_autostart_interval, created_by, icon ` type UpdateTemplateMetaByIDParams struct { @@ -2101,6 +2110,7 @@ type UpdateTemplateMetaByIDParams struct { MaxTtl int64 `db:"max_ttl" json:"max_ttl"` MinAutostartInterval int64 `db:"min_autostart_interval" json:"min_autostart_interval"` Name string `db:"name" json:"name"` + Icon string `db:"icon" json:"icon"` } func (q *sqlQuerier) UpdateTemplateMetaByID(ctx context.Context, arg UpdateTemplateMetaByIDParams) error { @@ -2111,6 +2121,7 @@ func (q *sqlQuerier) UpdateTemplateMetaByID(ctx context.Context, arg UpdateTempl arg.MaxTtl, arg.MinAutostartInterval, arg.Name, + arg.Icon, ) return err } diff --git a/coderd/database/queries/templates.sql b/coderd/database/queries/templates.sql index f59d899597414..b6583c9284fdf 100644 --- a/coderd/database/queries/templates.sql +++ b/coderd/database/queries/templates.sql @@ -67,10 +67,11 @@ INSERT INTO description, max_ttl, min_autostart_interval, - created_by + created_by, + icon ) VALUES - ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) RETURNING *; + ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING *; -- name: UpdateTemplateActiveVersionByID :exec UPDATE @@ -98,7 +99,8 @@ SET description = $3, max_ttl = $4, min_autostart_interval = $5, - name = $6 + name = $6, + icon = $7 WHERE id = $1 RETURNING diff --git a/coderd/templates.go b/coderd/templates.go index 321b9872a5f46..5dc43d11caaa3 100644 --- a/coderd/templates.go +++ b/coderd/templates.go @@ -411,6 +411,7 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) { if req.Name == template.Name && req.Description == template.Description && + req.Icon == template.Icon && req.MaxTTLMillis == time.Duration(template.MaxTtl).Milliseconds() && req.MinAutostartIntervalMillis == time.Duration(template.MinAutostartInterval).Milliseconds() { return nil @@ -419,6 +420,7 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) { // Update template metadata -- empty fields are not overwritten. name := req.Name desc := req.Description + icon := req.Icon maxTTL := time.Duration(req.MaxTTLMillis) * time.Millisecond minAutostartInterval := time.Duration(req.MinAutostartIntervalMillis) * time.Millisecond @@ -428,6 +430,9 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) { if desc == "" { desc = template.Description } + if icon == "" { + name = template.Icon + } if maxTTL == 0 { maxTTL = time.Duration(template.MaxTtl) } @@ -440,6 +445,7 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) { UpdatedAt: database.Now(), Name: name, Description: desc, + Icon: icon, MaxTtl: int64(maxTTL), MinAutostartInterval: int64(minAutostartInterval), }); err != nil { @@ -519,6 +525,7 @@ func convertTemplate(template database.Template, workspaceOwnerCount uint32, cre ActiveVersionID: template.ActiveVersionID, WorkspaceOwnerCount: workspaceOwnerCount, Description: template.Description, + Icon: template.Icon, MaxTTLMillis: time.Duration(template.MaxTtl).Milliseconds(), MinAutostartIntervalMillis: time.Duration(template.MinAutostartInterval).Milliseconds(), CreatedByID: template.CreatedBy, diff --git a/coderd/templates_test.go b/coderd/templates_test.go index c4cfae7f16ac6..20fec8e64e144 100644 --- a/coderd/templates_test.go +++ b/coderd/templates_test.go @@ -233,12 +233,14 @@ func TestPatchTemplateMeta(t *testing.T) { version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) { ctr.Description = "original description" + ctr.Icon = "/icons/original-icon.png" ctr.MaxTTLMillis = ptr.Ref(24 * time.Hour.Milliseconds()) ctr.MinAutostartIntervalMillis = ptr.Ref(time.Hour.Milliseconds()) }) req := codersdk.UpdateTemplateMeta{ Name: "new-template-name", Description: "lorem ipsum dolor sit amet et cetera", + Icon: "/icons/new-icon.png", MaxTTLMillis: 12 * time.Hour.Milliseconds(), MinAutostartIntervalMillis: time.Minute.Milliseconds(), } @@ -254,6 +256,7 @@ func TestPatchTemplateMeta(t *testing.T) { assert.Greater(t, updated.UpdatedAt, template.UpdatedAt) assert.Equal(t, req.Name, updated.Name) assert.Equal(t, req.Description, updated.Description) + assert.Equal(t, req.Icon, updated.Icon) assert.Equal(t, req.MaxTTLMillis, updated.MaxTTLMillis) assert.Equal(t, req.MinAutostartIntervalMillis, updated.MinAutostartIntervalMillis) @@ -263,6 +266,7 @@ func TestPatchTemplateMeta(t *testing.T) { assert.Greater(t, updated.UpdatedAt, template.UpdatedAt) assert.Equal(t, req.Name, updated.Name) assert.Equal(t, req.Description, updated.Description) + assert.Equal(t, req.Icon, updated.Icon) assert.Equal(t, req.MaxTTLMillis, updated.MaxTTLMillis) assert.Equal(t, req.MinAutostartIntervalMillis, updated.MinAutostartIntervalMillis) }) @@ -275,6 +279,7 @@ func TestPatchTemplateMeta(t *testing.T) { version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) { ctr.Description = "original description" + ctr.Icon = "/icons/original-icon.png" ctr.MaxTTLMillis = ptr.Ref(24 * time.Hour.Milliseconds()) ctr.MinAutostartIntervalMillis = ptr.Ref(time.Hour.Milliseconds()) }) @@ -285,6 +290,7 @@ func TestPatchTemplateMeta(t *testing.T) { req := codersdk.UpdateTemplateMeta{ Name: template.Name, Description: template.Description, + Icon: template.Icon, MaxTTLMillis: template.MaxTTLMillis, MinAutostartIntervalMillis: template.MinAutostartIntervalMillis, } @@ -295,6 +301,7 @@ func TestPatchTemplateMeta(t *testing.T) { assert.Equal(t, updated.UpdatedAt, template.UpdatedAt) assert.Equal(t, template.Name, updated.Name) assert.Equal(t, template.Description, updated.Description) + assert.Equal(t, template.Icon, updated.Icon) assert.Equal(t, template.MaxTTLMillis, updated.MaxTTLMillis) assert.Equal(t, template.MinAutostartIntervalMillis, updated.MinAutostartIntervalMillis) }) @@ -331,6 +338,7 @@ func TestPatchTemplateMeta(t *testing.T) { assert.WithinDuration(t, template.UpdatedAt, updated.UpdatedAt, time.Minute) assert.Equal(t, template.Name, updated.Name) assert.Equal(t, template.Description, updated.Description) + assert.Equal(t, template.Icon, updated.Icon) assert.Equal(t, template.MaxTTLMillis, updated.MaxTTLMillis) assert.Equal(t, template.MinAutostartIntervalMillis, updated.MinAutostartIntervalMillis) }) diff --git a/codersdk/organizations.go b/codersdk/organizations.go index 400c010f9d15b..a3ef0a7a000e3 100644 --- a/codersdk/organizations.go +++ b/codersdk/organizations.go @@ -52,6 +52,9 @@ type CreateTemplateRequest struct { // Description is a description of what the template contains. It must be // less than 128 bytes. Description string `json:"description,omitempty" validate:"lt=128"` + // Icon is a relative path or external URL that specifies + // an icon to be displayed in the dashboard. + Icon string `json:"icon,omitempty"` // VersionID is an in-progress or completed job to use as an initial version // of the template. diff --git a/codersdk/templates.go b/codersdk/templates.go index 6e8a0668f69c6..38ec2b4ce6d99 100644 --- a/codersdk/templates.go +++ b/codersdk/templates.go @@ -23,6 +23,7 @@ type Template struct { ActiveVersionID uuid.UUID `json:"active_version_id"` WorkspaceOwnerCount uint32 `json:"workspace_owner_count"` Description string `json:"description"` + Icon string `json:"icon"` MaxTTLMillis int64 `json:"max_ttl_ms"` MinAutostartIntervalMillis int64 `json:"min_autostart_interval_ms"` CreatedByID uuid.UUID `json:"created_by_id"` @@ -36,6 +37,7 @@ type UpdateActiveTemplateVersion struct { type UpdateTemplateMeta struct { Name string `json:"name,omitempty" validate:"omitempty,username"` Description string `json:"description,omitempty"` + Icon string `json:"icon,omitempty"` MaxTTLMillis int64 `json:"max_ttl_ms,omitempty"` MinAutostartIntervalMillis int64 `json:"min_autostart_interval_ms,omitempty"` } diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 929f6c00232d6..ccd4cc2a08c47 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -87,6 +87,7 @@ export interface CreateParameterRequest { export interface CreateTemplateRequest { readonly name: string readonly description?: string + readonly icon?: string readonly template_version_id: string readonly parameter_values?: CreateParameterRequest[] readonly max_ttl_ms?: number @@ -295,6 +296,7 @@ export interface Template { readonly active_version_id: string readonly workspace_owner_count: number readonly description: string + readonly icon: string readonly max_ttl_ms: number readonly min_autostart_interval_ms: number readonly created_by_id: string @@ -334,6 +336,7 @@ export interface UpdateRoles { export interface UpdateTemplateMeta { readonly name?: string readonly description?: string + readonly icon?: string readonly max_ttl_ms?: number readonly min_autostart_interval_ms?: number } diff --git a/site/src/pages/TemplateSettingsPage/TemplateSettingsPage.test.tsx b/site/src/pages/TemplateSettingsPage/TemplateSettingsPage.test.tsx index f3e7bdf81fd49..d28d5aa01bf7c 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSettingsPage.test.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSettingsPage.test.tsx @@ -54,6 +54,7 @@ describe("TemplateSettingsPage", () => { name: "edited-template-name", description: "Edited description", max_ttl_ms: 4000, + icon: "/icons/new-icon.png", } jest.spyOn(API, "updateTemplateMeta").mockResolvedValueOnce({ ...MockTemplate, diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index e544c2faa44c0..9f10dc7c9623b 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -155,6 +155,7 @@ export const MockTemplate: TypesGen.Template = { min_autostart_interval_ms: 60 * 60 * 1000, created_by_id: "test-creator-id", created_by_name: "test_creator", + icon: "/icon/code.svg", } export const MockWorkspaceAutostartDisabled: TypesGen.UpdateWorkspaceAutostartRequest = {
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: