From 0aca4d9abefe119ef3064603171ccf4e01cd629c Mon Sep 17 00:00:00 2001 From: Vincenzo Marcella <6026326+vmarcella@users.noreply.github.com> Date: Thu, 7 Mar 2024 14:08:27 -0800 Subject: [PATCH] Update Interactive mode to render different user interfaces based on the environment it's running in (#182) When running `ie interactive` on azure, Interactive mode will render a simplified user interface that mimics entering the commands with a prompt and displaying the output. --- internal/engine/engine.go | 17 +++++- internal/engine/environments/azure.go | 2 +- internal/engine/interactive.go | 84 ++++++++++++++++----------- internal/ui/text.go | 11 +++- 4 files changed, 77 insertions(+), 37 deletions(-) diff --git a/internal/engine/engine.go b/internal/engine/engine.go index fee9fd36..caf71753 100644 --- a/internal/engine/engine.go +++ b/internal/engine/engine.go @@ -2,6 +2,7 @@ package engine import ( "fmt" + "strings" "github.com/Azure/InnovationEngine/internal/az" "github.com/Azure/InnovationEngine/internal/engine/environments" @@ -78,7 +79,21 @@ func (e *Engine) InteractWithScenario(scenario *Scenario) error { } program = tea.NewProgram(model, tea.WithAltScreen(), tea.WithMouseCellMotion()) - _, err = program.Run() + + var finalModel tea.Model + var ok bool + finalModel, err = program.Run() + + model, ok = finalModel.(InteractiveModeModel) + + if environments.EnvironmentsAzure == e.Configuration.Environment { + if !ok { + return fmt.Errorf("failed to cast tea.Model to InteractiveModeModel") + } + + logging.GlobalLogger.Info("Writing session output to stdout") + fmt.Println(strings.Join(model.commandLines, "\n")) + } switch e.Configuration.Environment { case environments.EnvironmentsAzure, environments.EnvironmentsOCD: diff --git a/internal/engine/environments/azure.go b/internal/engine/environments/azure.go index c1e5f39d..8155c5fb 100644 --- a/internal/engine/environments/azure.go +++ b/internal/engine/environments/azure.go @@ -79,7 +79,7 @@ func ReportAzureStatus(status AzureDeploymentStatus, environment string) { } else { // We add these strings to the output so that the portal can find and parse // the JSON status. - ocdStatus := fmt.Sprintf("ie_us%sie_ue\n", statusJson) + ocdStatus := fmt.Sprintf("ie_us%sie_ue", statusJson) fmt.Println(ui.OcdStatusUpdateStyle.Render(ocdStatus)) } } diff --git a/internal/engine/interactive.go b/internal/engine/interactive.go index bf02070b..156615d1 100644 --- a/internal/engine/interactive.go +++ b/internal/engine/interactive.go @@ -2,6 +2,7 @@ package engine import ( "fmt" + "strings" "time" "github.com/Azure/InnovationEngine/internal/az" @@ -41,9 +42,10 @@ type CodeBlockState struct { } type interactiveModeComponents struct { - paginator paginator.Model - stepViewport viewport.Model - outputViewport viewport.Model + paginator paginator.Model + stepViewport viewport.Model + outputViewport viewport.Model + azureCLIViewport viewport.Model } type InteractiveModeModel struct { @@ -62,6 +64,7 @@ type InteractiveModeModel struct { scenarioCompleted bool components interactiveModeComponents ready bool + commandLines []string } // Initialize the intractive mode model @@ -91,11 +94,13 @@ func initializeComponents(model InteractiveModeModel, width, height int) interac stepViewport := viewport.New(width, 4) outputViewport := viewport.New(width, 2) + azureCLIViewport := viewport.New(width, height) components := interactiveModeComponents{ - paginator: p, - stepViewport: stepViewport, - outputViewport: outputViewport, + paginator: p, + stepViewport: stepViewport, + outputViewport: outputViewport, + azureCLIViewport: azureCLIViewport, } components.updateViewportHeight(height) @@ -207,6 +212,7 @@ func (components *interactiveModeComponents) updateViewportHeight(terminalHeight components.stepViewport.Height = stepViewportHeight components.outputViewport.Height = outputViewportHeight + components.azureCLIViewport.Height = terminalHeight - 1 } // Updates the intractive mode model @@ -225,6 +231,7 @@ func (model InteractiveModeModel) Update(message tea.Msg) (tea.Model, tea.Cmd) { } else { model.components.stepViewport.Width = message.Width model.components.outputViewport.Width = message.Width + model.components.azureCLIViewport.Width = message.Width model.components.updateViewportHeight(message.Height) } @@ -255,10 +262,18 @@ func (model InteractiveModeModel) Update(message tea.Msg) (tea.Model, tea.Cmd) { model.resourceGroupName = tmpResourceGroup } } + model.commandLines = append(model.commandLines, codeBlockState.StdOut) // Increment the codeblock and update the viewport content. model.currentCodeBlock++ + if model.currentCodeBlock < len(model.codeBlockState) { + nextCommand := model.codeBlockState[model.currentCodeBlock].CodeBlock.Content + nextLanguage := model.codeBlockState[model.currentCodeBlock].CodeBlock.Language + + model.commandLines = append(model.commandLines, ui.CommandPrompt(nextLanguage)+nextCommand) + } + // Only increment the step for azure if the step name has changed. nextCodeBlockState := model.codeBlockState[model.currentCodeBlock] @@ -301,6 +316,7 @@ func (model InteractiveModeModel) Update(message tea.Msg) (tea.Model, tea.Cmd) { codeBlockState.Success = false model.codeBlockState[step] = codeBlockState + model.commandLines = append(model.commandLines, codeBlockState.StdErr) // Report the error model.executingCommand = false @@ -381,6 +397,8 @@ func (model InteractiveModeModel) Update(message tea.Msg) (tea.Model, tea.Cmd) { model.components.outputViewport.SetContent(block.StdErr) } + model.components.azureCLIViewport.SetContent(strings.Join(model.commandLines, "\n")) + // Update all the viewports and append resulting commands. var command tea.Cmd @@ -392,6 +410,9 @@ func (model InteractiveModeModel) Update(message tea.Msg) (tea.Model, tea.Cmd) { model.components.outputViewport, command = model.components.outputViewport.Update(message) commands = append(commands, command) + model.components.azureCLIViewport, command = model.components.azureCLIViewport.Update(message) + commands = append(commands, command) + return model, tea.Batch(commands...) } @@ -419,44 +440,33 @@ func (model InteractiveModeModel) helpView() string { // Renders the interactive mode model. func (model InteractiveModeModel) View() string { + // When running in the portal, we only want to show the Azure CLI viewport + // which mimics a command line interface during execution. + if model.environment == "azure" { + return model.components.azureCLIViewport.View() + } + scenarioTitle := ui.ScenarioTitleStyle.Width(model.width). Align(lipgloss.Center). Render(model.scenarioTitle) - var stepTitle string - var stepView string - var stepSection string - stepTitle = ui.StepTitleStyle.Render( + + border := lipgloss.NewStyle(). + Width(model.components.stepViewport.Width - 2). + Border(lipgloss.NormalBorder()) + + stepTitle := ui.StepTitleStyle.Render( fmt.Sprintf( "Step %d - %s", model.currentCodeBlock+1, model.codeBlockState[model.currentCodeBlock].StepName, ), ) + stepView := border.Render(model.components.stepViewport.View()) + stepSection := fmt.Sprintf("%s\n%s\n\n", stepTitle, stepView) - border := lipgloss.NewStyle(). - Width(model.components.stepViewport.Width - 2) - // Border(lipgloss.NormalBorder()) - - stepView = border.Render(model.components.stepViewport.View()) - - if model.environment != "azure" { - stepSection = fmt.Sprintf("%s\n%s\n\n", stepTitle, stepView) - } else { - stepSection = fmt.Sprintf("%s\n%s\n", stepTitle, stepView) - } - - var outputTitle string - var outputView string - var outputSection string - if model.environment != "azure" { - outputTitle = ui.StepTitleStyle.Render("Output") - outputView = border.Render(model.components.outputViewport.View()) - outputSection = fmt.Sprintf("%s\n%s\n\n", outputTitle, outputView) - } else { - outputTitle = "" - outputView = "" - outputSection = "" - } + outputTitle := ui.StepTitleStyle.Render("Output") + outputView := border.Render(model.components.outputViewport.View()) + outputSection := fmt.Sprintf("%s\n%s\n\n", outputTitle, outputView) paginator := lipgloss.NewStyle(). Width(model.width). @@ -532,6 +542,11 @@ func NewInteractiveModeModel( azureStatus.AddStep(fmt.Sprintf("%d. %s", stepNumber+1, step.Name), azureCodeBlocks) } + language := codeBlockState[0].CodeBlock.Language + commandLines := []string{ + ui.CommandPrompt(language) + codeBlockState[0].CodeBlock.Content, + } + return InteractiveModeModel{ scenarioTitle: title, commands: InteractiveModeCommands{ @@ -562,5 +577,6 @@ func NewInteractiveModeModel( environment: engine.Configuration.Environment, scenarioCompleted: false, ready: false, + commandLines: commandLines, }, nil } diff --git a/internal/ui/text.go b/internal/ui/text.go index c04cb4f4..3ce1274e 100644 --- a/internal/ui/text.go +++ b/internal/ui/text.go @@ -44,8 +44,18 @@ var ( b.Left = "┤" return InteractiveModeStepTitleStyle.Copy().BorderStyle(b) }().Foreground(lipgloss.Color("#fff")) + + promptTextStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#6CB6FF")) + promptDollarStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#32CD32")) ) +// Command prompt for interactive environments +func CommandPrompt(language string) string { + promptText := promptTextStyle.Render(language) + promptDollar := promptDollarStyle.Render("$") + return promptText + ":" + promptDollar + " " +} + // Indents a multi-line command to be nested under the first line of the // command. func IndentMultiLineCommand(content string, indentation int) string { @@ -56,7 +66,6 @@ func IndentMultiLineCommand(content string, indentation int) string { } else if strings.TrimSpace(lines[i]) != "" { lines[i] = strings.Repeat(" ", indentation) + lines[i] } - } return strings.Join(lines, "\n") }