From 1969504611da76b8cd12fc9e888de43804600b42 Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Wed, 23 Mar 2022 11:56:33 +1100 Subject: [PATCH] sqs-browse: Added dynamo-browse Added another tool for browsing DynamoDB tables --- .gitignore | 1 + cmd/dynamo-browse/main.go | 65 ++++++ go.mod | 12 +- go.sum | 16 ++ internal/dynamo-browse/models/models.go | 10 + .../providers/dynamo/provider.go | 33 +++ .../dynamo-browse/services/tables/iface.go | 10 + .../dynamo-browse/services/tables/service.go | 52 +++++ internal/dynamo-browse/ui/events.go | 10 + internal/dynamo-browse/ui/iface.go | 7 + internal/dynamo-browse/ui/model.go | 196 ++++++++++++++++++ internal/dynamo-browse/ui/tblmodel.go | 40 ++++ .../sqs-browse/services/messages/iface.go | 10 + .../sqs-browse/services/messages/service.go | 19 ++ 14 files changed, 477 insertions(+), 4 deletions(-) create mode 100644 .gitignore create mode 100644 cmd/dynamo-browse/main.go create mode 100644 internal/dynamo-browse/models/models.go create mode 100644 internal/dynamo-browse/providers/dynamo/provider.go create mode 100644 internal/dynamo-browse/services/tables/iface.go create mode 100644 internal/dynamo-browse/services/tables/service.go create mode 100644 internal/dynamo-browse/ui/events.go create mode 100644 internal/dynamo-browse/ui/iface.go create mode 100644 internal/dynamo-browse/ui/model.go create mode 100644 internal/dynamo-browse/ui/tblmodel.go create mode 100644 internal/sqs-browse/services/messages/iface.go create mode 100644 internal/sqs-browse/services/messages/service.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b14c548 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +debug.log diff --git a/cmd/dynamo-browse/main.go b/cmd/dynamo-browse/main.go new file mode 100644 index 0000000..1c83968 --- /dev/null +++ b/cmd/dynamo-browse/main.go @@ -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) +} + diff --git a/go.mod b/go.mod index a184069..a734757 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index cbd4468..3884602 100644 --- a/go.sum +++ b/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= diff --git a/internal/dynamo-browse/models/models.go b/internal/dynamo-browse/models/models.go new file mode 100644 index 0000000..3bb2762 --- /dev/null +++ b/internal/dynamo-browse/models/models.go @@ -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 diff --git a/internal/dynamo-browse/providers/dynamo/provider.go b/internal/dynamo-browse/providers/dynamo/provider.go new file mode 100644 index 0000000..cb07719 --- /dev/null +++ b/internal/dynamo-browse/providers/dynamo/provider.go @@ -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 +} diff --git a/internal/dynamo-browse/services/tables/iface.go b/internal/dynamo-browse/services/tables/iface.go new file mode 100644 index 0000000..b8a059f --- /dev/null +++ b/internal/dynamo-browse/services/tables/iface.go @@ -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) +} diff --git a/internal/dynamo-browse/services/tables/service.go b/internal/dynamo-browse/services/tables/service.go new file mode 100644 index 0000000..b3fbbd5 --- /dev/null +++ b/internal/dynamo-browse/services/tables/service.go @@ -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 +} diff --git a/internal/dynamo-browse/ui/events.go b/internal/dynamo-browse/ui/events.go new file mode 100644 index 0000000..9165527 --- /dev/null +++ b/internal/dynamo-browse/ui/events.go @@ -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 \ No newline at end of file diff --git a/internal/dynamo-browse/ui/iface.go b/internal/dynamo-browse/ui/iface.go new file mode 100644 index 0000000..ade311a --- /dev/null +++ b/internal/dynamo-browse/ui/iface.go @@ -0,0 +1,7 @@ +package ui + +import tea "github.com/charmbracelet/bubbletea" + +type MessagePublisher interface { + Send(msg tea.Msg) +} diff --git a/internal/dynamo-browse/ui/model.go b/internal/dynamo-browse/ui/model.go new file mode 100644 index 0000000..30b80f1 --- /dev/null +++ b/internal/dynamo-browse/ui/model.go @@ -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 +} diff --git a/internal/dynamo-browse/ui/tblmodel.go b/internal/dynamo-browse/ui/tblmodel.go new file mode 100644 index 0000000..8609ce0 --- /dev/null +++ b/internal/dynamo-browse/ui/tblmodel.go @@ -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()) + } +} diff --git a/internal/sqs-browse/services/messages/iface.go b/internal/sqs-browse/services/messages/iface.go new file mode 100644 index 0000000..7efd351 --- /dev/null +++ b/internal/sqs-browse/services/messages/iface.go @@ -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 +} \ No newline at end of file diff --git a/internal/sqs-browse/services/messages/service.go b/internal/sqs-browse/services/messages/service.go new file mode 100644 index 0000000..e9ffc7c --- /dev/null +++ b/internal/sqs-browse/services/messages/service.go @@ -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) +}