github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/cli/preflight/interactive/interactive.go (about) 1 /* 2 Copyright (C) 2022-2023 ApeCloud Co., Ltd 3 4 This file is part of KubeBlocks project 5 6 This program is free software: you can redistribute it and/or modify 7 it under the terms of the GNU Affero General Public License as published by 8 the Free Software Foundation, either version 3 of the License, or 9 (at your option) any later version. 10 11 This program is distributed in the hope that it will be useful 12 but WITHOUT ANY WARRANTY; without even the implied warranty of 13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 GNU Affero General Public License for more details. 15 16 You should have received a copy of the GNU Affero General Public License 17 along with this program. If not, see <http://www.gnu.org/licenses/>. 18 */ 19 20 package interactive 21 22 import ( 23 "fmt" 24 "os" 25 "time" 26 27 "github.com/pkg/errors" 28 ui "github.com/replicatedhq/termui/v3" 29 "github.com/replicatedhq/termui/v3/widgets" 30 "github.com/replicatedhq/troubleshoot/cmd/util" 31 analyzerunner "github.com/replicatedhq/troubleshoot/pkg/analyze" 32 "github.com/replicatedhq/troubleshoot/pkg/convert" 33 ) 34 35 var ( 36 selectedResult = 0 37 table = widgets.NewTable() 38 isShowingSaved = false 39 ) 40 41 // ShowInteractiveResults displays the results in interactive mode 42 func ShowInteractiveResults(preflightName string, analyzeResults []*analyzerunner.AnalyzeResult, outputPath string) error { 43 if err := ui.Init(); err != nil { 44 return errors.Wrap(err, "failed to create terminal ui") 45 } 46 defer ui.Close() 47 48 drawUI(preflightName, analyzeResults) 49 50 uiEvents := ui.PollEvents() 51 for e := range uiEvents { 52 switch e.ID { 53 case "<C-c>": 54 return nil 55 case "q": 56 if isShowingSaved { 57 isShowingSaved = false 58 ui.Clear() 59 drawUI(preflightName, analyzeResults) 60 } else { 61 return nil 62 } 63 case "s": 64 filename, err := save(preflightName, outputPath, analyzeResults) 65 if err != nil { 66 // show 67 } else { 68 showSaved(filename) 69 go func() { 70 time.Sleep(time.Second * 5) 71 isShowingSaved = false 72 ui.Clear() 73 drawUI(preflightName, analyzeResults) 74 }() 75 } 76 case "<Resize>": 77 ui.Clear() 78 drawUI(preflightName, analyzeResults) 79 case "<Down>": 80 if selectedResult < len(analyzeResults)-1 { 81 selectedResult++ 82 } else { 83 selectedResult = 0 84 table.SelectedRow = 0 85 } 86 table.ScrollDown() 87 ui.Clear() 88 drawUI(preflightName, analyzeResults) 89 case "<Up>": 90 if selectedResult > 0 { 91 selectedResult-- 92 } else { 93 selectedResult = len(analyzeResults) - 1 94 table.SelectedRow = len(analyzeResults) 95 } 96 table.ScrollUp() 97 ui.Clear() 98 drawUI(preflightName, analyzeResults) 99 } 100 } 101 return nil 102 } 103 104 func drawUI(preflightName string, analyzeResults []*analyzerunner.AnalyzeResult) { 105 drawGrid(analyzeResults) 106 drawHeader(preflightName) 107 drawFooter() 108 } 109 110 func drawHeader(preflightName string) { 111 termWidth, _ := ui.TerminalDimensions() 112 113 title := widgets.NewParagraph() 114 title.Text = fmt.Sprintf("%s Preflight Checks", util.AppName(preflightName)) 115 title.TextStyle.Fg = ui.ColorWhite 116 title.TextStyle.Bg = ui.ColorClear 117 title.TextStyle.Modifier = ui.ModifierBold 118 title.Border = false 119 120 left := termWidth/2 - 2*len(title.Text)/3 121 right := termWidth/2 + (termWidth/2 - left) 122 123 title.SetRect(left, 0, right, 1) 124 ui.Render(title) 125 } 126 127 func drawGrid(analyzeResults []*analyzerunner.AnalyzeResult) { 128 drawPreflightTable(analyzeResults) 129 drawDetails(analyzeResults[selectedResult]) 130 } 131 132 func drawFooter() { 133 termWidth, termHeight := ui.TerminalDimensions() 134 135 instructions := widgets.NewParagraph() 136 instructions.Text = "[q] quit [s] save [↑][↓] scroll" 137 instructions.Border = false 138 139 left := 0 140 right := termWidth 141 top := termHeight - 1 142 bottom := termHeight 143 144 instructions.SetRect(left, top, right, bottom) 145 ui.Render(instructions) 146 } 147 148 func drawPreflightTable(analyzeResults []*analyzerunner.AnalyzeResult) { 149 termWidth, termHeight := ui.TerminalDimensions() 150 151 table.SetRect(0, 3, termWidth/2, termHeight-6) 152 table.FillRow = true 153 table.Border = true 154 table.Rows = [][]string{} 155 table.ColumnWidths = []int{termWidth} 156 157 for i, analyzeResult := range analyzeResults { 158 title := analyzeResult.Title 159 if analyzeResult.Strict { 160 title += fmt.Sprintf(" (Strict: %t)", analyzeResult.Strict) 161 } 162 switch { 163 case analyzeResult.IsPass: 164 title = fmt.Sprintf("✔ %s", title) 165 case analyzeResult.IsWarn: 166 title = fmt.Sprintf("⚠️ %s", title) 167 case analyzeResult.IsFail: 168 title = fmt.Sprintf("✘ %s", title) 169 } 170 table.Rows = append(table.Rows, []string{ 171 title, 172 }) 173 switch { 174 case analyzeResult.IsPass: 175 if i == selectedResult { 176 table.RowStyles[i] = ui.NewStyle(ui.ColorGreen, ui.ColorClear, ui.ModifierReverse) 177 } else { 178 table.RowStyles[i] = ui.NewStyle(ui.ColorGreen, ui.ColorClear) 179 } 180 case analyzeResult.IsWarn: 181 if i == selectedResult { 182 table.RowStyles[i] = ui.NewStyle(ui.ColorYellow, ui.ColorClear, ui.ModifierReverse) 183 } else { 184 table.RowStyles[i] = ui.NewStyle(ui.ColorYellow, ui.ColorClear) 185 } 186 case analyzeResult.IsFail: 187 if i == selectedResult { 188 table.RowStyles[i] = ui.NewStyle(ui.ColorRed, ui.ColorClear, ui.ModifierReverse) 189 } else { 190 table.RowStyles[i] = ui.NewStyle(ui.ColorRed, ui.ColorClear) 191 } 192 } 193 } 194 ui.Render(table) 195 } 196 197 func drawDetails(analysisResult *analyzerunner.AnalyzeResult) { 198 termWidth, _ := ui.TerminalDimensions() 199 200 currentTop := 4 201 title := widgets.NewParagraph() 202 title.Text = analysisResult.Title 203 title.Border = false 204 switch { 205 case analysisResult.IsPass: 206 title.TextStyle = ui.NewStyle(ui.ColorGreen, ui.ColorClear, ui.ModifierBold) 207 case analysisResult.IsWarn: 208 title.TextStyle = ui.NewStyle(ui.ColorYellow, ui.ColorClear, ui.ModifierBold) 209 case analysisResult.IsFail: 210 title.TextStyle = ui.NewStyle(ui.ColorRed, ui.ColorClear, ui.ModifierBold) 211 } 212 height := estimateNumberOfLines(title.Text, termWidth/2) 213 title.SetRect(termWidth/2, currentTop, termWidth, currentTop+height) 214 ui.Render(title) 215 currentTop = currentTop + height + 1 216 217 message := widgets.NewParagraph() 218 message.Text = analysisResult.Message 219 message.Border = false 220 height = estimateNumberOfLines(message.Text, termWidth/2) + 2 221 message.SetRect(termWidth/2, currentTop, termWidth, currentTop+height) 222 ui.Render(message) 223 currentTop = currentTop + height + 1 224 225 if analysisResult.URI != "" { 226 uri := widgets.NewParagraph() 227 uri.Text = fmt.Sprintf("For more information: %s", analysisResult.URI) 228 uri.Border = false 229 height = estimateNumberOfLines(uri.Text, termWidth/2) 230 uri.SetRect(termWidth/2, currentTop, termWidth, currentTop+height) 231 ui.Render(uri) 232 // currentTop = currentTop + height + 1 233 } 234 } 235 236 func estimateNumberOfLines(text string, width int) int { 237 if width == 0 { 238 return 0 239 } 240 lines := len(text)/width + 1 241 return lines 242 } 243 244 // save exports analyzeResults to local file against customize outputPath. 245 func save(preflightName string, outputPath string, analyzeResults []*analyzerunner.AnalyzeResult) (string, error) { 246 filename := "" 247 if outputPath != "" { 248 // use override output path 249 overridePath, err := convert.ValidateOutputPath(outputPath) 250 if err != nil { 251 return "", errors.Wrap(err, "override output file path") 252 } 253 filename = overridePath 254 } else { 255 // use default output path 256 filename = fmt.Sprintf("%s-results-%s.txt", preflightName, time.Now().Format("2006-01-02T15_04_05")) 257 } 258 259 if _, err := os.Stat(filename); err == nil { 260 _ = os.Remove(filename) 261 } 262 263 results := fmt.Sprintf("%s Preflight Checks\n\n", util.AppName(preflightName)) 264 for _, analyzeResult := range analyzeResults { 265 result := "" 266 switch { 267 case analyzeResult.IsPass: 268 result = "Check PASS\n" 269 case analyzeResult.IsWarn: 270 result = "Check WARN\n" 271 case analyzeResult.IsFail: 272 result = "Check FAIL\n" 273 } 274 result += fmt.Sprintf("Title: %s\n", analyzeResult.Title) 275 result += fmt.Sprintf("Message: %s\n", analyzeResult.Message) 276 277 if analyzeResult.URI != "" { 278 result += fmt.Sprintf("URI: %s\n", analyzeResult.URI) 279 } 280 281 if analyzeResult.Strict { 282 result += fmt.Sprintf("Strict: %t\n", analyzeResult.Strict) 283 } 284 result += "\n------------\n" 285 results += result 286 } 287 288 if err := os.WriteFile(filename, []byte(results), 0644); err != nil { 289 return "", errors.Wrap(err, "failed to save preflight results") 290 } 291 292 return filename, nil 293 } 294 295 func showSaved(filename string) { 296 termWidth, termHeight := ui.TerminalDimensions() 297 298 savedMessage := widgets.NewParagraph() 299 savedMessage.Text = fmt.Sprintf("Preflight results saved to\n\n%s", filename) 300 savedMessage.WrapText = true 301 savedMessage.Border = true 302 303 left := termWidth/2 - 20 304 right := termWidth/2 + 20 305 top := termHeight/2 - 4 306 bottom := termHeight/2 + 4 307 308 savedMessage.SetRect(left, top, right, bottom) 309 ui.Render(savedMessage) 310 311 isShowingSaved = true 312 }