ssm-browse: added mark and delete in dynamo-browse

This commit is contained in:
Leon Mika 2022-03-30 21:04:30 +11:00
parent b3d0fbfe29
commit c49f3913a8
10 changed files with 151 additions and 201 deletions

View file

@ -44,24 +44,10 @@ func main() {
tableService := tables.NewService(dynamoProvider) tableService := tables.NewService(dynamoProvider)
tableReadController := controllers.NewTableReadController(tableService, *flagTable) tableReadController := controllers.NewTableReadController(tableService, *flagTable)
tableWriteController := controllers.NewTableWriteController(tableService, tableReadController, *flagTable) tableWriteController := controllers.NewTableWriteController(tableService, tableReadController)
_ = tableWriteController
commandController := commandctrl.NewCommandController() commandController := commandctrl.NewCommandController()
commandController.AddCommands(&commandctrl.CommandContext{ model := ui.NewModel(tableReadController, tableWriteController, commandController)
Commands: map[string]commandctrl.Command{
"q": commandctrl.NoArgCommand(tea.Quit),
"table": func(args []string) tea.Cmd {
if len(args) == 0 {
return tableReadController.ListTables()
} else {
return tableReadController.ScanTable(args[0])
}
},
},
})
model := ui.NewModel(tableReadController, commandController)
// Pre-determine if layout has dark background. This prevents calls for creating a list to hang. // Pre-determine if layout has dark background. This prevents calls for creating a list to hang.
lipgloss.HasDarkBackground() lipgloss.HasDarkBackground()

View file

@ -23,3 +23,5 @@ type PromptForTableMsg struct {
Tables []string Tables []string
OnSelected func(tableName string) tea.Cmd OnSelected func(tableName string) tea.Cmd
} }
type ResultSetUpdated struct{}

View file

