diff --git a/internal/participation.go b/internal/participation.go index 5a05c9ad..29ef856a 100644 --- a/internal/participation.go +++ b/internal/participation.go @@ -3,8 +3,9 @@ package internal import ( "context" "errors" - "github.com/algorandfoundation/hack-tui/api" "time" + + "github.com/algorandfoundation/hack-tui/api" ) // GetPartKeys get the participation keys from the node @@ -116,3 +117,13 @@ func DeletePartKey(ctx context.Context, client *api.ClientWithResponses, partici } return nil } + +// Removes a participation key from the list of keys +func RemovePartKeyByID(slice *[]api.ParticipationKey, id string) { + for i, item := range *slice { + if item.Id == id { + *slice = append((*slice)[:i], (*slice)[i+1:]...) + return + } + } +} diff --git a/internal/participation_test.go b/internal/participation_test.go index 69038dac..25883663 100644 --- a/internal/participation_test.go +++ b/internal/participation_test.go @@ -146,3 +146,58 @@ func Test_DeleteParticipationKey(t *testing.T) { t.Fatal(err) } } +func Test_RemovePartKeyByID(t *testing.T) { + // Test case: Remove an existing key + t.Run("Remove existing key", func(t *testing.T) { + keys := []api.ParticipationKey{ + {Id: "key1"}, + {Id: "key2"}, + {Id: "key3"}, + } + expectedKeys := []api.ParticipationKey{ + {Id: "key1"}, + {Id: "key3"}, + } + RemovePartKeyByID(&keys, "key2") + if len(keys) != len(expectedKeys) { + t.Fatalf("expected %d keys, got %d", len(expectedKeys), len(keys)) + } + for i, key := range keys { + if key.Id != expectedKeys[i].Id { + t.Fatalf("expected key ID %s, got %s", expectedKeys[i].Id, key.Id) + } + } + }) + + // Test case: Remove a non-existing key + t.Run("Remove non-existing key", func(t *testing.T) { + keys := []api.ParticipationKey{ + {Id: "key1"}, + {Id: "key2"}, + {Id: "key3"}, + } + expectedKeys := []api.ParticipationKey{ + {Id: "key1"}, + {Id: "key2"}, + {Id: "key3"}, + } + RemovePartKeyByID(&keys, "key4") + if len(keys) != len(expectedKeys) { + t.Fatalf("expected %d keys, got %d", len(expectedKeys), len(keys)) + } + for i, key := range keys { + if key.Id != expectedKeys[i].Id { + t.Fatalf("expected key ID %s, got %s", expectedKeys[i].Id, key.Id) + } + } + }) + + // Test case: Remove a key from an empty list + t.Run("Remove key from empty list", func(t *testing.T) { + keys := []api.ParticipationKey{} + RemovePartKeyByID(&keys, "key1") + if len(keys) != 0 { + t.Fatalf("expected 0 keys, got %d", len(keys)) + } + }) +} diff --git a/ui/pages/accounts/controller.go b/ui/pages/accounts/controller.go index 434f0eaf..139ee6dc 100644 --- a/ui/pages/accounts/controller.go +++ b/ui/pages/accounts/controller.go @@ -26,7 +26,11 @@ func (m ViewModel) HandleMessage(msg tea.Msg) (ViewModel, tea.Cmd) { case tea.KeyMsg: switch msg.String() { case "enter": - return m, EmitAccountSelected(m.SelectedAccount()) + selAcc := m.SelectedAccount() + if selAcc != (internal.Account{}) { + return m, EmitAccountSelected(selAcc) + } + return m, nil case "ctrl+c": return m, tea.Quit } diff --git a/ui/pages/keys/cmds.go b/ui/pages/keys/cmds.go index 167c3560..a8f89ec4 100644 --- a/ui/pages/keys/cmds.go +++ b/ui/pages/keys/cmds.go @@ -7,6 +7,8 @@ import ( type DeleteKey *api.ParticipationKey +type DeleteFinished string + func EmitDeleteKey(key *api.ParticipationKey) tea.Cmd { return func() tea.Msg { return DeleteKey(key) diff --git a/ui/pages/keys/controller.go b/ui/pages/keys/controller.go index 76b93899..22e242f6 100644 --- a/ui/pages/keys/controller.go +++ b/ui/pages/keys/controller.go @@ -23,15 +23,41 @@ func (m ViewModel) HandleMessage(msg tea.Msg) (ViewModel, tea.Cmd) { case internal.Account: m.Address = msg.Address m.table.SetRows(m.makeRows(m.Data)) + case DeleteFinished: + if m.SelectedKeyToDelete == nil { + panic("SelectedKeyToDelete is unexpectedly nil") + } + internal.RemovePartKeyByID(m.Data, m.SelectedKeyToDelete.Id) + m.SelectedKeyToDelete = nil + m.table.SetRows(m.makeRows(m.Data)) + case tea.KeyMsg: switch msg.String() { case "enter": - return m, EmitKeySelected(m.SelectedKey()) + selKey := m.SelectedKey() + if selKey != nil { + return m, EmitKeySelected(selKey) + } + return m, nil case "g": // TODO: navigation - case "d": - return m, EmitDeleteKey(m.SelectedKey()) + if m.SelectedKeyToDelete == nil { + m.SelectedKeyToDelete = m.SelectedKey() + } else { + m.SelectedKeyToDelete = nil + } + return m, nil + case "y": // "Yes do delete" option in the delete confirmation modal + if m.SelectedKeyToDelete != nil { + return m, EmitDeleteKey(m.SelectedKeyToDelete) + } + return m, nil + case "n": // "do NOT delete" option in the delete confirmation modal + if m.SelectedKeyToDelete != nil { + m.SelectedKeyToDelete = nil + } + return m, nil case "ctrl+c": return m, tea.Quit } diff --git a/ui/pages/keys/model.go b/ui/pages/keys/model.go index 836ada58..e3879de5 100644 --- a/ui/pages/keys/model.go +++ b/ui/pages/keys/model.go @@ -1,9 +1,10 @@ package keys import ( - "github.com/algorandfoundation/hack-tui/ui/style" "sort" + "github.com/algorandfoundation/hack-tui/ui/style" + "github.com/algorandfoundation/hack-tui/api" "github.com/algorandfoundation/hack-tui/ui/utils" "github.com/charmbracelet/bubbles/table" @@ -16,6 +17,8 @@ type ViewModel struct { Width int Height int + SelectedKeyToDelete *api.ParticipationKey + table table.Model controls string navigation string diff --git a/ui/pages/keys/view.go b/ui/pages/keys/view.go index 595de2dd..8107e710 100644 --- a/ui/pages/keys/view.go +++ b/ui/pages/keys/view.go @@ -1,10 +1,19 @@ package keys import ( + "fmt" + + "github.com/algorandfoundation/hack-tui/api" "github.com/algorandfoundation/hack-tui/ui/style" + "github.com/charmbracelet/lipgloss" ) func (m ViewModel) View() string { + if m.SelectedKeyToDelete != nil { + modal := renderDeleteConfirmationModal(m.SelectedKeyToDelete) + overlay := lipgloss.Place(m.Width, m.Height, lipgloss.Center, lipgloss.Center, modal) + return overlay + } table := style.ApplyBorder(m.Width, m.Height, "8").Render(m.table.View()) return style.WithNavigation( m.navigation, @@ -17,3 +26,16 @@ func (m ViewModel) View() string { ), ) } + +func renderDeleteConfirmationModal(partKey *api.ParticipationKey) string { + modalStyle := lipgloss.NewStyle(). + Width(60). + Height(7). + Align(lipgloss.Center). + Border(lipgloss.RoundedBorder()). + Padding(1, 2) + + modalContent := fmt.Sprintf("Participation Key: %v\nAccount Address: %v\nPress either y (yes) or n (no).", partKey.Id, partKey.Address) + + return modalStyle.Render("Are you sure you want to delete this key from your node?\n" + modalContent) +} diff --git a/ui/viewport.go b/ui/viewport.go index adb885f0..660c73cf 100644 --- a/ui/viewport.go +++ b/ui/viewport.go @@ -48,15 +48,13 @@ type ViewportViewModel struct { errorPage ErrorViewModel } -type DeleteFinished string - func DeleteKey(client *api.ClientWithResponses, key keys.DeleteKey) tea.Cmd { return func() tea.Msg { err := internal.DeletePartKey(context.Background(), client, key.Id) if err != nil { - return DeleteFinished(err.Error()) + return keys.DeleteFinished(err.Error()) } - return DeleteFinished("Key deleted") + return keys.DeleteFinished(key.Id) } } @@ -99,8 +97,6 @@ func (m ViewportViewModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.page = KeysPage case keys.DeleteKey: return m, DeleteKey(m.client, msg) - case DeleteFinished: - // TODO case tea.KeyMsg: switch msg.String() { // Tab Backwards @@ -118,42 +114,53 @@ func (m ViewportViewModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // Tab Forwards case "tab": if m.page == AccountsPage { - m.page = KeysPage - return m, accounts.EmitAccountSelected(m.accountsPage.SelectedAccount()) + selAcc := m.accountsPage.SelectedAccount() + if selAcc != (internal.Account{}) { + m.page = KeysPage + return m, accounts.EmitAccountSelected(selAcc) + } + return m, nil } if m.page == KeysPage { - m.page = TransactionPage - // If there isn't a key already, select the first record - if m.keysPage.SelectedKey() == nil && m.Data != nil { - data := *m.Data.ParticipationKeys - return m, keys.EmitKeySelected(&data[0]) + selKey := m.keysPage.SelectedKey() + if selKey != nil { + m.page = TransactionPage + return m, keys.EmitKeySelected(selKey) } - // Navigate to the transaction page - return m, keys.EmitKeySelected(m.keysPage.SelectedKey()) } + return m, nil + case "a": + m.page = AccountsPage case "g": m.generatePage.Inputs[0].SetValue(m.accountsPage.SelectedAccount().Address) m.page = GeneratePage return m, nil - case "a": - m.page = AccountsPage case "k": - m.page = KeysPage - return m, accounts.EmitAccountSelected(m.accountsPage.SelectedAccount()) + selAcc := m.accountsPage.SelectedAccount() + if selAcc != (internal.Account{}) { + m.page = KeysPage + return m, accounts.EmitAccountSelected(selAcc) + } + return m, nil case "t": - m.page = TransactionPage - // If there isn't a key already, select the first record for that account - if m.keysPage.SelectedKey() == nil && m.Data != nil { - data := *m.Data.ParticipationKeys + if m.page == AccountsPage { acct := m.accountsPage.SelectedAccount() + data := *m.Data.ParticipationKeys for i, key := range data { if key.Address == acct.Address { + m.page = TransactionPage return m, keys.EmitKeySelected(&data[i]) } } } - // Navigate to the transaction page - return m, keys.EmitKeySelected(m.keysPage.SelectedKey()) + if m.page == KeysPage { + selKey := m.keysPage.SelectedKey() + if selKey != nil { + m.page = TransactionPage + return m, keys.EmitKeySelected(selKey) + } + } + return m, nil case "ctrl+c": if m.page != GeneratePage { return m, tea.Quit