diff --git a/cmd/dynamo-browse/main.go b/cmd/dynamo-browse/main.go index 9035221..803afd4 100644 --- a/cmd/dynamo-browse/main.go +++ b/cmd/dynamo-browse/main.go @@ -4,7 +4,12 @@ import ( "context" "flag" "fmt" + "log" + "os" + "time" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/credentials" "github.com/aws/aws-sdk-go-v2/service/dynamodb" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" @@ -16,15 +21,13 @@ import ( "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/teamodels" + "github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/dynamotableview" "github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/frame" "github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/layout" "github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/modal" "github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/statusandprompt" "github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/tableselect" "github.com/lmika/gopkgs/cli" - "log" - "os" - "time" ) func main() { @@ -33,7 +36,13 @@ func main() { flag.Parse() ctx := context.Background() - cfg, err := config.LoadDefaultConfig(ctx) + + // TEMP + cfg, err := config.LoadDefaultConfig(ctx, + config.WithRegion("ap-southeast-2"), + config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider("abc", "123", ""))) + + // END TEMP if err != nil { cli.Fatalf("cannot load AWS config: %v", err) } @@ -57,9 +66,9 @@ func main() { tableWriteController := controllers.NewTableWriteController(tableService, tableReadController, *flagTable) commandController := commandctrl.NewCommandController(map[string]uimodels.Operation{ - "scan": tableReadController.Scan(), - "rw": tableWriteController.ToggleReadWrite(), - "dup": tableWriteController.Duplicate(), + // "scan": tableReadController.Scan(), + "rw": tableWriteController.ToggleReadWrite(), + "dup": tableWriteController.Duplicate(), }) uiModel := ui.NewModel(uiDispatcher, commandController, tableReadController, tableWriteController) @@ -70,7 +79,8 @@ func main() { var model tea.Model = statusandprompt.New( layout.NewVBox( - frame.NewFrame("This is the header", true, layout.Model(newTestModel("this is the top"))), + layout.LastChildFixedAt(11), + frame.NewFrame("This is the header", true, dynamotableview.New(tableReadController)), frame.NewFrame("This is another header", false, layout.Model(newTestModel("this is the bottom"))), ), "Hello world", diff --git a/internal/dynamo-browse/controllers/tableread.go b/internal/dynamo-browse/controllers/tableread.go index 94ade82..85f4737 100644 --- a/internal/dynamo-browse/controllers/tableread.go +++ b/internal/dynamo-browse/controllers/tableread.go @@ -2,8 +2,9 @@ package controllers import ( "context" + "log" - "github.com/lmika/awstools/internal/common/ui/uimodels" + tea "github.com/charmbracelet/bubbletea" "github.com/lmika/awstools/internal/dynamo-browse/models" "github.com/lmika/awstools/internal/dynamo-browse/services/tables" "github.com/pkg/errors" @@ -21,6 +22,30 @@ func NewTableReadController(tableService *tables.Service, tableName string) *Tab } } +func (c *TableReadController) Scan() tea.Cmd { + return func() tea.Msg { + ctx := context.Background() + + log.Println("Fetching table info") + tableInfo, err := c.tableInfo(ctx) + if err != nil { + log.Println("error: ", err) + return err + } + + log.Println("Scanning") + resultSet, err := c.tableService.Scan(ctx, tableInfo) + if err != nil { + log.Println("error: ", err) + return err + } + + log.Println("Scan done") + return NewResultSet{resultSet} + } +} + +/* func (c *TableReadController) Scan() uimodels.Operation { return uimodels.OperationFn(func(ctx context.Context) error { return c.doScan(ctx, false) @@ -50,13 +75,16 @@ func (c *TableReadController) doScan(ctx context.Context, quiet bool) (err error uiCtx.Send(NewResultSet{resultSet}) return nil } +*/ // tableInfo returns the table info from the state if a result set exists. If not, it fetches the // table information from the service. func (c *TableReadController) tableInfo(ctx context.Context) (*models.TableInfo, error) { - if existingResultSet := CurrentState(ctx).ResultSet; existingResultSet != nil { - return existingResultSet.TableInfo, nil - } + /* + if existingResultSet := CurrentState(ctx).ResultSet; existingResultSet != nil { + return existingResultSet.TableInfo, nil + } + */ tableInfo, err := c.tableService.Describe(ctx, c.tableName) if err != nil { diff --git a/internal/dynamo-browse/controllers/tablewrite.go b/internal/dynamo-browse/controllers/tablewrite.go index 57b252c..938fd47 100644 --- a/internal/dynamo-browse/controllers/tablewrite.go +++ b/internal/dynamo-browse/controllers/tablewrite.go @@ -81,9 +81,9 @@ func (c *TableWriteController) Duplicate() uimodels.Operation { } // Rescan to get updated items - if err := c.tableReadControllers.doScan(ctx, true); err != nil { - return err - } + // if err := c.tableReadControllers.doScan(ctx, true); err != nil { + // return err + // } return nil })) @@ -122,9 +122,9 @@ func (c *TableWriteController) Delete() uimodels.Operation { } // Rescan to get updated items - if err := c.tableReadControllers.doScan(ctx, true); err != nil { - return err - } + // if err := c.tableReadControllers.doScan(ctx, true); err != nil { + // return err + // } uiCtx.Message("Item deleted") return nil diff --git a/internal/dynamo-browse/ui/model.go b/internal/dynamo-browse/ui/model.go index b9f4eee..4d86cc1 100644 --- a/internal/dynamo-browse/ui/model.go +++ b/internal/dynamo-browse/ui/model.go @@ -2,11 +2,8 @@ package ui import ( "context" - "fmt" "strings" - "text/tabwriter" - "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" table "github.com/calyptia/go-bubble-table" "github.com/charmbracelet/bubbles/textinput" "github.com/charmbracelet/bubbles/viewport" @@ -83,6 +80,7 @@ func (m uiModel) Init() tea.Cmd { return nil } +/* func (m *uiModel) updateTable() { if !m.ready { return @@ -99,6 +97,8 @@ func (m *uiModel) updateTable() { m.table = newTbl } + + func (m *uiModel) selectedItem() (itemTableRow, bool) { resultSet := m.state.ResultSet if m.ready && resultSet != nil && len(resultSet.Items) > 0 { @@ -136,6 +136,7 @@ func (m *uiModel) updateViewportToSelectedMessage() { tabWriter.Flush() m.viewport.SetContent(viewportContent.String()) } +*/ func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var textInputCommands tea.Cmd @@ -145,8 +146,8 @@ func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // Local events case controllers.NewResultSet: m.state.ResultSet = msg.ResultSet - m.updateTable() - m.updateViewportToSelectedMessage() + // m.updateTable() + // m.updateViewportToSelectedMessage() case controllers.SetReadWrite: m.state.InReadWriteMode = msg.NewValue @@ -203,16 +204,16 @@ func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, tea.Quit case "up", "i": m.table.GoUp() - m.updateViewportToSelectedMessage() + // m.updateViewportToSelectedMessage() case "down", "k": m.table.GoDown() - m.updateViewportToSelectedMessage() + // m.updateViewportToSelectedMessage() // TODO: these should be moved somewhere else case ":": m.invokeOperation(context.Background(), m.commandController.Prompt()) - case "s": - m.invokeOperation(context.Background(), m.tableReadController.Scan()) + // case "s": + // m.invokeOperation(context.Background(), m.tableReadController.Scan()) case "D": m.invokeOperation(context.Background(), m.tableWriteController.Delete()) } @@ -233,9 +234,9 @@ func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (m uiModel) invokeOperation(ctx context.Context, op uimodels.Operation) { state := m.state - if selectedItem, ok := m.selectedItem(); ok { - state.SelectedItem = selectedItem.item - } + // if selectedItem, ok := m.selectedItem(); ok { + // state.SelectedItem = selectedItem.item + // } ctx = controllers.ContextWithState(ctx, state) m.dispatcher.Start(ctx, op) diff --git a/internal/dynamo-browse/ui/teamodels/dynamotableview/model.go b/internal/dynamo-browse/ui/teamodels/dynamotableview/model.go new file mode 100644 index 0000000..925d067 --- /dev/null +++ b/internal/dynamo-browse/ui/teamodels/dynamotableview/model.go @@ -0,0 +1,120 @@ +package dynamotableview + +import ( + table "github.com/calyptia/go-bubble-table" + tea "github.com/charmbracelet/bubbletea" + "github.com/lmika/awstools/internal/dynamo-browse/controllers" + "github.com/lmika/awstools/internal/dynamo-browse/models" + "github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/layout" +) + +type Model struct { + tableReadControllers *controllers.TableReadController + table table.Model + w, h int + + // model state + resultSet *models.ResultSet +} + +func New(tableReadControllers *controllers.TableReadController) Model { + tbl := table.New([]string{"pk", "sk"}, 100, 100) + rows := make([]table.Row, 0) + tbl.SetRows(rows) + + return Model{tableReadControllers: tableReadControllers, table: tbl} +} + +func (m Model) Init() tea.Cmd { + return nil +} + +func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case controllers.NewResultSet: + m.resultSet = msg.ResultSet + m.updateTable() + return m, nil + case tea.KeyMsg: + switch msg.String() { + // Table nav + case "i", "up": + m.table.GoUp() + return m, nil + case "k", "down": + m.table.GoDown() + return m, nil + + // TEMP + case "s": + return m, m.tableReadControllers.Scan() + case "ctrl+c", "esc": + return m, tea.Quit + } + } + + return m, nil +} + +func (m Model) View() string { + return m.table.View() +} + +func (m Model) Resize(w, h int) layout.ResizingModel { + m.w, m.h = w, h + m.table.SetSize(w, h) + return m +} + +func (m *Model) updateTable() { + resultSet := m.resultSet + + newTbl := table.New(resultSet.Columns, m.w, m.h) + newRows := make([]table.Row, len(resultSet.Items)) + for i, r := range resultSet.Items { + newRows[i] = itemTableRow{resultSet, r} + } + newTbl.SetRows(newRows) + + m.table = newTbl +} + +func (m *Model) selectedItem() (itemTableRow, bool) { + resultSet := m.resultSet + if resultSet != nil && len(resultSet.Items) > 0 { + selectedItem, ok := m.table.SelectedRow().(itemTableRow) + if ok { + return selectedItem, true + } + } + + return itemTableRow{}, false +} + +/* +func (m *Model) updateViewportToSelectedMessage() { + selectedItem, ok := m.selectedItem() + 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 { + switch colVal := selectedItem.item[colName].(type) { + case nil: + break + case *types.AttributeValueMemberS: + fmt.Fprintf(tabWriter, "%v\tS\t%s\n", colName, colVal.Value) + case *types.AttributeValueMemberN: + fmt.Fprintf(tabWriter, "%v\tN\t%s\n", colName, colVal.Value) + default: + fmt.Fprintf(tabWriter, "%v\t?\t%s\n", colName, "(other)") + } + } + + tabWriter.Flush() + m.viewport.SetContent(viewportContent.String()) +} +*/ diff --git a/internal/dynamo-browse/ui/tblmodel.go b/internal/dynamo-browse/ui/teamodels/dynamotableview/tblmodel.go similarity index 97% rename from internal/dynamo-browse/ui/tblmodel.go rename to internal/dynamo-browse/ui/teamodels/dynamotableview/tblmodel.go index 6cb8d41..1137062 100644 --- a/internal/dynamo-browse/ui/tblmodel.go +++ b/internal/dynamo-browse/ui/teamodels/dynamotableview/tblmodel.go @@ -1,4 +1,4 @@ -package ui +package dynamotableview import ( "fmt" diff --git a/internal/dynamo-browse/ui/teamodels/layout/boxsize.go b/internal/dynamo-browse/ui/teamodels/layout/boxsize.go new file mode 100644 index 0000000..cfba488 --- /dev/null +++ b/internal/dynamo-browse/ui/teamodels/layout/boxsize.go @@ -0,0 +1,36 @@ +package layout + +type BoxSize interface { + childSize(idx, cnt, available int) int +} + +func EqualSize() BoxSize { + return equalSize{} +} + +type equalSize struct { +} + +func (l equalSize) childSize(idx, cnt, available int) int { + childrenHeight := available / cnt + lastChildRem := available % cnt + if idx == cnt-1 { + return childrenHeight + lastChildRem + } + return childrenHeight +} + +func LastChildFixedAt(size int) BoxSize { + return lastChildFixedAt{size} +} + +type lastChildFixedAt struct { + lastChildSize int +} + +func (l lastChildFixedAt) childSize(idx, cnt, available int) int { + if idx == cnt-1 { + return l.lastChildSize + } + return (equalSize{}).childSize(idx, cnt-1, available-l.lastChildSize) +} diff --git a/internal/dynamo-browse/ui/teamodels/layout/vbox.go b/internal/dynamo-browse/ui/teamodels/layout/vbox.go index 1dac2c2..232099f 100644 --- a/internal/dynamo-browse/ui/teamodels/layout/vbox.go +++ b/internal/dynamo-browse/ui/teamodels/layout/vbox.go @@ -1,18 +1,20 @@ package layout import ( + "strings" + tea "github.com/charmbracelet/bubbletea" "github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/utils" - "strings" ) // VBox is a model which will display its children vertically. type VBox struct { + boxSize BoxSize children []ResizingModel } -func NewVBox(children ...ResizingModel) VBox { - return VBox{children: children} +func NewVBox(boxSize BoxSize, children ...ResizingModel) VBox { + return VBox{boxSize: boxSize, children: children} } func (vb VBox) Init() tea.Cmd { @@ -43,14 +45,9 @@ func (vb VBox) View() string { } func (vb VBox) Resize(w, h int) ResizingModel { - childrenHeight := h / len(vb.children) - lastChildRem := h % len(vb.children) for i, c := range vb.children { - if i == len(vb.children)-1 { - vb.children[i] = c.Resize(w, childrenHeight+lastChildRem) - } else { - vb.children[i] = c.Resize(w, childrenHeight) - } + childHeight := vb.boxSize.childSize(i, len(vb.children), h) + vb.children[i] = c.Resize(w, childHeight) } return vb } diff --git a/test/cmd/load-test-table/main.go b/test/cmd/load-test-table/main.go index dfda661..1605966 100644 --- a/test/cmd/load-test-table/main.go +++ b/test/cmd/load-test-table/main.go @@ -6,6 +6,7 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/credentials" "github.com/aws/aws-sdk-go-v2/service/dynamodb" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" "github.com/brianvoe/gofakeit/v6" @@ -21,7 +22,9 @@ func main() { tableName := "awstools-test" totalItems := 300 - cfg, err := config.LoadDefaultConfig(ctx) + cfg, err := config.LoadDefaultConfig(ctx, + config.WithRegion("ap-southeast-2"), + config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider("abc", "123", ""))) if err != nil { cli.Fatalf("cannot load AWS config: %v", err) } @@ -32,7 +35,7 @@ func main() { if _, err = dynamoClient.DeleteTable(ctx, &dynamodb.DeleteTableInput{ TableName: aws.String(tableName), }); err != nil { - log.Printf("warn: cannot delete table: %v", tableName) + log.Printf("warn: cannot delete table: %v: %v", tableName, err) } if _, err = dynamoClient.CreateTable(ctx, &dynamodb.CreateTableInput{