diff --git a/cmd/instance_list.go b/cmd/instance_list.go index 228739e..76fa7ec 100644 --- a/cmd/instance_list.go +++ b/cmd/instance_list.go @@ -2,9 +2,11 @@ package cmd import ( "fmt" + "os" "strconv" "cloudamqp-cli/client" + "cloudamqp-cli/internal/table" "github.com/spf13/cobra" ) @@ -33,50 +35,17 @@ var instanceListCmd = &cobra.Command{ return nil } - // Calculate column widths - idWidth := len("ID") - nameWidth := len("NAME") - planWidth := len("PLAN") - regionWidth := len("REGION") - - for _, instance := range instances { - idLen := len(strconv.Itoa(instance.ID)) - if idLen > idWidth { - idWidth = idLen - } - if len(instance.Name) > nameWidth { - nameWidth = len(instance.Name) - } - if len(instance.Plan) > planWidth { - planWidth = len(instance.Plan) - } - if len(instance.Region) > regionWidth { - regionWidth = len(instance.Region) - } - } - - // Add padding - idWidth += 2 - nameWidth += 2 - planWidth += 2 - regionWidth += 2 - - // Create format strings - headerFormat := fmt.Sprintf("%%-%ds %%-%ds %%-%ds %%-%ds\n", idWidth, nameWidth, planWidth, regionWidth) - rowFormat := fmt.Sprintf("%%-%dd %%-%ds %%-%ds %%-%ds\n", idWidth, nameWidth, planWidth, regionWidth) - - // Print table header - fmt.Printf(headerFormat, "ID", "NAME", "PLAN", "REGION") - fmt.Printf(headerFormat, "--", "----", "----", "------") - - // Print instance data + // Create table and populate data + t := table.New(os.Stdout, "ID", "NAME", "PLAN", "REGION") for _, instance := range instances { - fmt.Printf(rowFormat, - instance.ID, + t.AddRow( + strconv.Itoa(instance.ID), instance.Name, instance.Plan, - instance.Region) + instance.Region, + ) } + t.Print() return nil }, diff --git a/cmd/instance_nodes.go b/cmd/instance_nodes.go index 2a62bf9..3cc20bb 100644 --- a/cmd/instance_nodes.go +++ b/cmd/instance_nodes.go @@ -2,8 +2,10 @@ package cmd import ( "fmt" + "os" "cloudamqp-cli/client" + "cloudamqp-cli/internal/table" "github.com/spf13/cobra" ) @@ -47,11 +49,8 @@ var instanceNodesListCmd = &cobra.Command{ return nil } - // Print table header - fmt.Printf("%-20s %-12s %-10s %-10s %-15s\n", "NAME", "CONFIGURED", "RUNNING", "DISK_SIZE", "RABBITMQ_VERSION") - fmt.Printf("%-20s %-12s %-10s %-10s %-15s\n", "----", "----------", "-------", "---------", "----------------") - - // Print node data + // Create table and populate data + t := table.New(os.Stdout, "NAME", "CONFIGURED", "RUNNING", "DISK_SIZE", "RABBITMQ_VERSION") for _, node := range nodes { configured := "No" if node.Configured { @@ -62,13 +61,15 @@ var instanceNodesListCmd = &cobra.Command{ running = "Yes" } totalDisk := node.DiskSize + node.AdditionalDiskSize - fmt.Printf("%-20s %-12s %-10s %-10dGB %-15s\n", + t.AddRow( node.Name, configured, running, - totalDisk, - node.RabbitMQVersion) + fmt.Sprintf("%d GB", totalDisk), + node.RabbitMQVersion, + ) } + t.Print() return nil }, diff --git a/cmd/instance_plugins.go b/cmd/instance_plugins.go index 16f57c3..0fd2db2 100644 --- a/cmd/instance_plugins.go +++ b/cmd/instance_plugins.go @@ -2,8 +2,10 @@ package cmd import ( "fmt" + "os" "cloudamqp-cli/client" + "cloudamqp-cli/internal/table" "github.com/spf13/cobra" ) @@ -47,18 +49,16 @@ var instancePluginsListCmd = &cobra.Command{ return nil } - // Print table header - fmt.Printf("%-30s %-10s\n", "NAME", "ENABLED") - fmt.Printf("%-30s %-10s\n", "----", "-------") - - // Print plugin data + // Create table and populate data + t := table.New(os.Stdout, "NAME", "ENABLED") for _, plugin := range plugins { enabled := "No" if plugin.Enabled { enabled = "Yes" } - fmt.Printf("%-30s %-10s\n", plugin.Name, enabled) + t.AddRow(plugin.Name, enabled) } + t.Print() return nil }, diff --git a/cmd/plans.go b/cmd/plans.go index 97fc840..744a879 100644 --- a/cmd/plans.go +++ b/cmd/plans.go @@ -1,10 +1,11 @@ package cmd import ( - "encoding/json" "fmt" + "os" "cloudamqp-cli/client" + "cloudamqp-cli/internal/table" "github.com/spf13/cobra" ) @@ -36,12 +37,25 @@ var plansCmd = &cobra.Command{ return nil } - output, err := json.MarshalIndent(plans, "", " ") - if err != nil { - return fmt.Errorf("failed to format response: %v", err) + // Create table and populate data + t := table.New(os.Stdout, "NAME", "PRICE", "BACKEND", "SHARED") + for _, plan := range plans { + shared := "No" + if plan.Shared { + shared = "Yes" + } + price := fmt.Sprintf("$%.2f", plan.Price) + if plan.Price == 0 { + price = "Free" + } + t.AddRow( + plan.Name, + price, + plan.Backend, + shared, + ) } - - fmt.Printf("Available plans:\n%s\n", string(output)) + t.Print() return nil }, } diff --git a/cmd/team_list.go b/cmd/team_list.go index b89669d..f3f58b8 100644 --- a/cmd/team_list.go +++ b/cmd/team_list.go @@ -2,8 +2,11 @@ package cmd import ( "fmt" + "os" + "strings" "cloudamqp-cli/client" + "cloudamqp-cli/internal/table" "github.com/spf13/cobra" ) @@ -32,14 +35,20 @@ var teamListCmd = &cobra.Command{ return nil } - // Print table header - fmt.Printf("%-40s\n", "EMAIL") - fmt.Printf("%-40s\n", "-----") - - // Print team member data + // Create table and populate data + t := table.New(os.Stdout, "EMAIL", "ROLES", "2FA") for _, member := range members { - fmt.Printf("%-40s\n", member.Email) + roles := strings.Join(member.Roles, ", ") + if roles == "" { + roles = "-" + } + tfa := "No" + if member.TFAAuthEnabled { + tfa = "Yes" + } + t.AddRow(member.Email, roles, tfa) } + t.Print() return nil }, diff --git a/cmd/vpc_list.go b/cmd/vpc_list.go index 9d5229a..ea02724 100644 --- a/cmd/vpc_list.go +++ b/cmd/vpc_list.go @@ -2,8 +2,11 @@ package cmd import ( "fmt" + "os" + "strconv" "cloudamqp-cli/client" + "cloudamqp-cli/internal/table" "github.com/spf13/cobra" ) @@ -32,17 +35,17 @@ var vpcListCmd = &cobra.Command{ return nil } - // Print table header - fmt.Printf("%-20s %-18s %-30s\n", "NAME", "SUBNET", "REGION") - fmt.Printf("%-20s %-18s %-30s\n", "----", "------", "------") - - // Print VPC data + // Create table and populate data + t := table.New(os.Stdout, "ID", "NAME", "SUBNET", "REGION") for _, vpc := range vpcs { - fmt.Printf("%-20s %-18s %-30s\n", + t.AddRow( + strconv.Itoa(vpc.ID), vpc.Name, vpc.Subnet, - vpc.Region) + vpc.Region, + ) } + t.Print() return nil }, diff --git a/internal/table/table.go b/internal/table/table.go new file mode 100644 index 0000000..574a58a --- /dev/null +++ b/internal/table/table.go @@ -0,0 +1,87 @@ +package table + +import ( + "fmt" + "io" + "strings" +) + +// Column represents a column in the table +type Column struct { + Header string + Width int +} + +// Printer handles dynamic table printing with automatic width calculation +type Printer struct { + columns []Column + rows [][]string + writer io.Writer +} + +// New creates a new table printer +func New(writer io.Writer, headers ...string) *Printer { + columns := make([]Column, len(headers)) + for i, header := range headers { + columns[i] = Column{ + Header: header, + Width: len(header), + } + } + return &Printer{ + columns: columns, + rows: make([][]string, 0), + writer: writer, + } +} + +// AddRow adds a row of data to the table +func (p *Printer) AddRow(values ...string) error { + if len(values) != len(p.columns) { + return fmt.Errorf("expected %d columns, got %d", len(p.columns), len(values)) + } + + // Update column widths based on this row's values + for i, value := range values { + if len(value) > p.columns[i].Width { + p.columns[i].Width = len(value) + } + } + + p.rows = append(p.rows, values) + return nil +} + +// Print outputs the table with calculated column widths +func (p *Printer) Print() { + // Add padding to widths + for i := range p.columns { + p.columns[i].Width += 2 + } + + // Build format string + formatParts := make([]string, len(p.columns)) + for i, col := range p.columns { + formatParts[i] = fmt.Sprintf("%%-%ds", col.Width) + } + format := strings.Join(formatParts, " ") + "\n" + + // Print header + headers := make([]interface{}, len(p.columns)) + separators := make([]interface{}, len(p.columns)) + for i, col := range p.columns { + headers[i] = col.Header + separators[i] = strings.Repeat("-", col.Width) + } + fmt.Fprintf(p.writer, format, headers...) + fmt.Fprintf(p.writer, format, separators...) + + // Print rows + for _, row := range p.rows { + rowInterface := make([]interface{}, len(row)) + for i, v := range row { + rowInterface[i] = v + } + fmt.Fprintf(p.writer, format, rowInterface...) + } +} diff --git a/internal/table/table_test.go b/internal/table/table_test.go new file mode 100644 index 0000000..8ab893c --- /dev/null +++ b/internal/table/table_test.go @@ -0,0 +1,47 @@ +package table + +import ( + "bytes" + "strings" + "testing" +) + +func TestTablePrinter(t *testing.T) { + var buf bytes.Buffer + + // Create table with headers + p := New(&buf, "NAME", "CONFIGURED", "RUNNING", "DISK_SIZE", "RABBITMQ_VERSION") + + // Add rows + p.AddRow("dev-calm-olive-reindeer-01", "Yes", "Yes", "15 GB", "4.2.1") + p.AddRow("short-name", "No", "Yes", "20 GB", "3.8.0") + + // Print + p.Print() + + output := buf.String() + lines := strings.Split(strings.TrimSpace(output), "\n") + + // Verify we have header, separator, and 2 data rows + if len(lines) != 4 { + t.Errorf("Expected 4 lines, got %d", len(lines)) + } + + // Verify alignment - all columns should be properly aligned + if !strings.Contains(output, "NAME") { + t.Error("Missing NAME header") + } + if !strings.Contains(output, "dev-calm-olive-reindeer-01") { + t.Error("Missing first row data") + } +} + +func TestTablePrinterColumnMismatch(t *testing.T) { + var buf bytes.Buffer + p := New(&buf, "COL1", "COL2") + + err := p.AddRow("value1", "value2", "value3") + if err == nil { + t.Error("Expected error when adding row with wrong number of columns") + } +}