Merge remote-tracking branch 'refs/remotes/origin/main'

This commit is contained in:
Leon Mika 2023-01-28 10:00:48 +11:00
commit 3bf5b6ec93
23 changed files with 265 additions and 29 deletions

View file

@ -13,8 +13,10 @@ import (
"github.com/lmika/audax/internal/common/workspaces"
"github.com/lmika/audax/internal/dynamo-browse/controllers"
"github.com/lmika/audax/internal/dynamo-browse/providers/dynamo"
"github.com/lmika/audax/internal/dynamo-browse/providers/inputhistorystore"
"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/services/inputhistory"
"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"
@ -81,6 +83,7 @@ func main() {
dynamoProvider := dynamo.NewProvider(dynamoClient)
resultSetSnapshotStore := workspacestore.NewResultSetSnapshotStore(ws)
settingStore := settingstore.New(ws)
inputHistoryStore := inputhistorystore.NewInputHistoryStore(ws)
if *flagRO {
if err := settingStore.SetReadOnly(*flagRO); err != nil {
@ -98,10 +101,11 @@ func main() {
itemRendererService := itemrenderer.NewService(uiStyles.ItemView.FieldType, uiStyles.ItemView.MetaInfo)
scriptManagerService := scriptmanager.New()
jobsService := jobs.NewService(eventBus)
inputHistoryService := inputhistory.New(inputHistoryStore)
state := controllers.NewState()
jobsController := controllers.NewJobsController(jobsService, eventBus, false)
tableReadController := controllers.NewTableReadController(state, tableService, workspaceService, itemRendererService, jobsController, eventBus, *flagTable)
tableReadController := controllers.NewTableReadController(state, tableService, workspaceService, itemRendererService, jobsController, inputHistoryService, eventBus, *flagTable)
tableWriteController := controllers.NewTableWriteController(state, tableService, jobsController, tableReadController, settingStore)
columnsController := controllers.NewColumnsController(eventBus)
exportController := controllers.NewExportController(state, columnsController)
@ -112,7 +116,7 @@ func main() {
keyBindingService := keybindings_service.NewService(keyBindings)
keyBindingController := controllers.NewKeyBindingController(keyBindingService)
commandController := commandctrl.NewCommandController()
commandController := commandctrl.NewCommandController(inputHistoryService)
commandController.AddCommandLookupExtension(scriptController)
model := ui.NewModel(

View file

@ -32,7 +32,7 @@ func main() {
ctrl := controllers.NewLogFileController(service, flag.Arg(0))
cmdController := commandctrl.NewCommandController()
cmdController := commandctrl.NewCommandController(nil)
//cmdController.AddCommands(&commandctrl.CommandList{
// Commands: map[string]commandctrl.Command{
// "cd": func(args []string) tea.Cmd {

View file

@ -47,7 +47,7 @@ func main() {
ctrl := controllers.New(service)
cmdController := commandctrl.NewCommandController()
cmdController := commandctrl.NewCommandController(nil)
cmdController.AddCommands(&commandctrl.CommandList{
Commands: map[string]commandctrl.Command{
"cd": func(ec commandctrl.ExecContext, args []string) tea.Msg {

View file

@ -3,6 +3,7 @@ package commandctrl
import (
"bufio"
"bytes"
"context"
tea "github.com/charmbracelet/bubbletea"
"github.com/pkg/errors"
"log"
@ -14,13 +15,17 @@ import (
"github.com/lmika/shellwords"
)
const commandsCategory = "commands"
type CommandController struct {
historyProvider IterProvider
commandList *CommandList
lookupExtensions []CommandLookupExtension
}
func NewCommandController() *CommandController {
func NewCommandController(historyProvider IterProvider) *CommandController {
return &CommandController{
historyProvider: historyProvider,
commandList: nil,
lookupExtensions: nil,
}
@ -38,6 +43,7 @@ func (c *CommandController) AddCommandLookupExtension(ext CommandLookupExtension
func (c *CommandController) Prompt() tea.Msg {
return events.PromptForInputMsg{
Prompt: ":",
History: c.historyProvider.Iter(context.Background(), commandsCategory),
OnDone: func(value string) tea.Msg {
return c.Execute(value)
},

View file

@ -1,7 +1,9 @@
package commandctrl_test
import (
"context"
"github.com/lmika/audax/internal/common/ui/events"
"github.com/lmika/audax/internal/dynamo-browse/services"
"testing"
"github.com/lmika/audax/internal/common/ui/commandctrl"
@ -10,7 +12,7 @@ import (
func TestCommandController_Prompt(t *testing.T) {
t.Run("prompt user for a command", func(t *testing.T) {
cmd := commandctrl.NewCommandController()
cmd := commandctrl.NewCommandController(mockIterProvider{})
res := cmd.Prompt()
@ -19,3 +21,10 @@ func TestCommandController_Prompt(t *testing.T) {
assert.Equal(t, ":", promptForInputMsg.Prompt)
})
}
type mockIterProvider struct {
}
func (m mockIterProvider) Iter(ctx context.Context, category string) services.HistoryProvider {
return nil
}

View file

@ -0,0 +1,10 @@
package commandctrl
import (
"context"
"github.com/lmika/audax/internal/dynamo-browse/services"
)
type IterProvider interface {
Iter(ctx context.Context, category string) services.HistoryProvider
}

View file

@ -2,6 +2,7 @@ package events
import (
tea "github.com/charmbracelet/bubbletea"
"github.com/lmika/audax/internal/dynamo-browse/services"
"log"
)
@ -22,21 +23,22 @@ func SetTeaMessage(event tea.Msg) tea.Cmd {
}
}
func PromptForInput(prompt string, onDone func(value string) tea.Msg) tea.Msg {
func PromptForInput(prompt string, history services.HistoryProvider, onDone func(value string) tea.Msg) tea.Msg {
return PromptForInputMsg{
Prompt: prompt,
History: history,
OnDone: onDone,
}
}
func Confirm(prompt string, onResult func(yes bool) tea.Msg) tea.Msg {
return PromptForInput(prompt, func(value string) tea.Msg {
return PromptForInput(prompt, nil, 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, nil, func(value string) tea.Msg {
if value == "y" {
return onYes()
}

View file

@ -2,6 +2,7 @@ package events
import (
tea "github.com/charmbracelet/bubbletea"
"github.com/lmika/audax/internal/dynamo-browse/services"
)
// Error indicates that an error occurred
@ -21,6 +22,7 @@ type ModeMessage string
// PromptForInput indicates that the context is requesting a line of input
type PromptForInputMsg struct {
Prompt string
History services.HistoryProvider
OnDone func(value string) tea.Msg
OnCancel func() tea.Msg
}

View file

@ -67,7 +67,7 @@ func (cc *ColumnsController) onNewResultSet(rs *models.ResultSet, op resultSetUp
}
func (cc *ColumnsController) AddColumn(afterIndex int) tea.Msg {
return events.PromptForInput("column expr: ", func(value string) tea.Msg {
return events.PromptForInput("column expr: ", nil, func(value string) tea.Msg {
colExpr, err := queryexpr.Parse(value)
if err != nil {
return events.Error(err)

View file

@ -11,6 +11,7 @@ import (
"github.com/lmika/audax/internal/dynamo-browse/models/attrcodec"
"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/inputhistory"
"github.com/lmika/audax/internal/dynamo-browse/services/itemrenderer"
"github.com/lmika/audax/internal/dynamo-browse/services/viewsnapshot"
bus "github.com/lmika/events"
@ -42,11 +43,17 @@ const (
MarkOpToggle
)
const (
queryInputHistoryCategory = "queries"
filterInputHistoryCategory = "filters"
)
type TableReadController struct {
tableService TableReadService
workspaceService *viewsnapshot.ViewSnapshotService
itemRendererService *itemrenderer.Service
jobController *JobsController
inputHistoryService *inputhistory.Service
eventBus *bus.Bus
tableName string
loadFromLastView bool
@ -63,6 +70,7 @@ func NewTableReadController(
workspaceService *viewsnapshot.ViewSnapshotService,
itemRendererService *itemrenderer.Service,
jobController *JobsController,
inputHistoryService *inputhistory.Service,
eventBus *bus.Bus,
tableName string,
) *TableReadController {
@ -72,6 +80,7 @@ func NewTableReadController(
workspaceService: workspaceService,
itemRendererService: itemRendererService,
jobController: jobController,
inputHistoryService: inputHistoryService,
eventBus: eventBus,
tableName: tableName,
mutex: new(sync.Mutex),
@ -137,6 +146,7 @@ func (c *TableReadController) ScanTable(name string) tea.Msg {
func (c *TableReadController) PromptForQuery() tea.Msg {
return events.PromptForInputMsg{
Prompt: "query: ",
History: c.inputHistoryService.Iter(context.Background(), queryInputHistoryCategory),
OnDone: func(value string) tea.Msg {
resultSet := c.state.ResultSet()
if resultSet == nil {
@ -289,6 +299,7 @@ func (c *TableReadController) Mark(op MarkOp) tea.Msg {
func (c *TableReadController) Filter() tea.Msg {
return events.PromptForInputMsg{
Prompt: "filter: ",
History: c.inputHistoryService.Iter(context.Background(), filterInputHistoryCategory),
OnDone: func(value string) tea.Msg {
resultSet := c.state.ResultSet()
if resultSet == nil {

View file

@ -8,8 +8,10 @@ import (
"github.com/lmika/audax/internal/dynamo-browse/controllers"
"github.com/lmika/audax/internal/dynamo-browse/models"
"github.com/lmika/audax/internal/dynamo-browse/providers/dynamo"
"github.com/lmika/audax/internal/dynamo-browse/providers/inputhistorystore"
"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/services/inputhistory"
"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/scriptmanager"
@ -600,9 +602,12 @@ func newService(t *testing.T, cfg serviceConfig) *services {
resultSetSnapshotStore := workspacestore.NewResultSetSnapshotStore(ws)
settingStore := settingstore.New(ws)
inputHistoryStore := inputhistorystore.NewInputHistoryStore(ws)
workspaceService := viewsnapshot.NewService(resultSetSnapshotStore)
itemRendererService := itemrenderer.NewService(itemrenderer.PlainTextRenderer(), itemrenderer.PlainTextRenderer())
scriptService := scriptmanager.New()
inputHistoryService := inputhistory.New(inputHistoryStore)
client := testdynamo.SetupTestTable(t, testData)
@ -612,14 +617,14 @@ func newService(t *testing.T, cfg serviceConfig) *services {
state := controllers.NewState()
jobsController := controllers.NewJobsController(jobs.NewService(eventBus), eventBus, true)
readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, jobsController, eventBus, cfg.tableName)
readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, jobsController, inputHistoryService, eventBus, cfg.tableName)
writeController := controllers.NewTableWriteController(state, service, jobsController, readController, settingStore)
settingsController := controllers.NewSettingsController(settingStore, eventBus)
columnsController := controllers.NewColumnsController(eventBus)
exportController := controllers.NewExportController(state, columnsController)
scriptController := controllers.NewScriptController(scriptService, readController, settingsController, eventBus)
commandController := commandctrl.NewCommandController()
commandController := commandctrl.NewCommandController(inputHistoryService)
commandController.AddCommandLookupExtension(scriptController)
if cfg.isReadOnly {

View file

@ -194,7 +194,7 @@ func (a irFieldBeginsWith) canBeExecutedAsQuery(info *models.TableInfo, qci *que
return false
}
if keyName == info.Keys.SortKey {
if keyName == info.Keys.SortKey && qci.hasSeenPrimaryKey(info) {
return qci.addKey(info, a.name.keyName())
}

View file

@ -143,6 +143,9 @@ func TestModExpr_Query(t *testing.T) {
scanCase("when request sk equals something", `sk="something"`, `#0 = :0`,
exprNameIsString(0, 0, "sk", "something"),
),
scanCase("when request sk starts with something", `sk^="something"`, `begins_with (#0, :0)`,
exprNameIsString(0, 0, "sk", "something"),
),
scanCase("with not equal", `sk != "something"`, `#0 <> :0`,
exprNameIsString(0, 0, "sk", "something"),
),

View file

@ -69,6 +69,10 @@ func (a *astLiteralValue) String() string {
return *a.StringVal
case a.IntVal != nil:
return strconv.FormatInt(*a.IntVal, 10)
case a.TrueBoolValue:
return "true"
case a.FalseBoolValue:
return "false"
}
return ""
}

View file

@ -0,0 +1,10 @@
package inputhistorystore
import "time"
type inputHistoryItem struct {
ID int `storm:"id,increment"`
Category string `storm:"index"`
Time time.Time
Item string
}

View file

@ -0,0 +1,49 @@
package inputhistorystore
import (
"context"
"github.com/asdine/storm"
"github.com/lmika/audax/internal/common/sliceutils"
"github.com/lmika/audax/internal/common/workspaces"
"github.com/pkg/errors"
"sort"
"time"
)
const inputHistoryStore = "InputHistoryStore"
type Store struct {
ws storm.Node
}
func NewInputHistoryStore(ws *workspaces.Workspace) *Store {
return &Store{
ws: ws.DB().From(inputHistoryStore),
}
}
// Items returns all items from a category ordered by the time
func (as *Store) Items(ctx context.Context, category string) ([]string, error) {
var items []inputHistoryItem
if err := as.ws.Find("Category", category, &items); err != nil {
if errors.Is(err, storm.ErrNotFound) {
return nil, nil
}
return nil, err
}
sort.Slice(items, func(i, j int) bool {
return items[i].Time.Before(items[j].Time)
})
return sliceutils.Map(items, func(t inputHistoryItem) string {
return t.Item
}), nil
}
func (as *Store) PutItem(ctx context.Context, category string, item string) error {
return as.ws.Save(&inputHistoryItem{
Time: time.Now(),
Category: category,
Item: item,
})
}

View file

@ -0,0 +1,13 @@
package services
type HistoryProvider interface {
// Len returns the number of historical items
Len() int
// Item returns the historical item at index 'idx', where items are chronologically ordered such that the
// item at 0 is the oldest item.
Item(idx int) string
// PutItem adds an item to the history
PutItem(item string)
}

View file

@ -0,0 +1,8 @@
package inputhistory
import "context"
type HistoryItemStore interface {
Items(ctx context.Context, category string) ([]string, error)
PutItem(ctx context.Context, category string, item string) error
}

View file

@ -0,0 +1,49 @@
package inputhistory
import (
"context"
"github.com/lmika/audax/internal/dynamo-browse/services"
"log"
"strings"
)
func (svc *Service) Iter(ctx context.Context, category string) services.HistoryProvider {
items, err := svc.store.Items(ctx, category)
if err != nil {
log.Printf("warn: cannot get iter for '%v': %v", category, err)
return nil
}
return &Iter{svc, items, category}
}
func (svc *Service) PutItem(ctx context.Context, category string, value string) error {
return svc.store.PutItem(ctx, category, value)
}
type Iter struct {
svc *Service
items []string
category string
}
func (i *Iter) Len() int {
return len(i.items)
}
func (i *Iter) Item(idx int) string {
return i.items[idx]
}
func (i *Iter) PutItem(item string) {
if strings.TrimSpace(item) == "" {
return
}
if len(i.items) > 0 && i.items[len(i.items)-1] == item {
return
}
if err := i.svc.PutItem(context.Background(), i.category, item); err != nil {
log.Printf("warn: cannot put input history: category = %v, value = %v, err = %v", i.category, item, err)
}
}

View file

@ -0,0 +1,9 @@
package inputhistory
type Service struct {
store HistoryItemStore
}
func New(store HistoryItemStore) *Service {
return &Service{store: store}
}

View file

@ -20,7 +20,7 @@ type StatusAndPrompt struct {
statusMessage string
spinner spinner.Model
spinnerVisible bool
pendingInput *events.PromptForInputMsg
pendingInput *pendingInputState
textInput textinput.Model
width, height int
lastModeLineHeight int
@ -83,15 +83,15 @@ func (s *StatusAndPrompt) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
s.textInput.Prompt = msg.Prompt
s.textInput.Focus()
s.textInput.SetValue("")
s.pendingInput = &msg
s.pendingInput = newPendingInputState(msg)
case tea.KeyMsg:
if s.pendingInput != nil {
switch msg.Type {
case tea.KeyCtrlC, tea.KeyEsc:
if s.pendingInput.OnCancel != nil {
if s.pendingInput.originalMsg.OnCancel != nil {
pendingInput := s.pendingInput
cc.Add(func() tea.Msg {
m := pendingInput.OnCancel()
m := pendingInput.originalMsg.OnCancel()
return m
})
}
@ -100,18 +100,48 @@ func (s *StatusAndPrompt) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
pendingInput := s.pendingInput
s.pendingInput = nil
return s, func() tea.Msg {
m := pendingInput.OnDone(s.textInput.Value())
return m
m := pendingInput.originalMsg.OnDone(s.textInput.Value())
return s, tea.Batch(
events.SetTeaMessage(m),
func() tea.Msg {
if historyProvider := pendingInput.originalMsg.History; historyProvider != nil {
if _, isErrMsg := m.(events.ErrorMsg); !isErrMsg {
historyProvider.PutItem(s.textInput.Value())
}
}
return nil
},
)
case tea.KeyUp:
if historyProvider := s.pendingInput.originalMsg.History; historyProvider != nil && historyProvider.Len() > 0 {
if s.pendingInput.historyIdx < 0 {
s.pendingInput.historyIdx = historyProvider.Len() - 1
} else if s.pendingInput.historyIdx > 0 {
s.pendingInput.historyIdx -= 1
} else {
s.pendingInput.historyIdx = 0
}
s.textInput.SetValue(historyProvider.Item(s.pendingInput.historyIdx))
s.textInput.SetCursor(len(s.textInput.Value()))
}
case tea.KeyDown:
if historyProvider := s.pendingInput.originalMsg.History; historyProvider != nil && historyProvider.Len() > 0 {
if s.pendingInput.historyIdx >= 0 && s.pendingInput.historyIdx < historyProvider.Len()-1 {
s.pendingInput.historyIdx += 1
}
s.textInput.SetValue(historyProvider.Item(s.pendingInput.historyIdx))
s.textInput.SetCursor(len(s.textInput.Value()))
}
default:
if msg.Type == tea.KeyRunes {
msg.Runes = sliceutils.Filter(msg.Runes, func(r rune) bool { return r != '\x0d' && r != '\x0a' })
}
}
newTextInput, cmd := s.textInput.Update(msg)
s.textInput = newTextInput
return s, cmd
}
} else {
s.statusMessage = ""
}

View file

@ -0,0 +1,12 @@
package statusandprompt
import "github.com/lmika/audax/internal/common/ui/events"
type pendingInputState struct {
originalMsg events.PromptForInputMsg
historyIdx int
}
func newPendingInputState(msg events.PromptForInputMsg) *pendingInputState {
return &pendingInputState{originalMsg: msg, historyIdx: -1}
}

View file

@ -56,7 +56,7 @@ func (c *SSMController) ChangePrefix(newPrefix string) tea.Msg {
}
func (c *SSMController) Clone(param models.SSMParameter) tea.Msg {
return events.PromptForInput("New key: ", func(value string) tea.Msg {
return events.PromptForInput("New key: ", nil, func(value string) tea.Msg {
return func() tea.Msg {
ctx := context.Background()
if err := c.service.Clone(ctx, param, value); err != nil {