Skip to content

Commit e0a7aec

Browse files
authored
fix: Match kubectl table style for simpler scripting (#1363)
Fixes #1322.
1 parent 2df92e6 commit e0a7aec

File tree

6 files changed

+164
-33
lines changed

6 files changed

+164
-33
lines changed

cli/cliui/table.go

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package cliui
2+
3+
import (
4+
"strings"
5+
6+
"github.com/jedib0t/go-pretty/v6/table"
7+
)
8+
9+
// Table creates a new table with standardized styles.
10+
func Table() table.Writer {
11+
tableWriter := table.NewWriter()
12+
tableWriter.Style().Box.PaddingLeft = ""
13+
tableWriter.Style().Box.PaddingRight = " "
14+
tableWriter.Style().Options.DrawBorder = false
15+
tableWriter.Style().Options.SeparateHeader = false
16+
tableWriter.Style().Options.SeparateColumns = false
17+
return tableWriter
18+
}
19+
20+
// FilterTableColumns returns configurations to hide columns
21+
// that are not provided in the array. If the array is empty,
22+
// no filtering will occur!
23+
func FilterTableColumns(header table.Row, columns []string) []table.ColumnConfig {
24+
if len(columns) == 0 {
25+
return nil
26+
}
27+
columnConfigs := make([]table.ColumnConfig, 0)
28+
for _, headerTextRaw := range header {
29+
headerText, _ := headerTextRaw.(string)
30+
hidden := true
31+
for _, column := range columns {
32+
if strings.EqualFold(strings.ReplaceAll(column, "_", " "), headerText) {
33+
hidden = false
34+
break
35+
}
36+
}
37+
columnConfigs = append(columnConfigs, table.ColumnConfig{
38+
Name: headerText,
39+
Hidden: hidden,
40+
})
41+
}
42+
return columnConfigs
43+
}

cli/list.go

+61-18
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ package cli
22

33
import (
44
"fmt"
5+
"strings"
6+
"time"
57

8+
"github.com/google/uuid"
69
"github.com/jedib0t/go-pretty/v6/table"
710
"github.com/spf13/cobra"
811

@@ -12,7 +15,10 @@ import (
1215
)
1316

1417
func list() *cobra.Command {
15-
return &cobra.Command{
18+
var (
19+
columns []string
20+
)
21+
cmd := &cobra.Command{
1622
Annotations: workspaceCommand,
1723
Use: "list",
1824
Short: "List all workspaces",
@@ -22,11 +28,7 @@ func list() *cobra.Command {
2228
if err != nil {
2329
return err
2430
}
25-
organization, err := currentOrganization(cmd, client)
26-
if err != nil {
27-
return err
28-
}
29-
workspaces, err := client.WorkspacesByOwner(cmd.Context(), organization.ID, codersdk.Me)
31+
workspaces, err := client.WorkspacesByUser(cmd.Context(), codersdk.Me)
3032
if err != nil {
3133
return err
3234
}
@@ -37,11 +39,22 @@ func list() *cobra.Command {
3739
_, _ = fmt.Fprintln(cmd.OutOrStdout())
3840
return nil
3941
}
42+
users, err := client.Users(cmd.Context(), codersdk.UsersRequest{})
43+
if err != nil {
44+
return err
45+
}
46+
usersByID := map[uuid.UUID]codersdk.User{}
47+
for _, user := range users {
48+
usersByID[user.ID] = user
49+
}
4050

41-
tableWriter := table.NewWriter()
42-
tableWriter.SetStyle(table.StyleLight)
43-
tableWriter.Style().Options.SeparateColumns = false
44-
tableWriter.AppendHeader(table.Row{"Workspace", "Template", "Status", "Last Built", "Outdated"})
51+
tableWriter := cliui.Table()
52+
header := table.Row{"workspace", "template", "status", "last built", "outdated"}
53+
tableWriter.AppendHeader(header)
54+
tableWriter.SortBy([]table.SortBy{{
55+
Name: "workspace",
56+
}})
57+
tableWriter.SetColumnConfigs(cliui.FilterTableColumns(header, columns))
4558

4659
for _, workspace := range workspaces {
4760
status := ""
@@ -53,32 +66,62 @@ func list() *cobra.Command {
5366

5467
switch workspace.LatestBuild.Transition {
5568
case database.WorkspaceTransitionStart:
56-
status = "start"
69+
status = "Running"
5770
if inProgress {
58-
status = "starting"
71+
status = "Starting"
5972
}
6073
case database.WorkspaceTransitionStop:
61-
status = "stop"
74+
status = "Stopped"
6275
if inProgress {
63-
status = "stopping"
76+
status = "Stopping"
6477
}
6578
case database.WorkspaceTransitionDelete:
66-
status = "delete"
79+
status = "Deleted"
6780
if inProgress {
68-
status = "deleting"
81+
status = "Deleting"
6982
}
7083
}
84+
if workspace.LatestBuild.Job.Status == codersdk.ProvisionerJobFailed {
85+
status = "Failed"
86+
}
87+
88+
duration := time.Now().UTC().Sub(workspace.LatestBuild.Job.CreatedAt).Truncate(time.Second)
89+
if duration > time.Hour {
90+
duration = duration.Truncate(time.Hour)
91+
}
92+
if duration > time.Minute {
93+
duration = duration.Truncate(time.Minute)
94+
}
95+
days := 0
96+
for duration.Hours() > 24 {
97+
days++
98+
duration -= 24 * time.Hour
99+
}
100+
durationDisplay := duration.String()
101+
if days > 0 {
102+
durationDisplay = fmt.Sprintf("%dd%s", days, durationDisplay)
103+
}
104+
if strings.HasSuffix(durationDisplay, "m0s") {
105+
durationDisplay = durationDisplay[:len(durationDisplay)-2]
106+
}
107+
if strings.HasSuffix(durationDisplay, "h0m") {
108+
durationDisplay = durationDisplay[:len(durationDisplay)-2]
109+
}
71110

111+
user := usersByID[workspace.OwnerID]
72112
tableWriter.AppendRow(table.Row{
73-
cliui.Styles.Bold.Render(workspace.Name),
113+
user.Username + "/" + workspace.Name,
74114
workspace.TemplateName,
75115
status,
76-
workspace.LatestBuild.Job.CreatedAt.Format("January 2, 2006"),
116+
durationDisplay,
77117
workspace.Outdated,
78118
})
79119
}
80120
_, err = fmt.Fprintln(cmd.OutOrStdout(), tableWriter.Render())
81121
return err
82122
},
83123
}
124+
cmd.Flags().StringArrayVarP(&columns, "column", "c", nil,
125+
"Specify a column to filter in the table.")
126+
return cmd
84127
}

cli/list_test.go

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package cli_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/require"
7+
8+
"github.com/coder/coder/cli/clitest"
9+
"github.com/coder/coder/coderd/coderdtest"
10+
"github.com/coder/coder/pty/ptytest"
11+
)
12+
13+
func TestList(t *testing.T) {
14+
t.Parallel()
15+
t.Run("Single", func(t *testing.T) {
16+
t.Parallel()
17+
client := coderdtest.New(t, nil)
18+
user := coderdtest.CreateFirstUser(t, client)
19+
coderdtest.NewProvisionerDaemon(t, client)
20+
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
21+
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
22+
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
23+
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
24+
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
25+
cmd, root := clitest.New(t, "ls")
26+
clitest.SetupConfig(t, client, root)
27+
doneChan := make(chan struct{})
28+
pty := ptytest.New(t)
29+
cmd.SetIn(pty.Input())
30+
cmd.SetOut(pty.Output())
31+
go func() {
32+
defer close(doneChan)
33+
err := cmd.Execute()
34+
require.NoError(t, err)
35+
}()
36+
pty.ExpectMatch(workspace.Name)
37+
pty.ExpectMatch("Running")
38+
<-doneChan
39+
})
40+
}

cli/root.go

+2
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,9 @@ func Root() *cobra.Command {
8080
cmd.PersistentFlags().String(varURL, "", "Specify the URL to your deployment.")
8181
cmd.PersistentFlags().String(varToken, "", "Specify an authentication token.")
8282
cliflag.String(cmd.PersistentFlags(), varAgentToken, "", "CODER_AGENT_TOKEN", "", "Specify an agent authentication token.")
83+
_ = cmd.PersistentFlags().MarkHidden(varAgentToken)
8384
cliflag.String(cmd.PersistentFlags(), varAgentURL, "", "CODER_AGENT_URL", "", "Specify the URL for an agent to access your deployment.")
85+
_ = cmd.PersistentFlags().MarkHidden(varAgentURL)
8486
cliflag.String(cmd.PersistentFlags(), varGlobalConfig, "", "CODER_CONFIG_DIR", configdir.LocalConfig("coderv2"), "Specify the path to the global `coder` config directory.")
8587
cmd.PersistentFlags().Bool(varForceTty, false, "Force the `coder` command to run as if connected to a TTY.")
8688
_ = cmd.PersistentFlags().MarkHidden(varForceTty)

cli/templatelist.go

+3-6
Original file line numberDiff line numberDiff line change
@@ -34,19 +34,16 @@ func templateList() *cobra.Command {
3434
return nil
3535
}
3636

37-
tableWriter := table.NewWriter()
38-
tableWriter.SetStyle(table.StyleLight)
39-
tableWriter.Style().Options.SeparateColumns = false
40-
tableWriter.AppendHeader(table.Row{"Name", "Source", "Last Updated", "Used By"})
37+
tableWriter := cliui.Table()
38+
tableWriter.AppendHeader(table.Row{"Name", "Last Updated", "Used By"})
4139

4240
for _, template := range templates {
4341
suffix := ""
4442
if template.WorkspaceOwnerCount != 1 {
4543
suffix = "s"
4644
}
4745
tableWriter.AppendRow(table.Row{
48-
cliui.Styles.Bold.Render(template.Name),
49-
"Archive",
46+
template.Name,
5047
template.UpdatedAt.Format("January 2, 2006"),
5148
cliui.Styles.Fuschia.Render(fmt.Sprintf("%d developer%s", template.WorkspaceOwnerCount, suffix)),
5249
})

cli/userlist.go

+15-9
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,20 @@ package cli
22

33
import (
44
"fmt"
5-
"sort"
65
"time"
76

87
"github.com/jedib0t/go-pretty/v6/table"
98
"github.com/spf13/cobra"
109

10+
"github.com/coder/coder/cli/cliui"
1111
"github.com/coder/coder/codersdk"
1212
)
1313

1414
func userList() *cobra.Command {
15-
return &cobra.Command{
15+
var (
16+
columns []string
17+
)
18+
cmd := &cobra.Command{
1619
Use: "list",
1720
Aliases: []string{"ls"},
1821
RunE: func(cmd *cobra.Command, args []string) error {
@@ -24,14 +27,14 @@ func userList() *cobra.Command {
2427
if err != nil {
2528
return err
2629
}
27-
sort.Slice(users, func(i, j int) bool {
28-
return users[i].Username < users[j].Username
29-
})
3030

31-
tableWriter := table.NewWriter()
32-
tableWriter.SetStyle(table.StyleLight)
33-
tableWriter.Style().Options.SeparateColumns = false
34-
tableWriter.AppendHeader(table.Row{"Username", "Email", "Created At"})
31+
tableWriter := cliui.Table()
32+
header := table.Row{"Username", "Email", "Created At"}
33+
tableWriter.AppendHeader(header)
34+
tableWriter.SetColumnConfigs(cliui.FilterTableColumns(header, columns))
35+
tableWriter.SortBy([]table.SortBy{{
36+
Name: "Username",
37+
}})
3538
for _, user := range users {
3639
tableWriter.AppendRow(table.Row{
3740
user.Username,
@@ -43,4 +46,7 @@ func userList() *cobra.Command {
4346
return err
4447
},
4548
}
49+
cmd.Flags().StringArrayVarP(&columns, "column", "c", nil,
50+
"Specify a column to filter in the table.")
51+
return cmd
4652
}

0 commit comments

Comments
 (0)