Skip to content

Commit 072abe8

Browse files
gregnrsweatybridge
andauthored
fix: grouped pretty print status output (#4276)
* feat: grouped pretty print status output * fix: re-gen openapi types * fix: lints * chore: show functions url only if enabled * chore: skip rendering group on error --------- Co-authored-by: Qiao Han <qiao@supabase.io>
1 parent 1bbf497 commit 072abe8

File tree

4 files changed

+169
-37
lines changed

4 files changed

+169
-37
lines changed

go.mod

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ require (
3939
github.com/mithrandie/csvq-driver v1.7.0
4040
github.com/muesli/reflow v0.3.0
4141
github.com/oapi-codegen/nullable v1.1.0
42+
github.com/olekukonko/tablewriter v1.1.0
4243
github.com/slack-go/slack v0.17.3
4344
github.com/spf13/afero v1.15.0
4445
github.com/spf13/cobra v1.10.1
@@ -315,7 +316,8 @@ require (
315316
github.com/oapi-codegen/runtime v1.1.2 // indirect
316317
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect
317318
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect
318-
github.com/olekukonko/tablewriter v0.0.5 // indirect
319+
github.com/olekukonko/errors v1.1.0 // indirect
320+
github.com/olekukonko/ll v0.0.9 // indirect
319321
github.com/opencontainers/go-digest v1.0.0 // indirect
320322
github.com/opencontainers/image-spec v1.1.1 // indirect
321323
github.com/pelletier/go-toml v1.9.5 // indirect

go.sum

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -719,7 +719,6 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
719719
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
720720
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
721721
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
722-
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
723722
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
724723
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
725724
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
@@ -829,8 +828,12 @@ github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//J
829828
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw=
830829
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c=
831830
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o=
832-
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
833-
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
831+
github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM=
832+
github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
833+
github.com/olekukonko/ll v0.0.9 h1:Y+1YqDfVkqMWuEQMclsF9HUR5+a82+dxJuL1HHSRpxI=
834+
github.com/olekukonko/ll v0.0.9/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g=
835+
github.com/olekukonko/tablewriter v1.1.0 h1:N0LHrshF4T39KvI96fn6GT8HEjXRXYNDrDjKFDB7RIY=
836+
github.com/olekukonko/tablewriter v1.1.0/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo=
834837
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
835838
github.com/onsi/ginkgo v1.10.2/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
836839
github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg=

internal/status/status.go

Lines changed: 156 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,16 @@ import (
1010
"net/http"
1111
"net/url"
1212
"os"
13-
"reflect"
1413
"slices"
15-
"strings"
1614
"sync"
1715
"time"
1816

17+
"github.com/Netflix/go-env"
1918
"github.com/docker/docker/api/types"
2019
"github.com/docker/docker/api/types/container"
2120
"github.com/go-errors/errors"
21+
"github.com/olekukonko/tablewriter"
22+
"github.com/olekukonko/tablewriter/tw"
2223
"github.com/spf13/afero"
2324
"github.com/supabase/cli/internal/utils"
2425
"github.com/supabase/cli/internal/utils/flags"
@@ -27,9 +28,11 @@ import (
2728

2829
type CustomName struct {
2930
ApiURL string `env:"api.url,default=API_URL"`
31+
RestURL string `env:"api.rest_url,default=REST_URL"`
3032
GraphqlURL string `env:"api.graphql_url,default=GRAPHQL_URL"`
3133
StorageS3URL string `env:"api.storage_s3_url,default=STORAGE_S3_URL"`
3234
McpURL string `env:"api.mcp_url,default=MCP_URL"`
35+
FunctionsURL string `env:"api.functions_url,default=FUNCTIONS_URL"`
3336
DbURL string `env:"db.url,default=DB_URL"`
3437
StudioURL string `env:"studio.url,default=STUDIO_URL"`
3538
InbucketURL string `env:"inbucket.url,default=INBUCKET_URL,deprecated"`
@@ -54,10 +57,15 @@ func (c *CustomName) toValues(exclude ...string) map[string]string {
5457
authEnabled := utils.Config.Auth.Enabled && !slices.Contains(exclude, utils.GotrueId) && !slices.Contains(exclude, utils.ShortContainerImageName(utils.Config.Auth.Image))
5558
inbucketEnabled := utils.Config.Inbucket.Enabled && !slices.Contains(exclude, utils.InbucketId) && !slices.Contains(exclude, utils.ShortContainerImageName(utils.Config.Inbucket.Image))
5659
storageEnabled := utils.Config.Storage.Enabled && !slices.Contains(exclude, utils.StorageId) && !slices.Contains(exclude, utils.ShortContainerImageName(utils.Config.Storage.Image))
60+
functionsEnabled := utils.Config.EdgeRuntime.Enabled && !slices.Contains(exclude, utils.EdgeRuntimeId) && !slices.Contains(exclude, utils.ShortContainerImageName(utils.Config.EdgeRuntime.Image))
5761

5862
if apiEnabled {
5963
values[c.ApiURL] = utils.Config.Api.ExternalUrl
64+
values[c.RestURL] = utils.GetApiUrl("/rest/v1")
6065
values[c.GraphqlURL] = utils.GetApiUrl("/graphql/v1")
66+
if functionsEnabled {
67+
values[c.FunctionsURL] = utils.GetApiUrl("/functions/v1")
68+
}
6169
if studioEnabled {
6270
values[c.McpURL] = utils.GetApiUrl("/mcp")
6371
}
@@ -210,44 +218,159 @@ func printStatus(names CustomName, format string, w io.Writer, exclude ...string
210218
}
211219

212220
func PrettyPrint(w io.Writer, exclude ...string) {
213-
names := CustomName{
214-
ApiURL: " " + utils.Aqua("API URL"),
215-
GraphqlURL: " " + utils.Aqua("GraphQL URL"),
216-
StorageS3URL: " " + utils.Aqua("S3 Storage URL"),
217-
McpURL: " " + utils.Aqua("MCP URL"),
218-
DbURL: " " + utils.Aqua("Database URL"),
219-
StudioURL: " " + utils.Aqua("Studio URL"),
220-
InbucketURL: " " + utils.Aqua("Inbucket URL"),
221-
MailpitURL: " " + utils.Aqua("Mailpit URL"),
222-
PublishableKey: " " + utils.Aqua("Publishable key"),
223-
SecretKey: " " + utils.Aqua("Secret key"),
224-
JWTSecret: " " + utils.Aqua("JWT secret"),
225-
AnonKey: " " + utils.Aqua("anon key"),
226-
ServiceRoleKey: "" + utils.Aqua("service_role key"),
227-
StorageS3AccessKeyId: " " + utils.Aqua("S3 Access Key"),
228-
StorageS3SecretAccessKey: " " + utils.Aqua("S3 Secret Key"),
229-
StorageS3Region: " " + utils.Aqua("S3 Region"),
221+
logger := utils.GetDebugLogger()
222+
223+
names := CustomName{}
224+
if err := env.Unmarshal(env.EnvSet{}, &names); err != nil {
225+
fmt.Fprintln(logger, err)
230226
}
231227
values := names.toValues(exclude...)
232-
// Iterate through map in order of declared struct fields
233-
t := reflect.TypeOf(names)
234-
val := reflect.ValueOf(names)
235-
for i := 0; i < val.NumField(); i++ {
236-
k := val.Field(i).String()
237-
if tag := t.Field(i).Tag.Get("env"); isDeprecated(tag) {
228+
229+
groups := []OutputGroup{
230+
{
231+
Name: "🛠️ Development Tools",
232+
Items: []OutputItem{
233+
{Label: "Studio", Value: values[names.StudioURL], Type: Link},
234+
{Label: "Mailpit", Value: values[names.MailpitURL], Type: Link},
235+
{Label: "MCP", Value: values[names.McpURL], Type: Link},
236+
},
237+
},
238+
{
239+
Name: "🌐 APIs",
240+
Items: []OutputItem{
241+
{Label: "Project URL", Value: values[names.ApiURL], Type: Link},
242+
{Label: "REST", Value: values[names.RestURL], Type: Link},
243+
{Label: "GraphQL", Value: values[names.GraphqlURL], Type: Link},
244+
{Label: "Edge Functions", Value: values[names.FunctionsURL], Type: Link},
245+
},
246+
},
247+
{
248+
Name: "🗄️ Database",
249+
Items: []OutputItem{
250+
{Label: "URL", Value: values[names.DbURL], Type: Link},
251+
},
252+
},
253+
{
254+
Name: "🔑 Authentication Keys",
255+
Items: []OutputItem{
256+
{Label: "Publishable", Value: values[names.PublishableKey], Type: Key},
257+
{Label: "Secret", Value: values[names.SecretKey], Type: Key},
258+
},
259+
},
260+
{
261+
Name: "📦 Storage (S3)",
262+
Items: []OutputItem{
263+
{Label: "URL", Value: values[names.StorageS3URL], Type: Link},
264+
{Label: "Access Key", Value: values[names.StorageS3AccessKeyId], Type: Key},
265+
{Label: "Secret Key", Value: values[names.StorageS3SecretAccessKey], Type: Key},
266+
{Label: "Region", Value: values[names.StorageS3Region], Type: Text},
267+
},
268+
},
269+
}
270+
271+
for _, group := range groups {
272+
if err := group.printTable(w); err != nil {
273+
fmt.Fprintln(logger, err)
274+
} else {
275+
fmt.Fprintln(w)
276+
}
277+
}
278+
}
279+
280+
type OutputType string
281+
282+
const (
283+
Text OutputType = "text"
284+
Link OutputType = "link"
285+
Key OutputType = "key"
286+
)
287+
288+
type OutputItem struct {
289+
Label string
290+
Value string
291+
Type OutputType
292+
}
293+
294+
type OutputGroup struct {
295+
Name string
296+
Items []OutputItem
297+
}
298+
299+
func (g *OutputGroup) printTable(w io.Writer) error {
300+
table := tablewriter.NewTable(w,
301+
// Rounded corners
302+
tablewriter.WithSymbols(tw.NewSymbols(tw.StyleRounded)),
303+
304+
// Table content formatting
305+
tablewriter.WithConfig(tablewriter.Config{
306+
Header: tw.CellConfig{
307+
Formatting: tw.CellFormatting{
308+
AutoFormat: tw.Off,
309+
MergeMode: tw.MergeHorizontal,
310+
},
311+
Alignment: tw.CellAlignment{
312+
Global: tw.AlignLeft,
313+
},
314+
Filter: tw.CellFilter{
315+
Global: func(s []string) []string {
316+
for i := range s {
317+
s[i] = utils.Bold(s[i])
318+
}
319+
return s
320+
},
321+
},
322+
},
323+
Row: tw.CellConfig{
324+
Alignment: tw.CellAlignment{
325+
Global: tw.AlignLeft,
326+
},
327+
ColMaxWidths: tw.CellWidth{
328+
PerColumn: map[int]int{0: 16},
329+
},
330+
Filter: tw.CellFilter{
331+
PerColumn: []func(string) string{
332+
func(s string) string {
333+
return utils.Green(s)
334+
},
335+
},
336+
},
337+
},
338+
Behavior: tw.Behavior{
339+
Compact: tw.Compact{
340+
Merge: tw.On,
341+
},
342+
},
343+
}),
344+
345+
// Set title as header (merged across all columns)
346+
tablewriter.WithHeader([]string{g.Name, g.Name}),
347+
)
348+
349+
// Add data rows with values colored based on type
350+
shouldRender := false
351+
for _, row := range g.Items {
352+
if row.Value == "" {
238353
continue
239354
}
240-
if v, ok := values[k]; ok {
241-
fmt.Fprintf(w, "%s: %s\n", k, v)
355+
value := row.Value
356+
switch row.Type {
357+
case Link:
358+
value = utils.Aqua(row.Value)
359+
case Key:
360+
value = utils.Yellow(row.Value)
361+
}
362+
if err := table.Append(row.Label, value); err != nil {
363+
return errors.Errorf("failed to append row: %w", err)
242364
}
365+
shouldRender = true
243366
}
244-
}
245367

246-
func isDeprecated(tag string) bool {
247-
for part := range strings.SplitSeq(tag, ",") {
248-
if strings.EqualFold(part, "deprecated") {
249-
return true
368+
// Ensure at least one item in the group is non-empty
369+
if shouldRender {
370+
if err := table.Render(); err != nil {
371+
return errors.Errorf("failed to render table: %w", err)
250372
}
251373
}
252-
return false
374+
375+
return nil
253376
}

internal/utils/colors.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ func Yellow(str string) string {
1313
return lipgloss.NewStyle().Foreground(lipgloss.Color("11")).Render(str)
1414
}
1515

16+
func Green(str string) string {
17+
return lipgloss.NewStyle().Foreground(lipgloss.Color("10")).Render(str)
18+
}
19+
1620
// For errors.
1721
func Red(str string) string {
1822
return lipgloss.NewStyle().Foreground(lipgloss.Color("9")).Render(str)

0 commit comments

Comments
 (0)