From d131bbe58071424bea0696ad86dcd15a6c2c98d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Rodr=C3=ADguez?= Date: Thu, 14 Nov 2024 09:55:19 +0100 Subject: [PATCH] feat(cli): Introduce workflow pagination on CLI (#1514) Signed-off-by: Javier Rodriguez --- app/cli/cmd/output.go | 1 + app/cli/cmd/workflow_list.go | 64 +++++++++++++++++++++--- app/cli/cmd/workflow_update.go | 2 +- app/cli/internal/action/action.go | 7 +++ app/cli/internal/action/workflow_list.go | 54 ++++++++++++++++---- 5 files changed, 110 insertions(+), 18 deletions(-) diff --git a/app/cli/cmd/output.go b/app/cli/cmd/output.go index 4679bd292..9b7abef82 100644 --- a/app/cli/cmd/output.go +++ b/app/cli/cmd/output.go @@ -33,6 +33,7 @@ const formatTable = "table" type tabulatedData interface { []*action.WorkflowItem | *action.WorkflowItem | + *action.WorkflowListResult | *action.AttestationStatusResult | []*action.WorkflowRunItem | *action.WorkflowRunItemFull | diff --git a/app/cli/cmd/workflow_list.go b/app/cli/cmd/workflow_list.go index 83a0c0ed6..e3ca66a89 100644 --- a/app/cli/cmd/workflow_list.go +++ b/app/cli/cmd/workflow_list.go @@ -24,32 +24,82 @@ import ( "github.com/spf13/cobra" ) +const ( + // defaultPageSize is the default page size + defaultPageSize = 15 + // defaultPage is the default page + defaultPage = 1 +) + +var ( + // page is the current page number + page int + // pageSize is the number of workflows per page + pageSize int +) + func newWorkflowListCmd() *cobra.Command { cmd := &cobra.Command{ Use: "list", Aliases: []string{"ls"}, Short: "List existing Workflows", - RunE: func(cmd *cobra.Command, args []string) error { - res, err := action.NewWorkflowList(actionOpts).Run() + Example: ` # Let the default pagination apply + chainloop workflow list + + # Specify the page and page size + chainloop workflow list --page 2 --page-size 10 + + # Output in json format to paginate using scripts + chainloop workflow list --page 2 --page-size 10 --output json + + # Show the full report + chainloop workflow list --full +`, + PreRunE: func(_ *cobra.Command, _ []string) error { + if page < 1 { + return fmt.Errorf("--page must be greater or equal than 1") + } + if pageSize < 1 { + return fmt.Errorf("--page-size must be greater or equal than 1") + } + + return nil + }, + RunE: func(_ *cobra.Command, _ []string) error { + res, err := action.NewWorkflowList(actionOpts).Run(page, pageSize) if err != nil { return err } - return encodeOutput(res, WorkflowListTableOutput) + if err := encodeOutput(res, WorkflowListTableOutput); err != nil { + return err + } + + pgResponse := res.Pagination + + logger.Info().Msg(fmt.Sprintf("Showing %d out of %d", len(res.Workflows), 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().BoolVar(&full, "full", false, "show the full report") + cmd.Flags().IntVar(&page, "page", defaultPage, "page number") + cmd.Flags().IntVar(&pageSize, "page-size", defaultPageSize, "number of workflows per page") return cmd } func workflowItemTableOutput(workflow *action.WorkflowItem) error { - return WorkflowListTableOutput([]*action.WorkflowItem{workflow}) + return WorkflowListTableOutput(&action.WorkflowListResult{Workflows: []*action.WorkflowItem{workflow}}) } -func WorkflowListTableOutput(workflows []*action.WorkflowItem) error { - if len(workflows) == 0 { +func WorkflowListTableOutput(workflowListResult *action.WorkflowListResult) error { + if len(workflowListResult.Workflows) == 0 { fmt.Println("there are no workflows yet") return nil } @@ -64,7 +114,7 @@ func WorkflowListTableOutput(workflows []*action.WorkflowItem) error { t.AppendHeader(headerRow) } - for _, p := range workflows { + for _, p := range workflowListResult.Workflows { var row table.Row var lastRunRunner, lastRunState string if lr := p.LastRun; lr != nil { diff --git a/app/cli/cmd/workflow_update.go b/app/cli/cmd/workflow_update.go index 444bc9515..c49ff4119 100644 --- a/app/cli/cmd/workflow_update.go +++ b/app/cli/cmd/workflow_update.go @@ -52,7 +52,7 @@ func newWorkflowUpdateCmd() *cobra.Command { } logger.Info().Msg("Workflow updated!") - return encodeOutput([]*action.WorkflowItem{res}, WorkflowListTableOutput) + return encodeOutput(&action.WorkflowListResult{Workflows: []*action.WorkflowItem{res}}, WorkflowListTableOutput) }, } diff --git a/app/cli/internal/action/action.go b/app/cli/internal/action/action.go index fd66bfed4..7b5957819 100644 --- a/app/cli/internal/action/action.go +++ b/app/cli/internal/action/action.go @@ -34,6 +34,13 @@ type ActionsOpts struct { Logger zerolog.Logger } +type OffsetPagination struct { + Page int `json:"page"` + PageSize int `json:"pageSize"` + TotalPages int `json:"totalPages"` + TotalCount int `json:"totalCount"` +} + func toTimePtr(t time.Time) *time.Time { return &t } diff --git a/app/cli/internal/action/workflow_list.go b/app/cli/internal/action/workflow_list.go index 5f96f56dc..c235718de 100644 --- a/app/cli/internal/action/workflow_list.go +++ b/app/cli/internal/action/workflow_list.go @@ -44,43 +44,77 @@ type WorkflowItem struct { Public bool `json:"public"` } +// WorkflowListResult holds the output of the workflow list action +type WorkflowListResult struct { + Workflows []*WorkflowItem `json:"workflows"` + Pagination *OffsetPagination `json:"pagination"` +} + +// NewWorkflowList creates a new instance of WorkflowList func NewWorkflowList(cfg *ActionsOpts) *WorkflowList { return &WorkflowList{cfg} } -func (action *WorkflowList) Run() ([]*WorkflowItem, error) { +// Run executes the workflow list action +func (action *WorkflowList) Run(page int, pageSize int) (*WorkflowListResult, 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.NewWorkflowServiceClient(action.cfg.CPConnection) - resp, err := client.List(context.Background(), &pb.WorkflowServiceListRequest{}) + res := &WorkflowListResult{} + + resp, err := client.List(context.Background(), &pb.WorkflowServiceListRequest{ + Pagination: &pb.OffsetPaginationRequest{ + Page: int32(page), + PageSize: int32(pageSize), + }, + }) if err != nil { return nil, err } - result := make([]*WorkflowItem, 0, len(resp.Result)) + // Convert the response to the output format for _, p := range resp.Result { - result = append(result, pbWorkflowItemToAction(p)) + res.Workflows = append(res.Workflows, pbWorkflowItemToAction(p)) + } + + // Add the pagination details + res.Pagination = &OffsetPagination{ + Page: int(resp.GetPagination().GetPage()), + PageSize: int(resp.GetPagination().GetPageSize()), + TotalPages: int(resp.GetPagination().GetTotalPages()), + TotalCount: int(resp.GetPagination().GetTotalCount()), } - return result, nil + return res, nil } +// pbWorkflowItemToAction converts API response to WorkflowItem func pbWorkflowItemToAction(wf *pb.WorkflowItem) *WorkflowItem { if wf == nil { return nil } - res := &WorkflowItem{ - Name: wf.Name, ID: wf.Id, CreatedAt: toTimePtr(wf.CreatedAt.AsTime()), - Project: wf.Project, Team: wf.Team, RunsCount: wf.RunsCount, + return &WorkflowItem{ + Name: wf.Name, + ID: wf.Id, + CreatedAt: toTimePtr(wf.CreatedAt.AsTime()), + Project: wf.Project, + Team: wf.Team, + RunsCount: wf.RunsCount, ContractName: wf.ContractName, ContractRevisionLatest: wf.ContractRevisionLatest, LastRun: pbWorkflowRunItemToAction(wf.LastRun), Public: wf.Public, Description: wf.Description, } - - return res } +// NamespacedName returns the project and workflow name in a formatted string func (wi *WorkflowItem) NamespacedName() string { return fmt.Sprintf("%s/%s", wi.Project, wi.Name) }