ssm-browse: added mark, filtering and delete items

This commit is contained in:
Leon Mika 2022-03-30 21:55:16 +11:00
parent c49f3913a8
commit 798150a403
10 changed files with 181 additions and 72 deletions

View file

@ -12,7 +12,7 @@ type NewResultSet struct {
} }
func (rs NewResultSet) StatusMessage() string { func (rs NewResultSet) StatusMessage() string {
return fmt.Sprintf("%d items returned", len(rs.ResultSet.Items)) return fmt.Sprintf("%d items returned", len(rs.ResultSet.Items()))
} }
type SetReadWrite struct { type SetReadWrite struct {

View file

@ -17,6 +17,7 @@ type TableReadController struct {
// state // state
mutex *sync.Mutex mutex *sync.Mutex
resultSet *models.ResultSet resultSet *models.ResultSet
filter string
} }
func NewTableReadController(tableService *tables.Service, tableName string) *TableReadController { func NewTableReadController(tableService *tables.Service, tableName string) *TableReadController {
@ -66,7 +67,7 @@ func (c *TableReadController) ScanTable(name string) tea.Cmd {
return events.Error(err) return events.Error(err)
} }
return c.setResultSet(resultSet) return c.setResultSetAndFilter(resultSet, c.filter)
} }
} }
@ -82,7 +83,9 @@ func (c *TableReadController) doScan(ctx context.Context, resultSet *models.Resu
return events.Error(err) return events.Error(err)
} }
return c.setResultSet(newResultSet) newResultSet = c.tableService.Filter(newResultSet, c.filter)
return c.setResultSetAndFilter(newResultSet, c.filter)
} }
func (c *TableReadController) ResultSet() *models.ResultSet { func (c *TableReadController) ResultSet() *models.ResultSet {
@ -92,10 +95,43 @@ func (c *TableReadController) ResultSet() *models.ResultSet {
return c.resultSet return c.resultSet
} }
func (c *TableReadController) setResultSet(resultSet *models.ResultSet) tea.Msg { func (c *TableReadController) setResultSetAndFilter(resultSet *models.ResultSet, filter string) tea.Msg {
c.mutex.Lock() c.mutex.Lock()
defer c.mutex.Unlock() defer c.mutex.Unlock()
c.resultSet = resultSet c.resultSet = resultSet
c.filter = filter
return NewResultSet{resultSet} return NewResultSet{resultSet}
} }
func (c *TableReadController) Unmark() tea.Cmd {
return func() tea.Msg {
resultSet := c.ResultSet()
for i := range resultSet.Items() {
resultSet.SetMark(i, false)
}
c.mutex.Lock()
defer c.mutex.Unlock()
c.resultSet = resultSet
return ResultSetUpdated{}
}
}
func (c *TableReadController) Filter() tea.Cmd {
return func() tea.Msg {
return events.PromptForInputMsg{
Prompt: "filter: ",
OnDone: func(value string) tea.Cmd {
return func() tea.Msg {
resultSet := c.ResultSet()
newResultSet := c.tableService.Filter(resultSet, value)
return c.setResultSetAndFilter(newResultSet, value)
}
},
}
}
}

View file

@ -34,6 +34,22 @@ func compareScalarAttributes(x, y types.AttributeValue) (int, bool) {
return 0, false return 0, false
} }
func attributeToString(x types.AttributeValue) (string, bool) {
switch xVal := x.(type) {
case *types.AttributeValueMemberS:
return xVal.Value, true
case *types.AttributeValueMemberN:
return xVal.Value, true
case *types.AttributeValueMemberBOOL:
if xVal.Value {
return "true", true
} else {
return "false", true
}
}
return "", false
}
func comparisonValue(isEqual bool, isLess bool) int { func comparisonValue(isEqual bool, isLess bool) int {
if isEqual { if isEqual {
return 0 return 0

View file

@ -0,0 +1,30 @@
package models
import "github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
type Item map[string]types.AttributeValue
// Clone creates a clone of the current item
func (i Item) Clone() Item {
newItem := Item{}
// TODO: should be a deep clone?
for k, v := range i {
newItem[k] = v
}
return newItem
}
func (i Item) KeyValue(info *TableInfo) map[string]types.AttributeValue {
itemKey := make(map[string]types.AttributeValue)
itemKey[info.Keys.PartitionKey] = i[info.Keys.PartitionKey]
if info.Keys.SortKey != "" {
itemKey[info.Keys.SortKey] = i[info.Keys.SortKey]
}
return itemKey
}
func (i Item) AttributeValueAsString(k string) (string, bool) {
return attributeToString(i[k])
}

View file

@ -1,57 +1,47 @@
package models package models
import "github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
type ResultSet struct { type ResultSet struct {
TableInfo *TableInfo TableInfo *TableInfo
Columns []string Columns []string
Items []Item items []Item
Marks map[int]bool attributes []ItemAttribute
} }
type Item map[string]types.AttributeValue type ItemAttribute struct {
Marked bool
// Clone creates a clone of the current item Hidden bool
func (i Item) Clone() Item {
newItem := Item{}
// TODO: should be a deep clone?
for k, v := range i {
newItem[k] = v
}
return newItem
} }
func (i Item) KeyValue(info *TableInfo) map[string]types.AttributeValue { func (rs *ResultSet) Items() []Item {
itemKey := make(map[string]types.AttributeValue) return rs.items
itemKey[info.Keys.PartitionKey] = i[info.Keys.PartitionKey] }
if info.Keys.SortKey != "" {
itemKey[info.Keys.SortKey] = i[info.Keys.SortKey] func (rs *ResultSet) SetItems(items []Item) {
} rs.items = items
return itemKey rs.attributes = make([]ItemAttribute, len(items))
} }
func (rs *ResultSet) SetMark(idx int, marked bool) { func (rs *ResultSet) SetMark(idx int, marked bool) {
if marked { rs.attributes[idx].Marked = marked
if rs.Marks == nil { }
rs.Marks = make(map[int]bool)
} func (rs *ResultSet) SetHidden(idx int, hidden bool) {
rs.Marks[idx] = true rs.attributes[idx].Hidden = hidden
} else {
delete(rs.Marks, idx)
}
} }
func (rs *ResultSet) Marked(idx int) bool { func (rs *ResultSet) Marked(idx int) bool {
return rs.Marks[idx] return rs.attributes[idx].Marked
}
func (rs *ResultSet) Hidden(idx int) bool {
return rs.attributes[idx].Hidden
} }
func (rs *ResultSet) MarkedItems() []Item { func (rs *ResultSet) MarkedItems() []Item {
items := make([]Item, 0) items := make([]Item, 0)
for i, marked := range rs.Marks { for i, itemAttr := range rs.attributes {
if marked { if itemAttr.Marked && !itemAttr.Hidden {
items = append(items, rs.Items[i]) items = append(items, rs.items[i])
} }
} }
return items return items

View file

@ -3,6 +3,7 @@ package tables
import ( import (
"context" "context"
"sort" "sort"
"strings"
"github.com/lmika/awstools/internal/dynamo-browse/models" "github.com/lmika/awstools/internal/dynamo-browse/models"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -67,11 +68,13 @@ func (s *Service) Scan(ctx context.Context, tableInfo *models.TableInfo) (*model
models.Sort(results, tableInfo) models.Sort(results, tableInfo)
return &models.ResultSet{ resultSet := &models.ResultSet{
TableInfo: tableInfo, TableInfo: tableInfo,
Columns: columns, Columns: columns,
Items: results, }
}, nil resultSet.SetItems(results)
return resultSet, nil
} }
func (s *Service) Put(ctx context.Context, tableInfo *models.TableInfo, item models.Item) error { func (s *Service) Put(ctx context.Context, tableInfo *models.TableInfo, item models.Item) error {
@ -86,3 +89,30 @@ func (s *Service) Delete(ctx context.Context, tableInfo *models.TableInfo, items
} }
return nil return nil
} }
// TODO: move into a new service
func (s *Service) Filter(resultSet *models.ResultSet, filter string) *models.ResultSet {
for i, item := range resultSet.Items() {
if filter == "" {
resultSet.SetHidden(i, false)
continue
}
var shouldHide = true
for k := range item {
str, ok := item.AttributeValueAsString(k)
if !ok {
continue
}
if strings.Contains(str, filter) {
shouldHide = false
break
}
}
resultSet.SetHidden(i, shouldHide)
}
return resultSet
}

View file

@ -38,6 +38,7 @@ func NewModel(rc *controllers.TableReadController, wc *controllers.TableWriteCon
return rc.ScanTable(args[0]) return rc.ScanTable(args[0])
} }
}, },
"unmark": commandctrl.NoArgCommand(rc.Unmark()),
"delete": commandctrl.NoArgCommand(wc.DeleteMarked()), "delete": commandctrl.NoArgCommand(wc.DeleteMarked()),
}, },
}) })
@ -67,9 +68,13 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if !m.statusAndPrompt.InPrompt() && !m.tableSelect.Visible() { if !m.statusAndPrompt.InPrompt() && !m.tableSelect.Visible() {
switch msg.String() { switch msg.String() {
case "m": case "m":
return m, m.tableWriteController.ToggleMark(m.tableView.SelectedItemIndex()) if idx := m.tableView.SelectedItemIndex(); idx >= 0 {
return m, m.tableWriteController.ToggleMark(idx)
}
case "s": case "s":
return m, m.tableReadController.Rescan() return m, m.tableReadController.Rescan()
case "/":
return m, m.tableReadController.Filter()
case ":": case ":":
return m, m.commandController.Prompt() return m, m.commandController.Prompt()
case "ctrl+c", "esc": case "ctrl+c", "esc":

View file

@ -17,6 +17,7 @@ type Model struct {
w, h int w, h int
// model state // model state
rows []table.Row
resultSet *models.ResultSet resultSet *models.ResultSet
} }
@ -52,6 +53,12 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case "k", "down": case "k", "down":
m.table.GoDown() m.table.GoDown()
return m, m.postSelectedItemChanged return m, m.postSelectedItemChanged
case "I", "pgup":
m.table.GoPageUp()
return m, m.postSelectedItemChanged
case "K", "pgdn":
m.table.GoPageDown()
return m, m.postSelectedItemChanged
} }
} }
@ -76,22 +83,32 @@ func (m *Model) updateTable() {
m.frameTitle.SetTitle("Table: " + resultSet.TableInfo.Name) m.frameTitle.SetTitle("Table: " + resultSet.TableInfo.Name)
newTbl := table.New(resultSet.Columns, m.w, m.h-m.frameTitle.HeaderHeight()) newTbl := table.New(resultSet.Columns, m.w, m.h-m.frameTitle.HeaderHeight())
newRows := make([]table.Row, len(resultSet.Items)) newRows := make([]table.Row, 0)
for i, r := range resultSet.Items { for i, r := range resultSet.Items() {
newRows[i] = itemTableRow{resultSet, r} if resultSet.Hidden(i) {
continue
}
newRows = append(newRows, itemTableRow{resultSet: resultSet, itemIndex: i, item: r})
} }
m.rows = newRows
newTbl.SetRows(newRows) newTbl.SetRows(newRows)
m.table = newTbl m.table = newTbl
} }
func (m *Model) SelectedItemIndex() int { func (m *Model) SelectedItemIndex() int {
return m.table.Cursor() selectedItem, ok := m.selectedItem()
if !ok {
return -1
}
return selectedItem.itemIndex
} }
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(m.rows) > 0 {
selectedItem, ok := m.table.SelectedRow().(itemTableRow) selectedItem, ok := m.table.SelectedRow().(itemTableRow)
if ok { if ok {
return selectedItem, true return selectedItem, true
@ -111,6 +128,5 @@ func (m *Model) postSelectedItemChanged() tea.Msg {
} }
func (m *Model) Refresh() { func (m *Model) Refresh() {
m.table.GoDown() m.table.SetRows(m.rows)
m.table.GoUp()
} }

View file

@ -18,11 +18,12 @@ var (
type itemTableRow struct { type itemTableRow struct {
resultSet *models.ResultSet resultSet *models.ResultSet
itemIndex int
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) isMarked := mtr.resultSet.Marked(mtr.itemIndex)
sb := strings.Builder{} sb := strings.Builder{}
for i, colName := range mtr.resultSet.Columns { for i, colName := range mtr.resultSet.Columns {

View file

@ -6,8 +6,6 @@ import (
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
"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"
"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
@ -47,38 +45,25 @@ 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())
default:
newTextInput, cmd := s.textInput.Update(msg)
s.textInput = newTextInput
return s, cmd
} }
} }
} }
if s.pendingInput != nil {
var cc utils.CmdCollector
newTextInput, cmd := s.textInput.Update(msg)
cc.Add(cmd)
s.textInput = newTextInput
if _, isKey := msg.(tea.Key); !isKey {
s.model = cc.Collect(s.model.Update(msg)).(layout.ResizingModel)
}
return s, cc.Cmd()
}
newModel, cmd := s.model.Update(msg) newModel, cmd := s.model.Update(msg)
s.model = newModel.(layout.ResizingModel) s.model = newModel.(layout.ResizingModel)
return s, cmd return s, cmd