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 {
|
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 {
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
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
|
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
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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":
|
||||||
|
|
|
@ -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()
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue