Issue 23: Added progress indicators and cancellation (#34)
- Wrapped all table operations in a new foreground job context, which mediates foreground tasks. - Added cancellation support and partial results for table read operations. - Added the "mark" command, which can mark, unmark & toggle marked items - Added support for alias arguments. - Removed the "unmark" command, and replaced it as an alias to the "marked" command - Fixed seg faults raised when there is no table shown in the result set.
This commit is contained in:
parent
982d3a9ca7
commit
79692302af
|
@ -16,6 +16,7 @@ import (
|
||||||
"github.com/lmika/audax/internal/dynamo-browse/providers/settingstore"
|
"github.com/lmika/audax/internal/dynamo-browse/providers/settingstore"
|
||||||
"github.com/lmika/audax/internal/dynamo-browse/providers/workspacestore"
|
"github.com/lmika/audax/internal/dynamo-browse/providers/workspacestore"
|
||||||
"github.com/lmika/audax/internal/dynamo-browse/services/itemrenderer"
|
"github.com/lmika/audax/internal/dynamo-browse/services/itemrenderer"
|
||||||
|
"github.com/lmika/audax/internal/dynamo-browse/services/jobs"
|
||||||
keybindings_service "github.com/lmika/audax/internal/dynamo-browse/services/keybindings"
|
keybindings_service "github.com/lmika/audax/internal/dynamo-browse/services/keybindings"
|
||||||
"github.com/lmika/audax/internal/dynamo-browse/services/tables"
|
"github.com/lmika/audax/internal/dynamo-browse/services/tables"
|
||||||
workspaces_service "github.com/lmika/audax/internal/dynamo-browse/services/workspaces"
|
workspaces_service "github.com/lmika/audax/internal/dynamo-browse/services/workspaces"
|
||||||
|
@ -94,10 +95,12 @@ func main() {
|
||||||
tableService := tables.NewService(dynamoProvider, settingStore)
|
tableService := tables.NewService(dynamoProvider, settingStore)
|
||||||
workspaceService := workspaces_service.NewService(resultSetSnapshotStore)
|
workspaceService := workspaces_service.NewService(resultSetSnapshotStore)
|
||||||
itemRendererService := itemrenderer.NewService(uiStyles.ItemView.FieldType, uiStyles.ItemView.MetaInfo)
|
itemRendererService := itemrenderer.NewService(uiStyles.ItemView.FieldType, uiStyles.ItemView.MetaInfo)
|
||||||
|
jobsService := jobs.NewService(eventBus)
|
||||||
|
|
||||||
state := controllers.NewState()
|
state := controllers.NewState()
|
||||||
tableReadController := controllers.NewTableReadController(state, tableService, workspaceService, itemRendererService, eventBus, *flagTable)
|
jobsController := controllers.NewJobsController(jobsService, eventBus, false)
|
||||||
tableWriteController := controllers.NewTableWriteController(state, tableService, tableReadController, settingStore)
|
tableReadController := controllers.NewTableReadController(state, tableService, workspaceService, itemRendererService, jobsController, eventBus, *flagTable)
|
||||||
|
tableWriteController := controllers.NewTableWriteController(state, tableService, jobsController, tableReadController, settingStore)
|
||||||
columnsController := controllers.NewColumnsController(eventBus)
|
columnsController := controllers.NewColumnsController(eventBus)
|
||||||
exportController := controllers.NewExportController(state, columnsController)
|
exportController := controllers.NewExportController(state, columnsController)
|
||||||
settingsController := controllers.NewSettingsController(settingStore)
|
settingsController := controllers.NewSettingsController(settingStore)
|
||||||
|
@ -114,6 +117,7 @@ func main() {
|
||||||
columnsController,
|
columnsController,
|
||||||
exportController,
|
exportController,
|
||||||
settingsController,
|
settingsController,
|
||||||
|
jobsController,
|
||||||
itemRendererService,
|
itemRendererService,
|
||||||
commandController,
|
commandController,
|
||||||
keyBindingController,
|
keyBindingController,
|
||||||
|
@ -122,9 +126,11 @@ func main() {
|
||||||
|
|
||||||
// Pre-determine if layout has dark background. This prevents calls for creating a list to hang.
|
// Pre-determine if layout has dark background. This prevents calls for creating a list to hang.
|
||||||
osstyle.DetectCurrentScheme()
|
osstyle.DetectCurrentScheme()
|
||||||
|
|
||||||
p := tea.NewProgram(model, tea.WithAltScreen())
|
p := tea.NewProgram(model, tea.WithAltScreen())
|
||||||
|
|
||||||
|
jobsController.SetMessageSender(p.Send)
|
||||||
|
|
||||||
log.Println("launching")
|
log.Println("launching")
|
||||||
if err := p.Start(); err != nil {
|
if err := p.Start(); err != nil {
|
||||||
fmt.Printf("Alas, there's been an error: %v", err)
|
fmt.Printf("Alas, there's been an error: %v", err)
|
||||||
|
|
|
@ -57,14 +57,20 @@ func (c *CommandController) execute(ctx ExecContext, commandInput string) tea.Ms
|
||||||
return command(ctx, tokens[1:])
|
return command(ctx, tokens[1:])
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *CommandController) Alias(commandName string) Command {
|
func (c *CommandController) Alias(commandName string, aliasArgs []string) Command {
|
||||||
return func(ctx ExecContext, args []string) tea.Msg {
|
return func(ctx ExecContext, args []string) tea.Msg {
|
||||||
command := c.lookupCommand(commandName)
|
command := c.lookupCommand(commandName)
|
||||||
if command == nil {
|
if command == nil {
|
||||||
return events.Error(errors.New("no such command: " + commandName))
|
return events.Error(errors.New("no such command: " + commandName))
|
||||||
}
|
}
|
||||||
|
|
||||||
return command(ctx, args)
|
var allArgs []string
|
||||||
|
if len(aliasArgs) > 0 {
|
||||||
|
allArgs = append(append([]string{}, aliasArgs...), args...)
|
||||||
|
} else {
|
||||||
|
allArgs = args
|
||||||
|
}
|
||||||
|
return command(ctx, allArgs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,7 +29,13 @@ func PromptForInput(prompt string, onDone func(value string) tea.Msg) tea.Msg {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Confirm(prompt string, onYes func() tea.Msg) tea.Msg {
|
func Confirm(prompt string, onResult func(yes bool) tea.Msg) tea.Msg {
|
||||||
|
return PromptForInput(prompt, func(value string) tea.Msg {
|
||||||
|
return onResult(value == "y")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func ConfirmYes(prompt string, onYes func() tea.Msg) tea.Msg {
|
||||||
return PromptForInput(prompt, func(value string) tea.Msg {
|
return PromptForInput(prompt, func(value string) tea.Msg {
|
||||||
if value == "y" {
|
if value == "y" {
|
||||||
return onYes()
|
return onYes()
|
||||||
|
|
6
internal/common/ui/events/jobs.go
Normal file
6
internal/common/ui/events/jobs.go
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
package events
|
||||||
|
|
||||||
|
type ForegroundJobUpdate struct {
|
||||||
|
JobRunning bool
|
||||||
|
JobStatus string
|
||||||
|
}
|
87
internal/dynamo-browse/controllers/jobbuilder.go
Normal file
87
internal/dynamo-browse/controllers/jobbuilder.go
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/lmika/audax/internal/common/ui/events"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewJob[T any](jc *JobsController, description string, job func(ctx context.Context) (T, error)) JobBuilder[T] {
|
||||||
|
return JobBuilder[T]{jc: jc, description: description, job: job}
|
||||||
|
}
|
||||||
|
|
||||||
|
type JobBuilder[T any] struct {
|
||||||
|
jc *JobsController
|
||||||
|
description string
|
||||||
|
job func(ctx context.Context) (T, error)
|
||||||
|
onDone func(res T) tea.Msg
|
||||||
|
onErr func(err error) tea.Msg
|
||||||
|
onEither func(res T, err error) tea.Msg
|
||||||
|
}
|
||||||
|
|
||||||
|
func (jb JobBuilder[T]) OnDone(fn func(res T) tea.Msg) JobBuilder[T] {
|
||||||
|
newJb := jb
|
||||||
|
newJb.onDone = fn
|
||||||
|
return newJb
|
||||||
|
}
|
||||||
|
|
||||||
|
func (jb JobBuilder[T]) OnErr(fn func(err error) tea.Msg) JobBuilder[T] {
|
||||||
|
newJb := jb
|
||||||
|
newJb.onErr = fn
|
||||||
|
return newJb
|
||||||
|
}
|
||||||
|
|
||||||
|
func (jb JobBuilder[T]) OnEither(fn func(res T, err error) tea.Msg) JobBuilder[T] {
|
||||||
|
newJb := jb
|
||||||
|
newJb.onEither = fn
|
||||||
|
return newJb
|
||||||
|
}
|
||||||
|
|
||||||
|
func (jb JobBuilder[T]) Submit() tea.Msg {
|
||||||
|
if jb.jc.immediate {
|
||||||
|
return jb.executeJob(context.Background())
|
||||||
|
}
|
||||||
|
return jb.doSubmit()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (jb JobBuilder[T]) executeJob(ctx context.Context) tea.Msg {
|
||||||
|
res, err := jb.job(ctx)
|
||||||
|
|
||||||
|
if jb.onEither != nil {
|
||||||
|
return jb.onEither(res, err)
|
||||||
|
} else if err == nil {
|
||||||
|
return jb.onDone(res)
|
||||||
|
} else {
|
||||||
|
if jb.onErr != nil {
|
||||||
|
return jb.onErr(err)
|
||||||
|
} else {
|
||||||
|
return events.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (jb JobBuilder[T]) doSubmit() tea.Msg {
|
||||||
|
jb.jc.service.SubmitForegroundJob(func(ctx context.Context) {
|
||||||
|
msg := jb.executeJob(ctx)
|
||||||
|
|
||||||
|
jb.jc.msgSender(msg)
|
||||||
|
|
||||||
|
if _, isForegroundJobUpdate := msg.(events.ForegroundJobUpdate); !isForegroundJobUpdate {
|
||||||
|
// Likely another job was scheduled so don't indicate that no jobs are running.
|
||||||
|
jb.jc.msgSender(events.ForegroundJobUpdate{
|
||||||
|
JobRunning: false,
|
||||||
|
JobStatus: "",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, func(msg string) {
|
||||||
|
jb.jc.msgSender(events.ForegroundJobUpdate{
|
||||||
|
JobRunning: true,
|
||||||
|
JobStatus: jb.description + " " + msg,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return events.ForegroundJobUpdate{
|
||||||
|
JobRunning: true,
|
||||||
|
JobStatus: jb.description,
|
||||||
|
}
|
||||||
|
}
|
38
internal/dynamo-browse/controllers/jobs.go
Normal file
38
internal/dynamo-browse/controllers/jobs.go
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/lmika/audax/internal/common/ui/events"
|
||||||
|
"github.com/lmika/audax/internal/dynamo-browse/services/jobs"
|
||||||
|
bus "github.com/lmika/events"
|
||||||
|
)
|
||||||
|
|
||||||
|
type JobsController struct {
|
||||||
|
service *jobs.Services
|
||||||
|
msgSender func(msg tea.Msg)
|
||||||
|
immediate bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewJobsController(service *jobs.Services, bus *bus.Bus, immediate bool) *JobsController {
|
||||||
|
jc := &JobsController{
|
||||||
|
service: service,
|
||||||
|
immediate: immediate,
|
||||||
|
}
|
||||||
|
|
||||||
|
return jc
|
||||||
|
}
|
||||||
|
|
||||||
|
func (js *JobsController) SetMessageSender(msgSender func(msg tea.Msg)) {
|
||||||
|
js.msgSender = msgSender
|
||||||
|
}
|
||||||
|
|
||||||
|
func (js *JobsController) CancelRunningJob(ifNoJobsRunning func() tea.Msg) tea.Msg {
|
||||||
|
hasCancelled := js.service.CancelForegroundJob()
|
||||||
|
if hasCancelled {
|
||||||
|
return events.ForegroundJobUpdate{
|
||||||
|
JobRunning: true,
|
||||||
|
JobStatus: "Cancelling running job…",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ifNoJobsRunning()
|
||||||
|
}
|
|
@ -27,7 +27,7 @@ func (kb *KeyBindingController) Rebind(bindingName string, newKey string, force
|
||||||
var keyAlreadyBoundErr keybindings.KeyAlreadyBoundError
|
var keyAlreadyBoundErr keybindings.KeyAlreadyBoundError
|
||||||
if errors.As(err, &keyAlreadyBoundErr) {
|
if errors.As(err, &keyAlreadyBoundErr) {
|
||||||
promptMsg := fmt.Sprintf("Key '%v' already bound to '%v'. Continue? ", keyAlreadyBoundErr.Key, keyAlreadyBoundErr.ExistingBindingName)
|
promptMsg := fmt.Sprintf("Key '%v' already bound to '%v'. Continue? ", keyAlreadyBoundErr.Key, keyAlreadyBoundErr.ExistingBindingName)
|
||||||
return events.Confirm(promptMsg, func() tea.Msg {
|
return events.ConfirmYes(promptMsg, func() tea.Msg {
|
||||||
err := kb.service.Rebind(bindingName, newKey, true)
|
err := kb.service.Rebind(bindingName, newKey, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return events.Error(err)
|
return events.Error(err)
|
||||||
|
|
|
@ -29,10 +29,19 @@ const (
|
||||||
resultSetUpdateTouch
|
resultSetUpdateTouch
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type MarkOp int
|
||||||
|
|
||||||
|
const (
|
||||||
|
MarkOpMark MarkOp = iota
|
||||||
|
MarkOpUnmark
|
||||||
|
MarkOpToggle
|
||||||
|
)
|
||||||
|
|
||||||
type TableReadController struct {
|
type TableReadController struct {
|
||||||
tableService TableReadService
|
tableService TableReadService
|
||||||
workspaceService *workspaces.ViewSnapshotService
|
workspaceService *workspaces.ViewSnapshotService
|
||||||
itemRendererService *itemrenderer.Service
|
itemRendererService *itemrenderer.Service
|
||||||
|
jobController *JobsController
|
||||||
eventBus *bus.Bus
|
eventBus *bus.Bus
|
||||||
tableName string
|
tableName string
|
||||||
loadFromLastView bool
|
loadFromLastView bool
|
||||||
|
@ -48,6 +57,7 @@ func NewTableReadController(
|
||||||
tableService TableReadService,
|
tableService TableReadService,
|
||||||
workspaceService *workspaces.ViewSnapshotService,
|
workspaceService *workspaces.ViewSnapshotService,
|
||||||
itemRendererService *itemrenderer.Service,
|
itemRendererService *itemrenderer.Service,
|
||||||
|
jobController *JobsController,
|
||||||
eventBus *bus.Bus,
|
eventBus *bus.Bus,
|
||||||
tableName string,
|
tableName string,
|
||||||
) *TableReadController {
|
) *TableReadController {
|
||||||
|
@ -56,6 +66,7 @@ func NewTableReadController(
|
||||||
tableService: tableService,
|
tableService: tableService,
|
||||||
workspaceService: workspaceService,
|
workspaceService: workspaceService,
|
||||||
itemRendererService: itemRendererService,
|
itemRendererService: itemRendererService,
|
||||||
|
jobController: jobController,
|
||||||
eventBus: eventBus,
|
eventBus: eventBus,
|
||||||
tableName: tableName,
|
tableName: tableName,
|
||||||
mutex: new(sync.Mutex),
|
mutex: new(sync.Mutex),
|
||||||
|
@ -79,57 +90,67 @@ func (c *TableReadController) Init() tea.Msg {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *TableReadController) ListTables() tea.Msg {
|
func (c *TableReadController) ListTables() tea.Msg {
|
||||||
tables, err := c.tableService.ListTables(context.Background())
|
return NewJob(c.jobController, "Listing tables…", func(ctx context.Context) (any, error) {
|
||||||
if err != nil {
|
tables, err := c.tableService.ListTables(context.Background())
|
||||||
return events.Error(err)
|
if err != nil {
|
||||||
}
|
return nil, err
|
||||||
|
}
|
||||||
|
return tables, nil
|
||||||
|
}).OnDone(func(res any) tea.Msg {
|
||||||
|
return PromptForTableMsg{
|
||||||
|
Tables: res.([]string),
|
||||||
|
OnSelected: func(tableName string) tea.Msg {
|
||||||
|
if tableName == "" {
|
||||||
|
return events.StatusMsg("No table selected")
|
||||||
|
}
|
||||||
|
|
||||||
return PromptForTableMsg{
|
return c.ScanTable(tableName)
|
||||||
Tables: tables,
|
},
|
||||||
OnSelected: func(tableName string) tea.Msg {
|
}
|
||||||
return c.ScanTable(tableName)
|
}).Submit()
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *TableReadController) ScanTable(name string) tea.Msg {
|
func (c *TableReadController) ScanTable(name string) tea.Msg {
|
||||||
ctx := context.Background()
|
return NewJob(c.jobController, "Scanning…", func(ctx context.Context) (*models.ResultSet, error) {
|
||||||
|
tableInfo, err := c.tableService.Describe(ctx, name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "cannot describe %v", c.tableName)
|
||||||
|
}
|
||||||
|
|
||||||
tableInfo, err := c.tableService.Describe(ctx, name)
|
resultSet, err := c.tableService.Scan(ctx, tableInfo)
|
||||||
if err != nil {
|
if resultSet != nil {
|
||||||
return events.Error(errors.Wrapf(err, "cannot describe %v", c.tableName))
|
resultSet = c.tableService.Filter(resultSet, c.state.Filter())
|
||||||
}
|
}
|
||||||
|
|
||||||
resultSet, err := c.tableService.Scan(ctx, tableInfo)
|
return resultSet, err
|
||||||
if err != nil {
|
}).OnEither(c.handleResultSetFromJobResult(c.state.Filter(), true, resultSetUpdateInit)).Submit()
|
||||||
return events.Error(err)
|
|
||||||
}
|
|
||||||
resultSet = c.tableService.Filter(resultSet, c.state.Filter())
|
|
||||||
|
|
||||||
return c.setResultSetAndFilter(resultSet, c.state.Filter(), true, resultSetUpdateInit)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *TableReadController) PromptForQuery() tea.Msg {
|
func (c *TableReadController) PromptForQuery() tea.Msg {
|
||||||
return events.PromptForInputMsg{
|
return events.PromptForInputMsg{
|
||||||
Prompt: "query: ",
|
Prompt: "query: ",
|
||||||
OnDone: func(value string) tea.Msg {
|
OnDone: func(value string) tea.Msg {
|
||||||
return c.runQuery(c.state.ResultSet().TableInfo, value, "", true)
|
resultSet := c.state.ResultSet()
|
||||||
|
if resultSet == nil {
|
||||||
|
return events.StatusMsg("Result-set is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.runQuery(resultSet.TableInfo, value, "", true)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *TableReadController) runQuery(tableInfo *models.TableInfo, query, newFilter string, pushSnapshot bool) tea.Msg {
|
func (c *TableReadController) runQuery(tableInfo *models.TableInfo, query, newFilter string, pushSnapshot bool) tea.Msg {
|
||||||
if query == "" {
|
if query == "" {
|
||||||
newResultSet, err := c.tableService.ScanOrQuery(context.Background(), tableInfo, nil)
|
return NewJob(c.jobController, "Scanning…", func(ctx context.Context) (*models.ResultSet, error) {
|
||||||
if err != nil {
|
newResultSet, err := c.tableService.ScanOrQuery(context.Background(), tableInfo, nil)
|
||||||
return events.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if newFilter != "" {
|
if newResultSet != nil && newFilter != "" {
|
||||||
newResultSet = c.tableService.Filter(newResultSet, newFilter)
|
newResultSet = c.tableService.Filter(newResultSet, newFilter)
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.setResultSetAndFilter(newResultSet, newFilter, pushSnapshot, resultSetUpdateQuery)
|
return newResultSet, err
|
||||||
|
}).OnEither(c.handleResultSetFromJobResult(newFilter, pushSnapshot, resultSetUpdateQuery)).Submit()
|
||||||
}
|
}
|
||||||
|
|
||||||
expr, err := queryexpr.Parse(query)
|
expr, err := queryexpr.Parse(query)
|
||||||
|
@ -138,15 +159,14 @@ func (c *TableReadController) runQuery(tableInfo *models.TableInfo, query, newFi
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.doIfNoneDirty(func() tea.Msg {
|
return c.doIfNoneDirty(func() tea.Msg {
|
||||||
newResultSet, err := c.tableService.ScanOrQuery(context.Background(), tableInfo, expr)
|
return NewJob(c.jobController, "Running query…", func(ctx context.Context) (*models.ResultSet, error) {
|
||||||
if err != nil {
|
newResultSet, err := c.tableService.ScanOrQuery(context.Background(), tableInfo, expr)
|
||||||
return events.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if newFilter != "" {
|
if newFilter != "" && newResultSet != nil {
|
||||||
newResultSet = c.tableService.Filter(newResultSet, newFilter)
|
newResultSet = c.tableService.Filter(newResultSet, newFilter)
|
||||||
}
|
}
|
||||||
return c.setResultSetAndFilter(newResultSet, newFilter, pushSnapshot, resultSetUpdateQuery)
|
return newResultSet, err
|
||||||
|
}).OnEither(c.handleResultSetFromJobResult(newFilter, pushSnapshot, resultSetUpdateQuery)).Submit()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -175,19 +195,19 @@ func (c *TableReadController) doIfNoneDirty(cmd tea.Cmd) tea.Msg {
|
||||||
func (c *TableReadController) Rescan() tea.Msg {
|
func (c *TableReadController) Rescan() tea.Msg {
|
||||||
return c.doIfNoneDirty(func() tea.Msg {
|
return c.doIfNoneDirty(func() tea.Msg {
|
||||||
resultSet := c.state.ResultSet()
|
resultSet := c.state.ResultSet()
|
||||||
return c.doScan(context.Background(), resultSet, resultSet.Query, true, resultSetUpdateRescan)
|
return c.doScan(resultSet, resultSet.Query, true, resultSetUpdateRescan)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *TableReadController) doScan(ctx context.Context, resultSet *models.ResultSet, query models.Queryable, pushBackstack bool, op resultSetUpdateOp) tea.Msg {
|
func (c *TableReadController) doScan(resultSet *models.ResultSet, query models.Queryable, pushBackstack bool, op resultSetUpdateOp) tea.Msg {
|
||||||
newResultSet, err := c.tableService.ScanOrQuery(ctx, resultSet.TableInfo, query)
|
return NewJob(c.jobController, "Rescan…", func(ctx context.Context) (*models.ResultSet, error) {
|
||||||
if err != nil {
|
newResultSet, err := c.tableService.ScanOrQuery(ctx, resultSet.TableInfo, query)
|
||||||
return events.Error(err)
|
if newResultSet != nil {
|
||||||
}
|
newResultSet = c.tableService.Filter(newResultSet, c.state.Filter())
|
||||||
|
}
|
||||||
|
|
||||||
newResultSet = c.tableService.Filter(newResultSet, c.state.Filter())
|
return newResultSet, err
|
||||||
|
}).OnEither(c.handleResultSetFromJobResult(c.state.Filter(), pushBackstack, op)).Submit()
|
||||||
return c.setResultSetAndFilter(newResultSet, c.state.Filter(), pushBackstack, op)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *TableReadController) setResultSetAndFilter(resultSet *models.ResultSet, filter string, pushBackstack bool, op resultSetUpdateOp) tea.Msg {
|
func (c *TableReadController) setResultSetAndFilter(resultSet *models.ResultSet, filter string, pushBackstack bool, op resultSetUpdateOp) tea.Msg {
|
||||||
|
@ -204,10 +224,21 @@ func (c *TableReadController) setResultSetAndFilter(resultSet *models.ResultSet,
|
||||||
return c.state.buildNewResultSetMessage("")
|
return c.state.buildNewResultSetMessage("")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *TableReadController) Unmark() tea.Msg {
|
func (c *TableReadController) Mark(op MarkOp) tea.Msg {
|
||||||
c.state.withResultSet(func(resultSet *models.ResultSet) {
|
c.state.withResultSet(func(resultSet *models.ResultSet) {
|
||||||
for i := range resultSet.Items() {
|
for i := range resultSet.Items() {
|
||||||
resultSet.SetMark(i, false)
|
if resultSet.Hidden(i) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch op {
|
||||||
|
case MarkOpMark:
|
||||||
|
resultSet.SetMark(i, true)
|
||||||
|
case MarkOpUnmark:
|
||||||
|
resultSet.SetMark(i, false)
|
||||||
|
case MarkOpToggle:
|
||||||
|
resultSet.SetMark(i, !resultSet.Marked(i))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return ResultSetUpdated{}
|
return ResultSetUpdated{}
|
||||||
|
@ -218,13 +249,37 @@ func (c *TableReadController) Filter() tea.Msg {
|
||||||
Prompt: "filter: ",
|
Prompt: "filter: ",
|
||||||
OnDone: func(value string) tea.Msg {
|
OnDone: func(value string) tea.Msg {
|
||||||
resultSet := c.state.ResultSet()
|
resultSet := c.state.ResultSet()
|
||||||
newResultSet := c.tableService.Filter(resultSet, value)
|
if resultSet == nil {
|
||||||
|
return events.StatusMsg("Result-set is nil")
|
||||||
|
}
|
||||||
|
|
||||||
return c.setResultSetAndFilter(newResultSet, value, true, resultSetUpdateFilter)
|
return NewJob(c.jobController, "Applying Filter…", func(ctx context.Context) (*models.ResultSet, error) {
|
||||||
|
newResultSet := c.tableService.Filter(resultSet, value)
|
||||||
|
return newResultSet, nil
|
||||||
|
}).OnEither(c.handleResultSetFromJobResult(value, true, resultSetUpdateFilter)).Submit()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *TableReadController) handleResultSetFromJobResult(filter string, pushbackStack bool, op resultSetUpdateOp) func(newResultSet *models.ResultSet, err error) tea.Msg {
|
||||||
|
return func(newResultSet *models.ResultSet, err error) tea.Msg {
|
||||||
|
if err == nil {
|
||||||
|
return c.setResultSetAndFilter(newResultSet, filter, pushbackStack, op)
|
||||||
|
}
|
||||||
|
|
||||||
|
var partialResultsErr models.PartialResultsError
|
||||||
|
if errors.As(err, &partialResultsErr) {
|
||||||
|
return events.Confirm(applyToN("View the ", len(newResultSet.Items()), "item", "items", " returned so far? "), func(yes bool) tea.Msg {
|
||||||
|
if yes {
|
||||||
|
return c.setResultSetAndFilter(newResultSet, filter, pushbackStack, op)
|
||||||
|
}
|
||||||
|
return events.StatusMsg("Operation cancelled")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return events.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (c *TableReadController) ViewBack() tea.Msg {
|
func (c *TableReadController) ViewBack() tea.Msg {
|
||||||
viewSnapshot, err := c.workspaceService.ViewBack()
|
viewSnapshot, err := c.workspaceService.ViewBack()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -252,11 +307,15 @@ func (c *TableReadController) updateViewToSnapshot(viewSnapshot *serialisable.Vi
|
||||||
currentResultSet := c.state.ResultSet()
|
currentResultSet := c.state.ResultSet()
|
||||||
|
|
||||||
if currentResultSet == nil {
|
if currentResultSet == nil {
|
||||||
tableInfo, err := c.tableService.Describe(context.Background(), viewSnapshot.TableName)
|
return NewJob(c.jobController, "Fetching table info…", func(ctx context.Context) (*models.TableInfo, error) {
|
||||||
if err != nil {
|
tableInfo, err := c.tableService.Describe(context.Background(), viewSnapshot.TableName)
|
||||||
return events.Error(err)
|
if err != nil {
|
||||||
}
|
return nil, err
|
||||||
return c.runQuery(tableInfo, viewSnapshot.Query, viewSnapshot.Filter, false)
|
}
|
||||||
|
return tableInfo, nil
|
||||||
|
}).OnDone(func(tableInfo *models.TableInfo) tea.Msg {
|
||||||
|
return c.runQuery(tableInfo, viewSnapshot.Query, viewSnapshot.Filter, false)
|
||||||
|
}).Submit()
|
||||||
}
|
}
|
||||||
|
|
||||||
var currentQueryExpr string
|
var currentQueryExpr string
|
||||||
|
@ -265,23 +324,24 @@ func (c *TableReadController) updateViewToSnapshot(viewSnapshot *serialisable.Vi
|
||||||
}
|
}
|
||||||
|
|
||||||
if viewSnapshot.TableName == currentResultSet.TableInfo.Name && viewSnapshot.Query == currentQueryExpr {
|
if viewSnapshot.TableName == currentResultSet.TableInfo.Name && viewSnapshot.Query == currentQueryExpr {
|
||||||
log.Printf("backstack: setting filter to '%v'", viewSnapshot.Filter)
|
return NewJob(c.jobController, "Applying filter…", func(ctx context.Context) (*models.ResultSet, error) {
|
||||||
|
return c.tableService.Filter(currentResultSet, viewSnapshot.Filter), nil
|
||||||
newResultSet := c.tableService.Filter(currentResultSet, viewSnapshot.Filter)
|
}).OnEither(c.handleResultSetFromJobResult(viewSnapshot.Filter, false, resultSetUpdateSnapshotRestore)).Submit()
|
||||||
return c.setResultSetAndFilter(newResultSet, viewSnapshot.Filter, false, resultSetUpdateSnapshotRestore)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tableInfo := currentResultSet.TableInfo
|
return NewJob(c.jobController, "Running query…", func(ctx context.Context) (tea.Msg, error) {
|
||||||
if viewSnapshot.TableName != currentResultSet.TableInfo.Name {
|
tableInfo := currentResultSet.TableInfo
|
||||||
tableInfo, err = c.tableService.Describe(context.Background(), viewSnapshot.TableName)
|
if viewSnapshot.TableName != currentResultSet.TableInfo.Name {
|
||||||
if err != nil {
|
tableInfo, err = c.tableService.Describe(context.Background(), viewSnapshot.TableName)
|
||||||
return events.Error(err)
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("backstack: running query: table = '%v', query = '%v', filter = '%v'",
|
return c.runQuery(tableInfo, viewSnapshot.Query, viewSnapshot.Filter, false), nil
|
||||||
tableInfo.Name, viewSnapshot.Query, viewSnapshot.Filter)
|
}).OnDone(func(m tea.Msg) tea.Msg {
|
||||||
return c.runQuery(tableInfo, viewSnapshot.Query, viewSnapshot.Filter, false)
|
return m
|
||||||
|
}).Submit()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *TableReadController) CopyItemToClipboard(idx int) tea.Msg {
|
func (c *TableReadController) CopyItemToClipboard(idx int) tea.Msg {
|
||||||
|
|
|
@ -16,14 +16,22 @@ import (
|
||||||
type TableWriteController struct {
|
type TableWriteController struct {
|
||||||
state *State
|
state *State
|
||||||
tableService *tables.Service
|
tableService *tables.Service
|
||||||
|
jobController *JobsController
|
||||||
tableReadControllers *TableReadController
|
tableReadControllers *TableReadController
|
||||||
settingProvider SettingsProvider
|
settingProvider SettingsProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTableWriteController(state *State, tableService *tables.Service, tableReadControllers *TableReadController, settingProvider SettingsProvider) *TableWriteController {
|
func NewTableWriteController(
|
||||||
|
state *State,
|
||||||
|
tableService *tables.Service,
|
||||||
|
jobController *JobsController,
|
||||||
|
tableReadControllers *TableReadController,
|
||||||
|
settingProvider SettingsProvider,
|
||||||
|
) *TableWriteController {
|
||||||
return &TableWriteController{
|
return &TableWriteController{
|
||||||
state: state,
|
state: state,
|
||||||
tableService: tableService,
|
tableService: tableService,
|
||||||
|
jobController: jobController,
|
||||||
tableReadControllers: tableReadControllers,
|
tableReadControllers: tableReadControllers,
|
||||||
settingProvider: settingProvider,
|
settingProvider: settingProvider,
|
||||||
}
|
}
|
||||||
|
@ -231,31 +239,6 @@ func (twc *TableWriteController) DeleteAttribute(idx int, key string) tea.Msg {
|
||||||
return ResultSetUpdated{}
|
return ResultSetUpdated{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (twc *TableWriteController) PutItem(idx int) tea.Msg {
|
|
||||||
if err := twc.assertReadWrite(); err != nil {
|
|
||||||
return events.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
resultSet := twc.state.ResultSet()
|
|
||||||
if !resultSet.IsDirty(idx) {
|
|
||||||
return events.Error(errors.New("item is not dirty"))
|
|
||||||
}
|
|
||||||
|
|
||||||
return events.PromptForInputMsg{
|
|
||||||
Prompt: "put item? ",
|
|
||||||
OnDone: func(value string) tea.Msg {
|
|
||||||
if value != "y" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := twc.tableService.PutItemAt(context.Background(), resultSet, idx); err != nil {
|
|
||||||
return events.Error(err)
|
|
||||||
}
|
|
||||||
return ResultSetUpdated{}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (twc *TableWriteController) PutItems() tea.Msg {
|
func (twc *TableWriteController) PutItems() tea.Msg {
|
||||||
if err := twc.assertReadWrite(); err != nil {
|
if err := twc.assertReadWrite(); err != nil {
|
||||||
return events.Error(err)
|
return events.Error(err)
|
||||||
|
@ -305,19 +288,18 @@ func (twc *TableWriteController) PutItems() tea.Msg {
|
||||||
return events.StatusMsg("operation aborted")
|
return events.StatusMsg("operation aborted")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := twc.state.withResultSetReturningError(func(rs *models.ResultSet) error {
|
return NewJob(twc.jobController, "Updating items…", func(ctx context.Context) (*models.ResultSet, error) {
|
||||||
err := twc.tableService.PutSelectedItems(context.Background(), rs, itemsToPut)
|
rs := twc.state.ResultSet()
|
||||||
|
err := twc.tableService.PutSelectedItems(ctx, rs, itemsToPut)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
return nil
|
return rs, nil
|
||||||
}); err != nil {
|
}).OnDone(func(rs *models.ResultSet) tea.Msg {
|
||||||
return events.Error(err)
|
return ResultSetUpdated{
|
||||||
}
|
statusMessage: applyToN("", len(itemsToPut), "item", "item", " put to table"),
|
||||||
|
}
|
||||||
return ResultSetUpdated{
|
}).Submit()
|
||||||
statusMessage: applyToN("", len(itemsToPut), "item", "item", " put to table"),
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -375,7 +357,7 @@ func (twc *TableWriteController) NoisyTouchItem(idx int) tea.Msg {
|
||||||
return events.Error(err)
|
return events.Error(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return twc.tableReadControllers.doScan(ctx, resultSet, resultSet.Query, false, resultSetUpdateTouch)
|
return twc.tableReadControllers.doScan(resultSet, resultSet.Query, false, resultSetUpdateTouch)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -399,14 +381,14 @@ func (twc *TableWriteController) DeleteMarked() tea.Msg {
|
||||||
return events.StatusMsg("operation aborted")
|
return events.StatusMsg("operation aborted")
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := context.Background()
|
return NewJob(twc.jobController, "Deleting items…", func(ctx context.Context) (struct{}, error) {
|
||||||
if err := twc.tableService.Delete(ctx, resultSet.TableInfo, sliceutils.Map(markedItems, func(index models.ItemIndex) models.Item {
|
err := twc.tableService.Delete(ctx, resultSet.TableInfo, sliceutils.Map(markedItems, func(index models.ItemIndex) models.Item {
|
||||||
return index.Item
|
return index.Item
|
||||||
})); err != nil {
|
}))
|
||||||
return events.Error(err)
|
return struct{}{}, err
|
||||||
}
|
}).OnDone(func(_ struct{}) tea.Msg {
|
||||||
|
return twc.tableReadControllers.doScan(resultSet, resultSet.Query, false, resultSetUpdateTouch)
|
||||||
return twc.tableReadControllers.doScan(ctx, resultSet, resultSet.Query, false, resultSetUpdateTouch)
|
}).Submit()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"github.com/lmika/audax/internal/dynamo-browse/providers/settingstore"
|
"github.com/lmika/audax/internal/dynamo-browse/providers/settingstore"
|
||||||
"github.com/lmika/audax/internal/dynamo-browse/providers/workspacestore"
|
"github.com/lmika/audax/internal/dynamo-browse/providers/workspacestore"
|
||||||
"github.com/lmika/audax/internal/dynamo-browse/services/itemrenderer"
|
"github.com/lmika/audax/internal/dynamo-browse/services/itemrenderer"
|
||||||
|
"github.com/lmika/audax/internal/dynamo-browse/services/jobs"
|
||||||
"github.com/lmika/audax/internal/dynamo-browse/services/tables"
|
"github.com/lmika/audax/internal/dynamo-browse/services/tables"
|
||||||
workspaces_service "github.com/lmika/audax/internal/dynamo-browse/services/workspaces"
|
workspaces_service "github.com/lmika/audax/internal/dynamo-browse/services/workspaces"
|
||||||
"github.com/lmika/audax/test/testdynamo"
|
"github.com/lmika/audax/test/testdynamo"
|
||||||
|
@ -48,7 +49,7 @@ func TestTableWriteController_NewItem(t *testing.T) {
|
||||||
// Prompt for keys
|
// Prompt for keys
|
||||||
invokeCommandExpectingError(t, srv.writeController.NewItem())
|
invokeCommandExpectingError(t, srv.writeController.NewItem())
|
||||||
|
|
||||||
// Confirm no changes
|
// ConfirmYes no changes
|
||||||
invokeCommand(t, srv.readController.Rescan())
|
invokeCommand(t, srv.readController.Rescan())
|
||||||
|
|
||||||
newResultSet := srv.state.ResultSet()
|
newResultSet := srv.state.ResultSet()
|
||||||
|
@ -240,7 +241,7 @@ func TestTableWriteController_PutItem(t *testing.T) {
|
||||||
|
|
||||||
// Modify the item and put it
|
// Modify the item and put it
|
||||||
invokeCommandWithPrompt(t, srv.writeController.SetAttributeValue(0, models.StringItemType, "alpha"), "a new value")
|
invokeCommandWithPrompt(t, srv.writeController.SetAttributeValue(0, models.StringItemType, "alpha"), "a new value")
|
||||||
invokeCommandWithPrompt(t, srv.writeController.PutItem(0), "y")
|
invokeCommandWithPrompt(t, srv.writeController.PutItems(), "y")
|
||||||
|
|
||||||
// Rescan the table
|
// Rescan the table
|
||||||
invokeCommand(t, srv.readController.Rescan())
|
invokeCommand(t, srv.readController.Rescan())
|
||||||
|
@ -260,7 +261,7 @@ func TestTableWriteController_PutItem(t *testing.T) {
|
||||||
|
|
||||||
// Modify the item but do not put it
|
// Modify the item but do not put it
|
||||||
invokeCommandWithPrompt(t, srv.writeController.SetAttributeValue(0, models.StringItemType, "alpha"), "a new value")
|
invokeCommandWithPrompt(t, srv.writeController.SetAttributeValue(0, models.StringItemType, "alpha"), "a new value")
|
||||||
invokeCommandWithPrompt(t, srv.writeController.PutItem(0), "n")
|
invokeCommandWithPrompt(t, srv.writeController.PutItems(), "n")
|
||||||
|
|
||||||
current, _ := srv.state.ResultSet().Items()[0].AttributeValueAsString("alpha")
|
current, _ := srv.state.ResultSet().Items()[0].AttributeValueAsString("alpha")
|
||||||
assert.Equal(t, "a new value", current)
|
assert.Equal(t, "a new value", current)
|
||||||
|
@ -282,7 +283,7 @@ func TestTableWriteController_PutItem(t *testing.T) {
|
||||||
assert.Equal(t, "This is some value", before)
|
assert.Equal(t, "This is some value", before)
|
||||||
assert.False(t, srv.state.ResultSet().IsDirty(0))
|
assert.False(t, srv.state.ResultSet().IsDirty(0))
|
||||||
|
|
||||||
invokeCommandExpectingError(t, srv.writeController.PutItem(0))
|
invokeCommand(t, srv.writeController.PutItems())
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("should not put the selected item if in read-only mode", func(t *testing.T) {
|
t.Run("should not put the selected item if in read-only mode", func(t *testing.T) {
|
||||||
|
@ -296,7 +297,7 @@ func TestTableWriteController_PutItem(t *testing.T) {
|
||||||
|
|
||||||
// Modify the item but do not put it
|
// Modify the item but do not put it
|
||||||
invokeCommandWithPrompt(t, srv.writeController.SetAttributeValue(0, models.StringItemType, "alpha"), "a new value")
|
invokeCommandWithPrompt(t, srv.writeController.SetAttributeValue(0, models.StringItemType, "alpha"), "a new value")
|
||||||
invokeCommandExpectingError(t, srv.writeController.PutItem(0))
|
invokeCommandExpectingError(t, srv.writeController.PutItems())
|
||||||
|
|
||||||
current, _ := srv.state.ResultSet().Items()[0].AttributeValueAsString("alpha")
|
current, _ := srv.state.ResultSet().Items()[0].AttributeValueAsString("alpha")
|
||||||
assert.Equal(t, "a new value", current)
|
assert.Equal(t, "a new value", current)
|
||||||
|
@ -596,8 +597,9 @@ func newService(t *testing.T, cfg serviceConfig) *services {
|
||||||
eventBus := bus.New()
|
eventBus := bus.New()
|
||||||
|
|
||||||
state := controllers.NewState()
|
state := controllers.NewState()
|
||||||
readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, eventBus, cfg.tableName)
|
jobsController := controllers.NewJobsController(jobs.NewService(eventBus), eventBus, true)
|
||||||
writeController := controllers.NewTableWriteController(state, service, readController, settingStore)
|
readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, jobsController, eventBus, cfg.tableName)
|
||||||
|
writeController := controllers.NewTableWriteController(state, service, jobsController, readController, settingStore)
|
||||||
settingsController := controllers.NewSettingsController(settingStore)
|
settingsController := controllers.NewSettingsController(settingStore)
|
||||||
columnsController := controllers.NewColumnsController(eventBus)
|
columnsController := controllers.NewColumnsController(eventBus)
|
||||||
exportController := controllers.NewExportController(state, columnsController)
|
exportController := controllers.NewExportController(state, columnsController)
|
||||||
|
|
|
@ -1,5 +1,23 @@
|
||||||
package models
|
package models
|
||||||
|
|
||||||
import "github.com/pkg/errors"
|
import (
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
var ErrReadOnly = errors.New("in read-only mode")
|
var ErrReadOnly = errors.New("in read-only mode")
|
||||||
|
|
||||||
|
type PartialResultsError struct {
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPartialResultsError(err error) PartialResultsError {
|
||||||
|
return PartialResultsError{err: err}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pr PartialResultsError) Error() string {
|
||||||
|
return "partial results received"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pr PartialResultsError) Unwrap() error {
|
||||||
|
return pr.err
|
||||||
|
}
|
||||||
|
|
|
@ -2,13 +2,16 @@ package dynamo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"github.com/aws/aws-sdk-go-v2/aws"
|
"github.com/aws/aws-sdk-go-v2/aws"
|
||||||
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression"
|
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression"
|
||||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
|
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
|
||||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||||
"github.com/lmika/audax/internal/common/sliceutils"
|
"github.com/lmika/audax/internal/common/sliceutils"
|
||||||
"github.com/lmika/audax/internal/dynamo-browse/models"
|
"github.com/lmika/audax/internal/dynamo-browse/models"
|
||||||
|
"github.com/lmika/audax/internal/dynamo-browse/services/jobs"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Provider struct {
|
type Provider struct {
|
||||||
|
@ -70,6 +73,8 @@ func (p *Provider) PutItems(ctx context.Context, name string, items []models.Ite
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provider) batchPutItems(ctx context.Context, name string, items []models.Item) error {
|
func (p *Provider) batchPutItems(ctx context.Context, name string, items []models.Item) error {
|
||||||
|
nextUpdate := time.Now().Add(1 * time.Second)
|
||||||
|
|
||||||
reqs := len(items)/25 + 1
|
reqs := len(items)/25 + 1
|
||||||
for rn := 0; rn < reqs; rn++ {
|
for rn := 0; rn < reqs; rn++ {
|
||||||
s, f := rn*25, (rn+1)*25
|
s, f := rn*25, (rn+1)*25
|
||||||
|
@ -90,6 +95,11 @@ func (p *Provider) batchPutItems(ctx context.Context, name string, items []model
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errors.Wrapf(err, "unable to put page %v of back puts", rn)
|
errors.Wrapf(err, "unable to put page %v of back puts", rn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if time.Now().After(nextUpdate) {
|
||||||
|
jobs.PostUpdate(ctx, fmt.Sprintf("updated %d items", f))
|
||||||
|
nextUpdate = time.Now().Add(1 * time.Second)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -109,10 +119,15 @@ func (p *Provider) ScanItems(ctx context.Context, tableName string, filterExpr *
|
||||||
|
|
||||||
items := make([]models.Item, 0)
|
items := make([]models.Item, 0)
|
||||||
|
|
||||||
|
nextUpdate := time.Now().Add(1 * time.Second)
|
||||||
|
|
||||||
outer:
|
outer:
|
||||||
for paginator.HasMorePages() {
|
for paginator.HasMorePages() {
|
||||||
res, err := paginator.NextPage(ctx)
|
res, err := paginator.NextPage(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return items, models.NewPartialResultsError(ctx.Err())
|
||||||
|
}
|
||||||
return nil, errors.Wrapf(err, "cannot execute scan on table %v", tableName)
|
return nil, errors.Wrapf(err, "cannot execute scan on table %v", tableName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -121,6 +136,11 @@ outer:
|
||||||
if len(items) >= maxItems {
|
if len(items) >= maxItems {
|
||||||
break outer
|
break outer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if time.Now().After(nextUpdate) {
|
||||||
|
jobs.PostUpdate(ctx, fmt.Sprintf("found %d items", len(items)))
|
||||||
|
nextUpdate = time.Now().Add(1 * time.Second)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -143,10 +163,15 @@ func (p *Provider) QueryItems(ctx context.Context, tableName string, filterExpr
|
||||||
|
|
||||||
items := make([]models.Item, 0)
|
items := make([]models.Item, 0)
|
||||||
|
|
||||||
|
nextUpdate := time.Now().Add(1 * time.Second)
|
||||||
|
|
||||||
outer:
|
outer:
|
||||||
for paginator.HasMorePages() {
|
for paginator.HasMorePages() {
|
||||||
res, err := paginator.NextPage(ctx)
|
res, err := paginator.NextPage(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return items, models.NewPartialResultsError(ctx.Err())
|
||||||
|
}
|
||||||
return nil, errors.Wrapf(err, "cannot execute query on table %v", tableName)
|
return nil, errors.Wrapf(err, "cannot execute query on table %v", tableName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -155,6 +180,11 @@ outer:
|
||||||
if len(items) >= maxItems {
|
if len(items) >= maxItems {
|
||||||
break outer
|
break outer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if time.Now().After(nextUpdate) {
|
||||||
|
jobs.PostUpdate(ctx, fmt.Sprintf("found %d items", len(items)))
|
||||||
|
nextUpdate = time.Now().Add(1 * time.Second)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
25
internal/dynamo-browse/services/jobs/ctx.go
Normal file
25
internal/dynamo-browse/services/jobs/ctx.go
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
package jobs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
type jobUpdaterKeyType struct{}
|
||||||
|
|
||||||
|
var jobUpdaterKey = jobUpdaterKeyType{}
|
||||||
|
|
||||||
|
type jobUpdaterValue struct {
|
||||||
|
msgUpdate chan string
|
||||||
|
}
|
||||||
|
|
||||||
|
func PostUpdate(ctx context.Context, msg string) {
|
||||||
|
val, hasVal := ctx.Value(jobUpdaterKey).(*jobUpdaterValue)
|
||||||
|
if !hasVal {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case val.msgUpdate <- msg:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
9
internal/dynamo-browse/services/jobs/events.go
Normal file
9
internal/dynamo-browse/services/jobs/events.go
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
package jobs
|
||||||
|
|
||||||
|
const (
|
||||||
|
JobEventForegroundDone = "job_foreground_done"
|
||||||
|
)
|
||||||
|
|
||||||
|
type JobDoneEvent struct {
|
||||||
|
Err error
|
||||||
|
}
|
79
internal/dynamo-browse/services/jobs/jobs.go
Normal file
79
internal/dynamo-browse/services/jobs/jobs.go
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
package jobs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
bus "github.com/lmika/events"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Job func(ctx context.Context)
|
||||||
|
|
||||||
|
type jobInfo struct {
|
||||||
|
ctx context.Context
|
||||||
|
cancelFn func()
|
||||||
|
}
|
||||||
|
|
||||||
|
type Services struct {
|
||||||
|
bus *bus.Bus
|
||||||
|
|
||||||
|
mutex *sync.Mutex
|
||||||
|
foregroundJob *jobInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewService(bus *bus.Bus) *Services {
|
||||||
|
return &Services{
|
||||||
|
bus: bus,
|
||||||
|
mutex: new(sync.Mutex),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubmitForegroundJob starts a foreground job.
|
||||||
|
func (jc *Services) SubmitForegroundJob(job Job, onJobUpdate func(msg string)) {
|
||||||
|
// TODO: if there's already a foreground job, then return error
|
||||||
|
|
||||||
|
ctx, cancelFn := context.WithCancel(context.Background())
|
||||||
|
|
||||||
|
jobUpdateChan := make(chan string)
|
||||||
|
jobUpdater := &jobUpdaterValue{msgUpdate: jobUpdateChan}
|
||||||
|
ctx = context.WithValue(ctx, jobUpdaterKey, jobUpdater)
|
||||||
|
|
||||||
|
newJobInfo := &jobInfo{
|
||||||
|
ctx: ctx,
|
||||||
|
cancelFn: cancelFn,
|
||||||
|
}
|
||||||
|
// TODO: needs to be protected by the mutex
|
||||||
|
jc.foregroundJob = newJobInfo
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer cancelFn()
|
||||||
|
defer close(jobUpdateChan)
|
||||||
|
|
||||||
|
job(newJobInfo.ctx)
|
||||||
|
|
||||||
|
// TODO: needs to be protected by the mutex
|
||||||
|
jc.foregroundJob = nil
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for update := range jobUpdateChan {
|
||||||
|
onJobUpdate(update)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (jc *Services) CancelForegroundJob() bool {
|
||||||
|
// TODO: needs to be protected by the mutex
|
||||||
|
if jc.foregroundJob != nil {
|
||||||
|
// A nil cancel for a non-nil foreground job indicates that the cancellation function
|
||||||
|
// has been called and the job is in the process of stopping
|
||||||
|
if jc.foregroundJob.cancelFn == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
jc.foregroundJob.cancelFn()
|
||||||
|
jc.foregroundJob.cancelFn = nil
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
|
@ -2,10 +2,13 @@ package tables
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression"
|
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression"
|
||||||
"github.com/lmika/audax/internal/common/sliceutils"
|
"github.com/lmika/audax/internal/common/sliceutils"
|
||||||
|
"github.com/lmika/audax/internal/dynamo-browse/services/jobs"
|
||||||
"log"
|
"log"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/lmika/audax/internal/dynamo-browse/models"
|
"github.com/lmika/audax/internal/dynamo-browse/models"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
@ -60,7 +63,7 @@ func (s *Service) doScan(ctx context.Context, tableInfo *models.TableInfo, expr
|
||||||
results, err = s.provider.ScanItems(ctx, tableInfo.Name, filterExpr, limit)
|
results, err = s.provider.ScanItems(ctx, tableInfo.Name, filterExpr, limit)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil && len(results) == 0 {
|
||||||
return nil, errors.Wrapf(err, "unable to scan table %v", tableInfo.Name)
|
return nil, errors.Wrapf(err, "unable to scan table %v", tableInfo.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -73,7 +76,7 @@ func (s *Service) doScan(ctx context.Context, tableInfo *models.TableInfo, expr
|
||||||
resultSet.SetItems(results)
|
resultSet.SetItems(results)
|
||||||
resultSet.RefreshColumns()
|
resultSet.RefreshColumns()
|
||||||
|
|
||||||
return resultSet, nil
|
return resultSet, err
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
|
@ -126,10 +129,17 @@ func (s *Service) Delete(ctx context.Context, tableInfo *models.TableInfo, items
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, item := range items {
|
nextUpdate := time.Now().Add(1 * time.Second)
|
||||||
|
|
||||||
|
for i, item := range items {
|
||||||
if err := s.provider.DeleteItem(ctx, tableInfo.Name, item.KeyValue(tableInfo)); err != nil {
|
if err := s.provider.DeleteItem(ctx, tableInfo.Name, item.KeyValue(tableInfo)); err != nil {
|
||||||
return errors.Wrapf(err, "cannot delete item")
|
return errors.Wrapf(err, "cannot delete item")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if time.Now().After(nextUpdate) {
|
||||||
|
jobs.PostUpdate(ctx, fmt.Sprintf("delete %d items", i))
|
||||||
|
nextUpdate = time.Now().Add(1 * time.Second)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -150,6 +160,10 @@ func (s *Service) assertReadWrite() error {
|
||||||
|
|
||||||
// TODO: move into a new service
|
// TODO: move into a new service
|
||||||
func (s *Service) Filter(resultSet *models.ResultSet, filter string) *models.ResultSet {
|
func (s *Service) Filter(resultSet *models.ResultSet, filter string) *models.ResultSet {
|
||||||
|
if resultSet == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
for i, item := range resultSet.Items() {
|
for i, item := range resultSet.Items() {
|
||||||
if filter == "" {
|
if filter == "" {
|
||||||
resultSet.SetHidden(i, false)
|
resultSet.SetHidden(i, false)
|
||||||
|
|
|
@ -35,7 +35,8 @@ func Default() *KeyBindings {
|
||||||
CycleLayoutBackwards: key.NewBinding(key.WithKeys("W"), key.WithHelp("W", "cycle layout backward")),
|
CycleLayoutBackwards: key.NewBinding(key.WithKeys("W"), key.WithHelp("W", "cycle layout backward")),
|
||||||
PromptForCommand: key.NewBinding(key.WithKeys(":"), key.WithHelp(":", "prompt for command")),
|
PromptForCommand: key.NewBinding(key.WithKeys(":"), key.WithHelp(":", "prompt for command")),
|
||||||
ShowColumnOverlay: key.NewBinding(key.WithKeys("f"), key.WithHelp("f", "show column overlay")),
|
ShowColumnOverlay: key.NewBinding(key.WithKeys("f"), key.WithHelp("f", "show column overlay")),
|
||||||
Quit: key.NewBinding(key.WithKeys("ctrl+c", "esc"), key.WithHelp("ctrl+c/esc", "quit")),
|
CancelRunningJob: key.NewBinding(key.WithKeys("ctrl+c"), key.WithHelp("ctrl+c", "cancel running job or quit")),
|
||||||
|
Quit: key.NewBinding(key.WithKeys("esc"), key.WithHelp("esc", "quit")),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,5 +42,6 @@ type ViewKeyBindings struct {
|
||||||
CycleLayoutBackwards key.Binding `keymap:"cycle-layout-backwards"`
|
CycleLayoutBackwards key.Binding `keymap:"cycle-layout-backwards"`
|
||||||
PromptForCommand key.Binding `keymap:"prompt-for-command"`
|
PromptForCommand key.Binding `keymap:"prompt-for-command"`
|
||||||
ShowColumnOverlay key.Binding `keymap:"show-column-overlay"`
|
ShowColumnOverlay key.Binding `keymap:"show-column-overlay"`
|
||||||
|
CancelRunningJob key.Binding `keymap:"cancel-running-job"`
|
||||||
Quit key.Binding `keymap:"quit"`
|
Quit key.Binding `keymap:"quit"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,6 +43,7 @@ type Model struct {
|
||||||
settingsController *controllers.SettingsController
|
settingsController *controllers.SettingsController
|
||||||
exportController *controllers.ExportController
|
exportController *controllers.ExportController
|
||||||
commandController *commandctrl.CommandController
|
commandController *commandctrl.CommandController
|
||||||
|
jobController *controllers.JobsController
|
||||||
colSelector *colselector.Model
|
colSelector *colselector.Model
|
||||||
itemEdit *dynamoitemedit.Model
|
itemEdit *dynamoitemedit.Model
|
||||||
statusAndPrompt *statusandprompt.StatusAndPrompt
|
statusAndPrompt *statusandprompt.StatusAndPrompt
|
||||||
|
@ -63,6 +64,7 @@ func NewModel(
|
||||||
columnsController *controllers.ColumnsController,
|
columnsController *controllers.ColumnsController,
|
||||||
exportController *controllers.ExportController,
|
exportController *controllers.ExportController,
|
||||||
settingsController *controllers.SettingsController,
|
settingsController *controllers.SettingsController,
|
||||||
|
jobController *controllers.JobsController,
|
||||||
itemRendererService *itemrenderer.Service,
|
itemRendererService *itemrenderer.Service,
|
||||||
cc *commandctrl.CommandController,
|
cc *commandctrl.CommandController,
|
||||||
keyBindingController *controllers.KeyBindingController,
|
keyBindingController *controllers.KeyBindingController,
|
||||||
|
@ -96,7 +98,23 @@ func NewModel(
|
||||||
}
|
}
|
||||||
return exportController.ExportCSV(args[0])
|
return exportController.ExportCSV(args[0])
|
||||||
},
|
},
|
||||||
"unmark": commandctrl.NoArgCommand(rc.Unmark),
|
"mark": func(ctx commandctrl.ExecContext, args []string) tea.Msg {
|
||||||
|
var markOp = controllers.MarkOpMark
|
||||||
|
if len(args) > 0 {
|
||||||
|
switch args[0] {
|
||||||
|
case "all":
|
||||||
|
markOp = controllers.MarkOpMark
|
||||||
|
case "none":
|
||||||
|
markOp = controllers.MarkOpUnmark
|
||||||
|
case "toggle":
|
||||||
|
markOp = controllers.MarkOpToggle
|
||||||
|
default:
|
||||||
|
return events.Error(errors.New("unrecognised mark operation"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rc.Mark(markOp)
|
||||||
|
},
|
||||||
"delete": commandctrl.NoArgCommand(wc.DeleteMarked),
|
"delete": commandctrl.NoArgCommand(wc.DeleteMarked),
|
||||||
|
|
||||||
// TEMP
|
// TEMP
|
||||||
|
@ -166,10 +184,11 @@ func NewModel(
|
||||||
},
|
},
|
||||||
|
|
||||||
// Aliases
|
// Aliases
|
||||||
"sa": cc.Alias("set-attr"),
|
"unmark": cc.Alias("mark", []string{"none"}),
|
||||||
"da": cc.Alias("del-attr"),
|
"sa": cc.Alias("set-attr", nil),
|
||||||
"w": cc.Alias("put"),
|
"da": cc.Alias("del-attr", nil),
|
||||||
"q": cc.Alias("quit"),
|
"w": cc.Alias("put", nil),
|
||||||
|
"q": cc.Alias("quit", nil),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -179,6 +198,7 @@ func NewModel(
|
||||||
tableReadController: rc,
|
tableReadController: rc,
|
||||||
tableWriteController: wc,
|
tableWriteController: wc,
|
||||||
commandController: cc,
|
commandController: cc,
|
||||||
|
jobController: jobController,
|
||||||
itemEdit: itemEdit,
|
itemEdit: itemEdit,
|
||||||
colSelector: colSelector,
|
colSelector: colSelector,
|
||||||
statusAndPrompt: statusAndPrompt,
|
statusAndPrompt: statusAndPrompt,
|
||||||
|
@ -236,6 +256,10 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
return m, m.commandController.Prompt
|
return m, m.commandController.Prompt
|
||||||
case key.Matches(msg, m.keyMap.PromptForTable):
|
case key.Matches(msg, m.keyMap.PromptForTable):
|
||||||
return m, events.SetTeaMessage(m.tableReadController.ListTables())
|
return m, events.SetTeaMessage(m.tableReadController.ListTables())
|
||||||
|
case key.Matches(msg, m.keyMap.CancelRunningJob):
|
||||||
|
return m, events.SetTeaMessage(m.jobController.CancelRunningJob(func() tea.Msg {
|
||||||
|
return tea.Quit()
|
||||||
|
}))
|
||||||
case key.Matches(msg, m.keyMap.Quit):
|
case key.Matches(msg, m.keyMap.Quit):
|
||||||
return m, tea.Quit
|
return m, tea.Quit
|
||||||
}
|
}
|
||||||
|
@ -254,7 +278,10 @@ func (m Model) Init() tea.Cmd {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return m.tableReadController.Init
|
return tea.Batch(
|
||||||
|
m.tableReadController.Init,
|
||||||
|
m.root.Init(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) View() string {
|
func (m Model) View() string {
|
||||||
|
|
|
@ -48,11 +48,11 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
m.compositor.ClearOverlay()
|
m.compositor.ClearOverlay()
|
||||||
case controllers.ColumnsUpdated:
|
case controllers.ColumnsUpdated:
|
||||||
m.colListModel.refreshTable()
|
m.colListModel.refreshTable()
|
||||||
m.subModel = cc.Collect(m.subModel.Update(msg))
|
m.subModel = cc.Collect(m.subModel.Update(msg)).(tea.Model)
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
m.compositor = cc.Collect(m.compositor.Update(msg)).(*layout.Compositor)
|
m.compositor = cc.Collect(m.compositor.Update(msg)).(*layout.Compositor)
|
||||||
default:
|
default:
|
||||||
m.subModel = cc.Collect(m.subModel.Update(msg))
|
m.subModel = cc.Collect(m.subModel.Update(msg)).(tea.Model)
|
||||||
}
|
}
|
||||||
return m, cc.Cmd()
|
return m, cc.Cmd()
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,10 @@ type columnModel struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cm columnModel) Len() int {
|
func (cm columnModel) Len() int {
|
||||||
|
if len(cm.m.columns) == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
return len(cm.m.columns[cm.m.colOffset:]) + 1
|
return len(cm.m.columns[cm.m.colOffset:]) + 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -118,6 +118,10 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) setLeftmostDisplayedColumn(newCol int) {
|
func (m *Model) setLeftmostDisplayedColumn(newCol int) {
|
||||||
|
if m.columnsProvider == nil || m.columnsProvider.Columns() == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if newCol < 0 {
|
if newCol < 0 {
|
||||||
m.colOffset = 0
|
m.colOffset = 0
|
||||||
} else if newCol >= len(m.columnsProvider.Columns().Columns) {
|
} else if newCol >= len(m.columnsProvider.Columns().Columns) {
|
||||||
|
@ -163,11 +167,8 @@ func (m *Model) rebuildTable(targetTbl *table.Model) {
|
||||||
|
|
||||||
// Use the target table model if you can, but if it's nil or the number of rows is smaller than the
|
// Use the target table model if you can, but if it's nil or the number of rows is smaller than the
|
||||||
// existing table, create a new one
|
// existing table, create a new one
|
||||||
if targetTbl == nil || len(resultSet.Items()) > len(m.rows) {
|
if targetTbl == nil {
|
||||||
tbl = table.New(columnModel{m}, m.w, m.h-m.frameTitle.HeaderHeight())
|
tbl = table.New(columnModel{m}, m.w, m.h-m.frameTitle.HeaderHeight())
|
||||||
if targetTbl != nil {
|
|
||||||
tbl.GoBottom()
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
tbl = *targetTbl
|
tbl = *targetTbl
|
||||||
}
|
}
|
||||||
|
@ -191,6 +192,7 @@ func (m *Model) rebuildTable(targetTbl *table.Model) {
|
||||||
|
|
||||||
m.rows = newRows
|
m.rows = newRows
|
||||||
tbl.SetRows(newRows)
|
tbl.SetRows(newRows)
|
||||||
|
tbl.GoTop() // Preserve top and cursor location
|
||||||
|
|
||||||
m.table = tbl
|
m.table = tbl
|
||||||
}
|
}
|
||||||
|
@ -205,6 +207,12 @@ func (m *Model) SelectedItemIndex() int {
|
||||||
|
|
||||||
func (m *Model) selectedItem() (itemTableRow, bool) {
|
func (m *Model) selectedItem() (itemTableRow, bool) {
|
||||||
resultSet := m.resultSet
|
resultSet := m.resultSet
|
||||||
|
|
||||||
|
// Fix bug??
|
||||||
|
if m.table.Cursor() < 0 {
|
||||||
|
return itemTableRow{}, false
|
||||||
|
}
|
||||||
|
|
||||||
if resultSet != nil && len(m.rows) > 0 {
|
if resultSet != nil && len(m.rows) > 0 {
|
||||||
selectedItem, ok := m.table.SelectedRow().(itemTableRow)
|
selectedItem, ok := m.table.SelectedRow().(itemTableRow)
|
||||||
if ok {
|
if ok {
|
||||||
|
|
|
@ -23,7 +23,7 @@ func (m *Model) Init() tea.Cmd {
|
||||||
func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
var cc utils.CmdCollector
|
var cc utils.CmdCollector
|
||||||
|
|
||||||
m.baseMode = cc.Collect(m.baseMode.Update(msg))
|
m.baseMode = cc.Collect(m.baseMode.Update(msg)).(tea.Model)
|
||||||
return m, cc.Cmd()
|
return m, cc.Cmd()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -39,10 +39,10 @@ func (vb ZStack) Update(msg tea.Msg) (m tea.Model, cmd tea.Cmd) {
|
||||||
|
|
||||||
// All other messages go to each model
|
// All other messages go to each model
|
||||||
var cc utils.CmdCollector
|
var cc utils.CmdCollector
|
||||||
vb.visibleModel = cc.Collect(vb.visibleModel.Update(msg))
|
vb.visibleModel = cc.Collect(vb.visibleModel.Update(msg)).(tea.Model)
|
||||||
vb.focusedModel = cc.Collect(vb.focusedModel.Update(msg))
|
vb.focusedModel = cc.Collect(vb.focusedModel.Update(msg)).(tea.Model)
|
||||||
for i, c := range vb.otherModels {
|
for i, c := range vb.otherModels {
|
||||||
vb.otherModels[i] = cc.Collect(c.Update(msg))
|
vb.otherModels[i] = cc.Collect(c.Update(msg)).(tea.Model)
|
||||||
}
|
}
|
||||||
return vb, cc.Cmd()
|
return vb, cc.Cmd()
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,16 +50,16 @@ func (m Modal) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
case tea.KeyMsg, tea.MouseMsg:
|
case tea.KeyMsg, tea.MouseMsg:
|
||||||
// only notify top level stack
|
// only notify top level stack
|
||||||
if len(m.modeStack) > 0 {
|
if len(m.modeStack) > 0 {
|
||||||
m.modeStack[len(m.modeStack)-1] = cc.Collect(m.modeStack[len(m.modeStack)-1].Update(msg))
|
m.modeStack[len(m.modeStack)-1] = cc.Collect(m.modeStack[len(m.modeStack)-1].Update(msg)).(tea.Model)
|
||||||
} else {
|
} else {
|
||||||
m.baseMode = cc.Collect(m.baseMode.Update(msg))
|
m.baseMode = cc.Collect(m.baseMode.Update(msg)).(tea.Model)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
// notify all modes of other events
|
// notify all modes of other events
|
||||||
// TODO: is this right?
|
// TODO: is this right?
|
||||||
m.baseMode = cc.Collect(m.baseMode.Update(msg))
|
m.baseMode = cc.Collect(m.baseMode.Update(msg)).(tea.Model)
|
||||||
for i, s := range m.modeStack {
|
for i, s := range m.modeStack {
|
||||||
m.modeStack[i] = cc.Collect(s.Update(msg))
|
m.modeStack[i] = cc.Collect(s.Update(msg)).(tea.Model)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package statusandprompt
|
package statusandprompt
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/charmbracelet/bubbles/spinner"
|
||||||
"github.com/charmbracelet/bubbles/textinput"
|
"github.com/charmbracelet/bubbles/textinput"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
@ -14,13 +15,15 @@ import (
|
||||||
// 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
|
||||||
// event is received, focus will be torn away and the user will be given a prompt the enter text.
|
// event is received, focus will be torn away and the user will be given a prompt the enter text.
|
||||||
type StatusAndPrompt struct {
|
type StatusAndPrompt struct {
|
||||||
model layout.ResizingModel
|
model layout.ResizingModel
|
||||||
style Style
|
style Style
|
||||||
modeLine string
|
modeLine string
|
||||||
statusMessage string
|
statusMessage string
|
||||||
pendingInput *events.PromptForInputMsg
|
spinner spinner.Model
|
||||||
textInput textinput.Model
|
spinnerVisible bool
|
||||||
width int
|
pendingInput *events.PromptForInputMsg
|
||||||
|
textInput textinput.Model
|
||||||
|
width int
|
||||||
}
|
}
|
||||||
|
|
||||||
type Style struct {
|
type Style struct {
|
||||||
|
@ -29,11 +32,20 @@ type Style struct {
|
||||||
|
|
||||||
func New(model layout.ResizingModel, initialMsg string, style Style) *StatusAndPrompt {
|
func New(model layout.ResizingModel, initialMsg string, style Style) *StatusAndPrompt {
|
||||||
textInput := textinput.New()
|
textInput := textinput.New()
|
||||||
return &StatusAndPrompt{model: model, style: style, statusMessage: initialMsg, modeLine: "", textInput: textInput}
|
return &StatusAndPrompt{
|
||||||
|
model: model,
|
||||||
|
style: style,
|
||||||
|
statusMessage: initialMsg,
|
||||||
|
modeLine: "",
|
||||||
|
spinner: spinner.New(spinner.WithSpinner(spinner.Line)),
|
||||||
|
textInput: textInput,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *StatusAndPrompt) Init() tea.Cmd {
|
func (s *StatusAndPrompt) Init() tea.Cmd {
|
||||||
return s.model.Init()
|
return tea.Batch(
|
||||||
|
s.model.Init(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *StatusAndPrompt) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
func (s *StatusAndPrompt) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
@ -47,6 +59,14 @@ func (s *StatusAndPrompt) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
case events.WrappedStatusMsg:
|
case events.WrappedStatusMsg:
|
||||||
s.statusMessage = string(msg.Message)
|
s.statusMessage = string(msg.Message)
|
||||||
cc.Add(func() tea.Msg { return msg.Next })
|
cc.Add(func() tea.Msg { return msg.Next })
|
||||||
|
case events.ForegroundJobUpdate:
|
||||||
|
if msg.JobRunning {
|
||||||
|
s.spinnerVisible = true
|
||||||
|
s.statusMessage = msg.JobStatus
|
||||||
|
cc.Add(s.spinner.Tick)
|
||||||
|
} else {
|
||||||
|
s.spinnerVisible = false
|
||||||
|
}
|
||||||
case events.ModeMessage:
|
case events.ModeMessage:
|
||||||
s.modeLine = string(msg)
|
s.modeLine = string(msg)
|
||||||
case events.MessageWithStatus:
|
case events.MessageWithStatus:
|
||||||
|
@ -92,8 +112,10 @@ func (s *StatusAndPrompt) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
newModel := cc.Collect(s.model.Update(msg))
|
if s.spinnerVisible {
|
||||||
s.model = newModel.(layout.ResizingModel)
|
s.spinner = cc.Collect(s.spinner.Update(msg)).(spinner.Model)
|
||||||
|
}
|
||||||
|
s.model = cc.Collect(s.model.Update(msg)).(layout.ResizingModel)
|
||||||
return s, cc.Cmd()
|
return s, cc.Cmd()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -122,5 +144,9 @@ func (s *StatusAndPrompt) viewStatus() string {
|
||||||
statusLine = s.statusMessage
|
statusLine = s.statusMessage
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if s.spinnerVisible {
|
||||||
|
statusLine = lipgloss.JoinHorizontal(lipgloss.Left, s.spinner.View(), " ", statusLine)
|
||||||
|
}
|
||||||
|
|
||||||
return lipgloss.JoinVertical(lipgloss.Top, modeLine, statusLine)
|
return lipgloss.JoinVertical(lipgloss.Top, modeLine, statusLine)
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"github.com/charmbracelet/bubbles/list"
|
"github.com/charmbracelet/bubbles/list"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
"github.com/lmika/audax/internal/common/ui/events"
|
||||||
"github.com/lmika/audax/internal/dynamo-browse/controllers"
|
"github.com/lmika/audax/internal/dynamo-browse/controllers"
|
||||||
"github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/frame"
|
"github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/frame"
|
||||||
"github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/layout"
|
"github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/layout"
|
||||||
|
@ -55,7 +56,10 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
var sel controllers.PromptForTableMsg
|
var sel controllers.PromptForTableMsg
|
||||||
sel, m.pendingSelection = *m.pendingSelection, nil
|
sel, m.pendingSelection = *m.pendingSelection, nil
|
||||||
|
|
||||||
return m, func() tea.Msg { return sel.OnSelected(m.listController.list.SelectedItem().(tableItem).name) }
|
if selTableItem, isTableItem := m.listController.list.SelectedItem().(tableItem); isTableItem {
|
||||||
|
return m, events.SetTeaMessage(sel.OnSelected(selTableItem.name))
|
||||||
|
}
|
||||||
|
return m, events.SetTeaMessage(sel.OnSelected(""))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,7 +71,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
if m.pendingSelection != nil {
|
if m.pendingSelection != nil {
|
||||||
m.listController = cc.Collect(m.listController.Update(msg)).(listController)
|
m.listController = cc.Collect(m.listController.Update(msg)).(listController)
|
||||||
}
|
}
|
||||||
m.submodel = cc.Collect(m.submodel.Update(msg))
|
m.submodel = cc.Collect(m.submodel.Update(msg)).(tea.Model)
|
||||||
return m, cc.Cmd()
|
return m, cc.Cmd()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@ func (c *CmdCollector) Add(cmd tea.Cmd) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *CmdCollector) Collect(m tea.Model, cmd tea.Cmd) tea.Model {
|
func (c *CmdCollector) Collect(m any, cmd tea.Cmd) any {
|
||||||
if cmd != nil {
|
if cmd != nil {
|
||||||
c.cmds = append(c.cmds, cmd)
|
c.cmds = append(c.cmds, cmd)
|
||||||
}
|
}
|
||||||
|
|
|
@ -77,7 +77,7 @@ func (c *SSMController) Clone(param models.SSMParameter) tea.Msg {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *SSMController) DeleteParameter(param models.SSMParameter) tea.Msg {
|
func (c *SSMController) DeleteParameter(param models.SSMParameter) tea.Msg {
|
||||||
return events.Confirm("delete parameter? ", func() tea.Msg {
|
return events.ConfirmYes("delete parameter? ", func() tea.Msg {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
if err := c.service.Delete(ctx, param); err != nil {
|
if err := c.service.Delete(ctx, param); err != nil {
|
||||||
return events.Error(err)
|
return events.Error(err)
|
||||||
|
|
Loading…
Reference in a new issue