diff --git a/cmd/root.go b/cmd/root.go index de53f422..7ca4466a 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -14,6 +14,7 @@ import ( "connectrpc.com/connect" tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" "github.com/google/uuid" "github.com/overmindtech/cli/tracing" "github.com/overmindtech/sdp-go" @@ -233,9 +234,9 @@ Then enter the code: // sp := createSpinner() // output += sp.View() + " Waiting for confirmation..." case Authenticated: - output = "✅ Authenticated successfully. Press any key to continue." + output = lipgloss.NewStyle().Foreground(ColorPalette.BgSuccess).Render("✔︎") + " Authenticated successfully. Press any key to continue." case ErrorAuthenticating: - output = "⛔️ Unable to authenticate. Try again." + output = lipgloss.NewStyle().Foreground(ColorPalette.BgDanger).Render("x") + " Unable to authenticate. Try again." } return containerStyle.Render(output) diff --git a/cmd/tea_initialisesources.go b/cmd/tea_initialisesources.go index 3c019b83..cd3af76e 100644 --- a/cmd/tea_initialisesources.go +++ b/cmd/tea_initialisesources.go @@ -12,6 +12,7 @@ import ( "connectrpc.com/connect" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/huh" + "github.com/charmbracelet/lipgloss" "github.com/overmindtech/sdp-go" "github.com/spf13/viper" "golang.org/x/oauth2" @@ -251,10 +252,10 @@ func (m initialiseSourcesModel) View() string { view += fmt.Sprintf("\n%v", m.profileInputForm.View()) } if m.awsSourceRunning { - view += "\n✅ AWS Source: running" + view += fmt.Sprintf("\n %v AWS Source: running", lipgloss.NewStyle().Foreground(ColorPalette.BgSuccess).Render("✔︎")) } if m.stdlibSourceRunning { - view += "\n✅ stdlib Source: running" + view += fmt.Sprintf("\n %v stdlib Source: running", lipgloss.NewStyle().Foreground(ColorPalette.BgSuccess).Render("✔︎")) } return view } diff --git a/cmd/tea_snapshot.go b/cmd/tea_snapshot.go index 2184ad04..9e8098b6 100644 --- a/cmd/tea_snapshot.go +++ b/cmd/tea_snapshot.go @@ -4,11 +4,10 @@ import ( "fmt" tea "github.com/charmbracelet/bubbletea" - log "github.com/sirupsen/logrus" ) type snapshotModel struct { - title string + taskModel state string items uint32 edges uint32 @@ -28,11 +27,18 @@ type finishSnapshotMsg struct { edges uint32 } +func NewSnapShotModel(title string) snapshotModel { + return snapshotModel{ + taskModel: NewTaskModel(title), + state: "pending", + } +} + func (m snapshotModel) Init() tea.Cmd { - return nil + return m.taskModel.Init() } -func (m snapshotModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (m snapshotModel) Update(msg tea.Msg) (snapshotModel, tea.Cmd) { switch msg := msg.(type) { case startSnapshotMsg: m.state = msg.newState @@ -44,8 +50,11 @@ func (m snapshotModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.state = msg.newState m.items = msg.items m.edges = msg.edges + default: + var cmd tea.Cmd + m.taskModel, cmd = m.taskModel.Update(msg) + return m, cmd } - log.Debugf("updated %+v => %v", msg, m) return m, nil } @@ -53,16 +62,16 @@ func (m snapshotModel) View() string { // TODO: add spinner and/or progressbar; complication: we do not have a // expected number of items/edges to count towards for the progressbar if m.items == 0 && m.edges == 0 { - return fmt.Sprintf("%s - %s", m.title, m.state) + return fmt.Sprintf("%v - %v", m.taskModel.View(), m.state) } else if m.items == 1 && m.edges == 0 { - return fmt.Sprintf("%s - %s: 1 item", m.title, m.state) + return fmt.Sprintf("%v - %v: 1 item", m.taskModel.View(), m.state) } else if m.items == 1 && m.edges == 1 { - return fmt.Sprintf("%s - %s: 1 item, 1 edge", m.title, m.state) + return fmt.Sprintf("%v - %v: 1 item, 1 edge", m.taskModel.View(), m.state) } else if m.items > 1 && m.edges == 0 { - return fmt.Sprintf("%s - %s: %d items", m.title, m.state, m.items) + return fmt.Sprintf("%v - %v: %d items", m.taskModel.View(), m.state, m.items) } else if m.items > 1 && m.edges == 1 { - return fmt.Sprintf("%s - %s: %d items, 1 edge", m.title, m.state, m.items) + return fmt.Sprintf("%v - %v: %d items, 1 edge", m.taskModel.View(), m.state, m.items) } else { - return fmt.Sprintf("%s - %s: %d items, %d edges", m.title, m.state, m.items, m.edges) + return fmt.Sprintf("%v - %v: %d items, %d edges", m.taskModel.View(), m.state, m.items, m.edges) } } diff --git a/cmd/tea_taskmodel.go b/cmd/tea_taskmodel.go index c755f848..3070cd2f 100644 --- a/cmd/tea_taskmodel.go +++ b/cmd/tea_taskmodel.go @@ -108,10 +108,6 @@ func (m taskModel) View() string { label = lipgloss.NewStyle().Foreground(ColorPalette.LabelFaint).Render("+") case taskStatusRunning: label = m.spinner.View() - // all other lables are 7 cells wide - // for ansi.PrintableRuneWidth(label) <= 7 { - // label += " " - // } case taskStatusDone: label = lipgloss.NewStyle().Foreground(ColorPalette.BgSuccess).Render("✔︎") case taskStatusError: diff --git a/cmd/tea_terraform.go b/cmd/tea_terraform.go index ba3db6ce..9b07535c 100644 --- a/cmd/tea_terraform.go +++ b/cmd/tea_terraform.go @@ -82,22 +82,6 @@ func (m cmdModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { batch := []tea.Cmd{} - // update the main command - var cmd tea.Cmd - m.cmd, cmd = m.cmd.Update(msg) - if cmd != nil { - batch = append(batch, cmd) - } - - // pass all messages to all tasks - for k, t := range m.tasks { - tm, cmd := t.Update(msg) - m.tasks[k] = tm - if cmd != nil { - batch = append(batch, cmd) - } - } - // special case the messages that need to be handled at this level switch msg := msg.(type) { case tea.KeyMsg: @@ -145,6 +129,22 @@ func (m cmdModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { skipView(m.View()) } + // update the main command + var cmd tea.Cmd + m.cmd, cmd = m.cmd.Update(msg) + if cmd != nil { + batch = append(batch, cmd) + } + + // pass all messages to all tasks + for k, t := range m.tasks { + tm, cmd := t.Update(msg) + m.tasks[k] = tm + if cmd != nil { + batch = append(batch, cmd) + } + } + return m, tea.Batch(batch...) } diff --git a/cmd/terraform_apply.go b/cmd/terraform_apply.go index 20c07c0d..89b43d97 100644 --- a/cmd/terraform_apply.go +++ b/cmd/terraform_apply.go @@ -81,9 +81,9 @@ Applying changes with ` + "`" + `terraform %v` + "`\n" processingHeader: processingHeader, startingChange: make(chan tea.Msg, 10), // provide a small buffer for sending updates, so we don't block the processing - startingChangeSnapshot: snapshotModel{title: "Starting Change", state: "pending"}, + startingChangeSnapshot: NewSnapShotModel("Starting Change"), endingChange: make(chan tea.Msg, 10), // provide a small buffer for sending updates, so we don't block the processing - endingChangeSnapshot: snapshotModel{title: "Ending Change", state: "pending"}, + endingChangeSnapshot: NewSnapShotModel("Ending Change"), progress: []string{}, } } diff --git a/cmd/terraform_plan.go b/cmd/terraform_plan.go index 2814f8fb..1c06c801 100644 --- a/cmd/terraform_plan.go +++ b/cmd/terraform_plan.go @@ -233,20 +233,20 @@ type tfPlanModel struct { ctx context.Context // note that this ctx is not initialized on NewTfPlanModel to instead get a modified context through the loadSourcesConfigMsg that has a timeout and cancelFunction configured oi OvermindInstance - args []string - planTask taskModel - planHeader string - processingTask taskModel - processingHeader string + args []string + planTask taskModel + planHeader string revlinkWarmupFinished bool - runTfPlan bool - tfPlanFinished bool - processing chan tea.Msg - processingModel snapshotModel - progress []string - changeUrl string + runTfPlan bool + tfPlanFinished bool + processing chan tea.Msg + blastRadiusModel snapshotModel + progress []string + changeUrl string + + riskTask taskModel riskMilestones []*sdp.RiskCalculationStatus_ProgressMilestone riskMilestoneTasks []taskModel risks []*sdp.Risk @@ -277,26 +277,24 @@ func NewTfPlanModel(args []string) tea.Model { planHeader := `Running ` + "`" + `terraform %v` + "`\n" planHeader = fmt.Sprintf(planHeader, strings.Join(args, " ")) - processingHeader := ` Processing plan from ` + "`" + `terraform %v` + "`\n" - processingHeader = fmt.Sprintf(processingHeader, strings.Join(args, " ")) - return tfPlanModel{ - args: args, - planTask: NewTaskModel("Planning Changes"), - planHeader: planHeader, - processingTask: NewTaskModel("Processing Planned Changes"), - processingHeader: processingHeader, - - processing: make(chan tea.Msg, 10), // provide a small buffer for sending updates, so we don't block the processing - processingModel: snapshotModel{title: "Calculating Blast Radius", state: "pending"}, - progress: []string{}, + args: args, + planTask: NewTaskModel("Planning Changes"), + planHeader: planHeader, + + processing: make(chan tea.Msg, 10), // provide a small buffer for sending updates, so we don't block the processing + blastRadiusModel: NewSnapShotModel("Calculating Blast Radius"), + progress: []string{}, + + riskTask: NewTaskModel("Calculating Risks"), } } func (m tfPlanModel) Init() tea.Cmd { return tea.Batch( m.planTask.Init(), - m.processingTask.Init(), + m.blastRadiusModel.Init(), + m.riskTask.Init(), ) } @@ -324,7 +322,7 @@ func (m tfPlanModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { c.Env = append(c.Env, fmt.Sprintf("AWS_PROFILE=%v", aws_profile)) } - m.processingModel.state = "executing terraform plan" + m.blastRadiusModel.state = "executing terraform plan" if viper.GetString("ovm-test-fake") != "" { c = exec.CommandContext(m.ctx, "bash", "-c", "for i in $(seq 100); do echo fake terraform plan progress line $i of 100; done; sleep 1") @@ -354,21 +352,22 @@ func (m tfPlanModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } case triggerPlanProcessingMsg: - m.processingTask.status = taskStatusRunning - m.processingModel.state = "executed terraform plan" + m.blastRadiusModel.status = taskStatusRunning + m.blastRadiusModel.state = "executed terraform plan" cmds = append(cmds, m.processPlanCmd, - m.processingTask.spinner.Tick, + m.blastRadiusModel.spinner.Tick, m.waitForProcessingActivity, ) case processingActivityMsg: - m.processingModel.state = "processing" + m.blastRadiusModel.state = "processing" m.progress = append(m.progress, msg.text) cmds = append(cmds, m.waitForProcessingActivity) case processingFinishedActivityMsg: - m.processingModel.state = "finished" - m.processingTask.status = taskStatusDone + m.blastRadiusModel.status = taskStatusDone + m.blastRadiusModel.state = "finished" + m.riskTask.status = taskStatusDone m.progress = append(m.progress, msg.text) cmds = append(cmds, m.waitForProcessingActivity) case changeUpdatedMsg: @@ -379,7 +378,7 @@ func (m tfPlanModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { for _, ms := range msg.riskMilestones { tm := NewTaskModel(ms.GetDescription()) m.riskMilestoneTasks = append(m.riskMilestoneTasks, tm) - cmds = append(cmds, tm.spinner.Tick) + cmds = append(cmds, tm.Init()) } } for i, ms := range msg.riskMilestones { @@ -393,25 +392,46 @@ func (m tfPlanModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.riskMilestoneTasks[i].status = taskStatusDone case sdp.RiskCalculationStatus_ProgressMilestone_STATUS_INPROGRESS: m.riskMilestoneTasks[i].status = taskStatusRunning + cmds = append(cmds, m.riskMilestoneTasks[i].spinner.Tick) case sdp.RiskCalculationStatus_ProgressMilestone_STATUS_SKIPPED: m.riskMilestoneTasks[i].status = taskStatusSkipped } } m.risks = msg.risks - m.processingModel.state = "Change updated" + + if len(m.riskMilestones) > 0 { + m.riskTask.status = taskStatusRunning + cmds = append(cmds, m.riskTask.spinner.Tick) + } else if len(m.risks) > 0 { + m.riskTask.status = taskStatusDone + } else { + var allSkipped = true + for _, ms := range m.riskMilestoneTasks { + if ms.status != taskStatusSkipped { + allSkipped = false + break + } + } + if allSkipped { + m.riskTask.status = taskStatusSkipped + } + } + + m.blastRadiusModel.status = taskStatusDone + m.blastRadiusModel.state = "Change updated" cmds = append(cmds, m.waitForProcessingActivity) case startSnapshotMsg: - mdl, cmd := m.processingModel.Update(msg) - m.processingModel = mdl.(snapshotModel) + var cmd tea.Cmd + m.blastRadiusModel, cmd = m.blastRadiusModel.Update(msg) cmds = append(cmds, m.waitForProcessingActivity, cmd) case progressSnapshotMsg: - mdl, cmd := m.processingModel.Update(msg) - m.processingModel = mdl.(snapshotModel) + var cmd tea.Cmd + m.blastRadiusModel, cmd = m.blastRadiusModel.Update(msg) cmds = append(cmds, m.waitForProcessingActivity, cmd) case finishSnapshotMsg: - mdl, cmd := m.processingModel.Update(msg) - m.processingModel = mdl.(snapshotModel) + var cmd tea.Cmd + m.blastRadiusModel, cmd = m.blastRadiusModel.Update(msg) cmds = append(cmds, tea.Sequence(cmd, func() tea.Msg { return delayQuitMsg{} })) case delayQuitMsg: cmds = append(cmds, tea.Quit) @@ -424,7 +444,10 @@ func (m tfPlanModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.planTask, cmd = m.planTask.Update(msg) cmds = append(cmds, cmd) - m.processingTask, cmd = m.processingTask.Update(msg) + m.blastRadiusModel, cmd = m.blastRadiusModel.Update(msg) + cmds = append(cmds, cmd) + + m.riskTask, cmd = m.riskTask.Update(msg) cmds = append(cmds, cmd) for i, ms := range m.riskMilestoneTasks { @@ -447,16 +470,15 @@ func (m tfPlanModel) View() string { bits = append(bits, markdownToString(m.planHeader)) } - if m.processingTask.status != taskStatusPending { - bits = append(bits, m.processingTask.View()) + if m.blastRadiusModel.status != taskStatusPending { + bits = append(bits, m.blastRadiusModel.View()) } - if m.tfPlanFinished { - bits = append(bits, m.processingHeader) - bits = append(bits, fmt.Sprintf(" %v", m.processingModel.View())) + if m.riskTask.status != taskStatusPending { + bits = append(bits, m.riskTask.View()) } - if m.changeUrl != "" && len(m.risks) == 0 { + if m.changeUrl != "" { for _, t := range m.riskMilestoneTasks { bits = append(bits, fmt.Sprintf(" %v", t.View())) } @@ -517,7 +539,7 @@ func (m tfPlanModel) processPlanCmd() tea.Msg { time.Sleep(time.Second) m.processing <- progressSnapshotMsg{newState: "fake processing"} time.Sleep(time.Second) - m.processing <- changeUpdatedMsg{url: "https://example.com"} + m.processing <- changeUpdatedMsg{url: "https://example.com/changes/abc"} time.Sleep(time.Second) m.processing <- processingActivityMsg{"Fake CalculateBlastRadiusResponse Status update: progress"}