Skip to content

fix: Match kubectl table style for simpler scripting #1363

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 1 commit into from
May 10, 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
43 changes: 43 additions & 0 deletions cli/cliui/table.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package cliui

import (
"strings"

"github.com/jedib0t/go-pretty/v6/table"
)

// Table creates a new table with standardized styles.
func Table() table.Writer {
tableWriter := table.NewWriter()
Copy link
Contributor

Choose a reason for hiding this comment

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

Not to bikeshed on this, but Go had a stdlib solution for this: https://pkg.go.dev/text/tabwriter. Haven't taken a look at this package though

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh actually nvm, I thought u were totally switching out the table packages

Copy link
Member Author

Choose a reason for hiding this comment

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

Oh, nah. This package handles hiding columns and sorting which is kinda nice.

Copy link
Contributor

Choose a reason for hiding this comment

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

ye i agree, should've looked through more b4 commenting

tableWriter.Style().Box.PaddingLeft = ""
tableWriter.Style().Box.PaddingRight = " "
tableWriter.Style().Options.DrawBorder = false
tableWriter.Style().Options.SeparateHeader = false
tableWriter.Style().Options.SeparateColumns = false
return tableWriter
}

// FilterTableColumns returns configurations to hide columns
// that are not provided in the array. If the array is empty,
// no filtering will occur!
func FilterTableColumns(header table.Row, columns []string) []table.ColumnConfig {
if len(columns) == 0 {
return nil
}
columnConfigs := make([]table.ColumnConfig, 0)
for _, headerTextRaw := range header {
headerText, _ := headerTextRaw.(string)
hidden := true
for _, column := range columns {
if strings.EqualFold(strings.ReplaceAll(column, "_", " "), headerText) {
hidden = false
break
}
}
columnConfigs = append(columnConfigs, table.ColumnConfig{
Name: headerText,
Hidden: hidden,
})
}
return columnConfigs
}
79 changes: 61 additions & 18 deletions cli/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ package cli

