Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: Use searchStacks over stacks for listing stacks #246

Merged
Show file tree
Hide file tree
Changes from 5 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
92 changes: 49 additions & 43 deletions internal/cmd/stack/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,11 @@ package stack
import (
"context"
"fmt"
"sort"
"strings"

"github.com/pkg/errors"
"github.com/shurcooL/graphql"
"github.com/spacelift-io/spacectl/client/structs"
"github.com/spacelift-io/spacectl/internal/cmd"
"github.com/spacelift-io/spacectl/internal/cmd/authenticated"
"github.com/urfave/cli/v2"
)

Expand All @@ -23,68 +22,54 @@ func listStacks() cli.ActionFunc {
case cmd.OutputFormatTable:
return listStacksTable(cliCtx)
case cmd.OutputFormatJSON:
return listStacksJSON(cliCtx.Context)
return listStacksJSON(cliCtx)
}

return fmt.Errorf("unknown output format: %v", outputFormat)
}
}

func listStacksJSON(ctx context.Context) error {
var query struct {
Stacks []stack `graphql:"stacks" json:"stacks,omitempty"`
func listStacksJSON(ctx *cli.Context) error {
stacks, err := searchAllStacks(ctx.Context, structs.SearchInput{
First: graphql.NewInt(50),
})
if err != nil {
return err
}

if err := authenticated.Client.Query(ctx, &query, map[string]interface{}{}); err != nil {
return errors.Wrap(err, "failed to query list of stacks")
}
return cmd.OutputJSON(query.Stacks)
return cmd.OutputJSON(stacks)
}

func listStacksTable(ctx *cli.Context) error {
var query struct {
Stacks []struct {
ID string `graphql:"id" json:"id,omitempty"`
LockedBy string `graphql:"lockedBy"`
Name string `graphql:"name"`
State string `graphql:"state"`
Labels []string `graphql:"labels"`
TrackedCommit struct {
AuthorName string `graphql:"authorName"`
Hash string `graphql:"hash"`
} `graphql:"trackedCommit"`
WorkerPool struct {
Name string `graphql:"name"`
} `graphql:"workerPool"`
} `graphql:"stacks"`
}

if err := authenticated.Client.Query(ctx.Context, &query, map[string]interface{}{}); err != nil {
return errors.Wrap(err, "failed to query list of stacks")
}

sort.SliceStable(query.Stacks, func(i, j int) bool {
return strings.Compare(strings.ToLower(query.Stacks[i].Name), strings.ToLower(query.Stacks[j].Name)) < 0
stacks, err := searchAllStacks(ctx.Context, structs.SearchInput{
First: graphql.NewInt(50),
OrderBy: &structs.QueryOrder{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should follow the same logic as the default view in the UI for stacks? I think that one is

"orderBy": [
    {
        "field": "starred",
        "direction": "DESC"
    }
]
``

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, precisely. 👍🏻

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Field: "starred",
Direction: "DESC",
},
})
if err != nil {
return err
}

columns := []string{"Name", "ID", "Commit", "Author", "State", "Worker Pool", "Locked By"}
if ctx.Bool(flagShowLabels.Name) {
columns = append(columns, "Labels")
}

tableData := [][]string{columns}
for _, stack := range query.Stacks {
for _, s := range stacks {
row := []string{
stack.Name,
stack.ID,
cmd.HumanizeGitHash(stack.TrackedCommit.Hash),
stack.TrackedCommit.AuthorName,
stack.State,
stack.WorkerPool.Name,
stack.LockedBy,
s.Name,
s.ID,
cmd.HumanizeGitHash(s.TrackedCommit.Hash),
s.TrackedCommit.AuthorName,
s.State,
s.WorkerPool.Name,
s.LockedBy,
}
if ctx.Bool(flagShowLabels.Name) {
row = append(row, strings.Join(stack.Labels, ", "))
row = append(row, strings.Join(s.Labels, ", "))
}

tableData = append(tableData, row)
Expand All @@ -93,6 +78,27 @@ func listStacksTable(ctx *cli.Context) error {
return cmd.OutputTable(tableData, true)
}

func searchAllStacks(ctx context.Context, input structs.SearchInput) ([]stack, error) {
out := []stack{}

for {
result, err := searchStacks(ctx, input)
if err != nil {
return nil, err
}

out = append(out, result.Stacks...)

if !result.PageInfo.HasNextPage {
break
} else {
input.After = graphql.NewString(graphql.String(result.PageInfo.EndCursor))
}
}

return out, nil
}

type stack struct {
ID string `graphql:"id" json:"id,omitempty"`
Administrative bool `graphql:"administrative" json:"administrative,omitempty"`
Expand Down
52 changes: 14 additions & 38 deletions internal/cmd/stack/open_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,12 @@ type stackSearchParams struct {
branch *string
}

func searchStacks(ctx context.Context, p *stackSearchParams) ([]stack, error) {
type searchStacksResult struct {
Stacks []stack
PageInfo structs.PageInfo
}

func searchStacks(ctx context.Context, input structs.SearchInput) (searchStacksResult, error) {
var query struct {
SearchStacksOutput struct {
Edges []struct {
Expand All @@ -172,52 +177,23 @@ func searchStacks(ctx context.Context, p *stackSearchParams) ([]stack, error) {
PageInfo structs.PageInfo `graphql:"pageInfo"`
} `graphql:"searchStacks(input: $input)"`
}
conditions := []structs.QueryPredicate{
{
Field: graphql.String("repository"),
Constraint: structs.QueryFieldConstraint{
StringMatches: &[]graphql.String{graphql.String(p.repositoryName)},
},
},
}

if p.projectRoot != nil && *p.projectRoot != "" {
root := strings.TrimSuffix(*p.projectRoot, "/")
conditions = append(conditions, structs.QueryPredicate{
Field: graphql.String("projectRoot"),
Constraint: structs.QueryFieldConstraint{
StringMatches: &[]graphql.String{graphql.String(root), graphql.String(root + "/")},
},
})
}

if p.branch != nil {
conditions = append(conditions, structs.QueryPredicate{
Field: graphql.String("branch"),
Constraint: structs.QueryFieldConstraint{
StringMatches: &[]graphql.String{graphql.String(*p.branch)},
},
})
}

variables := map[string]interface{}{"input": structs.SearchInput{
First: graphql.NewInt(graphql.Int(p.count)), //nolint: gosec
Predicates: &conditions,
}}

if err := authenticated.Client.Query(
ctx,
&query,
variables,
map[string]interface{}{"input": input},
graphql.WithHeader("Spacelift-GraphQL-Query", "StacksPage"),
); err != nil {
return nil, errors.Wrap(err, "failed search for stacks")
return searchStacksResult{}, errors.Wrap(err, "failed search for stacks")
}

result := make([]stack, 0)
stacks := make([]stack, 0)
for _, q := range query.SearchStacksOutput.Edges {
result = append(result, q.Node)
stacks = append(stacks, q.Node)
}

return result, nil
return searchStacksResult{
Stacks: stacks,
PageInfo: query.SearchStacksOutput.PageInfo,
}, nil
}
44 changes: 39 additions & 5 deletions internal/cmd/stack/stack_selector.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@ import (
"github.com/manifoldco/promptui"
"github.com/pkg/errors"
"github.com/shurcooL/graphql"
"github.com/spacelift-io/spacectl/client/structs"
"github.com/spacelift-io/spacectl/internal/cmd/authenticated"
"github.com/urfave/cli/v2"
)

var errNoStackFound = errors.New("no stack found")

// getStackID will try to retreive a stack ID from multiple sources.
// getStackID will try to retrieve a stack ID from multiple sources.
// It will do so in the following order:
// 1. Check the --id flag, if set, use that value.
// 2. Check the current directory to determine repository and subdirectory and search for a stack.
Expand Down Expand Up @@ -90,16 +91,49 @@ func stackGetByID(ctx context.Context, stackID string) (*stack, error) {
}

func findAndSelectStack(ctx context.Context, p *stackSearchParams, forcePrompt bool) (*stack, error) {
stacks, err := searchStacks(ctx, p)
conditions := []structs.QueryPredicate{
{
Field: graphql.String("repository"),
Constraint: structs.QueryFieldConstraint{
StringMatches: &[]graphql.String{graphql.String(p.repositoryName)},
},
},
}

if p.projectRoot != nil && *p.projectRoot != "" {
root := strings.TrimSuffix(*p.projectRoot, "/")
conditions = append(conditions, structs.QueryPredicate{
Field: "projectRoot",
Constraint: structs.QueryFieldConstraint{
StringMatches: &[]graphql.String{graphql.String(root), graphql.String(root + "/")},
},
})
}

if p.branch != nil {
conditions = append(conditions, structs.QueryPredicate{
Field: "branch",
Constraint: structs.QueryFieldConstraint{
StringMatches: &[]graphql.String{graphql.String(*p.branch)},
},
})
}

input := structs.SearchInput{
First: graphql.NewInt(graphql.Int(p.count)), //nolint: gosec
Predicates: &conditions,
}

result, err := searchStacks(ctx, input)
if err != nil {
return nil, err
}

items := []string{}
found := map[string]stack{}
for _, stack := range stacks {
items = append(items, stack.Name)
found[stack.Name] = stack
for _, s := range result.Stacks {
items = append(items, s.Name)
found[s.Name] = s
}

if len(found) == 0 {
Expand Down
Loading