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
99 changes: 85 additions & 14 deletions cmd/kosli/listFlows.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,42 +6,81 @@ import (
"io"
"net/http"
"net/url"
"strconv"
"strings"

"github.com/kosli-dev/cli/internal/output"
"github.com/kosli-dev/cli/internal/requests"
"github.com/spf13/cobra"
)

const listFlowsDesc = `List flows for an org.`
const listFlowsShortDesc = `List flows for an org. `

const listFlowsLongDesc = listFlowsShortDesc + `By default, all flows for the org are returned.
Pass --page-limit and/or --page to paginate the results; when pagination is requested the output
includes pagination metadata and a page footer (table) or a "data"/"pagination" envelope (JSON).
The list can be filtered by name with --name (and --ignore-case for case-insensitive matching).`

const listFlowsExample = `
# list all flows for an org:
kosli list flows \
--api-token yourAPIToken \
--org yourOrgName

# list the first 30 flows (paginated):
kosli list flows \
--page-limit 30 \
--api-token yourAPIToken \
--org yourOrgName

# show the second page of flows (30 per page):
kosli list flows \
--page-limit 30 \
--page 2 \
--api-token yourAPIToken \
--org yourOrgName

# list flows whose name contains "backend" (in JSON):
kosli list flows \
--name backend \
--api-token yourAPIToken \
--org yourOrgName \
--output json
`

type listFlowsOptions struct {
output string
listOptions
name string
ignoreCase bool
paginate bool
}

