tui/queue.go (251 lines of code) (raw):
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package tui
import (
"github.com/GoogleCloudPlatform/deploystack/config"
tea "github.com/charmbracelet/bubbletea"
)
// QueueModel is an extented version of tea.Model modified to work with the
// queue system
type QueueModel interface {
tea.Model
addQueue(*Queue)
getKey() string
setValue(string)
addContent(...string)
clearContent()
clear()
}
// Queue represents the flow of the application from screen to screen, or
// a the developer level tea.Model to tea.Model. It allows for progression
// and even going back through the queue all to manage the population of
// a deploystack setting and tfvars file
type Queue struct {
models []QueueModel
current int
header component
stack *config.Stack
store map[string]interface{}
index []string
client UIClient
}
// NewQueue creates a new queue. You should need only one per app
func NewQueue(s *config.Stack, client UIClient) Queue {
q := Queue{stack: s, store: map[string]interface{}{}}
q.client = client
q.index = []string{}
currentProject, _ := client.ProjectIDGet()
q.Save("currentProject", currentProject)
return q
}
// Model retrieves a given model by key from the queue
func (q *Queue) Model(key string) QueueModel {
for _, v := range q.models {
if v.getKey() == key {
return v
}
}
return nil
}
// Save stores a value in a simple cache for communicating between operations
// in the same process
func (q *Queue) Save(key string, val interface{}) {
q.store[key] = val
}
// Get returns a previously stored value from the Queue cache
func (q *Queue) Get(key string) interface{} {
val, ok := q.store[key]
if !ok {
return nil
}
return val
}
func (q *Queue) removeModel(key string) {
for i, v := range q.index {
if v == key {
q.models = append(q.models[:i], q.models[i+1:]...)
q.index = append(q.index[:i], q.index[i+1:]...)
}
}
}
func (q *Queue) goToModel(key string) (tea.Model, tea.Cmd) {
if key == "quit" {
return q.models[q.current], tea.Quit
}
for i, v := range q.models {
if v.getKey() == key {
q.current = i
r := q.models[q.current]
return r, r.Init()
}
}
r := q.models[q.current]
return r, nil
}
func (q *Queue) clear(key string) {
for i, v := range q.models {
if v.getKey() == key {
q.current = i
r := q.models[q.current]
q.stack.DeleteSetting(key)
r.clear()
}
}
}
func (q *Queue) next() (tea.Model, tea.Cmd) {
q.current++
if q.current >= len(q.models) {
return q.models[len(q.models)-1], tea.Quit
}
r := q.models[q.current]
return r, r.Init()
}
func (q *Queue) prev() (tea.Model, tea.Cmd) {
q.current--
if q.current <= 0 {
return q.models[0], nil
}
r := q.models[q.current]
r.setValue("")
return r, r.Init()
}
func (q *Queue) currentKey() string {
if len(q.models) == 0 {
return ""
}
r := q.models[q.current].getKey()
return r
}
// InitializeUI spins up everything we need to have a working queue in the
// hosting application
func (q *Queue) InitializeUI() {
desc := newDescription(q.stack)
appHeader := newHeader(appTitle, q.stack.Config.Title)
firstPage := newPage("firstpage", []component{newTextBlock(explainText)})
descPage := newPage("descpage", []component{desc})
firstPage.showProgress = false
descPage.showProgress = false
endpage := newPage("endpage", []component{
newTextBlock(titleStyle.Render("Project Settings")),
newSettingsTable(q.stack),
})
endpage.addPreProcessor(cleanUp(q))
q.header = appHeader
q.add(&firstPage)
q.add(&descPage)
q.ProcessConfig()
q.add(&endpage)
}
func (q *Queue) getSettings() string {
r := newSettingsTable(q.stack)
return r.render()
}
func (q *Queue) exitPage() (tea.Model, tea.Cmd) {
page := newPage("exit", []component{
newTextBlock("You've chosen to stop moving forward through DeployStack. \n"),
newTextBlock("If this was an error, you can try again by typing 'deploystack install' at the command prompt \n"),
})
page.showProgress = false
q.add(&page)
q.Save("halted", true)
quit := func(string, *Queue) tea.Cmd {
return tea.Quit
}
page.addPostProcessor(quit)
return page, nil
}
func (q *Queue) countTotalSteps() int {
total := len(q.models)
for _, v := range q.models {
if v.getKey() == "firstpage" {
total--
}
if v.getKey() == "descpage" {
total--
}
if v.getKey() == "endpage" {
total--
}
}
return total
}
func (q *Queue) calcPercent() int {
if q.current == 2 {
return 0
}
if q.current == len(q.models)-1 {
return 100
}
total := q.countTotalSteps()
current := q.current + 1 - 2
percentage := int((float32(current) / float32(total)) * 100)
if percentage >= 100 && q.current != len(q.models)-1 {
return 90
}
return percentage
}
// ProcessConfig does the work of turning a DeployStack config file to a set
// of tui screens. It's separate from Initialize in case we want to be able
// to populate setting and variables with other information before running
// the genreation of those screens
func (q *Queue) ProcessConfig() error {
var project, name string
var err error
s := q.stack
sets := s.Config.GetAuthorSettings()
for _, v := range sets {
s.AddSettingComplete(v)
}
project = s.GetSetting("project_id")
region := s.GetSetting("region")
zone := s.GetSetting("zone")
name = s.Config.Name
if name == "" {
err = s.Config.ComputeName(s.Config.Getwd())
if err != nil {
return err
}
}
s.AddSetting("stack_name", s.Config.Name)
if s.Config.Project && len(project) == 0 {
p := config.Project{
Name: "project_id",
UserPrompt: "Choose a project to use for this application.",
}
s.Config.Projects.Items = append(s.Config.Projects.Items, p)
}
if len(s.Config.Projects.Items) > 0 {
currentProject := q.Get("currentProject").(string)
for _, v := range s.Config.Projects.Items {
s := newProjectSelector(v.Name, v.UserPrompt, currentProject, getProjects(q))
c := newProjectCreator(v.Name + projNewSuffix)
b := newBillingSelector(v.Name+billNewSuffix, getBillingAccounts(q), attachBilling)
q.add(&s, &c, &b)
}
}
if s.Config.BillingAccount {
b := newBillingSelector("billing_account", getBillingAccounts(q), nil)
b.list.Title = "Choose a billing account to use for with this application"
q.add(&b)
}
if s.Config.ConfigureGCEInstance {
newGCEInstance(q)
}
region = s.GetSetting("region")
if s.Config.Region && len(region) == 0 {
newRegion(q)
}
zone = s.GetSetting("zone")
if s.Config.Zone && len(zone) == 0 {
newZone(q)
}
if s.Config.Domain {
newDomain(q)
}
newCustomPages(q)
return err
}
func (q *Queue) add(m ...QueueModel) {
uniques := map[string]bool{}
for _, v := range q.models {
uniques[v.getKey()] = true
}
for _, v := range m {
// Basically if something dumb happens we don't rewrite queue
// And since this is a author issue, not a user one, we should
// fail silently
if _, ok := uniques[v.getKey()]; ok {
continue
}
v.addQueue(q)
q.models = append(q.models, v)
q.index = append(q.index, v.getKey())
}
}
// method only used for testing
func (q *Queue) insert(m ...QueueModel) {
tmp := q.models[:len(q.models)-1]
tmp = append(tmp, m...)
tmp = append(tmp, q.models[len(q.models)-1])
q.models = tmp
}
// Start returns the first model to the hosting application so that it can
// be run through tea.NewProgram
func (q *Queue) Start() QueueModel {
return q.models[0]
}