Skip to content

Commit 6371686

Browse files
committed
Add human-readable table output for CLI commands
Replace JSON output with formatted tables for better readability: - Add format package with table rendering and color utilities - Add global --output (-o) flag to choose table/json/wide formats - Add --no-color flag with NO_COLOR env var support - Convert clone list/status to table output - Convert snapshot list to table output - Convert instance status to table output - Improve branch list and config list formatting Default output is now human-readable tables. Use --output=json or -o json for machine-readable JSON output (backwards compatible).
1 parent 4df2616 commit 6371686

File tree

10 files changed

+576
-77
lines changed

10 files changed

+576
-77
lines changed

engine/cmd/cli/commands/branch/actions.go

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@ import (
1313
"text/template"
1414
"time"
1515

16+
"github.com/fatih/color"
1617
"github.com/urfave/cli/v2"
1718

1819
"gitlab.com/postgres-ai/database-lab/v3/cmd/cli/commands"
1920
"gitlab.com/postgres-ai/database-lab/v3/cmd/cli/commands/config"
21+
"gitlab.com/postgres-ai/database-lab/v3/cmd/cli/commands/format"
2022
"gitlab.com/postgres-ai/database-lab/v3/pkg/client/dblabapi/types"
2123
"gitlab.com/postgres-ai/database-lab/v3/pkg/models"
2224
"gitlab.com/postgres-ai/database-lab/v3/pkg/util"
@@ -116,18 +118,22 @@ func list(cliCtx *cli.Context) error {
116118

117119
func formatBranchList(cliCtx *cli.Context, branches []string) string {
118120
baseBranch := getBaseBranch(cliCtx)
121+
cfg := format.FromContext(cliCtx)
119122

120123
s := strings.Builder{}
124+
green := color.New(color.FgGreen, color.Bold).SprintFunc()
121125

122126
for _, branch := range branches {
123-
var prefixStar = " "
127+
prefix := " "
124128

125129
if baseBranch == branch {
126-
prefixStar = "* "
127-
branch = "\033[1;32m" + branch + "\033[0m"
130+
prefix = "* "
131+
if !cfg.NoColor {
132+
branch = green(branch)
133+
}
128134
}
129135

130-
s.WriteString(prefixStar + branch + "\n")
136+
s.WriteString(prefix + branch + "\n")
131137
}
132138

133139
return s.String()

engine/cmd/cli/commands/clone/actions.go

Lines changed: 125 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"github.com/urfave/cli/v2"
2020

2121
"gitlab.com/postgres-ai/database-lab/v3/cmd/cli/commands"
22+
"gitlab.com/postgres-ai/database-lab/v3/cmd/cli/commands/format"
2223
"gitlab.com/postgres-ai/database-lab/v3/internal/observer"
2324
"gitlab.com/postgres-ai/database-lab/v3/pkg/client/dblabapi/types"
2425
"gitlab.com/postgres-ai/database-lab/v3/pkg/log"
@@ -49,12 +50,78 @@ func list(cliCtx *cli.Context) error {
4950
return err
5051
}
5152

52-
commandResponse, err := json.MarshalIndent(viewCloneList.Cloning.Clones, "", " ")
53+
cfg := format.FromContext(cliCtx)
54+
55+
if cfg.IsJSON() {
56+
return outputJSON(cliCtx.App.Writer, viewCloneList.Cloning.Clones)
57+
}
58+
59+
return printCloneList(cfg, viewCloneList.Cloning.Clones)
60+
}
61+
62+
func printCloneList(cfg format.Config, clones []*models.CloneView) error {
63+
if len(clones) == 0 {
64+
_, err := fmt.Fprintln(cfg.Writer, "No clones found.")
65+
return err
66+
}
67+
68+
t := format.NewTable(cfg.Writer, cfg.NoColor)
69+
70+
if cfg.IsWide() {
71+
t.SetHeaders("ID", "STATUS", "BRANCH", "SNAPSHOT", "SIZE", "DB", "PORT", "CREATED")
72+
} else {
73+
t.SetHeaders("ID", "STATUS", "BRANCH", "SNAPSHOT", "SIZE", "CREATED")
74+
}
75+
76+
for _, clone := range clones {
77+
snapshotID := ""
78+
if clone.Snapshot != nil {
79+
snapshotID = format.Truncate(clone.Snapshot.ID, 16)
80+
}
81+
82+
created := ""
83+
if clone.CreatedAt != nil {
84+
created = format.FormatTime(clone.CreatedAt.Time)
85+
}
86+
87+
status := format.FormatStatus(string(clone.Status.Code), cfg.NoColor)
88+
size := format.FormatBytes(uint64(clone.Metadata.CloneDiffSize))
89+
90+
if cfg.IsWide() {
91+
t.Append([]string{
92+
clone.ID,
93+
status,
94+
clone.Branch,
95+
snapshotID,
96+
size,
97+
clone.DB.DBName,
98+
clone.DB.Port,
99+
created,
100+
})
101+
} else {
102+
t.Append([]string{
103+
clone.ID,
104+
status,
105+
clone.Branch,
106+
snapshotID,
107+
size,
108+
created,
109+
})
110+
}
111+
}
112+
113+
t.Render()
114+
115+
return nil
116+
}
117+
118+
func outputJSON(w io.Writer, v any) error {
119+
data, err := json.MarshalIndent(v, "", " ")
53120
if err != nil {
54121
return err
55122
}
56123

57-
_, err = fmt.Fprintln(cliCtx.App.Writer, string(commandResponse))
124+
_, err = fmt.Fprintln(w, string(data))
58125

59126
return err
60127
}
@@ -79,14 +146,65 @@ func status(cliCtx *cli.Context) error {
79146
return err
80147
}
81148

82-
commandResponse, err := json.MarshalIndent(cloneView, "", " ")
83-
if err != nil {
84-
return err
149+
cfg := format.FromContext(cliCtx)
150+
151+
if cfg.IsJSON() {
152+
return outputJSON(cliCtx.App.Writer, cloneView)
85153
}
86154

87-
_, err = fmt.Fprintln(cliCtx.App.Writer, string(commandResponse))
155+
return printCloneStatus(cfg, cloneView)
156+
}
88157

89-
return err
158+
func printCloneStatus(cfg format.Config, clone *models.CloneView) error {
159+
w := cfg.Writer
160+
161+
status := format.FormatStatus(string(clone.Status.Code), cfg.NoColor)
162+
163+
fmt.Fprintf(w, "ID: %s\n", clone.ID)
164+
fmt.Fprintf(w, "Status: %s\n", status)
165+
166+
if clone.Status.Message != "" {
167+
fmt.Fprintf(w, "Message: %s\n", clone.Status.Message)
168+
}
169+
170+
fmt.Fprintf(w, "Branch: %s\n", clone.Branch)
171+
172+
if clone.Snapshot != nil {
173+
fmt.Fprintf(w, "Snapshot: %s\n", clone.Snapshot.ID)
174+
}
175+
176+
fmt.Fprintf(w, "Protected: %s\n", format.FormatBool(clone.Protected, cfg.NoColor))
177+
178+
if clone.CreatedAt != nil {
179+
fmt.Fprintf(w, "Created: %s (%s)\n", format.FormatTimeAbs(clone.CreatedAt.Time), format.FormatTime(clone.CreatedAt.Time))
180+
}
181+
182+
if clone.DeleteAt != nil {
183+
fmt.Fprintf(w, "Delete at: %s\n", format.FormatTimeAbs(clone.DeleteAt.Time))
184+
}
185+
186+
fmt.Fprintln(w)
187+
fmt.Fprintln(w, "Database:")
188+
fmt.Fprintf(w, " Host: %s\n", clone.DB.Host)
189+
fmt.Fprintf(w, " Port: %s\n", clone.DB.Port)
190+
fmt.Fprintf(w, " Username: %s\n", clone.DB.Username)
191+
fmt.Fprintf(w, " Database: %s\n", clone.DB.DBName)
192+
193+
if clone.DB.ConnStr != "" {
194+
fmt.Fprintf(w, " ConnStr: %s\n", clone.DB.ConnStr)
195+
}
196+
197+
fmt.Fprintln(w)
198+
fmt.Fprintln(w, "Metadata:")
199+
fmt.Fprintf(w, " Diff size: %s\n", format.FormatBytes(uint64(clone.Metadata.CloneDiffSize)))
200+
fmt.Fprintf(w, " Logical size: %s\n", format.FormatBytes(uint64(clone.Metadata.LogicalSize)))
201+
fmt.Fprintf(w, " Cloning time: %.2fs\n", clone.Metadata.CloningTime)
202+
203+
if clone.Metadata.MaxIdleMinutes > 0 {
204+
fmt.Fprintf(w, " Max idle: %d min\n", clone.Metadata.MaxIdleMinutes)
205+
}
206+
207+
return nil
90208
}
91209

92210
// create runs a request to create a new clone.

engine/cmd/cli/commands/config/actions.go

Lines changed: 28 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,11 @@ import (
99
"encoding/json"
1010
"fmt"
1111
"sort"
12-
"strings"
1312

1413
"github.com/urfave/cli/v2"
1514

1615
"gitlab.com/postgres-ai/database-lab/v3/cmd/cli/commands"
17-
)
18-
19-
// headers of a config list.
20-
const (
21-
envHeader = "ENV "
22-
urlHeader = "URL"
23-
fwServerHeader = "Forwarding server URL"
24-
fwPortHeader = "Forwarding local port"
16+
"gitlab.com/postgres-ai/database-lab/v3/cmd/cli/commands/format"
2517
)
2618

2719
// createEnvironment creates a new CLI environment.
@@ -126,73 +118,47 @@ func list() func(*cli.Context) error {
126118
return commands.ToActionError(err)
127119
}
128120

121+
if len(cfg.Environments) == 0 {
122+
_, err = fmt.Fprintln(cliCtx.App.Writer, "No environments configured.")
123+
return commands.ToActionError(err)
124+
}
125+
129126
environmentNames := make([]string, 0, len(cfg.Environments))
130-
maxNameLen := 0
131-
maxURLLen := len(urlHeader)
132-
maxFwServerLen := len(fwServerHeader)
133127

134128
for environmentName := range cfg.Environments {
135129
environmentNames = append(environmentNames, environmentName)
136-
137-
nameLength := len(environmentName)
138-
if maxNameLen < nameLength {
139-
maxNameLen = nameLength
140-
}
141-
142-
urlLength := len(cfg.Environments[environmentName].URL)
143-
if maxURLLen < urlLength {
144-
maxURLLen = urlLength
145-
}
146-
147-
urlFwLength := len(cfg.Environments[environmentName].Forwarding.ServerURL)
148-
if maxFwServerLen < urlFwLength {
149-
maxFwServerLen = urlFwLength
150-
}
151130
}
152131

153132
sort.Strings(environmentNames)
154133

155-
listOutput := buildListOutput(cfg, environmentNames, maxNameLen, maxURLLen, maxFwServerLen)
134+
fmtCfg := format.FromContext(cliCtx)
156135

157-
_, err = fmt.Fprintf(cliCtx.App.Writer, "Available CLI environments:\n%s", listOutput)
136+
_, _ = fmt.Fprintln(cliCtx.App.Writer, "Available CLI environments:")
158137

159-
return commands.ToActionError(err)
160-
}
161-
}
138+
t := format.NewTable(cliCtx.App.Writer, fmtCfg.NoColor)
139+
t.SetHeaders("", "ENV", "URL", "FORWARDING SERVER", "LOCAL PORT")
140+
141+
for _, envName := range environmentNames {
142+
env := cfg.Environments[envName]
143+
marker := ""
162144

163-
func buildListOutput(cfg *CLIConfig, environmentNames []string, maxNameLen, maxURLLen, maxFwLen int) string {
164-
// TODO(akartasov): Draw as a table.
165-
const outputAlign = 2
166-
167-
s := strings.Builder{}
168-
169-
s.WriteString(envHeader)
170-
s.WriteString(strings.Repeat(" ", maxNameLen+outputAlign))
171-
s.WriteString(urlHeader)
172-
s.WriteString(strings.Repeat(" ", maxURLLen-len(urlHeader)+outputAlign))
173-
s.WriteString(fwServerHeader)
174-
s.WriteString(strings.Repeat(" ", maxFwLen-len(fwServerHeader)+outputAlign))
175-
s.WriteString(fwPortHeader)
176-
s.WriteString("\n")
177-
178-
for _, environmentName := range environmentNames {
179-
if environmentName == cfg.CurrentEnvironment {
180-
s.WriteString("[*] ")
181-
} else {
182-
s.WriteString("[ ] ")
145+
if envName == cfg.CurrentEnvironment {
146+
marker = "*"
147+
}
148+
149+
t.Append([]string{
150+
marker,
151+
envName,
152+
env.URL,
153+
env.Forwarding.ServerURL,
154+
env.Forwarding.LocalPort,
155+
})
183156
}
184157

185-
s.WriteString(environmentName)
186-
s.WriteString(strings.Repeat(" ", maxNameLen-len(environmentName)+outputAlign))
187-
s.WriteString(cfg.Environments[environmentName].URL)
188-
s.WriteString(strings.Repeat(" ", maxURLLen-len(cfg.Environments[environmentName].URL)+outputAlign))
189-
s.WriteString(cfg.Environments[environmentName].Forwarding.ServerURL)
190-
s.WriteString(strings.Repeat(" ", maxFwLen-len(cfg.Environments[environmentName].Forwarding.ServerURL)+outputAlign))
191-
s.WriteString(cfg.Environments[environmentName].Forwarding.LocalPort)
192-
s.WriteString("\n")
193-
}
158+
t.Render()
194159

195-
return s.String()
160+
return nil
161+
}
196162
}
197163

198164
// switchEnvironment switches to another CLI environment.

0 commit comments

Comments
 (0)