sqs-browse: a lot of work to try to keep UI complexity down
Added the notion of controllers and a dispatcher which will queue up operations
This commit is contained in:
parent
1969504611
commit
7526c095ee
|
@ -7,6 +7,8 @@ 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/dispatcher"
|
||||||
|
"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"
|
||||||
"github.com/lmika/awstools/internal/dynamo-browse/ui"
|
"github.com/lmika/awstools/internal/dynamo-browse/ui"
|
||||||
|
@ -38,7 +40,12 @@ func main() {
|
||||||
tableService := tables.NewService(dynamoProvider)
|
tableService := tables.NewService(dynamoProvider)
|
||||||
|
|
||||||
loopback := &msgLoopback{}
|
loopback := &msgLoopback{}
|
||||||
uiModel := ui.NewModel(tableService, loopback, *flagTable)
|
uiDispatcher := dispatcher.NewDispatcher(loopback)
|
||||||
|
|
||||||
|
tableReadController := controllers.NewTableReadController(tableService, *flagTable)
|
||||||
|
tableWriteController := controllers.NewTableWriteController(tableService, tableReadController, *flagTable)
|
||||||
|
|
||||||
|
uiModel := ui.NewModel(uiDispatcher, tableReadController, tableWriteController)
|
||||||
p := tea.NewProgram(uiModel, tea.WithAltScreen())
|
p := tea.NewProgram(uiModel, tea.WithAltScreen())
|
||||||
loopback.program = p
|
loopback.program = p
|
||||||
|
|
||||||
|
|
|
@ -7,9 +7,12 @@ 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/sqs"
|
"github.com/aws/aws-sdk-go-v2/service/sqs"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/lmika/awstools/internal/common/ui/dispatcher"
|
||||||
|
"github.com/lmika/awstools/internal/sqs-browse/controllers"
|
||||||
"github.com/lmika/awstools/internal/sqs-browse/models"
|
"github.com/lmika/awstools/internal/sqs-browse/models"
|
||||||
"github.com/lmika/awstools/internal/sqs-browse/providers/memstore"
|
"github.com/lmika/awstools/internal/sqs-browse/providers/memstore"
|
||||||
sqsprovider "github.com/lmika/awstools/internal/sqs-browse/providers/sqs"
|
sqsprovider "github.com/lmika/awstools/internal/sqs-browse/providers/sqs"
|
||||||
|
"github.com/lmika/awstools/internal/sqs-browse/services/messages"
|
||||||
"github.com/lmika/awstools/internal/sqs-browse/services/pollmessage"
|
"github.com/lmika/awstools/internal/sqs-browse/services/pollmessage"
|
||||||
"github.com/lmika/awstools/internal/sqs-browse/ui"
|
"github.com/lmika/awstools/internal/sqs-browse/ui"
|
||||||
"github.com/lmika/events"
|
"github.com/lmika/events"
|
||||||
|
@ -20,6 +23,7 @@ import (
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
var flagQueue = flag.String("q", "", "queue to poll")
|
var flagQueue = flag.String("q", "", "queue to poll")
|
||||||
|
var flagTarget = flag.String("t", "", "target queue to push to")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
@ -32,12 +36,19 @@ func main() {
|
||||||
bus := events.New()
|
bus := events.New()
|
||||||
|
|
||||||
msgStore := memstore.NewStore()
|
msgStore := memstore.NewStore()
|
||||||
msgPoller := sqsprovider.NewProvider(sqsClient)
|
sqsProvider := sqsprovider.NewProvider(sqsClient)
|
||||||
|
|
||||||
pollService := pollmessage.NewService(msgStore, msgPoller, *flagQueue, bus)
|
messageService := messages.NewService(sqsProvider)
|
||||||
|
pollService := pollmessage.NewService(msgStore, sqsProvider, *flagQueue, bus)
|
||||||
|
|
||||||
uiModel := ui.NewModel()
|
msgSendingHandlers := controllers.NewMessageSendingController(messageService, *flagTarget)
|
||||||
|
|
||||||
|
loopback := &msgLoopback{}
|
||||||
|
uiDispatcher := dispatcher.NewDispatcher(loopback)
|
||||||
|
|
||||||
|
uiModel := ui.NewModel(uiDispatcher, msgSendingHandlers)
|
||||||
p := tea.NewProgram(uiModel, tea.WithAltScreen())
|
p := tea.NewProgram(uiModel, tea.WithAltScreen())
|
||||||
|
loopback.program = p
|
||||||
|
|
||||||
bus.On("new-messages", func(m []*models.Message) { p.Send(ui.NewMessagesEvent(m)) })
|
bus.On("new-messages", func(m []*models.Message) { p.Send(ui.NewMessagesEvent(m)) })
|
||||||
|
|
||||||
|
@ -58,4 +69,12 @@ func main() {
|
||||||
fmt.Printf("Alas, there's been an error: %v", err)
|
fmt.Printf("Alas, there's been an error: %v", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type msgLoopback struct {
|
||||||
|
program *tea.Program
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *msgLoopback) Send(msg tea.Msg) {
|
||||||
|
m.program.Send(msg)
|
||||||
|
}
|
||||||
|
|
1
go.mod
1
go.mod
|
@ -3,6 +3,7 @@ module github.com/lmika/awstools
|
||||||
go 1.17
|
go 1.17
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/atotto/clipboard v0.1.4 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2 v1.15.0 // indirect
|
github.com/aws/aws-sdk-go-v2 v1.15.0 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.13.1 // indirect
|
github.com/aws/aws-sdk-go-v2/config v1.13.1 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.8.0 // indirect
|
github.com/aws/aws-sdk-go-v2/credentials v1.8.0 // indirect
|
||||||
|
|
1
go.sum
1
go.sum
|
@ -1,3 +1,4 @@
|
||||||
|
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=
|
||||||
github.com/aws/aws-sdk-go-v2 v1.13.0/go.mod h1:L6+ZpqHaLbAaxsqV0L4cvxZY7QupWJB4fhkf8LXvC7w=
|
github.com/aws/aws-sdk-go-v2 v1.13.0/go.mod h1:L6+ZpqHaLbAaxsqV0L4cvxZY7QupWJB4fhkf8LXvC7w=
|
||||||
|
|
30
internal/common/ui/dispatcher/context.go
Normal file
30
internal/common/ui/dispatcher/context.go
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
package dispatcher
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/lmika/awstools/internal/common/ui/events"
|
||||||
|
"github.com/lmika/awstools/internal/common/ui/uimodels"
|
||||||
|
)
|
||||||
|
|
||||||
|
type dispatcherContext struct {
|
||||||
|
d *Dispatcher
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dc dispatcherContext) Messagef(format string, args ...interface{}) {
|
||||||
|
dc.d.publisher.Send(events.Message(fmt.Sprintf(format, args...)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dc dispatcherContext) Send(teaMessage tea.Msg) {
|
||||||
|
dc.d.publisher.Send(teaMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dc dispatcherContext) Message(msg string) {
|
||||||
|
dc.d.publisher.Send(events.Message(msg))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dc dispatcherContext) Input(prompt string, onDone uimodels.Operation) {
|
||||||
|
dc.d.publisher.Send(events.PromptForInput{
|
||||||
|
OnDone: onDone,
|
||||||
|
})
|
||||||
|
}
|
49
internal/common/ui/dispatcher/dispatcher.go
Normal file
49
internal/common/ui/dispatcher/dispatcher.go
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
package dispatcher
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/lmika/awstools/internal/common/ui/events"
|
||||||
|
"github.com/lmika/awstools/internal/common/ui/uimodels"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Dispatcher struct {
|
||||||
|
mutex *sync.Mutex
|
||||||
|
runningOp uimodels.Operation
|
||||||
|
publisher MessagePublisher
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func NewDispatcher(publisher MessagePublisher) *Dispatcher {
|
||||||
|
return &Dispatcher{
|
||||||
|
mutex: new(sync.Mutex),
|
||||||
|
publisher: publisher,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Dispatcher) Start(ctx context.Context, operation uimodels.Operation) {
|
||||||
|
d.mutex.Lock()
|
||||||
|
defer d.mutex.Unlock()
|
||||||
|
|
||||||
|
if d.runningOp != nil {
|
||||||
|
d.publisher.Send(events.Error(errors.New("operation already running")))
|
||||||
|
}
|
||||||
|
|
||||||
|
d.runningOp = operation
|
||||||
|
go func() {
|
||||||
|
subCtx := uimodels.WithContext(ctx, dispatcherContext{d})
|
||||||
|
|
||||||
|
err := operation.Execute(subCtx)
|
||||||
|
if err != nil {
|
||||||
|
d.publisher.Send(events.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
d.mutex.Lock()
|
||||||
|
defer d.mutex.Unlock()
|
||||||
|
d.runningOp = nil
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
7
internal/common/ui/dispatcher/iface.go
Normal file
7
internal/common/ui/dispatcher/iface.go
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
package dispatcher
|
||||||
|
|
||||||
|
import tea "github.com/charmbracelet/bubbletea"
|
||||||
|
|
||||||
|
type MessagePublisher interface {
|
||||||
|
Send(msg tea.Msg)
|
||||||
|
}
|
16
internal/common/ui/events/errors.go
Normal file
16
internal/common/ui/events/errors.go
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
package events
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/lmika/awstools/internal/common/ui/uimodels"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Error indicates that an error occurred
|
||||||
|
type Error error
|
||||||
|
|
||||||
|
// Message indicates that a message should be shown to the user
|
||||||
|
type Message string
|
||||||
|
|
||||||
|
// PromptForInput indicates that the context is requesting a line of input
|
||||||
|
type PromptForInput struct {
|
||||||
|
OnDone uimodels.Operation
|
||||||
|
}
|
15
internal/common/ui/uimodels/context.go
Normal file
15
internal/common/ui/uimodels/context.go
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
package uimodels
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
type uiContextKeyType struct {}
|
||||||
|
var uiContextKey = uiContextKeyType{}
|
||||||
|
|
||||||
|
func Ctx(ctx context.Context) UIContext {
|
||||||
|
uiCtx, _ := ctx.Value(uiContextKey).(UIContext)
|
||||||
|
return uiCtx
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithContext(ctx context.Context, uiContext UIContext) context.Context {
|
||||||
|
return context.WithValue(ctx, uiContextKey, uiContext)
|
||||||
|
}
|
10
internal/common/ui/uimodels/iface.go
Normal file
10
internal/common/ui/uimodels/iface.go
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
package uimodels
|
||||||
|
|
||||||
|
import tea "github.com/charmbracelet/bubbletea"
|
||||||
|
|
||||||
|
type UIContext interface {
|
||||||
|
Send(teaMessage tea.Msg)
|
||||||
|
Message(msg string)
|
||||||
|
Messagef(format string, args ...interface{})
|
||||||
|
Input(prompt string, onDone Operation)
|
||||||
|
}
|
14
internal/common/ui/uimodels/operations.go
Normal file
14
internal/common/ui/uimodels/operations.go
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
package uimodels
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
type Operation interface {
|
||||||
|
Execute(ctx context.Context) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type OperationFn func(ctx context.Context) error
|
||||||
|
|
||||||
|
func (f OperationFn) Execute(ctx context.Context) error {
|
||||||
|
return f(ctx)
|
||||||
|
}
|
||||||
|
|
15
internal/common/ui/uimodels/promptvalue.go
Normal file
15
internal/common/ui/uimodels/promptvalue.go
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
package uimodels
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
type promptValueKeyType struct {}
|
||||||
|
var promptValueKey = promptValueKeyType{}
|
||||||
|
|
||||||
|
func PromptValue(ctx context.Context) string {
|
||||||
|
value, _ := ctx.Value(promptValueKey).(string)
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithPromptValue(ctx context.Context, value string) context.Context {
|
||||||
|
return context.WithValue(ctx, promptValueKey, value)
|
||||||
|
}
|
|
@ -1,10 +1,7 @@
|
||||||
package ui
|
package controllers
|
||||||
|
|
||||||
import "github.com/lmika/awstools/internal/dynamo-browse/models"
|
import "github.com/lmika/awstools/internal/dynamo-browse/models"
|
||||||
|
|
||||||
type newResultSet struct {
|
type NewResultSet struct {
|
||||||
ResultSet *models.ResultSet
|
ResultSet *models.ResultSet
|
||||||
}
|
}
|
||||||
|
|
||||||
type setStatusMessage string
|
|
||||||
type errorRaised error
|
|
44
internal/dynamo-browse/controllers/tableread.go
Normal file
44
internal/dynamo-browse/controllers/tableread.go
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/lmika/awstools/internal/common/ui/uimodels"
|
||||||
|
"github.com/lmika/awstools/internal/dynamo-browse/services/tables"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TableReadController struct {
|
||||||
|
tableService *tables.Service
|
||||||
|
tableName string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTableReadController(tableService *tables.Service, tableName string) *TableReadController {
|
||||||
|
return &TableReadController{
|
||||||
|
tableService: tableService,
|
||||||
|
tableName: tableName,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TableReadController) Scan() uimodels.Operation {
|
||||||
|
return uimodels.OperationFn(func(ctx context.Context) error {
|
||||||
|
return c.doScan(ctx, false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TableReadController) doScan(ctx context.Context, quiet bool) error {
|
||||||
|
uiCtx := uimodels.Ctx(ctx)
|
||||||
|
|
||||||
|
if !quiet {
|
||||||
|
uiCtx.Message("Scanning...")
|
||||||
|
}
|
||||||
|
|
||||||
|
resultSet, err := c.tableService.Scan(ctx, c.tableName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !quiet {
|
||||||
|
uiCtx.Messagef("Found %d items", len(resultSet.Items))
|
||||||
|
}
|
||||||
|
uiCtx.Send(NewResultSet{resultSet})
|
||||||
|
return nil
|
||||||
|
}
|
53
internal/dynamo-browse/controllers/tablewrite.go
Normal file
53
internal/dynamo-browse/controllers/tablewrite.go
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/lmika/awstools/internal/common/ui/uimodels"
|
||||||
|
"github.com/lmika/awstools/internal/dynamo-browse/services/tables"
|
||||||
|
"github.com/lmika/awstools/internal/dynamo-browse/models"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TableWriteController struct {
|
||||||
|
tableService *tables.Service
|
||||||
|
tableReadControllers *TableReadController
|
||||||
|
tableName string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTableWriteController(tableService *tables.Service, tableReadControllers *TableReadController, tableName string) *TableWriteController {
|
||||||
|
return &TableWriteController{
|
||||||
|
tableService: tableService,
|
||||||
|
tableReadControllers: tableReadControllers,
|
||||||
|
tableName: tableName,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TableWriteController) Delete(item models.Item) uimodels.Operation {
|
||||||
|
return uimodels.OperationFn(func(ctx context.Context) error {
|
||||||
|
uiCtx := uimodels.Ctx(ctx)
|
||||||
|
|
||||||
|
// TODO: only do if rw mode enabled
|
||||||
|
|
||||||
|
uiCtx.Input("Delete item?", uimodels.OperationFn(func(ctx context.Context) error {
|
||||||
|
uiCtx := uimodels.Ctx(ctx)
|
||||||
|
|
||||||
|
if uimodels.PromptValue(ctx) != "y" {
|
||||||
|
return errors.New("operation aborted")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the item
|
||||||
|
if err := c.tableService.Delete(ctx, c.tableName, item); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rescan to get updated items
|
||||||
|
if err := c.tableReadControllers.doScan(ctx, true); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
uiCtx.Message("Item deleted")
|
||||||
|
return nil
|
||||||
|
}))
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"github.com/aws/aws-sdk-go-v2/aws"
|
"github.com/aws/aws-sdk-go-v2/aws"
|
||||||
"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/lmika/awstools/internal/dynamo-browse/models"
|
"github.com/lmika/awstools/internal/dynamo-browse/models"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
@ -31,3 +32,11 @@ func (p *Provider) ScanItems(ctx context.Context, tableName string) ([]models.It
|
||||||
|
|
||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Provider) DeleteItem(ctx context.Context, tableName string, key map[string]types.AttributeValue) error {
|
||||||
|
_, err := p.client.DeleteItem(ctx, &dynamodb.DeleteItemInput{
|
||||||
|
TableName: aws.String(tableName),
|
||||||
|
Key: key,
|
||||||
|
})
|
||||||
|
return errors.Wrap(err, "could not delete item")
|
||||||
|
}
|
|
@ -2,9 +2,11 @@ package tables
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"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"
|
||||||
)
|
)
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ package tables
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"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"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"sort"
|
"sort"
|
||||||
|
@ -50,3 +51,11 @@ func (s *Service) Scan(ctx context.Context, table string) (*models.ResultSet, er
|
||||||
Items: results,
|
Items: results,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) Delete(ctx context.Context, name string, item models.Item) error {
|
||||||
|
// TODO: do not hardcode keys
|
||||||
|
return s.provider.DeleteItem(ctx, name, map[string]types.AttributeValue{
|
||||||
|
"pk": item["pk"],
|
||||||
|
"sk": item["sk"],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -5,42 +5,59 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||||
table "github.com/calyptia/go-bubble-table"
|
table "github.com/calyptia/go-bubble-table"
|
||||||
|
"github.com/charmbracelet/bubbles/textinput"
|
||||||
"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/dispatcher"
|
||||||
|
"github.com/lmika/awstools/internal/common/ui/events"
|
||||||
|
"github.com/lmika/awstools/internal/common/ui/uimodels"
|
||||||
|
"github.com/lmika/awstools/internal/dynamo-browse/controllers"
|
||||||
"github.com/lmika/awstools/internal/dynamo-browse/models"
|
"github.com/lmika/awstools/internal/dynamo-browse/models"
|
||||||
"github.com/lmika/awstools/internal/dynamo-browse/services/tables"
|
|
||||||
"log"
|
|
||||||
"strings"
|
"strings"
|
||||||
"text/tabwriter"
|
"text/tabwriter"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
headerStyle = lipgloss.NewStyle().
|
||||||
|
Bold(true).
|
||||||
|
Foreground(lipgloss.Color("#ffffff")).
|
||||||
|
Background(lipgloss.Color("#4479ff"))
|
||||||
|
)
|
||||||
|
|
||||||
type uiModel struct {
|
type uiModel struct {
|
||||||
table table.Model
|
table table.Model
|
||||||
viewport viewport.Model
|
viewport viewport.Model
|
||||||
|
|
||||||
msgPublisher MessagePublisher
|
|
||||||
tableService *tables.Service
|
|
||||||
tableName string
|
|
||||||
|
|
||||||
tableWidth, tableHeight int
|
tableWidth, tableHeight int
|
||||||
|
|
||||||
ready bool
|
ready bool
|
||||||
resultSet *models.ResultSet
|
resultSet *models.ResultSet
|
||||||
message string
|
message string
|
||||||
|
|
||||||
|
pendingInput *events.PromptForInput
|
||||||
|
textInput textinput.Model
|
||||||
|
|
||||||
|
dispatcher *dispatcher.Dispatcher
|
||||||
|
tableReadController *controllers.TableReadController
|
||||||
|
tableWriteController *controllers.TableWriteController
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewModel(tableService *tables.Service, msgPublisher MessagePublisher, tableName string) tea.Model {
|
func NewModel(dispatcher *dispatcher.Dispatcher, 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)
|
||||||
|
|
||||||
|
textInput := textinput.New()
|
||||||
|
|
||||||
model := uiModel{
|
model := uiModel{
|
||||||
table: tbl,
|
table: tbl,
|
||||||
tableService: tableService,
|
message: "Press s to scan",
|
||||||
tableName: tableName,
|
textInput: textInput,
|
||||||
msgPublisher: msgPublisher,
|
|
||||||
message: "Press s to scan",
|
dispatcher: dispatcher,
|
||||||
|
tableReadController: tableReadController,
|
||||||
|
tableWriteController: tableWriteController,
|
||||||
}
|
}
|
||||||
|
|
||||||
return model
|
return model
|
||||||
|
@ -65,16 +82,19 @@ func (m *uiModel) updateTable() {
|
||||||
m.table = newTbl
|
m.table = newTbl
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *uiModel) selectedItem() (itemTableRow, bool) {
|
||||||
|
if m.ready && m.resultSet != nil && len(m.resultSet.Items) > 0 {
|
||||||
|
selectedItem, ok := m.table.SelectedRow().(itemTableRow)
|
||||||
|
if ok {
|
||||||
|
return selectedItem, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return itemTableRow{}, false
|
||||||
|
}
|
||||||
|
|
||||||
func (m *uiModel) updateViewportToSelectedMessage() {
|
func (m *uiModel) updateViewportToSelectedMessage() {
|
||||||
if !m.ready {
|
selectedItem, ok := m.selectedItem()
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if m.resultSet == nil || len(m.resultSet.Items) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
selectedItem, ok := m.table.SelectedRow().(itemTableRow)
|
|
||||||
if !ok {
|
if !ok {
|
||||||
m.viewport.SetContent("(no row selected)")
|
m.viewport.SetContent("(no row selected)")
|
||||||
return
|
return
|
||||||
|
@ -83,17 +103,15 @@ func (m *uiModel) updateViewportToSelectedMessage() {
|
||||||
viewportContent := &strings.Builder{}
|
viewportContent := &strings.Builder{}
|
||||||
tabWriter := tabwriter.NewWriter(viewportContent, 0, 1, 1, ' ', 0)
|
tabWriter := tabwriter.NewWriter(viewportContent, 0, 1, 1, ' ', 0)
|
||||||
for _, colName := range selectedItem.resultSet.Columns {
|
for _, colName := range selectedItem.resultSet.Columns {
|
||||||
fmt.Fprintf(tabWriter, "%v\t", colName)
|
|
||||||
|
|
||||||
switch colVal := selectedItem.item[colName].(type) {
|
switch colVal := selectedItem.item[colName].(type) {
|
||||||
case nil:
|
case nil:
|
||||||
fmt.Fprintln(tabWriter, "(nil)")
|
break
|
||||||
case *types.AttributeValueMemberS:
|
case *types.AttributeValueMemberS:
|
||||||
fmt.Fprintln(tabWriter, colVal.Value)
|
fmt.Fprintf(tabWriter, "%v\tS\t%s\n", colName, colVal.Value)
|
||||||
case *types.AttributeValueMemberN:
|
case *types.AttributeValueMemberN:
|
||||||
fmt.Fprintln(tabWriter, colVal.Value)
|
fmt.Fprintf(tabWriter, "%v\tN\t%s\n", colName, colVal.Value)
|
||||||
default:
|
default:
|
||||||
fmt.Fprintln(tabWriter, "(other)")
|
fmt.Fprintf(tabWriter, "%v\t?\t%s\n", colName, "(other)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -102,26 +120,38 @@ func (m *uiModel) updateViewportToSelectedMessage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
var textInputCommands tea.Cmd
|
||||||
|
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
case setStatusMessage:
|
|
||||||
m.message = ""
|
// Local events
|
||||||
case errorRaised:
|
case controllers.NewResultSet:
|
||||||
m.message = "Error: " + msg.Error()
|
|
||||||
case newResultSet:
|
|
||||||
m.resultSet = msg.ResultSet
|
m.resultSet = msg.ResultSet
|
||||||
m.updateTable()
|
m.updateTable()
|
||||||
m.updateViewportToSelectedMessage()
|
m.updateViewportToSelectedMessage()
|
||||||
|
|
||||||
|
// Shared events
|
||||||
|
case events.Error:
|
||||||
|
m.message = "Error: " + msg.Error()
|
||||||
|
case events.Message:
|
||||||
|
m.message = string(msg)
|
||||||
|
case events.PromptForInput:
|
||||||
|
m.textInput.Focus()
|
||||||
|
m.textInput.SetValue("")
|
||||||
|
m.pendingInput = &msg
|
||||||
|
|
||||||
|
// Tea events
|
||||||
case tea.WindowSizeMsg:
|
case tea.WindowSizeMsg:
|
||||||
footerHeight := lipgloss.Height(m.footerView())
|
fixedViewsHeight := lipgloss.Height(m.headerView()) + lipgloss.Height(m.splitterView()) + lipgloss.Height(m.footerView())
|
||||||
tableHeight := msg.Height / 2
|
tableHeight := msg.Height / 2
|
||||||
|
|
||||||
if !m.ready {
|
if !m.ready {
|
||||||
m.viewport = viewport.New(msg.Width, msg.Height-tableHeight-footerHeight)
|
m.viewport = viewport.New(msg.Width, msg.Height-tableHeight-fixedViewsHeight)
|
||||||
m.viewport.SetContent("(no message selected)")
|
m.viewport.SetContent("(no message selected)")
|
||||||
m.ready = true
|
m.ready = true
|
||||||
} else {
|
} else {
|
||||||
m.viewport.Width = msg.Width
|
m.viewport.Width = msg.Width
|
||||||
m.viewport.Height = msg.Height - tableHeight - footerHeight
|
m.viewport.Height = msg.Height - tableHeight - fixedViewsHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
m.tableWidth, m.tableHeight = msg.Width, tableHeight
|
m.tableWidth, m.tableHeight = msg.Width, tableHeight
|
||||||
|
@ -129,15 +159,21 @@ func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
|
|
||||||
|
// If text input in focus, allow that to accept input messages
|
||||||
|
if m.pendingInput != nil {
|
||||||
|
switch msg.String() {
|
||||||
|
case "ctrl+c", "esc":
|
||||||
|
m.pendingInput = nil
|
||||||
|
case "enter":
|
||||||
|
m.dispatcher.Start(uimodels.WithPromptValue(context.Background(), m.textInput.Value()), m.pendingInput.OnDone)
|
||||||
|
m.pendingInput = nil
|
||||||
|
default:
|
||||||
|
m.textInput, textInputCommands = m.textInput.Update(msg)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
switch msg.String() {
|
switch msg.String() {
|
||||||
case "s":
|
|
||||||
m.startOperation("Scanning...", func(ctx context.Context) (tea.Msg, error) {
|
|
||||||
resultSet, err := m.tableService.Scan(ctx, m.tableName)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return newResultSet{resultSet}, nil
|
|
||||||
})
|
|
||||||
case "ctrl+c", "q":
|
case "ctrl+c", "q":
|
||||||
return m, tea.Quit
|
return m, tea.Quit
|
||||||
case "up", "i":
|
case "up", "i":
|
||||||
|
@ -146,7 +182,17 @@ func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
case "down", "k":
|
case "down", "k":
|
||||||
m.table.GoDown()
|
m.table.GoDown()
|
||||||
m.updateViewportToSelectedMessage()
|
m.updateViewportToSelectedMessage()
|
||||||
|
|
||||||
|
// TODO: these should be moved somewhere else
|
||||||
|
case "s":
|
||||||
|
m.dispatcher.Start(context.Background(), m.tableReadController.Scan())
|
||||||
|
case "D":
|
||||||
|
if selectedItem, ok := m.selectedItem(); ok {
|
||||||
|
m.dispatcher.Start(context.Background(), m.tableWriteController.Delete(selectedItem.item))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
default:
|
||||||
|
m.textInput, textInputCommands = m.textInput.Update(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
updatedTable, tableMsgs := m.table.Update(msg)
|
updatedTable, tableMsgs := m.table.Update(msg)
|
||||||
|
@ -155,21 +201,7 @@ func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
m.table = updatedTable
|
m.table = updatedTable
|
||||||
m.viewport = updatedViewport
|
m.viewport = updatedViewport
|
||||||
|
|
||||||
return m, tea.Batch(tableMsgs, viewportMsgs)
|
return m, tea.Batch(textInputCommands, tableMsgs, viewportMsgs)
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: this should probably be a separate service
|
|
||||||
func (m *uiModel) startOperation(msg string, op func(ctx context.Context) (tea.Msg, error)) {
|
|
||||||
m.message = msg
|
|
||||||
go func() {
|
|
||||||
resMsg, err := op(context.Background())
|
|
||||||
if err != nil {
|
|
||||||
m.msgPublisher.Send(errorRaised(err))
|
|
||||||
} else if resMsg != nil {
|
|
||||||
m.msgPublisher.Send(resMsg)
|
|
||||||
}
|
|
||||||
m.msgPublisher.Send(setStatusMessage(""))
|
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m uiModel) View() string {
|
func (m uiModel) View() string {
|
||||||
|
@ -177,9 +209,35 @@ func (m uiModel) View() string {
|
||||||
return "Initializing"
|
return "Initializing"
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Println("Returning full view")
|
if m.pendingInput != nil {
|
||||||
return lipgloss.JoinVertical(lipgloss.Top, m.table.View(), m.viewport.View(), m.footerView())
|
return lipgloss.JoinVertical(lipgloss.Top,
|
||||||
//return lipgloss.JoinVertical(lipgloss.Top, m.table.View(), m.footerView())
|
m.headerView(),
|
||||||
|
m.table.View(),
|
||||||
|
m.splitterView(),
|
||||||
|
m.viewport.View(),
|
||||||
|
m.textInput.View(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return lipgloss.JoinVertical(lipgloss.Top,
|
||||||
|
m.headerView(),
|
||||||
|
m.table.View(),
|
||||||
|
m.splitterView(),
|
||||||
|
m.viewport.View(),
|
||||||
|
m.footerView(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m uiModel) headerView() string {
|
||||||
|
title := headerStyle.Render("Table: XXX")
|
||||||
|
line := headerStyle.Render(strings.Repeat(" ", max(0, m.viewport.Width-lipgloss.Width(title))))
|
||||||
|
return lipgloss.JoinHorizontal(lipgloss.Left, title, line)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m uiModel) splitterView() string {
|
||||||
|
title := headerStyle.Render("Item")
|
||||||
|
line := headerStyle.Render(strings.Repeat(" ", max(0, m.viewport.Width-lipgloss.Width(title))))
|
||||||
|
return lipgloss.JoinHorizontal(lipgloss.Left, title, line)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m uiModel) footerView() string {
|
func (m uiModel) footerView() string {
|
||||||
|
|
39
internal/sqs-browse/controllers/forward.go
Normal file
39
internal/sqs-browse/controllers/forward.go
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/lmika/awstools/internal/common/ui/uimodels"
|
||||||
|
"github.com/lmika/awstools/internal/sqs-browse/models"
|
||||||
|
"github.com/lmika/awstools/internal/sqs-browse/services/messages"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MessageSendingController struct {
|
||||||
|
messageService *messages.Service
|
||||||
|
targetQueue string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMessageSendingController(messageService *messages.Service, targetQueue string) *MessageSendingController {
|
||||||
|
return &MessageSendingController{
|
||||||
|
messageService: messageService,
|
||||||
|
targetQueue: targetQueue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (msh *MessageSendingController) ForwardMessage(message models.Message) uimodels.Operation {
|
||||||
|
return uimodels.OperationFn(func(ctx context.Context) error {
|
||||||
|
uiCtx := uimodels.Ctx(ctx)
|
||||||
|
|
||||||
|
if msh.targetQueue == "" {
|
||||||
|
return errors.New("target queue not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
messageId, err := msh.messageService.SendTo(ctx, message, msh.targetQueue)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "cannot send message to %v", msh.targetQueue)
|
||||||
|
}
|
||||||
|
|
||||||
|
uiCtx.Message("Message sent to " + msh.targetQueue + ", id = " + messageId)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
|
@ -19,6 +19,20 @@ func NewProvider(client *sqs.Client) *Provider {
|
||||||
return &Provider{client: client}
|
return &Provider{client: client}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Provider) SendMessage(ctx context.Context, msg models.Message, queue string) (string, error) {
|
||||||
|
// TEMP :: queue URL
|
||||||
|
|
||||||
|
out, err := p.client.SendMessage(ctx, &sqs.SendMessageInput{
|
||||||
|
QueueUrl: aws.String(queue),
|
||||||
|
MessageBody: aws.String(msg.Data),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.Wrapf(err, "unable to send message to %v", queue)
|
||||||
|
}
|
||||||
|
|
||||||
|
return aws.ToString(out.MessageId), nil
|
||||||
|
}
|
||||||
|
|
||||||
func (p *Provider) PollForNewMessages(ctx context.Context, queue string) ([]*models.Message, error) {
|
func (p *Provider) PollForNewMessages(ctx context.Context, queue string) ([]*models.Message, error) {
|
||||||
out, err := p.client.ReceiveMessage(ctx, &sqs.ReceiveMessageInput{
|
out, err := p.client.ReceiveMessage(ctx, &sqs.ReceiveMessageInput{
|
||||||
QueueUrl: aws.String(queue),
|
QueueUrl: aws.String(queue),
|
||||||
|
|
|
@ -6,5 +6,5 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type MessageSender interface {
|
type MessageSender interface {
|
||||||
SendMessage(ctx context.Context, msg models.Message, queue string) error
|
SendMessage(ctx context.Context, msg models.Message, queue string) (string, error)
|
||||||
}
|
}
|
|
@ -10,10 +10,16 @@ type Service struct {
|
||||||
messageSender MessageSender
|
messageSender MessageSender
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewService() *Service {
|
func NewService(messageSender MessageSender) *Service {
|
||||||
return &Service{}
|
return &Service{
|
||||||
|
messageSender: messageSender,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) SendTo(ctx context.Context, msg models.Message, destQueue string) error {
|
func (s *Service) SendTo(ctx context.Context, msg models.Message, destQueue string) (string, error) {
|
||||||
return errors.Wrapf(s.messageSender.SendMessage(ctx, msg, destQueue), "cannot send message to %v", destQueue)
|
messageId, err := s.messageSender.SendMessage(ctx, msg, destQueue)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.Wrapf(err, "cannot send message to %v", destQueue)
|
||||||
|
}
|
||||||
|
return messageId, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,28 @@
|
||||||
package ui
|
package ui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
table "github.com/calyptia/go-bubble-table"
|
table "github.com/calyptia/go-bubble-table"
|
||||||
|
"github.com/charmbracelet/bubbles/textinput"
|
||||||
"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/dispatcher"
|
||||||
|
"github.com/lmika/awstools/internal/common/ui/events"
|
||||||
|
"github.com/lmika/awstools/internal/common/ui/uimodels"
|
||||||
|
"github.com/lmika/awstools/internal/sqs-browse/controllers"
|
||||||
|
"github.com/lmika/awstools/internal/sqs-browse/models"
|
||||||
"log"
|
"log"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
headerStyle = lipgloss.NewStyle().
|
||||||
|
Bold(true).
|
||||||
|
Foreground(lipgloss.Color("#ffffff")).
|
||||||
|
Background(lipgloss.Color("#eac610"))
|
||||||
|
)
|
||||||
|
|
||||||
type uiModel struct {
|
type uiModel struct {
|
||||||
table table.Model
|
table table.Model
|
||||||
viewport viewport.Model
|
viewport viewport.Model
|
||||||
|
@ -16,17 +30,28 @@ type uiModel struct {
|
||||||
ready bool
|
ready bool
|
||||||
tableRows []table.Row
|
tableRows []table.Row
|
||||||
message string
|
message string
|
||||||
|
|
||||||
|
pendingInput *events.PromptForInput
|
||||||
|
textInput textinput.Model
|
||||||
|
|
||||||
|
dispatcher *dispatcher.Dispatcher
|
||||||
|
msgSendingHandlers *controllers.MessageSendingController
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewModel() tea.Model {
|
func NewModel(dispatcher *dispatcher.Dispatcher, msgSendingHandlers *controllers.MessageSendingController) tea.Model {
|
||||||
tbl := table.New([]string{"seq", "message"}, 100, 20)
|
tbl := table.New([]string{"seq", "message"}, 100, 20)
|
||||||
rows := make([]table.Row, 0)
|
rows := make([]table.Row, 0)
|
||||||
tbl.SetRows(rows)
|
tbl.SetRows(rows)
|
||||||
|
|
||||||
|
textInput := textinput.New()
|
||||||
|
|
||||||
model := uiModel{
|
model := uiModel{
|
||||||
table: tbl,
|
table: tbl,
|
||||||
tableRows: rows,
|
tableRows: rows,
|
||||||
message: "",
|
message: "",
|
||||||
|
textInput: textInput,
|
||||||
|
msgSendingHandlers: msgSendingHandlers,
|
||||||
|
dispatcher: dispatcher,
|
||||||
}
|
}
|
||||||
|
|
||||||
return model
|
return model
|
||||||
|
@ -37,23 +62,37 @@ func (m uiModel) Init() tea.Cmd {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *uiModel) updateViewportToSelectedMessage() {
|
func (m *uiModel) updateViewportToSelectedMessage() {
|
||||||
if !m.ready {
|
if message, ok := m.selectedMessage(); ok {
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(m.tableRows) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if message, ok := m.table.SelectedRow().(messageTableRow); ok {
|
|
||||||
m.viewport.SetContent(message.Data)
|
m.viewport.SetContent(message.Data)
|
||||||
} else {
|
} else {
|
||||||
m.viewport.SetContent("(no message selected)")
|
m.viewport.SetContent("(no message selected)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m uiModel) selectedMessage() (models.Message, bool) {
|
||||||
|
if m.ready && len(m.tableRows) > 0 {
|
||||||
|
if message, ok := m.table.SelectedRow().(messageTableRow); ok {
|
||||||
|
return models.Message(message), true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return models.Message{}, false
|
||||||
|
}
|
||||||
|
|
||||||
func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
var textInputCommands tea.Cmd
|
||||||
|
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
|
// Shared messages
|
||||||
|
case events.Error:
|
||||||
|
m.message = "Error: " + msg.Error()
|
||||||
|
case events.Message:
|
||||||
|
m.message = string(msg)
|
||||||
|
case events.PromptForInput:
|
||||||
|
m.textInput.Focus()
|
||||||
|
m.textInput.SetValue("")
|
||||||
|
m.pendingInput = &msg
|
||||||
|
|
||||||
|
// Local messages
|
||||||
case NewMessagesEvent:
|
case NewMessagesEvent:
|
||||||
for _, newMsg := range msg {
|
for _, newMsg := range msg {
|
||||||
m.tableRows = append(m.tableRows, messageTableRow(*newMsg))
|
m.tableRows = append(m.tableRows, messageTableRow(*newMsg))
|
||||||
|
@ -62,13 +101,13 @@ func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
m.updateViewportToSelectedMessage()
|
m.updateViewportToSelectedMessage()
|
||||||
|
|
||||||
case tea.WindowSizeMsg:
|
case tea.WindowSizeMsg:
|
||||||
footerHeight := lipgloss.Height(m.footerView())
|
fixedViewsHeight := lipgloss.Height(m.headerView()) + lipgloss.Height(m.splitterView()) + lipgloss.Height(m.footerView())
|
||||||
|
|
||||||
if !m.ready {
|
if !m.ready {
|
||||||
tableHeight := msg.Height / 2
|
tableHeight := msg.Height / 2
|
||||||
|
|
||||||
m.table.SetSize(msg.Width, tableHeight)
|
m.table.SetSize(msg.Width, tableHeight)
|
||||||
m.viewport = viewport.New(msg.Width, msg.Height-tableHeight-footerHeight)
|
m.viewport = viewport.New(msg.Width, msg.Height-tableHeight-fixedViewsHeight)
|
||||||
m.viewport.SetContent("(no message selected)")
|
m.viewport.SetContent("(no message selected)")
|
||||||
m.ready = true
|
m.ready = true
|
||||||
log.Println("Viewport is now ready")
|
log.Println("Viewport is now ready")
|
||||||
|
@ -77,12 +116,29 @@ func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
|
||||||
m.table.SetSize(msg.Width, tableHeight)
|
m.table.SetSize(msg.Width, tableHeight)
|
||||||
m.viewport.Width = msg.Width
|
m.viewport.Width = msg.Width
|
||||||
m.viewport.Height = msg.Height - tableHeight - footerHeight
|
m.viewport.Height = msg.Height - tableHeight - fixedViewsHeight
|
||||||
//m.viewport.YPosition = tableHeight
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m.textInput.Width = msg.Width
|
||||||
|
|
||||||
|
m.textInput, textInputCommands = m.textInput.Update(msg)
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
|
|
||||||
|
// If text input in focus, allow that to accept input messages
|
||||||
|
if m.pendingInput != nil {
|
||||||
|
switch msg.String() {
|
||||||
|
case "ctrl+c", "esc":
|
||||||
|
m.pendingInput = nil
|
||||||
|
case "enter":
|
||||||
|
m.dispatcher.Start(uimodels.WithPromptValue(context.Background(), m.textInput.Value()), m.pendingInput.OnDone)
|
||||||
|
m.pendingInput = nil
|
||||||
|
default:
|
||||||
|
m.textInput, textInputCommands = m.textInput.Update(msg)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normal focus
|
||||||
switch msg.String() {
|
switch msg.String() {
|
||||||
|
|
||||||
case "ctrl+c", "q":
|
case "ctrl+c", "q":
|
||||||
|
@ -93,7 +149,15 @@ func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
case "down", "k":
|
case "down", "k":
|
||||||
m.table.GoDown()
|
m.table.GoDown()
|
||||||
m.updateViewportToSelectedMessage()
|
m.updateViewportToSelectedMessage()
|
||||||
|
|
||||||
|
// TODO: these should be moved somewhere else
|
||||||
|
case "f":
|
||||||
|
if selectedMessage, ok := m.selectedMessage(); ok {
|
||||||
|
m.dispatcher.Start(context.Background(), m.msgSendingHandlers.ForwardMessage(selectedMessage))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
default:
|
||||||
|
m.textInput, textInputCommands = m.textInput.Update(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
updatedTable, tableMsgs := m.table.Update(msg)
|
updatedTable, tableMsgs := m.table.Update(msg)
|
||||||
|
@ -102,7 +166,7 @@ func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
m.table = updatedTable
|
m.table = updatedTable
|
||||||
m.viewport = updatedViewport
|
m.viewport = updatedViewport
|
||||||
|
|
||||||
return m, tea.Batch(tableMsgs, viewportMsgs)
|
return m, tea.Batch(textInputCommands, tableMsgs, viewportMsgs)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m uiModel) View() string {
|
func (m uiModel) View() string {
|
||||||
|
@ -110,9 +174,35 @@ func (m uiModel) View() string {
|
||||||
return "Initializing"
|
return "Initializing"
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Println("Returning full view")
|
if m.pendingInput != nil {
|
||||||
return lipgloss.JoinVertical(lipgloss.Top, m.table.View(), m.viewport.View(), m.footerView())
|
return lipgloss.JoinVertical(lipgloss.Top,
|
||||||
//return lipgloss.JoinVertical(lipgloss.Top, m.table.View(), m.footerView())
|
m.headerView(),
|
||||||
|
m.table.View(),
|
||||||
|
m.splitterView(),
|
||||||
|
m.viewport.View(),
|
||||||
|
m.textInput.View(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return lipgloss.JoinVertical(lipgloss.Top,
|
||||||
|
m.headerView(),
|
||||||
|
m.table.View(),
|
||||||
|
m.splitterView(),
|
||||||
|
m.viewport.View(),
|
||||||
|
m.footerView(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m uiModel) headerView() string {
|
||||||
|
title := headerStyle.Render("Queue: XXX")
|
||||||
|
line := headerStyle.Render(strings.Repeat(" ", max(0, m.viewport.Width-lipgloss.Width(title))))
|
||||||
|
return lipgloss.JoinHorizontal(lipgloss.Left, title, line)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m uiModel) splitterView() string {
|
||||||
|
title := headerStyle.Render("Message")
|
||||||
|
line := headerStyle.Render(strings.Repeat(" ", max(0, m.viewport.Width-lipgloss.Width(title))))
|
||||||
|
return lipgloss.JoinHorizontal(lipgloss.Left, title, line)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m uiModel) footerView() string {
|
func (m uiModel) footerView() string {
|
||||||
|
|
Loading…
Reference in a new issue