diff --git a/internal/dynamo-browse/controllers/tablewrite.go b/internal/dynamo-browse/controllers/tablewrite.go index a7276e1..8099779 100644 --- a/internal/dynamo-browse/controllers/tablewrite.go +++ b/internal/dynamo-browse/controllers/tablewrite.go @@ -8,6 +8,7 @@ import ( "github.com/lmika/awstools/internal/common/ui/events" "github.com/lmika/awstools/internal/dynamo-browse/models" "github.com/lmika/awstools/internal/dynamo-browse/services/tables" + "github.com/pkg/errors" ) type TableWriteController struct { @@ -46,7 +47,7 @@ func (twc *TableWriteController) NewItem() tea.Cmd { } } -func (twc *TableWriteController) SetItemValue(idx int, key string) tea.Cmd { +func (twc *TableWriteController) SetStringValue(idx int, key string) tea.Cmd { return func() tea.Msg { return events.PromptForInputMsg{ Prompt: "string value: ", @@ -63,6 +64,31 @@ func (twc *TableWriteController) SetItemValue(idx int, key string) tea.Cmd { } } +func (twc *TableWriteController) PutItem(idx int) tea.Cmd { + return func() tea.Msg { + resultSet := twc.state.ResultSet() + if !resultSet.IsDirty(idx) { + return events.Error(errors.New("item is not dirty")) + } + + return events.PromptForInputMsg{ + Prompt: "put item? ", + OnDone: func(value string) tea.Cmd { + return func() tea.Msg { + if value != "y" { + return nil + } + + if err := twc.tableService.PutItemAt(context.Background(), resultSet, idx); err != nil { + return events.Error(err) + } + return ResultSetUpdated{} + } + }, + } + } +} + func (twc *TableWriteController) DeleteMarked() tea.Cmd { return func() tea.Msg { resultSet := twc.state.ResultSet() diff --git a/internal/dynamo-browse/controllers/tablewrite_test.go b/internal/dynamo-browse/controllers/tablewrite_test.go index 745a869..589f628 100644 --- a/internal/dynamo-browse/controllers/tablewrite_test.go +++ b/internal/dynamo-browse/controllers/tablewrite_test.go @@ -204,7 +204,7 @@ func TestTableWriteController_NewItem(t *testing.T) { }) } -func TestTableWriteController_SetItemValue(t *testing.T) { +func TestTableWriteController_SetStringValue(t *testing.T) { client, cleanupFn := testdynamo.SetupTestTable(t, testData) defer cleanupFn() @@ -221,10 +221,97 @@ func TestTableWriteController_SetItemValue(t *testing.T) { assert.Equal(t, "This is some value", before) assert.False(t, state.ResultSet().IsDirty(0)) - invokeCommandWithPrompt(t, writeController.SetItemValue(0, "alpha"), "a new value") + invokeCommandWithPrompt(t, writeController.SetStringValue(0, "alpha"), "a new value") after, _ := state.ResultSet().Items()[0].AttributeValueAsString("alpha") assert.Equal(t, "a new value", after) assert.True(t, state.ResultSet().IsDirty(0)) }) + + t.Run("should prevent duplicate partition,sort keys", func(t *testing.T) { + t.Skip("TODO") + }) +} + +func TestTableWriteController_PutItem(t *testing.T) { + t.Run("should put the selected item if dirty", func(t *testing.T) { + client, cleanupFn := testdynamo.SetupTestTable(t, testData) + defer cleanupFn() + + provider := dynamo.NewProvider(client) + service := tables.NewService(provider) + + state := controllers.NewState() + readController := controllers.NewTableReadController(state, service, "alpha-table") + writeController := controllers.NewTableWriteController(state, service, readController) + + // Read the table + invokeCommand(t, readController.Init()) + before, _ := state.ResultSet().Items()[0].AttributeValueAsString("alpha") + assert.Equal(t, "This is some value", before) + assert.False(t, state.ResultSet().IsDirty(0)) + + // Modify the item and put it + invokeCommandWithPrompt(t, writeController.SetStringValue(0, "alpha"), "a new value") + invokeCommandWithPrompt(t, writeController.PutItem(0), "y") + + // Rescan the table + invokeCommand(t, readController.Rescan()) + after, _ := state.ResultSet().Items()[0].AttributeValueAsString("alpha") + assert.Equal(t, "a new value", after) + assert.False(t, state.ResultSet().IsDirty(0)) + }) + + t.Run("should not put the selected item if user does not confirm", func(t *testing.T) { + client, cleanupFn := testdynamo.SetupTestTable(t, testData) + defer cleanupFn() + + provider := dynamo.NewProvider(client) + service := tables.NewService(provider) + + state := controllers.NewState() + readController := controllers.NewTableReadController(state, service, "alpha-table") + writeController := controllers.NewTableWriteController(state, service, readController) + + // Read the table + invokeCommand(t, readController.Init()) + before, _ := state.ResultSet().Items()[0].AttributeValueAsString("alpha") + assert.Equal(t, "This is some value", before) + assert.False(t, state.ResultSet().IsDirty(0)) + + // Modify the item but do not put it + invokeCommandWithPrompt(t, writeController.SetStringValue(0, "alpha"), "a new value") + invokeCommandWithPrompt(t, writeController.PutItem(0), "n") + + current, _ := state.ResultSet().Items()[0].AttributeValueAsString("alpha") + assert.Equal(t, "a new value", current) + assert.True(t, state.ResultSet().IsDirty(0)) + + // Rescan the table to confirm item is not modified + invokeCommand(t, readController.Rescan()) + after, _ := state.ResultSet().Items()[0].AttributeValueAsString("alpha") + assert.Equal(t, "This is some value", after) + assert.False(t, state.ResultSet().IsDirty(0)) + }) + + t.Run("should not put the selected item if not dirty", func(t *testing.T) { + client, cleanupFn := testdynamo.SetupTestTable(t, testData) + defer cleanupFn() + + provider := dynamo.NewProvider(client) + service := tables.NewService(provider) + + state := controllers.NewState() + readController := controllers.NewTableReadController(state, service, "alpha-table") + writeController := controllers.NewTableWriteController(state, service, readController) + + // Read the table + invokeCommand(t, readController.Init()) + before, _ := state.ResultSet().Items()[0].AttributeValueAsString("alpha") + assert.Equal(t, "This is some value", before) + assert.False(t, state.ResultSet().IsDirty(0)) + + invokeCommandExpectingError(t, writeController.PutItem(0)) + }) + } diff --git a/internal/dynamo-browse/services/tables/service.go b/internal/dynamo-browse/services/tables/service.go index 12ee69b..8e3a54d 100644 --- a/internal/dynamo-browse/services/tables/service.go +++ b/internal/dynamo-browse/services/tables/service.go @@ -81,6 +81,17 @@ func (s *Service) Put(ctx context.Context, tableInfo *models.TableInfo, item mod return s.provider.PutItem(ctx, tableInfo.Name, item) } +func (s *Service) PutItemAt(ctx context.Context, resultSet *models.ResultSet, index int) error { + item := resultSet.Items()[index] + if err := s.provider.PutItem(ctx, resultSet.TableInfo.Name, item); err != nil { + return err + } + + resultSet.SetDirty(index, false) + resultSet.SetNew(index, false) + return nil +} + func (s *Service) Delete(ctx context.Context, tableInfo *models.TableInfo, items []models.Item) error { for _, item := range items { if err := s.provider.DeleteItem(ctx, tableInfo.Name, item.KeyValue(tableInfo)); err != nil { diff --git a/internal/dynamo-browse/ui/model.go b/internal/dynamo-browse/ui/model.go index 28ff05b..c048d15 100644 --- a/internal/dynamo-browse/ui/model.go +++ b/internal/dynamo-browse/ui/model.go @@ -51,11 +51,14 @@ func NewModel(rc *controllers.TableReadController, wc *controllers.TableWriteCon // TEMP "new-item": commandctrl.NoArgCommand(wc.NewItem()), - "set": func(args []string) tea.Cmd { + "set-string": func(args []string) tea.Cmd { if len(args) != 1 { return events.SetError(errors.New("expected attribute key")) } - return wc.SetItemValue(dtv.SelectedItemIndex(), args[0]) + return wc.SetStringValue(dtv.SelectedItemIndex(), args[0]) + }, + "put": func(args []string) tea.Cmd { + return wc.PutItem(dtv.SelectedItemIndex()) }, }, }) diff --git a/internal/dynamo-browse/ui/teamodels/dynamotableview/tblmodel.go b/internal/dynamo-browse/ui/teamodels/dynamotableview/tblmodel.go index 5e5a08d..65e8dd3 100644 --- a/internal/dynamo-browse/ui/teamodels/dynamotableview/tblmodel.go +++ b/internal/dynamo-browse/ui/teamodels/dynamotableview/tblmodel.go @@ -14,6 +14,10 @@ import ( var ( markedRowStyle = lipgloss.NewStyle(). Background(lipgloss.Color("#e1e1e1")) + dirtyRowStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("#e13131")) + newRowStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("#31e131")) ) type itemTableRow struct { @@ -24,6 +28,8 @@ type itemTableRow struct { func (mtr itemTableRow) Render(w io.Writer, model table.Model, index int) { isMarked := mtr.resultSet.Marked(mtr.itemIndex) + isDirty := mtr.resultSet.IsDirty(mtr.itemIndex) + isNew := mtr.resultSet.IsNew(mtr.itemIndex) sb := strings.Builder{} for i, colName := range mtr.resultSet.Columns { @@ -42,15 +48,20 @@ func (mtr itemTableRow) Render(w io.Writer, model table.Model, index int) { sb.WriteString("(other)") } } + + var style lipgloss.Style + if index == model.Cursor() { - style := model.Styles.SelectedRow - if isMarked { - style = style.Copy().Inherit(markedRowStyle) - } - fmt.Fprintln(w, style.Render(sb.String())) - } else if isMarked { - fmt.Fprintln(w, markedRowStyle.Render(sb.String())) - } else { - fmt.Fprintln(w, sb.String()) + style = model.Styles.SelectedRow } + if isMarked { + style = style.Copy().Inherit(markedRowStyle) + } + if isNew { + style = style.Copy().Inherit(newRowStyle) + } else if isDirty { + style = style.Copy().Inherit(dirtyRowStyle) + } + + fmt.Fprintln(w, style.Render(sb.String())) }