sqs-browse: started working on put commands
This commit is contained in:
parent
43680000a8
commit
cecdbafabb
|
@ -7,7 +7,9 @@ import (
|
||||||
"github.com/aws/aws-sdk-go-v2/config"
|
"github.com/aws/aws-sdk-go-v2/config"
|
||||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
|
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/lmika/awstools/internal/common/ui/commandctrl"
|
||||||
"github.com/lmika/awstools/internal/common/ui/dispatcher"
|
"github.com/lmika/awstools/internal/common/ui/dispatcher"
|
||||||
|
"github.com/lmika/awstools/internal/common/ui/uimodels"
|
||||||
"github.com/lmika/awstools/internal/dynamo-browse/controllers"
|
"github.com/lmika/awstools/internal/dynamo-browse/controllers"
|
||||||
"github.com/lmika/awstools/internal/dynamo-browse/providers/dynamo"
|
"github.com/lmika/awstools/internal/dynamo-browse/providers/dynamo"
|
||||||
"github.com/lmika/awstools/internal/dynamo-browse/services/tables"
|
"github.com/lmika/awstools/internal/dynamo-browse/services/tables"
|
||||||
|
@ -45,7 +47,13 @@ func main() {
|
||||||
tableReadController := controllers.NewTableReadController(tableService, *flagTable)
|
tableReadController := controllers.NewTableReadController(tableService, *flagTable)
|
||||||
tableWriteController := controllers.NewTableWriteController(tableService, tableReadController, *flagTable)
|
tableWriteController := controllers.NewTableWriteController(tableService, tableReadController, *flagTable)
|
||||||
|
|
||||||
uiModel := ui.NewModel(uiDispatcher, tableReadController, tableWriteController)
|
commandController := commandctrl.NewCommandController(map[string]uimodels.Operation{
|
||||||
|
"scan": tableReadController.Scan(),
|
||||||
|
"rw": tableWriteController.ToggleReadWrite(),
|
||||||
|
"dup": tableWriteController.Duplicate(),
|
||||||
|
})
|
||||||
|
|
||||||
|
uiModel := ui.NewModel(uiDispatcher, commandController, tableReadController, tableWriteController)
|
||||||
p := tea.NewProgram(uiModel, tea.WithAltScreen())
|
p := tea.NewProgram(uiModel, tea.WithAltScreen())
|
||||||
loopback.program = p
|
loopback.program = p
|
||||||
|
|
||||||
|
|
2
go.mod
2
go.mod
|
@ -20,6 +20,7 @@ require (
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/alecthomas/participle/v2 v2.0.0-alpha7 // indirect
|
||||||
github.com/atotto/clipboard v0.1.4 // indirect
|
github.com/atotto/clipboard v0.1.4 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.10.0 // indirect
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.10.0 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.6 // indirect
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.6 // indirect
|
||||||
|
@ -36,6 +37,7 @@ require (
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||||
github.com/juju/ansiterm v0.0.0-20210929141451-8b71cc96ebdc // indirect
|
github.com/juju/ansiterm v0.0.0-20210929141451-8b71cc96ebdc // indirect
|
||||||
|
github.com/lmika/shellwords v0.0.0-20140714114018-ce258dd729fe // indirect
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||||
github.com/lunixbochs/vtclean v1.0.0 // indirect
|
github.com/lunixbochs/vtclean v1.0.0 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||||
|
|
8
go.sum
8
go.sum
|
@ -1,3 +1,7 @@
|
||||||
|
github.com/alecthomas/participle v0.7.1 h1:2bN7reTw//5f0cugJcTOnY/NYZcWQOaajW+BwZB5xWs=
|
||||||
|
github.com/alecthomas/participle/v2 v2.0.0-alpha7 h1:cK4vjj0VSgb3lN1nuKA5F7dw+1s1pWBe5bx7nNCnN+c=
|
||||||
|
github.com/alecthomas/participle/v2 v2.0.0-alpha7/go.mod h1:NumScqsC42o9x+dGj8/YqsIfhrIQjFEOFovxotbBirA=
|
||||||
|
github.com/alecthomas/repr v0.0.0-20181024024818-d37bc2a10ba1/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ=
|
||||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||||
github.com/aws/aws-sdk-go-v2 v1.13.0 h1:1XIXAfxsEmbhbj5ry3D3vX+6ZcUYvIqSm4CWWEuGZCA=
|
github.com/aws/aws-sdk-go-v2 v1.13.0 h1:1XIXAfxsEmbhbj5ry3D3vX+6ZcUYvIqSm4CWWEuGZCA=
|
||||||
|
@ -75,6 +79,8 @@ github.com/lmika/events v0.0.0-20200906102219-a2269cd4394e h1:0QkUe2ejnT/i+xbgGy
|
||||||
github.com/lmika/events v0.0.0-20200906102219-a2269cd4394e/go.mod h1:qtkBmNC9OfD0STtOR9sF55pQchjIfNlC3gzm4n8CrqM=
|
github.com/lmika/events v0.0.0-20200906102219-a2269cd4394e/go.mod h1:qtkBmNC9OfD0STtOR9sF55pQchjIfNlC3gzm4n8CrqM=
|
||||||
github.com/lmika/gopkgs v0.0.0-20211210041137-0dc91e939890 h1:mwl/exYV/WkBMeShqK7q+B2w2r+b0vP1TSA7clBn9kI=
|
github.com/lmika/gopkgs v0.0.0-20211210041137-0dc91e939890 h1:mwl/exYV/WkBMeShqK7q+B2w2r+b0vP1TSA7clBn9kI=
|
||||||
github.com/lmika/gopkgs v0.0.0-20211210041137-0dc91e939890/go.mod h1:FH6OJSvYcJ9xY8CGs9yGgR89kMCK1UimuUQ6kE5YuJQ=
|
github.com/lmika/gopkgs v0.0.0-20211210041137-0dc91e939890/go.mod h1:FH6OJSvYcJ9xY8CGs9yGgR89kMCK1UimuUQ6kE5YuJQ=
|
||||||
|
github.com/lmika/shellwords v0.0.0-20140714114018-ce258dd729fe h1:1UXS/6OFkbi6JrihPykmYO1VtsABB02QQ+YmYYzTY18=
|
||||||
|
github.com/lmika/shellwords v0.0.0-20140714114018-ce258dd729fe/go.mod h1:qpdOkLougV5Yry4Px9f1w1pNMavcr6Z67VW5Ro+vW5I=
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
github.com/lunixbochs/vtclean v1.0.0 h1:xu2sLAri4lGiovBDQKxl5mrXyESr3gUr5m5SM5+LVb8=
|
github.com/lunixbochs/vtclean v1.0.0 h1:xu2sLAri4lGiovBDQKxl5mrXyESr3gUr5m5SM5+LVb8=
|
||||||
|
@ -106,6 +112,7 @@ github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
|
github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
|
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
@ -121,6 +128,7 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|
48
internal/common/ui/commandctrl/commandctrl.go
Normal file
48
internal/common/ui/commandctrl/commandctrl.go
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
package commandctrl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/lmika/awstools/internal/common/ui/events"
|
||||||
|
"github.com/lmika/awstools/internal/common/ui/uimodels"
|
||||||
|
"github.com/lmika/shellwords"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CommandController struct {
|
||||||
|
commands map[string]uimodels.Operation
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCommandController(commands map[string]uimodels.Operation) *CommandController {
|
||||||
|
return &CommandController{
|
||||||
|
commands: commands,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CommandController) Prompt() uimodels.Operation {
|
||||||
|
return uimodels.OperationFn(func(ctx context.Context) error {
|
||||||
|
uiCtx := uimodels.Ctx(ctx)
|
||||||
|
uiCtx.Send(events.PromptForInput{
|
||||||
|
Prompt: ":",
|
||||||
|
OnDone: c.Execute(),
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CommandController) Execute() uimodels.Operation {
|
||||||
|
return uimodels.OperationFn(func(ctx context.Context) error {
|
||||||
|
input := strings.TrimSpace(uimodels.PromptValue(ctx))
|
||||||
|
if input == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
tokens := shellwords.Split(input)
|
||||||
|
command, ok := c.commands[tokens[0]]
|
||||||
|
if !ok {
|
||||||
|
return errors.New("no such command: " + tokens[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
return command.Execute(WithCommandArgs(ctx, tokens[1:]))
|
||||||
|
})
|
||||||
|
}
|
25
internal/common/ui/commandctrl/commandctrl_test.go
Normal file
25
internal/common/ui/commandctrl/commandctrl_test.go
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
package commandctrl_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/lmika/awstools/internal/common/ui/commandctrl"
|
||||||
|
"github.com/lmika/awstools/internal/common/ui/events"
|
||||||
|
"github.com/lmika/awstools/test/testuictx"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCommandController_Prompt(t *testing.T) {
|
||||||
|
t.Run("prompt user for a command", func(t *testing.T) {
|
||||||
|
cmd := commandctrl.NewCommandController()
|
||||||
|
|
||||||
|
ctx, uiCtx := testuictx.New(context.Background())
|
||||||
|
err := cmd.Prompt().Execute(ctx)
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
promptMsg, ok := uiCtx.Messages[0].(events.PromptForInput)
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, ":", promptMsg.Prompt)
|
||||||
|
})
|
||||||
|
}
|
15
internal/common/ui/commandctrl/context.go
Normal file
15
internal/common/ui/commandctrl/context.go
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
package commandctrl
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
type commandArgContextKeyType struct {}
|
||||||
|
var commandArgContextKey = commandArgContextKeyType{}
|
||||||
|
|
||||||
|
func WithCommandArgs(ctx context.Context, args []string) context.Context {
|
||||||
|
return context.WithValue(ctx, commandArgContextKey, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
func CommandArgs(ctx context.Context) []string {
|
||||||
|
args, _ := ctx.Value(commandArgContextKey).([]string)
|
||||||
|
return args
|
||||||
|
}
|
|
@ -3,6 +3,7 @@ package controllers
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"github.com/lmika/awstools/internal/common/ui/uimodels"
|
"github.com/lmika/awstools/internal/common/ui/uimodels"
|
||||||
|
"github.com/lmika/awstools/internal/dynamo-browse/models/modexpr"
|
||||||
"github.com/lmika/awstools/internal/dynamo-browse/services/tables"
|
"github.com/lmika/awstools/internal/dynamo-browse/services/tables"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
@ -21,12 +22,67 @@ func NewTableWriteController(tableService *tables.Service, tableReadControllers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *TableWriteController) EnableReadWrite() uimodels.Operation {
|
func (c *TableWriteController) ToggleReadWrite() uimodels.Operation {
|
||||||
return uimodels.OperationFn(func(ctx context.Context) error {
|
return uimodels.OperationFn(func(ctx context.Context) error {
|
||||||
uiCtx := uimodels.Ctx(ctx)
|
uiCtx := uimodels.Ctx(ctx)
|
||||||
uiCtx.Send(SetReadWrite{NewValue: true})
|
state := CurrentState(ctx)
|
||||||
uiCtx.Message("read/write mode enabled")
|
|
||||||
|
|
||||||
|
if state.InReadWriteMode {
|
||||||
|
uiCtx.Send(SetReadWrite{NewValue: false})
|
||||||
|
uiCtx.Message("read/write mode disabled")
|
||||||
|
} else {
|
||||||
|
uiCtx.Send(SetReadWrite{NewValue: true})
|
||||||
|
uiCtx.Message("read/write mode enabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TableWriteController) Duplicate() uimodels.Operation {
|
||||||
|
return uimodels.OperationFn(func(ctx context.Context) error {
|
||||||
|
uiCtx := uimodels.Ctx(ctx)
|
||||||
|
state := CurrentState(ctx)
|
||||||
|
|
||||||
|
if state.SelectedItem == nil {
|
||||||
|
return errors.New("no selected item")
|
||||||
|
} else if !state.InReadWriteMode {
|
||||||
|
return errors.New("not in read/write mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
uiCtx.Input("Dup: ", uimodels.OperationFn(func(ctx context.Context) error {
|
||||||
|
modExpr, err := modexpr.Parse(uimodels.PromptValue(ctx))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
newItem, err := modExpr.Patch(state.SelectedItem)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: preview new item
|
||||||
|
|
||||||
|
uiCtx := uimodels.Ctx(ctx)
|
||||||
|
uiCtx.Input("Put item? ", uimodels.OperationFn(func(ctx context.Context) error {
|
||||||
|
if uimodels.PromptValue(ctx) != "y" {
|
||||||
|
return errors.New("operation aborted")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the item
|
||||||
|
if err := c.tableService.Put(ctx, c.tableName, newItem); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rescan to get updated items
|
||||||
|
if err := c.tableReadControllers.doScan(ctx, true); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}))
|
||||||
|
return nil
|
||||||
|
}))
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,21 +13,33 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestTableWriteController_EnableReadWrite(t *testing.T) {
|
func TestTableWriteController_ToggleReadWrite(t *testing.T) {
|
||||||
twc, _, closeFn := setupController(t)
|
twc, _, closeFn := setupController(t)
|
||||||
t.Cleanup(closeFn)
|
t.Cleanup(closeFn)
|
||||||
|
|
||||||
t.Run("should send event enabling read write", func(t *testing.T) {
|
t.Run("should enabling read write if disabled", func(t *testing.T) {
|
||||||
ctx, uiCtx := testuictx.New(context.Background())
|
ctx, uiCtx := testuictx.New(context.Background())
|
||||||
ctx = controllers.ContextWithState(ctx, controllers.State{
|
ctx = controllers.ContextWithState(ctx, controllers.State{
|
||||||
InReadWriteMode: false,
|
InReadWriteMode: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
err := twc.EnableReadWrite().Execute(ctx)
|
err := twc.ToggleReadWrite().Execute(ctx)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
assert.Contains(t, uiCtx.Messages, controllers.SetReadWrite{NewValue: true})
|
assert.Contains(t, uiCtx.Messages, controllers.SetReadWrite{NewValue: true})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("should disable read write if enabled", func(t *testing.T) {
|
||||||
|
ctx, uiCtx := testuictx.New(context.Background())
|
||||||
|
ctx = controllers.ContextWithState(ctx, controllers.State{
|
||||||
|
InReadWriteMode: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
err := twc.ToggleReadWrite().Execute(ctx)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Contains(t, uiCtx.Messages, controllers.SetReadWrite{NewValue: false})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTableWriteController_Delete(t *testing.T) {
|
func TestTableWriteController_Delete(t *testing.T) {
|
||||||
|
|
|
@ -9,3 +9,15 @@ type ResultSet struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type Item map[string]types.AttributeValue
|
type Item map[string]types.AttributeValue
|
||||||
|
|
||||||
|
// Clone creates a clone of the current item
|
||||||
|
func (i Item) Clone() Item {
|
||||||
|
newItem := Item{}
|
||||||
|
|
||||||
|
// TODO: should be a deep clone?
|
||||||
|
for k, v := range i {
|
||||||
|
newItem[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
return newItem
|
||||||
|
}
|
||||||
|
|
39
internal/dynamo-browse/models/modexpr/ast.go
Normal file
39
internal/dynamo-browse/models/modexpr/ast.go
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
package modexpr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/alecthomas/participle/v2"
|
||||||
|
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type astExpr struct {
|
||||||
|
Attributes []*astAttribute `parser:"@@ (',' @@)*"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type astAttribute struct {
|
||||||
|
Name string `parser:"@Ident '='"`
|
||||||
|
Value string `parser:"@String"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a astAttribute) dynamoValue() (types.AttributeValue, error) {
|
||||||
|
// TODO: should be based on type
|
||||||
|
s, err := strconv.Unquote(a.Value)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "cannot unquote string")
|
||||||
|
}
|
||||||
|
return &types.AttributeValueMemberS{Value: s}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var parser = participle.MustBuild(&astExpr{})
|
||||||
|
|
||||||
|
func Parse(expr string) (*ModExpr, error) {
|
||||||
|
var ast astExpr
|
||||||
|
|
||||||
|
if err := parser.ParseString("expr", expr, &ast); err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "cannot parse expression: '%v'", expr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ModExpr{ast: &ast}, nil
|
||||||
|
}
|
22
internal/dynamo-browse/models/modexpr/expr.go
Normal file
22
internal/dynamo-browse/models/modexpr/expr.go
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
package modexpr
|
||||||
|
|
||||||
|
import "github.com/lmika/awstools/internal/dynamo-browse/models"
|
||||||
|
|
||||||
|
type ModExpr struct {
|
||||||
|
ast *astExpr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (me *ModExpr) Patch(item models.Item) (models.Item, error) {
|
||||||
|
newItem := item.Clone()
|
||||||
|
|
||||||
|
for _, attribute := range me.ast.Attributes {
|
||||||
|
var err error
|
||||||
|
name := attribute.Name
|
||||||
|
newItem[name], err = attribute.dynamoValue()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newItem, nil
|
||||||
|
}
|
39
internal/dynamo-browse/models/modexpr/expr_test.go
Normal file
39
internal/dynamo-browse/models/modexpr/expr_test.go
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
package modexpr_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||||
|
"github.com/lmika/awstools/internal/dynamo-browse/models"
|
||||||
|
"github.com/lmika/awstools/internal/dynamo-browse/models/modexpr"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestModExpr_Patch(t *testing.T) {
|
||||||
|
t.Run("patch with new attributes", func(t *testing.T) {
|
||||||
|
modExpr, err := modexpr.Parse(`alpha="new value", beta="another new value"`)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
oldItem := models.Item{}
|
||||||
|
newItem, err := modExpr.Patch(oldItem)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, "new value", newItem["alpha"].(*types.AttributeValueMemberS).Value)
|
||||||
|
assert.Equal(t, "another new value", newItem["beta"].(*types.AttributeValueMemberS).Value)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("patch with existing attributes", func(t *testing.T) {
|
||||||
|
modExpr, err := modexpr.Parse(`alpha="new value", beta="another new value"`)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
oldItem := models.Item{
|
||||||
|
"old": &types.AttributeValueMemberS{Value: "before"},
|
||||||
|
"beta": &types.AttributeValueMemberS{Value: "before beta"},
|
||||||
|
}
|
||||||
|
newItem, err := modExpr.Patch(oldItem)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, "before", newItem["old"].(*types.AttributeValueMemberS).Value)
|
||||||
|
assert.Equal(t, "new value", newItem["alpha"].(*types.AttributeValueMemberS).Value)
|
||||||
|
assert.Equal(t, "another new value", newItem["beta"].(*types.AttributeValueMemberS).Value)
|
||||||
|
})
|
||||||
|
}
|
|
@ -13,6 +13,17 @@ type Provider struct {
|
||||||
client *dynamodb.Client
|
client *dynamodb.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Provider) PutItem(ctx context.Context, name string, item models.Item) error {
|
||||||
|
_, err := p.client.PutItem(ctx, &dynamodb.PutItemInput{
|
||||||
|
TableName: aws.String(name),
|
||||||
|
Item: item,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "cannot execute put on table %v", name)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func NewProvider(client *dynamodb.Client) *Provider {
|
func NewProvider(client *dynamodb.Client) *Provider {
|
||||||
return &Provider{client: client}
|
return &Provider{client: client}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,4 +9,5 @@ import (
|
||||||
type TableProvider interface {
|
type TableProvider interface {
|
||||||
ScanItems(ctx context.Context, tableName string) ([]models.Item, error)
|
ScanItems(ctx context.Context, tableName string) ([]models.Item, error)
|
||||||
DeleteItem(ctx context.Context, tableName string, key map[string]types.AttributeValue) error
|
DeleteItem(ctx context.Context, tableName string, key map[string]types.AttributeValue) error
|
||||||
|
PutItem(ctx context.Context, name string, item models.Item) error
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,6 +60,10 @@ func (s *Service) Scan(ctx context.Context, table string) (*models.ResultSet, er
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) Put(ctx context.Context, tableName string, item models.Item) error {
|
||||||
|
return s.provider.PutItem(ctx, tableName, item)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Service) Delete(ctx context.Context, name string, item models.Item) error {
|
func (s *Service) Delete(ctx context.Context, name string, item models.Item) error {
|
||||||
// TODO: do not hardcode keys
|
// TODO: do not hardcode keys
|
||||||
return s.provider.DeleteItem(ctx, name, map[string]types.AttributeValue{
|
return s.provider.DeleteItem(ctx, name, map[string]types.AttributeValue{
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"github.com/charmbracelet/bubbles/viewport"
|
"github.com/charmbracelet/bubbles/viewport"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
"github.com/lmika/awstools/internal/common/ui/commandctrl"
|
||||||
"github.com/lmika/awstools/internal/common/ui/dispatcher"
|
"github.com/lmika/awstools/internal/common/ui/dispatcher"
|
||||||
"github.com/lmika/awstools/internal/common/ui/events"
|
"github.com/lmika/awstools/internal/common/ui/events"
|
||||||
"github.com/lmika/awstools/internal/common/ui/uimodels"
|
"github.com/lmika/awstools/internal/common/ui/uimodels"
|
||||||
|
@ -19,13 +20,13 @@ import (
|
||||||
|
|
||||||
var (
|
var (
|
||||||
activeHeaderStyle = lipgloss.NewStyle().
|
activeHeaderStyle = lipgloss.NewStyle().
|
||||||
Bold(true).
|
Bold(true).
|
||||||
Foreground(lipgloss.Color("#ffffff")).
|
Foreground(lipgloss.Color("#ffffff")).
|
||||||
Background(lipgloss.Color("#4479ff"))
|
Background(lipgloss.Color("#4479ff"))
|
||||||
|
|
||||||
inactiveHeaderStyle = lipgloss.NewStyle().
|
inactiveHeaderStyle = lipgloss.NewStyle().
|
||||||
Foreground(lipgloss.Color("#000000")).
|
Foreground(lipgloss.Color("#000000")).
|
||||||
Background(lipgloss.Color("#d1d1d1"))
|
Background(lipgloss.Color("#d1d1d1"))
|
||||||
)
|
)
|
||||||
|
|
||||||
type uiModel struct {
|
type uiModel struct {
|
||||||
|
@ -34,20 +35,21 @@ type uiModel struct {
|
||||||
|
|
||||||
tableWidth, tableHeight int
|
tableWidth, tableHeight int
|
||||||
|
|
||||||
ready bool
|
ready bool
|
||||||
//resultSet *models.ResultSet
|
//resultSet *models.ResultSet
|
||||||
state controllers.State
|
state controllers.State
|
||||||
message string
|
message string
|
||||||
|
|
||||||
pendingInput *events.PromptForInput
|
pendingInput *events.PromptForInput
|
||||||
textInput textinput.Model
|
textInput textinput.Model
|
||||||
|
|
||||||
dispatcher *dispatcher.Dispatcher
|
dispatcher *dispatcher.Dispatcher
|
||||||
|
commandController *commandctrl.CommandController
|
||||||
tableReadController *controllers.TableReadController
|
tableReadController *controllers.TableReadController
|
||||||
tableWriteController *controllers.TableWriteController
|
tableWriteController *controllers.TableWriteController
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewModel(dispatcher *dispatcher.Dispatcher, tableReadController *controllers.TableReadController, tableWriteController *controllers.TableWriteController) tea.Model {
|
func NewModel(dispatcher *dispatcher.Dispatcher, commandController *commandctrl.CommandController, tableReadController *controllers.TableReadController, tableWriteController *controllers.TableWriteController) tea.Model {
|
||||||
tbl := table.New([]string{"pk", "sk"}, 100, 20)
|
tbl := table.New([]string{"pk", "sk"}, 100, 20)
|
||||||
rows := make([]table.Row, 0)
|
rows := make([]table.Row, 0)
|
||||||
tbl.SetRows(rows)
|
tbl.SetRows(rows)
|
||||||
|
@ -60,6 +62,7 @@ func NewModel(dispatcher *dispatcher.Dispatcher, tableReadController *controller
|
||||||
textInput: textInput,
|
textInput: textInput,
|
||||||
|
|
||||||
dispatcher: dispatcher,
|
dispatcher: dispatcher,
|
||||||
|
commandController: commandController,
|
||||||
tableReadController: tableReadController,
|
tableReadController: tableReadController,
|
||||||
tableWriteController: tableWriteController,
|
tableWriteController: tableWriteController,
|
||||||
}
|
}
|
||||||
|
@ -174,7 +177,7 @@ func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
case "ctrl+c", "esc":
|
case "ctrl+c", "esc":
|
||||||
m.pendingInput = nil
|
m.pendingInput = nil
|
||||||
case "enter":
|
case "enter":
|
||||||
m.dispatcher.Start(uimodels.WithPromptValue(context.Background(), m.textInput.Value()), m.pendingInput.OnDone)
|
m.invokeOperation(uimodels.WithPromptValue(context.Background(), m.textInput.Value()), m.pendingInput.OnDone)
|
||||||
m.pendingInput = nil
|
m.pendingInput = nil
|
||||||
default:
|
default:
|
||||||
m.textInput, textInputCommands = m.textInput.Update(msg)
|
m.textInput, textInputCommands = m.textInput.Update(msg)
|
||||||
|
@ -193,12 +196,12 @@ func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
m.updateViewportToSelectedMessage()
|
m.updateViewportToSelectedMessage()
|
||||||
|
|
||||||
// TODO: these should be moved somewhere else
|
// TODO: these should be moved somewhere else
|
||||||
|
case ":":
|
||||||
|
m.invokeOperation(context.Background(), m.commandController.Prompt())
|
||||||
case "s":
|
case "s":
|
||||||
m.invokeOperation(m.tableReadController.Scan())
|
m.invokeOperation(context.Background(), m.tableReadController.Scan())
|
||||||
case "D":
|
case "D":
|
||||||
m.invokeOperation(m.tableWriteController.Delete())
|
m.invokeOperation(context.Background(), m.tableWriteController.Delete())
|
||||||
case "w":
|
|
||||||
m.invokeOperation(m.tableWriteController.EnableReadWrite())
|
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
m.textInput, textInputCommands = m.textInput.Update(msg)
|
m.textInput, textInputCommands = m.textInput.Update(msg)
|
||||||
|
@ -213,13 +216,13 @@ func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
return m, tea.Batch(textInputCommands, tableMsgs, viewportMsgs)
|
return m, tea.Batch(textInputCommands, tableMsgs, viewportMsgs)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m uiModel) invokeOperation(op uimodels.Operation) {
|
func (m uiModel) invokeOperation(ctx context.Context, op uimodels.Operation) {
|
||||||
state := m.state
|
state := m.state
|
||||||
if selectedItem, ok := m.selectedItem(); ok {
|
if selectedItem, ok := m.selectedItem(); ok {
|
||||||
state.SelectedItem = selectedItem.item
|
state.SelectedItem = selectedItem.item
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := controllers.ContextWithState(context.Background(), state)
|
ctx = controllers.ContextWithState(ctx, state)
|
||||||
m.dispatcher.Start(ctx, op)
|
m.dispatcher.Start(ctx, op)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
package ui
|
package ui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
table "github.com/calyptia/go-bubble-table"
|
table "github.com/calyptia/go-bubble-table"
|
||||||
"github.com/charmbracelet/bubbles/textinput"
|
"github.com/charmbracelet/bubbles/textinput"
|
||||||
"github.com/charmbracelet/bubbles/viewport"
|
"github.com/charmbracelet/bubbles/viewport"
|
||||||
|
@ -67,7 +69,13 @@ func (m uiModel) Init() tea.Cmd {
|
||||||
|
|
||||||
func (m *uiModel) updateViewportToSelectedMessage() {
|
func (m *uiModel) updateViewportToSelectedMessage() {
|
||||||
if message, ok := m.selectedMessage(); ok {
|
if message, ok := m.selectedMessage(); ok {
|
||||||
m.viewport.SetContent(message.Data)
|
// TODO: not all messages are JSON
|
||||||
|
formattedJson := new(bytes.Buffer)
|
||||||
|
if err := json.Indent(formattedJson, []byte(message.Data), "", " "); err == nil {
|
||||||
|
m.viewport.SetContent(formattedJson.String())
|
||||||
|
} else {
|
||||||
|
m.viewport.SetContent(message.Data)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
m.viewport.SetContent("(no message selected)")
|
m.viewport.SetContent("(no message selected)")
|
||||||
}
|
}
|
||||||
|
@ -204,8 +212,8 @@ func (m uiModel) headerView() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m uiModel) splitterView() string {
|
func (m uiModel) splitterView() string {
|
||||||
title := activeHeaderStyle.Render("Message")
|
title := inactiveHeaderStyle.Render("Message")
|
||||||
line := activeHeaderStyle.Render(strings.Repeat(" ", max(0, m.viewport.Width-lipgloss.Width(title))))
|
line := inactiveHeaderStyle.Render(strings.Repeat(" ", max(0, m.viewport.Width-lipgloss.Width(title))))
|
||||||
return lipgloss.JoinHorizontal(lipgloss.Left, title, line)
|
return lipgloss.JoinHorizontal(lipgloss.Left, title, line)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue