-
Notifications
You must be signed in to change notification settings - Fork 37
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Allow to draw a basic worker pools view
- Loading branch information
Showing
6 changed files
with
379 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
package data | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"time" | ||
|
||
"github.com/charmbracelet/bubbles/table" | ||
"github.com/pkg/browser" | ||
"github.com/pkg/errors" | ||
"github.com/shurcooL/graphql" | ||
|
||
"github.com/spacelift-io/spacectl/client/structs" | ||
"github.com/spacelift-io/spacectl/internal/cmd/authenticated" | ||
) | ||
|
||
// WorkerPool allows to interact with a worker pool. | ||
type WorkerPool struct { | ||
WokerPoolID string | ||
} | ||
|
||
// Selected opens the selected worker pool in the browser. | ||
func (q *WorkerPool) Selected(row table.Row) error { | ||
return browser.OpenURL(authenticated.Client.URL("/stack/%s/run/%s", row[1], row[2])) | ||
} | ||
|
||
// Columns returns the columns of the worker pool table. | ||
func (q *WorkerPool) Columns() []table.Column { | ||
return []table.Column{ | ||
{Title: "#", Width: 2}, | ||
{Title: "Stack", Width: 25}, | ||
{Title: "Run", Width: 32}, | ||
{Title: "State", Width: 15}, | ||
{Title: "Type", Width: 10}, | ||
{Title: "Created At", Width: 27}, | ||
} | ||
} | ||
|
||
// Rows returns the rows of the worker pool table. | ||
func (q *WorkerPool) Rows(ctx context.Context) (rows []table.Row, err error) { | ||
var runs []runsEdge | ||
if q.WokerPoolID == "" { | ||
runs, err = q.getPublicPoolRuns(ctx) | ||
if err != nil { | ||
return nil, err | ||
} | ||
} else { | ||
runs, err = q.getPrivatePoolRuns(ctx) | ||
if err != nil { | ||
return nil, err | ||
} | ||
} | ||
|
||
for _, edge := range runs { | ||
tm := time.Unix(int64(edge.Node.Run.CreatedAt), 0) | ||
rows = append(rows, table.Row{ | ||
fmt.Sprint(edge.Node.Position), | ||
edge.Node.StackID, | ||
edge.Node.Run.ID, | ||
edge.Node.Run.State, | ||
edge.Node.Run.Type, | ||
tm.Format(time.DateTime), | ||
}) | ||
} | ||
|
||
return rows, nil | ||
} | ||
|
||
func (q *WorkerPool) getPublicPoolRuns(ctx context.Context) ([]runsEdge, error) { | ||
var query struct { | ||
WorkerPool struct { | ||
Runs runsQuery `graphql:"searchSchedulableRuns(input: $input)"` | ||
} `graphql:"publicWorkerPool"` | ||
} | ||
|
||
if err := authenticated.Client.Query(ctx, &query, q.baseSearchParams()); err != nil { | ||
return nil, errors.Wrap(err, "failed to query run list") | ||
} | ||
|
||
return query.WorkerPool.Runs.Edges, nil | ||
} | ||
|
||
func (q *WorkerPool) getPrivatePoolRuns(ctx context.Context) ([]runsEdge, error) { | ||
var query struct { | ||
WorkerPool struct { | ||
Runs runsQuery `graphql:"searchSchedulableRuns(input: $input)"` | ||
} `graphql:"workerPool(id: $id)"` | ||
} | ||
|
||
vars := q.baseSearchParams() | ||
vars["id"] = q.WokerPoolID | ||
|
||
if err := authenticated.Client.Query(ctx, &query, vars); err != nil { | ||
return nil, errors.Wrap(err, "failed to query run list") | ||
} | ||
|
||
return query.WorkerPool.Runs.Edges, nil | ||
} | ||
|
||
func (q *WorkerPool) baseSearchParams() map[string]interface{} { | ||
return map[string]interface{}{ | ||
"input": structs.SearchInput{ | ||
First: graphql.NewInt(graphql.Int(100)), | ||
OrderBy: &structs.QueryOrder{ | ||
Field: graphql.String("position"), | ||
Direction: graphql.String("ASC"), | ||
}, | ||
}, | ||
} | ||
} | ||
|
||
type runsQuery struct { | ||
Edges []runsEdge `graphql:"edges"` | ||
PageInfo structs.PageInfo `graphql:"pageInfo"` | ||
} | ||
|
||
type runsEdge struct { | ||
Node struct { | ||
StackID string `graphql:"stackId"` | ||
Run run `graphql:"run"` | ||
Position int `graphql:"position"` | ||
} `graphql:"node"` | ||
} | ||
|
||
type run struct { | ||
ID string `graphql:"id" json:"id"` | ||
CreatedAt int `graphql:"createdAt" json:"createdAt"` | ||
State string `graphql:"state" json:"state"` | ||
Type string `graphql:"type" json:"type"` | ||
Title string `graphql:"title" json:"title"` | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,154 @@ | ||
package draw | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"time" | ||
|
||
"github.com/charmbracelet/bubbles/table" | ||
tea "github.com/charmbracelet/bubbletea" | ||
"github.com/charmbracelet/lipgloss" | ||
"golang.org/x/term" | ||
) | ||
|
||
// Table is a table that can be drawn. | ||
type Table struct { | ||
table table.Model | ||
td TableData | ||
|
||
width int | ||
height int | ||
baseStyle lipgloss.Style | ||
|
||
lastErr error | ||
} | ||
|
||
// TableData is the data for a table. | ||
type TableData interface { | ||
// Columns returns the columns of the table. | ||
Columns() []table.Column | ||
|
||
// Rows returns the rows of the table. | ||
Rows(ctx context.Context) ([]table.Row, error) | ||
|
||
// Selected is called when a row is selected. | ||
// The entire row is passed to the function. | ||
Selected(table.Row) error | ||
} | ||
|
||
// NewTable creates a new table. | ||
func NewTable(ctx context.Context, d TableData) (*Table, error) { | ||
rows, err := d.Rows(ctx) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
t := table.New( | ||
table.WithColumns(d.Columns()), | ||
table.WithRows(rows), | ||
table.WithFocused(true), | ||
table.WithHeight(25), | ||
) | ||
|
||
s := table.DefaultStyles() | ||
s.Header = s.Header. | ||
BorderStyle(lipgloss.ThickBorder()). | ||
BorderForeground(lipgloss.Color("240")). | ||
BorderBottom(true). | ||
Bold(true) | ||
|
||
s.Selected = s.Selected. | ||
Foreground(lipgloss.Color("#FAFAFA")). | ||
Background(lipgloss.Color("#7C47FC")). | ||
Bold(false) | ||
t.SetStyles(s) | ||
|
||
bs := lipgloss.NewStyle(). | ||
BorderStyle(lipgloss.ThickBorder()). | ||
BorderForeground(lipgloss.Color("240")) | ||
|
||
width, height, err := term.GetSize(0) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return &Table{ | ||
table: t, | ||
td: d, | ||
|
||
width: width, | ||
height: height, | ||
baseStyle: bs, | ||
|
||
lastErr: nil, | ||
}, nil | ||
} | ||
|
||
// DrawTable should be called to draw the table. | ||
func (t *Table) DrawTable() error { | ||
if _, err := tea.NewProgram(t).Run(); err != nil { | ||
return fmt.Errorf("error running program: %w", err) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// Init implements tea.Model.Init. | ||
// Should not be called directly. | ||
func (t Table) Init() tea.Cmd { | ||
return tickCmd() | ||
} | ||
|
||
// Update implements tea.Model.Update. | ||
// Should not be called directly. | ||
func (t Table) Update(msg tea.Msg) (tea.Model, tea.Cmd) { | ||
var cmd tea.Cmd | ||
switch msg := msg.(type) { | ||
case tea.WindowSizeMsg: | ||
t.width = msg.Width | ||
t.height = msg.Height | ||
case tea.KeyMsg: | ||
switch msg.String() { | ||
case "esc", "ctrl+c", "q": | ||
return t, tea.Quit | ||
case "enter": | ||
err := t.td.Selected(t.table.SelectedRow()) | ||
if err != nil { | ||
return t, t.saveErrorAndExit(err) | ||
} | ||
} | ||
case tickMsg: | ||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute) | ||
defer cancel() | ||
|
||
rows, err := t.td.Rows(ctx) | ||
if err != nil { | ||
return t, t.saveErrorAndExit(err) | ||
} | ||
|
||
t.table.SetRows(rows) | ||
return t, tickCmd() | ||
} | ||
|
||
t.table, cmd = t.table.Update(msg) | ||
return t, cmd | ||
} | ||
|
||
// View implements tea.Model.View. | ||
// Should not be called directly. | ||
func (t Table) View() string { | ||
if t.lastErr != nil { | ||
return fmt.Sprintln("Exited with an error:", t.lastErr) | ||
} | ||
|
||
return lipgloss.Place( | ||
t.width, t.height, | ||
lipgloss.Center, lipgloss.Center, | ||
t.baseStyle.Render(t.table.View())+"\n", | ||
) | ||
} | ||
|
||
func (t *Table) saveErrorAndExit(err error) tea.Cmd { | ||
t.lastErr = err | ||
return tea.Quit | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
package draw | ||
|
||
import ( | ||
"time" | ||
|
||
tea "github.com/charmbracelet/bubbletea" | ||
) | ||
|
||
type tickMsg time.Time | ||
|
||
func tickCmd() tea.Cmd { | ||
return tea.Tick(time.Second*5, func(t time.Time) tea.Msg { | ||
return tickMsg(t) | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
package workerpools | ||
|
||
import ( | ||
"fmt" | ||
"strings" | ||
|
||
"github.com/manifoldco/promptui" | ||
"github.com/urfave/cli/v2" | ||
|
||
"github.com/spacelift-io/spacectl/internal/cmd/authenticated" | ||
"github.com/spacelift-io/spacectl/internal/cmd/draw" | ||
"github.com/spacelift-io/spacectl/internal/cmd/draw/data" | ||
) | ||
|
||
func watch(cliCtx *cli.Context) error { | ||
got, err := findAndSelectWorkerPool(cliCtx) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
wp := &data.WorkerPool{WokerPoolID: got} | ||
t, err := draw.NewTable(cliCtx.Context, wp) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
return t.DrawTable() | ||
} | ||
|
||
// findAndSelectWorkerPool finds all worker pools and lets the user select one. | ||
// | ||
// Returns the ID of the selected worker pool. | ||
// If public worker pool is selected and empty string is returned. | ||
func findAndSelectWorkerPool(cliCtx *cli.Context) (string, error) { | ||
var query listPoolsQuery | ||
if err := authenticated.Client.Query(cliCtx.Context, &query, map[string]interface{}{}); err != nil { | ||
return "", err | ||
} | ||
|
||
items := []string{"Public worker pool"} | ||
found := map[string]string{ | ||
"Public worker pool": "", | ||
} | ||
for _, p := range query.Pools { | ||
items = append(items, p.Name) | ||
found[p.Name] = p.ID | ||
} | ||
|
||
prompt := promptui.Select{ | ||
Label: fmt.Sprintf("Found %d worker pools, select one", len(items)), | ||
Items: items, | ||
Size: 10, | ||
StartInSearchMode: len(items) > 5, | ||
Searcher: func(input string, index int) bool { | ||
return strings.Contains(items[index], input) | ||
}, | ||
} | ||
|
||
_, result, err := prompt.Run() | ||
if err != nil { | ||
return "", err | ||
} | ||
|
||
return found[result], nil | ||
} |