tui/components.go (252 lines of code) (raw):

// Copyright 2023 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package tui import ( "fmt" "strconv" "strings" "github.com/GoogleCloudPlatform/deploystack/config" "github.com/charmbracelet/bubbles/table" "github.com/charmbracelet/lipgloss" "golang.org/x/text/cases" "golang.org/x/text/language" ) type component interface { render() string } type productList []struct { item string product string } func (p productList) longest(k string) int { longest := 0 for _, v := range p { if k == "item" { if len(v.item) > longest { longest = len(v.item) } } else { if len(v.product) > longest { longest = len(v.product) } } } return longest } type description struct { stack *config.Stack } func newDescription(stack *config.Stack) description { return description{stack: stack} } func (d *description) parse() (productList, []string) { p := productList{} if len(d.stack.Config.Products) > 0 { for _, v := range d.stack.Config.Products { tmp := struct{ item, product string }{} tmp.item = strings.TrimSpace(v.Info) tmp.product = strings.TrimSpace(v.Product) p = append(p, tmp) } } return p, []string{d.stack.Config.Description} } func (d description) render() string { doc := strings.Builder{} list, additionalText := d.parse() columns := []table.Column{ {Title: "", Width: list.longest("item") + 10}, {Title: "", Width: list.longest("product") + 10}, } rows := []table.Row{} for _, v := range list { rows = append(rows, table.Row{ titleStyle.Render(v.item), strong.Render(v.product), }) } t := table.New( table.WithColumns(columns), table.WithRows(rows), table.WithFocused(true), table.WithHeight(len(list)), ) t.SetStyles(tableStyle) if len(list) > 0 { doc.WriteString(normal.Render("This process will install the following resources:")) doc.WriteString(t.View()) doc.WriteString("\n\n") } for _, v := range additionalText { doc.WriteString(normal.Render(v)) doc.WriteString("\n\n") } doc.WriteString(normal.Render("It's going to take around ")) doc.WriteString(strong.Render(strconv.Itoa(d.stack.Config.Duration))) if d.stack.Config.Duration == 1 { doc.WriteString(normal.Render(" minute.")) } else { doc.WriteString(normal.Render(" minutes.")) } doc.WriteString("\n\n") if len(d.stack.Config.DocumentationLink) > 0 { doc.WriteString(normal.Render("If you would like more information about this stack, ")) doc.WriteString(normal.Render("please read the documentation at: ")) doc.WriteString(url.Render(d.stack.Config.DocumentationLink)) doc.WriteString("\n\n") } return doc.String() } type errorAlert struct { err errMsg } func (e errorAlert) Render() string { sb := strings.Builder{} height := len(e.err.Error()) / width style := errorAlertStyle.Copy() style.Height(height) sb.WriteString("\n") sb.WriteString(boldAlert.Render("There was an error!")) sb.WriteString("\n") if e.err.usermsg != "" { sb.WriteString(e.err.usermsg) sb.WriteString("\n") } sb.WriteString("\n") sb.WriteString("Details: \n") sb.WriteString(e.err.Error()) sb.WriteString("\n") sb.WriteString("\n") if !e.err.quit { sb.WriteString("You can exit the program by typing ") sb.WriteString(cmdStyle.Render("ctr+c.")) } if e.err.target != "" { text := " Press the Enter Key to go back and change choice " if e.err.target == "quit" { text = " Press the Enter Key exit " } sb.WriteString("\n") sb.WriteString("\n") sb.WriteString(bodyStyle.Render(promptStyle.Render(text))) sb.WriteString("\n") } return style.Render(sb.String()) } type header struct { title string subtitle string } func newHeader(title, subtitle string) header { return header{title: title, subtitle: subtitle} } func (h header) render() string { doc := strings.Builder{} content := lipgloss.JoinVertical(lipgloss.Left, fmt.Sprintf("%s%s%s", textColors.code("bright cyan"), titleStyle.Render(h.title), clear), subTitleStyle.Render(h.subtitle), ) doc.WriteString(headerStyle.Render(content)) doc.WriteString("\n\n") return doc.String() } func drawProgress(percent int) string { sb := strings.Builder{} label := " Progress " sb.WriteString(textStyle.Render(label)) totalWidth := hardWidthLimit - len(label) completeLength := int(float32(totalWidth) * (float32(percent) / float32(100))) pendingLength := totalWidth - completeLength comp := strings.Builder{} for i := 0; i < completeLength; i++ { comp.WriteString("█") } pend := strings.Builder{} for i := 0; i < pendingLength; i++ { pend.WriteString("░") } sb.WriteString(completeStyle.Render(comp.String())) sb.WriteString(pendingStyle.Render(pend.String())) return sb.String() } type settingsTable struct { stack *config.Stack } func newSettingsTable(s *config.Stack) settingsTable { return settingsTable{stack: s} } func (s settingsTable) render() string { doc := strings.Builder{} wSetting := 0 wValue := 0 s.stack.Settings.Sort() rows := []table.Row{} if s := s.stack.Settings.Find("stack_name"); s != nil && len(s.Value) > 0 { rows = append(rows, table.Row{ titleStyle.Render("Stack Name"), strong.Render(s.Value), }) } if s := s.stack.Settings.Find("project_name"); s != nil && len(s.Value) > 0 { rows = append(rows, table.Row{ titleStyle.Render("Project Name"), strong.Render(s.Value), }) } if s := s.stack.Settings.Find("project_id"); s != nil && len(s.Value) > 0 { rows = append(rows, table.Row{ titleStyle.Render("Project ID"), strong.Render(s.Value), }) } if s := s.stack.Settings.Find("project_number"); s != nil && len(s.Value) > 0 { rows = append(rows, table.Row{ titleStyle.Render("Project Number"), strong.Render(s.Value), }) } for _, setting := range s.stack.Settings { rawValue := setting.TFvarsValue() rawValue = strings.Trim(rawValue, "\"") value := strong.Render(strings.TrimSpace(rawValue)) if len(rawValue) > 45 { value = strong.Render(rawValue[:45] + "...") } if len(setting.Name) > wSetting { wSetting = len(setting.Name) } if len(value) > wValue { wValue = len(value) } if setting.Name == "project_id" || setting.Name == "project_number" || setting.Name == "project_name" || setting.Name == "stack_name" { continue } if len(value) < 1 { continue } nameRaw := strings.TrimSpace(setting.Name) nameRaw = strings.ReplaceAll(nameRaw, "_", " ") nameRaw = strings.ReplaceAll(nameRaw, "-", " ") formatted := cases.Title(language.English).String(nameRaw) rows = append(rows, table.Row{titleStyle.Render(formatted), value}) } columns := []table.Column{ {Title: "Setting", Width: 35}, {Title: "Value", Width: 55}, } t := table.New( table.WithColumns(columns), table.WithRows(rows), table.WithFocused(false), table.WithHeight(len(s.stack.Settings)), ) t.SetStyles(tableStyle) doc.WriteString("\n") doc.WriteString(t.View()) doc.WriteString("\n") return doc.String() } type textBlock string func (t textBlock) render() string { return string(t) } func newTextBlock(s string) textBlock { return textBlock(s) }