From dbd5be76ac27462197a4c326d6ffbee46e1341c2 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Mon, 29 Jan 2024 14:24:10 +0000 Subject: [PATCH 1/9] feat(site): display user avatar --- coderd/apidoc/docs.go | 6 ++ coderd/apidoc/swagger.json | 6 ++ coderd/users.go | 6 +- coderd/workspacebuilds.go | 61 ++++++++++--------- coderd/workspaces.go | 31 ++++++---- codersdk/workspacebuilds.go | 41 +++++++------ codersdk/workspaces.go | 1 + docs/api/builds.md | 6 ++ docs/api/schemas.md | 51 +++++++++------- docs/api/workspaces.md | 10 +++ site/src/api/typesGenerated.ts | 2 + .../pages/WorkspacePage/WorkspaceTopbar.tsx | 10 +-- site/src/testHelpers/entities.ts | 5 ++ 13 files changed, 144 insertions(+), 92 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 098ea767e4ffe..1bf78ea05545b 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -12014,6 +12014,9 @@ const docTemplate = `{ "outdated": { "type": "boolean" }, + "owner_avatar_url": { + "type": "string" + }, "owner_id": { "type": "string", "format": "uuid" @@ -12591,6 +12594,9 @@ const docTemplate = `{ "workspace_name": { "type": "string" }, + "workspace_owner_avatar_url": { + "type": "string" + }, "workspace_owner_id": { "type": "string", "format": "uuid" diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 24bc5e29cc05c..5b8737ca89691 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -10894,6 +10894,9 @@ "outdated": { "type": "boolean" }, + "owner_avatar_url": { + "type": "string" + }, "owner_id": { "type": "string", "format": "uuid" @@ -11442,6 +11445,9 @@ "workspace_name": { "type": "string" }, + "workspace_owner_avatar_url": { + "type": "string" + }, "workspace_owner_id": { "type": "string", "format": "uuid" diff --git a/coderd/users.go b/coderd/users.go index 6cb8b03d37b50..43e00f811271b 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -1271,13 +1271,13 @@ func userOrganizationIDs(ctx context.Context, api *API, user database.User) ([]u return member.OrganizationIDs, nil } -func usernameWithID(id uuid.UUID, users []database.User) (string, bool) { +func userByID(id uuid.UUID, users []database.User) (database.User, bool) { for _, user := range users { if id == user.ID { - return user.Username, true + return user, true } } - return "", false + return database.User{}, false } func convertAPIKey(k database.APIKey) codersdk.APIKey { diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go index 45813b79eb63e..08ff7a0f56925 100644 --- a/coderd/workspacebuilds.go +++ b/coderd/workspacebuilds.go @@ -69,7 +69,7 @@ func (api *API) workspaceBuild(rw http.ResponseWriter, r *http.Request) { }) return } - ownerName, ok := usernameWithID(workspace.OwnerID, data.users) + owner, ok := userByID(workspace.OwnerID, data.users) if !ok { httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ Message: "Internal error converting workspace build.", @@ -82,7 +82,7 @@ func (api *API) workspaceBuild(rw http.ResponseWriter, r *http.Request) { workspaceBuild, workspace, data.jobs[0], - ownerName, + owner, data.resources, data.metadata, data.agents, @@ -283,7 +283,7 @@ func (api *API) workspaceBuildByBuildNumber(rw http.ResponseWriter, r *http.Requ }) return } - ownerName, ok := usernameWithID(workspace.OwnerID, data.users) + owner, ok := userByID(workspace.OwnerID, data.users) if !ok { httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ Message: "Internal error converting workspace build.", @@ -296,7 +296,7 @@ func (api *API) workspaceBuildByBuildNumber(rw http.ResponseWriter, r *http.Requ workspaceBuild, workspace, data.jobs[0], - ownerName, + owner, data.resources, data.metadata, data.agents, @@ -416,7 +416,7 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { }) return } - ownerName, exists := usernameWithID(workspace.OwnerID, users) + owner, exists := userByID(workspace.OwnerID, users) if !exists { httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ Message: "Internal error converting workspace build.", @@ -432,7 +432,7 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { ProvisionerJob: *provisionerJob, QueuePosition: 0, }, - ownerName, + owner, []database.WorkspaceResource{}, []database.WorkspaceResourceMetadatum{}, []database.WorkspaceAgent{}, @@ -833,7 +833,7 @@ func (api *API) convertWorkspaceBuilds( if !exists { return nil, xerrors.New("template version not found") } - ownerName, exists := usernameWithID(workspace.OwnerID, users) + owner, exists := userByID(workspace.OwnerID, users) if !exists { return nil, xerrors.Errorf("owner not found for workspace: %q", workspace.Name) } @@ -842,7 +842,7 @@ func (api *API) convertWorkspaceBuilds( build, workspace, job, - ownerName, + owner, workspaceResources, resourceMetadata, resourceAgents, @@ -865,7 +865,7 @@ func (api *API) convertWorkspaceBuild( build database.WorkspaceBuild, workspace database.Workspace, job database.GetProvisionerJobsByIDsWithQueuePositionRow, - ownerName string, + owner database.User, workspaceResources []database.WorkspaceResource, resourceMetadata []database.WorkspaceResourceMetadatum, resourceAgents []database.WorkspaceAgent, @@ -909,7 +909,7 @@ func (api *API) convertWorkspaceBuild( scripts := scriptsByAgentID[agent.ID] logSources := logSourcesByAgentID[agent.ID] apiAgent, err := db2sdk.WorkspaceAgent( - api.DERPMap(), *api.TailnetCoordinator.Load(), agent, db2sdk.Apps(apps, agent, ownerName, workspace), convertScripts(scripts), convertLogSources(logSources), api.AgentInactiveDisconnectTimeout, + api.DERPMap(), *api.TailnetCoordinator.Load(), agent, db2sdk.Apps(apps, agent, owner.Name, workspace), convertScripts(scripts), convertLogSources(logSources), api.AgentInactiveDisconnectTimeout, api.DeploymentValues.AgentFallbackTroubleshootingURL.String(), ) if err != nil { @@ -923,26 +923,27 @@ func (api *API) convertWorkspaceBuild( apiJob := convertProvisionerJob(job) transition := codersdk.WorkspaceTransition(build.Transition) return codersdk.WorkspaceBuild{ - ID: build.ID, - CreatedAt: build.CreatedAt, - UpdatedAt: build.UpdatedAt, - WorkspaceOwnerID: workspace.OwnerID, - WorkspaceOwnerName: ownerName, - WorkspaceID: build.WorkspaceID, - WorkspaceName: workspace.Name, - TemplateVersionID: build.TemplateVersionID, - TemplateVersionName: templateVersion.Name, - BuildNumber: build.BuildNumber, - Transition: transition, - InitiatorID: build.InitiatorID, - InitiatorUsername: build.InitiatorByUsername, - Job: apiJob, - Deadline: codersdk.NewNullTime(build.Deadline, !build.Deadline.IsZero()), - MaxDeadline: codersdk.NewNullTime(build.MaxDeadline, !build.MaxDeadline.IsZero()), - Reason: codersdk.BuildReason(build.Reason), - Resources: apiResources, - Status: convertWorkspaceStatus(apiJob.Status, transition), - DailyCost: build.DailyCost, + ID: build.ID, + CreatedAt: build.CreatedAt, + UpdatedAt: build.UpdatedAt, + WorkspaceOwnerID: workspace.OwnerID, + WorkspaceOwnerName: owner.Name, + WorkspaceOwnerAvatarURL: owner.AvatarURL, + WorkspaceID: build.WorkspaceID, + WorkspaceName: workspace.Name, + TemplateVersionID: build.TemplateVersionID, + TemplateVersionName: templateVersion.Name, + BuildNumber: build.BuildNumber, + Transition: transition, + InitiatorID: build.InitiatorID, + InitiatorUsername: build.InitiatorByUsername, + Job: apiJob, + Deadline: codersdk.NewNullTime(build.Deadline, !build.Deadline.IsZero()), + MaxDeadline: codersdk.NewNullTime(build.MaxDeadline, !build.MaxDeadline.IsZero()), + Reason: codersdk.BuildReason(build.Reason), + Resources: apiResources, + Status: convertWorkspaceStatus(apiJob.Status, transition), + DailyCost: build.DailyCost, }, nil } diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 3f8494b4a481a..a57c45b48597b 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -94,7 +94,7 @@ func (api *API) workspace(rw http.ResponseWriter, r *http.Request) { httpapi.Forbidden(rw) return } - ownerName, ok := usernameWithID(workspace.OwnerID, data.users) + owner, ok := userByID(workspace.OwnerID, data.users) if !ok { httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ Message: "Internal error fetching workspace resources.", @@ -108,7 +108,7 @@ func (api *API) workspace(rw http.ResponseWriter, r *http.Request) { workspace, data.builds[0], data.templates[0], - ownerName, + owner, api.Options.AllowWorkspaceRenames, ) if err != nil { @@ -281,7 +281,7 @@ func (api *API) workspaceByOwnerAndName(rw http.ResponseWriter, r *http.Request) httpapi.ResourceNotFound(rw) return } - ownerName, ok := usernameWithID(workspace.OwnerID, data.users) + owner, ok := userByID(workspace.OwnerID, data.users) if !ok { httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ Message: "Internal error fetching workspace resources.", @@ -294,7 +294,7 @@ func (api *API) workspaceByOwnerAndName(rw http.ResponseWriter, r *http.Request) workspace, data.builds[0], data.templates[0], - ownerName, + owner, api.Options.AllowWorkspaceRenames, ) if err != nil { @@ -590,7 +590,9 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req ProvisionerJob: *provisionerJob, QueuePosition: 0, }, - member.Username, + database.User{ + Name: member.Username, + }, []database.WorkspaceResource{}, []database.WorkspaceResourceMetadatum{}, []database.WorkspaceAgent{}, @@ -612,7 +614,9 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req workspace, apiBuild, template, - member.Username, + database.User{ + Name: member.Username, + }, api.Options.AllowWorkspaceRenames, ) if err != nil { @@ -941,7 +945,7 @@ func (api *API) putWorkspaceDormant(rw http.ResponseWriter, r *http.Request) { }) return } - ownerName, ok := usernameWithID(workspace.OwnerID, data.users) + owner, ok := userByID(workspace.OwnerID, data.users) if !ok { httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ Message: "Internal error fetching workspace resources.", @@ -962,7 +966,7 @@ func (api *API) putWorkspaceDormant(rw http.ResponseWriter, r *http.Request) { workspace, data.builds[0], data.templates[0], - ownerName, + owner, api.Options.AllowWorkspaceRenames, ) if err != nil { @@ -1372,7 +1376,7 @@ func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) { return } - ownerName, ok := usernameWithID(workspace.OwnerID, data.users) + owner, ok := userByID(workspace.OwnerID, data.users) if !ok { _ = sendEvent(ctx, codersdk.ServerSentEvent{ Type: codersdk.ServerSentEventTypeError, @@ -1389,7 +1393,7 @@ func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) { workspace, data.builds[0], data.templates[0], - ownerName, + owner, api.Options.AllowWorkspaceRenames, ) if err != nil { @@ -1555,7 +1559,7 @@ func convertWorkspaces(requesterID uuid.UUID, workspaces []database.Workspace, d workspace, build, template, - owner.Username, + owner, data.allowRenames, ) if err != nil { @@ -1572,7 +1576,7 @@ func convertWorkspace( workspace database.Workspace, workspaceBuild codersdk.WorkspaceBuild, template database.Template, - ownerName string, + owner database.User, allowRenames bool, ) (codersdk.Workspace, error) { if requesterID == uuid.Nil { @@ -1612,7 +1616,8 @@ func convertWorkspace( CreatedAt: workspace.CreatedAt, UpdatedAt: workspace.UpdatedAt, OwnerID: workspace.OwnerID, - OwnerName: ownerName, + OwnerName: owner.Name, + OwnerAvatarURL: owner.AvatarURL, OrganizationID: workspace.OrganizationID, TemplateID: workspace.TemplateID, LatestBuild: workspaceBuild, diff --git a/codersdk/workspacebuilds.go b/codersdk/workspacebuilds.go index c7bdf022d238f..682cb424af1b1 100644 --- a/codersdk/workspacebuilds.go +++ b/codersdk/workspacebuilds.go @@ -51,26 +51,27 @@ const ( // WorkspaceBuild is an at-point representation of a workspace state. // BuildNumbers start at 1 and increase by 1 for each subsequent build type WorkspaceBuild struct { - ID uuid.UUID `json:"id" format:"uuid"` - CreatedAt time.Time `json:"created_at" format:"date-time"` - UpdatedAt time.Time `json:"updated_at" format:"date-time"` - WorkspaceID uuid.UUID `json:"workspace_id" format:"uuid"` - WorkspaceName string `json:"workspace_name"` - WorkspaceOwnerID uuid.UUID `json:"workspace_owner_id" format:"uuid"` - WorkspaceOwnerName string `json:"workspace_owner_name"` - TemplateVersionID uuid.UUID `json:"template_version_id" format:"uuid"` - TemplateVersionName string `json:"template_version_name"` - BuildNumber int32 `json:"build_number"` - Transition WorkspaceTransition `json:"transition" enums:"start,stop,delete"` - InitiatorID uuid.UUID `json:"initiator_id" format:"uuid"` - InitiatorUsername string `json:"initiator_name"` - Job ProvisionerJob `json:"job"` - Reason BuildReason `db:"reason" json:"reason" enums:"initiator,autostart,autostop"` - Resources []WorkspaceResource `json:"resources"` - Deadline NullTime `json:"deadline,omitempty" format:"date-time"` - MaxDeadline NullTime `json:"max_deadline,omitempty" format:"date-time"` - Status WorkspaceStatus `json:"status" enums:"pending,starting,running,stopping,stopped,failed,canceling,canceled,deleting,deleted"` - DailyCost int32 `json:"daily_cost"` + ID uuid.UUID `json:"id" format:"uuid"` + CreatedAt time.Time `json:"created_at" format:"date-time"` + UpdatedAt time.Time `json:"updated_at" format:"date-time"` + WorkspaceID uuid.UUID `json:"workspace_id" format:"uuid"` + WorkspaceName string `json:"workspace_name"` + WorkspaceOwnerID uuid.UUID `json:"workspace_owner_id" format:"uuid"` + WorkspaceOwnerName string `json:"workspace_owner_name"` + WorkspaceOwnerAvatarURL string `json:"workspace_owner_avatar_url"` + TemplateVersionID uuid.UUID `json:"template_version_id" format:"uuid"` + TemplateVersionName string `json:"template_version_name"` + BuildNumber int32 `json:"build_number"` + Transition WorkspaceTransition `json:"transition" enums:"start,stop,delete"` + InitiatorID uuid.UUID `json:"initiator_id" format:"uuid"` + InitiatorUsername string `json:"initiator_name"` + Job ProvisionerJob `json:"job"` + Reason BuildReason `db:"reason" json:"reason" enums:"initiator,autostart,autostop"` + Resources []WorkspaceResource `json:"resources"` + Deadline NullTime `json:"deadline,omitempty" format:"date-time"` + MaxDeadline NullTime `json:"max_deadline,omitempty" format:"date-time"` + Status WorkspaceStatus `json:"status" enums:"pending,starting,running,stopping,stopped,failed,canceling,canceled,deleting,deleted"` + DailyCost int32 `json:"daily_cost"` } // WorkspaceResource describes resources used to create a workspace, for instance: diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index 5cdd8cd1c6885..d5008b3234fa9 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -29,6 +29,7 @@ type Workspace struct { UpdatedAt time.Time `json:"updated_at" format:"date-time"` OwnerID uuid.UUID `json:"owner_id" format:"uuid"` OwnerName string `json:"owner_name"` + OwnerAvatarURL string `json:"owner_avatar_url"` OrganizationID uuid.UUID `json:"organization_id" format:"uuid"` TemplateID uuid.UUID `json:"template_id" format:"uuid"` TemplateName string `json:"template_name"` diff --git a/docs/api/builds.md b/docs/api/builds.md index 50072c8aa0d46..3b7a7080033ae 100644 --- a/docs/api/builds.md +++ b/docs/api/builds.md @@ -170,6 +170,7 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/workspace/{workspacenam "updated_at": "2019-08-24T14:15:22Z", "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", "workspace_name": "string", + "workspace_owner_avatar_url": "string", "workspace_owner_id": "e7078695-5279-4c86-8774-3ac2367a2fc7", "workspace_owner_name": "string" } @@ -351,6 +352,7 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild} \ "updated_at": "2019-08-24T14:15:22Z", "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", "workspace_name": "string", + "workspace_owner_avatar_url": "string", "workspace_owner_id": "e7078695-5279-4c86-8774-3ac2367a2fc7", "workspace_owner_name": "string" } @@ -960,6 +962,7 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild}/sta "updated_at": "2019-08-24T14:15:22Z", "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", "workspace_name": "string", + "workspace_owner_avatar_url": "string", "workspace_owner_id": "e7078695-5279-4c86-8774-3ac2367a2fc7", "workspace_owner_name": "string" } @@ -1146,6 +1149,7 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace}/builds \ "updated_at": "2019-08-24T14:15:22Z", "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", "workspace_name": "string", + "workspace_owner_avatar_url": "string", "workspace_owner_id": "e7078695-5279-4c86-8774-3ac2367a2fc7", "workspace_owner_name": "string" } @@ -1277,6 +1281,7 @@ Status Code **200** | `» updated_at` | string(date-time) | false | | | | `» workspace_id` | string(uuid) | false | | | | `» workspace_name` | string | false | | | +| `» workspace_owner_avatar_url` | string | false | | | | `» workspace_owner_id` | string(uuid) | false | | | | `» workspace_owner_name` | string | false | | | @@ -1524,6 +1529,7 @@ curl -X POST http://coder-server:8080/api/v2/workspaces/{workspace}/builds \ "updated_at": "2019-08-24T14:15:22Z", "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", "workspace_name": "string", + "workspace_owner_avatar_url": "string", "workspace_owner_id": "e7078695-5279-4c86-8774-3ac2367a2fc7", "workspace_owner_name": "string" } diff --git a/docs/api/schemas.md b/docs/api/schemas.md index 8114d0750b65e..09a07ff65d9b9 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -6072,12 +6072,14 @@ If the schedule is empty, the user will be updated to use the default schedule.| "updated_at": "2019-08-24T14:15:22Z", "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", "workspace_name": "string", + "workspace_owner_avatar_url": "string", "workspace_owner_id": "e7078695-5279-4c86-8774-3ac2367a2fc7", "workspace_owner_name": "string" }, "name": "string", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "outdated": true, + "owner_avatar_url": "string", "owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05", "owner_name": "string", "template_active_version_id": "b0da9c29-67d8-4c87-888c-bafe356f7f3c", @@ -6110,6 +6112,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| | `name` | string | false | | | | `organization_id` | string | false | | | | `outdated` | boolean | false | | | +| `owner_avatar_url` | string | false | | | | `owner_id` | string | false | | | | `owner_name` | string | false | | | | `template_active_version_id` | string | false | | | @@ -6766,6 +6769,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| "updated_at": "2019-08-24T14:15:22Z", "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", "workspace_name": "string", + "workspace_owner_avatar_url": "string", "workspace_owner_id": "e7078695-5279-4c86-8774-3ac2367a2fc7", "workspace_owner_name": "string" } @@ -6773,28 +6777,29 @@ If the schedule is empty, the user will be updated to use the default schedule.| ### Properties -| Name | Type | Required | Restrictions | Description | -| ----------------------- | ----------------------------------------------------------------- | -------- | ------------ | ----------- | -| `build_number` | integer | false | | | -| `created_at` | string | false | | | -| `daily_cost` | integer | false | | | -| `deadline` | string | false | | | -| `id` | string | false | | | -| `initiator_id` | string | false | | | -| `initiator_name` | string | false | | | -| `job` | [codersdk.ProvisionerJob](#codersdkprovisionerjob) | false | | | -| `max_deadline` | string | false | | | -| `reason` | [codersdk.BuildReason](#codersdkbuildreason) | false | | | -| `resources` | array of [codersdk.WorkspaceResource](#codersdkworkspaceresource) | false | | | -| `status` | [codersdk.WorkspaceStatus](#codersdkworkspacestatus) | false | | | -| `template_version_id` | string | false | | | -| `template_version_name` | string | false | | | -| `transition` | [codersdk.WorkspaceTransition](#codersdkworkspacetransition) | false | | | -| `updated_at` | string | false | | | -| `workspace_id` | string | false | | | -| `workspace_name` | string | false | | | -| `workspace_owner_id` | string | false | | | -| `workspace_owner_name` | string | false | | | +| Name | Type | Required | Restrictions | Description | +| ---------------------------- | ----------------------------------------------------------------- | -------- | ------------ | ----------- | +| `build_number` | integer | false | | | +| `created_at` | string | false | | | +| `daily_cost` | integer | false | | | +| `deadline` | string | false | | | +| `id` | string | false | | | +| `initiator_id` | string | false | | | +| `initiator_name` | string | false | | | +| `job` | [codersdk.ProvisionerJob](#codersdkprovisionerjob) | false | | | +| `max_deadline` | string | false | | | +| `reason` | [codersdk.BuildReason](#codersdkbuildreason) | false | | | +| `resources` | array of [codersdk.WorkspaceResource](#codersdkworkspaceresource) | false | | | +| `status` | [codersdk.WorkspaceStatus](#codersdkworkspacestatus) | false | | | +| `template_version_id` | string | false | | | +| `template_version_name` | string | false | | | +| `transition` | [codersdk.WorkspaceTransition](#codersdkworkspacetransition) | false | | | +| `updated_at` | string | false | | | +| `workspace_id` | string | false | | | +| `workspace_name` | string | false | | | +| `workspace_owner_avatar_url` | string | false | | | +| `workspace_owner_id` | string | false | | | +| `workspace_owner_name` | string | false | | | #### Enumerated Values @@ -7333,12 +7338,14 @@ If the schedule is empty, the user will be updated to use the default schedule.| "updated_at": "2019-08-24T14:15:22Z", "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", "workspace_name": "string", + "workspace_owner_avatar_url": "string", "workspace_owner_id": "e7078695-5279-4c86-8774-3ac2367a2fc7", "workspace_owner_name": "string" }, "name": "string", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "outdated": true, + "owner_avatar_url": "string", "owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05", "owner_name": "string", "template_active_version_id": "b0da9c29-67d8-4c87-888c-bafe356f7f3c", diff --git a/docs/api/workspaces.md b/docs/api/workspaces.md index f4c1b6957f527..f176653a171dd 100644 --- a/docs/api/workspaces.md +++ b/docs/api/workspaces.md @@ -204,12 +204,14 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/member "updated_at": "2019-08-24T14:15:22Z", "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", "workspace_name": "string", + "workspace_owner_avatar_url": "string", "workspace_owner_id": "e7078695-5279-4c86-8774-3ac2367a2fc7", "workspace_owner_name": "string" }, "name": "string", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "outdated": true, + "owner_avatar_url": "string", "owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05", "owner_name": "string", "template_active_version_id": "b0da9c29-67d8-4c87-888c-bafe356f7f3c", @@ -416,12 +418,14 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/workspace/{workspacenam "updated_at": "2019-08-24T14:15:22Z", "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", "workspace_name": "string", + "workspace_owner_avatar_url": "string", "workspace_owner_id": "e7078695-5279-4c86-8774-3ac2367a2fc7", "workspace_owner_name": "string" }, "name": "string", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "outdated": true, + "owner_avatar_url": "string", "owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05", "owner_name": "string", "template_active_version_id": "b0da9c29-67d8-4c87-888c-bafe356f7f3c", @@ -627,12 +631,14 @@ curl -X GET http://coder-server:8080/api/v2/workspaces \ "updated_at": "2019-08-24T14:15:22Z", "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", "workspace_name": "string", + "workspace_owner_avatar_url": "string", "workspace_owner_id": "e7078695-5279-4c86-8774-3ac2367a2fc7", "workspace_owner_name": "string" }, "name": "string", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "outdated": true, + "owner_avatar_url": "string", "owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05", "owner_name": "string", "template_active_version_id": "b0da9c29-67d8-4c87-888c-bafe356f7f3c", @@ -840,12 +846,14 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace} \ "updated_at": "2019-08-24T14:15:22Z", "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", "workspace_name": "string", + "workspace_owner_avatar_url": "string", "workspace_owner_id": "e7078695-5279-4c86-8774-3ac2367a2fc7", "workspace_owner_name": "string" }, "name": "string", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "outdated": true, + "owner_avatar_url": "string", "owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05", "owner_name": "string", "template_active_version_id": "b0da9c29-67d8-4c87-888c-bafe356f7f3c", @@ -1168,12 +1176,14 @@ curl -X PUT http://coder-server:8080/api/v2/workspaces/{workspace}/dormant \ "updated_at": "2019-08-24T14:15:22Z", "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", "workspace_name": "string", + "workspace_owner_avatar_url": "string", "workspace_owner_id": "e7078695-5279-4c86-8774-3ac2367a2fc7", "workspace_owner_name": "string" }, "name": "string", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "outdated": true, + "owner_avatar_url": "string", "owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05", "owner_name": "string", "template_active_version_id": "b0da9c29-67d8-4c87-888c-bafe356f7f3c", diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index eeab0f373bba6..7332cd0a95764 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1471,6 +1471,7 @@ export interface Workspace { readonly updated_at: string; readonly owner_id: string; readonly owner_name: string; + readonly owner_avatar_url: string; readonly organization_id: string; readonly template_id: string; readonly template_name: string; @@ -1625,6 +1626,7 @@ export interface WorkspaceBuild { readonly workspace_name: string; readonly workspace_owner_id: string; readonly workspace_owner_name: string; + readonly workspace_owner_avatar_url: string; readonly template_version_id: string; readonly template_version_name: string; readonly build_number: number; diff --git a/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx b/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx index 88e2634ef4418..1f5ebc26e37ec 100644 --- a/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx @@ -2,7 +2,6 @@ import Tooltip from "@mui/material/Tooltip"; import Link from "@mui/material/Link"; import MonetizationOnOutlined from "@mui/icons-material/MonetizationOnOutlined"; import DeleteOutline from "@mui/icons-material/DeleteOutline"; -import PersonOutline from "@mui/icons-material/PersonOutline"; import ArrowBackOutlined from "@mui/icons-material/ArrowBackOutlined"; import ScheduleOutlined from "@mui/icons-material/ScheduleOutlined"; import { useTheme } from "@emotion/react"; @@ -33,6 +32,7 @@ import { ExternalAvatar } from "components/Avatar/Avatar"; import { WorkspaceActions } from "./WorkspaceActions/WorkspaceActions"; import { WorkspaceNotifications } from "./WorkspaceNotifications/WorkspaceNotifications"; import { WorkspacePermissions } from "./permissions"; +import { UserAvatar } from "components/UserAvatar/UserAvatar"; export type WorkspaceError = | "getBuildsError" @@ -128,9 +128,11 @@ export const WorkspaceTopbar: FC = ({ }} > - - - + {workspace.owner_name} diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index b3ba287be5060..64d43881d2eb1 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -897,6 +897,7 @@ export const MockWorkspaceBuild: TypesGen.WorkspaceBuild = { workspace_name: "test-workspace", workspace_owner_id: MockUser.id, workspace_owner_name: MockUser.username, + workspace_owner_avatar_url: MockUser.avatar_url, workspace_id: "759f1d46-3174-453d-aa60-980a9c1442f3", deadline: "2022-05-17T23:39:00.00Z", reason: "initiator", @@ -919,6 +920,7 @@ export const MockWorkspaceBuildAutostart: TypesGen.WorkspaceBuild = { workspace_name: "test-workspace", workspace_owner_id: MockUser.id, workspace_owner_name: MockUser.username, + workspace_owner_avatar_url: MockUser.avatar_url, workspace_id: "759f1d46-3174-453d-aa60-980a9c1442f3", deadline: "2022-05-17T23:39:00.00Z", reason: "autostart", @@ -941,6 +943,7 @@ export const MockWorkspaceBuildAutostop: TypesGen.WorkspaceBuild = { workspace_name: "test-workspace", workspace_owner_id: MockUser.id, workspace_owner_name: MockUser.username, + workspace_owner_avatar_url: MockUser.avatar_url, workspace_id: "759f1d46-3174-453d-aa60-980a9c1442f3", deadline: "2022-05-17T23:39:00.00Z", reason: "autostop", @@ -965,6 +968,7 @@ export const MockFailedWorkspaceBuild = ( workspace_name: "test-workspace", workspace_owner_id: MockUser.id, workspace_owner_name: MockUser.username, + workspace_owner_avatar_url: MockUser.avatar_url, workspace_id: "759f1d46-3174-453d-aa60-980a9c1442f3", deadline: "2022-05-17T23:39:00.00Z", reason: "initiator", @@ -1010,6 +1014,7 @@ export const MockWorkspace: TypesGen.Workspace = { owner_id: MockUser.id, organization_id: MockOrganization.id, owner_name: MockUser.username, + owner_avatar_url: "", autostart_schedule: MockWorkspaceAutostartEnabled.schedule, ttl_ms: 2 * 60 * 60 * 1000, latest_build: MockWorkspaceBuild, From 47cc1c73c4ace11864878bfb4deabfb98cf1daeb Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Mon, 29 Jan 2024 17:41:45 +0000 Subject: [PATCH 2/9] Fix tests --- coderd/workspaces.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/coderd/workspaces.go b/coderd/workspaces.go index a57c45b48597b..be5267c0864a6 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -591,7 +591,7 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req QueuePosition: 0, }, database.User{ - Name: member.Username, + Username: member.Username, }, []database.WorkspaceResource{}, []database.WorkspaceResourceMetadatum{}, @@ -615,7 +615,7 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req apiBuild, template, database.User{ - Name: member.Username, + Username: member.Username, }, api.Options.AllowWorkspaceRenames, ) From ef9d63b63ffa81ccada6d573130c032b3104728d Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Tue, 30 Jan 2024 10:18:27 +0000 Subject: [PATCH 3/9] avoid initializing database.User struct with zero-valued fields --- coderd/workspaces.go | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/coderd/workspaces.go b/coderd/workspaces.go index be5267c0864a6..6bb576e625117 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -354,6 +354,15 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req return } + wsOwner, err := api.Database.GetUserByID(ctx, member.UserID) + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Requested workspace owner %q does not exist.", + Detail: err.Error(), + }) + return + } + var createWorkspace codersdk.CreateWorkspaceRequest if !httpapi.Read(ctx, rw, r, &createWorkspace) { return @@ -522,7 +531,7 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req ID: uuid.New(), CreatedAt: now, UpdatedAt: now, - OwnerID: member.UserID, + OwnerID: wsOwner.ID, OrganizationID: template.OrganizationID, TemplateID: template.ID, Name: createWorkspace.Name, @@ -590,9 +599,7 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req ProvisionerJob: *provisionerJob, QueuePosition: 0, }, - database.User{ - Username: member.Username, - }, + wsOwner, []database.WorkspaceResource{}, []database.WorkspaceResourceMetadatum{}, []database.WorkspaceAgent{}, @@ -614,9 +621,7 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req workspace, apiBuild, template, - database.User{ - Username: member.Username, - }, + wsOwner, api.Options.AllowWorkspaceRenames, ) if err != nil { From 194628d179fad8274ac3746e4258701c89a579c6 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Tue, 30 Jan 2024 10:18:59 +0000 Subject: [PATCH 4/9] correct references to Username instead of Name --- coderd/workspacebuilds.go | 4 ++-- coderd/workspaces.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go index 08ff7a0f56925..560a617caec24 100644 --- a/coderd/workspacebuilds.go +++ b/coderd/workspacebuilds.go @@ -909,7 +909,7 @@ func (api *API) convertWorkspaceBuild( scripts := scriptsByAgentID[agent.ID] logSources := logSourcesByAgentID[agent.ID] apiAgent, err := db2sdk.WorkspaceAgent( - api.DERPMap(), *api.TailnetCoordinator.Load(), agent, db2sdk.Apps(apps, agent, owner.Name, workspace), convertScripts(scripts), convertLogSources(logSources), api.AgentInactiveDisconnectTimeout, + api.DERPMap(), *api.TailnetCoordinator.Load(), agent, db2sdk.Apps(apps, agent, owner.Username, workspace), convertScripts(scripts), convertLogSources(logSources), api.AgentInactiveDisconnectTimeout, api.DeploymentValues.AgentFallbackTroubleshootingURL.String(), ) if err != nil { @@ -927,7 +927,7 @@ func (api *API) convertWorkspaceBuild( CreatedAt: build.CreatedAt, UpdatedAt: build.UpdatedAt, WorkspaceOwnerID: workspace.OwnerID, - WorkspaceOwnerName: owner.Name, + WorkspaceOwnerName: owner.Username, WorkspaceOwnerAvatarURL: owner.AvatarURL, WorkspaceID: build.WorkspaceID, WorkspaceName: workspace.Name, diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 6bb576e625117..733fba8297015 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -1621,7 +1621,7 @@ func convertWorkspace( CreatedAt: workspace.CreatedAt, UpdatedAt: workspace.UpdatedAt, OwnerID: workspace.OwnerID, - OwnerName: owner.Name, + OwnerName: owner.Username, OwnerAvatarURL: owner.AvatarURL, OrganizationID: workspace.OrganizationID, TemplateID: workspace.TemplateID, From aef1d44c99e83d51e91fac3edc3cd2582f2ef75b Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Tue, 30 Jan 2024 10:19:06 +0000 Subject: [PATCH 5/9] update golden files --- cli/testdata/coder_list_--output_json.golden | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cli/testdata/coder_list_--output_json.golden b/cli/testdata/coder_list_--output_json.golden index 319ac80c1554c..903e5681c2689 100644 --- a/cli/testdata/coder_list_--output_json.golden +++ b/cli/testdata/coder_list_--output_json.golden @@ -5,6 +5,7 @@ "updated_at": "[timestamp]", "owner_id": "[first user ID]", "owner_name": "testuser", + "owner_avatar_url": "", "organization_id": "[first org ID]", "template_id": "[template ID]", "template_name": "test-template", @@ -21,6 +22,7 @@ "workspace_name": "test-workspace", "workspace_owner_id": "[first user ID]", "workspace_owner_name": "testuser", + "workspace_owner_avatar_url": "", "template_version_id": "[version ID]", "template_version_name": "[version name]", "build_number": 1, From e94a0e16c82cb1016c0708ad6981924b4a449c23 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Tue, 30 Jan 2024 14:42:41 +0000 Subject: [PATCH 6/9] add avatar URL to org member httpmw --- coderd/httpmw/organizationparam.go | 24 ++++++++++-------- coderd/httpmw/organizationparam_test.go | 33 +++++++++++++++++++++++-- 2 files changed, 45 insertions(+), 12 deletions(-) diff --git a/coderd/httpmw/organizationparam.go b/coderd/httpmw/organizationparam.go index 76b085f019aee..0637fba3dc04b 100644 --- a/coderd/httpmw/organizationparam.go +++ b/coderd/httpmw/organizationparam.go @@ -63,12 +63,13 @@ func ExtractOrganizationParam(db database.Store) func(http.Handler) http.Handler } } -// OrganizationMember is the database object plus the Username. Including the Username in this -// middleware is preferable to a join at the SQL layer so that we can keep the autogenerated -// database types as they are. +// OrganizationMember is the database object plus the Username and Avatar URL. Including these +// in the middleware is preferable to a join at the SQL layer so that we can keep the +// autogenerated database types as they are. type OrganizationMember struct { database.OrganizationMember - Username string + Username string + AvatarURL string } // ExtractOrganizationMemberParam grabs a user membership from the "organization" and "user" URL parameter. @@ -107,14 +108,17 @@ func ExtractOrganizationMemberParam(db database.Store) func(http.Handler) http.H ctx = context.WithValue(ctx, organizationMemberParamContextKey{}, OrganizationMember{ OrganizationMember: organizationMember, - // Here we're making one exception to the rule about not leaking data about the user - // to the API handler, which is to include the username. If the caller has permission - // to read the OrganizationMember, then we're explicitly saying here that they also - // have permission to see the member's username, which is itself uncontroversial. + // Here we're making two exceptions to the rule about not leaking data about the user + // to the API handler, which is to include the username and avatar URL. + // If the caller has permission to read the OrganizationMember, then we're explicitly + // saying here that they also have permission to see the member's username and avatar. + // This is OK! // // API handlers need this information for audit logging and returning the owner's - // username in response to creating a workspace. - Username: user.Username, + // username in response to creating a workspace. Additionally, the frontend consumes + // the Avatar URL and this allows the FE to avoid an extra request. + Username: user.Username, + AvatarURL: user.AvatarURL, }) next.ServeHTTP(rw, r.WithContext(ctx)) }) diff --git a/coderd/httpmw/organizationparam_test.go b/coderd/httpmw/organizationparam_test.go index d492353e6815d..d9cf0c79130c8 100644 --- a/coderd/httpmw/organizationparam_test.go +++ b/coderd/httpmw/organizationparam_test.go @@ -8,6 +8,7 @@ import ( "github.com/go-chi/chi/v5" "github.com/google/uuid" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/coder/coder/v2/coderd/database" @@ -15,7 +16,9 @@ import ( "github.com/coder/coder/v2/coderd/database/dbmem" "github.com/coder/coder/v2/coderd/database/dbtime" "github.com/coder/coder/v2/coderd/httpmw" + "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/codersdk" + "github.com/coder/coder/v2/testutil" ) func TestOrganizationParam(t *testing.T) { @@ -139,6 +142,7 @@ func TestOrganizationParam(t *testing.T) { t.Run("Success", func(t *testing.T) { t.Parallel() var ( + ctx = testutil.Context(t, testutil.WaitShort) db = dbmem.New() rw = httptest.NewRecorder() r, user = setupAuthentication(db) @@ -148,7 +152,14 @@ func TestOrganizationParam(t *testing.T) { _ = dbgen.OrganizationMember(t, db, database.OrganizationMember{ OrganizationID: organization.ID, UserID: user.ID, + Roles: []string{rbac.RoleOrgMember(organization.ID)}, }) + _, err := db.UpdateUserRoles(ctx, database.UpdateUserRolesParams{ + ID: user.ID, + GrantedRoles: []string{rbac.RoleTemplateAdmin()}, + }) + require.NoError(t, err) + chi.RouteContext(r.Context()).URLParams.Add("organization", organization.ID.String()) chi.RouteContext(r.Context()).URLParams.Add("user", user.ID.String()) rtr.Use( @@ -161,9 +172,27 @@ func TestOrganizationParam(t *testing.T) { httpmw.ExtractOrganizationMemberParam(db), ) rtr.Get("/", func(rw http.ResponseWriter, r *http.Request) { - _ = httpmw.OrganizationParam(r) - _ = httpmw.OrganizationMemberParam(r) + org := httpmw.OrganizationParam(r) + assert.NotZero(t, org) + assert.NotZero(t, org.CreatedAt) + // assert.NotZero(t, org.Description) // not supported + assert.NotZero(t, org.ID) + assert.NotEmpty(t, org.Name) + orgMem := httpmw.OrganizationMemberParam(r) rw.WriteHeader(http.StatusOK) + assert.NotZero(t, orgMem) + assert.NotZero(t, orgMem.CreatedAt) + assert.NotZero(t, orgMem.UpdatedAt) + assert.Equal(t, org.ID, orgMem.OrganizationID) + assert.Equal(t, user.ID, orgMem.UserID) + assert.Equal(t, user.Username, orgMem.Username) + assert.Equal(t, user.AvatarURL, orgMem.AvatarURL) + assert.NotEmpty(t, orgMem.Roles) + assert.NotZero(t, orgMem.OrganizationMember) + assert.NotEmpty(t, orgMem.OrganizationMember.CreatedAt) + assert.NotEmpty(t, orgMem.OrganizationMember.UpdatedAt) + assert.NotEmpty(t, orgMem.OrganizationMember.UserID) + assert.NotEmpty(t, orgMem.OrganizationMember.Roles) }) rtr.ServeHTTP(rw, r) res := rw.Result() From b424e482e176a7686ef4f7de69b26511114d6859 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Tue, 30 Jan 2024 14:44:25 +0000 Subject: [PATCH 7/9] use avatar URL from org member httpmw --- coderd/workspacebuilds.go | 20 ++++++++++------- coderd/workspacebuilds_test.go | 3 +++ coderd/workspaces.go | 39 +++++++++++++++++----------------- 3 files changed, 34 insertions(+), 28 deletions(-) diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go index 560a617caec24..9704fa156a939 100644 --- a/coderd/workspacebuilds.go +++ b/coderd/workspacebuilds.go @@ -82,7 +82,8 @@ func (api *API) workspaceBuild(rw http.ResponseWriter, r *http.Request) { workspaceBuild, workspace, data.jobs[0], - owner, + owner.Username, + owner.AvatarURL, data.resources, data.metadata, data.agents, @@ -296,7 +297,8 @@ func (api *API) workspaceBuildByBuildNumber(rw http.ResponseWriter, r *http.Requ workspaceBuild, workspace, data.jobs[0], - owner, + owner.Username, + owner.AvatarURL, data.resources, data.metadata, data.agents, @@ -432,7 +434,8 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { ProvisionerJob: *provisionerJob, QueuePosition: 0, }, - owner, + owner.Username, + owner.AvatarURL, []database.WorkspaceResource{}, []database.WorkspaceResourceMetadatum{}, []database.WorkspaceAgent{}, @@ -842,7 +845,8 @@ func (api *API) convertWorkspaceBuilds( build, workspace, job, - owner, + owner.Username, + owner.AvatarURL, workspaceResources, resourceMetadata, resourceAgents, @@ -865,7 +869,7 @@ func (api *API) convertWorkspaceBuild( build database.WorkspaceBuild, workspace database.Workspace, job database.GetProvisionerJobsByIDsWithQueuePositionRow, - owner database.User, + username, avatarURL string, workspaceResources []database.WorkspaceResource, resourceMetadata []database.WorkspaceResourceMetadatum, resourceAgents []database.WorkspaceAgent, @@ -909,7 +913,7 @@ func (api *API) convertWorkspaceBuild( scripts := scriptsByAgentID[agent.ID] logSources := logSourcesByAgentID[agent.ID] apiAgent, err := db2sdk.WorkspaceAgent( - api.DERPMap(), *api.TailnetCoordinator.Load(), agent, db2sdk.Apps(apps, agent, owner.Username, workspace), convertScripts(scripts), convertLogSources(logSources), api.AgentInactiveDisconnectTimeout, + api.DERPMap(), *api.TailnetCoordinator.Load(), agent, db2sdk.Apps(apps, agent, username, workspace), convertScripts(scripts), convertLogSources(logSources), api.AgentInactiveDisconnectTimeout, api.DeploymentValues.AgentFallbackTroubleshootingURL.String(), ) if err != nil { @@ -927,8 +931,8 @@ func (api *API) convertWorkspaceBuild( CreatedAt: build.CreatedAt, UpdatedAt: build.UpdatedAt, WorkspaceOwnerID: workspace.OwnerID, - WorkspaceOwnerName: owner.Username, - WorkspaceOwnerAvatarURL: owner.AvatarURL, + WorkspaceOwnerName: username, + WorkspaceOwnerAvatarURL: avatarURL, WorkspaceID: build.WorkspaceID, WorkspaceName: workspace.Name, TemplateVersionID: build.TemplateVersionID, diff --git a/coderd/workspacebuilds_test.go b/coderd/workspacebuilds_test.go index 977c073652c0a..13e4ee2c49fe0 100644 --- a/coderd/workspacebuilds_test.go +++ b/coderd/workspacebuilds_test.go @@ -57,6 +57,9 @@ func TestWorkspaceBuild(t *testing.T) { assert.Equal(t, logs[0].Ip.IPNet.IP.String(), "127.0.0.1") && assert.Equal(t, logs[1].Ip.IPNet.IP.String(), "127.0.0.1") }, testutil.WaitShort, testutil.IntervalFast) + wb, err := client.WorkspaceBuild(testutil.Context(t, testutil.WaitShort), workspace.LatestBuild.ID) + require.NoError(t, err) + require.NotEmpty(t, wb.WorkspaceOwnerName) // note: we sadly cannot set avatar URL through client. } func TestWorkspaceBuildByBuildNumber(t *testing.T) { diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 733fba8297015..c185f6a900ccb 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -108,7 +108,8 @@ func (api *API) workspace(rw http.ResponseWriter, r *http.Request) { workspace, data.builds[0], data.templates[0], - owner, + owner.Username, + owner.AvatarURL, api.Options.AllowWorkspaceRenames, ) if err != nil { @@ -294,7 +295,8 @@ func (api *API) workspaceByOwnerAndName(rw http.ResponseWriter, r *http.Request) workspace, data.builds[0], data.templates[0], - owner, + owner.Username, + owner.AvatarURL, api.Options.AllowWorkspaceRenames, ) if err != nil { @@ -354,15 +356,6 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req return } - wsOwner, err := api.Database.GetUserByID(ctx, member.UserID) - if err != nil { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Requested workspace owner %q does not exist.", - Detail: err.Error(), - }) - return - } - var createWorkspace codersdk.CreateWorkspaceRequest if !httpapi.Read(ctx, rw, r, &createWorkspace) { return @@ -531,7 +524,7 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req ID: uuid.New(), CreatedAt: now, UpdatedAt: now, - OwnerID: wsOwner.ID, + OwnerID: member.UserID, OrganizationID: template.OrganizationID, TemplateID: template.ID, Name: createWorkspace.Name, @@ -599,7 +592,8 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req ProvisionerJob: *provisionerJob, QueuePosition: 0, }, - wsOwner, + member.Username, + member.AvatarURL, []database.WorkspaceResource{}, []database.WorkspaceResourceMetadatum{}, []database.WorkspaceAgent{}, @@ -621,7 +615,8 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req workspace, apiBuild, template, - wsOwner, + member.Username, + member.AvatarURL, api.Options.AllowWorkspaceRenames, ) if err != nil { @@ -971,7 +966,8 @@ func (api *API) putWorkspaceDormant(rw http.ResponseWriter, r *http.Request) { workspace, data.builds[0], data.templates[0], - owner, + owner.Username, + owner.AvatarURL, api.Options.AllowWorkspaceRenames, ) if err != nil { @@ -1398,7 +1394,8 @@ func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) { workspace, data.builds[0], data.templates[0], - owner, + owner.Username, + owner.AvatarURL, api.Options.AllowWorkspaceRenames, ) if err != nil { @@ -1564,7 +1561,8 @@ func convertWorkspaces(requesterID uuid.UUID, workspaces []database.Workspace, d workspace, build, template, - owner, + owner.Username, + owner.AvatarURL, data.allowRenames, ) if err != nil { @@ -1581,7 +1579,8 @@ func convertWorkspace( workspace database.Workspace, workspaceBuild codersdk.WorkspaceBuild, template database.Template, - owner database.User, + username string, + avatarURL string, allowRenames bool, ) (codersdk.Workspace, error) { if requesterID == uuid.Nil { @@ -1621,8 +1620,8 @@ func convertWorkspace( CreatedAt: workspace.CreatedAt, UpdatedAt: workspace.UpdatedAt, OwnerID: workspace.OwnerID, - OwnerName: owner.Username, - OwnerAvatarURL: owner.AvatarURL, + OwnerName: username, + OwnerAvatarURL: avatarURL, OrganizationID: workspace.OrganizationID, TemplateID: workspace.TemplateID, LatestBuild: workspaceBuild, From 763087947c12da74b37f91fd7f5e307b157a8cd7 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Tue, 30 Jan 2024 15:15:46 +0000 Subject: [PATCH 8/9] check avatar URL is set in workspacebuild test --- coderd/workspacebuilds_test.go | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/coderd/workspacebuilds_test.go b/coderd/workspacebuilds_test.go index 13e4ee2c49fe0..794c36cad6674 100644 --- a/coderd/workspacebuilds_test.go +++ b/coderd/workspacebuilds_test.go @@ -21,6 +21,7 @@ import ( "github.com/coder/coder/v2/coderd/audit" "github.com/coder/coder/v2/coderd/coderdtest" "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/database/dbauthz" "github.com/coder/coder/v2/coderd/database/dbtime" "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/codersdk" @@ -37,12 +38,23 @@ func TestWorkspaceBuild(t *testing.T) { propagation.Baggage{}, ), ) + ctx := testutil.Context(t, testutil.WaitShort) auditor := audit.NewMock() - client := coderdtest.New(t, &coderdtest.Options{ + client, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{ IncludeProvisionerDaemon: true, Auditor: auditor, }) user := coderdtest.CreateFirstUser(t, client) + //nolint:gocritic // testing + up, err := db.UpdateUserProfile(dbauthz.AsSystemRestricted(ctx), database.UpdateUserProfileParams{ + ID: user.UserID, + Email: coderdtest.FirstUserParams.Email, + Username: coderdtest.FirstUserParams.Username, + Name: "Admin", + AvatarURL: client.URL.String(), + UpdatedAt: dbtime.Now(), + }) + require.NoError(t, err) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) @@ -59,7 +71,8 @@ func TestWorkspaceBuild(t *testing.T) { }, testutil.WaitShort, testutil.IntervalFast) wb, err := client.WorkspaceBuild(testutil.Context(t, testutil.WaitShort), workspace.LatestBuild.ID) require.NoError(t, err) - require.NotEmpty(t, wb.WorkspaceOwnerName) // note: we sadly cannot set avatar URL through client. + require.Equal(t, up.Username, wb.WorkspaceOwnerName) + require.Equal(t, up.AvatarURL, wb.WorkspaceOwnerAvatarURL) } func TestWorkspaceBuildByBuildNumber(t *testing.T) { From c3d9c51a51152fe2a0c2537265d430c7f18c9e53 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Tue, 30 Jan 2024 15:27:29 +0000 Subject: [PATCH 9/9] add more kyle --- .../pages/ExternalAuthPage/ExternalAuthPageView.stories.tsx | 6 +++--- site/src/testHelpers/entities.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/site/src/pages/ExternalAuthPage/ExternalAuthPageView.stories.tsx b/site/src/pages/ExternalAuthPage/ExternalAuthPageView.stories.tsx index ad240241f9e09..8b8f3d57c5300 100644 --- a/site/src/pages/ExternalAuthPage/ExternalAuthPageView.stories.tsx +++ b/site/src/pages/ExternalAuthPage/ExternalAuthPageView.stories.tsx @@ -22,7 +22,7 @@ WebAuthenticated.args = { app_installable: false, display_name: "BitBucket", user: { - avatar_url: "", + avatar_url: "https://avatars.githubusercontent.com/u/7122116?v=4", login: "kylecarbs", name: "Kyle Carberry", profile_url: "", @@ -83,7 +83,7 @@ DeviceAuthenticatedNotInstalled.args = { app_install_url: "https://example.com", app_installable: true, user: { - avatar_url: "", + avatar_url: "https://avatars.githubusercontent.com/u/7122116?v=4", login: "kylecarbs", name: "Kyle Carberry", profile_url: "", @@ -112,7 +112,7 @@ DeviceAuthenticatedInstalled.args = { app_install_url: "https://example.com", app_installable: true, user: { - avatar_url: "", + avatar_url: "https://avatars.githubusercontent.com/u/7122116?v=4", login: "kylecarbs", name: "Kyle Carberry", profile_url: "", diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 64d43881d2eb1..48812c0807036 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -1014,7 +1014,7 @@ export const MockWorkspace: TypesGen.Workspace = { owner_id: MockUser.id, organization_id: MockOrganization.id, owner_name: MockUser.username, - owner_avatar_url: "", + owner_avatar_url: "https://avatars.githubusercontent.com/u/7122116?v=4", autostart_schedule: MockWorkspaceAutostartEnabled.schedule, ttl_ms: 2 * 60 * 60 * 1000, latest_build: MockWorkspaceBuild,