func newListFlowsCmd(out io.Writer) *cobra.Command {
o := new(listFlowsOptions)
cmd := &cobra.Command{
Use: "flows",
Short: listFlowsDesc,
Long: listFlowsDesc,
Args: cobra.NoArgs,
Use: "flows",
Short: listFlowsShortDesc,
Long: listFlowsLongDesc,
Example: listFlowsExample,
Args: cobra.NoArgs,
PreRunE: func(cmd *cobra.Command, args []string) error {
err := RequireGlobalFlags(global, []string{"Org", "ApiToken"})
if err != nil {
return ErrorBeforePrintingUsage(cmd, err.Error())
}
return nil
return o.validate(cmd)
},
RunE: func(cmd *cobra.Command, args []string) error {
// pagination is opt-in: only when the user explicitly sets --page or
// --page-limit do we paginate. Otherwise all flows are returned (the
// historical behaviour), keeping output backwards compatible.
o.paginate = cmd.Flags().Changed("page") || cmd.Flags().Changed("page-limit")
return o.run(out)
},
}

cmd.Flags().StringVarP(&o.output, "output", "o", "table", outputFlag)
cmd.Flags().StringVarP(&o.name, "name", "n", "", searchByNameFlag)
addListFlags(cmd, &o.listOptions, 20)
cmd.Flags().StringVarP(&o.name, "name", "N", "", searchByNameFlag)
Comment thread
mbevc1 marked this conversation as resolved.
cmd.Flags().BoolVarP(&o.ignoreCase, "ignore-case", "i", false, ignoreCaseFlag)

return cmd
Expand All @@ -54,6 +93,12 @@ func (o *listFlowsOptions) run(out io.Writer) error {
}

params := url.Values{}
if o.paginate {
// sending per_page switches the endpoint to the paginated {data, pagination}
// envelope; omitting it returns all flows as a plain array (the default)
params.Set("page", strconv.Itoa(o.pageNumber))
params.Set("per_page", strconv.Itoa(o.pageLimit))
}
if o.name != "" {
params.Set("search_by_name", o.name)
// case_sensitive only affects search, so only send it alongside a search term
Expand All @@ -76,22 +121,43 @@ func (o *listFlowsOptions) run(out io.Writer) error {
return err
}

return output.FormattedPrint(response.Body, o.output, out, 0,
return output.FormattedPrint(response.Body, o.output, out, o.pageNumber,
map[string]output.FormatOutputFunc{
"table": printFlowsListAsTable,
"json": output.PrintJson,
})
}

type listFlowsResponse struct {
Data []map[string]interface{} `json:"data"`
Pagination Pagination `json:"pagination"`
}

func printFlowsListAsTable(raw string, out io.Writer, page int) error {
var flows []map[string]interface{}
err := json.Unmarshal([]byte(raw), &flows)
if err != nil {
return err
var pagination *Pagination

// The endpoint returns a plain array when unpaginated and a {data, pagination}
// envelope when paginated; handle both so the default behaviour is unchanged.
if strings.HasPrefix(strings.TrimSpace(raw), "[") {
if err := json.Unmarshal([]byte(raw), &flows); err != nil {
return err
}
} else {
response := &listFlowsResponse{}
if err := json.Unmarshal([]byte(raw), response); err != nil {
return err
}
flows = response.Data
pagination = &response.Pagination
}

if len(flows) == 0 {
logger.Info("No flows were found.")
msg := "No flows were found"
if page != 1 {
msg = fmt.Sprintf("%s at page number %d", msg, page)
}
logger.Info(msg + ".")
return nil
}

Expand All @@ -107,6 +173,11 @@ func printFlowsListAsTable(raw string, out io.Writer, page int) error {
row := fmt.Sprintf("%s\t%s\t%s\t%s", flow["name"], flow["description"], flow["visibility"], tagsOutput)
rows = append(rows, row)
}
if pagination != nil {
paginationInfo := fmt.Sprintf("\nShowing page %.0f of %.0f, total %.0f items", pagination.Page, pagination.PageCount, pagination.Total)
rows = append(rows, paginationInfo)
}

tabFormattedPrint(out, header, rows)

return nil
Expand Down
45 changes: 42 additions & 3 deletions cmd/kosli/listFlows_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ func (suite *ListFlowsCommandTestSuite) TestListFlowsCmd() {
{
name: "listing flows with --output json works when there are flows",
cmd: fmt.Sprintf(`list flows --output json %s`, suite.defaultKosliArguments),
goldenJson: []jsonCheck{{"", "non-empty"}},
goldenJson: []jsonCheck{{"", "non-empty"}}, // unpaginated default returns a plain array
},
{
name: "listing flows with --output json works when there are no flows",
Expand Down Expand Up @@ -76,10 +76,49 @@ func (suite *ListFlowsCommandTestSuite) TestListFlowsCmd() {
goldenJson: []jsonCheck{{"", "non-empty"}, {"[0].name", "list-flows-search-target"}},
},
{
name: "short flags -n and -i work like --name and --ignore-case",
cmd: fmt.Sprintf(`list flows -n LIST-FLOWS-SEARCH-TARGET -i --output json %s`, suite.defaultKosliArguments),
name: "short flags -N and -i work like --name and --ignore-case",
cmd: fmt.Sprintf(`list flows -N LIST-FLOWS-SEARCH-TARGET -i --output json %s`, suite.defaultKosliArguments),
goldenJson: []jsonCheck{{"", "non-empty"}, {"[0].name", "list-flows-search-target"}},
},
{
name: "pagination metadata is returned on page 1",
cmd: fmt.Sprintf(`list flows --page-limit 1 --page 1 --output json %s`, suite.defaultKosliArguments),
goldenJson: []jsonCheck{{"data", "non-empty"}, {"pagination.page", float64(1)}},
},
{
name: "can page through flows with --page",
cmd: fmt.Sprintf(`list flows --page-limit 1 --page 2 --output json %s`, suite.defaultKosliArguments),
goldenJson: []jsonCheck{{"pagination.page", float64(2)}},
},
{
name: "an empty page reports the page number",
cmd: fmt.Sprintf(`list flows --page 99 %s`, suite.defaultKosliArguments),
golden: "No flows were found at page number 99.\n",
},
{
wantError: true,
name: "negative page limit causes an error",
cmd: fmt.Sprintf(`list flows --page-limit -1 %s`, suite.defaultKosliArguments),
golden: "Error: flag '--page-limit' has value '-1' which is illegal\n",
},
{
wantError: true,
name: "negative page number causes an error",
cmd: fmt.Sprintf(`list flows --page -1 %s`, suite.defaultKosliArguments),
golden: "Error: flag '--page' has value '-1' which is illegal\n",
},
{
wantError: true,
name: "zero page limit causes an error",
cmd: fmt.Sprintf(`list flows --page-limit 0 %s`, suite.defaultKosliArguments),
golden: "Error: page limit must be a positive integer\nUsage: kosli list flows [flags]\n",
},
{
wantError: true,
name: "zero page number causes an error",
cmd: fmt.Sprintf(`list flows --page 0 %s`, suite.defaultKosliArguments),
golden: "Error: page number must be a positive integer\nUsage: kosli list flows [flags]\n",
},
{
wantError: true,
name: "providing an argument causes an error",
Expand Down
Loading