issue-8: added going forward in backstack and restoring last view

This commit is contained in:
Leon Mika 2022-08-23 22:32:27 +10:00
parent 1109f2c9ee
commit 4c187ebb4d
9 changed files with 204 additions and 50 deletions

View file

@ -77,7 +77,7 @@ func main() {
itemRendererService := itemrenderer.NewService(uiStyles.ItemView.FieldType, uiStyles.ItemView.MetaInfo)
state := controllers.NewState()
tableReadController := controllers.NewTableReadController(state, tableService, workspaceService, itemRendererService, *flagTable)
tableReadController := controllers.NewTableReadController(state, tableService, workspaceService, itemRendererService, *flagTable, true)
tableWriteController := controllers.NewTableWriteController(state, tableService, tableReadController)
commandController := commandctrl.NewCommandController()

View file

@ -8,6 +8,7 @@ import (
"github.com/lmika/audax/internal/common/ui/events"
"github.com/lmika/audax/internal/dynamo-browse/models"
"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/pkg/errors"
@ -23,6 +24,7 @@ type TableReadController struct {
workspaceService *workspaces.ViewSnapshotService
itemRendererService *itemrenderer.Service
tableName string
loadFromLastView bool
// state
mutex *sync.Mutex
@ -36,6 +38,7 @@ func NewTableReadController(
workspaceService *workspaces.ViewSnapshotService,
itemRendererService *itemrenderer.Service,
tableName string,
loadFromLastView bool,
) *TableReadController {
return &TableReadController{
state: state,
@ -49,6 +52,13 @@ func NewTableReadController(
// Init does an initial scan of the table. If no table is specified, it prompts for a table, then does a scan.
func (c *TableReadController) Init() tea.Msg {
// Restore previous view
if c.loadFromLastView {
if vs, err := c.workspaceService.ViewRestore(); err == nil && vs != nil {
return c.updateViewToSnapshot(vs)
}
}
if c.tableName == "" {
return c.ListTables()
} else {
@ -234,15 +244,39 @@ func (c *TableReadController) Filter() tea.Msg {
}
func (c *TableReadController) ViewBack() tea.Msg {
viewSnapshot, err := c.workspaceService.PopSnapshot()
viewSnapshot, err := c.workspaceService.ViewBack()
if err != nil {
return events.Error(err)
} else if viewSnapshot == nil {
return events.StatusMsg("Backstack is empty")
}
return c.updateViewToSnapshot(viewSnapshot)
}
func (c *TableReadController) ViewForward() tea.Msg {
viewSnapshot, err := c.workspaceService.ViewForward()
if err != nil {
return events.Error(err)
} else if viewSnapshot == nil {
return events.StatusMsg("At top of view stack")
}
return c.updateViewToSnapshot(viewSnapshot)
}
func (c *TableReadController) updateViewToSnapshot(viewSnapshot *serialisable.ViewSnapshot) tea.Msg {
var err error
currentResultSet := c.state.ResultSet()
if currentResultSet == nil {
tableInfo, err := c.tableService.Describe(context.Background(), viewSnapshot.TableName)
if err != nil {
return events.Error(err)
}
return c.runQuery(tableInfo, viewSnapshot.Query, viewSnapshot.Filter, false)
}
var currentQueryExpr string
if currentResultSet.Query != nil {
currentQueryExpr = currentResultSet.Query.String()

View file

@ -29,7 +29,7 @@ func TestTableReadController_InitTable(t *testing.T) {
service := tables.NewService(provider)
t.Run("should prompt for table if no table name provided", func(t *testing.T) {
readController := controllers.NewTableReadController(controllers.NewState(), service, workspaceService, itemRendererService, "")
readController := controllers.NewTableReadController(controllers.NewState(), service, workspaceService, itemRendererService, "", false)
event := readController.Init()
@ -37,7 +37,7 @@ func TestTableReadController_InitTable(t *testing.T) {
})
t.Run("should scan table if table name provided", func(t *testing.T) {
readController := controllers.NewTableReadController(controllers.NewState(), service, workspaceService, itemRendererService, "")
readController := controllers.NewTableReadController(controllers.NewState(), service, workspaceService, itemRendererService, "", false)
event := readController.Init()
@ -54,7 +54,7 @@ func TestTableReadController_ListTables(t *testing.T) {
provider := dynamo.NewProvider(client)
service := tables.NewService(provider)
readController := controllers.NewTableReadController(controllers.NewState(), service, workspaceService, itemRendererService, "")
readController := controllers.NewTableReadController(controllers.NewState(), service, workspaceService, itemRendererService, "", false)
t.Run("returns a list of tables", func(t *testing.T) {
event := readController.ListTables().(controllers.PromptForTableMsg)
@ -80,7 +80,7 @@ func TestTableReadController_Rescan(t *testing.T) {
provider := dynamo.NewProvider(client)
service := tables.NewService(provider)
state := controllers.NewState()
readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, "bravo-table")
readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, "bravo-table", false)
t.Run("should perform a rescan", func(t *testing.T) {
invokeCommand(t, readController.Init())
@ -117,7 +117,7 @@ func TestTableReadController_ExportCSV(t *testing.T) {
provider := dynamo.NewProvider(client)
service := tables.NewService(provider)
readController := controllers.NewTableReadController(controllers.NewState(), service, workspaceService, itemRendererService, "bravo-table")
readController := controllers.NewTableReadController(controllers.NewState(), service, workspaceService, itemRendererService, "bravo-table", false)
t.Run("should export result set to CSV file", func(t *testing.T) {
tempFile := tempFile(t)
@ -138,7 +138,7 @@ func TestTableReadController_ExportCSV(t *testing.T) {
t.Run("should return error if result set is not set", func(t *testing.T) {
tempFile := tempFile(t)
readController := controllers.NewTableReadController(controllers.NewState(), service, workspaceService, itemRendererService, "non-existant-table")
readController := controllers.NewTableReadController(controllers.NewState(), service, workspaceService, itemRendererService, "non-existant-table", false)
invokeCommandExpectingError(t, readController.Init())
invokeCommandExpectingError(t, readController.ExportCSV(tempFile))
@ -156,7 +156,7 @@ func TestTableReadController_Query(t *testing.T) {
provider := dynamo.NewProvider(client)
service := tables.NewService(provider)
readController := controllers.NewTableReadController(controllers.NewState(), service, workspaceService, itemRendererService, "bravo-table")
readController := controllers.NewTableReadController(controllers.NewState(), service, workspaceService, itemRendererService, "bravo-table", false)
t.Run("should run scan with filter based on user query", func(t *testing.T) {
tempFile := tempFile(t)
@ -176,7 +176,7 @@ func TestTableReadController_Query(t *testing.T) {
t.Run("should return error if result set is not set", func(t *testing.T) {
tempFile := tempFile(t)
readController := controllers.NewTableReadController(controllers.NewState(), service, workspaceService, itemRendererService, "non-existant-table")
readController := controllers.NewTableReadController(controllers.NewState(), service, workspaceService, itemRendererService, "non-existant-table", false)
invokeCommandExpectingError(t, readController.Init())
invokeCommandExpectingError(t, readController.ExportCSV(tempFile))

View file

@ -27,7 +27,7 @@ func TestTableWriteController_NewItem(t *testing.T) {
service := tables.NewService(provider)
state := controllers.NewState()
readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, "alpha-table")
readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, "alpha-table", false)
writeController := controllers.NewTableWriteController(state, service, readController)
invokeCommand(t, readController.Init())
@ -91,7 +91,7 @@ func TestTableWriteController_SetAttributeValue(t *testing.T) {
service := tables.NewService(provider)
state := controllers.NewState()
readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, "alpha-table")
readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, "alpha-table", false)
writeController := controllers.NewTableWriteController(state, service, readController)
invokeCommand(t, readController.Init())
@ -111,7 +111,7 @@ func TestTableWriteController_SetAttributeValue(t *testing.T) {
service := tables.NewService(provider)
state := controllers.NewState()
readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, "alpha-table")
readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, "alpha-table", false)
writeController := controllers.NewTableWriteController(state, service, readController)
invokeCommand(t, readController.Init())
@ -165,7 +165,7 @@ func TestTableWriteController_SetAttributeValue(t *testing.T) {
for _, scenario := range scenarios {
t.Run(fmt.Sprintf("should change the value of a field to type %v", scenario.attrType), func(t *testing.T) {
state := controllers.NewState()
readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, "alpha-table")
readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, "alpha-table", false)
writeController := controllers.NewTableWriteController(state, service, readController)
invokeCommand(t, readController.Init())
@ -186,7 +186,7 @@ func TestTableWriteController_SetAttributeValue(t *testing.T) {
t.Run(fmt.Sprintf("should change value of nested field to type %v", scenario.attrType), func(t *testing.T) {
state := controllers.NewState()
readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, "alpha-table")
readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, "alpha-table", false)
writeController := controllers.NewTableWriteController(state, service, readController)
invokeCommand(t, readController.Init())
@ -225,7 +225,7 @@ func TestTableWriteController_DeleteAttribute(t *testing.T) {
t.Run("should delete top level attribute", func(t *testing.T) {
state := controllers.NewState()
readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, "alpha-table")
readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, "alpha-table", false)
writeController := controllers.NewTableWriteController(state, service, readController)
invokeCommand(t, readController.Init())
@ -241,7 +241,7 @@ func TestTableWriteController_DeleteAttribute(t *testing.T) {
t.Run("should delete attribute of map", func(t *testing.T) {
state := controllers.NewState()
readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, "alpha-table")
readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, "alpha-table", false)
writeController := controllers.NewTableWriteController(state, service, readController)
invokeCommand(t, readController.Init())
@ -273,7 +273,7 @@ func TestTableWriteController_PutItem(t *testing.T) {
service := tables.NewService(provider)
state := controllers.NewState()
readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, "alpha-table")
readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, "alpha-table", false)
writeController := controllers.NewTableWriteController(state, service, readController)
// Read the table
@ -300,7 +300,7 @@ func TestTableWriteController_PutItem(t *testing.T) {
service := tables.NewService(provider)
state := controllers.NewState()
readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, "alpha-table")
readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, "alpha-table", false)
writeController := controllers.NewTableWriteController(state, service, readController)
// Read the table
@ -331,7 +331,7 @@ func TestTableWriteController_PutItem(t *testing.T) {
service := tables.NewService(provider)
state := controllers.NewState()
readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, "alpha-table")
readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, "alpha-table", false)
writeController := controllers.NewTableWriteController(state, service, readController)
// Read the table
@ -356,7 +356,7 @@ func TestTableWriteController_PutItems(t *testing.T) {
service := tables.NewService(provider)
state := controllers.NewState()
readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, "alpha-table")
readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, "alpha-table", false)
writeController := controllers.NewTableWriteController(state, service, readController)
invokeCommand(t, readController.Init())
@ -384,7 +384,7 @@ func TestTableWriteController_PutItems(t *testing.T) {
service := tables.NewService(provider)
state := controllers.NewState()
readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, "alpha-table")
readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, "alpha-table", false)
writeController := controllers.NewTableWriteController(state, service, readController)
invokeCommand(t, readController.Init())
@ -420,7 +420,7 @@ func TestTableWriteController_PutItems(t *testing.T) {
service := tables.NewService(provider)
state := controllers.NewState()
readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, "alpha-table")
readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, "alpha-table", false)
writeController := controllers.NewTableWriteController(state, service, readController)
invokeCommand(t, readController.Init())
@ -462,7 +462,7 @@ func TestTableWriteController_TouchItem(t *testing.T) {
service := tables.NewService(provider)
state := controllers.NewState()
readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, "alpha-table")
readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, "alpha-table", false)
writeController := controllers.NewTableWriteController(state, service, readController)
// Read the table
@ -488,7 +488,7 @@ func TestTableWriteController_TouchItem(t *testing.T) {
service := tables.NewService(provider)
state := controllers.NewState()
readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, "alpha-table")
readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, "alpha-table", false)
writeController := controllers.NewTableWriteController(state, service, readController)
// Read the table
@ -515,7 +515,7 @@ func TestTableWriteController_NoisyTouchItem(t *testing.T) {
service := tables.NewService(provider)
state := controllers.NewState()
readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, "alpha-table")
readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, "alpha-table", false)
writeController := controllers.NewTableWriteController(state, service, readController)
// Read the table
@ -541,7 +541,7 @@ func TestTableWriteController_NoisyTouchItem(t *testing.T) {
service := tables.NewService(provider)
state := controllers.NewState()
readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, "alpha-table")
readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, "alpha-table", false)
writeController := controllers.NewTableWriteController(state, service, readController)
// Read the table

View file

@ -7,6 +7,7 @@ import (
type ViewSnapshot struct {
ID int64 `storm:"id,increment"`
BackLink int64 `storm:"index"`
ForeLink int64 `storm:"index"`
Time time.Time
TableName string
Query string

View file

@ -43,6 +43,55 @@ func (s *ResultSetSnapshotStore) SetAsHead(resultSetID int64) error {
return nil
}
func (s *ResultSetSnapshotStore) CurrentlyViewedSnapshot() (*serialisable.ViewSnapshot, error) {
var resultSetID int64
if err := s.ws.Get("viewIds", "current", &resultSetID); err != nil {
if errors.Is(err, storm.ErrNotFound) {
return nil, nil
}
return nil, errors.Wrap(err, "cannot get head")
}
var rss serialisable.ViewSnapshot
if err := s.ws.One("ID", resultSetID, &rss); err != nil {
if errors.Is(err, storm.ErrNotFound) {
return nil, nil
} else {
return nil, errors.Wrap(err, "cannot get head")
}
}
return &rss, nil
}
func (s *ResultSetSnapshotStore) SetCurrentlyViewedSnapshot(resultSetID int64) error {
if resultSetID == 0 {
if err := s.ws.Delete("viewIds", "current"); err != nil {
return errors.Wrap(err, "cannot remove head")
}
return nil
}
if err := s.ws.Set("viewIds", "current", resultSetID); err != nil {
return errors.Wrap(err, "cannot set as head")
}
return nil
}
func (s *ResultSetSnapshotStore) Find(resultSetID int64) (*serialisable.ViewSnapshot, error) {
var rss serialisable.ViewSnapshot
if err := s.ws.One("ID", resultSetID, &rss); err != nil {
if errors.Is(err, storm.ErrNotFound) {
return nil, nil
} else {
return nil, errors.Wrap(err, "cannot get head")
}
}
return &rss, nil
}
func (s *ResultSetSnapshotStore) Head() (*serialisable.ViewSnapshot, error) {
var headResultSetID int64
if err := s.ws.Get("head", "id", &headResultSetID); err != nil && !errors.Is(err, storm.ErrNotFound) {
@ -61,6 +110,23 @@ func (s *ResultSetSnapshotStore) Head() (*serialisable.ViewSnapshot, error) {
return &rss, nil
}
func (s *ResultSetSnapshotStore) Dehead(fromNode *serialisable.ViewSnapshot) error {
n := fromNode.ForeLink
for n != 0 {
node, err := s.Find(n)
if err != nil {
return errors.Wrapf(err, "cannot get node with ID: %v", n)
} else if node == nil {
return errors.Errorf("expected node with ID %v, but did not find it", n)
}
if err := s.Remove(node.ID); err != nil {
log.Printf("warn: cannot delete node with ID %v", node.ID)
}
n = node.ForeLink
}
return nil
}
func (s *ResultSetSnapshotStore) Remove(resultSetId int64) error {
var rss serialisable.ViewSnapshot
if err := s.ws.One("ID", resultSetId, &rss); err != nil {

View file

@ -5,6 +5,10 @@ import "github.com/lmika/audax/internal/dynamo-browse/models/serialisable"
type ViewSnapshotStore interface {
Save(rs *serialisable.ViewSnapshot) error
SetAsHead(resultSetId int64) error
CurrentlyViewedSnapshot() (*serialisable.ViewSnapshot, error)
SetCurrentlyViewedSnapshot(resultSetId int64) error
Find(resultSetID int64) (*serialisable.ViewSnapshot, error)
Head() (*serialisable.ViewSnapshot, error)
Remove(resultSetId int64) error
Dehead(fromNode *serialisable.ViewSnapshot) error
}

View file

@ -27,43 +27,89 @@ func (s *ViewSnapshotService) PushSnapshot(rs *models.ResultSet, filter string)
}
newSnapshot.Filter = filter
if head, err := s.store.Head(); head != nil {
newSnapshot.BackLink = head.ID
} else if err != nil {
return errors.Wrap(err, "cannot get head result set")
oldHead, err := s.store.CurrentlyViewedSnapshot()
if err != nil {
return errors.Wrap(err, "cannot get snapshot head")
}
if oldHead != nil {
newSnapshot.BackLink = oldHead.ID
// Remove all nodes from this point on the head
if err := s.store.Dehead(oldHead); err != nil {
return errors.Wrap(err, "cannot remove head")
}
}
if err := s.store.Save(newSnapshot); err != nil {
return errors.Wrap(err, "cannot save snapshot")
}
if oldHead != nil {
oldHead.ForeLink = newSnapshot.ID
if err := s.store.Save(oldHead); err != nil {
return errors.Wrap(err, "cannot update old head")
}
}
if err := s.store.SetAsHead(newSnapshot.ID); err != nil {
return errors.Wrap(err, "cannot set new snapshot as head")
}
if err := s.store.SetCurrentlyViewedSnapshot(newSnapshot.ID); err != nil {
return errors.Wrap(err, "cannot set new snapshot as head")
}
return nil
}
func (s *ViewSnapshotService) PopSnapshot() (*serialisable.ViewSnapshot, error) {
vs, err := s.store.Head()
func (s *ViewSnapshotService) ViewRestore() (*serialisable.ViewSnapshot, error) {
vs, err := s.store.CurrentlyViewedSnapshot()
if err != nil {
return nil, errors.Wrap(err, "cannot get snapshot head")
} else if vs == nil || vs.BackLink == 0 {
return nil, nil
}
if err := s.store.SetAsHead(vs.BackLink); err != nil {
return nil, errors.Wrap(err, "cannot set new head")
}
if err := s.store.Remove(vs.ID); err != nil {
return nil, errors.Wrap(err, "cannot remove old ID")
}
vs, err = s.store.Head()
if err != nil {
return nil, errors.Wrap(err, "cannot get snapshot head")
} else if vs == nil || vs.BackLink == 0 {
return nil, nil
}
return vs, nil
}
func (s *ViewSnapshotService) ViewBack() (*serialisable.ViewSnapshot, error) {
vs, err := s.store.CurrentlyViewedSnapshot()
if err != nil {
return nil, errors.Wrap(err, "cannot get snapshot head")
} else if vs == nil || vs.BackLink == 0 {
return nil, nil
}
vsToReturn, err := s.store.Find(vs.BackLink)
if err != nil {
return nil, errors.Wrap(err, "cannot get snapshot head")
} else if vsToReturn == nil {
return nil, nil
}
if err := s.store.SetCurrentlyViewedSnapshot(vsToReturn.ID); err != nil {
return nil, errors.Wrap(err, "cannot set new head")
}
return vsToReturn, nil
}
func (s *ViewSnapshotService) ViewForward() (*serialisable.ViewSnapshot, error) {
vs, err := s.store.CurrentlyViewedSnapshot()
if err != nil {
return nil, errors.Wrap(err, "cannot get snapshot head")
} else if vs == nil || vs.ForeLink == 0 {
return nil, nil
}
vsToReturn, err := s.store.Find(vs.ForeLink)
if err != nil {
return nil, errors.Wrap(err, "cannot get snapshot head")
} else if vsToReturn == nil {
return nil, nil
}
if err := s.store.SetCurrentlyViewedSnapshot(vsToReturn.ID); err != nil {
return nil, errors.Wrap(err, "cannot set new head")
}
return vsToReturn, nil
}

View file

@ -163,6 +163,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, m.tableView.Refresh()
case tea.KeyMsg:
if !m.statusAndPrompt.InPrompt() && !m.tableSelect.Visible() {
log.Printf("key = %+v", msg)
switch msg.String() {
case "m":
if idx := m.tableView.SelectedItemIndex(); idx >= 0 {
@ -180,6 +181,8 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, m.tableReadController.Filter
case "backspace":
return m, m.tableReadController.ViewBack
case "\\":
return m, m.tableReadController.ViewForward
case "w":
return m, func() tea.Msg {
return controllers.SetTableItemView{ViewIndex: utils.Cycle(m.mainViewIndex, 1, ViewModeCount)}