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  }