ssm-browse: added mark, filtering and delete items
This commit is contained in:
parent
c49f3913a8
commit
798150a403
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
30
internal/dynamo-browse/models/items.go
Normal file
30
internal/dynamo-browse/models/items.go
Normal 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])
|
||||
}
|
|
@ -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
|
||||
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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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":
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,36 +45,23 @@ 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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if s.pendingInput != nil {
|
||||
var cc utils.CmdCollector
|
||||
|
||||
default:
|
||||
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, cmd
|
||||
}
|
||||
}
|
||||
|
||||
return s, cc.Cmd()
|
||||
}
|
||||
|
||||
newModel, cmd := s.model.Update(msg)
|
||||
|
|
Loading…
Reference in a new issue