diff --git a/cmd/dynamo-browse/main.go b/cmd/dynamo-browse/main.go index 5089079..b000525 100644 --- a/cmd/dynamo-browse/main.go +++ b/cmd/dynamo-browse/main.go @@ -19,7 +19,7 @@ import ( "github.com/lmika/audax/internal/dynamo-browse/services/jobs" keybindings_service "github.com/lmika/audax/internal/dynamo-browse/services/keybindings" "github.com/lmika/audax/internal/dynamo-browse/services/tables" - workspaces_service "github.com/lmika/audax/internal/dynamo-browse/services/workspaces" + "github.com/lmika/audax/internal/dynamo-browse/services/viewsnapshot" "github.com/lmika/audax/internal/dynamo-browse/ui" "github.com/lmika/audax/internal/dynamo-browse/ui/keybindings" "github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/styles" @@ -93,7 +93,7 @@ func main() { } tableService := tables.NewService(dynamoProvider, settingStore) - workspaceService := workspaces_service.NewService(resultSetSnapshotStore) + workspaceService := viewsnapshot.NewService(resultSetSnapshotStore) itemRendererService := itemrenderer.NewService(uiStyles.ItemView.FieldType, uiStyles.ItemView.MetaInfo) jobsService := jobs.NewService(eventBus) diff --git a/internal/dynamo-browse/controllers/columns.go b/internal/dynamo-browse/controllers/columns.go index 75a2ed9..490204f 100644 --- a/internal/dynamo-browse/controllers/columns.go +++ b/internal/dynamo-browse/controllers/columns.go @@ -90,7 +90,10 @@ func (cc *ColumnsController) AddColumn(afterIndex int) tea.Msg { cc.colModel.Columns = newCols } - return ColumnsUpdated{} + return tea.Batch( + events.SetTeaMessage(ColumnsUpdated{}), + events.SetTeaMessage(SetSelectedColumnInColSelector(afterIndex+1)), + )() }) } diff --git a/internal/dynamo-browse/controllers/events.go b/internal/dynamo-browse/controllers/events.go index 1433ac1..8805de4 100644 --- a/internal/dynamo-browse/controllers/events.go +++ b/internal/dynamo-browse/controllers/events.go @@ -16,6 +16,8 @@ type SettingsUpdated struct { type ColumnsUpdated struct { } +type SetSelectedColumnInColSelector int + type MoveLeftmostDisplayedColumnInTableViewBy int type NewResultSet struct { diff --git a/internal/dynamo-browse/controllers/tableread.go b/internal/dynamo-browse/controllers/tableread.go index 3265edd..7f6dce7 100644 --- a/internal/dynamo-browse/controllers/tableread.go +++ b/internal/dynamo-browse/controllers/tableread.go @@ -9,7 +9,7 @@ import ( "github.com/lmika/audax/internal/dynamo-browse/models/queryexpr" "github.com/lmika/audax/internal/dynamo-browse/models/serialisable" "github.com/lmika/audax/internal/dynamo-browse/services/itemrenderer" - "github.com/lmika/audax/internal/dynamo-browse/services/workspaces" + "github.com/lmika/audax/internal/dynamo-browse/services/viewsnapshot" bus "github.com/lmika/events" "github.com/pkg/errors" "golang.design/x/clipboard" @@ -39,7 +39,7 @@ const ( type TableReadController struct { tableService TableReadService - workspaceService *workspaces.ViewSnapshotService + workspaceService *viewsnapshot.ViewSnapshotService itemRendererService *itemrenderer.Service jobController *JobsController eventBus *bus.Bus @@ -55,7 +55,7 @@ type TableReadController struct { func NewTableReadController( state *State, tableService TableReadService, - workspaceService *workspaces.ViewSnapshotService, + workspaceService *viewsnapshot.ViewSnapshotService, itemRendererService *itemrenderer.Service, jobController *JobsController, eventBus *bus.Bus, @@ -211,8 +211,16 @@ func (c *TableReadController) doScan(resultSet *models.ResultSet, query models.Q } func (c *TableReadController) setResultSetAndFilter(resultSet *models.ResultSet, filter string, pushBackstack bool, op resultSetUpdateOp) tea.Msg { - if pushBackstack { - if err := c.workspaceService.PushSnapshot(resultSet, filter); err != nil { + if resultSet != nil && pushBackstack { + details := serialisable.ViewSnapshotDetails{ + TableName: resultSet.TableInfo.Name, + Filter: filter, + } + if q := resultSet.Query; q != nil { + details.Query = q.String() + } + + if err := c.workspaceService.PushSnapshot(details); err != nil { log.Printf("cannot push snapshot: %v", err) } } @@ -308,13 +316,13 @@ func (c *TableReadController) updateViewToSnapshot(viewSnapshot *serialisable.Vi if currentResultSet == nil { return NewJob(c.jobController, "Fetching table info…", func(ctx context.Context) (*models.TableInfo, error) { - tableInfo, err := c.tableService.Describe(context.Background(), viewSnapshot.TableName) + tableInfo, err := c.tableService.Describe(context.Background(), viewSnapshot.Details.TableName) if err != nil { return nil, err } return tableInfo, nil }).OnDone(func(tableInfo *models.TableInfo) tea.Msg { - return c.runQuery(tableInfo, viewSnapshot.Query, viewSnapshot.Filter, false) + return c.runQuery(tableInfo, viewSnapshot.Details.Query, viewSnapshot.Details.Filter, false) }).Submit() } @@ -323,22 +331,22 @@ func (c *TableReadController) updateViewToSnapshot(viewSnapshot *serialisable.Vi currentQueryExpr = currentResultSet.Query.String() } - if viewSnapshot.TableName == currentResultSet.TableInfo.Name && viewSnapshot.Query == currentQueryExpr { + if viewSnapshot.Details.TableName == currentResultSet.TableInfo.Name && viewSnapshot.Details.Query == currentQueryExpr { return NewJob(c.jobController, "Applying filter…", func(ctx context.Context) (*models.ResultSet, error) { - return c.tableService.Filter(currentResultSet, viewSnapshot.Filter), nil - }).OnEither(c.handleResultSetFromJobResult(viewSnapshot.Filter, false, resultSetUpdateSnapshotRestore)).Submit() + return c.tableService.Filter(currentResultSet, viewSnapshot.Details.Filter), nil + }).OnEither(c.handleResultSetFromJobResult(viewSnapshot.Details.Filter, false, resultSetUpdateSnapshotRestore)).Submit() } return NewJob(c.jobController, "Running query…", func(ctx context.Context) (tea.Msg, error) { tableInfo := currentResultSet.TableInfo - if viewSnapshot.TableName != currentResultSet.TableInfo.Name { - tableInfo, err = c.tableService.Describe(context.Background(), viewSnapshot.TableName) + if viewSnapshot.Details.TableName != currentResultSet.TableInfo.Name { + tableInfo, err = c.tableService.Describe(context.Background(), viewSnapshot.Details.TableName) if err != nil { return nil, err } } - return c.runQuery(tableInfo, viewSnapshot.Query, viewSnapshot.Filter, false), nil + return c.runQuery(tableInfo, viewSnapshot.Details.Query, viewSnapshot.Details.Filter, false), nil }).OnDone(func(m tea.Msg) tea.Msg { return m }).Submit() diff --git a/internal/dynamo-browse/controllers/tableread_test.go b/internal/dynamo-browse/controllers/tableread_test.go index 4453fbc..ecbc1e9 100644 --- a/internal/dynamo-browse/controllers/tableread_test.go +++ b/internal/dynamo-browse/controllers/tableread_test.go @@ -4,7 +4,6 @@ import ( "fmt" tea "github.com/charmbracelet/bubbletea" "github.com/lmika/audax/internal/common/ui/events" - "github.com/lmika/audax/internal/common/workspaces" "github.com/lmika/audax/internal/dynamo-browse/controllers" "github.com/lmika/audax/test/testdynamo" "github.com/stretchr/testify/assert" @@ -124,19 +123,6 @@ func tempFile(t *testing.T) string { return tempFile.Name() } -func testWorkspace(t *testing.T) *workspaces.Workspace { - wsTempFile := tempFile(t) - - wsManager := workspaces.New(workspaces.MetaInfo{Command: "dynamo-browse"}) - ws, err := wsManager.Open(wsTempFile) - if err != nil { - t.Fatalf("cannot create workspace manager: %v", err) - } - t.Cleanup(func() { ws.Close() }) - - return ws -} - func invokeCommand(t *testing.T, msg tea.Msg) tea.Msg { err, isErr := msg.(events.ErrorMsg) if isErr { diff --git a/internal/dynamo-browse/controllers/tablewrite_test.go b/internal/dynamo-browse/controllers/tablewrite_test.go index f755c1c..b9a77c6 100644 --- a/internal/dynamo-browse/controllers/tablewrite_test.go +++ b/internal/dynamo-browse/controllers/tablewrite_test.go @@ -13,6 +13,7 @@ import ( "github.com/lmika/audax/internal/dynamo-browse/services/tables" workspaces_service "github.com/lmika/audax/internal/dynamo-browse/services/workspaces" "github.com/lmika/audax/test/testdynamo" + "github.com/lmika/audax/test/testworkspace" bus "github.com/lmika/events" "github.com/stretchr/testify/assert" "testing" @@ -583,7 +584,7 @@ type serviceConfig struct { } func newService(t *testing.T, cfg serviceConfig) *services { - ws := testWorkspace(t) + ws := testworkspace.New(t) resultSetSnapshotStore := workspacestore.NewResultSetSnapshotStore(ws) settingStore := settingstore.New(ws) diff --git a/internal/dynamo-browse/models/serialisable/viewsnapshot.go b/internal/dynamo-browse/models/serialisable/viewsnapshot.go index e04edd9..78a7191 100644 --- a/internal/dynamo-browse/models/serialisable/viewsnapshot.go +++ b/internal/dynamo-browse/models/serialisable/viewsnapshot.go @@ -5,10 +5,14 @@ import ( ) type ViewSnapshot struct { - ID int64 `storm:"id,increment"` - BackLink int64 `storm:"index"` - ForeLink int64 `storm:"index"` - Time time.Time + ID int64 `storm:"id,increment"` + BackLink int64 `storm:"index"` + ForeLink int64 `storm:"index"` + Time time.Time + Details ViewSnapshotDetails +} + +type ViewSnapshotDetails struct { TableName string Query string Filter string diff --git a/internal/dynamo-browse/providers/workspacestore/resultsetsnapshot.go b/internal/dynamo-browse/providers/workspacestore/resultsetsnapshot.go index 0867b24..6639e17 100644 --- a/internal/dynamo-browse/providers/workspacestore/resultsetsnapshot.go +++ b/internal/dynamo-browse/providers/workspacestore/resultsetsnapshot.go @@ -24,7 +24,6 @@ func (s *ResultSetSnapshotStore) Save(rs *serialisable.ViewSnapshot) error { if err := s.ws.Save(rs); err != nil { return errors.Wrap(err, "cannot save result set") } - log.Printf("saved result set: table='%v', query='%v', filter='%v'", rs.TableName, rs.Query, rs.Filter) return nil } @@ -142,3 +141,7 @@ func (s *ResultSetSnapshotStore) Remove(resultSetId int64) error { } return nil } + +func (s *ResultSetSnapshotStore) Len() (int, error) { + return s.ws.Count(&serialisable.ViewSnapshot{}) +} diff --git a/internal/dynamo-browse/services/workspaces/iface.go b/internal/dynamo-browse/services/viewsnapshot/iface.go similarity index 92% rename from internal/dynamo-browse/services/workspaces/iface.go rename to internal/dynamo-browse/services/viewsnapshot/iface.go index b2aebbe..69ec99c 100644 --- a/internal/dynamo-browse/services/workspaces/iface.go +++ b/internal/dynamo-browse/services/viewsnapshot/iface.go @@ -1,4 +1,4 @@ -package workspaces +package viewsnapshot import "github.com/lmika/audax/internal/dynamo-browse/models/serialisable" @@ -8,6 +8,7 @@ type ViewSnapshotStore interface { CurrentlyViewedSnapshot() (*serialisable.ViewSnapshot, error) SetCurrentlyViewedSnapshot(resultSetId int64) error Find(resultSetID int64) (*serialisable.ViewSnapshot, error) + Len() (int, error) Head() (*serialisable.ViewSnapshot, error) Remove(resultSetId int64) error Dehead(fromNode *serialisable.ViewSnapshot) error diff --git a/internal/dynamo-browse/services/workspaces/service.go b/internal/dynamo-browse/services/viewsnapshot/service.go similarity index 88% rename from internal/dynamo-browse/services/workspaces/service.go rename to internal/dynamo-browse/services/viewsnapshot/service.go index ad40fe0..ebff08e 100644 --- a/internal/dynamo-browse/services/workspaces/service.go +++ b/internal/dynamo-browse/services/viewsnapshot/service.go @@ -1,7 +1,6 @@ -package workspaces +package viewsnapshot import ( - "github.com/lmika/audax/internal/dynamo-browse/models" "github.com/lmika/audax/internal/dynamo-browse/models/serialisable" "github.com/pkg/errors" "time" @@ -17,21 +16,22 @@ func NewService(store ViewSnapshotStore) *ViewSnapshotService { } } -func (s *ViewSnapshotService) PushSnapshot(rs *models.ResultSet, filter string) error { +func (s *ViewSnapshotService) PushSnapshot(details serialisable.ViewSnapshotDetails) error { newSnapshot := &serialisable.ViewSnapshot{ - Time: time.Now(), - TableName: rs.TableInfo.Name, + Time: time.Now(), + Details: details, } - if q := rs.Query; q != nil { - newSnapshot.Query = q.String() - } - newSnapshot.Filter = filter oldHead, err := s.store.CurrentlyViewedSnapshot() if err != nil { return errors.Wrap(err, "cannot get snapshot head") } + if oldHead != nil && oldHead.Details == details { + // Attempting to push a duplicate + return nil + } + if oldHead != nil { newSnapshot.BackLink = oldHead.ID @@ -62,6 +62,10 @@ func (s *ViewSnapshotService) PushSnapshot(rs *models.ResultSet, filter string) return nil } +func (s *ViewSnapshotService) Len() (int, error) { + return s.store.Len() +} + func (s *ViewSnapshotService) ViewRestore() (*serialisable.ViewSnapshot, error) { vs, err := s.store.CurrentlyViewedSnapshot() if err != nil { diff --git a/internal/dynamo-browse/services/viewsnapshot/service_test.go b/internal/dynamo-browse/services/viewsnapshot/service_test.go new file mode 100644 index 0000000..ec06439 --- /dev/null +++ b/internal/dynamo-browse/services/viewsnapshot/service_test.go @@ -0,0 +1,53 @@ +package viewsnapshot_test + +import ( + "github.com/lmika/audax/internal/dynamo-browse/models/serialisable" + "github.com/lmika/audax/internal/dynamo-browse/providers/workspacestore" + "github.com/lmika/audax/internal/dynamo-browse/services/viewsnapshot" + "github.com/lmika/audax/test/testworkspace" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestViewSnapshotService_PushSnapshot(t *testing.T) { + t.Run("should not push duplicate snapshots", func(t *testing.T) { + ws := testworkspace.New(t) + + service := viewsnapshot.NewService(workspacestore.NewResultSetSnapshotStore(ws)) + + // Push some snapshots + err := service.PushSnapshot(serialisable.ViewSnapshotDetails{ + TableName: "normal-table", + Query: "pk = 'abc'", + Filter: "", + }) + assert.NoError(t, err) + + cnt, err := service.Len() + assert.NoError(t, err) + assert.Equal(t, 1, cnt) + + err = service.PushSnapshot(serialisable.ViewSnapshotDetails{ + TableName: "abnormal-table", + Query: "pk = 'abc'", + Filter: "fla", + }) + assert.NoError(t, err) + + cnt, err = service.Len() + assert.NoError(t, err) + assert.Equal(t, 2, cnt) + + // Push a duplicate + err = service.PushSnapshot(serialisable.ViewSnapshotDetails{ + TableName: "abnormal-table", + Query: "pk = 'abc'", + Filter: "fla", + }) + assert.NoError(t, err) + + cnt, err = service.Len() + assert.NoError(t, err) + assert.Equal(t, 2, cnt) + }) +} diff --git a/internal/dynamo-browse/ui/teamodels/colselector/colmodel.go b/internal/dynamo-browse/ui/teamodels/colselector/colmodel.go index eaeb984..7a6f2ae 100644 --- a/internal/dynamo-browse/ui/teamodels/colselector/colmodel.go +++ b/internal/dynamo-browse/ui/teamodels/colselector/colmodel.go @@ -10,6 +10,7 @@ import ( "github.com/lmika/audax/internal/dynamo-browse/ui/keybindings" "github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/layout" table "github.com/lmika/go-bubble-table" + "log" "strings" ) @@ -46,6 +47,12 @@ func (c *colListModel) Init() tea.Cmd { func (m *colListModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { + case controllers.SetSelectedColumnInColSelector: + // HACK: this needs to work for all cases + log.Printf("%d == %d?", int(msg), m.table.Cursor()+1) + if int(msg) == m.table.Cursor()+1 { + m.table.GoDown() + } case tea.KeyMsg: switch { // Column operations diff --git a/internal/dynamo-browse/ui/teamodels/colselector/model.go b/internal/dynamo-browse/ui/teamodels/colselector/model.go index 087546c..8d9d805 100644 --- a/internal/dynamo-browse/ui/teamodels/colselector/model.go +++ b/internal/dynamo-browse/ui/teamodels/colselector/model.go @@ -49,6 +49,8 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case controllers.ColumnsUpdated: m.colListModel.refreshTable() m.subModel = cc.Collect(m.subModel.Update(msg)).(tea.Model) + case controllers.SetSelectedColumnInColSelector: + m.compositor = cc.Collect(m.compositor.Update(msg)).(*layout.Compositor) case tea.KeyMsg: m.compositor = cc.Collect(m.compositor.Update(msg)).(*layout.Compositor) default: diff --git a/test/testworkspace/workspace.go b/test/testworkspace/workspace.go new file mode 100644 index 0000000..d3f6f34 --- /dev/null +++ b/test/testworkspace/workspace.go @@ -0,0 +1,35 @@ +package testworkspace + +import ( + "github.com/lmika/audax/internal/common/workspaces" + "github.com/stretchr/testify/assert" + "os" + "testing" +) + +func New(t *testing.T) *workspaces.Workspace { + wsTempFile := tempFile(t) + + wsManager := workspaces.New(workspaces.MetaInfo{Command: "dynamo-browse"}) + ws, err := wsManager.Open(wsTempFile) + if err != nil { + t.Fatalf("cannot create workspace manager: %v", err) + } + t.Cleanup(func() { ws.Close() }) + + return ws +} + +func tempFile(t *testing.T) string { + t.Helper() + + tempFile, err := os.CreateTemp("", "export.csv") + assert.NoError(t, err) + tempFile.Close() + + t.Cleanup(func() { + os.Remove(tempFile.Name()) + }) + + return tempFile.Name() +}