From a96e17279f1e9062ab8ca0321204e257956b25ae Mon Sep 17 00:00:00 2001 From: Marc Campbell Date: Fri, 19 Jul 2019 14:57:55 +0000 Subject: [PATCH] preflight ui --- cmd/preflight/cli/interactive_results.go | 254 +++++++++++++++--- cmd/preflight/cli/run_nocrd.go | 2 +- .../troubleshoot_v1beta1_preflight.yaml | 6 +- 3 files changed, 216 insertions(+), 46 deletions(-) diff --git a/cmd/preflight/cli/interactive_results.go b/cmd/preflight/cli/interactive_results.go index 8b133897..7170ac90 100644 --- a/cmd/preflight/cli/interactive_results.go +++ b/cmd/preflight/cli/interactive_results.go @@ -2,103 +2,273 @@ package cli import ( "fmt" + "io/ioutil" + "os" + "path" + "strings" + "time" ui "github.com/gizak/termui/v3" "github.com/gizak/termui/v3/widgets" analyzerunner "github.com/replicatedhq/troubleshoot/pkg/analyze" ) -type nodeValue string +var ( + selectedResult = 0 + isShowingSaved = false +) -func (nv nodeValue) String() string { - return string(nv) -} - -func showInteractiveResults(analyzeResults []*analyzerunner.AnalyzeResult) error { +func showInteractiveResults(preflightName string, analyzeResults []*analyzerunner.AnalyzeResult) error { if err := ui.Init(); err != nil { return err } defer ui.Close() - selectedResult := 0 - - preflightTable := getPreflightTable(analyzeResults) - details := getDetails(analyzeResults[selectedResult]) - - grid := ui.NewGrid() - termWidth, termHeight := ui.TerminalDimensions() - grid.SetRect(0, 0, termWidth, termHeight) - - grid.Set( - ui.NewRow(1.0, - ui.NewCol(1.0/2, preflightTable), - ui.NewCol(1.0/2, details), - ), - ) - - ui.Render(grid) + drawUI(preflightName, analyzeResults) uiEvents := ui.PollEvents() for { select { case e := <-uiEvents: switch e.ID { - case "q", "": + case "": return nil + case "q": + if isShowingSaved == true { + isShowingSaved = false + ui.Clear() + drawUI(preflightName, analyzeResults) + } else { + return nil + } + case "s": + filename, err := save(preflightName, analyzeResults) + if err != nil { + // show + } else { + showSaved(filename) + go func() { + time.Sleep(time.Second * 5) + isShowingSaved = false + ui.Clear() + drawUI(preflightName, analyzeResults) + }() + } case "": - payload := e.Payload.(ui.Resize) - grid.SetRect(0, 0, payload.Width, payload.Height) ui.Clear() - ui.Render(grid) + drawUI(preflightName, analyzeResults) + case "": + if selectedResult < len(analyzeResults)-1 { + selectedResult++ + } else { + selectedResult = 0 + } + ui.Clear() + drawUI(preflightName, analyzeResults) + case "": + if selectedResult > 0 { + selectedResult-- + } else { + selectedResult = len(analyzeResults) - 1 + } + ui.Clear() + drawUI(preflightName, analyzeResults) } } } } -func getPreflightTable(analyzeResults []*analyzerunner.AnalyzeResult) *widgets.Table { +func drawUI(preflightName string, analyzeResults []*analyzerunner.AnalyzeResult) { + drawGrid(analyzeResults) + drawHeader(preflightName) + drawFooter() +} + +func drawGrid(analyzeResults []*analyzerunner.AnalyzeResult) { + drawPreflightTable(analyzeResults) + drawDetails(analyzeResults[selectedResult]) +} + +func drawHeader(preflightName string) { + termWidth, _ := ui.TerminalDimensions() + + title := widgets.NewParagraph() + title.Text = fmt.Sprintf("%s Preflight Checks", strings.Title(strings.Replace(preflightName, "-", " ", -1))) + title.TextStyle.Fg = ui.ColorWhite + title.TextStyle.Bg = ui.ColorClear + title.TextStyle.Modifier = ui.ModifierBold + title.Border = false + + left := termWidth/2 - 2*len(title.Text)/3 + right := termWidth/2 + (termWidth/2 - left) + + title.SetRect(left, 0, right, 1) + ui.Render(title) +} + +func drawFooter() { + termWidth, termHeight := ui.TerminalDimensions() + + instructions := widgets.NewParagraph() + instructions.Text = "[q] quit [s] save [↑][↓] scroll" + instructions.Border = false + + left := 0 + right := termWidth + top := termHeight - 1 + bottom := termHeight + + instructions.SetRect(left, top, right, bottom) + ui.Render(instructions) +} + +func drawPreflightTable(analyzeResults []*analyzerunner.AnalyzeResult) { + termWidth, termHeight := ui.TerminalDimensions() + table := widgets.NewTable() + table.SetRect(0, 3, termWidth/2, termHeight-6) + table.FillRow = true table.Border = true table.Rows = [][]string{} + table.ColumnWidths = []int{termWidth} for i, analyzeResult := range analyzeResults { + title := analyzeResult.Title + if analyzeResult.IsPass { + title = fmt.Sprintf("✔ %s", title) + } else if analyzeResult.IsWarn { + title = fmt.Sprintf("⚠️ %s", title) + } else if analyzeResult.IsFail { + title = fmt.Sprintf("✘ %s", title) + } table.Rows = append(table.Rows, []string{ - analyzeResult.Title, + title, }) if analyzeResult.IsPass { - table.RowStyles[i] = ui.NewStyle(ui.ColorGreen, ui.ColorClear, ui.ModifierBold) + if i == selectedResult { + table.RowStyles[i] = ui.NewStyle(ui.ColorGreen, ui.ColorClear, ui.ModifierReverse) + } else { + table.RowStyles[i] = ui.NewStyle(ui.ColorGreen, ui.ColorClear) + } } else if analyzeResult.IsWarn { - table.RowStyles[i] = ui.NewStyle(ui.ColorYellow, ui.ColorClear, ui.ModifierBold) + if i == selectedResult { + table.RowStyles[i] = ui.NewStyle(ui.ColorYellow, ui.ColorClear, ui.ModifierReverse) + } else { + table.RowStyles[i] = ui.NewStyle(ui.ColorYellow, ui.ColorClear) + } } else if analyzeResult.IsFail { - table.RowStyles[i] = ui.NewStyle(ui.ColorRed, ui.ColorClear) + if i == selectedResult { + table.RowStyles[i] = ui.NewStyle(ui.ColorRed, ui.ColorClear, ui.ModifierReverse) + } else { + table.RowStyles[i] = ui.NewStyle(ui.ColorRed, ui.ColorClear) + } } } - return table + ui.Render(table) } -func getDetails(analysisResult *analyzerunner.AnalyzeResult) *ui.Grid { - grid := ui.NewGrid() - - entries := []interface{}{} +func drawDetails(analysisResult *analyzerunner.AnalyzeResult) { + termWidth, _ := ui.TerminalDimensions() + currentTop := 4 title := widgets.NewParagraph() title.Text = analysisResult.Title title.Border = false - entries = append(entries, ui.NewRow(0.2, ui.NewCol(1.0, title))) + if analysisResult.IsPass { + title.TextStyle = ui.NewStyle(ui.ColorGreen, ui.ColorClear, ui.ModifierBold) + } else if analysisResult.IsWarn { + title.TextStyle = ui.NewStyle(ui.ColorYellow, ui.ColorClear, ui.ModifierBold) + } else if analysisResult.IsFail { + title.TextStyle = ui.NewStyle(ui.ColorRed, ui.ColorClear, ui.ModifierBold) + } + height := estimateNumberOfLines(title.Text, termWidth/2) + title.SetRect(termWidth/2, currentTop, termWidth, currentTop+height) + ui.Render(title) + currentTop = currentTop + height + 1 message := widgets.NewParagraph() message.Text = analysisResult.Message message.Border = false - entries = append(entries, ui.NewRow(0.2, ui.NewCol(1.0, message))) + height = estimateNumberOfLines(message.Text, termWidth/2) + 2 + message.SetRect(termWidth/2, currentTop, termWidth, currentTop+height) + ui.Render(message) + currentTop = currentTop + height + 1 if analysisResult.URI != "" { uri := widgets.NewParagraph() uri.Text = fmt.Sprintf("For more information: %s", analysisResult.URI) uri.Border = false - entries = append(entries, ui.NewRow(0.2, ui.NewCol(1.0, uri))) + height = estimateNumberOfLines(uri.Text, termWidth/2) + uri.SetRect(termWidth/2, currentTop, termWidth, currentTop+height) + ui.Render(uri) + currentTop = currentTop + height + 1 + } +} + +func estimateNumberOfLines(text string, width int) int { + if len(text) < width { + return 1 } - grid.Set(entries...) - return grid + lines := len(text)/width + 1 + return lines +} + +func save(preflightName string, analyzeResults []*analyzerunner.AnalyzeResult) (string, error) { + filename := path.Join(homeDir(), fmt.Sprintf("%s-results.txt", preflightName)) + _, err := os.Stat(filename) + if err == nil { + os.Remove(filename) + } + + results := fmt.Sprintf("%s Preflight Checks\n\n", strings.Title(strings.Replace(preflightName, "-", " ", -1))) + for _, analyzeResult := range analyzeResults { + result := "" + + if analyzeResult.IsPass { + result = "Check PASS\n" + } else if analyzeResult.IsWarn { + result = "Check WARN\n" + } else if analyzeResult.IsFail { + result = "Check FAIL\n" + } + + result = result + fmt.Sprintf("Title: %s\n", analyzeResult.Title) + result = result + fmt.Sprintf("Message: %s\n", analyzeResult.Message) + + if analyzeResult.URI != "" { + result = result + fmt.Sprintf("URI: %s\n", analyzeResult.URI) + } + + result = result + "\n------------\n" + + results = results + result + } + + if err := ioutil.WriteFile(filename, []byte(results), 0644); err != nil { + return "", err + } + + return filename, nil +} + +func showSaved(filename string) { + termWidth, termHeight := ui.TerminalDimensions() + + savedMessage := widgets.NewParagraph() + savedMessage.Text = fmt.Sprintf("Preflight results saved to\n\n%s", filename) + savedMessage.WrapText = true + savedMessage.Border = true + + left := termWidth/2 - 20 + right := termWidth/2 + 20 + top := termHeight/2 - 4 + bottom := termHeight/2 + 4 + + savedMessage.SetRect(left, top, right, bottom) + ui.Render(savedMessage) + + isShowingSaved = true } diff --git a/cmd/preflight/cli/run_nocrd.go b/cmd/preflight/cli/run_nocrd.go index cff21bf3..75b0e39c 100644 --- a/cmd/preflight/cli/run_nocrd.go +++ b/cmd/preflight/cli/run_nocrd.go @@ -88,7 +88,7 @@ func runPreflightsNoCRD(v *viper.Viper, arg string) error { } if v.GetBool("interactive") { - return showInteractiveResults(analyzeResults) + return showInteractiveResults(preflight.Name, analyzeResults) } fmt.Printf("only interactive results are supported\n") diff --git a/config/samples/troubleshoot_v1beta1_preflight.yaml b/config/samples/troubleshoot_v1beta1_preflight.yaml index da8b83d8..0ad6f329 100644 --- a/config/samples/troubleshoot_v1beta1_preflight.yaml +++ b/config/samples/troubleshoot_v1beta1_preflight.yaml @@ -1,14 +1,14 @@ apiVersion: troubleshoot.replicated.com/v1beta1 kind: Preflight metadata: - name: preflight-sample + name: sample-app spec: analyzers: - clusterVersion: outcomes: - fail: when: "< 1.14.0" - message: You need more kubernetes + message: Sorry, this application requires at least Kubernetes 1.14.0. Please update your Kubernetes cluster before installing. uri: https://help.replicated.com/kubernetes-version - warn: when: "< 1.15.0" @@ -23,7 +23,7 @@ spec: - fail: message: The micr0k8s storage class thing was not found - pass: - message: All good on storage classes + message: The required storage class was found in the cluster. - secret: checkName: PG URI secretName: postgres