Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Use new table formatter everywhere #3544

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

Merged
merged 3 commits into from
Aug 18, 2022
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
24 changes: 6 additions & 18 deletions cli/cliui/table.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ func DisplayTable(out any, sort string, filterColumns []string) (string, error)
sort = strings.ToLower(strings.ReplaceAll(sort, "_", " "))
h, ok := headersMap[sort]
if !ok {
return "", xerrors.Errorf("specified sort column %q not found in table headers, available columns are %q", sort, strings.Join(headersRaw, `", "`))
return "", xerrors.Errorf(`specified sort column %q not found in table headers, available columns are "%v"`, sort, strings.Join(headersRaw, `", "`))
}

// Autocorrect
Expand All @@ -101,7 +101,7 @@ func DisplayTable(out any, sort string, filterColumns []string) (string, error)
column := strings.ToLower(strings.ReplaceAll(column, "_", " "))
h, ok := headersMap[column]
if !ok {
return "", xerrors.Errorf("specified filter column %q not found in table headers, available columns are %q", sort, strings.Join(headersRaw, `", "`))
return "", xerrors.Errorf(`specified filter column %q not found in table headers, available columns are "%v"`, sort, strings.Join(headersRaw, `", "`))
}

// Autocorrect
Expand Down Expand Up @@ -158,6 +158,10 @@ func DisplayTable(out any, sort string, filterColumns []string) (string, error)
if val != nil {
v = val.Format(time.Stamp)
}
case fmt.Stringer:
if val != nil {
v = val.String()
}
}

rowSlice[i] = v
Expand Down Expand Up @@ -301,19 +305,3 @@ func valueToTableMap(val reflect.Value) (map[string]any, error) {

return row, nil
}

func ValidateColumns(all, given []string) error {
for _, col := range given {
found := false
for _, c := range all {
if strings.EqualFold(strings.ReplaceAll(col, "_", " "), c) {
found = true
break
}
}
if !found {
return fmt.Errorf("unknown column: %s", col)
}
}
return nil
}
37 changes: 24 additions & 13 deletions cli/cliui/table_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cliui_test

