feat(site): display user avatar (#11893)

* add owner API to workspace and workspace build responses
* display user avatar in workspace top bar

Co-authored-by: Cian Johnston <cian@coder.com>
This commit is contained in:
Bruno Quaresma 2024-01-30 14:07:06 -03:00 committed by GitHub
parent 83eea2d323
commit dcab6fa5a4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 216 additions and 105 deletions

View File

@ -5,6 +5,7 @@
"updated_at": "[timestamp]", "updated_at": "[timestamp]",
"owner_id": "[first user ID]", "owner_id": "[first user ID]",
"owner_name": "testuser", "owner_name": "testuser",
"owner_avatar_url": "",
"organization_id": "[first org ID]", "organization_id": "[first org ID]",
"template_id": "[template ID]", "template_id": "[template ID]",
"template_name": "test-template", "template_name": "test-template",
@ -21,6 +22,7 @@
"workspace_name": "test-workspace", "workspace_name": "test-workspace",
"workspace_owner_id": "[first user ID]", "workspace_owner_id": "[first user ID]",
"workspace_owner_name": "testuser", "workspace_owner_name": "testuser",
"workspace_owner_avatar_url": "",
"template_version_id": "[version ID]", "template_version_id": "[version ID]",
"template_version_name": "[version name]", "template_version_name": "[version name]",
"build_number": 1, "build_number": 1,

6
coderd/apidoc/docs.go generated
View File

@ -12079,6 +12079,9 @@ const docTemplate = `{
"outdated": { "outdated": {
"type": "boolean" "type": "boolean"
}, },
"owner_avatar_url": {
"type": "string"
},
"owner_id": { "owner_id": {
"type": "string", "type": "string",
"format": "uuid" "format": "uuid"
@ -12656,6 +12659,9 @@ const docTemplate = `{
"workspace_name": { "workspace_name": {
"type": "string" "type": "string"
}, },
"workspace_owner_avatar_url": {
"type": "string"
},
"workspace_owner_id": { "workspace_owner_id": {
"type": "string", "type": "string",
"format": "uuid" "format": "uuid"

View File

@ -10953,6 +10953,9 @@
"outdated": { "outdated": {
"type": "boolean" "type": "boolean"
}, },
"owner_avatar_url": {
"type": "string"
},
"owner_id": { "owner_id": {
"type": "string", "type": "string",
"format": "uuid" "format": "uuid"
@ -11501,6 +11504,9 @@
"workspace_name": { "workspace_name": {
"type": "string" "type": "string"
}, },
"workspace_owner_avatar_url": {
"type": "string"
},
"workspace_owner_id": { "workspace_owner_id": {
"type": "string", "type": "string",
"format": "uuid" "format": "uuid"

View File

@ -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 // OrganizationMember is the database object plus the Username and Avatar URL. Including these
// middleware is preferable to a join at the SQL layer so that we can keep the autogenerated // in the middleware is preferable to a join at the SQL layer so that we can keep the
// database types as they are. // autogenerated database types as they are.
type OrganizationMember struct { type OrganizationMember struct {
database.OrganizationMember database.OrganizationMember
Username string Username string
AvatarURL string
} }
// ExtractOrganizationMemberParam grabs a user membership from the "organization" and "user" URL parameter. // 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{ ctx = context.WithValue(ctx, organizationMemberParamContextKey{}, OrganizationMember{
OrganizationMember: organizationMember, OrganizationMember: organizationMember,
// Here we're making one exception to the rule about not leaking data about the user // 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. If the caller has permission // to the API handler, which is to include the username and avatar URL.
// to read the OrganizationMember, then we're explicitly saying here that they also // If the caller has permission to read the OrganizationMember, then we're explicitly
// have permission to see the member's username, which is itself uncontroversial. // 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 // API handlers need this information for audit logging and returning the owner's
// username in response to creating a workspace. // username in response to creating a workspace. Additionally, the frontend consumes
Username: user.Username, // 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)) next.ServeHTTP(rw, r.WithContext(ctx))
}) })

View File

@ -8,6 +8,7 @@ import (
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/coder/coder/v2/coderd/database" "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/dbmem"
"github.com/coder/coder/v2/coderd/database/dbtime" "github.com/coder/coder/v2/coderd/database/dbtime"
"github.com/coder/coder/v2/coderd/httpmw" "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/codersdk"
"github.com/coder/coder/v2/testutil"
) )
func TestOrganizationParam(t *testing.T) { func TestOrganizationParam(t *testing.T) {
@ -139,6 +142,7 @@ func TestOrganizationParam(t *testing.T) {
t.Run("Success", func(t *testing.T) { t.Run("Success", func(t *testing.T) {
t.Parallel() t.Parallel()
var ( var (
ctx = testutil.Context(t, testutil.WaitShort)
db = dbmem.New() db = dbmem.New()
rw = httptest.NewRecorder() rw = httptest.NewRecorder()
r, user = setupAuthentication(db) r, user = setupAuthentication(db)
@ -148,7 +152,14 @@ func TestOrganizationParam(t *testing.T) {
_ = dbgen.OrganizationMember(t, db, database.OrganizationMember{ _ = dbgen.OrganizationMember(t, db, database.OrganizationMember{
OrganizationID: organization.ID, OrganizationID: organization.ID,
UserID: user.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("organization", organization.ID.String())
chi.RouteContext(r.Context()).URLParams.Add("user", user.ID.String()) chi.RouteContext(r.Context()).URLParams.Add("user", user.ID.String())
rtr.Use( rtr.Use(
@ -161,9 +172,27 @@ func TestOrganizationParam(t *testing.T) {
httpmw.ExtractOrganizationMemberParam(db), httpmw.ExtractOrganizationMemberParam(db),
) )
rtr.Get("/", func(rw http.ResponseWriter, r *http.Request) { rtr.Get("/", func(rw http.ResponseWriter, r *http.Request) {
_ = httpmw.OrganizationParam(r) org := httpmw.OrganizationParam(r)
_ = httpmw.OrganizationMemberParam(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) 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) rtr.ServeHTTP(rw, r)
res := rw.Result() res := rw.Result()

View File

@ -1271,13 +1271,13 @@ func userOrganizationIDs(ctx context.Context, api *API, user database.User) ([]u
return member.OrganizationIDs, nil 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 { for _, user := range users {
if id == user.ID { if id == user.ID {
return user.Username, true return user, true
} }
} }
return "", false return database.User{}, false
} }
func convertAPIKey(k database.APIKey) codersdk.APIKey { func convertAPIKey(k database.APIKey) codersdk.APIKey {

View File

@ -69,7 +69,7 @@ func (api *API) workspaceBuild(rw http.ResponseWriter, r *http.Request) {
}) })
return return
} }
ownerName, ok := usernameWithID(workspace.OwnerID, data.users) owner, ok := userByID(workspace.OwnerID, data.users)
if !ok { if !ok {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error converting workspace build.", Message: "Internal error converting workspace build.",
@ -82,7 +82,8 @@ func (api *API) workspaceBuild(rw http.ResponseWriter, r *http.Request) {
workspaceBuild, workspaceBuild,
workspace, workspace,
data.jobs[0], data.jobs[0],
ownerName, owner.Username,
owner.AvatarURL,
data.resources, data.resources,
data.metadata, data.metadata,
data.agents, data.agents,
@ -283,7 +284,7 @@ func (api *API) workspaceBuildByBuildNumber(rw http.ResponseWriter, r *http.Requ
}) })
return return
} }
ownerName, ok := usernameWithID(workspace.OwnerID, data.users) owner, ok := userByID(workspace.OwnerID, data.users)
if !ok { if !ok {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error converting workspace build.", Message: "Internal error converting workspace build.",
@ -296,7 +297,8 @@ func (api *API) workspaceBuildByBuildNumber(rw http.ResponseWriter, r *http.Requ
workspaceBuild, workspaceBuild,
workspace, workspace,
data.jobs[0], data.jobs[0],
ownerName, owner.Username,
owner.AvatarURL,
data.resources, data.resources,
data.metadata, data.metadata,
data.agents, data.agents,
@ -416,7 +418,7 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
}) })
return return
} }
ownerName, exists := usernameWithID(workspace.OwnerID, users) owner, exists := userByID(workspace.OwnerID, users)
if !exists { if !exists {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error converting workspace build.", Message: "Internal error converting workspace build.",
@ -432,7 +434,8 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
ProvisionerJob: *provisionerJob, ProvisionerJob: *provisionerJob,
QueuePosition: 0, QueuePosition: 0,
}, },
ownerName, owner.Username,
owner.AvatarURL,
[]database.WorkspaceResource{}, []database.WorkspaceResource{},
[]database.WorkspaceResourceMetadatum{}, []database.WorkspaceResourceMetadatum{},
[]database.WorkspaceAgent{}, []database.WorkspaceAgent{},
@ -833,7 +836,7 @@ func (api *API) convertWorkspaceBuilds(
if !exists { if !exists {
return nil, xerrors.New("template version not found") return nil, xerrors.New("template version not found")
} }
ownerName, exists := usernameWithID(workspace.OwnerID, users) owner, exists := userByID(workspace.OwnerID, users)
if !exists { if !exists {
return nil, xerrors.Errorf("owner not found for workspace: %q", workspace.Name) return nil, xerrors.Errorf("owner not found for workspace: %q", workspace.Name)
} }
@ -842,7 +845,8 @@ func (api *API) convertWorkspaceBuilds(
build, build,
workspace, workspace,
job, job,
ownerName, owner.Username,
owner.AvatarURL,
workspaceResources, workspaceResources,
resourceMetadata, resourceMetadata,
resourceAgents, resourceAgents,
@ -865,7 +869,7 @@ func (api *API) convertWorkspaceBuild(
build database.WorkspaceBuild, build database.WorkspaceBuild,
workspace database.Workspace, workspace database.Workspace,
job database.GetProvisionerJobsByIDsWithQueuePositionRow, job database.GetProvisionerJobsByIDsWithQueuePositionRow,
ownerName string, username, avatarURL string,
workspaceResources []database.WorkspaceResource, workspaceResources []database.WorkspaceResource,
resourceMetadata []database.WorkspaceResourceMetadatum, resourceMetadata []database.WorkspaceResourceMetadatum,
resourceAgents []database.WorkspaceAgent, resourceAgents []database.WorkspaceAgent,
@ -909,7 +913,7 @@ func (api *API) convertWorkspaceBuild(
scripts := scriptsByAgentID[agent.ID] scripts := scriptsByAgentID[agent.ID]
logSources := logSourcesByAgentID[agent.ID] logSources := logSourcesByAgentID[agent.ID]
apiAgent, err := db2sdk.WorkspaceAgent( 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(), api.DeploymentValues.AgentFallbackTroubleshootingURL.String(),
) )
if err != nil { if err != nil {
@ -923,26 +927,27 @@ func (api *API) convertWorkspaceBuild(
apiJob := convertProvisionerJob(job) apiJob := convertProvisionerJob(job)
transition := codersdk.WorkspaceTransition(build.Transition) transition := codersdk.WorkspaceTransition(build.Transition)
return codersdk.WorkspaceBuild{ return codersdk.WorkspaceBuild{
ID: build.ID, ID: build.ID,
CreatedAt: build.CreatedAt, CreatedAt: build.CreatedAt,
UpdatedAt: build.UpdatedAt, UpdatedAt: build.UpdatedAt,
WorkspaceOwnerID: workspace.OwnerID, WorkspaceOwnerID: workspace.OwnerID,
WorkspaceOwnerName: ownerName, WorkspaceOwnerName: username,
WorkspaceID: build.WorkspaceID, WorkspaceOwnerAvatarURL: avatarURL,
WorkspaceName: workspace.Name, WorkspaceID: build.WorkspaceID,
TemplateVersionID: build.TemplateVersionID, WorkspaceName: workspace.Name,
TemplateVersionName: templateVersion.Name, TemplateVersionID: build.TemplateVersionID,
BuildNumber: build.BuildNumber, TemplateVersionName: templateVersion.Name,
Transition: transition, BuildNumber: build.BuildNumber,
InitiatorID: build.InitiatorID, Transition: transition,
InitiatorUsername: build.InitiatorByUsername, InitiatorID: build.InitiatorID,
Job: apiJob, InitiatorUsername: build.InitiatorByUsername,
Deadline: codersdk.NewNullTime(build.Deadline, !build.Deadline.IsZero()), Job: apiJob,
MaxDeadline: codersdk.NewNullTime(build.MaxDeadline, !build.MaxDeadline.IsZero()), Deadline: codersdk.NewNullTime(build.Deadline, !build.Deadline.IsZero()),
Reason: codersdk.BuildReason(build.Reason), MaxDeadline: codersdk.NewNullTime(build.MaxDeadline, !build.MaxDeadline.IsZero()),
Resources: apiResources, Reason: codersdk.BuildReason(build.Reason),
Status: convertWorkspaceStatus(apiJob.Status, transition), Resources: apiResources,
DailyCost: build.DailyCost, Status: convertWorkspaceStatus(apiJob.Status, transition),
DailyCost: build.DailyCost,
}, nil }, nil
} }

View File

@ -21,6 +21,7 @@ import (
"github.com/coder/coder/v2/coderd/audit" "github.com/coder/coder/v2/coderd/audit"
"github.com/coder/coder/v2/coderd/coderdtest" "github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/coderd/database" "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/database/dbtime"
"github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk"
@ -37,12 +38,23 @@ func TestWorkspaceBuild(t *testing.T) {
propagation.Baggage{}, propagation.Baggage{},
), ),
) )
ctx := testutil.Context(t, testutil.WaitShort)
auditor := audit.NewMock() auditor := audit.NewMock()
client := coderdtest.New(t, &coderdtest.Options{ client, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{
IncludeProvisionerDaemon: true, IncludeProvisionerDaemon: true,
Auditor: auditor, Auditor: auditor,
}) })
user := coderdtest.CreateFirstUser(t, client) 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) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, 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[0].Ip.IPNet.IP.String(), "127.0.0.1") &&
assert.Equal(t, logs[1].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) }, 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) { func TestWorkspaceBuildByBuildNumber(t *testing.T) {

View File

@ -94,7 +94,7 @@ func (api *API) workspace(rw http.ResponseWriter, r *http.Request) {
httpapi.Forbidden(rw) httpapi.Forbidden(rw)
return return
} }
ownerName, ok := usernameWithID(workspace.OwnerID, data.users) owner, ok := userByID(workspace.OwnerID, data.users)
if !ok { if !ok {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching workspace resources.", Message: "Internal error fetching workspace resources.",
@ -108,7 +108,8 @@ func (api *API) workspace(rw http.ResponseWriter, r *http.Request) {
workspace, workspace,
data.builds[0], data.builds[0],
data.templates[0], data.templates[0],
ownerName, owner.Username,
owner.AvatarURL,
api.Options.AllowWorkspaceRenames, api.Options.AllowWorkspaceRenames,
) )
if err != nil { if err != nil {
@ -281,7 +282,7 @@ func (api *API) workspaceByOwnerAndName(rw http.ResponseWriter, r *http.Request)
httpapi.ResourceNotFound(rw) httpapi.ResourceNotFound(rw)
return return
} }
ownerName, ok := usernameWithID(workspace.OwnerID, data.users) owner, ok := userByID(workspace.OwnerID, data.users)
if !ok { if !ok {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching workspace resources.", Message: "Internal error fetching workspace resources.",
@ -294,7 +295,8 @@ func (api *API) workspaceByOwnerAndName(rw http.ResponseWriter, r *http.Request)
workspace, workspace,
data.builds[0], data.builds[0],
data.templates[0], data.templates[0],
ownerName, owner.Username,
owner.AvatarURL,
api.Options.AllowWorkspaceRenames, api.Options.AllowWorkspaceRenames,
) )
if err != nil { if err != nil {
@ -591,6 +593,7 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req
QueuePosition: 0, QueuePosition: 0,
}, },
member.Username, member.Username,
member.AvatarURL,
[]database.WorkspaceResource{}, []database.WorkspaceResource{},
[]database.WorkspaceResourceMetadatum{}, []database.WorkspaceResourceMetadatum{},
[]database.WorkspaceAgent{}, []database.WorkspaceAgent{},
@ -613,6 +616,7 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req
apiBuild, apiBuild,
template, template,
member.Username, member.Username,
member.AvatarURL,
api.Options.AllowWorkspaceRenames, api.Options.AllowWorkspaceRenames,
) )
if err != nil { if err != nil {
@ -941,7 +945,7 @@ func (api *API) putWorkspaceDormant(rw http.ResponseWriter, r *http.Request) {
}) })
return return
} }
ownerName, ok := usernameWithID(workspace.OwnerID, data.users) owner, ok := userByID(workspace.OwnerID, data.users)
if !ok { if !ok {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching workspace resources.", Message: "Internal error fetching workspace resources.",
@ -962,7 +966,8 @@ func (api *API) putWorkspaceDormant(rw http.ResponseWriter, r *http.Request) {
workspace, workspace,
data.builds[0], data.builds[0],
data.templates[0], data.templates[0],
ownerName, owner.Username,
owner.AvatarURL,
api.Options.AllowWorkspaceRenames, api.Options.AllowWorkspaceRenames,
) )
if err != nil { if err != nil {
@ -1372,7 +1377,7 @@ func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) {
return return
} }
ownerName, ok := usernameWithID(workspace.OwnerID, data.users) owner, ok := userByID(workspace.OwnerID, data.users)
if !ok { if !ok {
_ = sendEvent(ctx, codersdk.ServerSentEvent{ _ = sendEvent(ctx, codersdk.ServerSentEvent{
Type: codersdk.ServerSentEventTypeError, Type: codersdk.ServerSentEventTypeError,
@ -1389,7 +1394,8 @@ func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) {
workspace, workspace,
data.builds[0], data.builds[0],
data.templates[0], data.templates[0],
ownerName, owner.Username,
owner.AvatarURL,
api.Options.AllowWorkspaceRenames, api.Options.AllowWorkspaceRenames,
) )
if err != nil { if err != nil {
@ -1556,6 +1562,7 @@ func convertWorkspaces(requesterID uuid.UUID, workspaces []database.Workspace, d
build, build,
template, template,
owner.Username, owner.Username,
owner.AvatarURL,
data.allowRenames, data.allowRenames,
) )
if err != nil { if err != nil {
@ -1572,7 +1579,8 @@ func convertWorkspace(
workspace database.Workspace, workspace database.Workspace,
workspaceBuild codersdk.WorkspaceBuild, workspaceBuild codersdk.WorkspaceBuild,
template database.Template, template database.Template,
ownerName string, username string,
avatarURL string,
allowRenames bool, allowRenames bool,
) (codersdk.Workspace, error) { ) (codersdk.Workspace, error) {
if requesterID == uuid.Nil { if requesterID == uuid.Nil {
@ -1612,7 +1620,8 @@ func convertWorkspace(
CreatedAt: workspace.CreatedAt, CreatedAt: workspace.CreatedAt,
UpdatedAt: workspace.UpdatedAt, UpdatedAt: workspace.UpdatedAt,
OwnerID: workspace.OwnerID, OwnerID: workspace.OwnerID,
OwnerName: ownerName, OwnerName: username,
OwnerAvatarURL: avatarURL,
OrganizationID: workspace.OrganizationID, OrganizationID: workspace.OrganizationID,
TemplateID: workspace.TemplateID, TemplateID: workspace.TemplateID,
LatestBuild: workspaceBuild, LatestBuild: workspaceBuild,

View File

@ -51,26 +51,27 @@ const (
// WorkspaceBuild is an at-point representation of a workspace state. // WorkspaceBuild is an at-point representation of a workspace state.
// BuildNumbers start at 1 and increase by 1 for each subsequent build // BuildNumbers start at 1 and increase by 1 for each subsequent build
type WorkspaceBuild struct { type WorkspaceBuild struct {
ID uuid.UUID `json:"id" format:"uuid"` ID uuid.UUID `json:"id" format:"uuid"`
CreatedAt time.Time `json:"created_at" format:"date-time"` CreatedAt time.Time `json:"created_at" format:"date-time"`
UpdatedAt time.Time `json:"updated_at" format:"date-time"` UpdatedAt time.Time `json:"updated_at" format:"date-time"`
WorkspaceID uuid.UUID `json:"workspace_id" format:"uuid"` WorkspaceID uuid.UUID `json:"workspace_id" format:"uuid"`
WorkspaceName string `json:"workspace_name"` WorkspaceName string `json:"workspace_name"`
WorkspaceOwnerID uuid.UUID `json:"workspace_owner_id" format:"uuid"` WorkspaceOwnerID uuid.UUID `json:"workspace_owner_id" format:"uuid"`
WorkspaceOwnerName string `json:"workspace_owner_name"` WorkspaceOwnerName string `json:"workspace_owner_name"`
TemplateVersionID uuid.UUID `json:"template_version_id" format:"uuid"` WorkspaceOwnerAvatarURL string `json:"workspace_owner_avatar_url"`
TemplateVersionName string `json:"template_version_name"` TemplateVersionID uuid.UUID `json:"template_version_id" format:"uuid"`
BuildNumber int32 `json:"build_number"` TemplateVersionName string `json:"template_version_name"`
Transition WorkspaceTransition `json:"transition" enums:"start,stop,delete"` BuildNumber int32 `json:"build_number"`
InitiatorID uuid.UUID `json:"initiator_id" format:"uuid"` Transition WorkspaceTransition `json:"transition" enums:"start,stop,delete"`
InitiatorUsername string `json:"initiator_name"` InitiatorID uuid.UUID `json:"initiator_id" format:"uuid"`
Job ProvisionerJob `json:"job"` InitiatorUsername string `json:"initiator_name"`
Reason BuildReason `db:"reason" json:"reason" enums:"initiator,autostart,autostop"` Job ProvisionerJob `json:"job"`
Resources []WorkspaceResource `json:"resources"` Reason BuildReason `db:"reason" json:"reason" enums:"initiator,autostart,autostop"`
Deadline NullTime `json:"deadline,omitempty" format:"date-time"` Resources []WorkspaceResource `json:"resources"`
MaxDeadline NullTime `json:"max_deadline,omitempty" format:"date-time"` Deadline NullTime `json:"deadline,omitempty" format:"date-time"`
Status WorkspaceStatus `json:"status" enums:"pending,starting,running,stopping,stopped,failed,canceling,canceled,deleting,deleted"` MaxDeadline NullTime `json:"max_deadline,omitempty" format:"date-time"`
DailyCost int32 `json:"daily_cost"` 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: // WorkspaceResource describes resources used to create a workspace, for instance:

View File

@ -29,6 +29,7 @@ type Workspace struct {
UpdatedAt time.Time `json:"updated_at" format:"date-time"` UpdatedAt time.Time `json:"updated_at" format:"date-time"`
OwnerID uuid.UUID `json:"owner_id" format:"uuid"` OwnerID uuid.UUID `json:"owner_id" format:"uuid"`
OwnerName string `json:"owner_name"` OwnerName string `json:"owner_name"`
OwnerAvatarURL string `json:"owner_avatar_url"`
OrganizationID uuid.UUID `json:"organization_id" format:"uuid"` OrganizationID uuid.UUID `json:"organization_id" format:"uuid"`
TemplateID uuid.UUID `json:"template_id" format:"uuid"` TemplateID uuid.UUID `json:"template_id" format:"uuid"`
TemplateName string `json:"template_name"` TemplateName string `json:"template_name"`

6
docs/api/builds.md generated
View File

@ -170,6 +170,7 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/workspace/{workspacenam
"updated_at": "2019-08-24T14:15:22Z", "updated_at": "2019-08-24T14:15:22Z",
"workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
"workspace_name": "string", "workspace_name": "string",
"workspace_owner_avatar_url": "string",
"workspace_owner_id": "e7078695-5279-4c86-8774-3ac2367a2fc7", "workspace_owner_id": "e7078695-5279-4c86-8774-3ac2367a2fc7",
"workspace_owner_name": "string" "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", "updated_at": "2019-08-24T14:15:22Z",
"workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
"workspace_name": "string", "workspace_name": "string",
"workspace_owner_avatar_url": "string",
"workspace_owner_id": "e7078695-5279-4c86-8774-3ac2367a2fc7", "workspace_owner_id": "e7078695-5279-4c86-8774-3ac2367a2fc7",
"workspace_owner_name": "string" "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", "updated_at": "2019-08-24T14:15:22Z",
"workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
"workspace_name": "string", "workspace_name": "string",
"workspace_owner_avatar_url": "string",
"workspace_owner_id": "e7078695-5279-4c86-8774-3ac2367a2fc7", "workspace_owner_id": "e7078695-5279-4c86-8774-3ac2367a2fc7",
"workspace_owner_name": "string" "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", "updated_at": "2019-08-24T14:15:22Z",
"workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
"workspace_name": "string", "workspace_name": "string",
"workspace_owner_avatar_url": "string",
"workspace_owner_id": "e7078695-5279-4c86-8774-3ac2367a2fc7", "workspace_owner_id": "e7078695-5279-4c86-8774-3ac2367a2fc7",
"workspace_owner_name": "string" "workspace_owner_name": "string"
} }
@ -1277,6 +1281,7 @@ Status Code **200**
| `» updated_at` | string(date-time) | false | | | | `» updated_at` | string(date-time) | false | | |
| `» workspace_id` | string(uuid) | false | | | | `» workspace_id` | string(uuid) | false | | |
| `» workspace_name` | string | false | | | | `» workspace_name` | string | false | | |
| `» workspace_owner_avatar_url` | string | false | | |
| `» workspace_owner_id` | string(uuid) | false | | | | `» workspace_owner_id` | string(uuid) | false | | |
| `» workspace_owner_name` | string | 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", "updated_at": "2019-08-24T14:15:22Z",
"workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
"workspace_name": "string", "workspace_name": "string",
"workspace_owner_avatar_url": "string",
"workspace_owner_id": "e7078695-5279-4c86-8774-3ac2367a2fc7", "workspace_owner_id": "e7078695-5279-4c86-8774-3ac2367a2fc7",
"workspace_owner_name": "string" "workspace_owner_name": "string"
} }

51
docs/api/schemas.md generated
View File

@ -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", "updated_at": "2019-08-24T14:15:22Z",
"workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
"workspace_name": "string", "workspace_name": "string",
"workspace_owner_avatar_url": "string",
"workspace_owner_id": "e7078695-5279-4c86-8774-3ac2367a2fc7", "workspace_owner_id": "e7078695-5279-4c86-8774-3ac2367a2fc7",
"workspace_owner_name": "string" "workspace_owner_name": "string"
}, },
"name": "string", "name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"outdated": true, "outdated": true,
"owner_avatar_url": "string",
"owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05", "owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05",
"owner_name": "string", "owner_name": "string",
"template_active_version_id": "b0da9c29-67d8-4c87-888c-bafe356f7f3c", "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 | | | | `name` | string | false | | |
| `organization_id` | string | false | | | | `organization_id` | string | false | | |
| `outdated` | boolean | false | | | | `outdated` | boolean | false | | |
| `owner_avatar_url` | string | false | | |
| `owner_id` | string | false | | | | `owner_id` | string | false | | |
| `owner_name` | string | false | | | | `owner_name` | string | false | | |
| `template_active_version_id` | 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", "updated_at": "2019-08-24T14:15:22Z",
"workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
"workspace_name": "string", "workspace_name": "string",
"workspace_owner_avatar_url": "string",
"workspace_owner_id": "e7078695-5279-4c86-8774-3ac2367a2fc7", "workspace_owner_id": "e7078695-5279-4c86-8774-3ac2367a2fc7",
"workspace_owner_name": "string" "workspace_owner_name": "string"
} }
@ -6797,28 +6801,29 @@ If the schedule is empty, the user will be updated to use the default schedule.|
### Properties ### Properties
| Name | Type | Required | Restrictions | Description | | Name | Type | Required | Restrictions | Description |
| ----------------------- | ----------------------------------------------------------------- | -------- | ------------ | ----------- | | ---------------------------- | ----------------------------------------------------------------- | -------- | ------------ | ----------- |
| `build_number` | integer | false | | | | `build_number` | integer | false | | |
| `created_at` | string | false | | | | `created_at` | string | false | | |
| `daily_cost` | integer | false | | | | `daily_cost` | integer | false | | |
| `deadline` | string | false | | | | `deadline` | string | false | | |
| `id` | string | false | | | | `id` | string | false | | |
| `initiator_id` | string | false | | | | `initiator_id` | string | false | | |
| `initiator_name` | string | false | | | | `initiator_name` | string | false | | |
| `job` | [codersdk.ProvisionerJob](#codersdkprovisionerjob) | false | | | | `job` | [codersdk.ProvisionerJob](#codersdkprovisionerjob) | false | | |
| `max_deadline` | string | false | | | | `max_deadline` | string | false | | |
| `reason` | [codersdk.BuildReason](#codersdkbuildreason) | false | | | | `reason` | [codersdk.BuildReason](#codersdkbuildreason) | false | | |
| `resources` | array of [codersdk.WorkspaceResource](#codersdkworkspaceresource) | false | | | | `resources` | array of [codersdk.WorkspaceResource](#codersdkworkspaceresource) | false | | |
| `status` | [codersdk.WorkspaceStatus](#codersdkworkspacestatus) | false | | | | `status` | [codersdk.WorkspaceStatus](#codersdkworkspacestatus) | false | | |
| `template_version_id` | string | false | | | | `template_version_id` | string | false | | |
| `template_version_name` | string | false | | | | `template_version_name` | string | false | | |
| `transition` | [codersdk.WorkspaceTransition](#codersdkworkspacetransition) | false | | | | `transition` | [codersdk.WorkspaceTransition](#codersdkworkspacetransition) | false | | |
| `updated_at` | string | false | | | | `updated_at` | string | false | | |
| `workspace_id` | string | false | | | | `workspace_id` | string | false | | |
| `workspace_name` | string | false | | | | `workspace_name` | string | false | | |
| `workspace_owner_id` | string | false | | | | `workspace_owner_avatar_url` | string | false | | |
| `workspace_owner_name` | string | false | | | | `workspace_owner_id` | string | false | | |
| `workspace_owner_name` | string | false | | |
#### Enumerated Values #### 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", "updated_at": "2019-08-24T14:15:22Z",
"workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
"workspace_name": "string", "workspace_name": "string",
"workspace_owner_avatar_url": "string",
"workspace_owner_id": "e7078695-5279-4c86-8774-3ac2367a2fc7", "workspace_owner_id": "e7078695-5279-4c86-8774-3ac2367a2fc7",
"workspace_owner_name": "string" "workspace_owner_name": "string"
}, },
"name": "string", "name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"outdated": true, "outdated": true,
"owner_avatar_url": "string",
"owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05", "owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05",
"owner_name": "string", "owner_name": "string",
"template_active_version_id": "b0da9c29-67d8-4c87-888c-bafe356f7f3c", "template_active_version_id": "b0da9c29-67d8-4c87-888c-bafe356f7f3c",

10
docs/api/workspaces.md generated
View File

@ -204,12 +204,14 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/member
"updated_at": "2019-08-24T14:15:22Z", "updated_at": "2019-08-24T14:15:22Z",
"workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
"workspace_name": "string", "workspace_name": "string",
"workspace_owner_avatar_url": "string",
"workspace_owner_id": "e7078695-5279-4c86-8774-3ac2367a2fc7", "workspace_owner_id": "e7078695-5279-4c86-8774-3ac2367a2fc7",
"workspace_owner_name": "string" "workspace_owner_name": "string"
}, },
"name": "string", "name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"outdated": true, "outdated": true,
"owner_avatar_url": "string",
"owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05", "owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05",
"owner_name": "string", "owner_name": "string",
"template_active_version_id": "b0da9c29-67d8-4c87-888c-bafe356f7f3c", "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", "updated_at": "2019-08-24T14:15:22Z",
"workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
"workspace_name": "string", "workspace_name": "string",
"workspace_owner_avatar_url": "string",
"workspace_owner_id": "e7078695-5279-4c86-8774-3ac2367a2fc7", "workspace_owner_id": "e7078695-5279-4c86-8774-3ac2367a2fc7",
"workspace_owner_name": "string" "workspace_owner_name": "string"
}, },
"name": "string", "name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"outdated": true, "outdated": true,
"owner_avatar_url": "string",
"owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05", "owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05",
"owner_name": "string", "owner_name": "string",
"template_active_version_id": "b0da9c29-67d8-4c87-888c-bafe356f7f3c", "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", "updated_at": "2019-08-24T14:15:22Z",
"workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
"workspace_name": "string", "workspace_name": "string",
"workspace_owner_avatar_url": "string",
"workspace_owner_id": "e7078695-5279-4c86-8774-3ac2367a2fc7", "workspace_owner_id": "e7078695-5279-4c86-8774-3ac2367a2fc7",
"workspace_owner_name": "string" "workspace_owner_name": "string"
}, },
"name": "string", "name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"outdated": true, "outdated": true,
"owner_avatar_url": "string",
"owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05", "owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05",
"owner_name": "string", "owner_name": "string",
"template_active_version_id": "b0da9c29-67d8-4c87-888c-bafe356f7f3c", "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", "updated_at": "2019-08-24T14:15:22Z",
"workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
"workspace_name": "string", "workspace_name": "string",
"workspace_owner_avatar_url": "string",
"workspace_owner_id": "e7078695-5279-4c86-8774-3ac2367a2fc7", "workspace_owner_id": "e7078695-5279-4c86-8774-3ac2367a2fc7",
"workspace_owner_name": "string" "workspace_owner_name": "string"
}, },
"name": "string", "name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"outdated": true, "outdated": true,
"owner_avatar_url": "string",
"owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05", "owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05",
"owner_name": "string", "owner_name": "string",
"template_active_version_id": "b0da9c29-67d8-4c87-888c-bafe356f7f3c", "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", "updated_at": "2019-08-24T14:15:22Z",
"workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
"workspace_name": "string", "workspace_name": "string",
"workspace_owner_avatar_url": "string",
"workspace_owner_id": "e7078695-5279-4c86-8774-3ac2367a2fc7", "workspace_owner_id": "e7078695-5279-4c86-8774-3ac2367a2fc7",
"workspace_owner_name": "string" "workspace_owner_name": "string"
}, },
"name": "string", "name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"outdated": true, "outdated": true,
"owner_avatar_url": "string",
"owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05", "owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05",
"owner_name": "string", "owner_name": "string",
"template_active_version_id": "b0da9c29-67d8-4c87-888c-bafe356f7f3c", "template_active_version_id": "b0da9c29-67d8-4c87-888c-bafe356f7f3c",

View File

@ -1481,6 +1481,7 @@ export interface Workspace {
readonly updated_at: string; readonly updated_at: string;
readonly owner_id: string; readonly owner_id: string;
readonly owner_name: string; readonly owner_name: string;
readonly owner_avatar_url: string;
readonly organization_id: string; readonly organization_id: string;
readonly template_id: string; readonly template_id: string;
readonly template_name: string; readonly template_name: string;
@ -1635,6 +1636,7 @@ export interface WorkspaceBuild {
readonly workspace_name: string; readonly workspace_name: string;
readonly workspace_owner_id: string; readonly workspace_owner_id: string;
readonly workspace_owner_name: string; readonly workspace_owner_name: string;
readonly workspace_owner_avatar_url: string;
readonly template_version_id: string; readonly template_version_id: string;
readonly template_version_name: string; readonly template_version_name: string;
readonly build_number: number; readonly build_number: number;

View File

@ -22,7 +22,7 @@ WebAuthenticated.args = {
app_installable: false, app_installable: false,
display_name: "BitBucket", display_name: "BitBucket",
user: { user: {
avatar_url: "", avatar_url: "https://avatars.githubusercontent.com/u/7122116?v=4",
login: "kylecarbs", login: "kylecarbs",
name: "Kyle Carberry", name: "Kyle Carberry",
profile_url: "", profile_url: "",
@ -83,7 +83,7 @@ DeviceAuthenticatedNotInstalled.args = {
app_install_url: "https://example.com", app_install_url: "https://example.com",
app_installable: true, app_installable: true,
user: { user: {
avatar_url: "", avatar_url: "https://avatars.githubusercontent.com/u/7122116?v=4",
login: "kylecarbs", login: "kylecarbs",
name: "Kyle Carberry", name: "Kyle Carberry",
profile_url: "", profile_url: "",
@ -112,7 +112,7 @@ DeviceAuthenticatedInstalled.args = {
app_install_url: "https://example.com", app_install_url: "https://example.com",
app_installable: true, app_installable: true,
user: { user: {
avatar_url: "", avatar_url: "https://avatars.githubusercontent.com/u/7122116?v=4",
login: "kylecarbs", login: "kylecarbs",
name: "Kyle Carberry", name: "Kyle Carberry",
profile_url: "", profile_url: "",

View File

@ -2,7 +2,6 @@ import Tooltip from "@mui/material/Tooltip";
import Link from "@mui/material/Link"; import Link from "@mui/material/Link";
import MonetizationOnOutlined from "@mui/icons-material/MonetizationOnOutlined"; import MonetizationOnOutlined from "@mui/icons-material/MonetizationOnOutlined";
import DeleteOutline from "@mui/icons-material/DeleteOutline"; import DeleteOutline from "@mui/icons-material/DeleteOutline";
import PersonOutline from "@mui/icons-material/PersonOutline";
import ArrowBackOutlined from "@mui/icons-material/ArrowBackOutlined"; import ArrowBackOutlined from "@mui/icons-material/ArrowBackOutlined";
import ScheduleOutlined from "@mui/icons-material/ScheduleOutlined"; import ScheduleOutlined from "@mui/icons-material/ScheduleOutlined";
import { useTheme } from "@emotion/react"; import { useTheme } from "@emotion/react";
@ -33,6 +32,7 @@ import { ExternalAvatar } from "components/Avatar/Avatar";
import { WorkspaceActions } from "./WorkspaceActions/WorkspaceActions"; import { WorkspaceActions } from "./WorkspaceActions/WorkspaceActions";
import { WorkspaceNotifications } from "./WorkspaceNotifications/WorkspaceNotifications"; import { WorkspaceNotifications } from "./WorkspaceNotifications/WorkspaceNotifications";
import { WorkspacePermissions } from "./permissions"; import { WorkspacePermissions } from "./permissions";
import { UserAvatar } from "components/UserAvatar/UserAvatar";
export type WorkspaceError = export type WorkspaceError =
| "getBuildsError" | "getBuildsError"
@ -130,9 +130,11 @@ export const WorkspaceTopbar: FC<WorkspaceProps> = ({
}} }}
> >
<TopbarData> <TopbarData>
<TopbarIcon> <UserAvatar
<PersonOutline /> size="xs"
</TopbarIcon> username={workspace.owner_name}
avatarURL={workspace.owner_avatar_url}
/>
<Tooltip title="Owner"> <Tooltip title="Owner">
<span>{workspace.owner_name}</span> <span>{workspace.owner_name}</span>
</Tooltip> </Tooltip>

View File

@ -897,6 +897,7 @@ export const MockWorkspaceBuild: TypesGen.WorkspaceBuild = {
workspace_name: "test-workspace", workspace_name: "test-workspace",
workspace_owner_id: MockUser.id, workspace_owner_id: MockUser.id,
workspace_owner_name: MockUser.username, workspace_owner_name: MockUser.username,
workspace_owner_avatar_url: MockUser.avatar_url,
workspace_id: "759f1d46-3174-453d-aa60-980a9c1442f3", workspace_id: "759f1d46-3174-453d-aa60-980a9c1442f3",
deadline: "2022-05-17T23:39:00.00Z", deadline: "2022-05-17T23:39:00.00Z",
reason: "initiator", reason: "initiator",
@ -919,6 +920,7 @@ export const MockWorkspaceBuildAutostart: TypesGen.WorkspaceBuild = {
workspace_name: "test-workspace", workspace_name: "test-workspace",
workspace_owner_id: MockUser.id, workspace_owner_id: MockUser.id,
workspace_owner_name: MockUser.username, workspace_owner_name: MockUser.username,
workspace_owner_avatar_url: MockUser.avatar_url,
workspace_id: "759f1d46-3174-453d-aa60-980a9c1442f3", workspace_id: "759f1d46-3174-453d-aa60-980a9c1442f3",
deadline: "2022-05-17T23:39:00.00Z", deadline: "2022-05-17T23:39:00.00Z",
reason: "autostart", reason: "autostart",
@ -941,6 +943,7 @@ export const MockWorkspaceBuildAutostop: TypesGen.WorkspaceBuild = {
workspace_name: "test-workspace", workspace_name: "test-workspace",
workspace_owner_id: MockUser.id, workspace_owner_id: MockUser.id,
workspace_owner_name: MockUser.username, workspace_owner_name: MockUser.username,
workspace_owner_avatar_url: MockUser.avatar_url,
workspace_id: "759f1d46-3174-453d-aa60-980a9c1442f3", workspace_id: "759f1d46-3174-453d-aa60-980a9c1442f3",
deadline: "2022-05-17T23:39:00.00Z", deadline: "2022-05-17T23:39:00.00Z",
reason: "autostop", reason: "autostop",
@ -965,6 +968,7 @@ export const MockFailedWorkspaceBuild = (
workspace_name: "test-workspace", workspace_name: "test-workspace",
workspace_owner_id: MockUser.id, workspace_owner_id: MockUser.id,
workspace_owner_name: MockUser.username, workspace_owner_name: MockUser.username,
workspace_owner_avatar_url: MockUser.avatar_url,
workspace_id: "759f1d46-3174-453d-aa60-980a9c1442f3", workspace_id: "759f1d46-3174-453d-aa60-980a9c1442f3",
deadline: "2022-05-17T23:39:00.00Z", deadline: "2022-05-17T23:39:00.00Z",
reason: "initiator", reason: "initiator",
@ -1010,6 +1014,7 @@ export const MockWorkspace: TypesGen.Workspace = {
owner_id: MockUser.id, owner_id: MockUser.id,
organization_id: MockOrganization.id, organization_id: MockOrganization.id,
owner_name: MockUser.username, owner_name: MockUser.username,
owner_avatar_url: "https://avatars.githubusercontent.com/u/7122116?v=4",
autostart_schedule: MockWorkspaceAutostartEnabled.schedule, autostart_schedule: MockWorkspaceAutostartEnabled.schedule,
ttl_ms: 2 * 60 * 60 * 1000, ttl_ms: 2 * 60 * 60 * 1000,
latest_build: MockWorkspaceBuild, latest_build: MockWorkspaceBuild,