From 570b0f06edb03127cf34f8815b53300e75f7ba7b Mon Sep 17 00:00:00 2001 From: Kelly Hofmann Date: Sat, 16 Mar 2024 12:00:07 -0700 Subject: [PATCH 1/9] add new text input mmodel --- internal/setup/text_input.go | 69 ++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 internal/setup/text_input.go diff --git a/internal/setup/text_input.go b/internal/setup/text_input.go new file mode 100644 index 00000000..32514885 --- /dev/null +++ b/internal/setup/text_input.go @@ -0,0 +1,69 @@ +package setup + +import ( + "fmt" + + "github.com/charmbracelet/bubbles/textinput" + tea "github.com/charmbracelet/bubbletea" +) + +type ( + errMsg error +) + +type inputModel struct { + textInput textinput.Model + done bool + title string + err error +} + +func newTextInputModel(placeholder, title string) inputModel { + ti := textinput.New() + ti.Placeholder = placeholder + ti.Focus() + ti.CharLimit = 156 + ti.Width = 20 + + return inputModel{ + title: title, + textInput: ti, + err: nil, + } +} + +func (m inputModel) Init() tea.Cmd { + return textinput.Blink +} + +func (m inputModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmd tea.Cmd + + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.Type { + case tea.KeyEnter: + m.done = true + return m, nil + case tea.KeyCtrlC, tea.KeyEsc: + return m, tea.Quit + } + + // We handle errors just like any other message + case errMsg: + m.err = msg + return m, nil + } + + m.textInput, cmd = m.textInput.Update(msg) + return m, cmd +} + +func (m inputModel) View() string { + return fmt.Sprintf( + "%s\n\n%s\n\n%s", + m.title, + m.textInput.View(), + "(esc to quit)", + ) + "\n" +} From 5bba02aebdbfec8c5e24cb2156281461da318521 Mon Sep 17 00:00:00 2001 From: Kelly Hofmann Date: Sat, 16 Mar 2024 12:01:32 -0700 Subject: [PATCH 2/9] add new input step to project model, update wizard step to handle that update --- internal/setup/projects.go | 47 ++++++++++++++++++++++++++++++++++---- internal/setup/wizard.go | 7 +++++- 2 files changed, 48 insertions(+), 6 deletions(-) diff --git a/internal/setup/projects.go b/internal/setup/projects.go index 41ba8ab7..8d8b8a92 100644 --- a/internal/setup/projects.go +++ b/internal/setup/projects.go @@ -26,9 +26,11 @@ type project struct { func (p project) FilterValue() string { return "" } type projectModel struct { - choice string - err error - list list.Model + choice string + err error + list list.Model + showInput bool + textInput tea.Model } func NewProject() tea.Model { @@ -48,6 +50,26 @@ func (p projectModel) Init() tea.Cmd { func (m projectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd + if m.showInput { + m.textInput, cmd = m.textInput.Update(msg) + + // catch the enter key here to update the model when a final value is provided + switch msg := msg.(type) { + case tea.KeyMsg: + switch { + case key.Matches(msg, keys.Enter): + iModel, ok := m.textInput.(inputModel) + if ok { + m.choice = iModel.textInput.Value() + m.showInput = false + } + // TODO: send request to create project + } + default: + + } + return m, cmd + } switch msg := msg.(type) { case fetchProjects: projects, err := getProjects() @@ -59,9 +81,16 @@ func (m projectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case tea.KeyMsg: switch { case key.Matches(msg, keys.Enter): + i, ok := m.list.SelectedItem().(project) if ok { - m.choice = i.Key + if i.Key == "create-new-project" { + iModel := newTextInputModel("desired-proj-key", "Enter project name") + m.textInput = iModel + m.showInput = true + } else { + m.choice = i.Key + } } case key.Matches(msg, keys.Quit): return m, tea.Quit @@ -74,7 +103,11 @@ func (m projectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } func (m projectModel) View() string { - return "\n" + m.list.View() + if m.showInput { + return m.textInput.View() + } + + return "\n" + fmt.Sprintf("showInput: %t", m.showInput) + m.list.View() } // projectDelegate is used for display the list and its elements. @@ -130,6 +163,10 @@ func getProjects() ([]project, error) { Key: "proj3", Name: "project 3", }, + { + Key: "create-new-project", + Name: "Create a new project", + }, }, nil // uncomment out below to fetch projects locally after adding an access token to the diff --git a/internal/setup/wizard.go b/internal/setup/wizard.go index ad289207..154ba303 100644 --- a/internal/setup/wizard.go +++ b/internal/setup/wizard.go @@ -103,7 +103,12 @@ func (m WizardModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { p, ok := projModel.(projectModel) if ok { m.currProjectKey = p.choice - m.currStep += 1 + // update projModel with new input model + m.steps[projectsStep] = p + // only progress if we don't want to show input + if !p.showInput { + m.currStep += 1 + } } case environmentsStep: envModel, _ := m.steps[environmentsStep].Update(msg) From 0e346504235762916326290520d2c739663599e4 Mon Sep 17 00:00:00 2001 From: Kelly Hofmann Date: Sat, 16 Mar 2024 12:05:45 -0700 Subject: [PATCH 3/9] add note about error handling; --- internal/setup/text_input.go | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/internal/setup/text_input.go b/internal/setup/text_input.go index 32514885..a6c90073 100644 --- a/internal/setup/text_input.go +++ b/internal/setup/text_input.go @@ -7,10 +7,6 @@ import ( tea "github.com/charmbracelet/bubbletea" ) -type ( - errMsg error -) - type inputModel struct { textInput textinput.Model done bool @@ -49,10 +45,7 @@ func (m inputModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, tea.Quit } - // We handle errors just like any other message - case errMsg: - m.err = msg - return m, nil + // TODO: Handle errors } m.textInput, cmd = m.textInput.Update(msg) From 3aaf37e2c6d7d2ae51806f08959aa1342e1e0cde Mon Sep 17 00:00:00 2001 From: Kelly Hofmann Date: Sat, 16 Mar 2024 12:16:51 -0700 Subject: [PATCH 4/9] remove debugging stmt --- internal/setup/projects.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/setup/projects.go b/internal/setup/projects.go index 8d8b8a92..9d563210 100644 --- a/internal/setup/projects.go +++ b/internal/setup/projects.go @@ -107,7 +107,7 @@ func (m projectModel) View() string { return m.textInput.View() } - return "\n" + fmt.Sprintf("showInput: %t", m.showInput) + m.list.View() + return "\n" + m.list.View() } // projectDelegate is used for display the list and its elements. From 7b87371d7def68cbf75b41a2b1d00f144421e4be Mon Sep 17 00:00:00 2001 From: Kelly Hofmann Date: Sat, 16 Mar 2024 13:50:59 -0700 Subject: [PATCH 5/9] store projects in memory so we can mimic adding new ones --- internal/setup/projects.go | 41 ++++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/internal/setup/projects.go b/internal/setup/projects.go index 9d563210..c5aac64b 100644 --- a/internal/setup/projects.go +++ b/internal/setup/projects.go @@ -63,7 +63,9 @@ func (m projectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.choice = iModel.textInput.Value() m.showInput = false } - // TODO: send request to create project + + // TODO: send request to create project, hardcoding for now + projects = append(projects, project{Key: m.choice, Name: m.choice}) } default: @@ -149,25 +151,26 @@ type fetchProjects struct{} // Items []project `json:"items"` // } +var projects = []project{ + { + Key: "proj1", + Name: "project 1", + }, + { + Key: "proj2", + Name: "project 2", + }, + { + Key: "proj3", + Name: "project 3", + }, +} + func getProjects() ([]project, error) { - return []project{ - { - Key: "proj1", - Name: "project 1", - }, - { - Key: "proj2", - Name: "project 2", - }, - { - Key: "proj3", - Name: "project 3", - }, - { - Key: "create-new-project", - Name: "Create a new project", - }, - }, nil + projectList := projects + createNewOption := project{Key: "create-new-project", Name: "Create a new project"} + projectList = append(projectList, createNewOption) + return projectList, nil // uncomment out below to fetch projects locally after adding an access token to the // Authorization header From 3637ba5aa5115f4e8776126b296a38512abd03a4 Mon Sep 17 00:00:00 2001 From: Kelly Hofmann Date: Sat, 16 Mar 2024 13:51:29 -0700 Subject: [PATCH 6/9] refetch projects if we go back to that step from environment step --- internal/setup/wizard.go | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/internal/setup/wizard.go b/internal/setup/wizard.go index 154ba303..6058c28a 100644 --- a/internal/setup/wizard.go +++ b/internal/setup/wizard.go @@ -83,8 +83,6 @@ func (m WizardModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.currStep = flagsStep + 1 } else { projModel, _ := m.steps[projectsStep].Update(fetchProjects{}) - // we need to cast this to get the data out of it, but maybe we can create our own interface with - // common values such as Choice() and Err() so we don't have to cast p, ok := projModel.(projectModel) if ok { if p.err != nil { @@ -100,6 +98,8 @@ func (m WizardModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } case projectsStep: projModel, _ := m.steps[projectsStep].Update(msg) + // we need to cast this to get the data out of it, but maybe we can create our own interface with + // common values such as Choice() and Err() so we don't have to cast p, ok := projModel.(projectModel) if ok { m.currProjectKey = p.choice @@ -142,6 +142,20 @@ func (m WizardModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } // only go back if not on the first step if m.currStep > autoCreateStep { + // we'll want to get any newly created resources if we decide to go back a step + // we should be able to make the more generic but right now only checking going from env > proj + if m.currStep == environmentsStep { + projModel, _ := m.steps[projectsStep].Update(fetchProjects{}) + p, ok := projModel.(projectModel) + if ok { + if p.err != nil { + m.err = p.err + return m, nil + } + } + // update projModel with the fetched projects + m.steps[projectsStep] = projModel + } m.currStep -= 1 } case key.Matches(msg, keys.Quit): From eb68bffe03a776acaeccb218c54b0a5103f9899b Mon Sep 17 00:00:00 2001 From: Kelly Hofmann Date: Sat, 16 Mar 2024 14:07:03 -0700 Subject: [PATCH 7/9] make fetching resources more generic --- internal/setup/projects.go | 4 +--- internal/setup/wizard.go | 32 +++++++------------------------- 2 files changed, 8 insertions(+), 28 deletions(-) diff --git a/internal/setup/projects.go b/internal/setup/projects.go index c5aac64b..285133f3 100644 --- a/internal/setup/projects.go +++ b/internal/setup/projects.go @@ -73,7 +73,7 @@ func (m projectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, cmd } switch msg := msg.(type) { - case fetchProjects: + case fetchResources: projects, err := getProjects() if err != nil { m.err = err @@ -145,8 +145,6 @@ func projectsToItems(projects []project) []list.Item { return items } -type fetchProjects struct{} - // type projectsResponse struct { // Items []project `json:"items"` // } diff --git a/internal/setup/wizard.go b/internal/setup/wizard.go index 6058c28a..405b6459 100644 --- a/internal/setup/wizard.go +++ b/internal/setup/wizard.go @@ -13,6 +13,9 @@ import ( // TODO: we may want to rename this for clarity type sessionState int +// generic message type to pass into each models' Update method when we want to perform a new GET request +type fetchResources struct{} + // list of steps in the wizard const ( autoCreateStep sessionState = iota @@ -82,17 +85,8 @@ func (m WizardModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.currFlagKey = "setup-wizard-flag" m.currStep = flagsStep + 1 } else { - projModel, _ := m.steps[projectsStep].Update(fetchProjects{}) - p, ok := projModel.(projectModel) - if ok { - if p.err != nil { - m.err = p.err - return m, nil - } - } - // update projModel with the fetched projects - m.steps[projectsStep] = projModel - // go to the next step + // pre-load projects + m.steps[projectsStep], _ = m.steps[projectsStep].Update(fetchResources{}) m.currStep += 1 } } @@ -142,20 +136,8 @@ func (m WizardModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } // only go back if not on the first step if m.currStep > autoCreateStep { - // we'll want to get any newly created resources if we decide to go back a step - // we should be able to make the more generic but right now only checking going from env > proj - if m.currStep == environmentsStep { - projModel, _ := m.steps[projectsStep].Update(fetchProjects{}) - p, ok := projModel.(projectModel) - if ok { - if p.err != nil { - m.err = p.err - return m, nil - } - } - // update projModel with the fetched projects - m.steps[projectsStep] = projModel - } + // fetch resources for the previous step again in case we created new ones + m.steps[m.currStep-1], _ = m.steps[m.currStep-1].Update(fetchResources{}) m.currStep -= 1 } case key.Matches(msg, keys.Quit): From d63ed532c8f9dae65604f59f062ae72ce4d73788 Mon Sep 17 00:00:00 2001 From: Kelly Hofmann Date: Sat, 16 Mar 2024 15:19:37 -0700 Subject: [PATCH 8/9] fetch environments based on project selected --- internal/setup/environments.go | 52 ++++++++++++++++++++++++++++------ internal/setup/projects.go | 6 ++-- internal/setup/wizard.go | 9 +++++- 3 files changed, 56 insertions(+), 11 deletions(-) diff --git a/internal/setup/environments.go b/internal/setup/environments.go index 6cb977af..02b24f2c 100644 --- a/internal/setup/environments.go +++ b/internal/setup/environments.go @@ -26,13 +26,14 @@ type environment struct { func (p environment) FilterValue() string { return "" } type environmentModel struct { - choice string - err error - list list.Model + choice string + err error + list list.Model + parentKey string } -func NewEnvironment() tea.Model { - environments := []environment{ +var environments = map[string][]environment{ + "proj1": { { Key: "env1", Name: "environment 1", @@ -41,9 +42,38 @@ func NewEnvironment() tea.Model { Key: "env2", Name: "environment 2", }, - } + }, + "proj2": { + { + Key: "env3", + Name: "environment 3", + }, + { + Key: "env4", + Name: "environment 4", + }, + }, + "proj3": { + { + Key: "env5", + Name: "environment 5", + }, + { + Key: "env6", + Name: "environment 6", + }, + }, +} - l := list.New(environmentsToItems(environments), envDelegate{}, 30, 14) +func getEnvironments(projKey string) ([]environment, error) { + envList := environments[projKey] + createNewOption := environment{Key: CreateNewResourceKey, Name: "Create a new environment"} + envList = append(envList, createNewOption) + return envList, nil +} + +func NewEnvironment() tea.Model { + l := list.New(nil, envDelegate{}, 30, 14) l.Title = "Select an environment" l.SetShowStatusBar(false) l.SetFilteringEnabled(false) @@ -57,10 +87,16 @@ func (p environmentModel) Init() tea.Cmd { return nil } -// This method has drifted from the ProjectModel's version, but it should do something similar. func (m environmentModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd switch msg := msg.(type) { + case fetchResources: + envs, err := getEnvironments(m.parentKey) + if err != nil { + m.err = err + return m, nil + } + m.list.SetItems(environmentsToItems(envs)) case tea.KeyMsg: switch { case key.Matches(msg, keys.Enter): diff --git a/internal/setup/projects.go b/internal/setup/projects.go index 285133f3..88198bf1 100644 --- a/internal/setup/projects.go +++ b/internal/setup/projects.go @@ -11,6 +11,8 @@ import ( "github.com/charmbracelet/lipgloss" ) +const CreateNewResourceKey string = "create-new" + var ( projectStyle = lipgloss.NewStyle().PaddingLeft(4) selectedItemStyle = lipgloss.NewStyle().PaddingLeft(2).Foreground(lipgloss.Color("170")) @@ -86,7 +88,7 @@ func (m projectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { i, ok := m.list.SelectedItem().(project) if ok { - if i.Key == "create-new-project" { + if i.Key == CreateNewResourceKey { iModel := newTextInputModel("desired-proj-key", "Enter project name") m.textInput = iModel m.showInput = true @@ -166,7 +168,7 @@ var projects = []project{ func getProjects() ([]project, error) { projectList := projects - createNewOption := project{Key: "create-new-project", Name: "Create a new project"} + createNewOption := project{Key: CreateNewResourceKey, Name: "Create a new project"} projectList = append(projectList, createNewOption) return projectList, nil diff --git a/internal/setup/wizard.go b/internal/setup/wizard.go index 405b6459..24038506 100644 --- a/internal/setup/wizard.go +++ b/internal/setup/wizard.go @@ -101,7 +101,14 @@ func (m WizardModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.steps[projectsStep] = p // only progress if we don't want to show input if !p.showInput { - m.currStep += 1 + // pre-load environments based on project selected + envModel := m.steps[environmentsStep] + e, ok := envModel.(environmentModel) + if ok { + e.parentKey = m.currProjectKey + m.steps[environmentsStep], _ = e.Update(fetchResources{}) + m.currStep += 1 + } } } case environmentsStep: From bc8403434e34e9086ab6dc4e8defe1c34a928793 Mon Sep 17 00:00:00 2001 From: Kelly Hofmann Date: Sat, 16 Mar 2024 15:29:52 -0700 Subject: [PATCH 9/9] fetch flags based on environment selected --- internal/setup/flags.go | 81 ++++++++++++++++++++++++++++++++++++---- internal/setup/wizard.go | 9 ++++- 2 files changed, 82 insertions(+), 8 deletions(-) diff --git a/internal/setup/flags.go b/internal/setup/flags.go index 2f90ad05..c5d2f175 100644 --- a/internal/setup/flags.go +++ b/internal/setup/flags.go @@ -26,13 +26,14 @@ type flag struct { func (p flag) FilterValue() string { return "" } type flagModel struct { - choice string - err error - list list.Model + choice string + err error + list list.Model + parentKey string } -func NewFlag() tea.Model { - flags := []flag{ +var flags = map[string][]flag{ + "env1": { { Key: "flag1", Name: "flag 1", @@ -41,9 +42,61 @@ func NewFlag() tea.Model { Key: "flag2", Name: "flag 2", }, - } + }, + "env2": { + { + Key: "flag3", + Name: "flag 3", + }, + { + Key: "flag4", + Name: "flag 4", + }, + }, + "env3": { + { + Key: "flag5", + Name: "flag 5", + }, + { + Key: "flag6", + Name: "flag 6", + }, + }, + "env4": { + { + Key: "flag7", + Name: "flag 7", + }, + { + Key: "flag8", + Name: "flag 8", + }, + }, + "env5": { + { + Key: "flag9", + Name: "flag 9", + }, + { + Key: "flag10", + Name: "flag 10", + }, + }, + "env6": { + { + Key: "flag11", + Name: "flag 11", + }, + { + Key: "flag12", + Name: "flag 12", + }, + }, +} - l := list.New(flagsToItems(flags), flagDelegate{}, 30, 14) +func NewFlag() tea.Model { + l := list.New(nil, flagDelegate{}, 30, 14) l.Title = "Select a flag" l.SetShowStatusBar(false) l.SetFilteringEnabled(false) @@ -61,6 +114,13 @@ func (p flagModel) Init() tea.Cmd { func (m flagModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd switch msg := msg.(type) { + case fetchResources: + fs, err := getFlags(m.parentKey) + if err != nil { + m.err = err + return m, nil + } + m.list.SetItems(flagsToItems(fs)) case tea.KeyMsg: switch { case key.Matches(msg, keys.Enter): @@ -82,6 +142,13 @@ func (m flagModel) View() string { return "\n" + m.list.View() } +func getFlags(envKey string) ([]flag, error) { + flagList := flags[envKey] + createNewOption := flag{Key: CreateNewResourceKey, Name: "Create a new flag"} + flagList = append(flagList, createNewOption) + return flagList, nil +} + type flagDelegate struct{} func (d flagDelegate) Height() int { return 1 } diff --git a/internal/setup/wizard.go b/internal/setup/wizard.go index 24038506..15238487 100644 --- a/internal/setup/wizard.go +++ b/internal/setup/wizard.go @@ -116,7 +116,14 @@ func (m WizardModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { p, ok := envModel.(environmentModel) if ok { m.currEnvironmentKey = p.choice - m.currStep += 1 + // pre-load flags based on environment selected + fModel := m.steps[flagsStep] + f, ok := fModel.(flagModel) + if ok { + f.parentKey = m.currEnvironmentKey + m.steps[flagsStep], _ = f.Update(fetchResources{}) + m.currStep += 1 + } } case flagsStep: model, _ := m.steps[flagsStep].Update(msg)