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 {
return fmt.Sprintf("%d items returned", len(rs.ResultSet.Items))
return fmt.Sprintf("%d items returned", len(rs.ResultSet.Items()))
}
type SetReadWrite struct {

View file

@ -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)
}
},
}
}
}

View file

@ -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

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
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

View file

@ -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
}

View file

@ -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":

View file

@ -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)
}

View file

@ -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 {

View file

@ -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