sqs-browse: Added dynamo-browse
Added another tool for browsing DynamoDB tables
This commit is contained in:
		
							parent
							
								
									2c03f5160a
								
							
						
					
					
						commit
						1969504611
					
				
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
debug.log
 | 
			
		||||
							
								
								
									
										65
									
								
								cmd/dynamo-browse/main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								cmd/dynamo-browse/main.go
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,65 @@
 | 
			
		|||
package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"flag"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/aws/aws-sdk-go-v2/config"
 | 
			
		||||
	"github.com/aws/aws-sdk-go-v2/service/dynamodb"
 | 
			
		||||
	tea "github.com/charmbracelet/bubbletea"
 | 
			
		||||
	"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/ui"
 | 
			
		||||
	"github.com/lmika/gopkgs/cli"
 | 
			
		||||
	"os"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func main() {
 | 
			
		||||
	var flagTable = flag.String("t", "", "dynamodb table name")
 | 
			
		||||
	var flagLocal = flag.Bool("local", false, "local endpoint")
 | 
			
		||||
	flag.Parse()
 | 
			
		||||
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
	cfg, err := config.LoadDefaultConfig(ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		cli.Fatalf("cannot load AWS config: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var dynamoClient *dynamodb.Client
 | 
			
		||||
	if *flagLocal {
 | 
			
		||||
		dynamoClient = dynamodb.NewFromConfig(cfg,
 | 
			
		||||
			dynamodb.WithEndpointResolver(dynamodb.EndpointResolverFromURL("http://localhost:8000")))
 | 
			
		||||
	} else {
 | 
			
		||||
		dynamoClient = dynamodb.NewFromConfig(cfg)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	dynamoProvider := dynamo.NewProvider(dynamoClient)
 | 
			
		||||
 | 
			
		||||
	tableService := tables.NewService(dynamoProvider)
 | 
			
		||||
 | 
			
		||||
	loopback := &msgLoopback{}
 | 
			
		||||
	uiModel := ui.NewModel(tableService, loopback, *flagTable)
 | 
			
		||||
	p := tea.NewProgram(uiModel, tea.WithAltScreen())
 | 
			
		||||
	loopback.program = p
 | 
			
		||||
 | 
			
		||||
	f, err := tea.LogToFile("debug.log", "debug")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Println("fatal:", err)
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
	defer f.Close()
 | 
			
		||||
 | 
			
		||||
	if err := p.Start(); err != nil {
 | 
			
		||||
		fmt.Printf("Alas, there's been an error: %v", err)
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type msgLoopback struct {
 | 
			
		||||
	program *tea.Program
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *msgLoopback) Send(msg tea.Msg) {
 | 
			
		||||
	m.program.Send(msg)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										12
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								go.mod
									
									
									
									
									
								
							| 
						 | 
				
			
			@ -3,24 +3,28 @@ module github.com/lmika/awstools
 | 
			
		|||
go 1.17
 | 
			
		||||
 | 
			
		||||
require (
 | 
			
		||||
	github.com/aws/aws-sdk-go-v2 v1.13.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/credentials v1.8.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.4 // indirect
 | 
			
		||||
	github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.2.0 // indirect
 | 
			
		||||
	github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.6 // indirect
 | 
			
		||||
	github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.0 // indirect
 | 
			
		||||
	github.com/aws/aws-sdk-go-v2/internal/ini v1.3.5 // indirect
 | 
			
		||||
	github.com/aws/aws-sdk-go-v2/service/dynamodb v1.15.0 // indirect
 | 
			
		||||
	github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.0 // indirect
 | 
			
		||||
	github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.0 // indirect
 | 
			
		||||
	github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.7.0 // indirect
 | 
			
		||||
	github.com/aws/aws-sdk-go-v2/service/sqs v1.16.0 // indirect
 | 
			
		||||
	github.com/aws/aws-sdk-go-v2/service/sso v1.9.0 // indirect
 | 
			
		||||
	github.com/aws/aws-sdk-go-v2/service/sts v1.14.0 // indirect
 | 
			
		||||
	github.com/aws/smithy-go v1.10.0 // indirect
 | 
			
		||||
	github.com/aws/smithy-go v1.11.1 // indirect
 | 
			
		||||
	github.com/calyptia/go-bubble-table v0.1.0 // indirect
 | 
			
		||||
	github.com/charmbracelet/bubbles v0.10.3 // indirect
 | 
			
		||||
	github.com/charmbracelet/bubbletea v0.20.0 // indirect
 | 
			
		||||
	github.com/charmbracelet/lipgloss v0.5.0 // indirect
 | 
			
		||||
	github.com/containerd/console v1.0.3 // indirect
 | 
			
		||||
	github.com/davecgh/go-spew v1.1.1 // indirect
 | 
			
		||||
	github.com/jmespath/go-jmespath v0.4.0 // indirect
 | 
			
		||||
	github.com/juju/ansiterm v0.0.0-20210929141451-8b71cc96ebdc // indirect
 | 
			
		||||
	github.com/lmika/events v0.0.0-20200906102219-a2269cd4394e // indirect
 | 
			
		||||
	github.com/lmika/gopkgs v0.0.0-20211210041137-0dc91e939890 // indirect
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										16
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								go.sum
									
									
									
									
									
								
							| 
						 | 
				
			
			@ -1,6 +1,8 @@
 | 
			
		|||
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/go.mod h1:L6+ZpqHaLbAaxsqV0L4cvxZY7QupWJB4fhkf8LXvC7w=
 | 
			
		||||
github.com/aws/aws-sdk-go-v2 v1.15.0 h1:f9kWLNfyCzCB43eupDAk3/XgJ2EpgktiySD6leqs0js=
 | 
			
		||||
github.com/aws/aws-sdk-go-v2 v1.15.0/go.mod h1:lJYcuZZEHWNIb6ugJjbQY1fykdoobWbOS7kJYb4APoI=
 | 
			
		||||
github.com/aws/aws-sdk-go-v2/config v1.13.1 h1:yLv8bfNoT4r+UvUKQKqRtdnvuWGMK5a82l4ru9Jvnuo=
 | 
			
		||||
github.com/aws/aws-sdk-go-v2/config v1.13.1/go.mod h1:Ba5Z4yL/UGbjQUzsiaN378YobhFo0MLfueXGiOsYtEs=
 | 
			
		||||
github.com/aws/aws-sdk-go-v2/credentials v1.8.0 h1:8Ow0WcyDesGNL0No11jcgb1JAtE+WtubqXjgxau+S0o=
 | 
			
		||||
| 
						 | 
				
			
			@ -9,10 +11,20 @@ github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.10.0 h1:NITDuUZO34mqtOwFWZiXo7y
 | 
			
		|||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.10.0/go.mod h1:I6/fHT/fH460v09eg2gVrd8B/IqskhNdpcLH0WNO3QI=
 | 
			
		||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.4 h1:CRiQJ4E2RhfDdqbie1ZYDo8QtIo75Mk7oTdJSfwJTMQ=
 | 
			
		||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.4/go.mod h1:XHgQ7Hz2WY2GAn//UXHofLfPXWh+s62MbMOijrg12Lw=
 | 
			
		||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.6 h1:xiGjGVQsem2cxoIX61uRGy+Jux2s9C/kKbTrWLdrU54=
 | 
			
		||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.6/go.mod h1:SSPEdf9spsFgJyhjrXvawfpyzrXHBCUe+2eQ1CjC1Ak=
 | 
			
		||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.2.0 h1:3ADoioDMOtF4uiK59vCpplpCwugEU+v4ZFD29jDL3RQ=
 | 
			
		||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.2.0/go.mod h1:BsCSJHx5DnDXIrOcqB8KN1/B+hXLG/bi4Y6Vjcx/x9E=
 | 
			
		||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.0 h1:bt3zw79tm209glISdMRCIVRCwvSDXxgAxh5KWe2qHkY=
 | 
			
		||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.0/go.mod h1:viTrxhAuejD+LszDahzAE2x40YjYWhMqzHxv2ZiWaME=
 | 
			
		||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.5 h1:ixotxbfTCFpqbuwFv/RcZwyzhkxPSYDYEMcj4niB5Uk=
 | 
			
		||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.5/go.mod h1:R3sWUqPcfXSiF/LSFJhjyJmpg9uV6yP2yv3YZZjldVI=
 | 
			
		||||
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.15.0 h1:qnx+WyIH9/AD+wAxi05WCMNanO236ceqHg6hChCWs3M=
 | 
			
		||||
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.15.0/go.mod h1:+Kc1UmbE37ijaAsb3KogW6FR8z0myjX6VtdcCkQEK0k=
 | 
			
		||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.0 h1:uhb7moM7VjqIEpWzTpCvceLDSwrWpaleXm39OnVjuLE=
 | 
			
		||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.0/go.mod h1:pA2St3Pu2Ldy6fBPY45Azoh1WBG4oS7eIKOd4XN7Meg=
 | 
			
		||||
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.0 h1:6Bc0KHhAyxGe15JUHrK+Udw7KhE5LN+5HKZjQGo4yDI=
 | 
			
		||||
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.0/go.mod h1:0nXuX9UrkN4r0PX9TSKfcueGRfsdEYIKG4rjTeJ61X8=
 | 
			
		||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.7.0 h1:4QAOB3KrvI1ApJK14sliGr3Ie2pjyvNypn/lfzDHfUw=
 | 
			
		||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.7.0/go.mod h1:K/qPe6AP2TGYv4l6n7c88zh9jWBDf6nHhvg1fx/EWfU=
 | 
			
		||||
github.com/aws/aws-sdk-go-v2/service/sqs v1.16.0 h1:dzWS4r8E9bA0TesHM40FSAtedwpTVCuTsLI8EziSqyk=
 | 
			
		||||
| 
						 | 
				
			
			@ -23,6 +35,8 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.14.0 h1:ksiDXhvNYg0D2/UFkLejsaz3LqpW
 | 
			
		|||
github.com/aws/aws-sdk-go-v2/service/sts v1.14.0/go.mod h1:u0xMJKDvvfocRjiozsoZglVNXRG19043xzp3r2ivLIk=
 | 
			
		||||
github.com/aws/smithy-go v1.10.0 h1:gsoZQMNHnX+PaghNw4ynPsyGP7aUCqx5sY2dlPQsZ0w=
 | 
			
		||||
github.com/aws/smithy-go v1.10.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E=
 | 
			
		||||
github.com/aws/smithy-go v1.11.1 h1:IQ+lPZVkSM3FRtyaDox41R8YS6iwPMYIreejOgPW49g=
 | 
			
		||||
github.com/aws/smithy-go v1.11.1/go.mod h1:3xHYmszWVx2c0kIwQeEVf9uSm4fYZt67FBJnwub1bgM=
 | 
			
		||||
github.com/calyptia/go-bubble-table v0.1.0 h1:mXpaaBlrHGH4K8v5PvM8YqBFT9jlysS1YOycU2u3gEQ=
 | 
			
		||||
github.com/calyptia/go-bubble-table v0.1.0/go.mod h1:2nnweuFos+eEIIbgweXvZuX+ROOatsMwB3NHnX/vTC4=
 | 
			
		||||
github.com/charmbracelet/bubbles v0.10.3 h1:fKarbRaObLn/DCsZO4Y3vKCwRUzynQD9L+gGev1E/ho=
 | 
			
		||||
| 
						 | 
				
			
			@ -42,6 +56,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
 | 
			
		|||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 | 
			
		||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 | 
			
		||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 | 
			
		||||
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
 | 
			
		||||
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
 | 
			
		||||
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
 | 
			
		||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
 | 
			
		||||
github.com/juju/ansiterm v0.0.0-20210929141451-8b71cc96ebdc h1:ZQrgZFsLzkw7o3CoDzsfBhx0bf/1rVBXrLy8dXKRe8o=
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										10
									
								
								internal/dynamo-browse/models/models.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								internal/dynamo-browse/models/models.go
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,10 @@
 | 
			
		|||
package models
 | 
			
		||||
 | 
			
		||||
import "github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
 | 
			
		||||
 | 
			
		||||
type ResultSet struct {
 | 
			
		||||
	Columns []string
 | 
			
		||||
	Items   []Item
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Item map[string]types.AttributeValue
 | 
			
		||||
							
								
								
									
										33
									
								
								internal/dynamo-browse/providers/dynamo/provider.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								internal/dynamo-browse/providers/dynamo/provider.go
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,33 @@
 | 
			
		|||
package dynamo
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"github.com/aws/aws-sdk-go-v2/aws"
 | 
			
		||||
	"github.com/aws/aws-sdk-go-v2/service/dynamodb"
 | 
			
		||||
	"github.com/lmika/awstools/internal/dynamo-browse/models"
 | 
			
		||||
	"github.com/pkg/errors"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Provider struct {
 | 
			
		||||
	client *dynamodb.Client
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewProvider(client *dynamodb.Client) *Provider {
 | 
			
		||||
	return &Provider{client: client}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p *Provider) ScanItems(ctx context.Context, tableName string) ([]models.Item, error) {
 | 
			
		||||
	res, err := p.client.Scan(ctx, &dynamodb.ScanInput{
 | 
			
		||||
		TableName: aws.String(tableName),
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, errors.Wrapf(err, "cannot execute scan on table %v", tableName)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	items := make([]models.Item, len(res.Items))
 | 
			
		||||
	for i, itm := range res.Items {
 | 
			
		||||
		items[i] = itm
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return items, nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										10
									
								
								internal/dynamo-browse/services/tables/iface.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								internal/dynamo-browse/services/tables/iface.go
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,10 @@
 | 
			
		|||
package tables
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"github.com/lmika/awstools/internal/dynamo-browse/models"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type TableProvider interface {
 | 
			
		||||
	ScanItems(ctx context.Context, tableName string) ([]models.Item, error)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										52
									
								
								internal/dynamo-browse/services/tables/service.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								internal/dynamo-browse/services/tables/service.go
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,52 @@
 | 
			
		|||
package tables
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"github.com/lmika/awstools/internal/dynamo-browse/models"
 | 
			
		||||
	"github.com/pkg/errors"
 | 
			
		||||
	"sort"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Service struct {
 | 
			
		||||
	provider TableProvider
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewService(provider TableProvider) *Service {
 | 
			
		||||
	return &Service{
 | 
			
		||||
		provider: provider,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *Service) Scan(ctx context.Context, table string) (*models.ResultSet, error) {
 | 
			
		||||
	results, err := s.provider.ScanItems(ctx, table)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, errors.Wrapf(err, "unable to scan table %v", table)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Get the columns
 | 
			
		||||
	// TODO: need to get PKs and SKs from table
 | 
			
		||||
	seenColumns := make(map[string]int)
 | 
			
		||||
	seenColumns["pk"] = 0
 | 
			
		||||
	seenColumns["sk"] = 1
 | 
			
		||||
 | 
			
		||||
	for _, result := range results {
 | 
			
		||||
		for k := range result {
 | 
			
		||||
			if _, isSeen := seenColumns[k]; !isSeen {
 | 
			
		||||
				seenColumns[k] = len(seenColumns)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	columns := make([]string, 0, len(seenColumns))
 | 
			
		||||
	for k := range seenColumns {
 | 
			
		||||
		columns = append(columns, k)
 | 
			
		||||
	}
 | 
			
		||||
	sort.Slice(columns, func(i, j int) bool {
 | 
			
		||||
		return seenColumns[columns[i]] < seenColumns[columns[j]]
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	return &models.ResultSet{
 | 
			
		||||
		Columns: columns,
 | 
			
		||||
		Items: results,
 | 
			
		||||
	}, nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										10
									
								
								internal/dynamo-browse/ui/events.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								internal/dynamo-browse/ui/events.go
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,10 @@
 | 
			
		|||
package ui
 | 
			
		||||
 | 
			
		||||
import "github.com/lmika/awstools/internal/dynamo-browse/models"
 | 
			
		||||
 | 
			
		||||
type newResultSet struct {
 | 
			
		||||
	ResultSet *models.ResultSet
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type setStatusMessage string
 | 
			
		||||
type errorRaised error
 | 
			
		||||
							
								
								
									
										7
									
								
								internal/dynamo-browse/ui/iface.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								internal/dynamo-browse/ui/iface.go
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,7 @@
 | 
			
		|||
package ui
 | 
			
		||||
 | 
			
		||||
import tea "github.com/charmbracelet/bubbletea"
 | 
			
		||||
 | 
			
		||||
type MessagePublisher interface {
 | 
			
		||||
	Send(msg tea.Msg)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										196
									
								
								internal/dynamo-browse/ui/model.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										196
									
								
								internal/dynamo-browse/ui/model.go
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,196 @@
 | 
			
		|||
package ui
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
 | 
			
		||||
	table "github.com/calyptia/go-bubble-table"
 | 
			
		||||
	"github.com/charmbracelet/bubbles/viewport"
 | 
			
		||||
	tea "github.com/charmbracelet/bubbletea"
 | 
			
		||||
	"github.com/charmbracelet/lipgloss"
 | 
			
		||||
	"github.com/lmika/awstools/internal/dynamo-browse/models"
 | 
			
		||||
	"github.com/lmika/awstools/internal/dynamo-browse/services/tables"
 | 
			
		||||
	"log"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"text/tabwriter"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type uiModel struct {
 | 
			
		||||
	table    table.Model
 | 
			
		||||
	viewport viewport.Model
 | 
			
		||||
 | 
			
		||||
	msgPublisher MessagePublisher
 | 
			
		||||
	tableService *tables.Service
 | 
			
		||||
	tableName    string
 | 
			
		||||
 | 
			
		||||
	tableWidth, tableHeight int
 | 
			
		||||
 | 
			
		||||
	ready     bool
 | 
			
		||||
	resultSet *models.ResultSet
 | 
			
		||||
	message   string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewModel(tableService *tables.Service, msgPublisher MessagePublisher, tableName string) tea.Model {
 | 
			
		||||
	tbl := table.New([]string{"pk", "sk"}, 100, 20)
 | 
			
		||||
	rows := make([]table.Row, 0)
 | 
			
		||||
	tbl.SetRows(rows)
 | 
			
		||||
 | 
			
		||||
	model := uiModel{
 | 
			
		||||
		table:        tbl,
 | 
			
		||||
		tableService: tableService,
 | 
			
		||||
		tableName:    tableName,
 | 
			
		||||
		msgPublisher: msgPublisher,
 | 
			
		||||
		message:      "Press s to scan",
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return model
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m uiModel) Init() tea.Cmd {
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *uiModel) updateTable() {
 | 
			
		||||
	if !m.ready {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	newTbl := table.New(m.resultSet.Columns, m.tableWidth, m.tableHeight)
 | 
			
		||||
	newRows := make([]table.Row, len(m.resultSet.Items))
 | 
			
		||||
	for i, r := range m.resultSet.Items {
 | 
			
		||||
		newRows[i] = itemTableRow{m.resultSet, r}
 | 
			
		||||
	}
 | 
			
		||||
	newTbl.SetRows(newRows)
 | 
			
		||||
 | 
			
		||||
	m.table = newTbl
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *uiModel) updateViewportToSelectedMessage() {
 | 
			
		||||
	if !m.ready {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if m.resultSet == nil || len(m.resultSet.Items) == 0 {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	selectedItem, ok := m.table.SelectedRow().(itemTableRow)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		m.viewport.SetContent("(no row selected)")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	viewportContent := &strings.Builder{}
 | 
			
		||||
	tabWriter := tabwriter.NewWriter(viewportContent, 0, 1, 1, ' ', 0)
 | 
			
		||||
	for _, colName := range selectedItem.resultSet.Columns {
 | 
			
		||||
		fmt.Fprintf(tabWriter, "%v\t", colName)
 | 
			
		||||
 | 
			
		||||
		switch colVal := selectedItem.item[colName].(type) {
 | 
			
		||||
		case nil:
 | 
			
		||||
			fmt.Fprintln(tabWriter, "(nil)")
 | 
			
		||||
		case *types.AttributeValueMemberS:
 | 
			
		||||
			fmt.Fprintln(tabWriter, colVal.Value)
 | 
			
		||||
		case *types.AttributeValueMemberN:
 | 
			
		||||
			fmt.Fprintln(tabWriter, colVal.Value)
 | 
			
		||||
		default:
 | 
			
		||||
			fmt.Fprintln(tabWriter, "(other)")
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	tabWriter.Flush()
 | 
			
		||||
	m.viewport.SetContent(viewportContent.String())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 | 
			
		||||
	switch msg := msg.(type) {
 | 
			
		||||
	case setStatusMessage:
 | 
			
		||||
		m.message = ""
 | 
			
		||||
	case errorRaised:
 | 
			
		||||
		m.message = "Error: " + msg.Error()
 | 
			
		||||
	case newResultSet:
 | 
			
		||||
		m.resultSet = msg.ResultSet
 | 
			
		||||
		m.updateTable()
 | 
			
		||||
		m.updateViewportToSelectedMessage()
 | 
			
		||||
	case tea.WindowSizeMsg:
 | 
			
		||||
		footerHeight := lipgloss.Height(m.footerView())
 | 
			
		||||
		tableHeight := msg.Height / 2
 | 
			
		||||
 | 
			
		||||
		if !m.ready {
 | 
			
		||||
			m.viewport = viewport.New(msg.Width, msg.Height-tableHeight-footerHeight)
 | 
			
		||||
			m.viewport.SetContent("(no message selected)")
 | 
			
		||||
			m.ready = true
 | 
			
		||||
		} else {
 | 
			
		||||
			m.viewport.Width = msg.Width
 | 
			
		||||
			m.viewport.Height = msg.Height - tableHeight - footerHeight
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		m.tableWidth, m.tableHeight = msg.Width, tableHeight
 | 
			
		||||
		m.table.SetSize(m.tableWidth, m.tableHeight)
 | 
			
		||||
 | 
			
		||||
	case tea.KeyMsg:
 | 
			
		||||
 | 
			
		||||
		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":
 | 
			
		||||
			return m, tea.Quit
 | 
			
		||||
		case "up", "i":
 | 
			
		||||
			m.table.GoUp()
 | 
			
		||||
			m.updateViewportToSelectedMessage()
 | 
			
		||||
		case "down", "k":
 | 
			
		||||
			m.table.GoDown()
 | 
			
		||||
			m.updateViewportToSelectedMessage()
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	updatedTable, tableMsgs := m.table.Update(msg)
 | 
			
		||||
	updatedViewport, viewportMsgs := m.viewport.Update(msg)
 | 
			
		||||
 | 
			
		||||
	m.table = updatedTable
 | 
			
		||||
	m.viewport = updatedViewport
 | 
			
		||||
 | 
			
		||||
	return m, tea.Batch(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 {
 | 
			
		||||
	if !m.ready {
 | 
			
		||||
		return "Initializing"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	log.Println("Returning full view")
 | 
			
		||||
	return lipgloss.JoinVertical(lipgloss.Top, m.table.View(), m.viewport.View(), m.footerView())
 | 
			
		||||
	//return lipgloss.JoinVertical(lipgloss.Top, m.table.View(), m.footerView())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m uiModel) footerView() string {
 | 
			
		||||
	title := m.message
 | 
			
		||||
	line := strings.Repeat(" ", max(0, m.viewport.Width-lipgloss.Width(title)))
 | 
			
		||||
	return lipgloss.JoinHorizontal(lipgloss.Left, title, line)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func max(a, b int) int {
 | 
			
		||||
	if a > b {
 | 
			
		||||
		return a
 | 
			
		||||
	}
 | 
			
		||||
	return b
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										40
									
								
								internal/dynamo-browse/ui/tblmodel.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								internal/dynamo-browse/ui/tblmodel.go
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,40 @@
 | 
			
		|||
package ui
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
 | 
			
		||||
	table "github.com/calyptia/go-bubble-table"
 | 
			
		||||
	"github.com/lmika/awstools/internal/dynamo-browse/models"
 | 
			
		||||
	"io"
 | 
			
		||||
	"strings"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type itemTableRow struct {
 | 
			
		||||
	resultSet *models.ResultSet
 | 
			
		||||
	item      models.Item
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (mtr itemTableRow) Render(w io.Writer, model table.Model, index int) {
 | 
			
		||||
	sb := strings.Builder{}
 | 
			
		||||
	for i, colName := range mtr.resultSet.Columns {
 | 
			
		||||
		if i > 0 {
 | 
			
		||||
			sb.WriteString("\t")
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		switch colVal := mtr.item[colName].(type) {
 | 
			
		||||
		case nil:
 | 
			
		||||
			sb.WriteString("(nil)")
 | 
			
		||||
		case *types.AttributeValueMemberS:
 | 
			
		||||
			sb.WriteString(colVal.Value)
 | 
			
		||||
		case *types.AttributeValueMemberN:
 | 
			
		||||
			sb.WriteString(colVal.Value)
 | 
			
		||||
		default:
 | 
			
		||||
			sb.WriteString("(other)")
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	if index == model.Cursor() {
 | 
			
		||||
		fmt.Fprintln(w, model.Styles.SelectedRow.Render(sb.String()))
 | 
			
		||||
	} else {
 | 
			
		||||
		fmt.Fprintln(w, sb.String())
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										10
									
								
								internal/sqs-browse/services/messages/iface.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								internal/sqs-browse/services/messages/iface.go
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,10 @@
 | 
			
		|||
package messages
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"github.com/lmika/awstools/internal/sqs-browse/models"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type MessageSender interface {
 | 
			
		||||
	SendMessage(ctx context.Context, msg models.Message, queue string) error
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										19
									
								
								internal/sqs-browse/services/messages/service.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								internal/sqs-browse/services/messages/service.go
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,19 @@
 | 
			
		|||
package messages
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"github.com/lmika/awstools/internal/sqs-browse/models"
 | 
			
		||||
	"github.com/pkg/errors"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Service struct {
 | 
			
		||||
	messageSender MessageSender
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewService() *Service {
 | 
			
		||||
	return &Service{}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *Service) SendTo(ctx context.Context, msg models.Message, destQueue string) error {
 | 
			
		||||
	return errors.Wrapf(s.messageSender.SendMessage(ctx, msg, destQueue), "cannot send message to %v", destQueue)
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
		Reference in a new issue