From 798150a4034b482aafdfae8c020ad6ea36a172ea Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Wed, 30 Mar 2022 21:55:16 +1100 Subject: [PATCH] ssm-browse: added mark, filtering and delete items --- internal/dynamo-browse/controllers/events.go | 2 +- .../dynamo-browse/controllers/tableread.go | 42 +++++++++++- internal/dynamo-browse/models/attrutils.go | 16 +++++ internal/dynamo-browse/models/items.go | 30 +++++++++ internal/dynamo-browse/models/models.go | 64 ++++++++----------- .../dynamo-browse/services/tables/service.go | 36 ++++++++++- internal/dynamo-browse/ui/model.go | 7 +- .../ui/teamodels/dynamotableview/model.go | 30 +++++++-- .../ui/teamodels/dynamotableview/tblmodel.go | 3 +- .../ui/teamodels/statusandprompt/model.go | 23 ++----- 10 files changed, 181 insertions(+), 72 deletions(-) create mode 100644 internal/dynamo-browse/models/items.go diff --git a/internal/dynamo-browse/controllers/events.go b/internal/dynamo-browse/controllers/events.go index cd07142..441f05c 100644 --- a/internal/dynamo-browse/controllers/events.go +++ b/internal/dynamo-browse/controllers/events.go @@ -12,7 +12,7 @@ type NewResultSet struct { } 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 { diff --git a/internal/dynamo-browse/controllers/tableread.go b/internal/dynamo-browse/controllers/tableread.go index 5e42eae..d185179 100644 --- a/internal/dynamo-browse/controllers/tableread.go +++ b/internal/dynamo-browse/controllers/tableread.go @@ -17,6 +17,7 @@ type TableReadController struct { // state mutex *sync.Mutex resultSet *models.ResultSet + filter string } 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 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 c.setResultSet(newResultSet) + newResultSet = c.tableService.Filter(newResultSet, c.filter) + + return c.setResultSetAndFilter(newResultSet, c.filter) } func (c *TableReadController) ResultSet() *models.ResultSet { @@ -92,10 +95,43 @@ func (c *TableReadController) ResultSet() *models.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() defer c.mutex.Unlock() c.resultSet = resultSet + c.filter = filter 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) + } + }, + } + } +} diff --git a/internal/dynamo-browse/models/attrutils.go b/internal/dynamo-browse/models/attrutils.go index cf1855b..20cfc73 100644 --- a/internal/dynamo-browse/models/attrutils.go +++ b/internal/dynamo-browse/models/attrutils.go @@ -34,6 +34,22 @@ func compareScalarAttributes(x, y types.AttributeValue) (int, bool) { 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 { if isEqual { return 0 diff --git a/internal/dynamo-browse/models/items.go b/internal/dynamo-browse/models/items.go new file mode 100644 index 0000000..13a0b6a --- /dev/null +++ b/internal/dynamo-browse/models/items.go @@ -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]) +} diff --git a/internal/dynamo-browse/models/models.go b/internal/dynamo-browse/models/models.go index 7ec80c4..e709e1b 100644 --- a/internal/dynamo-browse/models/models.go +++ b/internal/dynamo-browse/models/models.go @@ -1,57 +1,47 @@ package models -import "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" - type ResultSet struct { - TableInfo *TableInfo - Columns []string - Items []Item - Marks map[int]bool + TableInfo *TableInfo + Columns []string + items []Item + attributes []ItemAttribute } -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 +type ItemAttribute struct { + Marked bool + Hidden bool } -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 (rs *ResultSet) Items() []Item { + return rs.items +} + +func (rs *ResultSet) SetItems(items []Item) { + rs.items = items + rs.attributes = make([]ItemAttribute, len(items)) } 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) - } + rs.attributes[idx].Marked = marked +} + +func (rs *ResultSet) SetHidden(idx int, hidden bool) { + rs.attributes[idx].Hidden = hidden } 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 { items := make([]Item, 0) - for i, marked := range rs.Marks { - if marked { - items = append(items, rs.Items[i]) + for i, itemAttr := range rs.attributes { + if itemAttr.Marked && !itemAttr.Hidden { + items = append(items, rs.items[i]) } } return items diff --git a/internal/dynamo-browse/services/tables/service.go b/internal/dynamo-browse/services/tables/service.go index b1898a6..12ee69b 100644 --- a/internal/dynamo-browse/services/tables/service.go +++ b/internal/dynamo-browse/services/tables/service.go @@ -3,6 +3,7 @@ package tables import ( "context" "sort" + "strings" "github.com/lmika/awstools/internal/dynamo-browse/models" "github.com/pkg/errors" @@ -67,11 +68,13 @@ func (s *Service) Scan(ctx context.Context, tableInfo *models.TableInfo) (*model models.Sort(results, tableInfo) - return &models.ResultSet{ + resultSet := &models.ResultSet{ TableInfo: tableInfo, 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 { @@ -86,3 +89,30 @@ func (s *Service) Delete(ctx context.Context, tableInfo *models.TableInfo, items } 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 +} diff --git a/internal/dynamo-browse/ui/model.go b/internal/dynamo-browse/ui/model.go index f53e051..e28b8f7 100644 --- a/internal/dynamo-browse/ui/model.go +++ b/internal/dynamo-browse/ui/model.go @@ -38,6 +38,7 @@ func NewModel(rc *controllers.TableReadController, wc *controllers.TableWriteCon return rc.ScanTable(args[0]) } }, + "unmark": commandctrl.NoArgCommand(rc.Unmark()), "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() { switch msg.String() { 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": return m, m.tableReadController.Rescan() + case "/": + return m, m.tableReadController.Filter() case ":": return m, m.commandController.Prompt() case "ctrl+c", "esc": diff --git a/internal/dynamo-browse/ui/teamodels/dynamotableview/model.go b/internal/dynamo-browse/ui/teamodels/dynamotableview/model.go index cb23f5b..feef2d3 100644 --- a/internal/dynamo-browse/ui/teamodels/dynamotableview/model.go +++ b/internal/dynamo-browse/ui/teamodels/dynamotableview/model.go @@ -17,6 +17,7 @@ type Model struct { w, h int // model state + rows []table.Row resultSet *models.ResultSet } @@ -52,6 +53,12 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case "k", "down": m.table.GoDown() 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) newTbl := table.New(resultSet.Columns, m.w, m.h-m.frameTitle.HeaderHeight()) - newRows := make([]table.Row, len(resultSet.Items)) - for i, r := range resultSet.Items { - newRows[i] = itemTableRow{resultSet, r} + newRows := make([]table.Row, 0) + for i, r := range resultSet.Items() { + if resultSet.Hidden(i) { + continue + } + + newRows = append(newRows, itemTableRow{resultSet: resultSet, itemIndex: i, item: r}) } + + m.rows = newRows newTbl.SetRows(newRows) m.table = newTbl } 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) { resultSet := m.resultSet - if resultSet != nil && len(resultSet.Items) > 0 { + if resultSet != nil && len(m.rows) > 0 { selectedItem, ok := m.table.SelectedRow().(itemTableRow) if ok { return selectedItem, true @@ -111,6 +128,5 @@ func (m *Model) postSelectedItemChanged() tea.Msg { } func (m *Model) Refresh() { - m.table.GoDown() - m.table.GoUp() + m.table.SetRows(m.rows) } diff --git a/internal/dynamo-browse/ui/teamodels/dynamotableview/tblmodel.go b/internal/dynamo-browse/ui/teamodels/dynamotableview/tblmodel.go index 6095e40..5e5a08d 100644 --- a/internal/dynamo-browse/ui/teamodels/dynamotableview/tblmodel.go +++ b/internal/dynamo-browse/ui/teamodels/dynamotableview/tblmodel.go @@ -18,11 +18,12 @@ var ( type itemTableRow struct { resultSet *models.ResultSet + itemIndex int item models.Item } 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{} for i, colName := range mtr.resultSet.Columns { diff --git a/internal/dynamo-browse/ui/teamodels/statusandprompt/model.go b/internal/dynamo-browse/ui/teamodels/statusandprompt/model.go index b422003..d9ee1dd 100644 --- a/internal/dynamo-browse/ui/teamodels/statusandprompt/model.go +++ b/internal/dynamo-browse/ui/teamodels/statusandprompt/model.go @@ -6,8 +6,6 @@ import ( "github.com/charmbracelet/lipgloss" "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/utils" - "log" ) // 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.SetValue("") s.pendingInput = &msg - log.Println("pending input == ", s.pendingInput) return s, nil case tea.KeyMsg: if s.pendingInput != nil { switch msg.String() { case "ctrl+c", "esc": s.pendingInput = nil - log.Println("pending input == ", s.pendingInput) case "enter": pendingInput := s.pendingInput s.pendingInput = nil - log.Println("pending input == ", s.pendingInput) 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) s.model = newModel.(layout.ResizingModel) return s, cmd