Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions app/cli/cmd/organization_member_delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@ import (

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

for _, m := range memberships {
for _, m := range res.Memberships {
if m.ID == membershipID {
return m, nil
}
Expand Down
89 changes: 79 additions & 10 deletions app/cli/cmd/organization_member_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,40 +19,109 @@ import (
"fmt"
"time"

"github.com/chainloop-dev/chainloop/app/cli/cmd/options"
"github.com/chainloop-dev/chainloop/app/cli/internal/action"

"github.com/jedib0t/go-pretty/v6/table"
"github.com/spf13/cobra"
)

func newOrganizationMemberList() *cobra.Command {
var (
paginationOpts = &options.OffsetPaginationOpts{}
name string
email string
role string
)

cmd := &cobra.Command{
Use: "list",
Aliases: []string{"ls"},
Short: "List the members of the current organization",
Example: ` # Let the default pagination apply
chainloop organization member list

# Specify the page and page size
chainloop organization member list --page 2 --limit 10

# Filter by name
chainloop organization member list --name alice

# Filter by email
chainloop organization member list --email alice@example.com

# Filter by role
chainloop organization member list --role admin

# Combine filters and pagination
chainloop organization member list --role admin --page 2 --limit 5
`,
PreRunE: func(_ *cobra.Command, _ []string) error {
if paginationOpts.Page < 1 {
return fmt.Errorf("--page must be greater or equal than 1")
}
if paginationOpts.Limit < 1 {
return fmt.Errorf("--limit must be greater or equal than 1")
}

return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
res, err := action.NewMembershipList(actionOpts).ListMembers(cmd.Context())
opts := &action.ListMembersOpts{}

switch {
case name != "":
opts.Name = &name
case email != "":
opts.Email = &email
case role != "":
opts.Role = &role
}

res, err := action.NewMembershipList(actionOpts).ListMembers(cmd.Context(), paginationOpts.Page, paginationOpts.Limit, opts)
if err != nil {
return err
}

return encodeOutput(res, orgMembershipsTableOutput)
if err := encodeOutput(res, orgMembershipsTableOutput); err != nil {
return err
}

pgResponse := res.PaginationMeta

if pgResponse.TotalPages >= paginationOpts.Page {
inPage := min(paginationOpts.Limit, len(res.Memberships))
lowerBound := (paginationOpts.Page - 1) * paginationOpts.Limit
logger.Info().Msg(fmt.Sprintf("Showing [%d-%d] out of %d", lowerBound+1, lowerBound+inPage, pgResponse.TotalCount))
}

if pgResponse.TotalCount > pgResponse.Page*pgResponse.PageSize {
logger.Info().Msg(fmt.Sprintf("Next page available: %d", pgResponse.Page+1))
}

return nil
},
}

cmd.Flags().StringVar(&name, "name", "", "Filter by member name or last name")
cmd.Flags().StringVar(&email, "email", "", "Filter by member email")
cmd.Flags().StringVar(&role, "role", "", fmt.Sprintf("Role of the user in the organization, available %s", action.AvailableRoles[:3]))
paginationOpts.AddFlags(cmd)

return cmd
}

func orgMembershipsTableOutput(items []*action.MembershipItem) error {
if len(items) == 0 {
fmt.Println(UserWithNoOrganizationMsg)
return nil
}

func orgMembershipsTableOutput(res *action.ListMembershipResult) error {
t := newTableWriter()
t.AppendHeader(table.Row{"ID", "Email", "Role", "Joined At"})

for _, i := range items {
t.AppendRow(table.Row{i.ID, i.User.PrintUserProfileWithEmail(), i.Role, i.CreatedAt.Format(time.RFC822)})
for _, m := range res.Memberships {
t.AppendRow(table.Row{
m.ID,
m.User.PrintUserProfileWithEmail(),
m.Role,
m.CreatedAt.Format(time.RFC822),
})
t.AppendSeparator()
}

Expand Down
2 changes: 1 addition & 1 deletion app/cli/cmd/organization_member_update.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ func newOrganizationMemberUpdateCmd() *cobra.Command {
return err
}

return encodeOutput([]*action.MembershipItem{res}, orgMembershipsTableOutput)
return encodeOutput(&action.ListMembershipResult{Memberships: []*action.MembershipItem{res}}, orgMembershipsTableOutput)
},
}

Expand Down
3 changes: 2 additions & 1 deletion app/cli/cmd/output.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ type tabulatedData interface {
[]*action.OrgInvitationItem |
*action.APITokenItem |
[]*action.APITokenItem |
*action.AttestationStatusMaterial
*action.AttestationStatusMaterial |
*action.ListMembershipResult
}

var ErrOutputFormatNotImplemented = errors.New("format not implemented")
Expand Down
30 changes: 29 additions & 1 deletion app/cli/documentation/cli-reference.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2573,10 +2573,38 @@ List the members of the current organization
chainloop organization member list [flags]
```

Examples

```
Let the default pagination apply
chainloop organization member list

Specify the page and page size
chainloop organization member list --page 2 --limit 10

Filter by name
chainloop organization member list --name alice

Filter by email
chainloop organization member list --email alice@example.com

Filter by role
chainloop organization member list --role admin

Combine filters and pagination
chainloop organization member list --role admin --page 2 --limit 5

```

Options

```
-h, --help help for list
--email string Filter by member email
-h, --help help for list
--limit int number of items to show (default 50)
--name string Filter by member name or last name
--page int page number (default 1)
--role string Role of the user in the organization, available admin, owner, viewer
```

Options inherited from parent commands
Expand Down
56 changes: 52 additions & 4 deletions app/cli/internal/action/membership_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package action

import (
"context"
"fmt"
"strings"
"time"

Expand All @@ -43,6 +44,22 @@ type MembershipItem struct {
Role Role `json:"role"`
}

type ListMembersOpts struct {
// MembershipID Optional, if provided, filters by a specific membership ID
MembershipID *string
// Name is the name of the user to filter by
Name *string
// Email is the email of the user to filter by
Email *string
// Role is the role of the user to filter by
Role *string
}

type ListMembershipResult struct {
Memberships []*MembershipItem
PaginationMeta *OffsetPagination
}

func NewMembershipList(cfg *ActionsOpts) *MembershipList {
return &MembershipList{cfg}
}
Expand All @@ -63,10 +80,33 @@ func (action *MembershipList) ListOrgs(ctx context.Context) ([]*MembershipItem,
return result, nil
}

// List members of the current organization
func (action *MembershipList) ListMembers(ctx context.Context) ([]*MembershipItem, error) {
// ListMembers lists the members of an organization with pagination and optional filters.
func (action *MembershipList) ListMembers(ctx context.Context, page int, pageSize int, opts *ListMembersOpts) (*ListMembershipResult, error) {
if page < 1 {
return nil, fmt.Errorf("page must be greater or equal to 1")
}
if pageSize < 1 {
return nil, fmt.Errorf("page-size must be greater or equal to 1")
}

client := pb.NewOrganizationServiceClient(action.cfg.CPConnection)
resp, err := client.ListMemberships(ctx, &pb.OrganizationServiceListMembershipsRequest{})
req := &pb.OrganizationServiceListMembershipsRequest{
MembershipId: opts.MembershipID,
Name: opts.Name,
Email: opts.Email,
Pagination: &pb.OffsetPaginationRequest{
Page: int32(page),
PageSize: int32(pageSize),
},
}

// If a role is specified, convert it to the protobuf enum
if opts.Role != nil {
casted := stringToPbRole(Role(*opts.Role))
req.Role = &casted
}

resp, err := client.ListMemberships(ctx, req)
if err != nil {
return nil, err
}
Expand All @@ -76,7 +116,15 @@ func (action *MembershipList) ListMembers(ctx context.Context) ([]*MembershipIte
result = append(result, pbMembershipItemToAction(p))
}

return result, nil
return &ListMembershipResult{
Memberships: result,
PaginationMeta: &OffsetPagination{
Page: int(resp.GetPagination().GetPage()),
PageSize: int(resp.GetPagination().GetPageSize()),
TotalPages: int(resp.GetPagination().GetTotalPages()),
TotalCount: int(resp.GetPagination().GetTotalCount()),
},
}, nil
}

func pbOrgItemToAction(in *pb.OrgItem) *OrgItem {
Expand Down
Loading
Loading