import (
"fmt"
"log"
"strings"
"testing"
Expand All @@ -12,6 +13,16 @@ import (
"github.com/coder/coder/cli/cliui"
)

type stringWrapper struct {
str string
}

var _ fmt.Stringer = stringWrapper{}

func (s stringWrapper) String() string {
return s.str
}

type tableTest1 struct {
Name string `table:"name"`
NotIncluded string // no table tag
Expand All @@ -28,9 +39,9 @@ type tableTest1 struct {
}

type tableTest2 struct {
Name string `table:"name"`
Age int `table:"age"`
NotIncluded string `table:"-"`
Name stringWrapper `table:"name"`
Age int `table:"age"`
NotIncluded string `table:"-"`
}

type tableTest3 struct {
Expand All @@ -48,21 +59,21 @@ func Test_DisplayTable(t *testing.T) {
Age: 10,
Roles: []string{"a", "b", "c"},
Sub1: tableTest2{
Name: "foo1",
Name: stringWrapper{str: "foo1"},
Age: 11,
},
Sub2: &tableTest2{
Name: "foo2",
Name: stringWrapper{str: "foo2"},
Age: 12,
},
Sub3: tableTest3{
Sub: tableTest2{
Name: "foo3",
Name: stringWrapper{str: "foo3"},
Age: 13,
},
},
Sub4: tableTest2{
Name: "foo4",
Name: stringWrapper{str: "foo4"},
Age: 14,
},
Time: someTime,
Expand All @@ -73,18 +84,18 @@ func Test_DisplayTable(t *testing.T) {
Age: 20,
Roles: []string{"a"},
Sub1: tableTest2{
Name: "bar1",
Name: stringWrapper{str: "bar1"},
Age: 21,
},
Sub2: nil,
Sub3: tableTest3{
Sub: tableTest2{
Name: "bar3",
Name: stringWrapper{str: "bar3"},
Age: 23,
},
},
Sub4: tableTest2{
Name: "bar4",
Name: stringWrapper{str: "bar4"},
Age: 24,
},
Time: someTime,
Expand All @@ -95,18 +106,18 @@ func Test_DisplayTable(t *testing.T) {
Age: 30,
Roles: nil,
Sub1: tableTest2{
Name: "baz1",
Name: stringWrapper{str: "baz1"},
Age: 31,
},
Sub2: nil,
Sub3: tableTest3{
Sub: tableTest2{
Name: "baz3",
Name: stringWrapper{str: "baz3"},
Age: 33,
},
},
Sub4: tableTest2{
Name: "baz4",
Name: stringWrapper{str: "baz4"},
Age: 34,
},
Time: someTime,
Expand Down
56 changes: 23 additions & 33 deletions cli/features.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,10 @@ import (
"fmt"
"strings"

"github.com/coder/coder/cli/cliui"
"github.com/jedib0t/go-pretty/v6/table"

"github.com/spf13/cobra"
"golang.org/x/xerrors"

"github.com/coder/coder/cli/cliui"
"github.com/coder/coder/codersdk"
)

Expand Down Expand Up @@ -38,10 +36,6 @@ func featuresList() *cobra.Command {
Use: "list",
Aliases: []string{"ls"},
RunE: func(cmd *cobra.Command, args []string) error {
err := cliui.ValidateColumns(featureColumns, columns)
if err != nil {
return err
}
Copy link
Member

Choose a reason for hiding this comment

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

One downside of this change is that we have to round-trip to the server for a response instead of exiting immediately on bad input.

Would it make sense to expose a cliui.ValidateTableColumns([]codersdk.Derp{}, columns) function?

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm hoping to automate this whole process of validating columns and adding the columns/sort flags in a future PR. I just removed this because it didn't match any of the other commands, but your comment is valid. I can add it back if you want or we could wait until we have better output utilities.

Copy link
Member

Choose a reason for hiding this comment

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

Sounds good. My opinion is that we’re not in a hurry to restore the early check. It’s a small QoL improvement but it can wait for the refactor just fine.

client, err := createClient(cmd)
if err != nil {
return err
Expand All @@ -54,11 +48,14 @@ func featuresList() *cobra.Command {
out := ""
switch outputFormat {
case "table", "":
out = displayFeatures(columns, entitlements.Features)
out, err = displayFeatures(columns, entitlements.Features)
if err != nil {
return xerrors.Errorf("render table: %w", err)
}
case "json":
outBytes, err := json.Marshal(entitlements)
if err != nil {
return xerrors.Errorf("marshal users to JSON: %w", err)
return xerrors.Errorf("marshal features to JSON: %w", err)
}

out = string(outBytes)
Expand All @@ -78,35 +75,28 @@ func featuresList() *cobra.Command {
return cmd
}

type featureRow struct {
Name string `table:"name"`
Entitlement string `table:"entitlement"`
Enabled bool `table:"enabled"`
Limit *int64 `table:"limit"`
Actual *int64 `table:"actual"`
}

// displayFeatures will return a table displaying all features passed in.
// filterColumns must be a subset of the feature fields and will determine which
// columns to display
func displayFeatures(filterColumns []string, features map[string]codersdk.Feature) string {
tableWriter := cliui.Table()
header := table.Row{}
for _, h := range featureColumns {
header = append(header, h)
}
tableWriter.AppendHeader(header)
tableWriter.SetColumnConfigs(cliui.FilterTableColumns(header, filterColumns))
tableWriter.SortBy([]table.SortBy{{
Name: "username",
}})
func displayFeatures(filterColumns []string, features map[string]codersdk.Feature) (string, error) {
rows := make([]featureRow, 0, len(features))
for name, feat := range features {
tableWriter.AppendRow(table.Row{
name,
feat.Entitlement,
feat.Enabled,
intOrNil(feat.Limit),
intOrNil(feat.Actual),
rows = append(rows, featureRow{
Name: name,
Entitlement: string(feat.Entitlement),
Enabled: feat.Enabled,
Limit: feat.Limit,
Actual: feat.Actual,
})
}
return tableWriter.Render()
}

func intOrNil(i *int64) string {
if i == nil {
return ""
}
return fmt.Sprintf("%d", *i)
return cliui.DisplayTable(rows, "name", filterColumns)
}
100 changes: 56 additions & 44 deletions cli/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"time"

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

"github.com/coder/coder/cli/cliui"
Expand All @@ -14,6 +13,49 @@ import (
"github.com/coder/coder/codersdk"
)

type workspaceListRow struct {
Workspace string `table:"workspace"`
Template string `table:"template"`
Status string `table:"status"`
LastBuilt string `table:"last built"`
Outdated bool `table:"outdated"`
StartsAt string `table:"starts at"`
StopsAfter string `table:"stops after"`
}

func workspaceListRowFromWorkspace(now time.Time, usersByID map[uuid.UUID]codersdk.User, workspace codersdk.Workspace) workspaceListRow {
status := codersdk.WorkspaceDisplayStatus(workspace.LatestBuild.Job.Status, workspace.LatestBuild.Transition)

lastBuilt := now.UTC().Sub(workspace.LatestBuild.Job.CreatedAt).Truncate(time.Second)
autostartDisplay := "-"
if !ptr.NilOrEmpty(workspace.AutostartSchedule) {
if sched, err := schedule.Weekly(*workspace.AutostartSchedule); err == nil {
autostartDisplay = fmt.Sprintf("%s %s (%s)", sched.Time(), sched.DaysOfWeek(), sched.Location())
}
}

autostopDisplay := "-"
if !ptr.NilOrZero(workspace.TTLMillis) {
dur := time.Duration(*workspace.TTLMillis) * time.Millisecond
autostopDisplay = durationDisplay(dur)
if !workspace.LatestBuild.Deadline.IsZero() && workspace.LatestBuild.Deadline.After(now) && status == "Running" {
remaining := time.Until(workspace.LatestBuild.Deadline)
autostopDisplay = fmt.Sprintf("%s (%s)", autostopDisplay, relative(remaining))
}
}

user := usersByID[workspace.OwnerID]
return workspaceListRow{
Workspace: user.Username + "/" + workspace.Name,
Template: workspace.TemplateName,
Status: status,
LastBuilt: durationDisplay(lastBuilt),
Outdated: workspace.Outdated,
StartsAt: autostartDisplay,
StopsAfter: autostopDisplay,
}
}

func list() *cobra.Command {
var columns []string
cmd := &cobra.Command{
Expand All @@ -32,10 +74,10 @@ func list() *cobra.Command {
return err
}
if len(workspaces) == 0 {
_, _ = fmt.Fprintln(cmd.OutOrStdout(), cliui.Styles.Prompt.String()+"No workspaces found! Create one:")
_, _ = fmt.Fprintln(cmd.OutOrStdout())
_, _ = fmt.Fprintln(cmd.OutOrStdout(), " "+cliui.Styles.Code.Render("coder create <name>"))
_, _ = fmt.Fprintln(cmd.OutOrStdout())
_, _ = fmt.Fprintln(cmd.ErrOrStderr(), cliui.Styles.Prompt.String()+"No workspaces found! Create one:")
_, _ = fmt.Fprintln(cmd.ErrOrStderr())
_, _ = fmt.Fprintln(cmd.ErrOrStderr(), " "+cliui.Styles.Code.Render("coder create <name>"))
_, _ = fmt.Fprintln(cmd.ErrOrStderr())
return nil
}
users, err := client.Users(cmd.Context(), codersdk.UsersRequest{})
Expand All @@ -47,48 +89,18 @@ func list() *cobra.Command {
usersByID[user.ID] = user
}

tableWriter := cliui.Table()
header := table.Row{"workspace", "template", "status", "last built", "outdated", "starts at", "stops after"}
tableWriter.AppendHeader(header)
tableWriter.SortBy([]table.SortBy{{
Name: "workspace",
}})
tableWriter.SetColumnConfigs(cliui.FilterTableColumns(header, columns))

now := time.Now()
for _, workspace := range workspaces {
status := codersdk.WorkspaceDisplayStatus(workspace.LatestBuild.Job.Status, workspace.LatestBuild.Transition)

lastBuilt := time.Now().UTC().Sub(workspace.LatestBuild.Job.CreatedAt).Truncate(time.Second)
autostartDisplay := "-"
if !ptr.NilOrEmpty(workspace.AutostartSchedule) {
if sched, err := schedule.Weekly(*workspace.AutostartSchedule); err == nil {
autostartDisplay = fmt.Sprintf("%s %s (%s)", sched.Time(), sched.DaysOfWeek(), sched.Location())
}
}

autostopDisplay := "-"
if !ptr.NilOrZero(workspace.TTLMillis) {
dur := time.Duration(*workspace.TTLMillis) * time.Millisecond
autostopDisplay = durationDisplay(dur)
if !workspace.LatestBuild.Deadline.IsZero() && workspace.LatestBuild.Deadline.After(now) && status == "Running" {
remaining := time.Until(workspace.LatestBuild.Deadline)
autostopDisplay = fmt.Sprintf("%s (%s)", autostopDisplay, relative(remaining))
}
}
displayWorkspaces := make([]workspaceListRow, len(workspaces))
for i, workspace := range workspaces {
displayWorkspaces[i] = workspaceListRowFromWorkspace(now, usersByID, workspace)
}

user := usersByID[workspace.OwnerID]
tableWriter.AppendRow(table.Row{
user.Username + "/" + workspace.Name,
workspace.TemplateName,
status,
durationDisplay(lastBuilt),
workspace.Outdated,
autostartDisplay,
autostopDisplay,
})
out, err := cliui.DisplayTable(displayWorkspaces, "workspace", columns)
if err != nil {
return err
}
_, err = fmt.Fprintln(cmd.OutOrStdout(), tableWriter.Render())

_, err = fmt.Fprintln(cmd.OutOrStdout(), out)
return err
},
}
Expand Down
Loading