Skip to content

Commit 17dbde7

Browse files
authored
feat(org): Introduce pagination and filtering on ListMemberships (#2265)
Signed-off-by: Javier Rodriguez <[email protected]>
1 parent 04ddb1b commit 17dbde7

18 files changed

+1115
-185
lines changed

app/cli/cmd/organization_member_delete.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,12 @@ import (
2525

2626
// Get the membership entry associated to the current user for the given organization
2727
func loadMembershipCurrentOrg(ctx context.Context, membershipID string) (*action.MembershipItem, error) {
28-
memberships, err := action.NewMembershipList(actionOpts).ListMembers(ctx)
28+
res, err := action.NewMembershipList(actionOpts).ListMembers(ctx, 1, 1, &action.ListMembersOpts{MembershipID: &membershipID})
2929
if err != nil {
3030
return nil, fmt.Errorf("listing memberships: %w", err)
3131
}
3232

33-
for _, m := range memberships {
33+
for _, m := range res.Memberships {
3434
if m.ID == membershipID {
3535
return m, nil
3636
}

app/cli/cmd/organization_member_list.go

Lines changed: 79 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,40 +19,109 @@ import (
1919
"fmt"
2020
"time"
2121

22+
"github.com/chainloop-dev/chainloop/app/cli/cmd/options"
2223
"github.com/chainloop-dev/chainloop/app/cli/internal/action"
24+
2325
"github.com/jedib0t/go-pretty/v6/table"
2426
"github.com/spf13/cobra"
2527
)
2628

2729
func newOrganizationMemberList() *cobra.Command {
30+
var (
31+
paginationOpts = &options.OffsetPaginationOpts{}
32+
name string
33+
email string
34+
role string
35+
)
36+
2837
cmd := &cobra.Command{
2938
Use: "list",
3039
Aliases: []string{"ls"},
3140
Short: "List the members of the current organization",
41+
Example: ` # Let the default pagination apply
42+
chainloop organization member list
43+
44+
# Specify the page and page size
45+
chainloop organization member list --page 2 --limit 10
46+
47+
# Filter by name
48+
chainloop organization member list --name alice
49+
50+
# Filter by email
51+
chainloop organization member list --email [email protected]
52+
53+
# Filter by role
54+
chainloop organization member list --role admin
55+
56+
# Combine filters and pagination
57+
chainloop organization member list --role admin --page 2 --limit 5
58+
`,
59+
PreRunE: func(_ *cobra.Command, _ []string) error {
60+
if paginationOpts.Page < 1 {
61+
return fmt.Errorf("--page must be greater or equal than 1")
62+
}
63+
if paginationOpts.Limit < 1 {
64+
return fmt.Errorf("--limit must be greater or equal than 1")
65+
}
66+
67+
return nil
68+
},
3269
RunE: func(cmd *cobra.Command, args []string) error {
33-
res, err := action.NewMembershipList(actionOpts).ListMembers(cmd.Context())
70+
opts := &action.ListMembersOpts{}
71+
72+
switch {
73+
case name != "":
74+
opts.Name = &name
75+
case email != "":
76+
opts.Email = &email
77+
case role != "":
78+
opts.Role = &role
79+
}
80+
81+
res, err := action.NewMembershipList(actionOpts).ListMembers(cmd.Context(), paginationOpts.Page, paginationOpts.Limit, opts)
3482
if err != nil {
3583
return err
3684
}
3785

38-
return encodeOutput(res, orgMembershipsTableOutput)
86+
if err := encodeOutput(res, orgMembershipsTableOutput); err != nil {
87+
return err
88+
}
89+
90+
pgResponse := res.PaginationMeta
91+
92+
if pgResponse.TotalPages >= paginationOpts.Page {
93+
inPage := min(paginationOpts.Limit, len(res.Memberships))
94+
lowerBound := (paginationOpts.Page - 1) * paginationOpts.Limit
95+
logger.Info().Msg(fmt.Sprintf("Showing [%d-%d] out of %d", lowerBound+1, lowerBound+inPage, pgResponse.TotalCount))
96+
}
97+
98+
if pgResponse.TotalCount > pgResponse.Page*pgResponse.PageSize {
99+
logger.Info().Msg(fmt.Sprintf("Next page available: %d", pgResponse.Page+1))
100+
}
101+
102+
return nil
39103
},
40104
}
41105

106+
cmd.Flags().StringVar(&name, "name", "", "Filter by member name or last name")
107+
cmd.Flags().StringVar(&email, "email", "", "Filter by member email")
108+
cmd.Flags().StringVar(&role, "role", "", fmt.Sprintf("Role of the user in the organization, available %s", action.AvailableRoles[:3]))
109+
paginationOpts.AddFlags(cmd)
110+
42111
return cmd
43112
}
44113

45-
func orgMembershipsTableOutput(items []*action.MembershipItem) error {
46-
if len(items) == 0 {
47-
fmt.Println(UserWithNoOrganizationMsg)
48-
return nil
49-
}
50-
114+
func orgMembershipsTableOutput(res *action.ListMembershipResult) error {
51115
t := newTableWriter()
52116
t.AppendHeader(table.Row{"ID", "Email", "Role", "Joined At"})
53117

54-
for _, i := range items {
55-
t.AppendRow(table.Row{i.ID, i.User.PrintUserProfileWithEmail(), i.Role, i.CreatedAt.Format(time.RFC822)})
118+
for _, m := range res.Memberships {
119+
t.AppendRow(table.Row{
120+
m.ID,
121+
m.User.PrintUserProfileWithEmail(),
122+
m.Role,
123+
m.CreatedAt.Format(time.RFC822),
124+
})
56125
t.AppendSeparator()
57126
}
58127

app/cli/cmd/organization_member_update.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ func newOrganizationMemberUpdateCmd() *cobra.Command {
4646
return err
4747
}
4848

49-
return encodeOutput([]*action.MembershipItem{res}, orgMembershipsTableOutput)
49+
return encodeOutput(&action.ListMembershipResult{Memberships: []*action.MembershipItem{res}}, orgMembershipsTableOutput)
5050
},
5151
}
5252

app/cli/cmd/output.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ type tabulatedData interface {
5353
[]*action.OrgInvitationItem |
5454
*action.APITokenItem |
5555
[]*action.APITokenItem |
56-
*action.AttestationStatusMaterial
56+
*action.AttestationStatusMaterial |
57+
*action.ListMembershipResult
5758
}
5859

5960
var ErrOutputFormatNotImplemented = errors.New("format not implemented")

app/cli/documentation/cli-reference.mdx

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2573,10 +2573,38 @@ List the members of the current organization
25732573
chainloop organization member list [flags]
25742574
```
25752575

2576+
Examples
2577+
2578+
```
2579+
Let the default pagination apply
2580+
chainloop organization member list
2581+
2582+
Specify the page and page size
2583+
chainloop organization member list --page 2 --limit 10
2584+
2585+
Filter by name
2586+
chainloop organization member list --name alice
2587+
2588+
Filter by email
2589+
chainloop organization member list --email [email protected]
2590+
2591+
Filter by role
2592+
chainloop organization member list --role admin
2593+
2594+
Combine filters and pagination
2595+
chainloop organization member list --role admin --page 2 --limit 5
2596+
2597+
```
2598+
25762599
Options
25772600

25782601
```
2579-
-h, --help help for list
2602+
--email string Filter by member email
2603+
-h, --help help for list
2604+
--limit int number of items to show (default 50)
2605+
--name string Filter by member name or last name
2606+
--page int page number (default 1)
2607+
--role string Role of the user in the organization, available admin, owner, viewer
25802608
```
25812609

25822610
Options inherited from parent commands

app/cli/internal/action/membership_list.go

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ package action
1717

1818
import (
1919
"context"
20+
"fmt"
2021
"strings"
2122
"time"
2223

@@ -43,6 +44,22 @@ type MembershipItem struct {
4344
Role Role `json:"role"`
4445
}
4546

47+
type ListMembersOpts struct {
48+
// MembershipID Optional, if provided, filters by a specific membership ID
49+
MembershipID *string
50+
// Name is the name of the user to filter by
51+
Name *string
52+
// Email is the email of the user to filter by
53+
Email *string
54+
// Role is the role of the user to filter by
55+
Role *string
56+
}
57+
58+
type ListMembershipResult struct {
59+
Memberships []*MembershipItem
60+
PaginationMeta *OffsetPagination
61+
}
62+
4663
func NewMembershipList(cfg *ActionsOpts) *MembershipList {
4764
return &MembershipList{cfg}
4865
}
@@ -63,10 +80,33 @@ func (action *MembershipList) ListOrgs(ctx context.Context) ([]*MembershipItem,
6380
return result, nil
6481
}
6582

66-
// List members of the current organization
67-
func (action *MembershipList) ListMembers(ctx context.Context) ([]*MembershipItem, error) {
83+
// ListMembers lists the members of an organization with pagination and optional filters.
84+
func (action *MembershipList) ListMembers(ctx context.Context, page int, pageSize int, opts *ListMembersOpts) (*ListMembershipResult, error) {
85+
if page < 1 {
86+
return nil, fmt.Errorf("page must be greater or equal to 1")
87+
}
88+
if pageSize < 1 {
89+
return nil, fmt.Errorf("page-size must be greater or equal to 1")
90+
}
91+
6892
client := pb.NewOrganizationServiceClient(action.cfg.CPConnection)
69-
resp, err := client.ListMemberships(ctx, &pb.OrganizationServiceListMembershipsRequest{})
93+
req := &pb.OrganizationServiceListMembershipsRequest{
94+
MembershipId: opts.MembershipID,
95+
Name: opts.Name,
96+
Email: opts.Email,
97+
Pagination: &pb.OffsetPaginationRequest{
98+
Page: int32(page),
99+
PageSize: int32(pageSize),
100+
},
101+
}
102+
103+
// If a role is specified, convert it to the protobuf enum
104+
if opts.Role != nil {
105+
casted := stringToPbRole(Role(*opts.Role))
106+
req.Role = &casted
107+
}
108+
109+
resp, err := client.ListMemberships(ctx, req)
70110
if err != nil {
71111
return nil, err
72112
}
@@ -76,7 +116,15 @@ func (action *MembershipList) ListMembers(ctx context.Context) ([]*MembershipIte
76116
result = append(result, pbMembershipItemToAction(p))
77117
}
78118

79-
return result, nil
119+
return &ListMembershipResult{
120+
Memberships: result,
121+
PaginationMeta: &OffsetPagination{
122+
Page: int(resp.GetPagination().GetPage()),
123+
PageSize: int(resp.GetPagination().GetPageSize()),
124+
TotalPages: int(resp.GetPagination().GetTotalPages()),
125+
TotalCount: int(resp.GetPagination().GetTotalCount()),
126+
},
127+
}, nil
80128
}
81129

82130
func pbOrgItemToAction(in *pb.OrgItem) *OrgItem {

0 commit comments

Comments
 (0)