@ -72,17 +72,26 @@ func (c *TableReadController) ScanTable(name string) tea.Cmd {
func (c *TableReadController) Rescan() tea.Cmd { func (c *TableReadController) Rescan() tea.Cmd {
return func() tea.Msg { return func() tea.Msg {
ctx := context.Background() return c.doScan(context.Background(), c.resultSet)
resultSet, err := c.tableService.Scan(ctx, c.resultSet.TableInfo)
if err != nil {
return events.Error(err)
}
return c.setResultSet(resultSet)
} }
} }
func (c *TableReadController) doScan(ctx context.Context, resultSet *models.ResultSet) tea.Msg {
newResultSet, err := c.tableService.Scan(ctx, resultSet.TableInfo)
if err != nil {
return events.Error(err)
}
return c.setResultSet(newResultSet)
}
func (c *TableReadController) ResultSet() *models.ResultSet {
c.mutex.Lock()
defer c.mutex.Unlock()
return c.resultSet
}
func (c *TableReadController) setResultSet(resultSet *models.ResultSet) tea.Msg { func (c *TableReadController) setResultSet(resultSet *models.ResultSet) tea.Msg {
c.mutex.Lock() c.mutex.Lock()
defer c.mutex.Unlock() defer c.mutex.Unlock()
@ -90,51 +99,3 @@ func (c *TableReadController) setResultSet(resultSet *models.ResultSet) tea.Msg
c.resultSet = resultSet c.resultSet = resultSet
return NewResultSet{resultSet} return NewResultSet{resultSet}
} }
/*
func (c *TableReadController) Scan() uimodels.Operation {
return uimodels.OperationFn(func(ctx context.Context) error {
return c.doScan(ctx, false)
})
}
func (c *TableReadController) doScan(ctx context.Context, quiet bool) (err error) {
uiCtx := uimodels.Ctx(ctx)
if !quiet {
uiCtx.Message("Scanning...")
}
tableInfo, err := c.tableInfo(ctx)
if err != nil {
return err
}
resultSet, err := c.tableService.Scan(ctx, tableInfo)
if err != nil {
return err
}
if !quiet {
uiCtx.Messagef("Found %d items", len(resultSet.Items))
}
uiCtx.Send(NewResultSet{resultSet})
return nil
}
*/
// tableInfo returns the table info from the state if a result set exists. If not, it fetches the
// table information from the service.
// func (c *TableReadController) tableInfo(ctx context.Context) (*models.TableInfo, error) {
// /*
// if existingResultSet := CurrentState(ctx).ResultSet; existingResultSet != nil {
// return existingResultSet.TableInfo, nil
// }
// */
// tableInfo, err := c.tableService.Describe(ctx, c.tableName)
// if err != nil {
// return nil, errors.Wrapf(err, "cannot describe %v", c.tableName)
// }
// return tableInfo, nil
// }

View file

@ -2,137 +2,58 @@ package controllers
import ( import (
"context" "context"
"fmt"
"github.com/lmika/awstools/internal/common/ui/uimodels" tea "github.com/charmbracelet/bubbletea"
"github.com/lmika/awstools/internal/common/ui/events"
"github.com/lmika/awstools/internal/dynamo-browse/services/tables" "github.com/lmika/awstools/internal/dynamo-browse/services/tables"
"github.com/pkg/errors"
) )
type TableWriteController struct { type TableWriteController struct {
tableService *tables.Service tableService *tables.Service
tableReadControllers *TableReadController tableReadControllers *TableReadController
tableName string
} }
func NewTableWriteController(tableService *tables.Service, tableReadControllers *TableReadController, tableName string) *TableWriteController { func NewTableWriteController(tableService *tables.Service, tableReadControllers *TableReadController) *TableWriteController {
return &TableWriteController{ return &TableWriteController{
tableService: tableService, tableService: tableService,
tableReadControllers: tableReadControllers, tableReadControllers: tableReadControllers,
tableName: tableName,
} }
} }
func (c *TableWriteController) ToggleReadWrite() uimodels.Operation { func (twc *TableWriteController) ToggleMark(idx int) tea.Cmd {
return uimodels.OperationFn(func(ctx context.Context) error { return func() tea.Msg {
uiCtx := uimodels.Ctx(ctx) resultSet := twc.tableReadControllers.ResultSet()
state := CurrentState(ctx) resultSet.SetMark(idx, !resultSet.Marked(idx))
if state.InReadWriteMode { return ResultSetUpdated{}
uiCtx.Send(SetReadWrite{NewValue: false}) }
uiCtx.Message("read/write mode disabled") }
} else {
uiCtx.Send(SetReadWrite{NewValue: true}) func (twc *TableWriteController) DeleteMarked() tea.Cmd {
uiCtx.Message("read/write mode enabled") return func() tea.Msg {
resultSet := twc.tableReadControllers.ResultSet()
markedItems := resultSet.MarkedItems()
if len(markedItems) == 0 {
return events.StatusMsg("no marked items")
} }
return nil return events.PromptForInputMsg{
}) Prompt: fmt.Sprintf("delete %d items? ", len(markedItems)),
} OnDone: func(value string) tea.Cmd {
if value != "y" {
func (c *TableWriteController) Duplicate() uimodels.Operation { return events.SetStatus("operation aborted")
return nil
/*
return uimodels.OperationFn(func(ctx context.Context) error {
uiCtx := uimodels.Ctx(ctx)
state := CurrentState(ctx)
if state.SelectedItem == nil {
return errors.New("no selected item")
} else if !state.InReadWriteMode {
return errors.New("not in read/write mode")
}
uiCtx.Input("Dup: ", uimodels.OperationFn(func(ctx context.Context) error {
modExpr, err := modexpr.Parse(uimodels.PromptValue(ctx))
if err != nil {
return err
} }
newItem, err := modExpr.Patch(state.SelectedItem) return func() tea.Msg {
if err != nil { ctx := context.Background()
return err if err := twc.tableService.Delete(ctx, resultSet.TableInfo, markedItems); err != nil {
return events.Error(err)
}
return twc.tableReadControllers.doScan(ctx, resultSet)
} }
},
// TODO: preview new item
uiCtx := uimodels.Ctx(ctx)
uiCtx.Input("Put item? ", uimodels.OperationFn(func(ctx context.Context) error {
if uimodels.PromptValue(ctx) != "y" {
return errors.New("operation aborted")
}
tableInfo, err := c.tableReadControllers.tableInfo(ctx)
if err != nil {
return err
}
// Delete the item
if err := c.tableService.Put(ctx, tableInfo, newItem); err != nil {
return err
}
// Rescan to get updated items
// if err := c.tableReadControllers.doScan(ctx, true); err != nil {
// return err
// }
return nil
}))
return nil
}))
return nil
})
*/
}
func (c *TableWriteController) Delete() uimodels.Operation {
return uimodels.OperationFn(func(ctx context.Context) error {
uiCtx := uimodels.Ctx(ctx)
state := CurrentState(ctx)
if state.SelectedItem == nil {
return errors.New("no selected item")
} else if !state.InReadWriteMode {
return errors.New("not in read/write mode")
} }
}
uiCtx.Input("Delete item? ", uimodels.OperationFn(func(ctx context.Context) error {
uiCtx := uimodels.Ctx(ctx)
if uimodels.PromptValue(ctx) != "y" {
return errors.New("operation aborted")
}
/*
tableInfo, err := c.tableReadControllers.tableInfo(ctx)
if err != nil {
return err
}
// Delete the item
if err := c.tableService.Delete(ctx, tableInfo, state.SelectedItem); err != nil {
return err
}
*/
// Rescan to get updated items
// if err := c.tableReadControllers.doScan(ctx, true); err != nil {
// return err
// }
uiCtx.Message("Item deleted")
return nil
}))
return nil
})
} }

View file

@ -6,6 +6,7 @@ type ResultSet struct {
TableInfo *TableInfo TableInfo *TableInfo
Columns []string Columns []string
Items []Item Items []Item
Marks map[int]bool
} }
type Item map[string]types.AttributeValue type Item map[string]types.AttributeValue
@ -30,3 +31,28 @@ func (i Item) KeyValue(info *TableInfo) map[string]types.AttributeValue {
} }
return itemKey return itemKey
} }
func (rs *ResultSet) SetMark(idx int, marked bool) {
if marked {
if rs.Marks == nil {
rs.Marks = make(map[int]bool)
}
rs.Marks[idx] = true
} else {
delete(rs.Marks, idx)
}
}
func (rs *ResultSet) Marked(idx int) bool {
return rs.Marks[idx]
}
func (rs *ResultSet) MarkedItems() []Item {
items := make([]Item, 0)
for i, marked := range rs.Marks {
if marked {
items = append(items, rs.Items[i])
}
}
return items
}

