internal/ui/importlist/importlist.go (217 lines of code) (raw):

package importlist import ( "context" "fmt" "regexp" "sort" "strings" "time" "github.com/Azure/aztfexport/pkg/meta" "github.com/Azure/aztfexport/internal/tfaddr" "github.com/Azure/aztfexport/internal/ui/aztfexportclient" "github.com/Azure/aztfexport/internal/ui/common" "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" "github.com/magodo/textinput" "github.com/magodo/tfadd/providers/azapi" "github.com/magodo/tfadd/providers/azurerm" "github.com/magodo/tfadd/schema" ) type Model struct { ctx context.Context c meta.Meta listkeys listKeyMap list list.Model } func NewModel(ctx context.Context, c meta.Meta, l meta.ImportList, idx int) Model { // Build candidate words for the textinput var resourceSchemas map[string]*schema.Schema switch c.ProviderName() { case "azapi": resourceSchemas = azapi.ProviderSchemaInfo.ResourceSchemas case "azurerm": resourceSchemas = azurerm.ProviderSchemaInfo.ResourceSchemas } candidates := make([]string, 0, len(resourceSchemas)) for rt := range resourceSchemas { candidates = append(candidates, rt) } sort.Strings(candidates) // Build list items var items []list.Item for idx, item := range l { ti := textinput.NewModel() ti.SetCursorMode(textinput.CursorStatic) if !item.Skip() { ti.SetValue(item.TFAddr.String()) } ti.CandidateWords = candidates items = append(items, Item{ idx: idx, v: item, textinput: ti, }) } lst := list.NewModel(items, NewImportItemDelegate(c.ProviderName()), 0, 0) lst.Title = " " + c.ScopeName() + " " lst.Styles.Title = common.SubtitleStyle lst.StatusMessageLifetime = 3 * time.Second lst.Select(idx) lst.Filter = func(term string, targets []string) []list.Rank { p, err := regexp.Compile(term) if err != nil { return nil } result := []list.Rank{} for idx, tgt := range targets { m := p.FindStringIndex(tgt) if m == nil { continue } rnk := list.Rank{ Index: idx, } for i := m[0]; i < m[1]; i++ { rnk.MatchedIndexes = append(rnk.MatchedIndexes, i) } result = append(result, rnk) } return result } bindKeyHelps(&lst, newListKeyMap().ToBindings()) // Reset the quit to deallocate the "ESC" as a quit key. lst.KeyMap.Quit = key.NewBinding( key.WithKeys("q"), key.WithHelp("q", "quit"), ) return Model{ ctx: ctx, c: c, listkeys: newListKeyMap(), list: lst, } } func (m Model) Init() tea.Cmd { return nil } func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { var cmd tea.Cmd switch msg := msg.(type) { case tea.KeyMsg: // Don't intercept the keys (e.g. "w") when user is inputting. if m.isUserTyping() { break } switch { case key.Matches(msg, m.listkeys.apply): // Leave filter applied state before applying the import list if m.list.FilterState() == list.FilterApplied { m.list.ResetFilter() } // In case all items are marked as skip, show a warning and do nothing. if m.isNothingToImport() { return m, m.list.NewStatusMessage(common.ErrorMsgStyle.Render("All resources are skipped, nothing to import")) } // Ensure all items pass validation if !m.userInputsAreValid() { return m, m.list.NewStatusMessage(common.ErrorMsgStyle.Render("One or more user input is invalid")) } return m, aztfexportclient.StartImport(m.importList(true)) case key.Matches(msg, m.listkeys.skip): sel := m.list.SelectedItem() if sel == nil { return m, nil } selItem := sel.(Item) if !selItem.v.Skip() { selItem.v.TFAddr = tfaddr.TFAddr{} selItem.textinput.Model.SetValue("") } else { selItem.v.TFAddr = selItem.v.TFAddrCache selItem.textinput.Model.SetValue(selItem.v.TFAddr.String()) } m.list.SetItem(selItem.idx, selItem) return m, nil case key.Matches(msg, m.listkeys.error): sel := m.list.SelectedItem() if sel == nil { return m, nil } selItem := sel.(Item) if selItem.v.ImportError == nil { return m, nil } return m, aztfexportclient.ShowImportError(selItem.v, selItem.idx, m.importList(false)) case key.Matches(msg, m.listkeys.recommendation): sel := m.list.SelectedItem() if sel == nil { return m, nil } selItem := sel.(Item) if len(selItem.v.Recommendations) == 0 { return m, m.list.NewStatusMessage(common.InfoStyle.Render("No resource type recommendation is available...")) } return m, m.list.NewStatusMessage(common.InfoStyle.Render(fmt.Sprintf("Possible resource type(s): %s", strings.Join(selItem.v.Recommendations, ",")))) case key.Matches(msg, m.listkeys.save): m.list.NewStatusMessage(common.InfoStyle.Render("Saving the resouce mapping...")) err := m.c.ExportResourceMapping(m.ctx, m.importList(false)) if err == nil { m.list.NewStatusMessage(common.InfoStyle.Render("Resource mapping saved")) } else { m.list.NewStatusMessage(common.ErrorMsgStyle.Render(err.Error())) } case key.Matches(msg, m.list.KeyMap.Quit): return m, aztfexportclient.Quit(m.ctx, m.c) } case tea.WindowSizeMsg: // The height here minus the height occupied by the title m.list.SetSize(msg.Width, msg.Height-3) } m.list, cmd = m.list.Update(msg) return m, cmd } func (m Model) View() string { return m.list.View() } func bindKeyHelps(l *list.Model, bindings []key.Binding) { l.AdditionalFullHelpKeys = func() []key.Binding { return bindings } l.AdditionalShortHelpKeys = func() []key.Binding { return bindings } } func (m Model) isUserTyping() bool { // In filtering mode if m.list.FilterState() == list.Filtering { return true } // Any textinput is in focused mode for _, item := range m.list.Items() { item := item.(Item) if item.textinput.Focused() { return true } } return false } func (m Model) isNothingToImport() bool { for _, item := range m.list.Items() { item := item.(Item) if !item.v.Skip() { return false } } return true } func (m Model) userInputsAreValid() bool { for _, item := range m.list.Items() { item := item.(Item) if item.v.ValidateError != nil { return false } } return true } func (m Model) importList(clearErr bool) meta.ImportList { out := make(meta.ImportList, 0, len(m.list.Items())) for _, item := range m.list.Items() { item := item.(Item) if clearErr { item.v.ImportError = nil } out = append(out, item.v) } return out }