From 9aac1de886cab5febb2a2b71b7335aeece5c29bf Mon Sep 17 00:00:00 2001 From: Anuj Chaudhari Date: Mon, 5 Feb 2024 16:28:28 -0800 Subject: [PATCH] Refactor `OutputWriterSpinner` component and Add `SetText` and `StartSpinner` APIs (#157) * Eliminate the use of parameters and use options to create a spinner * Implements SetText, StartSpinner APIs * Update docs --- component/README.md | 87 ++++++++++++------ component/output_spinner.go | 149 ++++++++++++++++++++++--------- component/output_spinner_test.go | 33 +++---- 3 files changed, 185 insertions(+), 84 deletions(-) diff --git a/component/README.md b/component/README.md index 35953d44..c826b414 100644 --- a/component/README.md +++ b/component/README.md @@ -179,51 +179,88 @@ This will output: ## OutputWriterSpinner Component -`OutputWriterSpinner` provides an interface to `OutputWriter` augmented with a spinner. It allows for rendering output with a spinner while also providing the ability to stop the spinner and render the final output. +`OutputWriterSpinner` provides an interface to `OutputWriter` augmented with a spinner. It allows for displaying output of multiple types (Text, Table, Yaml, Json) with a spinner while also providing the ability to stop the spinner, display the final text and render the output. ### Usage -To use `OutputWriterSpinner`, you can import the package in your Go code and create an instance of the `OutputWriterSpinner` interface using the `NewOutputWriterSpinnerWithOptions` function. +To use `OutputWriterSpinner`, you can import the package in your Go code and create an instance of the `OutputWriterSpinner` interface using the `NewOutputWriterSpinner` function. -Note that NewOutputWriterWithSpinner has been deprecated in favor of NewOutputWriterSpinnerWithOptions -One difference in the default rendering behavior for YAML/JSON output between -the writer created with NewOutputWriterWithSpinner vs that created with NewOutputWriterSpinnerWithOptions -is that row fields no longer gets auto stringified in the latter. +Note that `NewOutputWriterWithSpinner` and `NewOutputWriterSpinnerWithOptions` has been deprecated in favor of `NewOutputWriterSpinner`. +The main difference is how the parameters are provided when creating a `OutputWriterSpinner` component. -To retain the stringify behavior (unlikely what if needed except for backward compatibility -reasons), create the output writer by including WithAutoStringify() in the -options list . +#### Using Spinner to display Table/JSON/YAML output ``` go import "github.com/vmware-tanzu/tanzu-plugin-runtime/component" - -// create new OutputWriterSpinner -outputWriterSpinner, err := component.NewOutputWriterSpinnerWithOptions(os.Stdout, "json", "Loading...", true, []OutputWriterOption{}) +// Create new OutputWriterSpinner component. If `WithOutputStream` option is not provided, it will use os.Stdout as default output stream +owSpinner, err := component.NewOutputWriterSpinner( + component.WithOutputFormat(component.TableOutputType), // For JSON use JSONOutputType and for YAML use YAMLOutputType + component.WithSpinnerText("Fetching data..."), + component.WithSpinnerStarted(), + component.WithHeaders("Namespace", "Name", "Ready")) if err != nil { fmt.Println("Error creating OutputWriterSpinner:", err) return } -// Render output with spinner -outputWriterSpinner.RenderWithSpinner() +// Do some processing to fetch the data from server and fill rows for table +owSpinner.AddRow("default", "pod1", "False") +owSpinner.AddRow("default", "pod2", "True") + +// Stop the running spinner instance, displays FinalText if set, then renders the tabular output +owSpinner.Render() -// Stop spinner and render final output -outputWriterSpinner.StopSpinner() ``` -The `NewOutputWriterWithSpinner` function takes in the following parameters: +#### Using Spinner to display plain text + +``` go +import "github.com/vmware-tanzu/tanzu-plugin-runtime/component" + +// Create new OutputWriterSpinner component +spinner, err := component.NewOutputWriterSpinner(component.WithOutputStream(os.Stderr)) +if err != nil { + fmt.Println("Error creating spinner:", err) + return +} -- `output io.Writer`: The output writer for the spinner and final output. -- `outputFormat string`: The output format for the final output. It can be either "json" or "yaml". -- `spinnerText string`: The text to display next to the spinner. -- `startSpinner bool`: Whether to start the spinner immediately or not. -- `headers ...string`: Optional headers for the final output. +spinner.SetText("Installing plugin 'apps'") +spinner.StartSpinner() -The created `OutputWriterSpinner` instance provides two methods: +// Do some processing to fetch and install the required plugin +err := InstallPlugin("apps") + +if err != nil { + spinner.SetFinalText("Error while installing the plugin 'apps'", log.LogTypeERROR) +} else { + spinner.SetFinalText("Successfully installed the plugin 'apps'", log.LogTypeSuccess) +} + +// Stop the spinner and displays FinalText +spinner.StopSpinner() + +``` -- `RenderWithSpinner()`: Renders the output with a spinner. -- `StopSpinner()`: Stops the spinner and renders the final output. +The `NewOutputWriterSpinner` function takes in the following optional options: + +- `WithOutputStream(writer io.Writer)`: sets the output stream for the OutputWriterSpinner component. +- `WithOutputFormat(outputFormat OutputType)`: sets output format for the OutputWriterSpinner component. +- `WithOutputWriterOptions(opts ...OutputWriterOption)`: configures OutputWriterOptions to the OutputWriterSpinner component. +- `WithSpinnerText(text string)`: sets the spinner text +- `WithSpinnerFinalText(finalText string, prefix log.LogType)`: sets the spinner final text and prefix log indicator +- `WithSpinnerStarted()`: starts the spinner when the OutputWriterSpinner component gets created +- `WithHeaders(headers ...string)`: Sets the headers for the OutputWriterOptions component + +The created `OutputWriterSpinner` instance provides following methods: + +- `SetKeys(headerKeys ...string)`: sets the headers for the OutputWriter component +- `AddRow(items ...interface{})`: adds rows to the OutputWriter component +- `SetText(text string)`: sets the spinner text +- `SetFinalText(finalText string, prefix log.LogType)`: sets the spinner final text and prefix log indicator +- `StartSpinner()`: starts the spinner instance, showing the spinnerText +- `StopSpinner()`: stops the running spinner instance, displays FinalText if set +- `Render()`: stops the running spinner instance, displays FinalText if set, then renders the output ## Prompt Component diff --git a/component/output_spinner.go b/component/output_spinner.go index 32f8974b..b202fa70 100644 --- a/component/output_spinner.go +++ b/component/output_spinner.go @@ -18,8 +18,15 @@ import ( // OutputWriterSpinner is OutputWriter augmented with a spinner. type OutputWriterSpinner interface { OutputWriter + // RenderWithSpinner will stop spinner and render the output + // Deprecated: RenderWithSpinner is being deprecated in favor of Render. RenderWithSpinner() + // StartSpinner starts the spinner instance, showing the spinnerText + StartSpinner() + // StopSpinner stops the running spinner instance, displays FinalText if set StopSpinner() + // SetText sets the spinner text + SetText(text string) // SetFinalText sets the spinner final text and prefix // log indicator (log.LogTypeOUTPUT can be used for no prefix) SetFinalText(finalText string, prefix log.LogType) @@ -28,32 +35,69 @@ type OutputWriterSpinner interface { // outputwriterspinner is our internal implementation. type outputwriterspinner struct { outputwriter - spinnerText string - spinnerFinalText string - spinner *spinner.Spinner + spinnerText string + spinnerFinalText string + startSpinnerOnInit bool + spinner *spinner.Spinner } -type OutputWriterSpinnerOptions struct { - OutputWriterOptions []OutputWriterOption - SpinnerOptions []OutputWriterSpinnerOption -} - -// OutputWriterSpinnerOption is an option for outputwriterspinner +// OutputWriterSpinnerOption is an option to configure outputwriterspinner type OutputWriterSpinnerOption func(*outputwriterspinner) // WithSpinnerFinalText sets the spinner final text and prefix log indicator // (log.LogTypeOUTPUT can be used for no prefix) func WithSpinnerFinalText(finalText string, prefix log.LogType) OutputWriterSpinnerOption { - finalText = fmt.Sprintf("%s%s", log.GetLogTypeIndicator(prefix), finalText) return func(ows *outputwriterspinner) { - ows.spinnerFinalText = finalText + ows.spinnerFinalText = fmt.Sprintf("%s%s", log.GetLogTypeIndicator(prefix), finalText) + } +} + +// WithOutputWriterOptions configures OutputWriterOptions to the spinner +func WithOutputWriterOptions(opts ...OutputWriterOption) OutputWriterSpinnerOption { + return func(ow *outputwriterspinner) { + ow.applyOptions(opts) + } +} + +// WithSpinnerText sets the spinner text +func WithSpinnerText(text string) OutputWriterSpinnerOption { + return func(ows *outputwriterspinner) { + ows.spinnerText = text + } +} + +// WithSpinnerStarted starts the spinner +func WithSpinnerStarted() OutputWriterSpinnerOption { + return func(ows *outputwriterspinner) { + ows.startSpinnerOnInit = true + } +} + +// WithHeaders sets key headers +func WithHeaders(headers ...string) OutputWriterSpinnerOption { + return func(ows *outputwriterspinner) { + ows.keys = headers + } +} + +// WithOutputFormat sets output format for the OutputWriterSpinner component +func WithOutputFormat(outputFormat OutputType) OutputWriterSpinnerOption { + return func(ows *outputwriterspinner) { + ows.outputFormat = outputFormat + } +} + +// WithOutputStream sets the output stream for the OutputWriterSpinner component +func WithOutputStream(writer io.Writer) OutputWriterSpinnerOption { + return func(ows *outputwriterspinner) { + ows.out = writer } } // NewOutputWriterWithSpinner returns implementation of OutputWriterSpinner. // // Deprecated: NewOutputWriterWithSpinner is being deprecated in favor of -// NewOutputWriterSpinnerWithSpinnerOptions. +// NewOutputWriterSpinner. // Until it is removed, it will retain the existing behavior of converting // incoming row values to their golang string representation for backward // compatibility reasons @@ -65,60 +109,69 @@ func NewOutputWriterWithSpinner(output io.Writer, outputFormat, spinnerText stri // NewOutputWriterSpinnerWithOptions returns implementation of OutputWriterSpinner. // // Deprecated: NewOutputWriterSpinnerWithOptions is being deprecated in favor of -// NewOutputWriterSpinnerWithSpinnerOptions. +// NewOutputWriterSpinner. func NewOutputWriterSpinnerWithOptions(output io.Writer, outputFormat, spinnerText string, startSpinner bool, opts []OutputWriterOption, headers ...string) (OutputWriterSpinner, error) { ows := &outputwriterspinner{} ows.out = output ows.outputFormat = OutputType(outputFormat) ows.keys = headers ows.applyOptions(opts) - - return setAndInitializeSpinner(ows, spinnerText, startSpinner) + ows.spinnerText = spinnerText + ows.startSpinnerOnInit = startSpinner + return initializeSpinner(ows), nil } -// NewOutputWriterSpinnerWithSpinnerOptions returns implementation of OutputWriterSpinner. -func NewOutputWriterSpinnerWithSpinnerOptions(output io.Writer, outputFormat OutputType, spinnerText string, startSpinner bool, opts OutputWriterSpinnerOptions, headers ...string) (OutputWriterSpinner, error) { +// NewOutputWriterSpinner returns implementation of OutputWriterSpinner +func NewOutputWriterSpinner(opts ...OutputWriterSpinnerOption) (OutputWriterSpinner, error) { ows := &outputwriterspinner{} - ows.out = output - ows.outputFormat = outputFormat - ows.keys = headers - ows.applyOptions(opts.OutputWriterOptions) - ows.applyOutputWriterSpinnerOptions(opts.SpinnerOptions) - return setAndInitializeSpinner(ows, spinnerText, startSpinner) + ows.out = os.Stdout + ows.applySpinnerOptions(opts) + // Note: We are returning the 'nil' error always to protect against the possible API + // enhancement which might require throwing an error in future + return initializeSpinner(ows), nil } -// setAndInitializeSpinner sets the spinner text and initializes the spinner -func setAndInitializeSpinner(ows *outputwriterspinner, spinnerText string, startSpinner bool) (OutputWriterSpinner, error) { +// initializeSpinner initializes the spinner +func initializeSpinner(ows *outputwriterspinner) OutputWriterSpinner { if ows.outputFormat != JSONOutputType && ows.outputFormat != YAMLOutputType { - ows.spinnerText = spinnerText - ows.spinner = spinner.New(spinner.CharSets[9], 100*time.Millisecond) - if err := ows.spinner.Color("bgBlack", "bold", "fgWhite"); err != nil { - return nil, err - } - ows.spinner.Suffix = fmt.Sprintf(" %s", spinnerText) - if ows.spinnerFinalText != "" { - spinner.WithFinalMSG(ows.spinnerFinalText)(ows.spinner) - } + ows.spinner = spinner.New(spinner.CharSets[9], 100*time.Millisecond, + spinner.WithWriter(ows.out), + spinner.WithFinalMSG(ows.spinnerFinalText), + spinner.WithSuffix(fmt.Sprintf(" %s", ows.spinnerText)), + spinner.WithColor("bold"), + ) // Start the spinner only if attached to terminal attachedToTerminal := isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd()) - if startSpinner && attachedToTerminal { + if ows.startSpinnerOnInit && attachedToTerminal { ows.spinner.Start() } } - return ows, nil + return ows } -// RenderWithSpinner will stop spinner and render the output +// RenderWithSpinner stops the running spinner instance, displays FinalText if set, then renders the output +// +// Deprecated: RenderWithSpinner is being deprecated in favor of Render. func (ows *outputwriterspinner) RenderWithSpinner() { - if ows.spinner != nil && ows.spinner.Active() { - ows.spinner.Stop() - fmt.Fprintln(ows.out) - } ows.Render() } -// stop spinner +// Render stops the running spinner instance, displays FinalText if set, then renders the output +func (ows *outputwriterspinner) Render() { + ows.StopSpinner() + ows.outputwriter.Render() +} + +// StartSpinner starts the spinner instance, showing the spinnerText +func (ows *outputwriterspinner) StartSpinner() { + attachedToTerminal := isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd()) + if ows.spinner != nil && !ows.spinner.Active() && attachedToTerminal { + ows.spinner.Start() + } +} + +// StopSpinner stops the running spinner instance, displays FinalText if set func (ows *outputwriterspinner) StopSpinner() { if ows.spinner != nil && ows.spinner.Active() { ows.spinner.Stop() @@ -135,8 +188,16 @@ func (ows *outputwriterspinner) SetFinalText(finalText string, prefix log.LogTyp } } -// applyOutputWriterSpinnerOptions applies the options to the outputwriterspinner -func (ows *outputwriterspinner) applyOutputWriterSpinnerOptions(spinnerOpts []OutputWriterSpinnerOption) { +// SetText sets the spinner text +func (ows *outputwriterspinner) SetText(text string) { + if ows.spinner != nil { + ows.spinnerText = text + ows.spinner.Suffix = fmt.Sprintf(" %s", text) + } +} + +// applySpinnerOptions applies the options to the outputwriterspinner +func (ows *outputwriterspinner) applySpinnerOptions(spinnerOpts []OutputWriterSpinnerOption) { for i := range spinnerOpts { spinnerOpts[i](ows) } diff --git a/component/output_spinner_test.go b/component/output_spinner_test.go index 5dedca5f..f6bc2fa7 100644 --- a/component/output_spinner_test.go +++ b/component/output_spinner_test.go @@ -59,31 +59,34 @@ func TestNewOutputWriterSpinnerWithOptions(t *testing.T) { assert.NotNil(t, ows) } -func TestNewOutputWriterSpinnerWithSpinnerOptions(t *testing.T) { +func TestNewOutputWriterSpinner(t *testing.T) { output := bytes.Buffer{} spinnerText := loading headers := []string{"Name", "Age"} - // Test creating an OutputWriterSpinner with spinner options and a spinner - opts := OutputWriterSpinnerOptions{ - OutputWriterOptions: []OutputWriterOption{WithAutoStringify()}, - SpinnerOptions: []OutputWriterSpinnerOption{WithSpinnerFinalText("Done!", log.LogTypeSUCCESS)}, - } - ows, err := NewOutputWriterSpinnerWithSpinnerOptions(&output, "table", spinnerText, true, opts, headers...) + ows, err := NewOutputWriterSpinner(WithOutputStream(&output), + WithOutputFormat(TableOutputType), + WithSpinnerText(spinnerText), + WithSpinnerStarted(), + WithOutputWriterOptions(WithAutoStringify()), + WithHeaders(headers...), + WithSpinnerFinalText("Done!", log.LogTypeSUCCESS)) assert.NoError(t, err) assert.NotNil(t, ows) - // Test creating an OutputWriterSpinner with spinner options without a spinner - opts = OutputWriterSpinnerOptions{ - SpinnerOptions: []OutputWriterSpinnerOption{WithSpinnerFinalText("Done!", log.LogTypeSUCCESS)}, - } - ows, err = NewOutputWriterSpinnerWithSpinnerOptions(&output, "table", spinnerText, false, opts, headers...) + ows, err = NewOutputWriterSpinner(WithOutputStream(&output), + WithOutputFormat(TableOutputType), + WithSpinnerText(spinnerText), + WithHeaders(headers...), + WithSpinnerFinalText("Done!", log.LogTypeSUCCESS)) assert.NoError(t, err) assert.NotNil(t, ows) - // Test creating an OutputWriterSpinner with unsupported output format - opts = OutputWriterSpinnerOptions{} - ows, err = NewOutputWriterSpinnerWithSpinnerOptions(&output, "unsupported", spinnerText, true, opts, headers...) + ows, err = NewOutputWriterSpinner(WithOutputStream(&output), + WithOutputFormat("unsupported"), + WithSpinnerText(spinnerText), + WithSpinnerStarted(), + WithHeaders(headers...)) assert.NoError(t, err) assert.NotNil(t, ows) }