import (
"fmt"
"strings"
"time"

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

Expand All @@ -12,7 +15,10 @@ import (
)

func list() *cobra.Command {
return &cobra.Command{
var (
columns []string
)
cmd := &cobra.Command{
Annotations: workspaceCommand,
Use: "list",
Short: "List all workspaces",
Expand All @@ -22,11 +28,7 @@ func list() *cobra.Command {
if err != nil {
return err
}
organization, err := currentOrganization(cmd, client)
if err != nil {
return err
}
workspaces, err := client.WorkspacesByOwner(cmd.Context(), organization.ID, codersdk.Me)
workspaces, err := client.WorkspacesByUser(cmd.Context(), codersdk.Me)
if err != nil {
return err
}
Expand All @@ -37,11 +39,22 @@ func list() *cobra.Command {
_, _ = fmt.Fprintln(cmd.OutOrStdout())
return nil
}
users, err := client.Users(cmd.Context(), codersdk.UsersRequest{})
if err != nil {
return err
}
usersByID := map[uuid.UUID]codersdk.User{}
for _, user := range users {
usersByID[user.ID] = user
}

tableWriter := table.NewWriter()
tableWriter.SetStyle(table.StyleLight)
tableWriter.Style().Options.SeparateColumns = false
tableWriter.AppendHeader(table.Row{"Workspace", "Template", "Status", "Last Built", "Outdated"})
tableWriter := cliui.Table()
header := table.Row{"workspace", "template", "status", "last built", "outdated"}
tableWriter.AppendHeader(header)
tableWriter.SortBy([]table.SortBy{{
Name: "workspace",
}})
tableWriter.SetColumnConfigs(cliui.FilterTableColumns(header, columns))

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

switch workspace.LatestBuild.Transition {
case database.WorkspaceTransitionStart:
status = "start"
status = "Running"
if inProgress {
status = "starting"
status = "Starting"
}
case database.WorkspaceTransitionStop:
status = "stop"
status = "Stopped"
if inProgress {
status = "stopping"
status = "Stopping"
}
case database.WorkspaceTransitionDelete:
status = "delete"
status = "Deleted"
if inProgress {
status = "deleting"
status = "Deleting"
}
}
if workspace.LatestBuild.Job.Status == codersdk.ProvisionerJobFailed {
status = "Failed"
}

duration := time.Now().UTC().Sub(workspace.LatestBuild.Job.CreatedAt).Truncate(time.Second)
if duration > time.Hour {
duration = duration.Truncate(time.Hour)
}
if duration > time.Minute {
duration = duration.Truncate(time.Minute)
}
days := 0
for duration.Hours() > 24 {
days++
duration -= 24 * time.Hour
}
durationDisplay := duration.String()
if days > 0 {
durationDisplay = fmt.Sprintf("%dd%s", days, durationDisplay)
}
if strings.HasSuffix(durationDisplay, "m0s") {
durationDisplay = durationDisplay[:len(durationDisplay)-2]
}
if strings.HasSuffix(durationDisplay, "h0m") {
durationDisplay = durationDisplay[:len(durationDisplay)-2]
}

user := usersByID[workspace.OwnerID]
tableWriter.AppendRow(table.Row{
cliui.Styles.Bold.Render(workspace.Name),
user.Username + "/" + workspace.Name,
workspace.TemplateName,
status,
workspace.LatestBuild.Job.CreatedAt.Format("January 2, 2006"),
durationDisplay,
workspace.Outdated,
})
}
_, err = fmt.Fprintln(cmd.OutOrStdout(), tableWriter.Render())
return err
},
}
cmd.Flags().StringArrayVarP(&columns, "column", "c", nil,
"Specify a column to filter in the table.")
return cmd
}
40 changes: 40 additions & 0 deletions cli/list_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package cli_test

import (
"testing"

"github.com/stretchr/testify/require"

"github.com/coder/coder/cli/clitest"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/pty/ptytest"
)

func TestList(t *testing.T) {
t.Parallel()
t.Run("Single", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
coderdtest.NewProvisionerDaemon(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
cmd, root := clitest.New(t, "ls")
clitest.SetupConfig(t, client, root)
doneChan := make(chan struct{})
pty := ptytest.New(t)
cmd.SetIn(pty.Input())
cmd.SetOut(pty.Output())
go func() {
defer close(doneChan)
err := cmd.Execute()
require.NoError(t, err)
}()
pty.ExpectMatch(workspace.Name)
pty.ExpectMatch("Running")
<-doneChan
})
}
2 changes: 2 additions & 0 deletions cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,9 @@ func Root() *cobra.Command {
cmd.PersistentFlags().String(varURL, "", "Specify the URL to your deployment.")
cmd.PersistentFlags().String(varToken, "", "Specify an authentication token.")
cliflag.String(cmd.PersistentFlags(), varAgentToken, "", "CODER_AGENT_TOKEN", "", "Specify an agent authentication token.")
_ = cmd.PersistentFlags().MarkHidden(varAgentToken)
cliflag.String(cmd.PersistentFlags(), varAgentURL, "", "CODER_AGENT_URL", "", "Specify the URL for an agent to access your deployment.")
_ = cmd.PersistentFlags().MarkHidden(varAgentURL)
cliflag.String(cmd.PersistentFlags(), varGlobalConfig, "", "CODER_CONFIG_DIR", configdir.LocalConfig("coderv2"), "Specify the path to the global `coder` config directory.")
cmd.PersistentFlags().Bool(varForceTty, false, "Force the `coder` command to run as if connected to a TTY.")
_ = cmd.PersistentFlags().MarkHidden(varForceTty)
Expand Down
9 changes: 3 additions & 6 deletions cli/templatelist.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,19 +34,16 @@ func templateList() *cobra.Command {
return nil
}

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

for _, template := range templates {
suffix := ""
if template.WorkspaceOwnerCount != 1 {
suffix = "s"
}
tableWriter.AppendRow(table.Row{
cliui.Styles.Bold.Render(template.Name),
"Archive",
template.Name,
template.UpdatedAt.Format("January 2, 2006"),
cliui.Styles.Fuschia.Render(fmt.Sprintf("%d developer%s", template.WorkspaceOwnerCount, suffix)),
})
Expand Down
24 changes: 15 additions & 9 deletions cli/userlist.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,20 @@ package cli

import (
"fmt"
"sort"
"time"

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

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

func userList() *cobra.Command {
return &cobra.Command{
var (
columns []string
)
cmd := &cobra.Command{
Use: "list",
Aliases: []string{"ls"},
RunE: func(cmd *cobra.Command, args []string) error {
Expand All @@ -24,14 +27,14 @@ func userList() *cobra.Command {
if err != nil {
return err
}
sort.Slice(users, func(i, j int) bool {
return users[i].Username < users[j].Username
})

tableWriter := table.NewWriter()
tableWriter.SetStyle(table.StyleLight)
tableWriter.Style().Options.SeparateColumns = false
tableWriter.AppendHeader(table.Row{"Username", "Email", "Created At"})
tableWriter := cliui.Table()
header := table.Row{"Username", "Email", "Created At"}
tableWriter.AppendHeader(header)
tableWriter.SetColumnConfigs(cliui.FilterTableColumns(header, columns))
tableWriter.SortBy([]table.SortBy{{
Name: "Username",
}})
for _, user := range users {
tableWriter.AppendRow(table.Row{
user.Username,
Expand All @@ -43,4 +46,7 @@ func userList() *cobra.Command {
return err
},
}
cmd.Flags().StringArrayVarP(&columns, "column", "c", nil,
"Specify a column to filter in the table.")
Comment on lines +49 to +50
Copy link
Contributor

Choose a reason for hiding this comment

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

To filter by multiple columns, do you need to specify the flag multiple times? Or can it accept a comma delimited string? I think we should document how to specify multiple.

Copy link
Member Author

Choose a reason for hiding this comment

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

You have to specify multiple, but the flag does specify the type as doing so.

return cmd
}