diff --git a/cli/testdata/coder_list_--output_json.golden b/cli/testdata/coder_list_--output_json.golden index 319ac80c15..903e5681c2 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, diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 6d27bbc65a..b4b60423d0 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -12079,6 +12079,9 @@ const docTemplate = `{ "outdated": { "type": "boolean" }, + "owner_avatar_url": { + "type": "string" + }, "owner_id": { "type": "string", "format": "uuid" @@ -12656,6 +12659,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 c84b96d370..75c0f6b64a 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -10953,6 +10953,9 @@ "outdated": { "type": "boolean" }, + "owner_avatar_url": { + "type": "string" + }, "owner_id": { "type": "string", "format": "uuid" @@ -11501,6 +11504,9 @@ "workspace_name": { "type": "string" }, + "workspace_owner_avatar_url": { + "type": "string" + }, "workspace_owner_id": { "type": "string", "format": "uuid" diff --git a/coderd/httpmw/organizationparam.go b/coderd/httpmw/organizationparam.go index 76b085f019..0637fba3dc 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 d492353e68..d9cf0c7913 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() diff --git a/coderd/users.go b/coderd/users.go index 6cb8b03d37..43e00f8112 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 45813b79eb..9704fa156a 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,8 @@ func (api *API) workspaceBuild(rw http.ResponseWriter, r *http.Request) { workspaceBuild, workspace, data.jobs[0], - ownerName, + owner.Username, + owner.AvatarURL, data.resources, data.metadata, data.agents, @@ -283,7 +284,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 +297,8 @@ func (api *API) workspaceBuildByBuildNumber(rw http.ResponseWriter, r *http.Requ workspaceBuild, workspace, data.jobs[0], - ownerName, + owner.Username, + owner.AvatarURL, data.resources, data.metadata, data.agents, @@ -416,7 +418,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 +434,8 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { ProvisionerJob: *provisionerJob, QueuePosition: 0, }, - ownerName, + owner.Username, + owner.AvatarURL, []database.WorkspaceResource{}, []database.WorkspaceResourceMetadatum{}, []database.WorkspaceAgent{}, @@ -833,7 +836,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 +845,8 @@ func (api *API) convertWorkspaceBuilds( build, workspace, job, - ownerName, + owner.Username, + owner.AvatarURL, workspaceResources, resourceMetadata, resourceAgents, @@ -865,7 +869,7 @@ func (api *API) convertWorkspaceBuild( build database.WorkspaceBuild, workspace database.Workspace, job database.GetProvisionerJobsByIDsWithQueuePositionRow, - ownerName string, + 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, ownerName, 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 { @@ -923,26 +927,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: username, + WorkspaceOwnerAvatarURL: 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/workspacebuilds_test.go b/coderd/workspacebuilds_test.go index 977c073652..794c36cad6 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) @@ -57,6 +69,10 @@ 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.Equal(t, up.Username, wb.WorkspaceOwnerName) + require.Equal(t, up.AvatarURL, wb.WorkspaceOwnerAvatarURL) } func TestWorkspaceBuildByBuildNumber(t *testing.T) { diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 3f8494b4a4..c185f6a900 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,8 @@ func (api *API) workspace(rw http.ResponseWriter, r *http.Request) { workspace, data.builds[0], data.templates[0], - ownerName, + owner.Username, + owner.AvatarURL, api.Options.AllowWorkspaceRenames, ) if err != nil { @@ -281,7 +282,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 +295,8 @@ func (api *API) workspaceByOwnerAndName(rw http.ResponseWriter, r *http.Request) workspace, data.builds[0], data.templates[0], - ownerName, + owner.Username, + owner.AvatarURL, api.Options.AllowWorkspaceRenames, ) if err != nil { @@ -591,6 +593,7 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req QueuePosition: 0, }, member.Username, + member.AvatarURL, []database.WorkspaceResource{}, []database.WorkspaceResourceMetadatum{}, []database.WorkspaceAgent{}, @@ -613,6 +616,7 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req apiBuild, template, member.Username, + member.AvatarURL, 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,8 @@ func (api *API) putWorkspaceDormant(rw http.ResponseWriter, r *http.Request) { workspace, data.builds[0], data.templates[0], - ownerName, + owner.Username, + owner.AvatarURL, api.Options.AllowWorkspaceRenames, ) if err != nil { @@ -1372,7 +1377,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 +1394,8 @@ func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) { workspace, data.builds[0], data.templates[0], - ownerName, + owner.Username, + owner.AvatarURL, api.Options.AllowWorkspaceRenames, ) if err != nil { @@ -1556,6 +1562,7 @@ func convertWorkspaces(requesterID uuid.UUID, workspaces []database.Workspace, d build, template, owner.Username, + owner.AvatarURL, data.allowRenames, ) if err != nil { @@ -1572,7 +1579,8 @@ func convertWorkspace( workspace database.Workspace, workspaceBuild codersdk.WorkspaceBuild, template database.Template, - ownerName string, + username string, + avatarURL string, allowRenames bool, ) (codersdk.Workspace, error) { if requesterID == uuid.Nil { @@ -1612,7 +1620,8 @@ func convertWorkspace( CreatedAt: workspace.CreatedAt, UpdatedAt: workspace.UpdatedAt, OwnerID: workspace.OwnerID, - OwnerName: ownerName, + OwnerName: username, + OwnerAvatarURL: avatarURL, OrganizationID: workspace.OrganizationID, TemplateID: workspace.TemplateID, LatestBuild: workspaceBuild, diff --git a/codersdk/workspacebuilds.go b/codersdk/workspacebuilds.go index c7bdf022d2..682cb424af 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 5cdd8cd1c6..d5008b3234 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 50072c8aa0..3b7a708003 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 e4cd7c722f..dd5b1d4876 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -6096,12 +6096,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", @@ -6134,6 +6136,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 | | | @@ -6790,6 +6793,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" } @@ -6797,28 +6801,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 @@ -7357,12 +7362,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 f4c1b6957f..f176653a17 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 5770e3b6fe..d8ad438558 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1481,6 +1481,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; @@ -1635,6 +1636,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/ExternalAuthPage/ExternalAuthPageView.stories.tsx b/site/src/pages/ExternalAuthPage/ExternalAuthPageView.stories.tsx index ad240241f9..8b8f3d57c5 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/pages/WorkspacePage/WorkspaceTopbar.tsx b/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx index 0bba8c41a5..e6ff5b25f0 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" @@ -130,9 +130,11 @@ export const WorkspaceTopbar: FC = ({ }} > - - - + {workspace.owner_name} diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index e35a44b26c..6f72d80d3d 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: "https://avatars.githubusercontent.com/u/7122116?v=4", autostart_schedule: MockWorkspaceAutostartEnabled.schedule, ttl_ms: 2 * 60 * 60 * 1000, latest_build: MockWorkspaceBuild,