From c32d12f5ccdb46b62dc0a6c1f6abdfc22e6a5d11 Mon Sep 17 00:00:00 2001 From: Pulak Kanti Bhowmick Date: Sun, 8 Dec 2024 17:38:33 +0600 Subject: [PATCH 01/15] improve `atmos list components` view Signed-off-by: Pulak Kanti Bhowmick --- cmd/list_components.go | 7 ++- examples/quick-start-advanced/atmos.yaml | 10 ++++ pkg/list/list_components.go | 76 ++++++++++++++++++++++-- pkg/schema/schema.go | 14 ++++- 4 files changed, 98 insertions(+), 9 deletions(-) diff --git a/cmd/list_components.go b/cmd/list_components.go index 03cf6feff..2006c7bee 100644 --- a/cmd/list_components.go +++ b/cmd/list_components.go @@ -3,13 +3,14 @@ package cmd import ( "fmt" + "github.com/fatih/color" + "github.com/spf13/cobra" + e "github.com/cloudposse/atmos/internal/exec" "github.com/cloudposse/atmos/pkg/config" l "github.com/cloudposse/atmos/pkg/list" "github.com/cloudposse/atmos/pkg/schema" u "github.com/cloudposse/atmos/pkg/utils" - "github.com/fatih/color" - "github.com/spf13/cobra" ) // listComponentsCmd lists atmos components @@ -38,7 +39,7 @@ var listComponentsCmd = &cobra.Command{ return } - output, err := l.FilterAndListComponents(stackFlag, stacksMap) + output, err := l.FilterAndListComponents(stackFlag, stacksMap, cliConfig.Components.List) if err != nil { u.PrintMessageInColor(fmt.Sprintf("Error: %v"+"\n", err), color.New(color.FgYellow)) return diff --git a/examples/quick-start-advanced/atmos.yaml b/examples/quick-start-advanced/atmos.yaml index d0383d510..398571d66 100644 --- a/examples/quick-start-advanced/atmos.yaml +++ b/examples/quick-start-advanced/atmos.yaml @@ -18,6 +18,16 @@ base_path: "." components: + list: + columns: + - name: Component + value: '{{ .atmos_component }}' + - name: Type + value: '{{ .atmos_component_type }}' + - name: Stack + value: '{{ .atmos_stack }}' + - name: Folder + value: '{{ .vars.tenant }}' terraform: # Optional `command` specifies the executable to be called by `atmos` when running Terraform commands # If not defined, `terraform` is used diff --git a/pkg/list/list_components.go b/pkg/list/list_components.go index 9b71ee4d3..2d4591e89 100644 --- a/pkg/list/list_components.go +++ b/pkg/list/list_components.go @@ -2,14 +2,19 @@ package list import ( "fmt" + "os" + "regexp" "sort" "strings" + "text/tabwriter" "github.com/samber/lo" + + "github.com/cloudposse/atmos/pkg/schema" ) // getStackComponents extracts Terraform components from the final map of stacks -func getStackComponents(stackData any) ([]string, error) { +func getStackComponents(stackData any, listConfig schema.ListConfig) ([]string, error) { stackMap, ok := stackData.(map[string]any) if !ok { return nil, fmt.Errorf("could not parse stacks") @@ -25,17 +30,80 @@ func getStackComponents(stackData any) ([]string, error) { return nil, fmt.Errorf("could not parse Terraform components") } + writer := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', tabwriter.AlignRight) + header := make([]string, 0) + values := make([]string, 0) + + re := regexp.MustCompile(`\{\{\s*(.*?)\s*\}\}`) + + for _, v := range listConfig.Columns { + header = append(header, v.Name) + match := re.FindStringSubmatch(v.Value) + + if len(match) > 1 { + values = append(values, match[1]) + } else { + return nil, fmt.Errorf("invalid value format for column name %s", v.Name) + } + } + fmt.Fprintln(writer, strings.Join(header, "\t\t")) + + uniqueKeys := lo.Keys(terraformComponents) + + for _, dataKey := range uniqueKeys { + data := terraformComponents[dataKey] + result := make([]string, 0) + for _, key := range values { + value, found := resolveKey(data.(map[string]any), key) + if !found { + value = "-" + } + result = append(result, fmt.Sprintf("%s", value)) + } + fmt.Fprintln(writer, strings.Join(result, "\t\t")) + } + writer.Flush() return lo.Keys(terraformComponents), nil } +// resolveKey resolves a key from a map, supporting nested keys with dot notation +func resolveKey(data map[string]any, key string) (any, bool) { + // Remove leading dot from the key (e.g., `.vars.tenant` -> `vars.tenant`) + key = strings.TrimPrefix(key, ".") + + // Split key on `.` + parts := strings.Split(key, ".") + current := data + + // Traverse the map for each part + for i, part := range parts { + if i == len(parts)-1 { + // Return the value for the last part + if value, exists := current[part]; exists { + return value, true + } + return nil, false + } + + // Traverse deeper + if nestedMap, ok := current[part].(map[string]any); ok { + current = nestedMap + } else { + return nil, false + } + } + + return nil, false +} + // FilterAndListComponents filters and lists components based on the given stack -func FilterAndListComponents(stackFlag string, stacksMap map[string]any) (string, error) { +func FilterAndListComponents(stackFlag string, stacksMap map[string]any, listConfig schema.ListConfig) (string, error) { components := []string{} if stackFlag != "" { // Filter components for the specified stack if stackData, ok := stacksMap[stackFlag]; ok { - stackComponents, err := getStackComponents(stackData) + stackComponents, err := getStackComponents(stackData, listConfig) if err != nil { return "", fmt.Errorf("error processing stack '%s': %w", stackFlag, err) } @@ -46,7 +114,7 @@ func FilterAndListComponents(stackFlag string, stacksMap map[string]any) (string } else { // Get all components from all stacks for _, stackData := range stacksMap { - stackComponents, err := getStackComponents(stackData) + stackComponents, err := getStackComponents(stackData, listConfig) if err != nil { continue // Skip invalid stacks } diff --git a/pkg/schema/schema.go b/pkg/schema/schema.go index 7eacc76ff..a39060613 100644 --- a/pkg/schema/schema.go +++ b/pkg/schema/schema.go @@ -90,8 +90,18 @@ type Helmfile struct { } type Components struct { - Terraform Terraform `yaml:"terraform" json:"terraform" mapstructure:"terraform"` - Helmfile Helmfile `yaml:"helmfile" json:"helmfile" mapstructure:"helmfile"` + Terraform Terraform `yaml:"terraform" json:"terraform" mapstructure:"terraform"` + Helmfile Helmfile `yaml:"helmfile" json:"helmfile" mapstructure:"helmfile"` + List ListConfig `yaml:"list" json:"list" mapstructure:"list"` +} + +type ListConfig struct { + Columns []ListColumnConfig `yaml:"columns" json:"columns" mapstructure:"columns"` +} + +type ListColumnConfig struct { + Name string `yaml:"name" json:"name" mapstructure:"name"` + Value string `yaml:"value" json:"value" mapstructure:"value"` } type Stacks struct { From 53c74ef98469d7a64a4160a8ecd6ab0678256ace Mon Sep 17 00:00:00 2001 From: Pulak Kanti Bhowmick Date: Sun, 8 Dec 2024 19:32:45 +0600 Subject: [PATCH 02/15] refactor code Signed-off-by: Pulak Kanti Bhowmick --- examples/quick-start-advanced/atmos.yaml | 2 - pkg/list/list_components.go | 61 +++++++++++++----------- 2 files changed, 33 insertions(+), 30 deletions(-) diff --git a/examples/quick-start-advanced/atmos.yaml b/examples/quick-start-advanced/atmos.yaml index 398571d66..76a8e6fb2 100644 --- a/examples/quick-start-advanced/atmos.yaml +++ b/examples/quick-start-advanced/atmos.yaml @@ -22,8 +22,6 @@ components: columns: - name: Component value: '{{ .atmos_component }}' - - name: Type - value: '{{ .atmos_component_type }}' - name: Stack value: '{{ .atmos_stack }}' - name: Folder diff --git a/pkg/list/list_components.go b/pkg/list/list_components.go index 2d4591e89..565d93e00 100644 --- a/pkg/list/list_components.go +++ b/pkg/list/list_components.go @@ -14,7 +14,7 @@ import ( ) // getStackComponents extracts Terraform components from the final map of stacks -func getStackComponents(stackData any, listConfig schema.ListConfig) ([]string, error) { +func getStackComponents(stackData any, listFields []string) ([]string, error) { stackMap, ok := stackData.(map[string]any) if !ok { return nil, fmt.Errorf("could not parse stacks") @@ -30,40 +30,22 @@ func getStackComponents(stackData any, listConfig schema.ListConfig) ([]string, return nil, fmt.Errorf("could not parse Terraform components") } - writer := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', tabwriter.AlignRight) - header := make([]string, 0) - values := make([]string, 0) - - re := regexp.MustCompile(`\{\{\s*(.*?)\s*\}\}`) - - for _, v := range listConfig.Columns { - header = append(header, v.Name) - match := re.FindStringSubmatch(v.Value) - - if len(match) > 1 { - values = append(values, match[1]) - } else { - return nil, fmt.Errorf("invalid value format for column name %s", v.Name) - } - } - fmt.Fprintln(writer, strings.Join(header, "\t\t")) - uniqueKeys := lo.Keys(terraformComponents) + result := make([]string, 0) for _, dataKey := range uniqueKeys { data := terraformComponents[dataKey] - result := make([]string, 0) - for _, key := range values { + rowData := make([]string, 0) + for _, key := range listFields { value, found := resolveKey(data.(map[string]any), key) if !found { value = "-" } - result = append(result, fmt.Sprintf("%s", value)) + rowData = append(rowData, fmt.Sprintf("%s", value)) } - fmt.Fprintln(writer, strings.Join(result, "\t\t")) + result = append(result, strings.Join(rowData, "\t\t")) } - writer.Flush() - return lo.Keys(terraformComponents), nil + return result, nil } // resolveKey resolves a key from a map, supporting nested keys with dot notation @@ -100,10 +82,28 @@ func resolveKey(data map[string]any, key string) (any, bool) { func FilterAndListComponents(stackFlag string, stacksMap map[string]any, listConfig schema.ListConfig) (string, error) { components := []string{} + writer := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', tabwriter.AlignRight) + header := make([]string, 0) + listFields := make([]string, 0) + + re := regexp.MustCompile(`\{\{\s*(.*?)\s*\}\}`) + + for _, v := range listConfig.Columns { + header = append(header, v.Name) + match := re.FindStringSubmatch(v.Value) + + if len(match) > 1 { + listFields = append(listFields, match[1]) + } else { + return "", fmt.Errorf("invalid value format for column name %s", v.Name) + } + } + fmt.Fprintln(writer, strings.Join(header, "\t\t")) + if stackFlag != "" { // Filter components for the specified stack if stackData, ok := stacksMap[stackFlag]; ok { - stackComponents, err := getStackComponents(stackData, listConfig) + stackComponents, err := getStackComponents(stackData, listFields) if err != nil { return "", fmt.Errorf("error processing stack '%s': %w", stackFlag, err) } @@ -114,7 +114,7 @@ func FilterAndListComponents(stackFlag string, stacksMap map[string]any, listCon } else { // Get all components from all stacks for _, stackData := range stacksMap { - stackComponents, err := getStackComponents(stackData, listConfig) + stackComponents, err := getStackComponents(stackData, listFields) if err != nil { continue // Skip invalid stacks } @@ -129,5 +129,10 @@ func FilterAndListComponents(stackFlag string, stacksMap map[string]any, listCon if len(components) == 0 { return "No components found", nil } - return strings.Join(components, "\n") + "\n", nil + + for _, com := range components { + fmt.Fprintln(writer, com) + } + writer.Flush() + return "", nil } From a3fec3df89bed680d05c907677bba71a7cad0a2f Mon Sep 17 00:00:00 2001 From: Pulak Kanti Bhowmick Date: Sun, 8 Dec 2024 20:02:00 +0600 Subject: [PATCH 03/15] update unit test Signed-off-by: Pulak Kanti Bhowmick --- pkg/list/list_components_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/list/list_components_test.go b/pkg/list/list_components_test.go index 1d30b020f..87da3bf60 100644 --- a/pkg/list/list_components_test.go +++ b/pkg/list/list_components_test.go @@ -25,7 +25,7 @@ func TestListComponents(t *testing.T) { nil, false, false, false) assert.Nil(t, err) - output, err := FilterAndListComponents("", stacksMap) + output, err := FilterAndListComponents("", stacksMap, schema.ListConfig{}) assert.Nil(t, err) dependentsYaml, err := u.ConvertToYAML(output) assert.Nil(t, err) From d192dcfdd906be946730583c686ca2e628820e84 Mon Sep 17 00:00:00 2001 From: Pulak Kanti Bhowmick Date: Sun, 8 Dec 2024 20:17:38 +0600 Subject: [PATCH 04/15] Update pkg/list/list_components.go Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- pkg/list/list_components.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pkg/list/list_components.go b/pkg/list/list_components.go index 565d93e00..057c75175 100644 --- a/pkg/list/list_components.go +++ b/pkg/list/list_components.go @@ -35,9 +35,13 @@ func getStackComponents(stackData any, listFields []string) ([]string, error) { for _, dataKey := range uniqueKeys { data := terraformComponents[dataKey] + dataMap, ok := data.(map[string]any) + if !ok { + return nil, fmt.Errorf("unexpected data type for component '%s'", dataKey) + } rowData := make([]string, 0) for _, key := range listFields { - value, found := resolveKey(data.(map[string]any), key) + value, found := resolveKey(dataMap, key) if !found { value = "-" } From 382851e02abd5b3e230b175ce8ff9bb5db341af8 Mon Sep 17 00:00:00 2001 From: Pulak Kanti Bhowmick Date: Sun, 8 Dec 2024 20:27:04 +0600 Subject: [PATCH 05/15] update unit tests Signed-off-by: Pulak Kanti Bhowmick --- pkg/list/list_components_test.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pkg/list/list_components_test.go b/pkg/list/list_components_test.go index 87da3bf60..abeb4502a 100644 --- a/pkg/list/list_components_test.go +++ b/pkg/list/list_components_test.go @@ -25,7 +25,14 @@ func TestListComponents(t *testing.T) { nil, false, false, false) assert.Nil(t, err) - output, err := FilterAndListComponents("", stacksMap, schema.ListConfig{}) + listConfig := schema.ListConfig{ + Columns: []schema.ListColumnConfig{ + {Name: "Component", Value: "{{ .atmos_component }}"}, + {Name: "Stack", Value: "{{ .atmos_stack }}"}, + {Name: "Folder", Value: "{{ .vars.tenant }}"}, + }, + } + output, err := FilterAndListComponents("", stacksMap, listConfig) assert.Nil(t, err) dependentsYaml, err := u.ConvertToYAML(output) assert.Nil(t, err) From 277e5b73150629a664987e9ad9309b6e93ae40b3 Mon Sep 17 00:00:00 2001 From: Pulak Kanti Bhowmick Date: Tue, 10 Dec 2024 11:35:20 +0600 Subject: [PATCH 06/15] use lipgloss Signed-off-by: Pulak Kanti Bhowmick --- pkg/list/list_components.go | 74 +++++++++++++++++++++++++++++-------- 1 file changed, 59 insertions(+), 15 deletions(-) diff --git a/pkg/list/list_components.go b/pkg/list/list_components.go index 057c75175..39f792edf 100644 --- a/pkg/list/list_components.go +++ b/pkg/list/list_components.go @@ -2,12 +2,11 @@ package list import ( "fmt" - "os" "regexp" "sort" "strings" - "text/tabwriter" + "github.com/charmbracelet/lipgloss" "github.com/samber/lo" "github.com/cloudposse/atmos/pkg/schema" @@ -37,7 +36,7 @@ func getStackComponents(stackData any, listFields []string) ([]string, error) { data := terraformComponents[dataKey] dataMap, ok := data.(map[string]any) if !ok { - return nil, fmt.Errorf("unexpected data type for component '%s'", dataKey) + return nil, fmt.Errorf("unexpected data type for component '%s'", dataKey) } rowData := make([]string, 0) for _, key := range listFields { @@ -84,14 +83,18 @@ func resolveKey(data map[string]any, key string) (any, bool) { // FilterAndListComponents filters and lists components based on the given stack func FilterAndListComponents(stackFlag string, stacksMap map[string]any, listConfig schema.ListConfig) (string, error) { - components := []string{} + components := [][]string{} + + // Define lipgloss styles for headers and rows + headerStyle := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#00BFFF")) + rowStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#FFFFFF")) - writer := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', tabwriter.AlignRight) header := make([]string, 0) listFields := make([]string, 0) re := regexp.MustCompile(`\{\{\s*(.*?)\s*\}\}`) + // Extract and format headers for _, v := range listConfig.Columns { header = append(header, v.Name) match := re.FindStringSubmatch(v.Value) @@ -102,8 +105,8 @@ func FilterAndListComponents(stackFlag string, stacksMap map[string]any, listCon return "", fmt.Errorf("invalid value format for column name %s", v.Name) } } - fmt.Fprintln(writer, strings.Join(header, "\t\t")) + // Collect components for the table if stackFlag != "" { // Filter components for the specified stack if stackData, ok := stacksMap[stackFlag]; ok { @@ -111,7 +114,9 @@ func FilterAndListComponents(stackFlag string, stacksMap map[string]any, listCon if err != nil { return "", fmt.Errorf("error processing stack '%s': %w", stackFlag, err) } - components = append(components, stackComponents...) + for _, c := range stackComponents { + components = append(components, strings.Fields(c)) + } } else { return "", fmt.Errorf("stack '%s' not found", stackFlag) } @@ -122,21 +127,60 @@ func FilterAndListComponents(stackFlag string, stacksMap map[string]any, listCon if err != nil { continue // Skip invalid stacks } - components = append(components, stackComponents...) + for _, c := range stackComponents { + components = append(components, strings.Fields(c)) + } } } - // Remove duplicates and sort components - components = lo.Uniq(components) - sort.Strings(components) + // Remove duplicates, sort, and prepare rows + componentsMap := lo.UniqBy(components, func(item []string) string { + return strings.Join(item, "\t") + }) + sort.Slice(componentsMap, func(i, j int) bool { + return strings.Join(componentsMap[i], "\t") < strings.Join(componentsMap[j], "\t") + }) - if len(components) == 0 { + if len(componentsMap) == 0 { return "No components found", nil } - for _, com := range components { - fmt.Fprintln(writer, com) + // Determine column widths + colWidths := make([]int, len(header)) + for i, h := range header { + colWidths[i] = len(h) + } + for _, row := range componentsMap { + for i, field := range row { + if len(field) > colWidths[i] { + colWidths[i] = len(field) + } + } + } + + // Format the headers + headerRow := make([]string, len(header)) + for i, h := range header { + headerRow[i] = headerStyle.Render(padToWidth(h, colWidths[i])) } - writer.Flush() + fmt.Println(strings.Join(headerRow, " ")) + + // Format the rows + for _, row := range componentsMap { + formattedRow := make([]string, len(row)) + for i, field := range row { + formattedRow[i] = rowStyle.Render(padToWidth(field, colWidths[i])) + } + fmt.Println(strings.Join(formattedRow, " ")) + } + return "", nil } + +// padToWidth ensures a string is padded to the given width +func padToWidth(str string, width int) string { + for len(str) < width { + str += " " + } + return str +} From 2555c35ee59c411d84a927eb42d4b169e6ace218 Mon Sep 17 00:00:00 2001 From: Pulak Kanti Bhowmick Date: Tue, 10 Dec 2024 11:54:42 +0600 Subject: [PATCH 07/15] refactor code Signed-off-by: Pulak Kanti Bhowmick --- pkg/list/list_components.go | 118 ++++++++++++++++++++++++------------ 1 file changed, 80 insertions(+), 38 deletions(-) diff --git a/pkg/list/list_components.go b/pkg/list/list_components.go index 39f792edf..9b051bcac 100644 --- a/pkg/list/list_components.go +++ b/pkg/list/list_components.go @@ -12,6 +12,17 @@ import ( "github.com/cloudposse/atmos/pkg/schema" ) +const ( + HeaderColor = "#00BFFF" + RowColor = "#FFFFFF" +) + +type tableData struct { + header []string + rows [][]string + colWidths []int +} + // getStackComponents extracts Terraform components from the final map of stacks func getStackComponents(stackData any, listFields []string) ([]string, error) { stackMap, ok := stackData.(map[string]any) @@ -81,47 +92,43 @@ func resolveKey(data map[string]any, key string) (any, bool) { return nil, false } -// FilterAndListComponents filters and lists components based on the given stack -func FilterAndListComponents(stackFlag string, stacksMap map[string]any, listConfig schema.ListConfig) (string, error) { - components := [][]string{} - - // Define lipgloss styles for headers and rows - headerStyle := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#00BFFF")) - rowStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#FFFFFF")) - +// parseColumns extracts the header and list fields from the listConfig +func parseColumns(listConfig schema.ListConfig) ([]string, []string, error) { header := make([]string, 0) listFields := make([]string, 0) - re := regexp.MustCompile(`\{\{\s*(.*?)\s*\}\}`) - // Extract and format headers - for _, v := range listConfig.Columns { - header = append(header, v.Name) - match := re.FindStringSubmatch(v.Value) - + for _, col := range listConfig.Columns { + header = append(header, col.Name) + match := re.FindStringSubmatch(col.Value) if len(match) > 1 { listFields = append(listFields, match[1]) } else { - return "", fmt.Errorf("invalid value format for column name %s", v.Name) + return nil, nil, fmt.Errorf("invalid value format for column name %s", col.Name) } } + return header, listFields, nil +} + +// collectComponents gathers components for the specified stack or all stacks +func collectComponents(stackFlag string, stacksMap map[string]any, listFields []string) ([][]string, error) { + components := [][]string{} - // Collect components for the table if stackFlag != "" { // Filter components for the specified stack if stackData, ok := stacksMap[stackFlag]; ok { stackComponents, err := getStackComponents(stackData, listFields) if err != nil { - return "", fmt.Errorf("error processing stack '%s': %w", stackFlag, err) + return nil, fmt.Errorf("error processing stack '%s': %w", stackFlag, err) } for _, c := range stackComponents { components = append(components, strings.Fields(c)) } } else { - return "", fmt.Errorf("stack '%s' not found", stackFlag) + return nil, fmt.Errorf("stack '%s' not found", stackFlag) } } else { - // Get all components from all stacks + // Collect components from all stacks for _, stackData := range stacksMap { stackComponents, err := getStackComponents(stackData, listFields) if err != nil { @@ -132,25 +139,23 @@ func FilterAndListComponents(stackFlag string, stacksMap map[string]any, listCon } } } + return components, nil +} - // Remove duplicates, sort, and prepare rows - componentsMap := lo.UniqBy(components, func(item []string) string { +// processComponents deduplicates, sorts, and calculates column widths +func processComponents(header []string, components [][]string) ([][]string, []int) { + uniqueComponents := lo.UniqBy(components, func(item []string) string { return strings.Join(item, "\t") }) - sort.Slice(componentsMap, func(i, j int) bool { - return strings.Join(componentsMap[i], "\t") < strings.Join(componentsMap[j], "\t") + sort.Slice(uniqueComponents, func(i, j int) bool { + return strings.Join(uniqueComponents[i], "\t") < strings.Join(uniqueComponents[j], "\t") }) - if len(componentsMap) == 0 { - return "No components found", nil - } - - // Determine column widths colWidths := make([]int, len(header)) for i, h := range header { colWidths[i] = len(h) } - for _, row := range componentsMap { + for _, row := range uniqueComponents { for i, field := range row { if len(field) > colWidths[i] { colWidths[i] = len(field) @@ -158,23 +163,29 @@ func FilterAndListComponents(stackFlag string, stacksMap map[string]any, listCon } } - // Format the headers - headerRow := make([]string, len(header)) - for i, h := range header { - headerRow[i] = headerStyle.Render(padToWidth(h, colWidths[i])) + return uniqueComponents, colWidths +} + +// formatTable generates the formatted table +func formatTable(data tableData) { + headerStyle := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color(HeaderColor)) + rowStyle := lipgloss.NewStyle().Foreground(lipgloss.Color(RowColor)) + + // Format and print headers + headerRow := make([]string, len(data.header)) + for i, h := range data.header { + headerRow[i] = headerStyle.Render(padToWidth(h, data.colWidths[i])) } fmt.Println(strings.Join(headerRow, " ")) - // Format the rows - for _, row := range componentsMap { + // Format and print rows + for _, row := range data.rows { formattedRow := make([]string, len(row)) for i, field := range row { - formattedRow[i] = rowStyle.Render(padToWidth(field, colWidths[i])) + formattedRow[i] = rowStyle.Render(padToWidth(field, data.colWidths[i])) } fmt.Println(strings.Join(formattedRow, " ")) } - - return "", nil } // padToWidth ensures a string is padded to the given width @@ -184,3 +195,34 @@ func padToWidth(str string, width int) string { } return str } + +// FilterAndListComponents orchestrates the process +func FilterAndListComponents(stackFlag string, stacksMap map[string]any, listConfig schema.ListConfig) (string, error) { + // Step 1: Parse columns + header, listFields, err := parseColumns(listConfig) + if err != nil { + return "", err + } + + // Step 2: Collect components + components, err := collectComponents(stackFlag, stacksMap, listFields) + if err != nil { + return "", err + } + + // Step 3: Process components + processedComponents, colWidths := processComponents(header, components) + if len(processedComponents) == 0 { + return "No components found", nil + } + + // Step 4: Format and display table + data := tableData{ + header: header, + rows: processedComponents, + colWidths: colWidths, + } + formatTable(data) + + return "", nil +} From bb305d1a8a872fd9954cd9bdf28fdc820de094fe Mon Sep 17 00:00:00 2001 From: Pulak Kanti Bhowmick Date: Tue, 10 Dec 2024 12:06:26 +0600 Subject: [PATCH 08/15] improve errro handling Signed-off-by: Pulak Kanti Bhowmick --- pkg/list/list_components.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pkg/list/list_components.go b/pkg/list/list_components.go index 9b051bcac..c52df554a 100644 --- a/pkg/list/list_components.go +++ b/pkg/list/list_components.go @@ -99,6 +99,9 @@ func parseColumns(listConfig schema.ListConfig) ([]string, []string, error) { re := regexp.MustCompile(`\{\{\s*(.*?)\s*\}\}`) for _, col := range listConfig.Columns { + if col.Value == "" { + return nil, nil, fmt.Errorf("empty value for column name %s", col.Name) + } header = append(header, col.Name) match := re.FindStringSubmatch(col.Value) if len(match) > 1 { @@ -129,15 +132,20 @@ func collectComponents(stackFlag string, stacksMap map[string]any, listFields [] } } else { // Collect components from all stacks + var errors []string for _, stackData := range stacksMap { stackComponents, err := getStackComponents(stackData, listFields) if err != nil { + errors = append(errors, err.Error()) continue // Skip invalid stacks } for _, c := range stackComponents { components = append(components, strings.Fields(c)) } } + if len(errors) > 0 { + return components, fmt.Errorf("errors processing stacks: %s", strings.Join(errors, "; ")) + } } return components, nil } From 964ba878aa0651c592fd1cbd7d948cbbe65079a6 Mon Sep 17 00:00:00 2001 From: Pulak Kanti Bhowmick Date: Wed, 11 Dec 2024 02:08:26 +0600 Subject: [PATCH 09/15] update table logic Signed-off-by: Pulak Kanti Bhowmick --- examples/quick-start-advanced/atmos.yaml | 4 +- pkg/list/list_components.go | 91 +++++++++++++++++------- 2 files changed, 68 insertions(+), 27 deletions(-) diff --git a/examples/quick-start-advanced/atmos.yaml b/examples/quick-start-advanced/atmos.yaml index 76a8e6fb2..cbd80d68e 100644 --- a/examples/quick-start-advanced/atmos.yaml +++ b/examples/quick-start-advanced/atmos.yaml @@ -24,8 +24,10 @@ components: value: '{{ .atmos_component }}' - name: Stack value: '{{ .atmos_stack }}' - - name: Folder + - name: Tenant value: '{{ .vars.tenant }}' + - name: Region + value: '{{ .vars.region }}' terraform: # Optional `command` specifies the executable to be called by `atmos` when running Terraform commands # If not defined, `terraform` is used diff --git a/pkg/list/list_components.go b/pkg/list/list_components.go index c52df554a..abc291c32 100644 --- a/pkg/list/list_components.go +++ b/pkg/list/list_components.go @@ -12,11 +12,6 @@ import ( "github.com/cloudposse/atmos/pkg/schema" ) -const ( - HeaderColor = "#00BFFF" - RowColor = "#FFFFFF" -) - type tableData struct { header []string rows [][]string @@ -174,34 +169,78 @@ func processComponents(header []string, components [][]string) ([][]string, []in return uniqueComponents, colWidths } -// formatTable generates the formatted table -func formatTable(data tableData) { - headerStyle := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color(HeaderColor)) - rowStyle := lipgloss.NewStyle().Foreground(lipgloss.Color(RowColor)) +func generateTable(data tableData) { + // Style definitions + purple := lipgloss.Color("5") + headerStyle := lipgloss.NewStyle(). + Foreground(purple). + Bold(true). + Align(lipgloss.Center) + cellStyle := lipgloss.NewStyle(). + Padding(0, 1) + borderStyle := lipgloss.NewStyle(). + Border(lipgloss.RoundedBorder()). + BorderForeground(purple). + Padding(0) - // Format and print headers - headerRow := make([]string, len(data.header)) - for i, h := range data.header { - headerRow[i] = headerStyle.Render(padToWidth(h, data.colWidths[i])) + // Calculate the maximum width for each column + colWidths := make([]int, len(data.header)) + for i, header := range data.header { + colWidths[i] = len(header) } - fmt.Println(strings.Join(headerRow, " ")) - - // Format and print rows for _, row := range data.rows { - formattedRow := make([]string, len(row)) - for i, field := range row { - formattedRow[i] = rowStyle.Render(padToWidth(field, data.colWidths[i])) + for i, cell := range row { + if len(cell) > colWidths[i] { + colWidths[i] = len(cell) + } } - fmt.Println(strings.Join(formattedRow, " ")) } + + // Standardize column widths across all rows + for i, width := range colWidths { + colWidths[i] = width + 2 // Add padding to each column + } + + // Build formatted table rows + var tableRows []string + + // Add the header + headerRow := formatRow(data.header, colWidths, headerStyle) + tableRows = append(tableRows, headerRow) + + // Add a separator + separator := createSeparator(colWidths) + tableRows = append(tableRows, separator) + + // Add data rows + for _, row := range data.rows { + formattedRow := formatRow(row, colWidths, cellStyle) + tableRows = append(tableRows, formattedRow) + } + + // Combine rows into a single string + table := strings.Join(tableRows, "\n") + + // Apply border and print the table + fmt.Println(borderStyle.Render(table)) +} + +// formatRow creates a single formatted row +func formatRow(row []string, colWidths []int, style lipgloss.Style) string { + formattedCells := make([]string, len(row)) + for i, cell := range row { + formattedCells[i] = style.Width(colWidths[i]).Render(cell) + } + return strings.Join(formattedCells, "│") } -// padToWidth ensures a string is padded to the given width -func padToWidth(str string, width int) string { - for len(str) < width { - str += " " +// createSeparator generates a horizontal separator for the table +func createSeparator(colWidths []int) string { + segments := make([]string, len(colWidths)) + for i, width := range colWidths { + segments[i] = strings.Repeat("─", width) } - return str + return strings.Join(segments, "┼") } // FilterAndListComponents orchestrates the process @@ -230,7 +269,7 @@ func FilterAndListComponents(stackFlag string, stacksMap map[string]any, listCon rows: processedComponents, colWidths: colWidths, } - formatTable(data) + generateTable(data) return "", nil } From c24bf75c683bdc20e993cca0a64ef9985ad7963a Mon Sep 17 00:00:00 2001 From: Pulak Kanti Bhowmick Date: Wed, 11 Dec 2024 02:23:18 +0600 Subject: [PATCH 10/15] Update pkg/list/list_components.go Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- pkg/list/list_components.go | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/pkg/list/list_components.go b/pkg/list/list_components.go index abc291c32..d5cf66a36 100644 --- a/pkg/list/list_components.go +++ b/pkg/list/list_components.go @@ -89,23 +89,26 @@ func resolveKey(data map[string]any, key string) (any, bool) { // parseColumns extracts the header and list fields from the listConfig func parseColumns(listConfig schema.ListConfig) ([]string, []string, error) { - header := make([]string, 0) - listFields := make([]string, 0) - re := regexp.MustCompile(`\{\{\s*(.*?)\s*\}\}`) - - for _, col := range listConfig.Columns { - if col.Value == "" { - return nil, nil, fmt.Errorf("empty value for column name %s", col.Name) - } - header = append(header, col.Name) - match := re.FindStringSubmatch(col.Value) - if len(match) > 1 { - listFields = append(listFields, match[1]) - } else { - return nil, nil, fmt.Errorf("invalid value format for column name %s", col.Name) - } - } - return header, listFields, nil + if len(listConfig.Columns) == 0 { + return nil, nil, fmt.Errorf("no columns configured") + } + header := make([]string, 0) + listFields := make([]string, 0) + re := regexp.MustCompile(`\{\{\s*(.*?)\s*\}\}`) + + for _, col := range listConfig.Columns { + if col.Value == "" { + return nil, nil, fmt.Errorf("empty value for column name %s", col.Name) + } + header = append(header, col.Name) + match := re.FindStringSubmatch(col.Value) + if len(match) > 1 { + listFields = append(listFields, match[1]) + } else { + return nil, nil, fmt.Errorf("invalid value format for column name %s", col.Name) + } + } + return header, listFields, nil } // collectComponents gathers components for the specified stack or all stacks From 82a8a73dad73c92630a9fae5e8900d82bc532b9f Mon Sep 17 00:00:00 2001 From: Pulak Kanti Bhowmick Date: Sat, 14 Dec 2024 13:28:34 +0600 Subject: [PATCH 11/15] update table color Signed-off-by: Pulak Kanti Bhowmick --- pkg/list/list_components.go | 160 ++++++++++++++++-------------------- 1 file changed, 73 insertions(+), 87 deletions(-) diff --git a/pkg/list/list_components.go b/pkg/list/list_components.go index d5cf66a36..13d449200 100644 --- a/pkg/list/list_components.go +++ b/pkg/list/list_components.go @@ -2,11 +2,13 @@ package list import ( "fmt" + "os" "regexp" "sort" "strings" "github.com/charmbracelet/lipgloss" + "github.com/charmbracelet/lipgloss/table" "github.com/samber/lo" "github.com/cloudposse/atmos/pkg/schema" @@ -89,26 +91,26 @@ func resolveKey(data map[string]any, key string) (any, bool) { // parseColumns extracts the header and list fields from the listConfig func parseColumns(listConfig schema.ListConfig) ([]string, []string, error) { - if len(listConfig.Columns) == 0 { - return nil, nil, fmt.Errorf("no columns configured") - } - header := make([]string, 0) - listFields := make([]string, 0) - re := regexp.MustCompile(`\{\{\s*(.*?)\s*\}\}`) - - for _, col := range listConfig.Columns { - if col.Value == "" { - return nil, nil, fmt.Errorf("empty value for column name %s", col.Name) - } - header = append(header, col.Name) - match := re.FindStringSubmatch(col.Value) - if len(match) > 1 { - listFields = append(listFields, match[1]) - } else { - return nil, nil, fmt.Errorf("invalid value format for column name %s", col.Name) - } - } - return header, listFields, nil + if len(listConfig.Columns) == 0 { + return nil, nil, fmt.Errorf("no columns configured") + } + header := make([]string, 0) + listFields := make([]string, 0) + re := regexp.MustCompile(`\{\{\s*(.*?)\s*\}\}`) + + for _, col := range listConfig.Columns { + if col.Value == "" { + return nil, nil, fmt.Errorf("empty value for column name %s", col.Name) + } + header = append(header, col.Name) + match := re.FindStringSubmatch(col.Value) + if len(match) > 1 { + listFields = append(listFields, match[1]) + } else { + return nil, nil, fmt.Errorf("invalid value format for column name %s", col.Name) + } + } + return header, listFields, nil } // collectComponents gathers components for the specified stack or all stacks @@ -172,78 +174,62 @@ func processComponents(header []string, components [][]string) ([][]string, []in return uniqueComponents, colWidths } +const ( + purple = lipgloss.Color("99") // Purple color for headers and borders + gray = lipgloss.Color("245") // Gray for odd rows + lightGray = lipgloss.Color("241") // Light gray for even rows +) + func generateTable(data tableData) { - // Style definitions - purple := lipgloss.Color("5") - headerStyle := lipgloss.NewStyle(). - Foreground(purple). - Bold(true). - Align(lipgloss.Center) - cellStyle := lipgloss.NewStyle(). - Padding(0, 1) - borderStyle := lipgloss.NewStyle(). - Border(lipgloss.RoundedBorder()). - BorderForeground(purple). - Padding(0) - - // Calculate the maximum width for each column - colWidths := make([]int, len(data.header)) - for i, header := range data.header { - colWidths[i] = len(header) - } - for _, row := range data.rows { - for i, cell := range row { - if len(cell) > colWidths[i] { - colWidths[i] = len(cell) + // Renderer for styling + re := lipgloss.NewRenderer(os.Stdout) + + // Define styles + var ( + HeaderStyle = re.NewStyle(). + Foreground(purple). + Bold(true). + Align(lipgloss.Center) // Header style + + CellStyle = re.NewStyle(). + Padding(0, 1). + Width(20) // Base style for rows + + OddRowStyle = CellStyle.Foreground(gray) // Style for odd rows + EvenRowStyle = CellStyle.Foreground(lightGray) // Style for even rows + + BorderStyle = lipgloss.NewStyle(). + Foreground(purple) // Border style with purple color + ) + + // Create the table with headers, rows, and styles + t := table.New(). + Border(lipgloss.ThickBorder()). + BorderStyle(BorderStyle). + StyleFunc(func(row, col int) lipgloss.Style { + var style lipgloss.Style + + switch { + case row == table.HeaderRow: + return HeaderStyle // Style for header + case row%2 == 0: + style = EvenRowStyle // Even rows + default: + style = OddRowStyle // Odd rows } - } - } - - // Standardize column widths across all rows - for i, width := range colWidths { - colWidths[i] = width + 2 // Add padding to each column - } - - // Build formatted table rows - var tableRows []string - - // Add the header - headerRow := formatRow(data.header, colWidths, headerStyle) - tableRows = append(tableRows, headerRow) - - // Add a separator - separator := createSeparator(colWidths) - tableRows = append(tableRows, separator) - - // Add data rows - for _, row := range data.rows { - formattedRow := formatRow(row, colWidths, cellStyle) - tableRows = append(tableRows, formattedRow) - } - // Combine rows into a single string - table := strings.Join(tableRows, "\n") - - // Apply border and print the table - fmt.Println(borderStyle.Render(table)) -} + // Optional: Adjust specific columns (e.g., make the middle column wider) + if col == 1 { + style = style.Width(25) + } -// formatRow creates a single formatted row -func formatRow(row []string, colWidths []int, style lipgloss.Style) string { - formattedCells := make([]string, len(row)) - for i, cell := range row { - formattedCells[i] = style.Width(colWidths[i]).Render(cell) - } - return strings.Join(formattedCells, "│") -} + return style + }). + Headers(data.header...). + Rows(data.rows...) -// createSeparator generates a horizontal separator for the table -func createSeparator(colWidths []int) string { - segments := make([]string, len(colWidths)) - for i, width := range colWidths { - segments[i] = strings.Repeat("─", width) - } - return strings.Join(segments, "┼") + // Render and print the table + fmt.Println(t) } // FilterAndListComponents orchestrates the process From 2c85549effa9a5d3d967b90b7ed57684402be388 Mon Sep 17 00:00:00 2001 From: Pulak Kanti Bhowmick Date: Mon, 16 Dec 2024 13:25:50 +0600 Subject: [PATCH 12/15] add abstruct flag Signed-off-by: Pulak Kanti Bhowmick --- cmd/list_components.go | 4 +++- pkg/list/list_components.go | 23 +++++++++++++++-------- pkg/list/list_components_test.go | 2 +- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/cmd/list_components.go b/cmd/list_components.go index 2006c7bee..c40a91725 100644 --- a/cmd/list_components.go +++ b/cmd/list_components.go @@ -25,6 +25,7 @@ var listComponentsCmd = &cobra.Command{ checkAtmosConfig() stackFlag, _ := cmd.Flags().GetString("stack") + abstractFlag, _ := cmd.Flags().GetBool("abstract") configAndStacksInfo := schema.ConfigAndStacksInfo{} cliConfig, err := config.InitCliConfig(configAndStacksInfo, true) @@ -39,7 +40,7 @@ var listComponentsCmd = &cobra.Command{ return } - output, err := l.FilterAndListComponents(stackFlag, stacksMap, cliConfig.Components.List) + output, err := l.FilterAndListComponents(stackFlag, abstractFlag, stacksMap, cliConfig.Components.List) if err != nil { u.PrintMessageInColor(fmt.Sprintf("Error: %v"+"\n", err), color.New(color.FgYellow)) return @@ -51,5 +52,6 @@ var listComponentsCmd = &cobra.Command{ func init() { listComponentsCmd.PersistentFlags().StringP("stack", "s", "", "Filter components by stack (e.g., atmos list components -s stack1)") + listComponentsCmd.PersistentFlags().Bool("abstract", false, "Filter abstract component if true") listCmd.AddCommand(listComponentsCmd) } diff --git a/pkg/list/list_components.go b/pkg/list/list_components.go index 13d449200..7420e507f 100644 --- a/pkg/list/list_components.go +++ b/pkg/list/list_components.go @@ -11,6 +11,7 @@ import ( "github.com/charmbracelet/lipgloss/table" "github.com/samber/lo" + "github.com/cloudposse/atmos/internal/exec" "github.com/cloudposse/atmos/pkg/schema" ) @@ -21,7 +22,7 @@ type tableData struct { } // getStackComponents extracts Terraform components from the final map of stacks -func getStackComponents(stackData any, listFields []string) ([]string, error) { +func getStackComponents(stackData any, abstractFlag bool, listFields []string) ([]string, error) { stackMap, ok := stackData.(map[string]any) if !ok { return nil, fmt.Errorf("could not parse stacks") @@ -37,7 +38,13 @@ func getStackComponents(stackData any, listFields []string) ([]string, error) { return nil, fmt.Errorf("could not parse Terraform components") } - uniqueKeys := lo.Keys(terraformComponents) + var uniqueKeys []string + + if abstractFlag { + uniqueKeys = lo.Keys(terraformComponents) + } else { + uniqueKeys = exec.FilterAbstractComponents(terraformComponents) + } result := make([]string, 0) for _, dataKey := range uniqueKeys { @@ -114,13 +121,13 @@ func parseColumns(listConfig schema.ListConfig) ([]string, []string, error) { } // collectComponents gathers components for the specified stack or all stacks -func collectComponents(stackFlag string, stacksMap map[string]any, listFields []string) ([][]string, error) { +func collectComponents(stackFlag string, abstractFlag bool, stacksMap map[string]any, listFields []string) ([][]string, error) { components := [][]string{} if stackFlag != "" { // Filter components for the specified stack if stackData, ok := stacksMap[stackFlag]; ok { - stackComponents, err := getStackComponents(stackData, listFields) + stackComponents, err := getStackComponents(stackData, abstractFlag, listFields) if err != nil { return nil, fmt.Errorf("error processing stack '%s': %w", stackFlag, err) } @@ -134,7 +141,7 @@ func collectComponents(stackFlag string, stacksMap map[string]any, listFields [] // Collect components from all stacks var errors []string for _, stackData := range stacksMap { - stackComponents, err := getStackComponents(stackData, listFields) + stackComponents, err := getStackComponents(stackData, abstractFlag, listFields) if err != nil { errors = append(errors, err.Error()) continue // Skip invalid stacks @@ -199,7 +206,7 @@ func generateTable(data tableData) { EvenRowStyle = CellStyle.Foreground(lightGray) // Style for even rows BorderStyle = lipgloss.NewStyle(). - Foreground(purple) // Border style with purple color + Foreground(gray) ) // Create the table with headers, rows, and styles @@ -233,7 +240,7 @@ func generateTable(data tableData) { } // FilterAndListComponents orchestrates the process -func FilterAndListComponents(stackFlag string, stacksMap map[string]any, listConfig schema.ListConfig) (string, error) { +func FilterAndListComponents(stackFlag string, abstractFlag bool, stacksMap map[string]any, listConfig schema.ListConfig) (string, error) { // Step 1: Parse columns header, listFields, err := parseColumns(listConfig) if err != nil { @@ -241,7 +248,7 @@ func FilterAndListComponents(stackFlag string, stacksMap map[string]any, listCon } // Step 2: Collect components - components, err := collectComponents(stackFlag, stacksMap, listFields) + components, err := collectComponents(stackFlag, abstractFlag, stacksMap, listFields) if err != nil { return "", err } diff --git a/pkg/list/list_components_test.go b/pkg/list/list_components_test.go index abeb4502a..be153aa4c 100644 --- a/pkg/list/list_components_test.go +++ b/pkg/list/list_components_test.go @@ -32,7 +32,7 @@ func TestListComponents(t *testing.T) { {Name: "Folder", Value: "{{ .vars.tenant }}"}, }, } - output, err := FilterAndListComponents("", stacksMap, listConfig) + output, err := FilterAndListComponents("", false, stacksMap, listConfig) assert.Nil(t, err) dependentsYaml, err := u.ConvertToYAML(output) assert.Nil(t, err) From 1fa0f47341dc536b3aeb6cfeae2575ec40fd0342 Mon Sep 17 00:00:00 2001 From: Pulak Kanti Bhowmick Date: Mon, 16 Dec 2024 13:40:19 +0600 Subject: [PATCH 13/15] handle TTY Signed-off-by: Pulak Kanti Bhowmick --- pkg/list/list_components.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/pkg/list/list_components.go b/pkg/list/list_components.go index 7420e507f..aface8581 100644 --- a/pkg/list/list_components.go +++ b/pkg/list/list_components.go @@ -10,6 +10,7 @@ import ( "github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss/table" "github.com/samber/lo" + "golang.org/x/term" "github.com/cloudposse/atmos/internal/exec" "github.com/cloudposse/atmos/pkg/schema" @@ -187,7 +188,25 @@ const ( lightGray = lipgloss.Color("241") // Light gray for even rows ) +// Fallback for non-TTY environments +func printSimpleTable(data tableData) { + // Print headers + fmt.Println(data.header) + + // Print rows + for _, row := range data.rows { + fmt.Println(row) + } +} + func generateTable(data tableData) { + // Check if TTY is attached + if !term.IsTerminal(int(os.Stdout.Fd())) { + // Degrade to a simple tabular format + printSimpleTable(data) + return + } + // Renderer for styling re := lipgloss.NewRenderer(os.Stdout) From f368057135d29e78acc9cfdb712e326c2ead9edd Mon Sep 17 00:00:00 2001 From: Pulak Kanti Bhowmick Date: Mon, 16 Dec 2024 13:45:07 +0600 Subject: [PATCH 14/15] calculate width based on data width Signed-off-by: Pulak Kanti Bhowmick --- pkg/list/list_components.go | 41 +++++++++++++++++++++++++++++++------ 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/pkg/list/list_components.go b/pkg/list/list_components.go index aface8581..a5ba5d9c3 100644 --- a/pkg/list/list_components.go +++ b/pkg/list/list_components.go @@ -207,6 +207,9 @@ func generateTable(data tableData) { return } + // Dynamically calculate column widths + columnWidths := calculateColumnWidths(data) + // Renderer for styling re := lipgloss.NewRenderer(os.Stdout) @@ -218,8 +221,7 @@ func generateTable(data tableData) { Align(lipgloss.Center) // Header style CellStyle = re.NewStyle(). - Padding(0, 1). - Width(20) // Base style for rows + Padding(0, 1) // Base style for rows OddRowStyle = CellStyle.Foreground(gray) // Style for odd rows EvenRowStyle = CellStyle.Foreground(lightGray) // Style for even rows @@ -244,10 +246,8 @@ func generateTable(data tableData) { style = OddRowStyle // Odd rows } - // Optional: Adjust specific columns (e.g., make the middle column wider) - if col == 1 { - style = style.Width(25) - } + // Apply dynamic width to each column + style = style.Width(columnWidths[col]) return style }). @@ -258,6 +258,35 @@ func generateTable(data tableData) { fmt.Println(t) } +// Calculate the maximum width for each column +func calculateColumnWidths(data tableData) []int { + columnCount := len(data.header) + columnWidths := make([]int, columnCount) + + // Check headers + for i, header := range data.header { + if len(header) > columnWidths[i] { + columnWidths[i] = len(header) + } + } + + // Check rows + for _, row := range data.rows { + for i, cell := range row { + if len(cell) > columnWidths[i] { + columnWidths[i] = len(cell) + } + } + } + + // Add padding for aesthetics + for i := range columnWidths { + columnWidths[i] += 2 // Add 2 spaces for padding + } + + return columnWidths +} + // FilterAndListComponents orchestrates the process func FilterAndListComponents(stackFlag string, abstractFlag bool, stacksMap map[string]any, listConfig schema.ListConfig) (string, error) { // Step 1: Parse columns From fa0c9f258fd5b1862f47a4123a5d95ddd4969cda Mon Sep 17 00:00:00 2001 From: "Erik Osterman (CEO @ Cloud Posse)" Date: Tue, 7 Jan 2025 08:15:23 -0600 Subject: [PATCH 15/15] Update cmd/list_components.go Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- cmd/list_components.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/list_components.go b/cmd/list_components.go index 5235abe86..be9fa04ec 100644 --- a/cmd/list_components.go +++ b/cmd/list_components.go @@ -40,7 +40,7 @@ var listComponentsCmd = &cobra.Command{ return } - output, err := l.FilterAndListComponents(stackFlag, abstractFlag, stacksMap, cliConfig.Components.List) + output, err := l.FilterAndListComponents(stackFlag, abstractFlag, stacksMap, atmosConfig.Components.List) if err != nil { u.PrintMessageInColor(fmt.Sprintf("Error: %v"+"\n", err), color.New(color.FgYellow)) return