backstack: an initial complete version of the backstack

This needs a lot of work, and a fair bit of refactoring.
This commit is contained in:
Leon Mika 2022-08-13 11:42:21 +10:00
parent 721d3abe5e
commit ec9ac34d26
7 changed files with 195 additions and 72 deletions

View file

@ -16,7 +16,7 @@ import (
type TableReadController struct { type TableReadController struct {
tableService TableReadService tableService TableReadService
workspaceService *workspaces.Service workspaceService *workspaces.ViewSnapshotService
tableName string tableName string
// state // state
@ -26,7 +26,7 @@ type TableReadController struct {
//filter string //filter string
} }
func NewTableReadController(state *State, tableService TableReadService, workspaceService *workspaces.Service, tableName string) *TableReadController { func NewTableReadController(state *State, tableService TableReadService, workspaceService *workspaces.ViewSnapshotService, tableName string) *TableReadController {
return &TableReadController{ return &TableReadController{
state: state, state: state,
tableService: tableService, tableService: tableService,
@ -84,65 +84,107 @@ func (c *TableReadController) PromptForQuery() tea.Cmd {
return events.PromptForInputMsg{ return events.PromptForInputMsg{
Prompt: "query: ", Prompt: "query: ",
OnDone: func(value string) tea.Cmd { OnDone: func(value string) tea.Cmd {
if value == "" { return func() tea.Msg {
return func() tea.Msg { return c.runQuery(c.state.ResultSet().TableInfo, value, "", true)
resultSet := c.state.ResultSet() }
return c.doScan(context.Background(), resultSet, nil)
/*
if value == "" {
return func() tea.Msg {
resultSet := c.state.ResultSet()
return c.doScan(context.Background(), resultSet, nil)
}
} }
}
expr, err := queryexpr.Parse(value) expr, err := queryexpr.Parse(value)
if err != nil {
return events.SetError(err)
}
return c.doIfNoneDirty(func() tea.Msg {
resultSet := c.state.ResultSet()
newResultSet, err := c.tableService.ScanOrQuery(context.Background(), resultSet.TableInfo, expr)
if err != nil { if err != nil {
return events.Error(err) return events.SetError(err)
} }
if err := c.workspaceService.PushSnapshot(resultSet); err != nil { return c.doIfNoneDirty(func() tea.Msg {
log.Printf("cannot push snapshot: %v", err) resultSet := c.state.ResultSet()
} newResultSet, err := c.tableService.ScanOrQuery(context.Background(), resultSet.TableInfo, expr)
return c.setResultSetAndFilter(newResultSet, "") if err != nil {
}) return events.Error(err)
}
if err := c.workspaceService.PushSnapshot(resultSet, ""); err != nil {
log.Printf("cannot push snapshot: %v", err)
}
return c.setResultSetAndFilter(newResultSet, "")
})
*/
}, },
} }
} }
} }
func (c *TableReadController) doIfNoneDirty(cmd tea.Cmd) tea.Cmd { func (c *TableReadController) runQuery(tableInfo *models.TableInfo, query, newFilter string, pushSnapshot bool) tea.Msg {
if query == "" {
newResultSet, err := c.tableService.ScanOrQuery(context.Background(), tableInfo, nil)
if err != nil {
return events.Error(err)
}
if newFilter != "" {
newResultSet = c.tableService.Filter(newResultSet, newFilter)
}
return c.setResultSetAndFilter(newResultSet, newFilter)
}
expr, err := queryexpr.Parse(query)
if err != nil {
return events.SetError(err)
}
return c.doIfNoneDirty(func() tea.Msg {
resultSet := c.state.ResultSet()
newResultSet, err := c.tableService.ScanOrQuery(context.Background(), tableInfo, expr)
if err != nil {
return events.Error(err)
}
if pushSnapshot {
if err := c.workspaceService.PushSnapshot(resultSet, c.state.Filter()); err != nil {
log.Printf("cannot push snapshot: %v", err)
}
}
if newFilter != "" {
newResultSet = c.tableService.Filter(newResultSet, newFilter)
}
return c.setResultSetAndFilter(newResultSet, newFilter)
})
}
func (c *TableReadController) doIfNoneDirty(cmd tea.Cmd) tea.Msg {
var anyDirty = false var anyDirty = false
for i := 0; i < len(c.state.ResultSet().Items()); i++ { for i := 0; i < len(c.state.ResultSet().Items()); i++ {
anyDirty = anyDirty || c.state.ResultSet().IsDirty(i) anyDirty = anyDirty || c.state.ResultSet().IsDirty(i)
} }
if !anyDirty { if !anyDirty {
return cmd return cmd()
} }
return func() tea.Msg { return events.PromptForInputMsg{
return events.PromptForInputMsg{ Prompt: "reset modified items? ",
Prompt: "reset modified items? ", OnDone: func(value string) tea.Cmd {
OnDone: func(value string) tea.Cmd { if value != "y" {
if value != "y" { return events.SetStatus("operation aborted")
return events.SetStatus("operation aborted") }
}
return cmd return cmd
}, },
}
} }
} }
func (c *TableReadController) Rescan() tea.Cmd { func (c *TableReadController) Rescan() tea.Cmd {
return c.doIfNoneDirty(func() tea.Msg { return func() tea.Msg {
resultSet := c.state.ResultSet() return c.doIfNoneDirty(func() tea.Msg {
return c.doScan(context.Background(), resultSet, resultSet.Query) resultSet := c.state.ResultSet()
}) return c.doScan(context.Background(), resultSet, resultSet.Query)
})
}
} }
func (c *TableReadController) ExportCSV(filename string) tea.Cmd { func (c *TableReadController) ExportCSV(filename string) tea.Cmd {
@ -186,6 +228,7 @@ func (c *TableReadController) doScan(ctx context.Context, resultSet *models.Resu
return events.Error(err) return events.Error(err)
} }
c.workspaceService.PushSnapshot(resultSet, c.state.Filter())
newResultSet = c.tableService.Filter(newResultSet, c.state.Filter()) newResultSet = c.tableService.Filter(newResultSet, c.state.Filter())
return c.setResultSetAndFilter(newResultSet, c.state.Filter()) return c.setResultSetAndFilter(newResultSet, c.state.Filter())
@ -214,6 +257,8 @@ func (c *TableReadController) Filter() tea.Cmd {
OnDone: func(value string) tea.Cmd { OnDone: func(value string) tea.Cmd {
return func() tea.Msg { return func() tea.Msg {
resultSet := c.state.ResultSet() resultSet := c.state.ResultSet()
c.workspaceService.PushSnapshot(resultSet, c.state.Filter())
newResultSet := c.tableService.Filter(resultSet, value) newResultSet := c.tableService.Filter(resultSet, value)
return c.setResultSetAndFilter(newResultSet, value) return c.setResultSetAndFilter(newResultSet, value)
@ -222,3 +267,40 @@ func (c *TableReadController) Filter() tea.Cmd {
} }
} }
} }
func (c *TableReadController) ViewBack() tea.Cmd {
return func() tea.Msg {
viewSnapshot, err := c.workspaceService.PopSnapshot()
if err != nil {
return events.Error(err)
} else if viewSnapshot == nil {
return events.StatusMsg("Backstack is empty")
}
currentResultSet := c.state.ResultSet()
var currentQueryExpr string
if currentResultSet.Query != nil {
currentQueryExpr = currentResultSet.Query.String()
}
if viewSnapshot.TableName == currentResultSet.TableInfo.Name && viewSnapshot.Query == currentQueryExpr {
log.Printf("backstack: setting filter to '%v'", viewSnapshot.Filter)
newResultSet := c.tableService.Filter(currentResultSet, viewSnapshot.Filter)
return c.setResultSetAndFilter(newResultSet, viewSnapshot.Filter)
}
tableInfo := currentResultSet.TableInfo
if viewSnapshot.TableName != currentResultSet.TableInfo.Name {
tableInfo, err = c.tableService.Describe(context.Background(), viewSnapshot.TableName)
if err != nil {
return events.Error(err)
}
}
log.Printf("backstack: running query: table = '%v', query = '%v', filter = '%v'",
tableInfo.Name, viewSnapshot.Query, viewSnapshot.Filter)
return c.runQuery(tableInfo, viewSnapshot.Query, viewSnapshot.Filter, false)
}
}

View file

@ -1,19 +0,0 @@
package serialisable
import (
"github.com/lmika/audax/internal/dynamo-browse/models"
"time"
)
type ResultSetSnapshot struct {
ID int64 `storm:"id,increment"`
BackLink int64 `storm:"index"`
Time time.Time
TableInfo *models.TableInfo
Query Query
Filter string
}
type Query struct {
Expression string
}

View file

@ -0,0 +1,14 @@
package serialisable
import (
"time"
)
type ViewSnapshot struct {
ID int64 `storm:"id,increment"`
BackLink int64 `storm:"index"`
Time time.Time
TableName string
Query string
Filter string
}

View file

@ -20,15 +20,22 @@ func NewResultSetSnapshotStore(ws *workspaces.Workspace) *ResultSetSnapshotStore
} }
} }
func (s *ResultSetSnapshotStore) Save(rs *serialisable.ResultSetSnapshot) error { func (s *ResultSetSnapshotStore) Save(rs *serialisable.ViewSnapshot) error {
if err := s.ws.Save(rs); err != nil { if err := s.ws.Save(rs); err != nil {
return errors.Wrap(err, "cannot save result set") return errors.Wrap(err, "cannot save result set")
} }
log.Printf("saved result set") log.Printf("saved result set: table='%v', query='%v', filter='%v'", rs.TableName, rs.Query, rs.Filter)
return nil return nil
} }
func (s *ResultSetSnapshotStore) SetAsHead(resultSetID int64) error { func (s *ResultSetSnapshotStore) SetAsHead(resultSetID int64) error {
if resultSetID == 0 {
if err := s.ws.Delete("head", "id"); err != nil {
return errors.Wrap(err, "cannot remove head")
}
return nil
}
if err := s.ws.Set("head", "id", resultSetID); err != nil { if err := s.ws.Set("head", "id", resultSetID); err != nil {
return errors.Wrap(err, "cannot set as head") return errors.Wrap(err, "cannot set as head")
} }
@ -36,13 +43,13 @@ func (s *ResultSetSnapshotStore) SetAsHead(resultSetID int64) error {
return nil return nil
} }
func (s *ResultSetSnapshotStore) Head() (*serialisable.ResultSetSnapshot, error) { func (s *ResultSetSnapshotStore) Head() (*serialisable.ViewSnapshot, error) {
var headResultSetID int64 var headResultSetID int64
if err := s.ws.Get("head", "id", &headResultSetID); err != nil && !errors.Is(err, storm.ErrNotFound) { if err := s.ws.Get("head", "id", &headResultSetID); err != nil && !errors.Is(err, storm.ErrNotFound) {
return nil, errors.Wrap(err, "cannot get head") return nil, errors.Wrap(err, "cannot get head")
} }
var rss serialisable.ResultSetSnapshot var rss serialisable.ViewSnapshot
if err := s.ws.One("ID", headResultSetID, &rss); err != nil { if err := s.ws.One("ID", headResultSetID, &rss); err != nil {
if errors.Is(err, storm.ErrNotFound) { if errors.Is(err, storm.ErrNotFound) {
return nil, nil return nil, nil
@ -53,3 +60,19 @@ func (s *ResultSetSnapshotStore) Head() (*serialisable.ResultSetSnapshot, error)
return &rss, nil return &rss, nil
} }
func (s *ResultSetSnapshotStore) Remove(resultSetId int64) error {
var rss serialisable.ViewSnapshot
if err := s.ws.One("ID", resultSetId, &rss); err != nil {
if errors.Is(err, storm.ErrNotFound) {
return nil
} else {
return errors.Wrapf(err, "cannot get snapshot with ID %v", resultSetId)
}
}
if err := s.ws.DeleteStruct(&rss); err != nil {
return errors.Wrap(err, "cannot delete snapshot")
}
return nil
}

View file

@ -2,8 +2,9 @@ package workspaces
import "github.com/lmika/audax/internal/dynamo-browse/models/serialisable" import "github.com/lmika/audax/internal/dynamo-browse/models/serialisable"
type ResultSetSnapshotStore interface { type ViewSnapshotStore interface {
Save(rs *serialisable.ResultSetSnapshot) error Save(rs *serialisable.ViewSnapshot) error
SetAsHead(resultSetId int64) error SetAsHead(resultSetId int64) error
Head() (*serialisable.ResultSetSnapshot, error) Head() (*serialisable.ViewSnapshot, error)
Remove(resultSetId int64) error
} }

View file

@ -7,24 +7,25 @@ import (
"time" "time"
) )
type Service struct { type ViewSnapshotService struct {
store ResultSetSnapshotStore store ViewSnapshotStore
} }
func NewService(store ResultSetSnapshotStore) *Service { func NewService(store ViewSnapshotStore) *ViewSnapshotService {
return &Service{ return &ViewSnapshotService{
store: store, store: store,
} }
} }
func (s *Service) PushSnapshot(rs *models.ResultSet) error { func (s *ViewSnapshotService) PushSnapshot(rs *models.ResultSet, filter string) error {
newSnapshot := &serialisable.ResultSetSnapshot{ newSnapshot := &serialisable.ViewSnapshot{
Time: time.Now(), Time: time.Now(),
TableInfo: rs.TableInfo, TableName: rs.TableInfo.Name,
} }
if q := rs.Query; q != nil { if q := rs.Query; q != nil {
newSnapshot.Query.Expression = q.String() newSnapshot.Query = q.String()
} }
newSnapshot.Filter = filter
if head, err := s.store.Head(); head != nil { if head, err := s.store.Head(); head != nil {
newSnapshot.BackLink = head.ID newSnapshot.BackLink = head.ID
@ -41,3 +42,22 @@ func (s *Service) PushSnapshot(rs *models.ResultSet) error {
return nil return nil
} }
func (s *ViewSnapshotService) PopSnapshot() (*serialisable.ViewSnapshot, error) {
vs, err := s.store.Head()
if err != nil {
return nil, errors.Wrap(err, "cannot get snapshot head")
}
if vs == nil {
return vs, 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")
}
return vs, nil
}

View file

@ -149,6 +149,8 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, m.tableReadController.PromptForQuery() return m, m.tableReadController.PromptForQuery()
case "/": case "/":
return m, m.tableReadController.Filter() return m, m.tableReadController.Filter()
case "backspace":
return m, m.tableReadController.ViewBack()
//case "e": //case "e":
// m.itemEdit.Visible() // m.itemEdit.Visible()
// return m, nil // return m, nil