diff --git a/src/app.rs b/src/app.rs index bef840e..c39b466 100644 --- a/src/app.rs +++ b/src/app.rs @@ -14,7 +14,7 @@ use crate::{ client::Client, color::ColorTheme, config::Config, - data::{Item, Table, TableDescription, TableInsight}, + data::{to_key_string, Item, Table, TableDescription, TableInsight}, error::{AppError, AppResult}, event::{AppEvent, Receiver, Sender, UserEvent, UserEventMapper}, handle_user_events, @@ -147,6 +147,12 @@ impl App { AppEvent::CopyToClipboard(name, content) => { self.copy_to_clipboard(name, content); } + AppEvent::DeleteItem(desc, item) => { + self.delete_item(desc, item); + } + AppEvent::CompleteDeleteItem(desc, key_string, result) => { + self.complete_delete_item(desc, key_string, result); + } AppEvent::ClearStatus => { self.clear_status(); } @@ -349,6 +355,38 @@ impl App { } } + fn delete_item(&mut self, desc: TableDescription, item: Item) { + self.loading = true; + let client = self.client.clone(); + let tx = self.tx.clone(); + let schema = desc.key_schema_type.clone(); + let table_name = desc.table_name.clone(); + let key_string = to_key_string(&item, &schema); + spawn(async move { + let result = client.delete_item(&table_name, &schema, &item).await; + tx.send(AppEvent::CompleteDeleteItem(desc, key_string, result)); + }); + } + + fn complete_delete_item( + &mut self, + desc: TableDescription, + key_string: String, + result: AppResult<()>, + ) { + match result { + Ok(()) => { + let msg = format!("Deleted item {key_string}"); + self.tx.send(AppEvent::NotifySuccess(msg)); + self.load_table_items(desc); + } + Err(e) => { + self.loading = false; + self.tx.send(AppEvent::NotifyError(e)); + } + } + } + fn clear_status(&mut self) { self.status = Status::None; } diff --git a/src/client.rs b/src/client.rs index c2031dc..5def16b 100644 --- a/src/client.rs +++ b/src/client.rs @@ -14,7 +14,7 @@ use aws_sdk_dynamodb::types::{ ScalarAttributeType as AwsScalarAttributeType, TableDescription as AwsTableDescription, TableStatus as AwsTableStatus, }; -use aws_smithy_types::DateTime as AwsDateTime; +use aws_smithy_types::{Blob, DateTime as AwsDateTime}; use chrono::{DateTime, Local, TimeZone as _}; use rust_decimal::Decimal; @@ -127,6 +127,26 @@ impl Client { sort_items(&mut items, schema); Ok(items) } + + pub async fn delete_item( + &self, + table_name: &str, + schema: &KeySchemaType, + item: &Item, + ) -> AppResult<()> { + let key = build_key_attributes(item, schema); + let result = self + .client + .delete_item() + .table_name(table_name) + .set_key(Some(key)) + .send() + .await; + + result + .map(|_| ()) + .map_err(|e| AppError::new("failed to delete item", e)) + } } impl From for Table { @@ -317,6 +337,61 @@ fn to_item(attributes: HashMap) -> Item { Item { attributes } } +fn build_key_attributes(item: &Item, schema: &KeySchemaType) -> HashMap { + match schema { + KeySchemaType::Hash(hash_key) => { + let mut key = HashMap::with_capacity(1); + let attr = item + .attributes + .get(hash_key) + .expect("missing hash key attribute"); + key.insert(hash_key.clone(), attribute_to_aws(attr)); + key + } + KeySchemaType::HashRange(hash_key, range_key) => { + let mut key = HashMap::with_capacity(2); + let hash_attr = item + .attributes + .get(hash_key) + .expect("missing hash key attribute"); + let range_attr = item + .attributes + .get(range_key) + .expect("missing range key attribute"); + key.insert(hash_key.clone(), attribute_to_aws(hash_attr)); + key.insert(range_key.clone(), attribute_to_aws(range_attr)); + key + } + } +} + +fn attribute_to_aws(attr: &Attribute) -> AwsAttributeValue { + match attr { + Attribute::S(s) => AwsAttributeValue::S(s.clone()), + Attribute::N(n) => AwsAttributeValue::N(n.to_string()), + Attribute::B(b) => AwsAttributeValue::B(Blob::new(b.clone())), + Attribute::BOOL(b) => AwsAttributeValue::Bool(*b), + Attribute::NULL => AwsAttributeValue::Null(true), + Attribute::L(list) => { + let values = list.iter().map(attribute_to_aws).collect(); + AwsAttributeValue::L(values) + } + Attribute::M(map) => { + let values = map + .iter() + .map(|(k, v)| (k.clone(), attribute_to_aws(v))) + .collect(); + AwsAttributeValue::M(values) + } + Attribute::SS(set) => AwsAttributeValue::Ss(set.iter().cloned().collect()), + Attribute::NS(set) => AwsAttributeValue::Ns(set.iter().map(|n| n.to_string()).collect()), + Attribute::BS(set) => { + let values = set.iter().cloned().map(Blob::new).collect(); + AwsAttributeValue::Bs(values) + } + } +} + impl From for Attribute { fn from(value: AwsAttributeValue) -> Self { match value { diff --git a/src/event.rs b/src/event.rs index d81b5df..4cd0896 100644 --- a/src/event.rs +++ b/src/event.rs @@ -22,6 +22,8 @@ pub enum AppEvent { OpenHelp(Vec), BackToBeforeView, CopyToClipboard(String, String), + DeleteItem(TableDescription, Item), + CompleteDeleteItem(TableDescription, String, AppResult<()>), ClearStatus, UpdateStatusInput(String, Option), NotifySuccess(String), @@ -104,6 +106,7 @@ pub enum UserEvent { Narrow, Reload, CopyToClipboard, + Delete, Help, } @@ -146,6 +149,8 @@ impl UserEventMapper { (KeyEvent::new(KeyCode::Char('-'), KeyModifiers::NONE), UserEvent::Narrow), (KeyEvent::new(KeyCode::Char('R'), KeyModifiers::NONE), UserEvent::Reload), (KeyEvent::new(KeyCode::Char('c'), KeyModifiers::NONE), UserEvent::CopyToClipboard), + (KeyEvent::new(KeyCode::Char('d'), KeyModifiers::NONE), UserEvent::Delete), + (KeyEvent::new(KeyCode::Delete, KeyModifiers::NONE), UserEvent::Delete), (KeyEvent::new(KeyCode::Char('?'), KeyModifiers::NONE), UserEvent::Help), ]; UserEventMapper { map } diff --git a/src/view/table.rs b/src/view/table.rs index 9b8dafe..1e7e5f5 100644 --- a/src/view/table.rs +++ b/src/view/table.rs @@ -1,5 +1,5 @@ use ratatui::{ - crossterm::event::KeyEvent, + crossterm::event::{KeyCode, KeyEvent}, layout::{Margin, Rect}, style::Stylize, symbols::border, @@ -13,8 +13,8 @@ use crate::{ config::UiTableConfig, constant::APP_NAME, data::{ - list_attribute_keys, Attribute, Item, KeySchemaType, RawAttributeJsonWrapper, RawJsonItem, - TableDescription, TableInsight, + list_attribute_keys, to_key_string, Attribute, Item, KeySchemaType, + RawAttributeJsonWrapper, RawJsonItem, TableDescription, TableInsight, }, event::{AppEvent, Sender, UserEvent, UserEventMapper}, handle_user_events, @@ -45,6 +45,11 @@ pub struct TableView { table_state: TableState, attr_expanded: bool, attr_scroll_lines_state: ScrollLinesState, + pending_delete: Option, +} + +struct PendingDelete { + row_index: usize, } impl TableView { @@ -80,12 +85,18 @@ impl TableView { table_state, attr_expanded: false, attr_scroll_lines_state, + pending_delete: None, } } } impl TableView { - pub fn handle_user_key_event(&mut self, user_events: Vec, _key_event: KeyEvent) { + pub fn handle_user_key_event(&mut self, user_events: Vec, key_event: KeyEvent) { + if self.pending_delete.is_some() && self.handle_delete_confirmation(&user_events, key_event) + { + return; + } + if self.attr_expanded { handle_user_events! { user_events => UserEvent::Close | UserEvent::Expand => { @@ -127,6 +138,9 @@ impl TableView { UserEvent::CopyToClipboard => { self.copy_to_clipboard(); } + UserEvent::Delete => { + self.start_delete_confirmation(); + } UserEvent::Help => { self.open_help(); } @@ -199,6 +213,9 @@ impl TableView { UserEvent::CopyToClipboard => { self.copy_to_clipboard(); } + UserEvent::Delete => { + self.start_delete_confirmation(); + } UserEvent::Help => { self.open_help(); } @@ -256,6 +273,7 @@ fn build_helps(mapper: &UserEventMapper, theme: ColorTheme) -> (Vec, Vec< BuildHelpsItem::new(UserEvent::Narrow, "Narrow selected column"), BuildHelpsItem::new(UserEvent::Reload, "Reload table data"), BuildHelpsItem::new(UserEvent::CopyToClipboard, "Copy selected item"), + BuildHelpsItem::new(UserEvent::Delete, "Delete selected item"), ]; #[rustfmt::skip] let attr_helps = vec![ @@ -273,6 +291,7 @@ fn build_helps(mapper: &UserEventMapper, theme: ColorTheme) -> (Vec, Vec< BuildHelpsItem::new(UserEvent::ToggleNumber, "Toggle number"), BuildHelpsItem::new(UserEvent::Reload, "Reload table data"), BuildHelpsItem::new(UserEvent::CopyToClipboard, "Copy selected item"), + BuildHelpsItem::new(UserEvent::Delete, "Delete selected item"), ]; ( build_help_spans(table_helps, mapper, theme), @@ -294,6 +313,7 @@ fn build_short_helps(mapper: &UserEventMapper) -> (Vec, Vec (Vec, Vec bool { + if self.pending_delete.is_none() { + return false; + } + + // if user_events.iter().any(|e| *e == UserEvent::Confirm) { + // self.confirm_delete(); + // return true; + // } + + if user_events.iter().any(|e| *e == UserEvent::Close) { + self.cancel_delete(); + return true; + } + + match key_event.code { + KeyCode::Char('y') | KeyCode::Char('Y') => { + self.confirm_delete(); + true + } + KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => { + self.cancel_delete(); + true + } + _ => true, + } + } } fn new_table_state(