Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commitdcab6fa

Browse files
feat(site): display user avatar (#11893)
* add owner API to workspace and workspace build responses* display user avatar in workspace top barCo-authored-by: Cian Johnston <cian@coder.com>
1 parent83eea2d commitdcab6fa

18 files changed

+216
-105
lines changed

‎cli/testdata/coder_list_--output_json.golden‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"updated_at": "[timestamp]",
66
"owner_id": "[first user ID]",
77
"owner_name": "testuser",
8+
"owner_avatar_url": "",
89
"organization_id": "[first org ID]",
910
"template_id": "[template ID]",
1011
"template_name": "test-template",
@@ -21,6 +22,7 @@
2122
"workspace_name": "test-workspace",
2223
"workspace_owner_id": "[first user ID]",
2324
"workspace_owner_name": "testuser",
25+
"workspace_owner_avatar_url": "",
2426
"template_version_id": "[version ID]",
2527
"template_version_name": "[version name]",
2628
"build_number": 1,

‎coderd/apidoc/docs.go‎

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more aboutcustomizing how changed files appear on GitHub.

‎coderd/apidoc/swagger.json‎

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more aboutcustomizing how changed files appear on GitHub.

‎coderd/httpmw/organizationparam.go‎

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -63,12 +63,13 @@ func ExtractOrganizationParam(db database.Store) func(http.Handler) http.Handler
6363
}
6464
}
6565

66-
// OrganizationMember is the database object plus the Username. Including the Username in this
67-
// middleware is preferable to a join at the SQL layer so that we can keep the autogenerated
68-
// database types as they are.
66+
// OrganizationMember is the database object plus the Username and Avatar URL. Including these
67+
//in themiddleware is preferable to a join at the SQL layer so that we can keep the
68+
//autogenerateddatabase types as they are.
6969
typeOrganizationMemberstruct {
7070
database.OrganizationMember
71-
Usernamestring
71+
Usernamestring
72+
AvatarURLstring
7273
}
7374

7475
// 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
107108

108109
ctx=context.WithValue(ctx,organizationMemberParamContextKey{},OrganizationMember{
109110
OrganizationMember:organizationMember,
110-
// Here we're making one exception to the rule about not leaking data about the user
111-
// to the API handler, which is to include the username. If the caller has permission
112-
// to read the OrganizationMember, then we're explicitly saying here that they also
113-
// have permission to see the member's username, which is itself uncontroversial.
111+
// Here we're making two exceptions to the rule about not leaking data about the user
112+
// to the API handler, which is to include the username and avatar URL.
113+
// If the caller has permission to read the OrganizationMember, then we're explicitly
114+
// saying here that they also have permission to see the member's username and avatar.
115+
// This is OK!
114116
//
115117
// API handlers need this information for audit logging and returning the owner's
116-
// username in response to creating a workspace.
117-
Username:user.Username,
118+
// username in response to creating a workspace. Additionally, the frontend consumes
119+
// the Avatar URL and this allows the FE to avoid an extra request.
120+
Username:user.Username,
121+
AvatarURL:user.AvatarURL,
118122
})
119123
next.ServeHTTP(rw,r.WithContext(ctx))
120124
})

‎coderd/httpmw/organizationparam_test.go‎

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,17 @@ import (
88

99
"github.com/go-chi/chi/v5"
1010
"github.com/google/uuid"
11+
"github.com/stretchr/testify/assert"
1112
"github.com/stretchr/testify/require"
1213

1314
"github.com/coder/coder/v2/coderd/database"
1415
"github.com/coder/coder/v2/coderd/database/dbgen"
1516
"github.com/coder/coder/v2/coderd/database/dbmem"
1617
"github.com/coder/coder/v2/coderd/database/dbtime"
1718
"github.com/coder/coder/v2/coderd/httpmw"
19+
"github.com/coder/coder/v2/coderd/rbac"
1820
"github.com/coder/coder/v2/codersdk"
21+
"github.com/coder/coder/v2/testutil"
1922
)
2023

2124
funcTestOrganizationParam(t*testing.T) {
@@ -139,6 +142,7 @@ func TestOrganizationParam(t *testing.T) {
139142
t.Run("Success",func(t*testing.T) {
140143
t.Parallel()
141144
var (
145+
ctx=testutil.Context(t,testutil.WaitShort)
142146
db=dbmem.New()
143147
rw=httptest.NewRecorder()
144148
r,user=setupAuthentication(db)
@@ -148,7 +152,14 @@ func TestOrganizationParam(t *testing.T) {
148152
_=dbgen.OrganizationMember(t,db, database.OrganizationMember{
149153
OrganizationID:organization.ID,
150154
UserID:user.ID,
155+
Roles: []string{rbac.RoleOrgMember(organization.ID)},
151156
})
157+
_,err:=db.UpdateUserRoles(ctx, database.UpdateUserRolesParams{
158+
ID:user.ID,
159+
GrantedRoles: []string{rbac.RoleTemplateAdmin()},
160+
})
161+
require.NoError(t,err)
162+
152163
chi.RouteContext(r.Context()).URLParams.Add("organization",organization.ID.String())
153164
chi.RouteContext(r.Context()).URLParams.Add("user",user.ID.String())
154165
rtr.Use(
@@ -161,9 +172,27 @@ func TestOrganizationParam(t *testing.T) {
161172
httpmw.ExtractOrganizationMemberParam(db),
162173
)
163174
rtr.Get("/",func(rw http.ResponseWriter,r*http.Request) {
164-
_=httpmw.OrganizationParam(r)
165-
_=httpmw.OrganizationMemberParam(r)
175+
org:=httpmw.OrganizationParam(r)
176+
assert.NotZero(t,org)
177+
assert.NotZero(t,org.CreatedAt)
178+
// assert.NotZero(t, org.Description) // not supported
179+
assert.NotZero(t,org.ID)
180+
assert.NotEmpty(t,org.Name)
181+
orgMem:=httpmw.OrganizationMemberParam(r)
166182
rw.WriteHeader(http.StatusOK)
183+
assert.NotZero(t,orgMem)
184+
assert.NotZero(t,orgMem.CreatedAt)
185+
assert.NotZero(t,orgMem.UpdatedAt)
186+
assert.Equal(t,org.ID,orgMem.OrganizationID)
187+
assert.Equal(t,user.ID,orgMem.UserID)
188+
assert.Equal(t,user.Username,orgMem.Username)
189+
assert.Equal(t,user.AvatarURL,orgMem.AvatarURL)
190+
assert.NotEmpty(t,orgMem.Roles)
191+
assert.NotZero(t,orgMem.OrganizationMember)
192+
assert.NotEmpty(t,orgMem.OrganizationMember.CreatedAt)
193+
assert.NotEmpty(t,orgMem.OrganizationMember.UpdatedAt)
194+
assert.NotEmpty(t,orgMem.OrganizationMember.UserID)
195+
assert.NotEmpty(t,orgMem.OrganizationMember.Roles)
167196
})
168197
rtr.ServeHTTP(rw,r)
169198
res:=rw.Result()

‎coderd/users.go‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1271,13 +1271,13 @@ func userOrganizationIDs(ctx context.Context, api *API, user database.User) ([]u
12711271
returnmember.OrganizationIDs,nil
12721272
}
12731273

1274-
funcusernameWithID(id uuid.UUID,users []database.User) (string,bool) {
1274+
funcuserByID(id uuid.UUID,users []database.User) (database.User,bool) {
12751275
for_,user:=rangeusers {
12761276
ifid==user.ID {
1277-
returnuser.Username,true
1277+
returnuser,true
12781278
}
12791279
}
1280-
return"",false
1280+
returndatabase.User{},false
12811281
}
12821282

12831283
funcconvertAPIKey(k database.APIKey) codersdk.APIKey {

‎coderd/workspacebuilds.go‎

Lines changed: 35 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ func (api *API) workspaceBuild(rw http.ResponseWriter, r *http.Request) {
6969
})
7070
return
7171
}
72-
ownerName,ok:=usernameWithID(workspace.OwnerID,data.users)
72+
owner,ok:=userByID(workspace.OwnerID,data.users)
7373
if!ok {
7474
httpapi.Write(ctx,rw,http.StatusInternalServerError, codersdk.Response{
7575
Message:"Internal error converting workspace build.",
@@ -82,7 +82,8 @@ func (api *API) workspaceBuild(rw http.ResponseWriter, r *http.Request) {
8282
workspaceBuild,
8383
workspace,
8484
data.jobs[0],
85-
ownerName,
85+
owner.Username,
86+
owner.AvatarURL,
8687
data.resources,
8788
data.metadata,
8889
data.agents,
@@ -283,7 +284,7 @@ func (api *API) workspaceBuildByBuildNumber(rw http.ResponseWriter, r *http.Requ
283284
})
284285
return
285286
}
286-
ownerName,ok:=usernameWithID(workspace.OwnerID,data.users)
287+
owner,ok:=userByID(workspace.OwnerID,data.users)
287288
if!ok {
288289
httpapi.Write(ctx,rw,http.StatusInternalServerError, codersdk.Response{
289290
Message:"Internal error converting workspace build.",
@@ -296,7 +297,8 @@ func (api *API) workspaceBuildByBuildNumber(rw http.ResponseWriter, r *http.Requ
296297
workspaceBuild,
297298
workspace,
298299
data.jobs[0],
299-
ownerName,
300+
owner.Username,
301+
owner.AvatarURL,
300302
data.resources,
301303
data.metadata,
302304
data.agents,
@@ -416,7 +418,7 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
416418
})
417419
return
418420
}
419-
ownerName,exists:=usernameWithID(workspace.OwnerID,users)
421+
owner,exists:=userByID(workspace.OwnerID,users)
420422
if!exists {
421423
httpapi.Write(ctx,rw,http.StatusInternalServerError, codersdk.Response{
422424
Message:"Internal error converting workspace build.",
@@ -432,7 +434,8 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
432434
ProvisionerJob:*provisionerJob,
433435
QueuePosition:0,
434436
},
435-
ownerName,
437+
owner.Username,
438+
owner.AvatarURL,
436439
[]database.WorkspaceResource{},
437440
[]database.WorkspaceResourceMetadatum{},
438441
[]database.WorkspaceAgent{},
@@ -833,7 +836,7 @@ func (api *API) convertWorkspaceBuilds(
833836
if!exists {
834837
returnnil,xerrors.New("template version not found")
835838
}
836-
ownerName,exists:=usernameWithID(workspace.OwnerID,users)
839+
owner,exists:=userByID(workspace.OwnerID,users)
837840
if!exists {
838841
returnnil,xerrors.Errorf("owner not found for workspace: %q",workspace.Name)
839842
}
@@ -842,7 +845,8 @@ func (api *API) convertWorkspaceBuilds(
842845
build,
843846
workspace,
844847
job,
845-
ownerName,
848+
owner.Username,
849+
owner.AvatarURL,
846850
workspaceResources,
847851
resourceMetadata,
848852
resourceAgents,
@@ -865,7 +869,7 @@ func (api *API) convertWorkspaceBuild(
865869
build database.WorkspaceBuild,
866870
workspace database.Workspace,
867871
job database.GetProvisionerJobsByIDsWithQueuePositionRow,
868-
ownerNamestring,
872+
username,avatarURLstring,
869873
workspaceResources []database.WorkspaceResource,
870874
resourceMetadata []database.WorkspaceResourceMetadatum,
871875
resourceAgents []database.WorkspaceAgent,
@@ -909,7 +913,7 @@ func (api *API) convertWorkspaceBuild(
909913
scripts:=scriptsByAgentID[agent.ID]
910914
logSources:=logSourcesByAgentID[agent.ID]
911915
apiAgent,err:=db2sdk.WorkspaceAgent(
912-
api.DERPMap(),*api.TailnetCoordinator.Load(),agent,db2sdk.Apps(apps,agent,ownerName,workspace),convertScripts(scripts),convertLogSources(logSources),api.AgentInactiveDisconnectTimeout,
916+
api.DERPMap(),*api.TailnetCoordinator.Load(),agent,db2sdk.Apps(apps,agent,username,workspace),convertScripts(scripts),convertLogSources(logSources),api.AgentInactiveDisconnectTimeout,
913917
api.DeploymentValues.AgentFallbackTroubleshootingURL.String(),
914918
)
915919
iferr!=nil {
@@ -923,26 +927,27 @@ func (api *API) convertWorkspaceBuild(
923927
apiJob:=convertProvisionerJob(job)
924928
transition:=codersdk.WorkspaceTransition(build.Transition)
925929
return codersdk.WorkspaceBuild{
926-
ID:build.ID,
927-
CreatedAt:build.CreatedAt,
928-
UpdatedAt:build.UpdatedAt,
929-
WorkspaceOwnerID:workspace.OwnerID,
930-
WorkspaceOwnerName:ownerName,
931-
WorkspaceID:build.WorkspaceID,
932-
WorkspaceName:workspace.Name,
933-
TemplateVersionID:build.TemplateVersionID,
934-
TemplateVersionName:templateVersion.Name,
935-
BuildNumber:build.BuildNumber,
936-
Transition:transition,
937-
InitiatorID:build.InitiatorID,
938-
InitiatorUsername:build.InitiatorByUsername,
939-
Job:apiJob,
940-
Deadline:codersdk.NewNullTime(build.Deadline,!build.Deadline.IsZero()),
941-
MaxDeadline:codersdk.NewNullTime(build.MaxDeadline,!build.MaxDeadline.IsZero()),
942-
Reason:codersdk.BuildReason(build.Reason),
943-
Resources:apiResources,
944-
Status:convertWorkspaceStatus(apiJob.Status,transition),
945-
DailyCost:build.DailyCost,
930+
ID:build.ID,
931+
CreatedAt:build.CreatedAt,
932+
UpdatedAt:build.UpdatedAt,
933+
WorkspaceOwnerID:workspace.OwnerID,
934+
WorkspaceOwnerName:username,
935+
WorkspaceOwnerAvatarURL:avatarURL,
936+
WorkspaceID:build.WorkspaceID,
937+
WorkspaceName:workspace.Name,
938+
TemplateVersionID:build.TemplateVersionID,
939+
TemplateVersionName:templateVersion.Name,
940+
BuildNumber:build.BuildNumber,
941+
Transition:transition,
942+
InitiatorID:build.InitiatorID,
943+
InitiatorUsername:build.InitiatorByUsername,
944+
Job:apiJob,
945+
Deadline:codersdk.NewNullTime(build.Deadline,!build.Deadline.IsZero()),
946+
MaxDeadline:codersdk.NewNullTime(build.MaxDeadline,!build.MaxDeadline.IsZero()),
947+
Reason:codersdk.BuildReason(build.Reason),
948+
Resources:apiResources,
949+
Status:convertWorkspaceStatus(apiJob.Status,transition),
950+
DailyCost:build.DailyCost,
946951
},nil
947952
}
948953

‎coderd/workspacebuilds_test.go‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"github.com/coder/coder/v2/coderd/audit"
2222
"github.com/coder/coder/v2/coderd/coderdtest"
2323
"github.com/coder/coder/v2/coderd/database"
24+
"github.com/coder/coder/v2/coderd/database/dbauthz"
2425
"github.com/coder/coder/v2/coderd/database/dbtime"
2526
"github.com/coder/coder/v2/coderd/rbac"
2627
"github.com/coder/coder/v2/codersdk"
@@ -37,12 +38,23 @@ func TestWorkspaceBuild(t *testing.T) {
3738
propagation.Baggage{},
3839
),
3940
)
41+
ctx:=testutil.Context(t,testutil.WaitShort)
4042
auditor:=audit.NewMock()
41-
client:=coderdtest.New(t,&coderdtest.Options{
43+
client,db:=coderdtest.NewWithDatabase(t,&coderdtest.Options{
4244
IncludeProvisionerDaemon:true,
4345
Auditor:auditor,
4446
})
4547
user:=coderdtest.CreateFirstUser(t,client)
48+
//nolint:gocritic // testing
49+
up,err:=db.UpdateUserProfile(dbauthz.AsSystemRestricted(ctx), database.UpdateUserProfileParams{
50+
ID:user.UserID,
51+
Email:coderdtest.FirstUserParams.Email,
52+
Username:coderdtest.FirstUserParams.Username,
53+
Name:"Admin",
54+
AvatarURL:client.URL.String(),
55+
UpdatedAt:dbtime.Now(),
56+
})
57+
require.NoError(t,err)
4658
version:=coderdtest.CreateTemplateVersion(t,client,user.OrganizationID,nil)
4759
template:=coderdtest.CreateTemplate(t,client,user.OrganizationID,version.ID)
4860
coderdtest.AwaitTemplateVersionJobCompleted(t,client,version.ID)
@@ -57,6 +69,10 @@ func TestWorkspaceBuild(t *testing.T) {
5769
assert.Equal(t,logs[0].Ip.IPNet.IP.String(),"127.0.0.1")&&
5870
assert.Equal(t,logs[1].Ip.IPNet.IP.String(),"127.0.0.1")
5971
},testutil.WaitShort,testutil.IntervalFast)
72+
wb,err:=client.WorkspaceBuild(testutil.Context(t,testutil.WaitShort),workspace.LatestBuild.ID)
73+
require.NoError(t,err)
74+
require.Equal(t,up.Username,wb.WorkspaceOwnerName)
75+
require.Equal(t,up.AvatarURL,wb.WorkspaceOwnerAvatarURL)
6076
}
6177

6278
funcTestWorkspaceBuildByBuildNumber(t*testing.T) {

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp