satellite/console/consoleweb/consoleql: include invites in member pages

Project member invitations may now be requested through GraphQL
queries. This is necessary for the satellite frontend to display
invitations in the Team page.

References #5855

Change-Id: Ibc8526ba768fd82c1b1890201004ef0f066df2fc
This commit is contained in:
Jeremy Wharton 2023-06-07 22:30:01 -05:00 committed by Storj Robot
parent 0cbb0ee12e
commit 5b1c22a1e7
6 changed files with 185 additions and 89 deletions

View File

@ -32,13 +32,15 @@ const (
BucketUsageCursorInputType = "bucketUsageCursor"
// BucketUsageType is a graphql type name for bucket usage.
BucketUsageType = "bucketUsage"
// BucketUsagePageType is a field name for bucket usage page.
// BucketUsagePageType is a graphql type name for bucket usage page.
BucketUsagePageType = "bucketUsagePage"
// ProjectMembersPageType is a field name for project members page.
// ProjectMembersPageType is a graphql type name for project members page.
ProjectMembersPageType = "projectMembersPage"
// ProjectMembersAndInvitationsPageType is a graphql type name for a page of project members and invitations.
ProjectMembersAndInvitationsPageType = "projectMembersAndInvitationsPage"
// ProjectMembersCursorInputType is a graphql type name for project members.
ProjectMembersCursorInputType = "projectMembersCursor"
// APIKeysPageType is a field name for api keys page.
// APIKeysPageType is a graphql type name for api keys page.
APIKeysPageType = "apiKeysPage"
// APIKeysCursorInputType is a graphql type name for api keys.
APIKeysCursorInputType = "apiKeysCursor"
@ -54,6 +56,8 @@ const (
FieldDescription = "description"
// FieldMembers is field name for members.
FieldMembers = "members"
// FieldMembersAndInvitations is field name for members and invitations.
FieldMembersAndInvitations = "membersAndInvitations"
// FieldAPIKeys is a field name for api keys.
FieldAPIKeys = "apiKeys"
// FieldUsage is a field name for usage rollup.
@ -84,6 +88,8 @@ const (
FieldProjects = "projects"
// FieldProjectMembers is a field name for project members.
FieldProjectMembers = "projectMembers"
// FieldProjectInvitations is a field name for project member invitations.
FieldProjectInvitations = "projectInvitations"
// CursorArg is an argument name for cursor.
CursorArg = "cursor"
// PageArg ia an argument name for page number.
@ -106,6 +112,48 @@ const (
// graphqlProject creates *graphql.Object type representation of satellite.ProjectInfo.
func graphqlProject(service *console.Service, types *TypeCreator) *graphql.Object {
resolveMembersAndInvites := func(p graphql.ResolveParams, pagingType console.ProjectMembersPagingType) (interface{}, error) {
project, _ := p.Source.(*console.Project)
_, err := console.GetUser(p.Context)
if err != nil {
return nil, err
}
cursor := cursorArgsToProjectMembersCursor(p.Args[CursorArg].(map[string]interface{}))
page, err := service.GetProjectMembers(p.Context, project.ID, cursor, pagingType)
if err != nil {
return nil, err
}
var users []projectMember
for _, member := range page.ProjectMembers {
user, err := service.GetUser(p.Context, member.MemberID)
if err != nil {
return nil, err
}
users = append(users, projectMember{
User: user,
JoinedAt: member.CreatedAt,
})
}
projectMembersPage := projectMembersPage{
ProjectMembers: users,
ProjectInvitations: page.ProjectInvitations,
TotalCount: page.TotalCount,
Offset: page.Offset,
Limit: page.Limit,
Order: int(page.Order),
OrderDirection: int(page.OrderDirection),
Search: page.Search,
CurrentPage: page.CurrentPage,
PageCount: page.PageCount,
}
return projectMembersPage, nil
}
return graphql.NewObject(graphql.ObjectConfig{
Name: ProjectType,
Fields: graphql.Fields{
@ -130,6 +178,7 @@ func graphqlProject(service *console.Service, types *TypeCreator) *graphql.Objec
FieldMemberCount: &graphql.Field{
Type: graphql.Int,
},
// TODO: Remove once the frontend has been updated to select membersAndInvitations.
FieldMembers: &graphql.Field{
Type: types.projectMemberPage,
Args: graphql.FieldConfigArgument{
@ -138,44 +187,18 @@ func graphqlProject(service *console.Service, types *TypeCreator) *graphql.Objec
},
},
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
project, _ := p.Source.(*console.Project)
_, err := console.GetUser(p.Context)
if err != nil {
return nil, err
}
cursor := cursorArgsToProjectMembersCursor(p.Args[CursorArg].(map[string]interface{}))
page, err := service.GetProjectMembers(p.Context, project.ID, cursor)
if err != nil {
return nil, err
}
var users []projectMember
for _, member := range page.ProjectMembers {
user, err := service.GetUser(p.Context, member.MemberID)
if err != nil {
return nil, err
}
users = append(users, projectMember{
User: user,
JoinedAt: member.CreatedAt,
})
}
projectMembersPage := projectMembersPage{
ProjectMembers: users,
TotalCount: page.TotalCount,
Offset: page.Offset,
Limit: page.Limit,
Order: int(page.Order),
OrderDirection: int(page.OrderDirection),
Search: page.Search,
CurrentPage: page.CurrentPage,
PageCount: page.PageCount,
}
return projectMembersPage, nil
return resolveMembersAndInvites(p, console.Members)
},
},
FieldMembersAndInvitations: &graphql.Field{
Type: types.projectMembersAndInvitationsPage,
Args: graphql.FieldConfigArgument{
CursorArg: &graphql.ArgumentConfig{
Type: graphql.NewNonNull(types.projectMembersCursor),
},
},
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
return resolveMembersAndInvites(p, console.MembersAndInvitations)
},
},
FieldAPIKeys: &graphql.Field{

View File

@ -14,6 +14,8 @@ import (
const (
// ProjectMemberType is a graphql type name for project member.
ProjectMemberType = "projectMember"
// ProjectInvitationType is a graphql type name for project member invitation.
ProjectInvitationType = "projectInvitation"
// FieldJoinedAt is a field name for joined at timestamp.
FieldJoinedAt = "joinedAt"
)
@ -38,6 +40,21 @@ func graphqlProjectMember(service *console.Service, types *TypeCreator) *graphql
})
}
// graphqlProjectInvitation creates projectInvitation type.
func graphqlProjectInvitation() *graphql.Object {
return graphql.NewObject(graphql.ObjectConfig{
Name: ProjectInvitationType,
Fields: graphql.Fields{
FieldEmail: &graphql.Field{
Type: graphql.String,
},
FieldCreatedAt: &graphql.Field{
Type: graphql.DateTime,
},
},
})
}
func graphqlProjectMembersCursor() *graphql.InputObject {
return graphql.NewInputObject(graphql.InputObjectConfig{
Name: ProjectMembersCursorInputType,
@ -62,12 +79,40 @@ func graphqlProjectMembersCursor() *graphql.InputObject {
}
func graphqlProjectMembersPage(types *TypeCreator) *graphql.Object {
return graphql.NewObject(graphql.ObjectConfig{
Name: ProjectMembersPageType,
Fields: graphql.Fields{
fields := graphql.Fields{
FieldProjectMembers: &graphql.Field{
Type: graphql.NewList(types.projectMember),
},
}
for k, v := range commonProjectMembersPageFields() {
fields[k] = v
}
return graphql.NewObject(graphql.ObjectConfig{
Name: ProjectMembersPageType,
Fields: fields,
})
}
func graphqlProjectMembersAndInvitationsPage(types *TypeCreator) *graphql.Object {
fields := graphql.Fields{
FieldProjectMembers: &graphql.Field{
Type: graphql.NewList(types.projectMember),
},
FieldProjectInvitations: &graphql.Field{
Type: graphql.NewList(types.projectInvitation),
},
}
for k, v := range commonProjectMembersPageFields() {
fields[k] = v
}
return graphql.NewObject(graphql.ObjectConfig{
Name: ProjectMembersAndInvitationsPageType,
Fields: fields,
})
}
func commonProjectMembersPageFields() graphql.Fields {
return graphql.Fields{
SearchArg: &graphql.Field{
Type: graphql.String,
},
@ -92,8 +137,7 @@ func graphqlProjectMembersPage(types *TypeCreator) *graphql.Object {
FieldTotalCount: &graphql.Field{
Type: graphql.Int,
},
},
})
}
}
// projectMember encapsulates User and joinedAt.
@ -104,6 +148,7 @@ type projectMember struct {
type projectMembersPage struct {
ProjectMembers []projectMember
ProjectInvitations []console.ProjectInvitation
Search string
Limit uint

View File

@ -24,7 +24,9 @@ type TypeCreator struct {
bucketUsage *graphql.Object
bucketUsagePage *graphql.Object
projectMember *graphql.Object
projectInvitation *graphql.Object
projectMemberPage *graphql.Object
projectMembersAndInvitationsPage *graphql.Object
apiKeyPage *graphql.Object
apiKeyInfo *graphql.Object
createAPIKey *graphql.Object
@ -112,11 +114,21 @@ func (c *TypeCreator) Create(log *zap.Logger, service *console.Service, mailServ
return err
}
c.projectInvitation = graphqlProjectInvitation()
if err := c.projectInvitation.Error(); err != nil {
return err
}
c.projectMemberPage = graphqlProjectMembersPage(c)
if err := c.projectMemberPage.Error(); err != nil {
return err
}
c.projectMembersAndInvitationsPage = graphqlProjectMembersAndInvitationsPage(c)
if err := c.projectMembersAndInvitationsPage.Error(); err != nil {
return err
}
c.apiKeyPage = graphqlAPIKeysPage(c)
if err := c.apiKeyPage.Error(); err != nil {
return err

View File

@ -62,6 +62,18 @@ type ProjectMembersPage struct {
TotalCount uint64
}
// ProjectMembersPagingType determines what types of results should be returned
// in a project members page.
type ProjectMembersPagingType int
const (
// Members indicates that only project members should be returned in a project members page.
Members ProjectMembersPagingType = iota
// MembersAndInvitations indicates that project members and project invitations should be
// returned in a project members page.
MembersAndInvitations
)
// ProjectMemberOrder is used for querying project members in specified order.
type ProjectMemberOrder int8

View File

@ -2113,7 +2113,7 @@ func (s *Service) DeleteProjectMembersAndInvitations(ctx context.Context, projec
}
// GetProjectMembers returns ProjectMembers for given Project.
func (s *Service) GetProjectMembers(ctx context.Context, projectID uuid.UUID, cursor ProjectMembersCursor) (pmp *ProjectMembersPage, err error) {
func (s *Service) GetProjectMembers(ctx context.Context, projectID uuid.UUID, cursor ProjectMembersCursor, pagingType ProjectMembersPagingType) (pmp *ProjectMembersPage, err error) {
defer mon.Task()(&ctx)(&err)
user, err := s.getUserAndAuditLog(ctx, "get project members", zap.String("projectID", projectID.String()))
@ -2130,7 +2130,11 @@ func (s *Service) GetProjectMembers(ctx context.Context, projectID uuid.UUID, cu
cursor.Limit = maxLimit
}
if pagingType == MembersAndInvitations {
pmp, err = s.store.ProjectMembers().GetPagedWithInvitationsByProjectID(ctx, projectID, cursor)
} else {
pmp, err = s.store.ProjectMembers().GetPagedByProjectID(ctx, projectID, cursor)
}
if err != nil {
return nil, Error.Wrap(err)
}

View File

@ -289,17 +289,17 @@ func TestService(t *testing.T) {
t.Run("GetProjectMembers", func(t *testing.T) {
// Getting the project members of an own project that one is a part of should work
userPage, err := service.GetProjectMembers(userCtx1, up1Proj.ID, console.ProjectMembersCursor{Page: 1, Limit: 10})
userPage, err := service.GetProjectMembers(userCtx1, up1Proj.ID, console.ProjectMembersCursor{Page: 1, Limit: 10}, console.Members)
require.NoError(t, err)
require.Len(t, userPage.ProjectMembers, 2)
// Getting the project members of a foreign project that one is a part of should work
userPage, err = service.GetProjectMembers(userCtx2, up1Proj.ID, console.ProjectMembersCursor{Page: 1, Limit: 10})
userPage, err = service.GetProjectMembers(userCtx2, up1Proj.ID, console.ProjectMembersCursor{Page: 1, Limit: 10}, console.Members)
require.NoError(t, err)
require.Len(t, userPage.ProjectMembers, 2)
// Getting the project members of a foreign project that one is not a part of should not work
userPage, err = service.GetProjectMembers(userCtx1, up2Proj.ID, console.ProjectMembersCursor{Page: 1, Limit: 10})
userPage, err = service.GetProjectMembers(userCtx1, up2Proj.ID, console.ProjectMembersCursor{Page: 1, Limit: 10}, console.Members)
require.Error(t, err)
require.Nil(t, userPage)
})