View file

@ -78,6 +78,11 @@ func (s *Service) Put(ctx context.Context, tableInfo *models.TableInfo, item mod
return s.provider.PutItem(ctx, tableInfo.Name, item) return s.provider.PutItem(ctx, tableInfo.Name, item)
} }
func (s *Service) Delete(ctx context.Context, tableInfo *models.TableInfo, item models.Item) error { func (s *Service) Delete(ctx context.Context, tableInfo *models.TableInfo, items []models.Item) error {
return s.provider.DeleteItem(ctx, tableInfo.Name, item.KeyValue(tableInfo)) for _, item := range items {
if err := s.provider.DeleteItem(ctx, tableInfo.Name, item.KeyValue(tableInfo)); err != nil {
return errors.Wrapf(err, "cannot delete item")
}
}
return nil
} }

View file

@ -12,28 +12,46 @@ import (
) )
type Model struct { type Model struct {
tableReadController *controllers.TableReadController tableReadController *controllers.TableReadController
commandController *commandctrl.CommandController tableWriteController *controllers.TableWriteController
statusAndPrompt *statusandprompt.StatusAndPrompt commandController *commandctrl.CommandController
tableSelect *tableselect.Model statusAndPrompt *statusandprompt.StatusAndPrompt
tableSelect *tableselect.Model
root tea.Model root tea.Model
tableView *dynamotableview.Model
} }
func NewModel(rc *controllers.TableReadController, cc *commandctrl.CommandController) Model { func NewModel(rc *controllers.TableReadController, wc *controllers.TableWriteController, cc *commandctrl.CommandController) Model {
dtv := dynamotableview.New() dtv := dynamotableview.New()
div := dynamoitemview.New() div := dynamoitemview.New()
statusAndPrompt := statusandprompt.New(layout.NewVBox(layout.LastChildFixedAt(17), dtv, div), "") statusAndPrompt := statusandprompt.New(layout.NewVBox(layout.LastChildFixedAt(17), dtv, div), "")
tableSelect := tableselect.New(statusAndPrompt) tableSelect := tableselect.New(statusAndPrompt)
cc.AddCommands(&commandctrl.CommandContext{
Commands: map[string]commandctrl.Command{
"q": commandctrl.NoArgCommand(tea.Quit),
"table": func(args []string) tea.Cmd {
if len(args) == 0 {
return rc.ListTables()
} else {
return rc.ScanTable(args[0])
}
},
"delete": commandctrl.NoArgCommand(wc.DeleteMarked()),
},
})
root := layout.FullScreen(tableSelect) root := layout.FullScreen(tableSelect)
return Model{ return Model{
tableReadController: rc, tableReadController: rc,
commandController: cc, tableWriteController: wc,
statusAndPrompt: statusAndPrompt, commandController: cc,
tableSelect: tableSelect, statusAndPrompt: statusAndPrompt,
root: root, tableSelect: tableSelect,
root: root,
tableView: dtv,
} }
} }
@ -43,9 +61,13 @@ func (m Model) Init() tea.Cmd {
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) { switch msg := msg.(type) {
case controllers.ResultSetUpdated:
m.tableView.Refresh()
case tea.KeyMsg: case tea.KeyMsg:
if !m.statusAndPrompt.InPrompt() && !m.tableSelect.Visible() { if !m.statusAndPrompt.InPrompt() && !m.tableSelect.Visible() {
switch msg.String() { switch msg.String() {
case "m":
return m, m.tableWriteController.ToggleMark(m.tableView.SelectedItemIndex())
case "s": case "s":
return m, m.tableReadController.Rescan() return m, m.tableReadController.Rescan()
case ":": case ":":

View file

@ -28,8 +28,8 @@ func New() *Model {
frameTitle := frame.NewFrameTitle("No table", true) frameTitle := frame.NewFrameTitle("No table", true)
return &Model{ return &Model{
frameTitle: frameTitle, frameTitle: frameTitle,
table: tbl, table: tbl,
} }
} }
@ -85,6 +85,10 @@ func (m *Model) updateTable() {
m.table = newTbl m.table = newTbl
} }
func (m *Model) SelectedItemIndex() int {
return m.table.Cursor()
}
func (m *Model) selectedItem() (itemTableRow, bool) { func (m *Model) selectedItem() (itemTableRow, bool) {
resultSet := m.resultSet resultSet := m.resultSet
if resultSet != nil && len(resultSet.Items) > 0 { if resultSet != nil && len(resultSet.Items) > 0 {
@ -105,3 +109,8 @@ func (m *Model) postSelectedItemChanged() tea.Msg {
return dynamoitemview.NewItemSelected{ResultSet: item.resultSet, Item: item.item} return dynamoitemview.NewItemSelected{ResultSet: item.resultSet, Item: item.item}
} }
func (m *Model) Refresh() {
m.table.GoDown()
m.table.GoUp()
}

View file

@ -2,6 +2,7 @@ package dynamotableview
import ( import (
"fmt" "fmt"
"github.com/charmbracelet/lipgloss"
"io" "io"
"strings" "strings"
@ -10,12 +11,19 @@ import (
"github.com/lmika/awstools/internal/dynamo-browse/models" "github.com/lmika/awstools/internal/dynamo-browse/models"
) )
var (
markedRowStyle = lipgloss.NewStyle().
Background(lipgloss.Color("#e1e1e1"))
)
type itemTableRow struct { type itemTableRow struct {
resultSet *models.ResultSet resultSet *models.ResultSet
item models.Item item models.Item
} }
func (mtr itemTableRow) Render(w io.Writer, model table.Model, index int) { func (mtr itemTableRow) Render(w io.Writer, model table.Model, index int) {
isMarked := mtr.resultSet.Marked(index)
sb := strings.Builder{} sb := strings.Builder{}
for i, colName := range mtr.resultSet.Columns { for i, colName := range mtr.resultSet.Columns {
if i > 0 { if i > 0 {
@ -34,7 +42,13 @@ func (mtr itemTableRow) Render(w io.Writer, model table.Model, index int) {
} }
} }
if index == model.Cursor() { if index == model.Cursor() {
fmt.Fprintln(w, model.Styles.SelectedRow.Render(sb.String())) style := model.Styles.SelectedRow
if isMarked {
style = style.Copy().Inherit(markedRowStyle)
}
fmt.Fprintln(w, style.Render(sb.String()))
} else if isMarked {
fmt.Fprintln(w, markedRowStyle.Render(sb.String()))
} else { } else {
fmt.Fprintln(w, sb.String()) fmt.Fprintln(w, sb.String())
} }

View file

@ -7,6 +7,7 @@ import (
"github.com/lmika/awstools/internal/common/ui/events" "github.com/lmika/awstools/internal/common/ui/events"
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/layout" "github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/layout"
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/utils" "github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/utils"
"log"
) )
// StatusAndPrompt is a resizing model which displays a submodel and a status bar. When the start prompt // StatusAndPrompt is a resizing model which displays a submodel and a status bar. When the start prompt
@ -33,7 +34,7 @@ func (s *StatusAndPrompt) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case events.ErrorMsg: case events.ErrorMsg:
s.statusMessage = "Error: " + msg.Error() s.statusMessage = "Error: " + msg.Error()
case events.StatusMsg: case events.StatusMsg:
s.statusMessage = string(s.statusMessage) s.statusMessage = string(msg)
case events.MessageWithStatus: case events.MessageWithStatus:
s.statusMessage = msg.StatusMessage() s.statusMessage = msg.StatusMessage()
case events.PromptForInputMsg: case events.PromptForInputMsg:
@ -46,15 +47,18 @@ func (s *StatusAndPrompt) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
s.textInput.Focus() s.textInput.Focus()
s.textInput.SetValue("") s.textInput.SetValue("")
s.pendingInput = &msg s.pendingInput = &msg
log.Println("pending input == ", s.pendingInput)
return s, nil return s, nil
case tea.KeyMsg: case tea.KeyMsg:
if s.pendingInput != nil { if s.pendingInput != nil {
switch msg.String() { switch msg.String() {
case "ctrl+c", "esc": case "ctrl+c", "esc":
s.pendingInput = nil s.pendingInput = nil
log.Println("pending input == ", s.pendingInput)
case "enter": case "enter":
pendingInput := s.pendingInput pendingInput := s.pendingInput
s.pendingInput = nil s.pendingInput = nil
log.Println("pending input == ", s.pendingInput)
return s, pendingInput.OnDone(s.textInput.Value()) return s, pendingInput.OnDone(s.textInput.Value())
} }