Fixed a glaring error where the user cannot close the column selector
Some checks failed
ci / build (push) Has been cancelled

Cause of this was that the close event type was also being used by the related overlay, and the event was being caught by that even though the overlay was hidden.

Also started working on changing the sort order within the column selector by pressing S.
This commit is contained in:
Leon Mika 2024-04-02 23:00:19 +11:00
parent f5bf31a903
commit e37b8099a3
20 changed files with 213 additions and 67 deletions

View file

@ -123,7 +123,7 @@ func main() {
*flagTable,
)
tableWriteController := controllers.NewTableWriteController(state, tableService, jobsController, tableReadController, settingStore)
columnsController := controllers.NewColumnsController(eventBus)
columnsController := controllers.NewColumnsController(tableReadController, eventBus)
exportController := controllers.NewExportController(state, tableService, jobsController, columnsController, pasteboardProvider)
settingsController := controllers.NewSettingsController(settingStore, eventBus)
keyBindings := keybindings.Default()

View file

@ -5,19 +5,22 @@ import (
"github.com/lmika/dynamo-browse/internal/common/ui/events"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/columns"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/evaluators"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/queryexpr"
bus "github.com/lmika/events"
"strings"
)
type ColumnsController struct {
tr *TableReadController
// State
colModel *columns.Columns
resultSet *models.ResultSet
}
func NewColumnsController(eventBus *bus.Bus) *ColumnsController {
cc := &ColumnsController{}
func NewColumnsController(tr *TableReadController, eventBus *bus.Bus) *ColumnsController {
cc := &ColumnsController{tr: tr}
eventBus.On(newResultSetEvent, cc.onNewResultSet)
return cc
@ -80,7 +83,7 @@ func (cc *ColumnsController) AddColumn(afterIndex int) tea.Msg {
newCol := columns.Column{
Name: colExpr.String(),
Evaluator: columns.ExprFieldValueEvaluator{Expr: colExpr},
Evaluator: queryexpr.ExprFieldValueEvaluator{Expr: colExpr},
}
if afterIndex >= len(cc.colModel.Columns)-1 {
@ -117,6 +120,25 @@ func (cc *ColumnsController) DeleteColumn(afterIndex int) tea.Msg {
return ColumnsUpdated{}
}
func (cc *ColumnsController) SortByColumn(index int) tea.Msg {
if index >= len(cc.colModel.Columns) {
return nil
}
column := cc.colModel.Columns[index]
newCriteria := models.SortCriteria{
Fields: []models.SortField{
{Field: column.Evaluator, Asc: true},
},
}
if ff := cc.SortCriteria().FirstField(); evaluators.Equals(ff.Field, column.Evaluator) {
newCriteria.Fields[0].Asc = !ff.Asc
}
cc.SetSortCriteria(newCriteria)
return ColumnsUpdated{}
}
func (c *ColumnsController) AttributesWithPrefix(prefix string) []string {
options := make([]string, 0)
for _, col := range c.resultSet.Columns() {
@ -126,3 +148,15 @@ func (c *ColumnsController) AttributesWithPrefix(prefix string) []string {
}
return options
}
func (cc *ColumnsController) SortCriteria() models.SortCriteria {
if cc.resultSet == nil {
return models.SortCriteria{}
}
return cc.resultSet.SortCriteria()
}
func (cc *ColumnsController) SetSortCriteria(criteria models.SortCriteria) {
cc.tr.SortResultSet(criteria)
}

View file

@ -35,6 +35,7 @@ const (
resultSetUpdateTouch
resultSetUpdateNextPage
resultSetUpdateScript
resultSetUpdateResort
)
type MarkOp int
@ -150,6 +151,13 @@ func (c *TableReadController) ScanTable(name string) tea.Msg {
}).OnEither(c.handleResultSetFromJobResult(c.state.Filter(), true, false, resultSetUpdateInit)).Submit()
}
func (c *TableReadController) SortResultSet(newCriteria models.SortCriteria) {
c.state.withResultSet(func(rs *models.ResultSet) {
rs.Sort(newCriteria.Append(models.PKSKSortFilter(rs.TableInfo)))
})
c.eventBus.Fire(newResultSetEvent, c.state.resultSet, resultSetUpdateResort)
}
func (c *TableReadController) PromptForQuery() tea.Msg {
return events.PromptForInputMsg{
Prompt: "query: ",

View file

@ -632,7 +632,7 @@ func newService(t *testing.T, cfg serviceConfig) *services {
)
writeController := controllers.NewTableWriteController(state, service, jobsController, readController, settingStore)
settingsController := controllers.NewSettingsController(settingStore, eventBus)
columnsController := controllers.NewColumnsController(eventBus)
columnsController := controllers.NewColumnsController(readController, eventBus)
exportController := controllers.NewExportController(state, service, jobsController, columnsController, pasteboardprovider.NilProvider{})
scriptController := controllers.NewScriptController(scriptService, readController, jobsController, settingsController, eventBus)

View file

@ -1,9 +1,7 @@
package columns
import (
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/queryexpr"
)
type Columns struct {
@ -19,7 +17,7 @@ func NewColumnsFromResultSet(rs *models.ResultSet) *Columns {
for i, c := range rsCols {
cols[i] = Column{
Name: c,
Evaluator: SimpleFieldValueEvaluator(c),
Evaluator: models.SimpleFieldValueEvaluator(c),
}
}
@ -44,7 +42,7 @@ func (cols *Columns) AddMissingColumns(rs *models.ResultSet) {
if _, hasCol := existingColumns[c]; !hasCol {
newCols = append(newCols, Column{
Name: c,
Evaluator: SimpleFieldValueEvaluator(c),
Evaluator: models.SimpleFieldValueEvaluator(c),
})
}
}
@ -56,7 +54,7 @@ func (cols *Columns) AddMissingColumns(rs *models.ResultSet) {
} else {
newCols[i] = Column{
Name: c,
Evaluator: SimpleFieldValueEvaluator(c),
Evaluator: models.SimpleFieldValueEvaluator(c),
}
}
}
@ -82,25 +80,6 @@ func (cols *Columns) VisibleColumns() []Column {
type Column struct {
Name string
Evaluator FieldValueEvaluator
Evaluator models.FieldValueEvaluator
Hidden bool
}
type FieldValueEvaluator interface {
EvaluateForItem(item models.Item) types.AttributeValue
}
type SimpleFieldValueEvaluator string
func (sfve SimpleFieldValueEvaluator) EvaluateForItem(item models.Item) types.AttributeValue {
return item[string(sfve)]
}
type ExprFieldValueEvaluator struct {
Expr *queryexpr.QueryExpr
}
func (sfve ExprFieldValueEvaluator) EvaluateForItem(item models.Item) types.AttributeValue {
val, _ := sfve.Expr.EvalItem(item)
return val
}

View file

@ -0,0 +1,15 @@
package models
import (
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
)
type FieldValueEvaluator interface {
EvaluateForItem(item Item) types.AttributeValue
}
type SimpleFieldValueEvaluator string
func (sfve SimpleFieldValueEvaluator) EvaluateForItem(item Item) types.AttributeValue {
return item[string(sfve)]
}

View file

@ -0,0 +1,25 @@
package evaluators
import (
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/queryexpr"
)
func Equals(x, y models.FieldValueEvaluator) bool {
if x == nil {
return y == nil
}
switch xt := x.(type) {
case models.SimpleFieldValueEvaluator:
if yt, ok := y.(models.SimpleFieldValueEvaluator); ok {
return xt == yt
}
case queryexpr.ExprFieldValueEvaluator:
if yt, ok := y.(queryexpr.ExprFieldValueEvaluator); ok {
return xt.Expr.Equal(yt.Expr)
}
}
return false
}

View file

@ -18,7 +18,8 @@ type ResultSet struct {
items []Item
attributes []ItemAttribute
columns []string
columns []string
sortCriteria SortCriteria
}
type Queryable interface {
@ -48,6 +49,10 @@ func (rs *ResultSet) SetItems(items []Item) {
rs.attributes = make([]ItemAttribute, len(items))
}
func (rs *ResultSet) SortCriteria() SortCriteria {
return rs.sortCriteria
}
func (rs *ResultSet) AddNewItem(item Item, attrs ItemAttribute) {
rs.items = append(rs.items, item)
rs.attributes = append(rs.attributes, attrs)
@ -141,3 +146,8 @@ func (rs *ResultSet) RefreshColumns() {
func (rs *ResultSet) HasNextPage() bool {
return rs.LastEvaluatedKey != nil
}
func (rs *ResultSet) Sort(criteria SortCriteria) {
rs.sortCriteria = criteria
Sort(rs.items, criteria)
}

View file

@ -53,6 +53,11 @@ func TestModExpr_Query(t *testing.T) {
`#0 = :0`,
exprNameIsString(0, 0, "pk", "prefix"),
),
//scanCase("when request pk is fixed (reverse)",
// `prefix="pk"`,
// `#0 = :0`,
// exprNameIsString(0, 0, "pk", "prefix"),
//),
scanCase("when request pk is fixed in parens #1",
`(pk="prefix")`,
`#0 = :0`,
@ -513,6 +518,7 @@ func TestQueryExpr_EvalItem(t *testing.T) {
{expr: "three <= 2", expected: &types.AttributeValueMemberBOOL{Value: false}},
// Between
{expr: "3 between 1 and 5", expected: &types.AttributeValueMemberBOOL{Value: true}},
{expr: "three between 1 and 5", expected: &types.AttributeValueMemberBOOL{Value: true}},
{expr: "three between one and five", expected: &types.AttributeValueMemberBOOL{Value: true}},
{expr: "three between 10 and 15", expected: &types.AttributeValueMemberBOOL{Value: false}},

View file

@ -0,0 +1,15 @@
package queryexpr
import (
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
)
type ExprFieldValueEvaluator struct {
Expr *QueryExpr
}
func (sfve ExprFieldValueEvaluator) EvaluateForItem(item models.Item) types.AttributeValue {
val, _ := sfve.Expr.EvalItem(item)
return val
}

View file

@ -8,13 +8,60 @@ import (
// sortedItems is a collection of items that is sorted.
// Items are sorted based on the PK, and SK in ascending order
type sortedItems struct {
tableInfo *TableInfo
items []Item
criteria SortCriteria
items []Item
}
type SortField struct {
Field FieldValueEvaluator
Asc bool
}
type SortCriteria struct {
Fields []SortField
}
func (sc SortCriteria) FirstField() SortField {
if len(sc.Fields) == 0 {
return SortField{}
}
return sc.Fields[0]
}
func (sc SortCriteria) Equals(osc SortCriteria) bool {
if len(sc.Fields) != len(osc.Fields) {
return false
}
for i := range osc.Fields {
if sc.Fields[i].Field != osc.Fields[i].Field ||
sc.Fields[i].Asc != osc.Fields[i].Asc {
return false
}
}
return true
}
func (sc SortCriteria) Append(osc SortCriteria) SortCriteria {
newItems := make([]SortField, 0, len(osc.Fields))
newItems = append(newItems, sc.Fields...)
newItems = append(newItems, osc.Fields...)
return SortCriteria{Fields: newItems}
}
func PKSKSortFilter(ti *TableInfo) SortCriteria {
return SortCriteria{
Fields: []SortField{
{Field: SimpleFieldValueEvaluator(ti.Keys.PartitionKey), Asc: true},
{Field: SimpleFieldValueEvaluator(ti.Keys.SortKey), Asc: true},
},
}
}
// Sort sorts the items in place
func Sort(items []Item, tableInfo *TableInfo) {
si := sortedItems{items: items, tableInfo: tableInfo}
func Sort(items []Item, criteria SortCriteria) {
si := sortedItems{items: items, criteria: criteria}
sort.Sort(&si)
}
@ -23,30 +70,21 @@ func (si *sortedItems) Len() int {
}
func (si *sortedItems) Less(i, j int) bool {
// Compare primary keys
pv1, pv2 := si.items[i][si.tableInfo.Keys.PartitionKey], si.items[j][si.tableInfo.Keys.PartitionKey]
pc, ok := attrutils.CompareScalarAttributes(pv1, pv2)
if !ok {
return i < j
}
if pc < 0 {
return true
} else if pc > 0 {
return false
}
// Partition keys are equal, compare sort key
if sortKey := si.tableInfo.Keys.SortKey; sortKey != "" {
sv1, sv2 := si.items[i][sortKey], si.items[j][sortKey]
sc, ok := attrutils.CompareScalarAttributes(sv1, sv2)
for _, field := range si.criteria.Fields {
// Compare primary keys
pv1, pv2 := field.Field.EvaluateForItem(si.items[i]), field.Field.EvaluateForItem(si.items[j])
pc, ok := attrutils.CompareScalarAttributes(pv1, pv2)
if !ok {
return i < j
}
if sc < 0 {
if !field.Asc {
pc = -pc
}
if pc < 0 {
return true
} else if sc > 0 {
} else if pc > 0 {
return false
}
}

View file

@ -15,7 +15,7 @@ func TestSort(t *testing.T) {
items := make([]models.Item, len(testStringData))
copy(items, testStringData)
models.Sort(items, tableInfo)
models.Sort(items, models.PKSKSortFilter(tableInfo))
assert.Equal(t, items[0], testStringData[1])
assert.Equal(t, items[1], testStringData[2])
@ -28,7 +28,7 @@ func TestSort(t *testing.T) {
items := make([]models.Item, len(testNumberData))
copy(items, testNumberData)
models.Sort(items, tableInfo)
models.Sort(items, models.PKSKSortFilter(tableInfo))
assert.Equal(t, items[0], testNumberData[2])
assert.Equal(t, items[1], testNumberData[1])
@ -41,7 +41,7 @@ func TestSort(t *testing.T) {
items := make([]models.Item, len(testBoolData))
copy(items, testBoolData)
models.Sort(items, tableInfo)
models.Sort(items, models.PKSKSortFilter(tableInfo))
assert.Equal(t, items[0], testBoolData[2])
assert.Equal(t, items[1], testBoolData[1])

View file

@ -86,8 +86,6 @@ func (s *Service) doScan(
}, errors.Wrapf(err, "unable to scan table %v", tableInfo.Name)
}
models.Sort(results, tableInfo)
resultSet := &models.ResultSet{
TableInfo: tableInfo,
Created: time.Now(),
@ -97,6 +95,7 @@ func (s *Service) doScan(
}
resultSet.SetItems(results)
resultSet.RefreshColumns()
resultSet.Sort(models.PKSKSortFilter(tableInfo))
return resultSet, err
}

View file

@ -12,6 +12,7 @@ func Default() *KeyBindings {
ResetColumns: key.NewBinding(key.WithKeys("R", "reset columns")),
AddColumn: key.NewBinding(key.WithKeys("a", "add new column")),
DeleteColumn: key.NewBinding(key.WithKeys("d", "delete column")),
SortByColumn: key.NewBinding(key.WithKeys("s", "sort by column")),
},
TableView: &TableKeyBinding{
MoveUp: key.NewBinding(key.WithKeys("i", "up")),

View file

@ -16,6 +16,7 @@ type FieldsPopupBinding struct {
ResetColumns key.Binding `keymap:"reset-columns"`
AddColumn key.Binding `keymap:"add-column"`
DeleteColumn key.Binding `keymap:"delete-column"`
SortByColumn key.Binding `keymap:"sort-by-column"`
}
type TableKeyBinding struct {

View file

@ -6,6 +6,7 @@ import (
"github.com/charmbracelet/lipgloss"
"github.com/lmika/dynamo-browse/internal/common/ui/events"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/controllers"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/columns"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/keybindings"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/layout"
@ -25,8 +26,9 @@ type colListModel struct {
keyBinding *keybindings.KeyBindings
colController *controllers.ColumnsController
rows []table.Row
table table.Model
rows []table.Row
table table.Model
sortCriteria models.SortCriteria
}
func newColListModel(keyBinding *keybindings.KeyBindings, colController *controllers.ColumnsController) *colListModel {
@ -68,6 +70,8 @@ func (m *colListModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, events.SetTeaMessage(m.colController.AddColumn(m.table.Cursor()))
case key.Matches(msg, m.keyBinding.ColumnPopup.DeleteColumn):
return m, events.SetTeaMessage(m.colController.DeleteColumn(m.table.Cursor()))
case key.Matches(msg, m.keyBinding.ColumnPopup.SortByColumn):
return m, events.SetTeaMessage(m.colController.SortByColumn(m.table.Cursor()))
// Main table nav
case key.Matches(msg, m.keyBinding.TableView.ColLeft):
@ -122,6 +126,7 @@ func (c *colListModel) Resize(w, h int) layout.ResizingModel {
func (c *colListModel) refreshTable() {
colsFromController := c.colController.Columns()
c.sortCriteria = c.colController.SortCriteria()
if len(c.rows) != len(colsFromController.Columns) {
c.setColumnsFromModel(colsFromController)
}

View file

@ -42,6 +42,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cc utils.CmdCollector
switch msg := msg.(type) {
case controllers.ShowColumnOverlay:
m.colListModel.sortCriteria = m.columnsController.SortCriteria()
m.colListModel.setColumnsFromModel(m.columnsController.Columns())
m.compositor.SetOverlay(m.colListModel, m.w/2-overlayWidth/2, m.h/2-overlayHeight/2, overlayWidth, overlayHeight)
case controllers.HideColumnOverlay:

View file

@ -3,6 +3,7 @@ package colselector
import (
"fmt"
"github.com/charmbracelet/lipgloss"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/evaluators"
table "github.com/lmika/go-bubble-table"
"io"
)
@ -23,9 +24,17 @@ func (clr colListRowModel) Render(w io.Writer, model table.Model, index int) {
}
col := clr.m.colController.Columns().Columns[index]
if !col.Hidden {
fmt.Fprintln(w, style.Render(fmt.Sprintf("⋅\t%v", col.Name)))
} else {
ff := clr.m.sortCriteria.FirstField()
switch {
case col.Hidden:
fmt.Fprintln(w, style.Render(fmt.Sprintf("✕\t%v", col.Name)))
case evaluators.Equals(ff.Field, col.Evaluator):
if ff.Asc {
fmt.Fprintln(w, style.Render(fmt.Sprintf("v\t%v", col.Name)))
} else {
fmt.Fprintln(w, style.Render(fmt.Sprintf("^\t%v", col.Name)))
}
default:
fmt.Fprintln(w, style.Render(fmt.Sprintf("⋅\t%v", col.Name)))
}
}

View file

@ -90,9 +90,9 @@ func (m *listModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if onSel := m.event.OnSelected; onSel != nil {
cc.Add(events.SetTeaMessage(onSel(m.event.Items[m.list.Index()])))
}
return m, events.SetTeaMessage(controllers.HideColumnOverlay{})
return m, events.SetTeaMessage(controllers.HideRelatedItemsOverlay{})
case key.Matches(msg, keyEsc):
return m, events.SetTeaMessage(controllers.HideColumnOverlay{})
return m, events.SetTeaMessage(controllers.HideRelatedItemsOverlay{})
default:
m.list = cc.Collect(m.list.Update(msg)).(list.Model)
}

View file

@ -45,7 +45,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.listModel.setItems(msg, newHeight)
m.compositor.SetOverlay(m.listModel, m.w/2-overlayWidth/2, m.h/2-newHeight/2, overlayWidth, newHeight)
case controllers.HideColumnOverlay:
case controllers.HideRelatedItemsOverlay:
m.compositor.ClearOverlay()
case tea.KeyMsg:
m.compositor = cc.Collect(m.compositor.Update(msg)).(*layout.Compositor)