From ae10d81f4923e7abdff7d385fbae038e5f114088 Mon Sep 17 00:00:00 2001 From: Scott Donnelly Date: Wed, 22 Jan 2025 08:07:20 +0000 Subject: [PATCH 1/7] feat: introduce component trait and make treemap a layout component --- tanic-tui/component.rs | 17 ++++ tanic-tui/lib.rs | 1 + tanic-tui/ui_components/app_container.rs | 9 +- tanic-tui/ui_components/mod.rs | 5 +- .../ui_components/namespace_list_item.rs | 55 +++++++++++ .../ui_components/namespace_list_view.rs | 90 +++++------------- tanic-tui/ui_components/table_list_item.rs | 55 +++++++++++ tanic-tui/ui_components/table_list_view.rs | 93 ++++++------------- tanic-tui/ui_components/treemap_layout.rs | 50 ++++++++++ 9 files changed, 237 insertions(+), 138 deletions(-) create mode 100644 tanic-tui/component.rs create mode 100644 tanic-tui/ui_components/namespace_list_item.rs create mode 100644 tanic-tui/ui_components/table_list_item.rs create mode 100644 tanic-tui/ui_components/treemap_layout.rs diff --git a/tanic-tui/component.rs b/tanic-tui/component.rs new file mode 100644 index 0000000..72077ec --- /dev/null +++ b/tanic-tui/component.rs @@ -0,0 +1,17 @@ +use ratatui::crossterm::event::{KeyEvent, MouseEvent}; +use ratatui::prelude::*; + +use tanic_svc::TanicAction; + +pub trait Component { + fn handle_key_event(&mut self, _key: KeyEvent) -> Option { + None + } + + #[allow(dead_code)] // not using any mouse events yet + fn handle_mouse_event(&mut self, _mouse: MouseEvent) -> Option { + None + } + + fn render(&self, area: Rect, buf: &mut Buffer); +} diff --git a/tanic-tui/lib.rs b/tanic-tui/lib.rs index 94bf98e..010a112 100644 --- a/tanic-tui/lib.rs +++ b/tanic-tui/lib.rs @@ -8,6 +8,7 @@ use crate::ui_components::app_container::AppContainer; use tanic_core::{Result, TanicError}; use tanic_svc::{TanicAction, TanicAppState}; +mod component; mod ui_components; pub struct TanicTui { diff --git a/tanic-tui/ui_components/app_container.rs b/tanic-tui/ui_components/app_container.rs index 52d985a..cf78eb7 100644 --- a/tanic-tui/ui_components/app_container.rs +++ b/tanic-tui/ui_components/app_container.rs @@ -1,3 +1,4 @@ +use crate::component::Component; use crate::ui_components::{ namespace_list_view::NamespaceListView, splash_screen::SplashScreen, table_list_view::TableListView, @@ -39,10 +40,10 @@ impl<'a> AppContainer<'a> { } key_event => match &self.state { TanicAppState::ViewingNamespacesList(_) => { - self.namespace_list_view.handle_key_event(key_event) + (&self.namespace_list_view).handle_key_event(key_event) } TanicAppState::ViewingTablesList(_) => { - self.table_list_view.handle_key_event(key_event) + (&self.table_list_view).handle_key_event(key_event) } _ => None, }, @@ -72,8 +73,8 @@ impl Widget for &AppContainer<'_> { match &self.state { TanicAppState::Initializing => self.splash_screen.render(top, buf), - TanicAppState::ViewingNamespacesList(_) => self.namespace_list_view.render(top, buf), - TanicAppState::ViewingTablesList(_) => self.table_list_view.render(top, buf), + TanicAppState::ViewingNamespacesList(_) => (&self.namespace_list_view).render(top, buf), + TanicAppState::ViewingTablesList(_) => (&self.table_list_view).render(top, buf), TanicAppState::Exiting => {} _ => {} } diff --git a/tanic-tui/ui_components/mod.rs b/tanic-tui/ui_components/mod.rs index c09dbe1..f577c19 100644 --- a/tanic-tui/ui_components/mod.rs +++ b/tanic-tui/ui_components/mod.rs @@ -1,4 +1,7 @@ pub(crate) mod app_container; +pub(crate) mod namespace_list_item; pub(crate) mod namespace_list_view; -mod splash_screen; +pub mod splash_screen; +pub(crate) mod table_list_item; pub(crate) mod table_list_view; +pub mod treemap_layout; diff --git a/tanic-tui/ui_components/namespace_list_item.rs b/tanic-tui/ui_components/namespace_list_item.rs new file mode 100644 index 0000000..d2ede14 --- /dev/null +++ b/tanic-tui/ui_components/namespace_list_item.rs @@ -0,0 +1,55 @@ +use crate::component::Component; +use ratatui::prelude::*; +use ratatui::symbols::border; +use ratatui::widgets::{Block, Paragraph}; +use tanic_core::message::NamespaceDeets; + +const NERD_FONT_ICON_TABLE_FOLDER: &str = "\u{f12e4}"; // 󱋤 + +pub(crate) struct NamespaceListItem<'a> { + pub(crate) ns: &'a NamespaceDeets, + pub(crate) is_selected: bool, +} + +impl<'a> NamespaceListItem<'a> { + pub(crate) fn new(ns: &'a NamespaceDeets, is_selected: bool) -> Self { + Self { ns, is_selected } + } +} + +impl Component for &NamespaceListItem<'_> { + fn render(&self, area: Rect, buf: &mut Buffer) { + let mut block = Block::new().border_set(border::THICK); + let block_inner = block.inner(area); + + if self.is_selected { + block = block.style(Style::new().bg(Color::Cyan)); + } + + let name = self.ns.name.clone(); + let plural_suffix = if self.ns.table_count == 1 { "" } else { "s" }; + let name = format!( + "{} {} ({} table{})", + NERD_FONT_ICON_TABLE_FOLDER, name, self.ns.table_count, plural_suffix + ); + + let para_rect = Rect::new( + block_inner.x, + block_inner.y + (block_inner.height / 2), + block_inner.width, + 1, + ); + + let mut para = Paragraph::new(name) + .alignment(Alignment::Center) + .white() + .bold(); + + if self.is_selected { + para = para.black(); + } + + block.render(area, buf); + para.render(para_rect, buf); + } +} diff --git a/tanic-tui/ui_components/namespace_list_view.rs b/tanic-tui/ui_components/namespace_list_view.rs index eadb574..0daad18 100644 --- a/tanic-tui/ui_components/namespace_list_view.rs +++ b/tanic-tui/ui_components/namespace_list_view.rs @@ -1,14 +1,13 @@ use crossterm::event::{KeyCode, KeyEvent}; use ratatui::prelude::*; -use ratatui::widgets::canvas::{Canvas, Rectangle}; +use ratatui::symbols::border; use ratatui::widgets::Block; -use treemap::{MapItem, Mappable, Rect as TreeMapRect, TreemapLayout}; +use crate::component::Component; +use crate::ui_components::namespace_list_item::NamespaceListItem; +use crate::ui_components::treemap_layout::TreeMapLayout; use tanic_svc::{TanicAction, TanicAppState}; -// find more at https://www.nerdfonts.com/cheat-sheet -const NERD_FONT_ICON_TABLE_FOLDER: &str = "\u{f12e4}"; // 󱋤 - pub(crate) struct NamespaceListView<'a> { state: &'a TanicAppState, } @@ -17,8 +16,10 @@ impl<'a> NamespaceListView<'a> { pub(crate) fn new(state: &'a TanicAppState) -> Self { Self { state } } +} - pub(crate) fn handle_key_event(&self, key_event: KeyEvent) -> Option { +impl Component for &NamespaceListView<'_> { + fn handle_key_event(&mut self, key_event: KeyEvent) -> Option { match key_event.code { KeyCode::Left => Some(TanicAction::FocusPrevNamespace), KeyCode::Right => Some(TanicAction::FocusNextNamespace), @@ -26,79 +27,34 @@ impl<'a> NamespaceListView<'a> { _ => None, } } -} -impl Widget for &NamespaceListView<'_> { - fn render(self, area: Rect, buf: &mut Buffer) { - let layout = TreemapLayout::new(); - let bounds = TreeMapRect::from_points( - area.x as f64, - area.y as f64, - area.width as f64, - area.height as f64, - ); + fn render(&self, area: Rect, buf: &mut Buffer) { + let block = Block::bordered() + .title(" Tanic //// Root Namespaces") + .border_set(border::PLAIN); + let block_inner_area = block.inner(area); let TanicAppState::ViewingNamespacesList(view_state) = self.state else { panic!(); }; - let mut items: Vec> = view_state + let items = view_state .namespaces .iter() - .map(|namespace| { - let res: Box = - Box::new(MapItem::with_size(namespace.table_count.max(1) as f64)); - res + .enumerate() + .map(|(idx, ns)| { + NamespaceListItem::new(ns, view_state.selected_idx.unwrap_or(usize::MAX) == idx) }) .collect::>(); - layout.layout_items(&mut items, bounds); - - let selected_idx = view_state.selected_idx; - - let canvas = Canvas::default() - .block(Block::bordered().title(" Tanic //// Root Namespaces")) - .x_bounds([area.x as f64, (area.x + area.width) as f64]) - .y_bounds([area.y as f64, (area.y + area.height) as f64]) - .paint(|ctx| { - for (idx, item) in items.iter().enumerate() { - let item_bounds = item.bounds(); - - let rect = Rectangle { - x: item_bounds.x, - y: item_bounds.y, - width: item_bounds.w, - height: item_bounds.h, - color: Color::White, - }; - - ctx.draw(&rect); - - let style = if Some(idx) == selected_idx { - Style::new().black().bold().on_white() - } else { - Style::new().white() - }; - - let ns = &view_state.namespaces[idx]; - let name = ns.name.clone(); - let plural_suffix = if ns.table_count == 1 { "" } else { "s" }; - let name = format!( - "{} {} ({} table{})", - NERD_FONT_ICON_TABLE_FOLDER, name, ns.table_count, plural_suffix - ); - - let name_len = name.len(); - let text = Line::styled(name, style); + let children: Vec<(&NamespaceListItem, usize)> = items + .iter() + .map(|item| (item, item.ns.table_count)) + .collect::>(); - ctx.print( - item_bounds.x + (item_bounds.w * 0.5) - (name_len as f64 * 0.5), - item_bounds.y + (item_bounds.h * 0.5), - text, - ); - } - }); + let layout = TreeMapLayout::new(children); - canvas.render(area, buf); + block.render(area, buf); + (&layout).render(block_inner_area, buf); } } diff --git a/tanic-tui/ui_components/table_list_item.rs b/tanic-tui/ui_components/table_list_item.rs new file mode 100644 index 0000000..897fa0f --- /dev/null +++ b/tanic-tui/ui_components/table_list_item.rs @@ -0,0 +1,55 @@ +use crate::component::Component; +use ratatui::prelude::*; +use ratatui::symbols::border; +use ratatui::widgets::{Block, Paragraph}; +use tanic_core::message::TableDeets; + +const NERD_FONT_ICON_TABLE: &str = "\u{ebb7}"; //  + +pub(crate) struct TableListItem<'a> { + pub(crate) table: &'a TableDeets, + pub(crate) is_selected: bool, +} + +impl<'a> TableListItem<'a> { + pub(crate) fn new(table: &'a TableDeets, is_selected: bool) -> Self { + Self { table, is_selected } + } +} + +impl Component for &TableListItem<'_> { + fn render(&self, area: Rect, buf: &mut Buffer) { + let mut block = Block::new().border_set(border::THICK); + let block_inner = block.inner(area); + + if self.is_selected { + block = block.style(Style::new().bg(Color::Cyan)); + } + + let name = self.table.name.clone(); + let plural_suffix = if self.table.row_count == 1 { "" } else { "s" }; + let name = format!( + "{} {} ({} row{})", + NERD_FONT_ICON_TABLE, name, self.table.row_count, plural_suffix + ); + + let para_rect = Rect::new( + block_inner.x, + block_inner.y + (block_inner.height / 2), + block_inner.width, + 1, + ); + + let mut para = Paragraph::new(name) + .alignment(Alignment::Center) + .white() + .bold(); + + if self.is_selected { + para = para.black(); + } + + block.render(area, buf); + para.render(para_rect, buf); + } +} diff --git a/tanic-tui/ui_components/table_list_view.rs b/tanic-tui/ui_components/table_list_view.rs index 88fd36e..33e7b85 100644 --- a/tanic-tui/ui_components/table_list_view.rs +++ b/tanic-tui/ui_components/table_list_view.rs @@ -1,14 +1,13 @@ use crossterm::event::{KeyCode, KeyEvent}; use ratatui::prelude::*; -use ratatui::widgets::canvas::{Canvas, Rectangle}; +use ratatui::symbols::border; use ratatui::widgets::Block; -use treemap::{MapItem, Mappable, Rect as TreeMapRect, TreemapLayout}; +use crate::component::Component; +use crate::ui_components::table_list_item::TableListItem; +use crate::ui_components::treemap_layout::TreeMapLayout; use tanic_svc::{TanicAction, TanicAppState}; -// find more at https://www.nerdfonts.com/cheat-sheet -const NERD_FONT_ICON_TABLE: &str = "\u{ebb7}"; //  - pub(crate) struct TableListView<'a> { state: &'a TanicAppState, } @@ -17,8 +16,10 @@ impl<'a> TableListView<'a> { pub(crate) fn new(state: &'a TanicAppState) -> Self { Self { state } } +} - pub(crate) fn handle_key_event(&self, key_event: KeyEvent) -> Option { +impl Component for &TableListView<'_> { + fn handle_key_event(&mut self, key_event: KeyEvent) -> Option { match key_event.code { KeyCode::Left => Some(TanicAction::FocusPrevTable), KeyCode::Right => Some(TanicAction::FocusNextTable), @@ -27,77 +28,37 @@ impl<'a> TableListView<'a> { _ => None, } } -} - -impl Widget for &TableListView<'_> { - fn render(self, area: Rect, buf: &mut Buffer) { - let layout = TreemapLayout::new(); - let bounds = TreeMapRect::from_points( - area.x as f64, - area.y as f64, - area.width as f64, - area.height as f64, - ); + fn render(&self, area: Rect, buf: &mut Buffer) { let TanicAppState::ViewingTablesList(view_state) = self.state else { panic!(); }; - let mut items: Vec> = view_state + let block = Block::bordered() + .title(format!( + " Tanic //// {} Namespace ", + view_state.namespace.name + )) + .border_set(border::PLAIN); + let block_inner_area = block.inner(area); + + let items = view_state .tables .iter() - .map(|table| { - let res: Box = - Box::new(MapItem::with_size(table.row_count.max(1) as f64)); - res + .enumerate() + .map(|(idx, ns)| { + TableListItem::new(ns, view_state.selected_idx.unwrap_or(usize::MAX) == idx) }) .collect::>(); - layout.layout_items(&mut items, bounds); - - let selected_idx = view_state.selected_idx; - - let canvas = Canvas::default() - .block(Block::bordered().title(format!( - " Tanic //// {} Namespace ", - view_state.namespace.name - ))) - .x_bounds([area.x as f64, (area.x + area.width) as f64]) - .y_bounds([area.y as f64, (area.y + area.height) as f64]) - .paint(|ctx| { - for (idx, item) in items.iter().enumerate() { - let item_bounds = item.bounds(); - - let rect = Rectangle { - x: item_bounds.x, - y: item_bounds.y, - width: item_bounds.w, - height: item_bounds.h, - color: Color::White, - }; - - ctx.draw(&rect); - - let style = if Some(idx) == selected_idx { - Style::new().black().bold().on_white() - } else { - Style::new().white() - }; - - let name = view_state.tables[idx].name.clone(); - let name = format!("{} {}", NERD_FONT_ICON_TABLE, name); - - let name_len = name.len(); - let text = Line::styled(name, style); + let children: Vec<(&TableListItem, usize)> = items + .iter() + .map(|item| (item, item.table.row_count)) + .collect::>(); - ctx.print( - item_bounds.x + (item_bounds.w * 0.5) - (name_len as f64 * 0.5), - item_bounds.y + (item_bounds.h * 0.5), - text, - ); - } - }); + let layout = TreeMapLayout::new(children); - canvas.render(area, buf); + block.render(area, buf); + (&layout).render(block_inner_area, buf); } } diff --git a/tanic-tui/ui_components/treemap_layout.rs b/tanic-tui/ui_components/treemap_layout.rs new file mode 100644 index 0000000..d7dea73 --- /dev/null +++ b/tanic-tui/ui_components/treemap_layout.rs @@ -0,0 +1,50 @@ +use ratatui::prelude::*; +use treemap::{MapItem, Mappable, Rect as TreeMapRect, TreemapLayout}; + +use crate::component::Component; + +pub(crate) struct TreeMapLayout { + children: Vec<(T, usize)>, +} + +impl TreeMapLayout { + pub(crate) fn new(children: Vec<(T, usize)>) -> Self { + Self { children } + } +} + +impl Component for &TreeMapLayout { + fn render(&self, area: Rect, buf: &mut Buffer) { + let layout = TreemapLayout::new(); + let bounds = TreeMapRect::from_points( + area.x as f64, + area.y as f64, + area.width as f64, + area.height as f64, + ); + + let mut regions: Vec> = self + .children + .iter() + .map(|&(_, size)| { + let res: Box = Box::new(MapItem::with_size(size.max(1) as f64)); + res + }) + .collect::>(); + + layout.layout_items(&mut regions, bounds); + + for ((child, _), region) in self.children.iter().zip(regions.iter()) { + let region_bounds = region.bounds(); + + let rect = Rect { + x: region_bounds.x as u16, + y: region_bounds.y as u16, + width: region_bounds.w as u16, + height: region_bounds.h as u16, + }; + + child.render(rect, buf); + } + } +} From 9f12746981031b9fb183bedc05fdcb1712328940 Mon Sep 17 00:00:00 2001 From: Scott Donnelly Date: Thu, 23 Jan 2025 07:57:56 +0000 Subject: [PATCH 2/7] feat(wip): refactor context so that multiple tasks can run and send actions --- tanic-core/config.rs | 6 + tanic-core/error.rs | 6 + tanic-core/message.rs | 11 ++ tanic-svc/src/iceberg_context.rs | 223 +++++++++++++++++-------------- 4 files changed, 145 insertions(+), 101 deletions(-) diff --git a/tanic-core/config.rs b/tanic-core/config.rs index 4050634..2d42cbf 100644 --- a/tanic-core/config.rs +++ b/tanic-core/config.rs @@ -37,6 +37,12 @@ impl ConnectionDetails { } } +impl PartialEq for ConnectionDetails { + fn eq(&self, other: &Self) -> bool { + self.uri == other.uri + } +} + /// persistable user config. /// /// Loaded in at application startup from $CONFIG/tanic/tanic.toml diff --git a/tanic-core/error.rs b/tanic-core/error.rs index 755088e..c6dfe11 100644 --- a/tanic-core/error.rs +++ b/tanic-core/error.rs @@ -27,3 +27,9 @@ pub enum TanicError { #[error("Unexpected")] UnexpectedError(String), } + +impl TanicError { + pub fn unexpected(msg: T) -> Self { + Self::UnexpectedError(msg.to_string()) + } +} diff --git a/tanic-core/message.rs b/tanic-core/message.rs index e96e7f3..621122b 100644 --- a/tanic-core/message.rs +++ b/tanic-core/message.rs @@ -11,3 +11,14 @@ pub struct TableDeets { pub name: String, pub row_count: usize, } + +impl NamespaceDeets { + pub fn from_parts(parts: Vec) -> Self { + let name = parts.clone().join("."); + Self { + parts, + name, + table_count: 0, + } + } +} diff --git a/tanic-svc/src/iceberg_context.rs b/tanic-svc/src/iceberg_context.rs index 6e0f064..00c9e9f 100644 --- a/tanic-svc/src/iceberg_context.rs +++ b/tanic-svc/src/iceberg_context.rs @@ -2,9 +2,12 @@ use iceberg::{Catalog, NamespaceIdent}; use iceberg_catalog_rest::{RestCatalog, RestCatalogConfig}; +use std::ops::DerefMut; use std::sync::Arc; use tokio::sync::mpsc::UnboundedSender; use tokio::sync::watch::Receiver; +use tokio::sync::RwLock; +use tokio::task::JoinHandle; use tokio_stream::{wrappers::WatchStream, StreamExt}; use tanic_core::config::ConnectionDetails; @@ -13,80 +16,46 @@ use tanic_core::{Result, TanicError}; use crate::state::{TanicAction, TanicAppState, ViewingNamespacesListState}; -#[derive(Debug)] -enum Connection { - Disconnected, - Connected(IcebergContext), -} +type ActionTx = UnboundedSender; +type IceCtxRef = Arc>; -#[derive(Debug)] +#[derive(Debug, Default)] struct IcebergContext { - connection_details: ConnectionDetails, + connection_details: Option, /// Iceberg Catalog catalog: Option>, namespaces: Vec, tables: Vec, + + pub cancellable_action: Option>, } /// Iceberg Context #[derive(Debug)] pub struct IcebergContextManager { - action_tx: UnboundedSender, + action_tx: ActionTx, + iceberg_context: IceCtxRef, } impl IcebergContextManager { - pub fn new(action_tx: UnboundedSender) -> Self { - Self { action_tx } + pub fn new(action_tx: ActionTx) -> Self { + Self { + action_tx, + iceberg_context: Arc::new(RwLock::new(IcebergContext::default())), + } } - pub async fn event_loop(self, state_rx: Receiver) -> Result<()> { - let mut connection = Connection::Disconnected; - + pub async fn event_loop(&self, state_rx: Receiver) -> Result<()> { let mut state_stream = WatchStream::new(state_rx); while let Some(state) = state_stream.next().await { match state { TanicAppState::ConnectingTo(ref new_conn_details) => { - match &mut connection { - // initial connection - Connection::Disconnected => { - let mut context = IcebergContext::connect_to(new_conn_details); - - context.populate_namespaces().await?; - - self.action_tx - .send(TanicAction::RetrievedNamespaceList( - context.namespaces.clone(), - )) - .map_err(|err| TanicError::UnexpectedError(err.to_string()))?; - - connection = Connection::Connected(context); - } - - // already existing connection? No Op - Connection::Connected(IcebergContext { - connection_details, .. - }) if connection_details.uri == new_conn_details.uri => {} - - // switch connection - Connection::Connected(_) => { - let mut context = IcebergContext::connect_to(new_conn_details); - - context.populate_namespaces().await?; - - self.action_tx - .send(TanicAction::RetrievedNamespaceList( - context.namespaces.clone(), - )) - .map_err(|err| TanicError::UnexpectedError(err.to_string()))?; - - connection = Connection::Connected(context); - } - } + self.connect_to(new_conn_details).await?; } - TanicAppState::ViewingNamespacesList(_) => {} + TanicAppState::RetrievingTableList(ViewingNamespacesListState { namespaces, selected_idx, @@ -94,93 +63,145 @@ impl IcebergContextManager { let Some(selected_idx) = selected_idx else { continue; }; - let namespace = &namespaces[selected_idx]; - if let Connection::Connected(ref mut iceberg_ctx) = &mut connection { - iceberg_ctx.populate_table_list(&namespace.parts).await?; - - self.action_tx - .send(TanicAction::RetrievedTableList( - namespace.clone(), - iceberg_ctx.tables.clone(), - )) - .map_err(|err| TanicError::UnexpectedError(err.to_string()))?; - } + let namespace = namespaces[selected_idx].parts.clone(); + + // spawn a task to start populating the namespaces + let action_tx = self.action_tx.clone(); + let ctx = self.iceberg_context.clone(); + + // TODO: handle handle, lol + let _jh = tokio::spawn(async move { + Self::populate_tables(ctx, action_tx, namespace).await + }); } + TanicAppState::Exiting => { break; } + _ => {} } } Ok(()) } -} -impl IcebergContext { - /// Create a new Iceberg Context from a Uri - pub fn connect_to(connection_details: &ConnectionDetails) -> Self { - let connection_details = connection_details.clone(); + async fn connect_to(&self, new_conn_details: &ConnectionDetails) -> Result<()> { + { + let ctx = self.iceberg_context.read().await; + if let Some(ref existing_conn_details) = ctx.connection_details { + if new_conn_details == existing_conn_details { + // do nothing, already connected to this catalog + return Ok(()); + } + } + } - let mut uri_str = connection_details.uri.to_string(); - uri_str.pop(); + // cancel any in-progress action and connect to the new connection + { + let mut ctx = self.iceberg_context.write().await; + // TODO: cancel in-prog action + // if let Some(cancellable) = *ctx.deref_mut().cancellable_action { + // cancellable.abort(); + // } + ctx.connect_to(new_conn_details); + } - let config = RestCatalogConfig::builder().uri(uri_str).build(); - let rest_catalog = RestCatalog::new(config); + // spawn a task to start populating the namespaces + let action_tx = self.action_tx.clone(); + let ctx = self.iceberg_context.clone(); + let jh = tokio::spawn(async move { + Self::populate_namespaces(ctx.clone(), action_tx.clone()).await; + }); - Self { - connection_details, - namespaces: vec![], - tables: vec![], - catalog: Some(Arc::new(rest_catalog)), - } + Ok(()) } - pub async fn populate_namespaces(&mut self) -> Result<()> { - let Some(ref catalog) = self.catalog else { - panic!(); - }; + async fn populate_namespaces(ctx: IceCtxRef, action_tx: ActionTx) -> Result<()> { + let root_namespaces = { + let r_ctx = ctx.read().await; - let root_namespaces = catalog.list_namespaces(None).await?; + let Some(ref catalog) = r_ctx.catalog else { + return Err(TanicError::unexpected( + "Attempted to populate namespaces when catalog not initialised", + )); + }; + + catalog.list_namespaces(None).await? + }; let namespaces = root_namespaces .into_iter() - .map(|ns| { - let parts = ns.inner(); - let name = parts.clone().join("."); - NamespaceDeets { - parts, - name, - table_count: 0, - } - }) + .map(|ns| NamespaceDeets::from_parts(ns.inner())) .collect::>(); - self.namespaces = namespaces; + { + let namespaces = namespaces.clone(); + ctx.write().await.namespaces = namespaces; + } + + action_tx + .send(TanicAction::RetrievedNamespaceList(namespaces)) + .map_err(|err| TanicError::UnexpectedError(err.to_string()))?; Ok(()) } - pub async fn populate_table_list(&mut self, namespace_parts: &Vec) -> Result<()> { - let Some(ref catalog) = self.catalog else { - panic!(); + async fn populate_tables( + ctx: IceCtxRef, + action_tx: ActionTx, + namespace: Vec, + ) -> Result<()> { + let namespace_ident = NamespaceIdent::from_strs(namespace.clone())?; + let tables = { + let r_ctx = ctx.read().await; + + let Some(ref catalog) = r_ctx.catalog else { + return Err(TanicError::unexpected( + "Attempted to populate namespaces when catalog not initialised", + )); + }; + + catalog.list_tables(&namespace_ident).await? }; - let tables = catalog - .list_tables(&NamespaceIdent::from_strs(namespace_parts)?) - .await?; - - let table_names = tables + let tables = tables .into_iter() .map(|ti| TableDeets { - namespace: namespace_parts.clone(), + namespace: namespace.clone(), name: ti.name().to_string(), row_count: 1, }) .collect::>(); - self.tables = table_names; + { + let tables = tables.clone(); + ctx.write().await.tables = tables; + } + + action_tx + .send(TanicAction::RetrievedTableList( + NamespaceDeets::from_parts(namespace), + tables, + )) + .map_err(TanicError::unexpected)?; Ok(()) } } + +impl IcebergContext { + /// Create a new Iceberg Context from a Uri + pub fn connect_to(&mut self, connection_details: &ConnectionDetails) -> () { + self.connection_details = Some(connection_details.clone()); + + let mut uri_str = connection_details.uri.to_string(); + uri_str.pop(); + + let config = RestCatalogConfig::builder().uri(uri_str).build(); + self.catalog = Some(Arc::new(RestCatalog::new(config))); + + self.namespaces = vec![]; + self.tables = vec![]; + } +} From 801273409846e68eb198eb15b006b6869c316c71 Mon Sep 17 00:00:00 2001 From: Scott Donnelly Date: Thu, 23 Jan 2025 19:08:55 +0000 Subject: [PATCH 3/7] chore: clean up lint errors --- tanic-svc/src/iceberg_context.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tanic-svc/src/iceberg_context.rs b/tanic-svc/src/iceberg_context.rs index 00c9e9f..82b83f9 100644 --- a/tanic-svc/src/iceberg_context.rs +++ b/tanic-svc/src/iceberg_context.rs @@ -2,7 +2,6 @@ use iceberg::{Catalog, NamespaceIdent}; use iceberg_catalog_rest::{RestCatalog, RestCatalogConfig}; -use std::ops::DerefMut; use std::sync::Arc; use tokio::sync::mpsc::UnboundedSender; use tokio::sync::watch::Receiver; @@ -29,6 +28,7 @@ struct IcebergContext { namespaces: Vec, tables: Vec, + #[allow(unused)] // TODO: cancellation pub cancellable_action: Option>, } @@ -110,8 +110,12 @@ impl IcebergContextManager { // spawn a task to start populating the namespaces let action_tx = self.action_tx.clone(); let ctx = self.iceberg_context.clone(); - let jh = tokio::spawn(async move { - Self::populate_namespaces(ctx.clone(), action_tx.clone()).await; + // TODO: store the join handle for cancellation + let _jh = tokio::spawn(async move { + let res = Self::populate_namespaces(ctx.clone(), action_tx.clone()).await; + if let Err(error) = res { + tracing::error!(%error, "Error populating namespaces"); + } }); Ok(()) @@ -192,7 +196,7 @@ impl IcebergContextManager { impl IcebergContext { /// Create a new Iceberg Context from a Uri - pub fn connect_to(&mut self, connection_details: &ConnectionDetails) -> () { + pub fn connect_to(&mut self, connection_details: &ConnectionDetails) { self.connection_details = Some(connection_details.clone()); let mut uri_str = connection_details.uri.to_string(); From a583239ede8ede8c6c09df4ab126124b77bf95e9 Mon Sep 17 00:00:00 2001 From: Scott Donnelly Date: Thu, 13 Feb 2025 09:21:54 +0000 Subject: [PATCH 4/7] refactor: everything --- tanic-svc/Cargo.toml | 2 + tanic-svc/src/iceberg_context.rs | 108 ++-- tanic-svc/src/lib.rs | 29 +- tanic-svc/src/state.rs | 563 ++++++++++++++---- tanic-tui/lib.rs | 27 +- tanic-tui/ui_components/app_container.rs | 57 +- .../ui_components/namespace_list_item.rs | 12 +- .../ui_components/namespace_list_view.rs | 58 +- tanic-tui/ui_components/splash_screen.rs | 11 +- tanic-tui/ui_components/table_list_item.rs | 18 +- tanic-tui/ui_components/table_list_view.rs | 63 +- tanic/src/main.rs | 22 +- 12 files changed, 709 insertions(+), 261 deletions(-) diff --git a/tanic-svc/Cargo.toml b/tanic-svc/Cargo.toml index d020a2c..421ad75 100644 --- a/tanic-svc/Cargo.toml +++ b/tanic-svc/Cargo.toml @@ -31,3 +31,5 @@ tracing = { workspace = true } uuid = { version = "1.12.0", features = ["v4"] } names = "0.14.0" tokio-stream = { version = "0.1.17", features = ["sync"] } +parquet = "54.0.0" +indexmap = "2.7.1" diff --git a/tanic-svc/src/iceberg_context.rs b/tanic-svc/src/iceberg_context.rs index 82b83f9..e657d7f 100644 --- a/tanic-svc/src/iceberg_context.rs +++ b/tanic-svc/src/iceberg_context.rs @@ -5,7 +5,7 @@ use iceberg_catalog_rest::{RestCatalog, RestCatalogConfig}; use std::sync::Arc; use tokio::sync::mpsc::UnboundedSender; use tokio::sync::watch::Receiver; -use tokio::sync::RwLock; +use std::sync::RwLock; use tokio::task::JoinHandle; use tokio_stream::{wrappers::WatchStream, StreamExt}; @@ -13,7 +13,7 @@ use tanic_core::config::ConnectionDetails; use tanic_core::message::{NamespaceDeets, TableDeets}; use tanic_core::{Result, TanicError}; -use crate::state::{TanicAction, TanicAppState, ViewingNamespacesListState}; +use crate::state::{TanicAction, TanicAppState, TanicIcebergState}; type ActionTx = UnboundedSender; type IceCtxRef = Arc>; @@ -37,33 +37,46 @@ struct IcebergContext { pub struct IcebergContextManager { action_tx: ActionTx, iceberg_context: IceCtxRef, + state_ref: Arc>, } impl IcebergContextManager { - pub fn new(action_tx: ActionTx) -> Self { + pub fn new(action_tx: ActionTx, state_ref: Arc>) -> Self { Self { action_tx, + state_ref, iceberg_context: Arc::new(RwLock::new(IcebergContext::default())), } } - pub async fn event_loop(&self, state_rx: Receiver) -> Result<()> { + pub async fn event_loop(&self, state_rx: Receiver<()>) -> Result<()> { let mut state_stream = WatchStream::new(state_rx); - while let Some(state) = state_stream.next().await { - match state { - TanicAppState::ConnectingTo(ref new_conn_details) => { - self.connect_to(new_conn_details).await?; + while state_stream.next().await.is_some() { + + let new_conn_details = { + let state = self.state_ref.read().unwrap(); + + match &state.iceberg { + TanicIcebergState::ConnectingTo(ref new_conn_details) => { + Some(new_conn_details.clone()) + } + TanicIcebergState::Exiting => { + break; + } + _ => None } + }; - TanicAppState::RetrievingTableList(ViewingNamespacesListState { - namespaces, - selected_idx, - }) => { - let Some(selected_idx) = selected_idx else { - continue; - }; - let namespace = namespaces[selected_idx].parts.clone(); + if let Some(new_conn_details) = new_conn_details { + self.connect_to(&new_conn_details).await?; + + let namespaces = { + self.iceberg_context.read().unwrap().namespaces.clone() + }; + + // begin crawl + for namespace in namespaces { // spawn a task to start populating the namespaces let action_tx = self.action_tx.clone(); @@ -74,12 +87,6 @@ impl IcebergContextManager { Self::populate_tables(ctx, action_tx, namespace).await }); } - - TanicAppState::Exiting => { - break; - } - - _ => {} } } @@ -88,7 +95,7 @@ impl IcebergContextManager { async fn connect_to(&self, new_conn_details: &ConnectionDetails) -> Result<()> { { - let ctx = self.iceberg_context.read().await; + let ctx = self.iceberg_context.read().unwrap(); if let Some(ref existing_conn_details) = ctx.connection_details { if new_conn_details == existing_conn_details { // do nothing, already connected to this catalog @@ -99,7 +106,7 @@ impl IcebergContextManager { // cancel any in-progress action and connect to the new connection { - let mut ctx = self.iceberg_context.write().await; + let mut ctx = self.iceberg_context.write().unwrap(); // TODO: cancel in-prog action // if let Some(cancellable) = *ctx.deref_mut().cancellable_action { // cancellable.abort(); @@ -112,7 +119,7 @@ impl IcebergContextManager { let ctx = self.iceberg_context.clone(); // TODO: store the join handle for cancellation let _jh = tokio::spawn(async move { - let res = Self::populate_namespaces(ctx.clone(), action_tx.clone()).await; + let res = Self::populate_namespaces(ctx, action_tx).await; if let Err(error) = res { tracing::error!(%error, "Error populating namespaces"); } @@ -123,12 +130,16 @@ impl IcebergContextManager { async fn populate_namespaces(ctx: IceCtxRef, action_tx: ActionTx) -> Result<()> { let root_namespaces = { - let r_ctx = ctx.read().await; + let catalog = { + let r_ctx = ctx.read().unwrap(); + + let Some(ref catalog) = r_ctx.catalog else { + return Err(TanicError::unexpected( + "Attempted to populate namespaces when catalog not initialised", + )); + }; - let Some(ref catalog) = r_ctx.catalog else { - return Err(TanicError::unexpected( - "Attempted to populate namespaces when catalog not initialised", - )); + catalog.clone() }; catalog.list_namespaces(None).await? @@ -141,11 +152,15 @@ impl IcebergContextManager { { let namespaces = namespaces.clone(); - ctx.write().await.namespaces = namespaces; + ctx.write().unwrap().namespaces = namespaces; } action_tx - .send(TanicAction::RetrievedNamespaceList(namespaces)) + .send(TanicAction::UpdateNamespacesList( + namespaces.iter().map(|ns| { + ns.name.clone() + }).collect::>() + )) .map_err(|err| TanicError::UnexpectedError(err.to_string()))?; Ok(()) @@ -154,16 +169,21 @@ impl IcebergContextManager { async fn populate_tables( ctx: IceCtxRef, action_tx: ActionTx, - namespace: Vec, + namespace: NamespaceDeets, ) -> Result<()> { - let namespace_ident = NamespaceIdent::from_strs(namespace.clone())?; + let namespace_ident = NamespaceIdent::from_strs(namespace.parts.clone())?; let tables = { - let r_ctx = ctx.read().await; - let Some(ref catalog) = r_ctx.catalog else { - return Err(TanicError::unexpected( - "Attempted to populate namespaces when catalog not initialised", - )); + let catalog = { + let r_ctx = ctx.read().unwrap(); + + let Some(ref catalog) = r_ctx.catalog else { + return Err(TanicError::unexpected( + "Attempted to populate namespaces when catalog not initialised", + )); + }; + + catalog.clone() }; catalog.list_tables(&namespace_ident).await? @@ -172,7 +192,7 @@ impl IcebergContextManager { let tables = tables .into_iter() .map(|ti| TableDeets { - namespace: namespace.clone(), + namespace: namespace.parts.clone(), name: ti.name().to_string(), row_count: 1, }) @@ -180,13 +200,13 @@ impl IcebergContextManager { { let tables = tables.clone(); - ctx.write().await.tables = tables; + ctx.write().unwrap().tables = tables; } action_tx - .send(TanicAction::RetrievedTableList( - NamespaceDeets::from_parts(namespace), - tables, + .send(TanicAction::UpdateNamespaceTableList( + namespace.name.clone(), + tables.iter().map(|t|&t.name).cloned().collect(), )) .map_err(TanicError::unexpected)?; diff --git a/tanic-svc/src/lib.rs b/tanic-svc/src/lib.rs index 7f97aee..932316d 100644 --- a/tanic-svc/src/lib.rs +++ b/tanic-svc/src/lib.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; +use std::sync::RwLock; use tanic_core::TanicConfig; use tanic_core::{Result, TanicError}; use tokio::sync::mpsc::{UnboundedReceiver as MpscReceiver, UnboundedSender as MpscSender}; @@ -7,25 +9,26 @@ pub mod iceberg_context; pub mod state; pub use state::{TanicAction, TanicAppState}; +use crate::state::TanicIcebergState; pub struct AppStateManager { action_rx: MpscReceiver, #[allow(unused)] action_tx: MpscSender, - state_tx: WatchSender, + state_tx: WatchSender<()>, - state: TanicAppState, + state: Arc>, } impl AppStateManager { pub fn new( _config: TanicConfig, - ) -> (Self, MpscSender, WatchReceiver) { - let state = TanicAppState::default(); + ) -> (Self, MpscSender, WatchReceiver<()>) { + let state = Arc::new(RwLock::new(TanicAppState::default())); let (action_tx, action_rx) = tokio::sync::mpsc::unbounded_channel(); - let (state_tx, state_rx) = tokio::sync::watch::channel(state.clone()); + let (state_tx, state_rx) = tokio::sync::watch::channel(()); ( Self { @@ -39,25 +42,31 @@ impl AppStateManager { ) } + pub fn get_state(&self) -> Arc> { + self.state.clone() + } + pub async fn event_loop(self) -> Result<()> { let Self { - mut state, + state, state_tx, mut action_rx, .. } = self; - while !matches!(state, TanicAppState::Exiting) { + while !matches!(state.read().unwrap().iceberg, TanicIcebergState::Exiting) { let Some(action) = action_rx.recv().await else { break; }; tracing::info!(?action, "AppState received an action"); - let next_state = state.reduce(action); + { + let mut mut_state = state.write().unwrap(); + *mut_state = mut_state.clone().update(action); + } - state = next_state; state_tx - .send(state.clone()) + .send(()) .map_err(|err| TanicError::UnexpectedError(err.to_string()))?; } diff --git a/tanic-svc/src/state.rs b/tanic-svc/src/state.rs index 7b1f16d..fb33998 100644 --- a/tanic-svc/src/state.rs +++ b/tanic-svc/src/state.rs @@ -1,5 +1,11 @@ +use std::collections::HashMap; +use indexmap::IndexMap; +use iceberg::spec::{DataFile, Manifest, ManifestList, Snapshot}; +use iceberg::table::Table; +use parquet::file::metadata::ParquetMetaData; use tanic_core::config::ConnectionDetails; -use tanic_core::message::{NamespaceDeets, TableDeets}; + +const TABLE_SUMMARY_KEY_ROW_COUNT: &str = "row-count"; #[derive(Debug)] pub enum TanicAction { @@ -7,191 +13,518 @@ pub enum TanicAction { ConnectTo(ConnectionDetails), - RetrievedNamespaceList(Vec), + // Iceberg metadata update actions + UpdateNamespacesList(Vec), + UpdateNamespaceProperties(String, HashMap), + UpdateNamespaceTableList(String, Vec), + UpdateTable { namespace: String, table_name: String, table: Table }, + UpdateTableSummary { namespace: String, table_name: String, table_summary: HashMap }, + UpdateTableCurrentSnapshot { namespace: String, table_name: String, snapshot: Snapshot }, + UpdateTableCurrentManifestList { namespace: String, table_name: String, manifest_list: ManifestList }, + UpdateTableManifest { namespace: String, table_name: String, manifest: Manifest, file_path: String, }, + UpdateTableDataFile { namespace: String, table_name: String, data_file: DataFile }, + UpdateTableParquetMetaData { namespace: String, table_name: String, file_path: String, metadata: ParquetMetaData }, + + ///// UI Actions /////// FocusPrevNamespace, FocusNextNamespace, SelectNamespace, - RetrievedTableList(NamespaceDeets, Vec), - EnrichedTableDetails(), FocusPrevTable, FocusNextTable, SelectTable, - LeaveNamespace, + + Escape, + + FocusNextPartition, + FocusPrevPartition, + SelectPartition, + + FocusNextDataFile, + FocusPrevDataFile, + SelectDataFile, } #[derive(Clone, Debug, Default)] -pub enum TanicAppState { +pub struct TanicAppState { + pub iceberg: TanicIcebergState, + pub ui: TanicUiState, +} + +#[derive(Clone, Debug, Default)] +pub enum TanicIcebergState { #[default] Initializing, ConnectingTo(ConnectionDetails), + Connected(RetrievedIcebergMetadata), + Exiting, +} + +#[derive(Clone, Debug)] +pub struct RetrievedIcebergMetadata { + pub namespaces: IndexMap, +} + +#[derive(Clone, Debug)] +pub struct NamespaceDescriptor { + pub name: String, + properties: Option>, + pub tables: Option>, +} + +#[derive(Clone, Debug)] +pub struct TableDescriptor { + pub name: String, + #[allow(unused)] + namespace: Vec, + current_snapshot_summary: Option>, + table: Option, + current_snapshot: Option, + current_manifest_list: Option, + manifests: IndexMap, + datafiles: HashMap, + parquet_metadata: HashMap, +} + +impl TableDescriptor { + pub fn row_count(&self) -> Option { + self.current_snapshot_summary.as_ref().and_then(|summary|summary.get( + TABLE_SUMMARY_KEY_ROW_COUNT + )).and_then(|val| str::parse::(val).ok()) + } +} + +#[derive(Clone, Debug, Default)] +pub enum TanicUiState { + #[default] + SplashScreen, ViewingNamespacesList(ViewingNamespacesListState), - RetrievingTableList(ViewingNamespacesListState), ViewingTablesList(ViewingTablesListState), Exiting, } #[derive(Clone, Debug)] pub struct ViewingNamespacesListState { - pub namespaces: Vec, pub selected_idx: Option, } #[derive(Clone, Debug)] pub struct ViewingTablesListState { pub namespaces: ViewingNamespacesListState, - pub namespace: NamespaceDeets, - pub tables: Vec, pub selected_idx: Option, } impl TanicAppState { - pub(crate) fn reduce(self, action: TanicAction) -> Self { - match (action, &self) { - (TanicAction::Exit, _) => TanicAppState::Exiting, + pub(crate) fn update(mut self, action: TanicAction) -> Self { + match (action, &mut self) { + (TanicAction::Exit, _) => { + self.iceberg = TanicIcebergState::Exiting; + self.ui = TanicUiState::Exiting; + }, - (TanicAction::ConnectTo(conn_details), _) => TanicAppState::ConnectingTo(conn_details), + (TanicAction::ConnectTo(conn_details), _) => { + self.iceberg = TanicIcebergState::ConnectingTo(conn_details); + self.ui = TanicUiState::SplashScreen; + }, - (TanicAction::RetrievedNamespaceList(namespaces), _) => { - let selected_idx = if namespaces.is_empty() { None } else { Some(0) }; + (TanicAction::UpdateNamespacesList(namespaces), _) => { + let selected_idx = if namespaces.is_empty() { None } else { + Some(0) + }; - TanicAppState::ViewingNamespacesList(ViewingNamespacesListState { + let namespaces = IndexMap::from_iter( + namespaces.iter().map(|ns|(ns.clone(), NamespaceDescriptor { + name: ns.clone(), + properties: None, + tables: None + })) + ); + + self.iceberg = TanicIcebergState::Connected(RetrievedIcebergMetadata { namespaces, - selected_idx, - }) - } + }); + self.ui = TanicUiState::ViewingNamespacesList(ViewingNamespacesListState { + selected_idx + }); + }, + + (TanicAction::UpdateNamespaceProperties(namespace, properties), prev_state) => { + let TanicAppState { iceberg, .. } = prev_state; + + let TanicIcebergState::Connected(ref mut retrieved_iceberg_metadata ) = iceberg else { + panic!(); + }; + + let Some(namespacce_desc) = retrieved_iceberg_metadata.namespaces.get_mut(&namespace) else { + panic!(); + }; + + namespacce_desc.properties = Some(properties); + }, + + (TanicAction::UpdateNamespaceTableList(namespace, table_names), prev_state) => { + let TanicAppState { iceberg, .. } = prev_state; + + let TanicIcebergState::Connected(ref mut retrieved_iceberg_metadata ) = iceberg else { + panic!(); + }; + + let Some(namespacce_desc) = retrieved_iceberg_metadata.namespaces.get_mut(&namespace) else { + panic!(); + }; + + namespacce_desc.tables = Some(IndexMap::from_iter(table_names.into_iter().map( + |name| ( + name.clone(), + TableDescriptor { + name, + namespace: namespace.split(" ").map(|s|s.to_string()).collect::>(), + current_snapshot_summary: None, + table: None, + current_snapshot: None, + current_manifest_list: None, + manifests: IndexMap::default(), + datafiles: HashMap::default(), + parquet_metadata: HashMap::default(), + } + ) + ))) + }, + + (TanicAction::UpdateTable { namespace, table_name, table }, prev_state) => { + let TanicAppState { iceberg, .. } = prev_state; + + let TanicIcebergState::Connected(ref mut retrieved_iceberg_metadata ) = iceberg else { + panic!(); + }; + + let Some(namespacce_desc) = retrieved_iceberg_metadata.namespaces.get_mut(&namespace) else { + panic!(); + }; + + let Some(ref mut table_desc) = namespacce_desc.tables else { + panic!(); + }; + + let Some(table_desc) = table_desc.get_mut(&table_name) else { + panic!(); + }; + + table_desc.table = Some(table); + }, + + (TanicAction::UpdateTableSummary { namespace, table_name, table_summary }, prev_state) => { + let TanicAppState { iceberg, .. } = prev_state; + + let TanicIcebergState::Connected(ref mut retrieved_iceberg_metadata ) = iceberg else { + panic!(); + }; + + let Some(namespacce_desc) = retrieved_iceberg_metadata.namespaces.get_mut(&namespace) else { + panic!(); + }; + + let Some(ref mut table_desc) = namespacce_desc.tables else { + panic!(); + }; + + let Some(table_desc) = table_desc.get_mut(&table_name) else { + panic!(); + }; + + table_desc.current_snapshot_summary = Some(table_summary); + }, + + (TanicAction::UpdateTableCurrentSnapshot { namespace, table_name, snapshot }, prev_state) => { + let TanicAppState { iceberg, .. } = prev_state; + + let TanicIcebergState::Connected(ref mut retrieved_iceberg_metadata ) = iceberg else { + panic!(); + }; + + let Some(namespacce_desc) = retrieved_iceberg_metadata.namespaces.get_mut(&namespace) else { + panic!(); + }; + + let Some(ref mut table_desc) = namespacce_desc.tables else { + panic!(); + }; + + let Some(table_desc) = table_desc.get_mut(&table_name) else { + panic!(); + }; + + table_desc.current_snapshot = Some(snapshot); + }, + + (TanicAction::UpdateTableCurrentManifestList { namespace, table_name, manifest_list }, prev_state) => { + let TanicAppState { iceberg, .. } = prev_state; + + let TanicIcebergState::Connected(ref mut retrieved_iceberg_metadata ) = iceberg else { + panic!(); + }; + + let Some(namespacce_desc) = retrieved_iceberg_metadata.namespaces.get_mut(&namespace) else { + panic!(); + }; + + let Some(ref mut table_desc) = namespacce_desc.tables else { + panic!(); + }; + + let Some(table_desc) = table_desc.get_mut(&table_name) else { + panic!(); + }; + + table_desc.current_manifest_list = Some(manifest_list); + }, + + (TanicAction::UpdateTableManifest { namespace, table_name, manifest, file_path: uri }, prev_state) => { + let TanicAppState { iceberg, .. } = prev_state; + + let TanicIcebergState::Connected(ref mut retrieved_iceberg_metadata ) = iceberg else { + panic!(); + }; + + let Some(namespacce_desc) = retrieved_iceberg_metadata.namespaces.get_mut(&namespace) else { + panic!(); + }; + + let Some(ref mut table_desc) = namespacce_desc.tables else { + panic!(); + }; + + let Some(table_desc) = table_desc.get_mut(&table_name) else { + panic!(); + }; + + table_desc.manifests.insert( + uri, + manifest + ); + }, + + (TanicAction::UpdateTableDataFile { namespace, table_name, data_file }, prev_state) => { + let TanicAppState { iceberg, .. } = prev_state; + + let TanicIcebergState::Connected(ref mut retrieved_iceberg_metadata ) = iceberg else { + panic!(); + }; + + let Some(namespacce_desc) = retrieved_iceberg_metadata.namespaces.get_mut(&namespace) else { + panic!(); + }; + + let Some(ref mut table_desc) = namespacce_desc.tables else { + panic!(); + }; + + let Some(table_desc) = table_desc.get_mut(&table_name) else { + panic!(); + }; + + table_desc.datafiles.insert( + data_file.file_path().to_string(), + data_file + ); + }, + + (TanicAction::UpdateTableParquetMetaData { namespace, table_name, file_path, metadata }, prev_state) => { + let TanicAppState { iceberg, .. } = prev_state; + + let TanicIcebergState::Connected(ref mut retrieved_iceberg_metadata ) = iceberg else { + panic!(); + }; + + let Some(namespacce_desc) = retrieved_iceberg_metadata.namespaces.get_mut(&namespace) else { + panic!(); + }; + + let Some(ref mut table_desc) = namespacce_desc.tables else { + panic!(); + }; + + let Some(table_desc) = table_desc.get_mut(&table_name) else { + panic!(); + }; + + table_desc.parquet_metadata.insert( + file_path, + metadata + ); + }, ( TanicAction::FocusPrevNamespace, - TanicAppState::ViewingNamespacesList(ViewingNamespacesListState { - namespaces, - selected_idx, - }), + prev_state ) => { - let selected_idx = selected_idx.map(|selected_idx| { + let TanicAppState { iceberg, ui } = prev_state; + + let TanicUiState::ViewingNamespacesList(ref mut viewing_namespaces_list_state ) = ui else { + panic!(); + }; + + let TanicIcebergState::Connected(ref mut retrieved_iceberg_metadata ) = iceberg else { + panic!(); + }; + + viewing_namespaces_list_state.selected_idx = viewing_namespaces_list_state.selected_idx.map(|selected_idx| { if selected_idx == 0 { - namespaces.len() - 1 + retrieved_iceberg_metadata.namespaces.len() - 1 } else { selected_idx - 1 } }); - - TanicAppState::ViewingNamespacesList(ViewingNamespacesListState { - namespaces: namespaces.clone(), - selected_idx, - }) } ( TanicAction::FocusNextNamespace, - TanicAppState::ViewingNamespacesList(ViewingNamespacesListState { - namespaces, - selected_idx, - }), + prev_state ) => { - let selected_idx = selected_idx.map(|selected_idx| { - if selected_idx == namespaces.len() - 1 { + let TanicAppState { iceberg, ui } = prev_state; + + let TanicUiState::ViewingNamespacesList(ref mut viewing_namespaces_list_state ) = ui else { + panic!(); + }; + + let TanicIcebergState::Connected(ref mut retrieved_iceberg_metadata ) = iceberg else { + panic!(); + }; + + viewing_namespaces_list_state.selected_idx = viewing_namespaces_list_state.selected_idx.map(|selected_idx| { + if selected_idx == retrieved_iceberg_metadata.namespaces.len() - 1 { 0 } else { selected_idx + 1 } }); - - TanicAppState::ViewingNamespacesList(ViewingNamespacesListState { - namespaces: namespaces.clone(), - selected_idx, - }) - } + }, ( TanicAction::SelectNamespace, - TanicAppState::ViewingNamespacesList(ViewingNamespacesListState { - selected_idx, - namespaces, - }), - ) => TanicAppState::RetrievingTableList(ViewingNamespacesListState { - selected_idx: *selected_idx, - namespaces: namespaces.clone(), - }), - - ( - TanicAction::RetrievedTableList(namespace, tables), - TanicAppState::RetrievingTableList(ViewingNamespacesListState { - selected_idx: namespace_selected_idx, - namespaces, - }), + prev_state, ) => { - let table_selected_idx = if tables.is_empty() { None } else { Some(0) }; - - TanicAppState::ViewingTablesList(ViewingTablesListState { - namespaces: ViewingNamespacesListState { - selected_idx: *namespace_selected_idx, - namespaces: namespaces.clone(), - }, - namespace, - tables, - selected_idx: table_selected_idx, - }) - } + let TanicAppState { ui, .. } = prev_state; + + let TanicUiState::ViewingNamespacesList(namespaces ) = ui else { + panic!(); + }; + + self.ui = TanicUiState::ViewingTablesList(ViewingTablesListState { + namespaces: namespaces.clone(), + selected_idx: None, + }); + }, ( TanicAction::FocusPrevTable, - TanicAppState::ViewingTablesList(ViewingTablesListState { - namespaces, - namespace, - tables, - selected_idx, - }), + prev_state, ) => { - let selected_idx = selected_idx.map(|selected_idx| { - if selected_idx == 0 { - tables.len() - 1 - } else { - selected_idx - 1 - } - }); + let TanicAppState { iceberg, ui } = prev_state; - TanicAppState::ViewingTablesList(ViewingTablesListState { - namespaces: namespaces.clone(), - namespace: namespace.clone(), - tables: tables.clone(), - selected_idx, - }) - } + let TanicIcebergState::Connected(ref mut retrieved_iceberg_metadata ) = iceberg else { + panic!(); + }; + + let TanicUiState::ViewingTablesList(ref mut viewing_tables_list_state ) = ui else { + panic!(); + }; + + let Some(namespace_selected_idx) = viewing_tables_list_state.namespaces.selected_idx else { + panic!(); + }; + + let Some(&namespace_selected_name) = retrieved_iceberg_metadata.namespaces.keys().collect::>().get(namespace_selected_idx) else { + panic!(); + }; + + let Some(namespace) = retrieved_iceberg_metadata.namespaces.get(namespace_selected_name) else { + panic!(); + }; + + if let Some(ref table_list) = namespace.tables { + let table_list_len = table_list.len(); + + viewing_tables_list_state.selected_idx = viewing_tables_list_state.selected_idx.map(|selected_idx| { + if selected_idx == 0 { + table_list_len - 1 + } else { + selected_idx - 1 + } + }); + } + }, ( TanicAction::FocusNextTable, - TanicAppState::ViewingTablesList(ViewingTablesListState { - namespaces, - namespace, - tables, - selected_idx, - }), + prev_state, ) => { - let selected_idx = selected_idx.map(|selected_idx| { - if selected_idx == tables.len() - 1 { - 0 - } else { - selected_idx + 1 - } - }); + let TanicAppState { iceberg, ui } = prev_state; - TanicAppState::ViewingTablesList(ViewingTablesListState { - namespaces: namespaces.clone(), - namespace: namespace.clone(), - tables: tables.clone(), - selected_idx, - }) - } + let TanicIcebergState::Connected(ref mut retrieved_iceberg_metadata ) = iceberg else { + panic!(); + }; + + let TanicUiState::ViewingTablesList(ref mut viewing_tables_list_state ) = ui else { + panic!(); + }; - (TanicAction::SelectTable, _) => self, + let Some(namespace_selected_idx) = viewing_tables_list_state.namespaces.selected_idx else { + panic!(); + }; + + let Some(&namespace_selected_name) = retrieved_iceberg_metadata.namespaces.keys().collect::>().get(namespace_selected_idx) else { + panic!(); + }; + + let Some(namespace) = retrieved_iceberg_metadata.namespaces.get(namespace_selected_name) else { + panic!(); + }; + + if let Some(ref table_list) = namespace.tables { + viewing_tables_list_state.selected_idx = viewing_tables_list_state.selected_idx.map(|selected_idx| { + if selected_idx == table_list.len() - 1 { + 0 + } else { + selected_idx + 1 + } + }); + } + }, ( - TanicAction::LeaveNamespace, - TanicAppState::ViewingTablesList(ViewingTablesListState { namespaces, .. }), - ) => TanicAppState::ViewingNamespacesList(namespaces.clone()), + TanicAction::Escape, + TanicAppState { + ui, + .. + } + ) => { + match ui { + TanicUiState::ViewingTablesList(ViewingTablesListState { + namespaces, + .. + }) => { + self.ui = TanicUiState::ViewingNamespacesList(namespaces.clone()) + } + _ => {} + } + } + // TODO: - _ => self, + // * Escape + // * SelectTable + // * FocusNextPartition + // * FocusPrevPartition, + // * SelectPartition, + // * FocusNextDataFile, + // * FocusPrevDataFile, + // * SelectDataFile + + _ => { + unimplemented!() + }, } + + self } } diff --git a/tanic-tui/lib.rs b/tanic-tui/lib.rs index 010a112..4cb4851 100644 --- a/tanic-tui/lib.rs +++ b/tanic-tui/lib.rs @@ -1,12 +1,15 @@ +use std::sync::Arc; use crossterm::event::{Event, EventStream}; use ratatui::Frame; use tokio::sync::mpsc::UnboundedSender as MpscSender; +use std::sync::RwLock; use tokio::sync::watch::Receiver as WatchReceiver; use tokio_stream::{wrappers::WatchStream, StreamExt}; use crate::ui_components::app_container::AppContainer; use tanic_core::{Result, TanicError}; use tanic_svc::{TanicAction, TanicAppState}; +use tanic_svc::state::TanicUiState; mod component; mod ui_components; @@ -20,16 +23,26 @@ impl TanicTui { Self { action_tx } } - pub async fn event_loop(self, state_rx: WatchReceiver) -> Result<()> { + pub async fn event_loop(self, state_rx: WatchReceiver<()>, state: Arc>) -> Result<()> { let mut terminal = ratatui::init(); let mut term_event_stream = EventStream::new(); let mut state_stream = WatchStream::new(state_rx); - let mut state = TanicAppState::Initializing; + let Some(_) = state_stream.next().await else { + return Ok(()); + }; - while !matches!(&state, TanicAppState::Exiting) { - let ui = AppContainer::new(&state); - terminal.draw(|frame| self.draw(frame, &ui))?; + let ui = AppContainer::new(state.clone()); + + loop { + { + let state = state.read().unwrap(); + if matches!(state.ui, TanicUiState::Exiting) { + break; + } + + terminal.draw(|frame| self.draw(frame, &ui))?; + }; tokio::select! { // Catch and handle crossterm events @@ -47,9 +60,7 @@ impl TanicTui { }, // Handle state updates - Some(new_state) = state_stream.next() => { - state = new_state; - }, + _ = state_stream.next() => {} } } diff --git a/tanic-tui/ui_components/app_container.rs b/tanic-tui/ui_components/app_container.rs index cf78eb7..97a4d0b 100644 --- a/tanic-tui/ui_components/app_container.rs +++ b/tanic-tui/ui_components/app_container.rs @@ -1,3 +1,4 @@ +use std::sync::{Arc, RwLock}; use crate::component::Component; use crate::ui_components::{ namespace_list_view::NamespaceListView, splash_screen::SplashScreen, @@ -10,22 +11,23 @@ use ratatui::prelude::{Color, Style, Widget}; use ratatui::widgets::Block; use tanic_svc::{TanicAction, TanicAppState}; use tui_logger::{LevelFilter, TuiLoggerLevelOutput, TuiLoggerWidget, TuiWidgetState}; +use tanic_svc::state::TanicUiState; -pub(crate) struct AppContainer<'a> { - state: &'a TanicAppState, - namespace_list_view: NamespaceListView<'a>, - table_list_view: TableListView<'a>, - splash_screen: SplashScreen<'a>, +pub(crate) struct AppContainer { + state: Arc>, + namespace_list_view: NamespaceListView, + table_list_view: TableListView, + splash_screen: SplashScreen, } -impl<'a> AppContainer<'a> { - pub(crate) fn new(state: &'a TanicAppState) -> Self { +impl AppContainer { + pub(crate) fn new(state: Arc>) -> Self { Self { - state, + state: state.clone(), - namespace_list_view: NamespaceListView::new(state), - table_list_view: TableListView::new(state), - splash_screen: SplashScreen::new(state), + namespace_list_view: NamespaceListView::new(state.clone()), + table_list_view: TableListView::new(state.clone()), + splash_screen: SplashScreen::new(state.clone()), } } @@ -38,20 +40,24 @@ impl<'a> AppContainer<'a> { // User pressed Q. Dispatch an exit action Some(TanicAction::Exit) } - key_event => match &self.state { - TanicAppState::ViewingNamespacesList(_) => { - (&self.namespace_list_view).handle_key_event(key_event) - } - TanicAppState::ViewingTablesList(_) => { - (&self.table_list_view).handle_key_event(key_event) + + key_event => { + let state = self.state.read().unwrap(); + match state.ui { + TanicUiState::ViewingNamespacesList(_) => { + (&self.namespace_list_view).handle_key_event(key_event) + } + TanicUiState::ViewingTablesList(_) => { + (&self.table_list_view).handle_key_event(key_event) + } + _ => None, } - _ => None, }, } } } -impl Widget for &AppContainer<'_> { +impl Widget for &AppContainer { fn render(self, area: Rect, buf: &mut Buffer) { let [top, bottom] = Layout::vertical([Constraint::Fill(1), Constraint::Max(6)]).areas(area); @@ -71,12 +77,13 @@ impl Widget for &AppContainer<'_> { .state(&filter_state) .render(bottom, buf); - match &self.state { - TanicAppState::Initializing => self.splash_screen.render(top, buf), - TanicAppState::ViewingNamespacesList(_) => (&self.namespace_list_view).render(top, buf), - TanicAppState::ViewingTablesList(_) => (&self.table_list_view).render(top, buf), - TanicAppState::Exiting => {} - _ => {} + let state = self.state.read().unwrap(); + match state.ui { + TanicUiState::SplashScreen => self.splash_screen.render(top, buf), + TanicUiState::ViewingNamespacesList(_) => (&self.namespace_list_view).render(top, buf), + TanicUiState::ViewingTablesList(_) => (&self.table_list_view).render(top, buf), + TanicUiState::Exiting => {} + // _ => {} } } } diff --git a/tanic-tui/ui_components/namespace_list_item.rs b/tanic-tui/ui_components/namespace_list_item.rs index d2ede14..5ec291e 100644 --- a/tanic-tui/ui_components/namespace_list_item.rs +++ b/tanic-tui/ui_components/namespace_list_item.rs @@ -2,17 +2,17 @@ use crate::component::Component; use ratatui::prelude::*; use ratatui::symbols::border; use ratatui::widgets::{Block, Paragraph}; -use tanic_core::message::NamespaceDeets; +use tanic_svc::state::NamespaceDescriptor; const NERD_FONT_ICON_TABLE_FOLDER: &str = "\u{f12e4}"; // 󱋤 pub(crate) struct NamespaceListItem<'a> { - pub(crate) ns: &'a NamespaceDeets, + pub(crate) ns: &'a NamespaceDescriptor, pub(crate) is_selected: bool, } impl<'a> NamespaceListItem<'a> { - pub(crate) fn new(ns: &'a NamespaceDeets, is_selected: bool) -> Self { + pub(crate) fn new(ns: &'a NamespaceDescriptor, is_selected: bool) -> Self { Self { ns, is_selected } } } @@ -27,10 +27,12 @@ impl Component for &NamespaceListItem<'_> { } let name = self.ns.name.clone(); - let plural_suffix = if self.ns.table_count == 1 { "" } else { "s" }; + let tables = &self.ns.tables; + let table_count = tables.as_ref().map(|t|t.len()).unwrap_or(0); + let plural_suffix = if table_count == 1 { "" } else { "s" }; let name = format!( "{} {} ({} table{})", - NERD_FONT_ICON_TABLE_FOLDER, name, self.ns.table_count, plural_suffix + NERD_FONT_ICON_TABLE_FOLDER, name, table_count, plural_suffix ); let para_rect = Rect::new( diff --git a/tanic-tui/ui_components/namespace_list_view.rs b/tanic-tui/ui_components/namespace_list_view.rs index 0daad18..0cc3933 100644 --- a/tanic-tui/ui_components/namespace_list_view.rs +++ b/tanic-tui/ui_components/namespace_list_view.rs @@ -1,3 +1,4 @@ +use std::sync::{Arc, RwLock}; use crossterm::event::{KeyCode, KeyEvent}; use ratatui::prelude::*; use ratatui::symbols::border; @@ -7,18 +8,19 @@ use crate::component::Component; use crate::ui_components::namespace_list_item::NamespaceListItem; use crate::ui_components::treemap_layout::TreeMapLayout; use tanic_svc::{TanicAction, TanicAppState}; +use tanic_svc::state::{TanicIcebergState, TanicUiState}; -pub(crate) struct NamespaceListView<'a> { - state: &'a TanicAppState, +pub(crate) struct NamespaceListView { + state: Arc>, } -impl<'a> NamespaceListView<'a> { - pub(crate) fn new(state: &'a TanicAppState) -> Self { +impl NamespaceListView { + pub(crate) fn new(state: Arc>) -> Self { Self { state } } } -impl Component for &NamespaceListView<'_> { +impl Component for &NamespaceListView { fn handle_key_event(&mut self, key_event: KeyEvent) -> Option { match key_event.code { KeyCode::Left => Some(TanicAction::FocusPrevNamespace), @@ -34,22 +36,17 @@ impl Component for &NamespaceListView<'_> { .border_set(border::PLAIN); let block_inner_area = block.inner(area); - let TanicAppState::ViewingNamespacesList(view_state) = self.state else { - panic!(); - }; - - let items = view_state - .namespaces - .iter() - .enumerate() - .map(|(idx, ns)| { - NamespaceListItem::new(ns, view_state.selected_idx.unwrap_or(usize::MAX) == idx) - }) - .collect::>(); + let state = self.state.read().unwrap(); + let items = self.get_items(&state); let children: Vec<(&NamespaceListItem, usize)> = items .iter() - .map(|item| (item, item.ns.table_count)) + .map(|item| { + let tables = &item.ns.tables; + let table_count = tables.as_ref().map(|t|t.len()).unwrap_or(0); + + (item, table_count) + }) .collect::>(); let layout = TreeMapLayout::new(children); @@ -58,3 +55,28 @@ impl Component for &NamespaceListView<'_> { (&layout).render(block_inner_area, buf); } } + +impl NamespaceListView { + fn get_items<'a>(&self, state: &'a TanicAppState) -> Vec> { + // let state = self.state.read().unwrap(); + + let TanicIcebergState::Connected(ref iceberg_state) = state.iceberg else { + return vec![]; + }; + + let TanicUiState::ViewingNamespacesList(ref view_state) = state.ui else { + return vec![]; + }; + + let items = iceberg_state + .namespaces + .iter() + .enumerate() + .map(|(idx, (_, ns))| { + NamespaceListItem::new(ns, view_state.selected_idx.unwrap_or(usize::MAX) == idx) + }) + .collect::>(); + + items + } +} diff --git a/tanic-tui/ui_components/splash_screen.rs b/tanic-tui/ui_components/splash_screen.rs index 1958539..c04378f 100644 --- a/tanic-tui/ui_components/splash_screen.rs +++ b/tanic-tui/ui_components/splash_screen.rs @@ -1,19 +1,20 @@ +use std::sync::{Arc, RwLock}; use ratatui::prelude::*; use ratatui::symbols::border; use ratatui::widgets::{Block, Paragraph}; use tanic_svc::TanicAppState; -pub(crate) struct SplashScreen<'a> { - _state: &'a TanicAppState, +pub(crate) struct SplashScreen { + _state: Arc>, } -impl<'a> SplashScreen<'a> { - pub(crate) fn new(state: &'a TanicAppState) -> Self { +impl SplashScreen { + pub(crate) fn new(state: Arc>) -> Self { Self { _state: state } } } -impl Widget for &SplashScreen<'_> { +impl Widget for &SplashScreen { fn render(self, area: Rect, buf: &mut Buffer) { let style = Style::new().white().bold(); let title = Line::styled(" Tanic ".to_string(), style); diff --git a/tanic-tui/ui_components/table_list_item.rs b/tanic-tui/ui_components/table_list_item.rs index 897fa0f..eb0c320 100644 --- a/tanic-tui/ui_components/table_list_item.rs +++ b/tanic-tui/ui_components/table_list_item.rs @@ -2,17 +2,17 @@ use crate::component::Component; use ratatui::prelude::*; use ratatui::symbols::border; use ratatui::widgets::{Block, Paragraph}; -use tanic_core::message::TableDeets; +use tanic_svc::state::TableDescriptor; const NERD_FONT_ICON_TABLE: &str = "\u{ebb7}"; //  pub(crate) struct TableListItem<'a> { - pub(crate) table: &'a TableDeets, + pub(crate) table: &'a TableDescriptor, pub(crate) is_selected: bool, } impl<'a> TableListItem<'a> { - pub(crate) fn new(table: &'a TableDeets, is_selected: bool) -> Self { + pub(crate) fn new(table: &'a TableDescriptor, is_selected: bool) -> Self { Self { table, is_selected } } } @@ -27,10 +27,16 @@ impl Component for &TableListItem<'_> { } let name = self.table.name.clone(); - let plural_suffix = if self.table.row_count == 1 { "" } else { "s" }; + + let row_count_str = match self.table.row_count() { + None => "".to_string(), + Some(1) => " (1 row)".to_string(), + Some(n) => format!(" ({n} rows)") + }; + let name = format!( - "{} {} ({} row{})", - NERD_FONT_ICON_TABLE, name, self.table.row_count, plural_suffix + "{} {}{}", + NERD_FONT_ICON_TABLE, name, row_count_str ); let para_rect = Rect::new( diff --git a/tanic-tui/ui_components/table_list_view.rs b/tanic-tui/ui_components/table_list_view.rs index 33e7b85..34c5961 100644 --- a/tanic-tui/ui_components/table_list_view.rs +++ b/tanic-tui/ui_components/table_list_view.rs @@ -1,3 +1,4 @@ +use std::sync::{Arc, RwLock}; use crossterm::event::{KeyCode, KeyEvent}; use ratatui::prelude::*; use ratatui::symbols::border; @@ -7,53 +8,55 @@ use crate::component::Component; use crate::ui_components::table_list_item::TableListItem; use crate::ui_components::treemap_layout::TreeMapLayout; use tanic_svc::{TanicAction, TanicAppState}; +use tanic_svc::state::{RetrievedIcebergMetadata, TanicIcebergState, TanicUiState, ViewingTablesListState}; -pub(crate) struct TableListView<'a> { - state: &'a TanicAppState, +pub(crate) struct TableListView { + state: Arc>, } -impl<'a> TableListView<'a> { - pub(crate) fn new(state: &'a TanicAppState) -> Self { +impl TableListView { + pub(crate) fn new(state: Arc>) -> Self { Self { state } } } -impl Component for &TableListView<'_> { +impl Component for &TableListView { fn handle_key_event(&mut self, key_event: KeyEvent) -> Option { match key_event.code { KeyCode::Left => Some(TanicAction::FocusPrevTable), KeyCode::Right => Some(TanicAction::FocusNextTable), KeyCode::Enter => Some(TanicAction::SelectTable), - KeyCode::Esc => Some(TanicAction::LeaveNamespace), + KeyCode::Esc => Some(TanicAction::Escape), _ => None, } } fn render(&self, area: Rect, buf: &mut Buffer) { - let TanicAppState::ViewingTablesList(view_state) = self.state else { + let state = self.state.read().unwrap(); + + let TanicIcebergState::Connected(ref iceberg_state) = state.iceberg else { + panic!(); + }; + + let TanicUiState::ViewingTablesList(ref view_state) = state.ui else { panic!(); }; let block = Block::bordered() .title(format!( " Tanic //// {} Namespace ", - view_state.namespace.name + view_state.namespaces.selected_idx.and_then( + |idx| iceberg_state.namespaces.get_index(idx) + ).map(|(k, _)|k.to_string()).unwrap_or("???".to_string()) )) .border_set(border::PLAIN); let block_inner_area = block.inner(area); - let items = view_state - .tables - .iter() - .enumerate() - .map(|(idx, ns)| { - TableListItem::new(ns, view_state.selected_idx.unwrap_or(usize::MAX) == idx) - }) - .collect::>(); + let items = TableListView::get_items(iceberg_state, view_state); let children: Vec<(&TableListItem, usize)> = items .iter() - .map(|item| (item, item.table.row_count)) + .map(|item| (item, item.table.row_count().unwrap_or(1) as usize)) .collect::>(); let layout = TreeMapLayout::new(children); @@ -62,3 +65,29 @@ impl Component for &TableListView<'_> { (&layout).render(block_inner_area, buf); } } + +impl TableListView { + fn get_items<'a>(iceberg_state: &'a RetrievedIcebergMetadata, view_state: &'a ViewingTablesListState) -> Vec> { + let Some(ref selected_namespace) = view_state.selected_idx else { + return vec![]; + }; + + let Some((_, namespace_desc)) = iceberg_state.namespaces.get_index(*selected_namespace) else { + return vec![]; + }; + + let Some(tables) = &namespace_desc.tables else { + return vec![]; + }; + + let items = tables + .iter() + .enumerate() + .map(|(idx, (_, ns))| { + TableListItem::new(ns, Some(idx) == view_state.selected_idx) + }) + .collect::>(); + + items + } +} diff --git a/tanic/src/main.rs b/tanic/src/main.rs index cd846a4..d02aa0f 100644 --- a/tanic/src/main.rs +++ b/tanic/src/main.rs @@ -19,18 +19,24 @@ async fn main() -> Result<()> { let args = Args::try_parse().into_diagnostic()?; let config = TanicConfig::load().into_diagnostic()?; tracing::info!(?config, "loaded config"); - // let config = Arc::new(RwLock::new(config)); let (app_state, action_tx, state_rx) = AppStateManager::new(config); - let tanic_tui = TanicTui::new(action_tx.clone()); - let iceberg_ctx_mgr = IcebergContextManager::new(action_tx.clone()); + + let ui_task = tokio::spawn({ + let tanic_tui = TanicTui::new(action_tx.clone()); + let state_rx = state_rx.clone(); + let app_state = app_state.get_state(); + async move { tanic_tui.event_loop(state_rx, app_state).await } + }); + + let iceberg_task = tokio::spawn({ + let state_rx = state_rx.clone(); + let app_state = app_state.get_state(); + let iceberg_ctx_mgr = IcebergContextManager::new(action_tx.clone(), app_state); + async move { iceberg_ctx_mgr.event_loop(state_rx).await } + }); let svc_task = tokio::spawn(async move { app_state.event_loop().await }); - let ui_state_rx = state_rx.clone(); - let ui_task = tokio::spawn(async move { tanic_tui.event_loop(ui_state_rx).await }); - let iceberg_task_state_rx = state_rx.clone(); - let iceberg_task = - tokio::spawn(async move { iceberg_ctx_mgr.event_loop(iceberg_task_state_rx).await }); if let Some(ref uri) = args.catalogue_uri { let connection = ConnectionDetails::new_anon(uri.clone()); From 8996ff6f665c12d7f7e6ab9fb0ee7ca67cdd7c11 Mon Sep 17 00:00:00 2001 From: Scott Donnelly Date: Sat, 15 Feb 2025 10:27:40 +0000 Subject: [PATCH 5/7] feat: summary loading --- tanic-svc/Cargo.toml | 1 + tanic-svc/src/iceberg_context.rs | 205 ++++++-- tanic-svc/src/lib.rs | 6 +- tanic-svc/src/state.rs | 483 +++++++++++------- tanic-tui/lib.rs | 12 +- tanic-tui/ui_components/app_container.rs | 12 +- .../ui_components/namespace_list_item.rs | 15 +- .../ui_components/namespace_list_view.rs | 6 +- tanic-tui/ui_components/splash_screen.rs | 2 +- tanic-tui/ui_components/table_list_item.rs | 7 +- tanic-tui/ui_components/table_list_view.rs | 29 +- tanic/Cargo.toml | 1 + tanic/src/args.rs | 3 + tanic/src/logging.rs | 19 +- tanic/src/main.rs | 34 +- 15 files changed, 558 insertions(+), 277 deletions(-) diff --git a/tanic-svc/Cargo.toml b/tanic-svc/Cargo.toml index 421ad75..5b3bc14 100644 --- a/tanic-svc/Cargo.toml +++ b/tanic-svc/Cargo.toml @@ -33,3 +33,4 @@ names = "0.14.0" tokio-stream = { version = "0.1.17", features = ["sync"] } parquet = "54.0.0" indexmap = "2.7.1" +futures = "0.3.31" diff --git a/tanic-svc/src/iceberg_context.rs b/tanic-svc/src/iceberg_context.rs index e657d7f..742d40a 100644 --- a/tanic-svc/src/iceberg_context.rs +++ b/tanic-svc/src/iceberg_context.rs @@ -1,23 +1,29 @@ //! Iceberg Context -use iceberg::{Catalog, NamespaceIdent}; -use iceberg_catalog_rest::{RestCatalog, RestCatalogConfig}; use std::sync::Arc; +use std::sync::RwLock; + +use futures::stream::StreamExt; +use iceberg::{Catalog, NamespaceIdent, TableIdent}; +use iceberg_catalog_rest::{RestCatalog, RestCatalogConfig}; use tokio::sync::mpsc::UnboundedSender; use tokio::sync::watch::Receiver; -use std::sync::RwLock; use tokio::task::JoinHandle; -use tokio_stream::{wrappers::WatchStream, StreamExt}; +use tokio_stream::wrappers::WatchStream; use tanic_core::config::ConnectionDetails; use tanic_core::message::{NamespaceDeets, TableDeets}; use tanic_core::{Result, TanicError}; +use tokio::sync::mpsc::{channel, Receiver as MpscReceiver, Sender as MpscSender}; +use tokio_stream::wrappers::ReceiverStream; use crate::state::{TanicAction, TanicAppState, TanicIcebergState}; type ActionTx = UnboundedSender; type IceCtxRef = Arc>; +const JOB_STREAM_CONCURRENCY: usize = 1; + #[derive(Debug, Default)] struct IcebergContext { connection_details: Option, @@ -40,6 +46,13 @@ pub struct IcebergContextManager { state_ref: Arc>, } +#[derive(Debug)] +enum IcebergTask { + Namespaces, + TablesForNamespace(NamespaceDeets), + SummaryForTable(TableDeets), +} + impl IcebergContextManager { pub fn new(action_tx: ActionTx, state_ref: Arc>) -> Self { Self { @@ -52,8 +65,16 @@ impl IcebergContextManager { pub async fn event_loop(&self, state_rx: Receiver<()>) -> Result<()> { let mut state_stream = WatchStream::new(state_rx); - while state_stream.next().await.is_some() { + let (job_queue_tx, job_queue_rx) = channel(10); + + tokio::spawn({ + let action_tx = self.action_tx.clone(); + let job_queue_tx = job_queue_tx.clone(); + let iceberg_ctx = self.iceberg_context.clone(); + async move { Self::job_handler(job_queue_rx, job_queue_tx, action_tx, iceberg_ctx).await } + }); + while state_stream.next().await.is_some() { let new_conn_details = { let state = self.state_ref.read().unwrap(); @@ -64,36 +85,27 @@ impl IcebergContextManager { TanicIcebergState::Exiting => { break; } - _ => None + _ => None, } }; if let Some(new_conn_details) = new_conn_details { - self.connect_to(&new_conn_details).await?; - - let namespaces = { - self.iceberg_context.read().unwrap().namespaces.clone() - }; + self.connect_to(&new_conn_details, job_queue_tx.clone()) + .await?; // begin crawl - for namespace in namespaces { - - // spawn a task to start populating the namespaces - let action_tx = self.action_tx.clone(); - let ctx = self.iceberg_context.clone(); - - // TODO: handle handle, lol - let _jh = tokio::spawn(async move { - Self::populate_tables(ctx, action_tx, namespace).await - }); - } + let _ = job_queue_tx.send(IcebergTask::Namespaces).await; } } Ok(()) } - async fn connect_to(&self, new_conn_details: &ConnectionDetails) -> Result<()> { + async fn connect_to( + &self, + new_conn_details: &ConnectionDetails, + _job_queue_tx: MpscSender, + ) -> Result<()> { { let ctx = self.iceberg_context.read().unwrap(); if let Some(ref existing_conn_details) = ctx.connection_details { @@ -104,31 +116,19 @@ impl IcebergContextManager { } } - // cancel any in-progress action and connect to the new connection { let mut ctx = self.iceberg_context.write().unwrap(); - // TODO: cancel in-prog action - // if let Some(cancellable) = *ctx.deref_mut().cancellable_action { - // cancellable.abort(); - // } ctx.connect_to(new_conn_details); } - // spawn a task to start populating the namespaces - let action_tx = self.action_tx.clone(); - let ctx = self.iceberg_context.clone(); - // TODO: store the join handle for cancellation - let _jh = tokio::spawn(async move { - let res = Self::populate_namespaces(ctx, action_tx).await; - if let Err(error) = res { - tracing::error!(%error, "Error populating namespaces"); - } - }); - Ok(()) } - async fn populate_namespaces(ctx: IceCtxRef, action_tx: ActionTx) -> Result<()> { + async fn populate_namespaces( + ctx: IceCtxRef, + action_tx: ActionTx, + job_queue_tx: MpscSender, + ) -> Result<()> { let root_namespaces = { let catalog = { let r_ctx = ctx.read().unwrap(); @@ -157,12 +157,19 @@ impl IcebergContextManager { action_tx .send(TanicAction::UpdateNamespacesList( - namespaces.iter().map(|ns| { - ns.name.clone() - }).collect::>() + namespaces + .iter() + .map(|ns| ns.name.clone()) + .collect::>(), )) .map_err(|err| TanicError::UnexpectedError(err.to_string()))?; + for namespace in namespaces { + let _ = job_queue_tx + .send(IcebergTask::TablesForNamespace(namespace.clone())) + .await; + } + Ok(()) } @@ -170,10 +177,10 @@ impl IcebergContextManager { ctx: IceCtxRef, action_tx: ActionTx, namespace: NamespaceDeets, + job_queue_tx: MpscSender, ) -> Result<()> { let namespace_ident = NamespaceIdent::from_strs(namespace.parts.clone())?; let tables = { - let catalog = { let r_ctx = ctx.read().unwrap(); @@ -206,10 +213,59 @@ impl IcebergContextManager { action_tx .send(TanicAction::UpdateNamespaceTableList( namespace.name.clone(), - tables.iter().map(|t|&t.name).cloned().collect(), + tables.iter().map(|t| &t.name).cloned().collect(), )) .map_err(TanicError::unexpected)?; + for table in tables { + let _ = job_queue_tx + .send(IcebergTask::SummaryForTable(table.clone())) + .await; + } + + Ok(()) + } + + async fn populate_table_summary( + ctx: IceCtxRef, + action_tx: ActionTx, + table: TableDeets, + _job_queue_tx: MpscSender, + ) -> Result<()> { + let namespace_ident = NamespaceIdent::from_strs(table.namespace.clone())?; + let table_ident = TableIdent::new(namespace_ident.clone(), table.name.clone()); + + let loaded_table = { + let catalog = { + let r_ctx = ctx.read().unwrap(); + + let Some(ref catalog) = r_ctx.catalog else { + return Err(TanicError::unexpected( + "Attempted to populate table summary when catalog not initialised", + )); + }; + + catalog.clone() + }; + + catalog.load_table(&table_ident).await? + }; + + let summary = loaded_table + .metadata() + .current_snapshot() + .unwrap() + .summary(); + tracing::info!(?summary); + + action_tx + .send(TanicAction::UpdateTableSummary { + namespace: namespace_ident.to_url_string(), + table_name: table_ident.name.clone(), + table_summary: summary.additional_properties.clone(), + }) + .map_err(TanicError::unexpected)?; + Ok(()) } } @@ -229,3 +285,62 @@ impl IcebergContext { self.tables = vec![]; } } + +impl IcebergContextManager { + async fn job_handler( + job_queue_rx: MpscReceiver, + job_queue_tx: MpscSender, + action_tx: ActionTx, + iceberg_ctx: IceCtxRef, + ) { + let job_stream = ReceiverStream::new(job_queue_rx); + + // let _ = tokio::spawn(async move { + job_stream + .map(|task| { + ( + task, + iceberg_ctx.clone(), + action_tx.clone(), + job_queue_tx.clone(), + ) + }) + .for_each_concurrent( + JOB_STREAM_CONCURRENCY, + async move |(task, iceberg_ctx, action_tx, job_queue_tx)| { + match task { + IcebergTask::Namespaces => { + let _ = IcebergContextManager::populate_namespaces( + iceberg_ctx, + action_tx, + job_queue_tx, + ) + .await; + } + + IcebergTask::TablesForNamespace(namespace) => { + let _ = IcebergContextManager::populate_tables( + iceberg_ctx, + action_tx, + namespace, + job_queue_tx, + ) + .await; + } + + IcebergTask::SummaryForTable(table) => { + let _ = IcebergContextManager::populate_table_summary( + iceberg_ctx, + action_tx, + table, + job_queue_tx, + ) + .await; + } // _ => {} + } + }, + ) + .await; + // }).await; + } +} diff --git a/tanic-svc/src/lib.rs b/tanic-svc/src/lib.rs index 932316d..8e571ae 100644 --- a/tanic-svc/src/lib.rs +++ b/tanic-svc/src/lib.rs @@ -8,8 +8,8 @@ use tokio::sync::watch::{Receiver as WatchReceiver, Sender as WatchSender}; pub mod iceberg_context; pub mod state; -pub use state::{TanicAction, TanicAppState}; use crate::state::TanicIcebergState; +pub use state::{TanicAction, TanicAppState}; pub struct AppStateManager { action_rx: MpscReceiver, @@ -22,9 +22,7 @@ pub struct AppStateManager { } impl AppStateManager { - pub fn new( - _config: TanicConfig, - ) -> (Self, MpscSender, WatchReceiver<()>) { + pub fn new(_config: TanicConfig) -> (Self, MpscSender, WatchReceiver<()>) { let state = Arc::new(RwLock::new(TanicAppState::default())); let (action_tx, action_rx) = tokio::sync::mpsc::unbounded_channel(); diff --git a/tanic-svc/src/state.rs b/tanic-svc/src/state.rs index fb33998..bd53cba 100644 --- a/tanic-svc/src/state.rs +++ b/tanic-svc/src/state.rs @@ -1,11 +1,11 @@ -use std::collections::HashMap; -use indexmap::IndexMap; use iceberg::spec::{DataFile, Manifest, ManifestList, Snapshot}; use iceberg::table::Table; +use indexmap::IndexMap; use parquet::file::metadata::ParquetMetaData; +use std::collections::HashMap; use tanic_core::config::ConnectionDetails; -const TABLE_SUMMARY_KEY_ROW_COUNT: &str = "row-count"; +const TABLE_SUMMARY_KEY_ROW_COUNT: &str = "total-records"; #[derive(Debug)] pub enum TanicAction { @@ -15,15 +15,45 @@ pub enum TanicAction { // Iceberg metadata update actions UpdateNamespacesList(Vec), - UpdateNamespaceProperties(String, HashMap), + UpdateNamespaceProperties(String, HashMap), UpdateNamespaceTableList(String, Vec), - UpdateTable { namespace: String, table_name: String, table: Table }, - UpdateTableSummary { namespace: String, table_name: String, table_summary: HashMap }, - UpdateTableCurrentSnapshot { namespace: String, table_name: String, snapshot: Snapshot }, - UpdateTableCurrentManifestList { namespace: String, table_name: String, manifest_list: ManifestList }, - UpdateTableManifest { namespace: String, table_name: String, manifest: Manifest, file_path: String, }, - UpdateTableDataFile { namespace: String, table_name: String, data_file: DataFile }, - UpdateTableParquetMetaData { namespace: String, table_name: String, file_path: String, metadata: ParquetMetaData }, + UpdateTable { + namespace: String, + table_name: String, + table: Table, + }, + UpdateTableSummary { + namespace: String, + table_name: String, + table_summary: HashMap, + }, + UpdateTableCurrentSnapshot { + namespace: String, + table_name: String, + snapshot: Snapshot, + }, + UpdateTableCurrentManifestList { + namespace: String, + table_name: String, + manifest_list: Box, + }, + UpdateTableManifest { + namespace: String, + table_name: String, + manifest: Box, + file_path: String, + }, + UpdateTableDataFile { + namespace: String, + table_name: String, + data_file: Box, + }, + UpdateTableParquetMetaData { + namespace: String, + table_name: String, + file_path: String, + metadata: Box, + }, ///// UI Actions /////// FocusPrevNamespace, @@ -70,6 +100,8 @@ pub struct NamespaceDescriptor { pub name: String, properties: Option>, pub tables: Option>, + + pub row_count: Option, } #[derive(Clone, Debug)] @@ -84,13 +116,13 @@ pub struct TableDescriptor { manifests: IndexMap, datafiles: HashMap, parquet_metadata: HashMap, + + row_count: Option, } impl TableDescriptor { pub fn row_count(&self) -> Option { - self.current_snapshot_summary.as_ref().and_then(|summary|summary.get( - TABLE_SUMMARY_KEY_ROW_COUNT - )).and_then(|val| str::parse::(val).ok()) + self.row_count } } @@ -120,85 +152,108 @@ impl TanicAppState { (TanicAction::Exit, _) => { self.iceberg = TanicIcebergState::Exiting; self.ui = TanicUiState::Exiting; - }, + } (TanicAction::ConnectTo(conn_details), _) => { self.iceberg = TanicIcebergState::ConnectingTo(conn_details); self.ui = TanicUiState::SplashScreen; - }, + } (TanicAction::UpdateNamespacesList(namespaces), _) => { - let selected_idx = if namespaces.is_empty() { None } else { - Some(0) - }; - - let namespaces = IndexMap::from_iter( - namespaces.iter().map(|ns|(ns.clone(), NamespaceDescriptor { - name: ns.clone(), - properties: None, - tables: None - })) - ); + let selected_idx = if namespaces.is_empty() { None } else { Some(0) }; + + let namespaces = IndexMap::from_iter(namespaces.iter().map(|ns| { + ( + ns.clone(), + NamespaceDescriptor { + name: ns.clone(), + properties: None, + tables: None, + row_count: None, + }, + ) + })); - self.iceberg = TanicIcebergState::Connected(RetrievedIcebergMetadata { - namespaces, - }); + self.iceberg = + TanicIcebergState::Connected(RetrievedIcebergMetadata { namespaces }); self.ui = TanicUiState::ViewingNamespacesList(ViewingNamespacesListState { - selected_idx + selected_idx, }); - }, + } (TanicAction::UpdateNamespaceProperties(namespace, properties), prev_state) => { let TanicAppState { iceberg, .. } = prev_state; - let TanicIcebergState::Connected(ref mut retrieved_iceberg_metadata ) = iceberg else { + let TanicIcebergState::Connected(ref mut retrieved_iceberg_metadata) = iceberg + else { panic!(); }; - let Some(namespacce_desc) = retrieved_iceberg_metadata.namespaces.get_mut(&namespace) else { + let Some(namespacce_desc) = + retrieved_iceberg_metadata.namespaces.get_mut(&namespace) + else { panic!(); }; namespacce_desc.properties = Some(properties); - }, + } (TanicAction::UpdateNamespaceTableList(namespace, table_names), prev_state) => { let TanicAppState { iceberg, .. } = prev_state; - let TanicIcebergState::Connected(ref mut retrieved_iceberg_metadata ) = iceberg else { + let TanicIcebergState::Connected(ref mut retrieved_iceberg_metadata) = iceberg + else { panic!(); }; - let Some(namespacce_desc) = retrieved_iceberg_metadata.namespaces.get_mut(&namespace) else { + let Some(namespacce_desc) = + retrieved_iceberg_metadata.namespaces.get_mut(&namespace) + else { panic!(); }; - namespacce_desc.tables = Some(IndexMap::from_iter(table_names.into_iter().map( - |name| ( - name.clone(), - TableDescriptor { - name, - namespace: namespace.split(" ").map(|s|s.to_string()).collect::>(), - current_snapshot_summary: None, - table: None, - current_snapshot: None, - current_manifest_list: None, - manifests: IndexMap::default(), - datafiles: HashMap::default(), - parquet_metadata: HashMap::default(), - } - ) - ))) - }, + namespacce_desc.tables = + Some(IndexMap::from_iter(table_names.into_iter().map(|name| { + ( + name.clone(), + TableDescriptor { + name, + namespace: namespace + .split(" ") + .map(|s| s.to_string()) + .collect::>(), + current_snapshot_summary: None, + table: None, + current_snapshot: None, + current_manifest_list: None, + manifests: IndexMap::default(), + datafiles: HashMap::default(), + parquet_metadata: HashMap::default(), - (TanicAction::UpdateTable { namespace, table_name, table }, prev_state) => { + row_count: None, + }, + ) + }))) + } + + ( + TanicAction::UpdateTable { + namespace, + table_name, + table, + }, + prev_state, + ) => { let TanicAppState { iceberg, .. } = prev_state; - let TanicIcebergState::Connected(ref mut retrieved_iceberg_metadata ) = iceberg else { + let TanicIcebergState::Connected(ref mut retrieved_iceberg_metadata) = iceberg + else { panic!(); }; - let Some(namespacce_desc) = retrieved_iceberg_metadata.namespaces.get_mut(&namespace) else { + let Some(namespacce_desc) = + retrieved_iceberg_metadata.namespaces.get_mut(&namespace) + else { panic!(); }; @@ -211,16 +266,26 @@ impl TanicAppState { }; table_desc.table = Some(table); - }, + } - (TanicAction::UpdateTableSummary { namespace, table_name, table_summary }, prev_state) => { + ( + TanicAction::UpdateTableSummary { + namespace, + table_name, + table_summary, + }, + prev_state, + ) => { let TanicAppState { iceberg, .. } = prev_state; - let TanicIcebergState::Connected(ref mut retrieved_iceberg_metadata ) = iceberg else { + let TanicIcebergState::Connected(ref mut retrieved_iceberg_metadata) = iceberg + else { panic!(); }; - let Some(namespacce_desc) = retrieved_iceberg_metadata.namespaces.get_mut(&namespace) else { + let Some(namespacce_desc) = + retrieved_iceberg_metadata.namespaces.get_mut(&namespace) + else { panic!(); }; @@ -232,17 +297,37 @@ impl TanicAppState { panic!(); }; + if let Some(row_count_str) = table_summary.get(TABLE_SUMMARY_KEY_ROW_COUNT) { + if let Ok(row_count) = row_count_str.trim().parse::() { + table_desc.row_count = Some(row_count); + namespacce_desc.row_count = namespacce_desc + .row_count + .map(|rc| rc + row_count) + .or(Some(row_count)); + } + } + table_desc.current_snapshot_summary = Some(table_summary); - }, + } - (TanicAction::UpdateTableCurrentSnapshot { namespace, table_name, snapshot }, prev_state) => { + ( + TanicAction::UpdateTableCurrentSnapshot { + namespace, + table_name, + snapshot, + }, + prev_state, + ) => { let TanicAppState { iceberg, .. } = prev_state; - let TanicIcebergState::Connected(ref mut retrieved_iceberg_metadata ) = iceberg else { + let TanicIcebergState::Connected(ref mut retrieved_iceberg_metadata) = iceberg + else { panic!(); }; - let Some(namespacce_desc) = retrieved_iceberg_metadata.namespaces.get_mut(&namespace) else { + let Some(namespacce_desc) = + retrieved_iceberg_metadata.namespaces.get_mut(&namespace) + else { panic!(); }; @@ -255,16 +340,26 @@ impl TanicAppState { }; table_desc.current_snapshot = Some(snapshot); - }, + } - (TanicAction::UpdateTableCurrentManifestList { namespace, table_name, manifest_list }, prev_state) => { + ( + TanicAction::UpdateTableCurrentManifestList { + namespace, + table_name, + manifest_list, + }, + prev_state, + ) => { let TanicAppState { iceberg, .. } = prev_state; - let TanicIcebergState::Connected(ref mut retrieved_iceberg_metadata ) = iceberg else { + let TanicIcebergState::Connected(ref mut retrieved_iceberg_metadata) = iceberg + else { panic!(); }; - let Some(namespacce_desc) = retrieved_iceberg_metadata.namespaces.get_mut(&namespace) else { + let Some(namespacce_desc) = + retrieved_iceberg_metadata.namespaces.get_mut(&namespace) + else { panic!(); }; @@ -276,17 +371,28 @@ impl TanicAppState { panic!(); }; - table_desc.current_manifest_list = Some(manifest_list); - }, + table_desc.current_manifest_list = Some(*manifest_list); + } - (TanicAction::UpdateTableManifest { namespace, table_name, manifest, file_path: uri }, prev_state) => { + ( + TanicAction::UpdateTableManifest { + namespace, + table_name, + manifest, + file_path: uri, + }, + prev_state, + ) => { let TanicAppState { iceberg, .. } = prev_state; - let TanicIcebergState::Connected(ref mut retrieved_iceberg_metadata ) = iceberg else { + let TanicIcebergState::Connected(ref mut retrieved_iceberg_metadata) = iceberg + else { panic!(); }; - let Some(namespacce_desc) = retrieved_iceberg_metadata.namespaces.get_mut(&namespace) else { + let Some(namespacce_desc) = + retrieved_iceberg_metadata.namespaces.get_mut(&namespace) + else { panic!(); }; @@ -298,20 +404,27 @@ impl TanicAppState { panic!(); }; - table_desc.manifests.insert( - uri, - manifest - ); - }, + table_desc.manifests.insert(uri, *manifest); + } - (TanicAction::UpdateTableDataFile { namespace, table_name, data_file }, prev_state) => { + ( + TanicAction::UpdateTableDataFile { + namespace, + table_name, + data_file, + }, + prev_state, + ) => { let TanicAppState { iceberg, .. } = prev_state; - let TanicIcebergState::Connected(ref mut retrieved_iceberg_metadata ) = iceberg else { + let TanicIcebergState::Connected(ref mut retrieved_iceberg_metadata) = iceberg + else { panic!(); }; - let Some(namespacce_desc) = retrieved_iceberg_metadata.namespaces.get_mut(&namespace) else { + let Some(namespacce_desc) = + retrieved_iceberg_metadata.namespaces.get_mut(&namespace) + else { panic!(); }; @@ -323,20 +436,30 @@ impl TanicAppState { panic!(); }; - table_desc.datafiles.insert( - data_file.file_path().to_string(), - data_file - ); - }, + table_desc + .datafiles + .insert(data_file.file_path().to_string(), *data_file); + } - (TanicAction::UpdateTableParquetMetaData { namespace, table_name, file_path, metadata }, prev_state) => { + ( + TanicAction::UpdateTableParquetMetaData { + namespace, + table_name, + file_path, + metadata, + }, + prev_state, + ) => { let TanicAppState { iceberg, .. } = prev_state; - let TanicIcebergState::Connected(ref mut retrieved_iceberg_metadata ) = iceberg else { + let TanicIcebergState::Connected(ref mut retrieved_iceberg_metadata) = iceberg + else { panic!(); }; - let Some(namespacce_desc) = retrieved_iceberg_metadata.namespaces.get_mut(&namespace) else { + let Some(namespacce_desc) = + retrieved_iceberg_metadata.namespaces.get_mut(&namespace) + else { panic!(); }; @@ -348,65 +471,61 @@ impl TanicAppState { panic!(); }; - table_desc.parquet_metadata.insert( - file_path, - metadata - ); - }, + table_desc.parquet_metadata.insert(file_path, *metadata); + } - ( - TanicAction::FocusPrevNamespace, - prev_state - ) => { + (TanicAction::FocusPrevNamespace, prev_state) => { let TanicAppState { iceberg, ui } = prev_state; - let TanicUiState::ViewingNamespacesList(ref mut viewing_namespaces_list_state ) = ui else { + let TanicUiState::ViewingNamespacesList(ref mut viewing_namespaces_list_state) = ui + else { panic!(); }; - let TanicIcebergState::Connected(ref mut retrieved_iceberg_metadata ) = iceberg else { + let TanicIcebergState::Connected(ref mut retrieved_iceberg_metadata) = iceberg + else { panic!(); }; - viewing_namespaces_list_state.selected_idx = viewing_namespaces_list_state.selected_idx.map(|selected_idx| { - if selected_idx == 0 { - retrieved_iceberg_metadata.namespaces.len() - 1 - } else { - selected_idx - 1 - } - }); + viewing_namespaces_list_state.selected_idx = viewing_namespaces_list_state + .selected_idx + .map(|selected_idx| { + if selected_idx == 0 { + retrieved_iceberg_metadata.namespaces.len() - 1 + } else { + selected_idx - 1 + } + }); } - ( - TanicAction::FocusNextNamespace, - prev_state - ) => { + (TanicAction::FocusNextNamespace, prev_state) => { let TanicAppState { iceberg, ui } = prev_state; - let TanicUiState::ViewingNamespacesList(ref mut viewing_namespaces_list_state ) = ui else { + let TanicUiState::ViewingNamespacesList(ref mut viewing_namespaces_list_state) = ui + else { panic!(); }; - let TanicIcebergState::Connected(ref mut retrieved_iceberg_metadata ) = iceberg else { + let TanicIcebergState::Connected(ref mut retrieved_iceberg_metadata) = iceberg + else { panic!(); }; - viewing_namespaces_list_state.selected_idx = viewing_namespaces_list_state.selected_idx.map(|selected_idx| { - if selected_idx == retrieved_iceberg_metadata.namespaces.len() - 1 { - 0 - } else { - selected_idx + 1 - } - }); - }, + viewing_namespaces_list_state.selected_idx = viewing_namespaces_list_state + .selected_idx + .map(|selected_idx| { + if selected_idx == retrieved_iceberg_metadata.namespaces.len() - 1 { + 0 + } else { + selected_idx + 1 + } + }); + } - ( - TanicAction::SelectNamespace, - prev_state, - ) => { + (TanicAction::SelectNamespace, prev_state) => { let TanicAppState { ui, .. } = prev_state; - let TanicUiState::ViewingNamespacesList(namespaces ) = ui else { + let TanicUiState::ViewingNamespacesList(namespaces) = ui else { panic!(); }; @@ -414,104 +533,115 @@ impl TanicAppState { namespaces: namespaces.clone(), selected_idx: None, }); - }, + } - ( - TanicAction::FocusPrevTable, - prev_state, - ) => { + (TanicAction::FocusPrevTable, prev_state) => { let TanicAppState { iceberg, ui } = prev_state; - let TanicIcebergState::Connected(ref mut retrieved_iceberg_metadata ) = iceberg else { + let TanicIcebergState::Connected(ref mut retrieved_iceberg_metadata) = iceberg + else { panic!(); }; - let TanicUiState::ViewingTablesList(ref mut viewing_tables_list_state ) = ui else { + let TanicUiState::ViewingTablesList(ref mut viewing_tables_list_state) = ui else { panic!(); }; - let Some(namespace_selected_idx) = viewing_tables_list_state.namespaces.selected_idx else { + let Some(namespace_selected_idx) = + viewing_tables_list_state.namespaces.selected_idx + else { panic!(); }; - let Some(&namespace_selected_name) = retrieved_iceberg_metadata.namespaces.keys().collect::>().get(namespace_selected_idx) else { + let Some(&namespace_selected_name) = retrieved_iceberg_metadata + .namespaces + .keys() + .collect::>() + .get(namespace_selected_idx) + else { panic!(); }; - let Some(namespace) = retrieved_iceberg_metadata.namespaces.get(namespace_selected_name) else { + let Some(namespace) = retrieved_iceberg_metadata + .namespaces + .get(namespace_selected_name) + else { panic!(); }; if let Some(ref table_list) = namespace.tables { let table_list_len = table_list.len(); - viewing_tables_list_state.selected_idx = viewing_tables_list_state.selected_idx.map(|selected_idx| { - if selected_idx == 0 { - table_list_len - 1 - } else { - selected_idx - 1 - } - }); + viewing_tables_list_state.selected_idx = + viewing_tables_list_state.selected_idx.map(|selected_idx| { + if selected_idx == 0 { + table_list_len - 1 + } else { + selected_idx - 1 + } + }); } - }, + } - ( - TanicAction::FocusNextTable, - prev_state, - ) => { + (TanicAction::FocusNextTable, prev_state) => { let TanicAppState { iceberg, ui } = prev_state; - let TanicIcebergState::Connected(ref mut retrieved_iceberg_metadata ) = iceberg else { + let TanicIcebergState::Connected(ref mut retrieved_iceberg_metadata) = iceberg + else { panic!(); }; - let TanicUiState::ViewingTablesList(ref mut viewing_tables_list_state ) = ui else { + let TanicUiState::ViewingTablesList(ref mut viewing_tables_list_state) = ui else { panic!(); }; - let Some(namespace_selected_idx) = viewing_tables_list_state.namespaces.selected_idx else { + let Some(namespace_selected_idx) = + viewing_tables_list_state.namespaces.selected_idx + else { panic!(); }; - let Some(&namespace_selected_name) = retrieved_iceberg_metadata.namespaces.keys().collect::>().get(namespace_selected_idx) else { + let Some(&namespace_selected_name) = retrieved_iceberg_metadata + .namespaces + .keys() + .collect::>() + .get(namespace_selected_idx) + else { panic!(); }; - let Some(namespace) = retrieved_iceberg_metadata.namespaces.get(namespace_selected_name) else { + let Some(namespace) = retrieved_iceberg_metadata + .namespaces + .get(namespace_selected_name) + else { panic!(); }; if let Some(ref table_list) = namespace.tables { - viewing_tables_list_state.selected_idx = viewing_tables_list_state.selected_idx.map(|selected_idx| { - if selected_idx == table_list.len() - 1 { - 0 - } else { - selected_idx + 1 - } - }); + viewing_tables_list_state.selected_idx = + viewing_tables_list_state.selected_idx.map(|selected_idx| { + if selected_idx == table_list.len() - 1 { + 0 + } else { + selected_idx + 1 + } + }); } - }, + } - ( - TanicAction::Escape, - TanicAppState { - ui, - .. - } - ) => { + (TanicAction::Escape, TanicAppState { ui, .. }) => { + #[allow(clippy::single_match)] // remove once more than one match below match ui { TanicUiState::ViewingTablesList(ViewingTablesListState { - namespaces, - .. - }) => { - self.ui = TanicUiState::ViewingNamespacesList(namespaces.clone()) - } + namespaces, .. + }) => self.ui = TanicUiState::ViewingNamespacesList(namespaces.clone()), + + // TODO: Escape from Partition and DataFile _ => {} } } // TODO: - // * Escape // * SelectTable // * FocusNextPartition // * FocusPrevPartition, @@ -519,10 +649,9 @@ impl TanicAppState { // * FocusNextDataFile, // * FocusPrevDataFile, // * SelectDataFile - _ => { unimplemented!() - }, + } } self diff --git a/tanic-tui/lib.rs b/tanic-tui/lib.rs index 4cb4851..ac8dbde 100644 --- a/tanic-tui/lib.rs +++ b/tanic-tui/lib.rs @@ -1,15 +1,15 @@ -use std::sync::Arc; use crossterm::event::{Event, EventStream}; use ratatui::Frame; -use tokio::sync::mpsc::UnboundedSender as MpscSender; +use std::sync::Arc; use std::sync::RwLock; +use tokio::sync::mpsc::UnboundedSender as MpscSender; use tokio::sync::watch::Receiver as WatchReceiver; use tokio_stream::{wrappers::WatchStream, StreamExt}; use crate::ui_components::app_container::AppContainer; use tanic_core::{Result, TanicError}; -use tanic_svc::{TanicAction, TanicAppState}; use tanic_svc::state::TanicUiState; +use tanic_svc::{TanicAction, TanicAppState}; mod component; mod ui_components; @@ -23,7 +23,11 @@ impl TanicTui { Self { action_tx } } - pub async fn event_loop(self, state_rx: WatchReceiver<()>, state: Arc>) -> Result<()> { + pub async fn event_loop( + self, + state_rx: WatchReceiver<()>, + state: Arc>, + ) -> Result<()> { let mut terminal = ratatui::init(); let mut term_event_stream = EventStream::new(); let mut state_stream = WatchStream::new(state_rx); diff --git a/tanic-tui/ui_components/app_container.rs b/tanic-tui/ui_components/app_container.rs index 97a4d0b..f9e871a 100644 --- a/tanic-tui/ui_components/app_container.rs +++ b/tanic-tui/ui_components/app_container.rs @@ -1,4 +1,3 @@ -use std::sync::{Arc, RwLock}; use crate::component::Component; use crate::ui_components::{ namespace_list_view::NamespaceListView, splash_screen::SplashScreen, @@ -9,9 +8,10 @@ use ratatui::buffer::Buffer; use ratatui::layout::{Constraint, Layout, Rect}; use ratatui::prelude::{Color, Style, Widget}; use ratatui::widgets::Block; +use std::sync::{Arc, RwLock}; +use tanic_svc::state::TanicUiState; use tanic_svc::{TanicAction, TanicAppState}; use tui_logger::{LevelFilter, TuiLoggerLevelOutput, TuiLoggerWidget, TuiWidgetState}; -use tanic_svc::state::TanicUiState; pub(crate) struct AppContainer { state: Arc>, @@ -52,14 +52,15 @@ impl AppContainer { } _ => None, } - }, + } } } } impl Widget for &AppContainer { fn render(self, area: Rect, buf: &mut Buffer) { - let [top, bottom] = Layout::vertical([Constraint::Fill(1), Constraint::Max(6)]).areas(area); + let [top, bottom] = + Layout::vertical([Constraint::Fill(1), Constraint::Max(10)]).areas(area); let filter_state = TuiWidgetState::new() .set_default_display_level(LevelFilter::Info) @@ -82,8 +83,7 @@ impl Widget for &AppContainer { TanicUiState::SplashScreen => self.splash_screen.render(top, buf), TanicUiState::ViewingNamespacesList(_) => (&self.namespace_list_view).render(top, buf), TanicUiState::ViewingTablesList(_) => (&self.table_list_view).render(top, buf), - TanicUiState::Exiting => {} - // _ => {} + TanicUiState::Exiting => {} // _ => {} } } } diff --git a/tanic-tui/ui_components/namespace_list_item.rs b/tanic-tui/ui_components/namespace_list_item.rs index 5ec291e..2ec8649 100644 --- a/tanic-tui/ui_components/namespace_list_item.rs +++ b/tanic-tui/ui_components/namespace_list_item.rs @@ -28,11 +28,20 @@ impl Component for &NamespaceListItem<'_> { let name = self.ns.name.clone(); let tables = &self.ns.tables; - let table_count = tables.as_ref().map(|t|t.len()).unwrap_or(0); + let table_count = tables.as_ref().map(|t| t.len()).unwrap_or(0); let plural_suffix = if table_count == 1 { "" } else { "s" }; + + let row_count = self.ns.row_count.unwrap_or(0); + let row_plural_suffix = if row_count == 1 { "" } else { "s" }; + let name = format!( - "{} {} ({} table{})", - NERD_FONT_ICON_TABLE_FOLDER, name, table_count, plural_suffix + "{} {} ({} table{}, {} row{})", + NERD_FONT_ICON_TABLE_FOLDER, + name, + table_count, + plural_suffix, + row_count, + row_plural_suffix ); let para_rect = Rect::new( diff --git a/tanic-tui/ui_components/namespace_list_view.rs b/tanic-tui/ui_components/namespace_list_view.rs index 0cc3933..7058300 100644 --- a/tanic-tui/ui_components/namespace_list_view.rs +++ b/tanic-tui/ui_components/namespace_list_view.rs @@ -1,14 +1,14 @@ -use std::sync::{Arc, RwLock}; use crossterm::event::{KeyCode, KeyEvent}; use ratatui::prelude::*; use ratatui::symbols::border; use ratatui::widgets::Block; +use std::sync::{Arc, RwLock}; use crate::component::Component; use crate::ui_components::namespace_list_item::NamespaceListItem; use crate::ui_components::treemap_layout::TreeMapLayout; -use tanic_svc::{TanicAction, TanicAppState}; use tanic_svc::state::{TanicIcebergState, TanicUiState}; +use tanic_svc::{TanicAction, TanicAppState}; pub(crate) struct NamespaceListView { state: Arc>, @@ -43,7 +43,7 @@ impl Component for &NamespaceListView { .iter() .map(|item| { let tables = &item.ns.tables; - let table_count = tables.as_ref().map(|t|t.len()).unwrap_or(0); + let table_count = tables.as_ref().map(|t| t.len()).unwrap_or(0); (item, table_count) }) diff --git a/tanic-tui/ui_components/splash_screen.rs b/tanic-tui/ui_components/splash_screen.rs index c04378f..ac12312 100644 --- a/tanic-tui/ui_components/splash_screen.rs +++ b/tanic-tui/ui_components/splash_screen.rs @@ -1,7 +1,7 @@ -use std::sync::{Arc, RwLock}; use ratatui::prelude::*; use ratatui::symbols::border; use ratatui::widgets::{Block, Paragraph}; +use std::sync::{Arc, RwLock}; use tanic_svc::TanicAppState; pub(crate) struct SplashScreen { diff --git a/tanic-tui/ui_components/table_list_item.rs b/tanic-tui/ui_components/table_list_item.rs index eb0c320..5076d26 100644 --- a/tanic-tui/ui_components/table_list_item.rs +++ b/tanic-tui/ui_components/table_list_item.rs @@ -31,13 +31,10 @@ impl Component for &TableListItem<'_> { let row_count_str = match self.table.row_count() { None => "".to_string(), Some(1) => " (1 row)".to_string(), - Some(n) => format!(" ({n} rows)") + Some(n) => format!(" ({n} rows)"), }; - let name = format!( - "{} {}{}", - NERD_FONT_ICON_TABLE, name, row_count_str - ); + let name = format!("{} {}{}", NERD_FONT_ICON_TABLE, name, row_count_str); let para_rect = Rect::new( block_inner.x, diff --git a/tanic-tui/ui_components/table_list_view.rs b/tanic-tui/ui_components/table_list_view.rs index 34c5961..2091c3c 100644 --- a/tanic-tui/ui_components/table_list_view.rs +++ b/tanic-tui/ui_components/table_list_view.rs @@ -1,14 +1,16 @@ -use std::sync::{Arc, RwLock}; use crossterm::event::{KeyCode, KeyEvent}; use ratatui::prelude::*; use ratatui::symbols::border; use ratatui::widgets::Block; +use std::sync::{Arc, RwLock}; use crate::component::Component; use crate::ui_components::table_list_item::TableListItem; use crate::ui_components::treemap_layout::TreeMapLayout; +use tanic_svc::state::{ + RetrievedIcebergMetadata, TanicIcebergState, TanicUiState, ViewingTablesListState, +}; use tanic_svc::{TanicAction, TanicAppState}; -use tanic_svc::state::{RetrievedIcebergMetadata, TanicIcebergState, TanicUiState, ViewingTablesListState}; pub(crate) struct TableListView { state: Arc>, @@ -45,9 +47,12 @@ impl Component for &TableListView { let block = Block::bordered() .title(format!( " Tanic //// {} Namespace ", - view_state.namespaces.selected_idx.and_then( - |idx| iceberg_state.namespaces.get_index(idx) - ).map(|(k, _)|k.to_string()).unwrap_or("???".to_string()) + view_state + .namespaces + .selected_idx + .and_then(|idx| iceberg_state.namespaces.get_index(idx)) + .map(|(k, _)| k.to_string()) + .unwrap_or("???".to_string()) )) .border_set(border::PLAIN); let block_inner_area = block.inner(area); @@ -67,12 +72,16 @@ impl Component for &TableListView { } impl TableListView { - fn get_items<'a>(iceberg_state: &'a RetrievedIcebergMetadata, view_state: &'a ViewingTablesListState) -> Vec> { - let Some(ref selected_namespace) = view_state.selected_idx else { + fn get_items<'a>( + iceberg_state: &'a RetrievedIcebergMetadata, + view_state: &'a ViewingTablesListState, + ) -> Vec> { + let Some(ref selected_namespace) = view_state.namespaces.selected_idx else { return vec![]; }; - let Some((_, namespace_desc)) = iceberg_state.namespaces.get_index(*selected_namespace) else { + let Some((_, namespace_desc)) = iceberg_state.namespaces.get_index(*selected_namespace) + else { return vec![]; }; @@ -83,9 +92,7 @@ impl TableListView { let items = tables .iter() .enumerate() - .map(|(idx, (_, ns))| { - TableListItem::new(ns, Some(idx) == view_state.selected_idx) - }) + .map(|(idx, (_, ns))| TableListItem::new(ns, Some(idx) == view_state.selected_idx)) .collect::>(); items diff --git a/tanic/Cargo.toml b/tanic/Cargo.toml index 4f92be0..7674c1e 100644 --- a/tanic/Cargo.toml +++ b/tanic/Cargo.toml @@ -35,3 +35,4 @@ tokio = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true } tui-logger = { version = "0.14.1", features = ["tracing-support"] } +console-subscriber = "0.4.1" diff --git a/tanic/src/args.rs b/tanic/src/args.rs index 7fc5595..af3b344 100644 --- a/tanic/src/args.rs +++ b/tanic/src/args.rs @@ -6,4 +6,7 @@ use http::Uri; pub struct Args { /// URI of an Iceberg Catalog to connect to pub catalogue_uri: Option, + + #[clap(long, default_value_t = false)] + pub no_ui: bool, } diff --git a/tanic/src/logging.rs b/tanic/src/logging.rs index 56ed5ab..6b30d4f 100644 --- a/tanic/src/logging.rs +++ b/tanic/src/logging.rs @@ -18,9 +18,18 @@ pub(crate) fn init() { .expect("Unable to set global subscriber"); } -pub(crate) fn init_tui_logger() { - tracing_subscriber::registry() - .with(tui_logger::tracing_subscriber_layer()) - .init(); - tui_logger::init_logger(tui_logger::LevelFilter::Trace).expect("Could not initialize logger"); +pub(crate) fn init_tui_logger(no_ui: bool) { + if no_ui { + tracing_subscriber::registry() + .with(tracing_subscriber::fmt::layer().pretty()) + .init(); + } else { + tracing_subscriber::registry() + // .with(console_subscriber::spawn()) + .with(tui_logger::tracing_subscriber_layer()) + .init(); + + tui_logger::init_logger(tui_logger::LevelFilter::Trace) + .expect("Could not initialize logger"); + } } diff --git a/tanic/src/main.rs b/tanic/src/main.rs index d02aa0f..d416310 100644 --- a/tanic/src/main.rs +++ b/tanic/src/main.rs @@ -14,21 +14,15 @@ mod logging; #[tokio::main] async fn main() -> Result<()> { - logging::init_tui_logger(); - let args = Args::try_parse().into_diagnostic()?; + + logging::init_tui_logger(args.no_ui); + let config = TanicConfig::load().into_diagnostic()?; tracing::info!(?config, "loaded config"); let (app_state, action_tx, state_rx) = AppStateManager::new(config); - let ui_task = tokio::spawn({ - let tanic_tui = TanicTui::new(action_tx.clone()); - let state_rx = state_rx.clone(); - let app_state = app_state.get_state(); - async move { tanic_tui.event_loop(state_rx, app_state).await } - }); - let iceberg_task = tokio::spawn({ let state_rx = state_rx.clone(); let app_state = app_state.get_state(); @@ -36,6 +30,7 @@ async fn main() -> Result<()> { async move { iceberg_ctx_mgr.event_loop(state_rx).await } }); + let ui_app_state = app_state.get_state(); let svc_task = tokio::spawn(async move { app_state.event_loop().await }); if let Some(ref uri) = args.catalogue_uri { @@ -45,9 +40,22 @@ async fn main() -> Result<()> { action_tx.send(message).into_diagnostic()?; } - tokio::select! { - _ = ui_task => Ok(()), - _ = svc_task => Ok(()), - _ = iceberg_task => Ok(()), + if args.no_ui { + tokio::select! { + _ = svc_task => Ok(()), + _ = iceberg_task => Ok(()), + } + } else { + let ui_task = tokio::spawn({ + let tanic_tui = TanicTui::new(action_tx.clone()); + let state_rx = state_rx.clone(); + async move { tanic_tui.event_loop(state_rx, ui_app_state).await } + }); + + tokio::select! { + _ = ui_task => Ok(()), + _ = svc_task => Ok(()), + _ = iceberg_task => Ok(()), + } } } From c82e13f7699baabfea567782afd165139688b819 Mon Sep 17 00:00:00 2001 From: Scott Donnelly Date: Mon, 17 Feb 2025 08:40:45 +0000 Subject: [PATCH 6/7] feat(wip): added debug logging to track down deadlock. Fixed deadlock by using spawn_bloocking for ui task --- Cargo.toml | 2 +- tanic-svc/src/iceberg_context.rs | 66 +++++++++++++++++-- tanic-svc/src/lib.rs | 4 ++ tanic-svc/src/state.rs | 32 ++++++++- tanic-tui/lib.rs | 3 + tanic-tui/ui_components/app_container.rs | 46 ++++++++++--- .../ui_components/namespace_list_view.rs | 47 ++++++++----- tanic-tui/ui_components/table_list_view.rs | 2 + tanic/Cargo.toml | 2 +- tanic/src/logging.rs | 21 +++++- tanic/src/main.rs | 11 ++-- 11 files changed, 193 insertions(+), 43 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3248f10..7a98da9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,4 +31,4 @@ terminal_size = "0.4" thiserror = "2" tokio = "1" tracing = "0.1" -tracing-subscriber = { version = "0.3", features = ["env-filter"] } +tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] } diff --git a/tanic-svc/src/iceberg_context.rs b/tanic-svc/src/iceberg_context.rs index 742d40a..521a4c1 100644 --- a/tanic-svc/src/iceberg_context.rs +++ b/tanic-svc/src/iceberg_context.rs @@ -71,11 +71,18 @@ impl IcebergContextManager { let action_tx = self.action_tx.clone(); let job_queue_tx = job_queue_tx.clone(); let iceberg_ctx = self.iceberg_context.clone(); - async move { Self::job_handler(job_queue_rx, job_queue_tx, action_tx, iceberg_ctx).await } + async move { + tracing::debug!("await job_handler()"); + Self::job_handler(job_queue_rx, job_queue_tx, action_tx, iceberg_ctx).await + } }); - while state_stream.next().await.is_some() { + tracing::debug!("await state_stream.next()"); + let mut next_item = state_stream.next().await; + tracing::debug!("await state_stream.next() complete"); + while next_item.is_some() { let new_conn_details = { + tracing::debug!("self.state_ref.read()"); let state = self.state_ref.read().unwrap(); match &state.iceberg { @@ -88,14 +95,23 @@ impl IcebergContextManager { _ => None, } }; + tracing::debug!("self.state_ref.read() done"); if let Some(new_conn_details) = new_conn_details { + tracing::debug!("await self.connect_to()"); self.connect_to(&new_conn_details, job_queue_tx.clone()) .await?; + tracing::debug!("await self.connect_to() done"); // begin crawl + tracing::debug!("await job_queue_tx.send()"); let _ = job_queue_tx.send(IcebergTask::Namespaces).await; + tracing::debug!("await job_queue_tx.send() done"); } + + tracing::debug!("await state_stream.next()"); + next_item = state_stream.next().await; + tracing::debug!("await state_stream.next() complete"); } Ok(()) @@ -107,6 +123,7 @@ impl IcebergContextManager { _job_queue_tx: MpscSender, ) -> Result<()> { { + tracing::debug!("self.iceberg_context.read()"); let ctx = self.iceberg_context.read().unwrap(); if let Some(ref existing_conn_details) = ctx.connection_details { if new_conn_details == existing_conn_details { @@ -115,11 +132,14 @@ impl IcebergContextManager { } } } + tracing::debug!("self.iceberg_context.read() done"); { + tracing::debug!("self.iceberg_context.write()"); let mut ctx = self.iceberg_context.write().unwrap(); ctx.connect_to(new_conn_details); } + tracing::debug!("self.iceberg_context.write() done"); Ok(()) } @@ -131,6 +151,7 @@ impl IcebergContextManager { ) -> Result<()> { let root_namespaces = { let catalog = { + tracing::debug!("ctx.read()"); let r_ctx = ctx.read().unwrap(); let Some(ref catalog) = r_ctx.catalog else { @@ -141,8 +162,13 @@ impl IcebergContextManager { catalog.clone() }; + tracing::debug!("ctx.read() done"); + + tracing::debug!("catalog.list_namespaces(None).await"); + let res = catalog.list_namespaces(None).await?; + tracing::debug!("catalog.list_namespaces(None).await done"); - catalog.list_namespaces(None).await? + res }; let namespaces = root_namespaces @@ -152,8 +178,10 @@ impl IcebergContextManager { { let namespaces = namespaces.clone(); + tracing::debug!("ctx.write()"); ctx.write().unwrap().namespaces = namespaces; } + tracing::debug!("ctx.write() done"); action_tx .send(TanicAction::UpdateNamespacesList( @@ -165,9 +193,11 @@ impl IcebergContextManager { .map_err(|err| TanicError::UnexpectedError(err.to_string()))?; for namespace in namespaces { + tracing::debug!("job_queue_tx.send await"); let _ = job_queue_tx .send(IcebergTask::TablesForNamespace(namespace.clone())) .await; + tracing::debug!("job_queue_tx.send await sone"); } Ok(()) @@ -182,6 +212,7 @@ impl IcebergContextManager { let namespace_ident = NamespaceIdent::from_strs(namespace.parts.clone())?; let tables = { let catalog = { + tracing::debug!("ctx.read()"); let r_ctx = ctx.read().unwrap(); let Some(ref catalog) = r_ctx.catalog else { @@ -192,8 +223,13 @@ impl IcebergContextManager { catalog.clone() }; + tracing::debug!("ctx.read() done"); - catalog.list_tables(&namespace_ident).await? + tracing::debug!("catalog.list_tables(&namespace_ident).await"); + let res = catalog.list_tables(&namespace_ident).await?; + tracing::debug!("catalog.list_tables(&namespace_ident).await done"); + + res }; let tables = tables @@ -207,8 +243,10 @@ impl IcebergContextManager { { let tables = tables.clone(); + tracing::debug!("ctx.write()"); ctx.write().unwrap().tables = tables; } + tracing::debug!("ctx.write() done"); action_tx .send(TanicAction::UpdateNamespaceTableList( @@ -218,9 +256,12 @@ impl IcebergContextManager { .map_err(TanicError::unexpected)?; for table in tables { + tracing::debug!("job_queue_tx.send await"); + tracing::info!(?table, "sending SummaryForTable"); let _ = job_queue_tx .send(IcebergTask::SummaryForTable(table.clone())) .await; + tracing::debug!("job_queue_tx.send await done"); } Ok(()) @@ -237,6 +278,7 @@ impl IcebergContextManager { let loaded_table = { let catalog = { + tracing::debug!("ctx.read()"); let r_ctx = ctx.read().unwrap(); let Some(ref catalog) = r_ctx.catalog else { @@ -247,8 +289,13 @@ impl IcebergContextManager { catalog.clone() }; + tracing::debug!("ctx.read() done"); + + tracing::debug!("catalog.load_table(&table_ident).await"); + let res = catalog.load_table(&table_ident).await?; + tracing::debug!("catalog.load_table(&table_ident).await done"); - catalog.load_table(&table_ident).await? + res }; let summary = loaded_table @@ -256,7 +303,7 @@ impl IcebergContextManager { .current_snapshot() .unwrap() .summary(); - tracing::info!(?summary); + // tracing::info!(?summary); action_tx .send(TanicAction::UpdateTableSummary { @@ -295,7 +342,6 @@ impl IcebergContextManager { ) { let job_stream = ReceiverStream::new(job_queue_rx); - // let _ = tokio::spawn(async move { job_stream .map(|task| { ( @@ -310,15 +356,18 @@ impl IcebergContextManager { async move |(task, iceberg_ctx, action_tx, job_queue_tx)| { match task { IcebergTask::Namespaces => { + tracing::debug!("populate_namespaces.await"); let _ = IcebergContextManager::populate_namespaces( iceberg_ctx, action_tx, job_queue_tx, ) .await; + tracing::debug!("populate_namespaces.await done"); } IcebergTask::TablesForNamespace(namespace) => { + tracing::debug!("populate_tables.await"); let _ = IcebergContextManager::populate_tables( iceberg_ctx, action_tx, @@ -326,9 +375,11 @@ impl IcebergContextManager { job_queue_tx, ) .await; + tracing::debug!("populate_tables.await done"); } IcebergTask::SummaryForTable(table) => { + tracing::debug!("populate_table_summary.await"); let _ = IcebergContextManager::populate_table_summary( iceberg_ctx, action_tx, @@ -336,6 +387,7 @@ impl IcebergContextManager { job_queue_tx, ) .await; + tracing::debug!("populate_table_summary.await done"); } // _ => {} } }, diff --git a/tanic-svc/src/lib.rs b/tanic-svc/src/lib.rs index 8e571ae..36ec63a 100644 --- a/tanic-svc/src/lib.rs +++ b/tanic-svc/src/lib.rs @@ -53,15 +53,19 @@ impl AppStateManager { } = self; while !matches!(state.read().unwrap().iceberg, TanicIcebergState::Exiting) { + tracing::debug!("await action_rx.recv()"); let Some(action) = action_rx.recv().await else { break; }; + tracing::debug!("await action_rx.recv() complete"); tracing::info!(?action, "AppState received an action"); { + tracing::debug!("state.write()"); let mut mut_state = state.write().unwrap(); *mut_state = mut_state.clone().update(action); } + tracing::debug!("state.write() done"); state_tx .send(()) diff --git a/tanic-svc/src/state.rs b/tanic-svc/src/state.rs index bd53cba..f0fb348 100644 --- a/tanic-svc/src/state.rs +++ b/tanic-svc/src/state.rs @@ -186,12 +186,14 @@ impl TanicAppState { let TanicIcebergState::Connected(ref mut retrieved_iceberg_metadata) = iceberg else { + tracing::error!("panic! not connected"); panic!(); }; let Some(namespacce_desc) = retrieved_iceberg_metadata.namespaces.get_mut(&namespace) else { + tracing::error!("panic! namespace not found"); panic!(); }; @@ -203,12 +205,14 @@ impl TanicAppState { let TanicIcebergState::Connected(ref mut retrieved_iceberg_metadata) = iceberg else { + tracing::error!("panic! not connected"); panic!(); }; let Some(namespacce_desc) = retrieved_iceberg_metadata.namespaces.get_mut(&namespace) else { + tracing::error!("panic! namepsace not found"); panic!(); }; @@ -248,20 +252,24 @@ impl TanicAppState { let TanicIcebergState::Connected(ref mut retrieved_iceberg_metadata) = iceberg else { + tracing::error!("panic! not connected"); panic!(); }; let Some(namespacce_desc) = retrieved_iceberg_metadata.namespaces.get_mut(&namespace) else { + tracing::error!("panic! ns not found"); panic!(); }; let Some(ref mut table_desc) = namespacce_desc.tables else { + tracing::error!("panic! tables not found"); panic!(); }; let Some(table_desc) = table_desc.get_mut(&table_name) else { + tracing::error!("panic! table desc not found"); panic!(); }; @@ -280,27 +288,36 @@ impl TanicAppState { let TanicIcebergState::Connected(ref mut retrieved_iceberg_metadata) = iceberg else { + tracing::error!("panic! not connected"); panic!(); }; - let Some(namespacce_desc) = + let Some(namespace_desc) = retrieved_iceberg_metadata.namespaces.get_mut(&namespace) else { + tracing::error!("panic! ns not found"); panic!(); }; - let Some(ref mut table_desc) = namespacce_desc.tables else { + let Some(ref mut table_desc) = namespace_desc.tables else { + tracing::error!("panic! tables not found"); panic!(); }; let Some(table_desc) = table_desc.get_mut(&table_name) else { + tracing::error!("panic! table desc not found"); panic!(); }; if let Some(row_count_str) = table_summary.get(TABLE_SUMMARY_KEY_ROW_COUNT) { if let Ok(row_count) = row_count_str.trim().parse::() { table_desc.row_count = Some(row_count); - namespacce_desc.row_count = namespacce_desc + tracing::info!( + table_row_count = row_count, + orig_ns_rows = namespace_desc.row_count, + "Bumping NS row count" + ); + namespace_desc.row_count = namespace_desc .row_count .map(|rc| rc + row_count) .or(Some(row_count)); @@ -322,20 +339,24 @@ impl TanicAppState { let TanicIcebergState::Connected(ref mut retrieved_iceberg_metadata) = iceberg else { + tracing::error!("panic! not connected"); panic!(); }; let Some(namespacce_desc) = retrieved_iceberg_metadata.namespaces.get_mut(&namespace) else { + tracing::error!("panic! ns not found"); panic!(); }; let Some(ref mut table_desc) = namespacce_desc.tables else { + tracing::error!("panic! tble desc not found"); panic!(); }; let Some(table_desc) = table_desc.get_mut(&table_name) else { + tracing::error!("panic! table not found"); panic!(); }; @@ -354,20 +375,24 @@ impl TanicAppState { let TanicIcebergState::Connected(ref mut retrieved_iceberg_metadata) = iceberg else { + tracing::error!("panic! not connected"); panic!(); }; let Some(namespacce_desc) = retrieved_iceberg_metadata.namespaces.get_mut(&namespace) else { + tracing::error!("panic!ns not found"); panic!(); }; let Some(ref mut table_desc) = namespacce_desc.tables else { + tracing::error!("panic! tables not found"); panic!(); }; let Some(table_desc) = table_desc.get_mut(&table_name) else { + tracing::error!("panic! table not found"); panic!(); }; @@ -387,6 +412,7 @@ impl TanicAppState { let TanicIcebergState::Connected(ref mut retrieved_iceberg_metadata) = iceberg else { + tracing::error!("panic! not connected"); panic!(); }; diff --git a/tanic-tui/lib.rs b/tanic-tui/lib.rs index ac8dbde..803e3ac 100644 --- a/tanic-tui/lib.rs +++ b/tanic-tui/lib.rs @@ -33,6 +33,7 @@ impl TanicTui { let mut state_stream = WatchStream::new(state_rx); let Some(_) = state_stream.next().await else { + tracing::debug!("state_stream.next().await done"); return Ok(()); }; @@ -40,7 +41,9 @@ impl TanicTui { loop { { + tracing::debug!("state.read"); let state = state.read().unwrap(); + tracing::debug!("state.read done"); if matches!(state.ui, TanicUiState::Exiting) { break; } diff --git a/tanic-tui/ui_components/app_container.rs b/tanic-tui/ui_components/app_container.rs index f9e871a..3b09fb8 100644 --- a/tanic-tui/ui_components/app_container.rs +++ b/tanic-tui/ui_components/app_container.rs @@ -8,7 +8,7 @@ use ratatui::buffer::Buffer; use ratatui::layout::{Constraint, Layout, Rect}; use ratatui::prelude::{Color, Style, Widget}; use ratatui::widgets::Block; -use std::sync::{Arc, RwLock}; +use std::sync::{Arc, RwLock, TryLockError}; use tanic_svc::state::TanicUiState; use tanic_svc::{TanicAction, TanicAppState}; use tui_logger::{LevelFilter, TuiLoggerLevelOutput, TuiLoggerWidget, TuiWidgetState}; @@ -42,7 +42,16 @@ impl AppContainer { } key_event => { - let state = self.state.read().unwrap(); + tracing::debug!("key_event self.state.read"); + let state = match self.state.read() { + Ok(state) => state, + Err(err) => { + tracing::error!(?err, %err, "poison ☠"); + panic!(); + } + }; + tracing::debug!("key_event self.state.read done"); + match state.ui { TanicUiState::ViewingNamespacesList(_) => { (&self.namespace_list_view).handle_key_event(key_event) @@ -60,7 +69,7 @@ impl AppContainer { impl Widget for &AppContainer { fn render(self, area: Rect, buf: &mut Buffer) { let [top, bottom] = - Layout::vertical([Constraint::Fill(1), Constraint::Max(10)]).areas(area); + Layout::vertical([Constraint::Fill(1), Constraint::Max(20)]).areas(area); let filter_state = TuiWidgetState::new() .set_default_display_level(LevelFilter::Info) @@ -78,12 +87,31 @@ impl Widget for &AppContainer { .state(&filter_state) .render(bottom, buf); - let state = self.state.read().unwrap(); - match state.ui { - TanicUiState::SplashScreen => self.splash_screen.render(top, buf), - TanicUiState::ViewingNamespacesList(_) => (&self.namespace_list_view).render(top, buf), - TanicUiState::ViewingTablesList(_) => (&self.table_list_view).render(top, buf), - TanicUiState::Exiting => {} // _ => {} + { + tracing::debug!("render self.state.read"); + let state = match self.state.try_read() { + Ok(state) => state, + Err(TryLockError::Poisoned(err)) => { + tracing::error!(?err, %err, "poison ☠"); + panic!(); + } + Err(TryLockError::WouldBlock) => { + tracing::error!("WouldBlock"); + + // just skip this render if we can't get a read lock + return; + } + }; + + match state.ui { + TanicUiState::SplashScreen => self.splash_screen.render(top, buf), + TanicUiState::ViewingNamespacesList(_) => { + (&self.namespace_list_view).render(top, buf) + } + TanicUiState::ViewingTablesList(_) => (&self.table_list_view).render(top, buf), + TanicUiState::Exiting => {} // _ => {} + } } + tracing::debug!("render self.state.read done"); } } diff --git a/tanic-tui/ui_components/namespace_list_view.rs b/tanic-tui/ui_components/namespace_list_view.rs index 7058300..11ea228 100644 --- a/tanic-tui/ui_components/namespace_list_view.rs +++ b/tanic-tui/ui_components/namespace_list_view.rs @@ -2,7 +2,7 @@ use crossterm::event::{KeyCode, KeyEvent}; use ratatui::prelude::*; use ratatui::symbols::border; use ratatui::widgets::Block; -use std::sync::{Arc, RwLock}; +use std::sync::{Arc, RwLock, TryLockError}; use crate::component::Component; use crate::ui_components::namespace_list_item::NamespaceListItem; @@ -36,30 +36,45 @@ impl Component for &NamespaceListView { .border_set(border::PLAIN); let block_inner_area = block.inner(area); - let state = self.state.read().unwrap(); - let items = self.get_items(&state); + { + tracing::debug!("render self.state.read"); + let state = match self.state.try_read() { + Ok(state) => state, + Err(TryLockError::Poisoned(err)) => { + tracing::error!(?err, %err, "poison ☠"); + panic!(); + } + Err(TryLockError::WouldBlock) => { + tracing::error!("WouldBlock"); - let children: Vec<(&NamespaceListItem, usize)> = items - .iter() - .map(|item| { - let tables = &item.ns.tables; - let table_count = tables.as_ref().map(|t| t.len()).unwrap_or(0); + // just skip this render if we can't get a read lock + return; + } + }; - (item, table_count) - }) - .collect::>(); + let items = self.get_items(&state); + + let children: Vec<(&NamespaceListItem, usize)> = items + .iter() + .map(|item| { + let tables = &item.ns.tables; + let table_count = tables.as_ref().map(|t| t.len()).unwrap_or(0); - let layout = TreeMapLayout::new(children); + (item, table_count) + }) + .collect::>(); - block.render(area, buf); - (&layout).render(block_inner_area, buf); + let layout = TreeMapLayout::new(children); + + block.render(area, buf); + (&layout).render(block_inner_area, buf); + } + tracing::debug!("render self.state.read done"); } } impl NamespaceListView { fn get_items<'a>(&self, state: &'a TanicAppState) -> Vec> { - // let state = self.state.read().unwrap(); - let TanicIcebergState::Connected(ref iceberg_state) = state.iceberg else { return vec![]; }; diff --git a/tanic-tui/ui_components/table_list_view.rs b/tanic-tui/ui_components/table_list_view.rs index 2091c3c..3e5e840 100644 --- a/tanic-tui/ui_components/table_list_view.rs +++ b/tanic-tui/ui_components/table_list_view.rs @@ -34,7 +34,9 @@ impl Component for &TableListView { } fn render(&self, area: Rect, buf: &mut Buffer) { + tracing::debug!("self.state.read"); let state = self.state.read().unwrap(); + tracing::debug!("self.state.read done"); let TanicIcebergState::Connected(ref iceberg_state) = state.iceberg else { panic!(); diff --git a/tanic/Cargo.toml b/tanic/Cargo.toml index 7674c1e..9d8452a 100644 --- a/tanic/Cargo.toml +++ b/tanic/Cargo.toml @@ -33,6 +33,6 @@ terminal_size = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } -tracing-subscriber = { workspace = true } +tracing-subscriber = { workspace = true, features = ["json"] } tui-logger = { version = "0.14.1", features = ["tracing-support"] } console-subscriber = "0.4.1" diff --git a/tanic/src/logging.rs b/tanic/src/logging.rs index 6b30d4f..6b79287 100644 --- a/tanic/src/logging.rs +++ b/tanic/src/logging.rs @@ -1,3 +1,4 @@ +use std::fs::OpenOptions; use tracing::level_filters::LevelFilter; use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::prelude::*; @@ -24,12 +25,30 @@ pub(crate) fn init_tui_logger(no_ui: bool) { .with(tracing_subscriber::fmt::layer().pretty()) .init(); } else { + let log_file = OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open("tanic-log.txt") + .unwrap(); + tracing_subscriber::registry() // .with(console_subscriber::spawn()) + .with( + EnvFilter::builder() + .with_env_var("TANIC_LOG") + .with_default_directive(LevelFilter::INFO.into()) + .from_env_lossy(), + ) + .with( + tracing_subscriber::fmt::layer() + .json() + .with_writer(log_file), + ) .with(tui_logger::tracing_subscriber_layer()) .init(); - tui_logger::init_logger(tui_logger::LevelFilter::Trace) + tui_logger::init_logger(tui_logger::LevelFilter::Debug) .expect("Could not initialize logger"); } } diff --git a/tanic/src/main.rs b/tanic/src/main.rs index d416310..4a9e3c6 100644 --- a/tanic/src/main.rs +++ b/tanic/src/main.rs @@ -46,14 +46,15 @@ async fn main() -> Result<()> { _ = iceberg_task => Ok(()), } } else { - let ui_task = tokio::spawn({ - let tanic_tui = TanicTui::new(action_tx.clone()); - let state_rx = state_rx.clone(); - async move { tanic_tui.event_loop(state_rx, ui_app_state).await } + tokio::task::spawn_blocking(move || { + tokio::spawn(async move { + let tanic_tui = TanicTui::new(action_tx.clone()); + let state_rx = state_rx.clone(); + tanic_tui.event_loop(state_rx, ui_app_state).await + }) }); tokio::select! { - _ = ui_task => Ok(()), _ = svc_task => Ok(()), _ = iceberg_task => Ok(()), } From 7a64fec58b58a62a32536199df64917be9a9d566 Mon Sep 17 00:00:00 2001 From: Scott Donnelly Date: Mon, 17 Feb 2025 20:44:22 +0000 Subject: [PATCH 7/7] fix: number formatting and double count bug --- Cargo.toml | 1 + tanic-svc/Cargo.toml | 11 +-- tanic-svc/src/iceberg_context.rs | 35 +++++---- tanic-svc/src/state.rs | 75 +++++++++++++++++-- tanic-tui/Cargo.toml | 1 + tanic-tui/component.rs | 3 +- tanic-tui/ui_components/app_container.rs | 9 ++- .../ui_components/namespace_list_item.rs | 7 +- .../ui_components/namespace_list_view.rs | 13 ++-- tanic-tui/ui_components/table_list_item.rs | 5 +- tanic-tui/ui_components/table_list_view.rs | 12 +-- tanic-tui/ui_components/treemap_layout.rs | 5 +- 12 files changed, 125 insertions(+), 52 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7a98da9..4c65e1e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ http-serde = "2" iceberg = "0.4" iceberg-catalog-rest = "0.4" miette = { version = "7", features = ["fancy"] } +num-format = { version = "0.4", features = ["with-system-locale"] } ratatui = "0.29" serde = "1" streemap = "0.1" diff --git a/tanic-svc/Cargo.toml b/tanic-svc/Cargo.toml index 5b3bc14..40464da 100644 --- a/tanic-svc/Cargo.toml +++ b/tanic-svc/Cargo.toml @@ -28,9 +28,10 @@ iceberg-catalog-rest = "0.4.0" serde = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } -uuid = { version = "1.12.0", features = ["v4"] } -names = "0.14.0" -tokio-stream = { version = "0.1.17", features = ["sync"] } +uuid = { version = "1", features = ["v4"] } +names = "0.14" +num-format = { workspace = true } +tokio-stream = { version = "0.1", features = ["sync"] } parquet = "54.0.0" -indexmap = "2.7.1" -futures = "0.3.31" +indexmap = "2" +futures = "0.3" diff --git a/tanic-svc/src/iceberg_context.rs b/tanic-svc/src/iceberg_context.rs index 521a4c1..bf9da1d 100644 --- a/tanic-svc/src/iceberg_context.rs +++ b/tanic-svc/src/iceberg_context.rs @@ -64,9 +64,10 @@ impl IcebergContextManager { pub async fn event_loop(&self, state_rx: Receiver<()>) -> Result<()> { let mut state_stream = WatchStream::new(state_rx); - let (job_queue_tx, job_queue_rx) = channel(10); + let mut current_conn_details: Option = None; + tokio::spawn({ let action_tx = self.action_tx.clone(); let job_queue_tx = job_queue_tx.clone(); @@ -77,9 +78,9 @@ impl IcebergContextManager { } }); - tracing::debug!("await state_stream.next()"); + tracing::debug!("await state_stream.next() 1"); let mut next_item = state_stream.next().await; - tracing::debug!("await state_stream.next() complete"); + tracing::debug!("await state_stream.next() 1 complete"); while next_item.is_some() { let new_conn_details = { tracing::debug!("self.state_ref.read()"); @@ -98,20 +99,24 @@ impl IcebergContextManager { tracing::debug!("self.state_ref.read() done"); if let Some(new_conn_details) = new_conn_details { - tracing::debug!("await self.connect_to()"); - self.connect_to(&new_conn_details, job_queue_tx.clone()) - .await?; - tracing::debug!("await self.connect_to() done"); - - // begin crawl - tracing::debug!("await job_queue_tx.send()"); - let _ = job_queue_tx.send(IcebergTask::Namespaces).await; - tracing::debug!("await job_queue_tx.send() done"); + if Some(new_conn_details.clone()) != current_conn_details { + current_conn_details = Some(new_conn_details.clone()); + + tracing::debug!("await self.connect_to()"); + self.connect_to(&new_conn_details, job_queue_tx.clone()) + .await?; + tracing::debug!("await self.connect_to() done"); + + // begin crawl + tracing::debug!("await job_queue_tx.send()"); + let _ = job_queue_tx.send(IcebergTask::Namespaces).await; + tracing::debug!("await job_queue_tx.send() done"); + } } - tracing::debug!("await state_stream.next()"); + tracing::debug!("await state_stream.next() 2"); next_item = state_stream.next().await; - tracing::debug!("await state_stream.next() complete"); + tracing::debug!("await state_stream.next() 2 complete"); } Ok(()) @@ -197,7 +202,7 @@ impl IcebergContextManager { let _ = job_queue_tx .send(IcebergTask::TablesForNamespace(namespace.clone())) .await; - tracing::debug!("job_queue_tx.send await sone"); + tracing::debug!("job_queue_tx.send await done"); } Ok(()) diff --git a/tanic-svc/src/state.rs b/tanic-svc/src/state.rs index f0fb348..ddd63b8 100644 --- a/tanic-svc/src/state.rs +++ b/tanic-svc/src/state.rs @@ -1,6 +1,7 @@ use iceberg::spec::{DataFile, Manifest, ManifestList, Snapshot}; use iceberg::table::Table; use indexmap::IndexMap; +use num_format::SystemLocale; use parquet::file::metadata::ParquetMetaData; use std::collections::HashMap; use tanic_core::config::ConnectionDetails; @@ -75,10 +76,11 @@ pub enum TanicAction { SelectDataFile, } -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug)] pub struct TanicAppState { pub iceberg: TanicIcebergState, pub ui: TanicUiState, + pub locale: SystemLocale, } #[derive(Clone, Debug, Default)] @@ -132,6 +134,7 @@ pub enum TanicUiState { SplashScreen, ViewingNamespacesList(ViewingNamespacesListState), ViewingTablesList(ViewingTablesListState), + ViewingTable(ViewingTableState), Exiting, } @@ -146,6 +149,23 @@ pub struct ViewingTablesListState { pub selected_idx: Option, } +#[derive(Clone, Debug)] +pub struct ViewingTableState { + pub tables: ViewingTablesListState, +} + +impl Default for TanicAppState { + fn default() -> Self { + let locale = SystemLocale::default().unwrap(); + + Self { + iceberg: Default::default(), + ui: Default::default(), + locale, + } + } +} + impl TanicAppState { pub(crate) fn update(mut self, action: TanicAction) -> Self { match (action, &mut self) { @@ -501,7 +521,7 @@ impl TanicAppState { } (TanicAction::FocusPrevNamespace, prev_state) => { - let TanicAppState { iceberg, ui } = prev_state; + let TanicAppState { iceberg, ui, .. } = prev_state; let TanicUiState::ViewingNamespacesList(ref mut viewing_namespaces_list_state) = ui else { @@ -525,7 +545,7 @@ impl TanicAppState { } (TanicAction::FocusNextNamespace, prev_state) => { - let TanicAppState { iceberg, ui } = prev_state; + let TanicAppState { iceberg, ui, .. } = prev_state; let TanicUiState::ViewingNamespacesList(ref mut viewing_namespaces_list_state) = ui else { @@ -549,20 +569,43 @@ impl TanicAppState { } (TanicAction::SelectNamespace, prev_state) => { - let TanicAppState { ui, .. } = prev_state; + let TanicAppState { iceberg, ui, .. } = prev_state; let TanicUiState::ViewingNamespacesList(namespaces) = ui else { panic!(); }; + let TanicIcebergState::Connected(ref iceberg_state) = iceberg else { + panic!(); + }; + + let has_some_tables = if let Some(selected_namespace_idx) = namespaces.selected_idx + { + if let Some((_, ns)) = + iceberg_state.namespaces.get_index(selected_namespace_idx) + { + if let Some(ref tables) = ns.tables { + !tables.is_empty() + } else { + false + } + } else { + false + } + } else { + false + }; + + let selected_idx = if has_some_tables { Some(0) } else { None }; + self.ui = TanicUiState::ViewingTablesList(ViewingTablesListState { namespaces: namespaces.clone(), - selected_idx: None, + selected_idx, }); } (TanicAction::FocusPrevTable, prev_state) => { - let TanicAppState { iceberg, ui } = prev_state; + let TanicAppState { iceberg, ui, .. } = prev_state; let TanicIcebergState::Connected(ref mut retrieved_iceberg_metadata) = iceberg else { @@ -610,7 +653,7 @@ impl TanicAppState { } (TanicAction::FocusNextTable, prev_state) => { - let TanicAppState { iceberg, ui } = prev_state; + let TanicAppState { iceberg, ui, .. } = prev_state; let TanicIcebergState::Connected(ref mut retrieved_iceberg_metadata) = iceberg else { @@ -666,6 +709,24 @@ impl TanicAppState { _ => {} } } + + (TanicAction::SelectTable, prev_state) => { + let TanicAppState { iceberg, ui, .. } = prev_state; + + let TanicIcebergState::Connected(ref iceberg_state) = iceberg else { + panic!(); + }; + + let TanicUiState::ViewingTablesList(ref mut viewing_tables_list_state) = ui else { + panic!(); + }; + + // self.ui = TanicUiState::ViewingTable(ViewingTableState { + // namespaces: namespaces.clone(), + // selected_idx, + // }); + } + // TODO: // * SelectTable diff --git a/tanic-tui/Cargo.toml b/tanic-tui/Cargo.toml index 2897a25..b95dc76 100644 --- a/tanic-tui/Cargo.toml +++ b/tanic-tui/Cargo.toml @@ -33,3 +33,4 @@ futures = "0.3.31" tui-logger = "0.14.1" treemap = "0.3.2" tokio-stream = { version = "0.1.17", features = ["sync"] } +num-format = { workspace = true, features = ["with-system-locale"] } diff --git a/tanic-tui/component.rs b/tanic-tui/component.rs index 72077ec..77864db 100644 --- a/tanic-tui/component.rs +++ b/tanic-tui/component.rs @@ -1,3 +1,4 @@ +use num_format::SystemLocale; use ratatui::crossterm::event::{KeyEvent, MouseEvent}; use ratatui::prelude::*; @@ -13,5 +14,5 @@ pub trait Component { None } - fn render(&self, area: Rect, buf: &mut Buffer); + fn render(&self, area: Rect, buf: &mut Buffer, locale: &SystemLocale); } diff --git a/tanic-tui/ui_components/app_container.rs b/tanic-tui/ui_components/app_container.rs index 3b09fb8..6e11f92 100644 --- a/tanic-tui/ui_components/app_container.rs +++ b/tanic-tui/ui_components/app_container.rs @@ -68,8 +68,7 @@ impl AppContainer { impl Widget for &AppContainer { fn render(self, area: Rect, buf: &mut Buffer) { - let [top, bottom] = - Layout::vertical([Constraint::Fill(1), Constraint::Max(20)]).areas(area); + let [top, bottom] = Layout::vertical([Constraint::Fill(1), Constraint::Max(6)]).areas(area); let filter_state = TuiWidgetState::new() .set_default_display_level(LevelFilter::Info) @@ -106,9 +105,11 @@ impl Widget for &AppContainer { match state.ui { TanicUiState::SplashScreen => self.splash_screen.render(top, buf), TanicUiState::ViewingNamespacesList(_) => { - (&self.namespace_list_view).render(top, buf) + (&self.namespace_list_view).render(top, buf, &state.locale) + } + TanicUiState::ViewingTablesList(_) => { + (&self.table_list_view).render(top, buf, &state.locale) } - TanicUiState::ViewingTablesList(_) => (&self.table_list_view).render(top, buf), TanicUiState::Exiting => {} // _ => {} } } diff --git a/tanic-tui/ui_components/namespace_list_item.rs b/tanic-tui/ui_components/namespace_list_item.rs index 2ec8649..9cdf382 100644 --- a/tanic-tui/ui_components/namespace_list_item.rs +++ b/tanic-tui/ui_components/namespace_list_item.rs @@ -1,4 +1,5 @@ use crate::component::Component; +use num_format::{SystemLocale, ToFormattedString}; use ratatui::prelude::*; use ratatui::symbols::border; use ratatui::widgets::{Block, Paragraph}; @@ -18,7 +19,7 @@ impl<'a> NamespaceListItem<'a> { } impl Component for &NamespaceListItem<'_> { - fn render(&self, area: Rect, buf: &mut Buffer) { + fn render(&self, area: Rect, buf: &mut Buffer, locale: &SystemLocale) { let mut block = Block::new().border_set(border::THICK); let block_inner = block.inner(area); @@ -38,9 +39,9 @@ impl Component for &NamespaceListItem<'_> { "{} {} ({} table{}, {} row{})", NERD_FONT_ICON_TABLE_FOLDER, name, - table_count, + table_count.to_formatted_string(locale), plural_suffix, - row_count, + row_count.to_formatted_string(locale), row_plural_suffix ); diff --git a/tanic-tui/ui_components/namespace_list_view.rs b/tanic-tui/ui_components/namespace_list_view.rs index 11ea228..5b744ab 100644 --- a/tanic-tui/ui_components/namespace_list_view.rs +++ b/tanic-tui/ui_components/namespace_list_view.rs @@ -1,12 +1,12 @@ +use crate::component::Component; +use crate::ui_components::namespace_list_item::NamespaceListItem; +use crate::ui_components::treemap_layout::TreeMapLayout; use crossterm::event::{KeyCode, KeyEvent}; +use num_format::SystemLocale; use ratatui::prelude::*; use ratatui::symbols::border; use ratatui::widgets::Block; use std::sync::{Arc, RwLock, TryLockError}; - -use crate::component::Component; -use crate::ui_components::namespace_list_item::NamespaceListItem; -use crate::ui_components::treemap_layout::TreeMapLayout; use tanic_svc::state::{TanicIcebergState, TanicUiState}; use tanic_svc::{TanicAction, TanicAppState}; @@ -30,7 +30,7 @@ impl Component for &NamespaceListView { } } - fn render(&self, area: Rect, buf: &mut Buffer) { + fn render(&self, area: Rect, buf: &mut Buffer, locale: &SystemLocale) { let block = Block::bordered() .title(" Tanic //// Root Namespaces") .border_set(border::PLAIN); @@ -59,7 +59,6 @@ impl Component for &NamespaceListView { .map(|item| { let tables = &item.ns.tables; let table_count = tables.as_ref().map(|t| t.len()).unwrap_or(0); - (item, table_count) }) .collect::>(); @@ -67,7 +66,7 @@ impl Component for &NamespaceListView { let layout = TreeMapLayout::new(children); block.render(area, buf); - (&layout).render(block_inner_area, buf); + (&layout).render(block_inner_area, buf, locale); } tracing::debug!("render self.state.read done"); } diff --git a/tanic-tui/ui_components/table_list_item.rs b/tanic-tui/ui_components/table_list_item.rs index 5076d26..57ade16 100644 --- a/tanic-tui/ui_components/table_list_item.rs +++ b/tanic-tui/ui_components/table_list_item.rs @@ -1,4 +1,5 @@ use crate::component::Component; +use num_format::{SystemLocale, ToFormattedString}; use ratatui::prelude::*; use ratatui::symbols::border; use ratatui::widgets::{Block, Paragraph}; @@ -18,7 +19,7 @@ impl<'a> TableListItem<'a> { } impl Component for &TableListItem<'_> { - fn render(&self, area: Rect, buf: &mut Buffer) { + fn render(&self, area: Rect, buf: &mut Buffer, locale: &SystemLocale) { let mut block = Block::new().border_set(border::THICK); let block_inner = block.inner(area); @@ -31,7 +32,7 @@ impl Component for &TableListItem<'_> { let row_count_str = match self.table.row_count() { None => "".to_string(), Some(1) => " (1 row)".to_string(), - Some(n) => format!(" ({n} rows)"), + Some(n) => format!(" ({} rows)", n.to_formatted_string(locale)), }; let name = format!("{} {}{}", NERD_FONT_ICON_TABLE, name, row_count_str); diff --git a/tanic-tui/ui_components/table_list_view.rs b/tanic-tui/ui_components/table_list_view.rs index 3e5e840..e9a9438 100644 --- a/tanic-tui/ui_components/table_list_view.rs +++ b/tanic-tui/ui_components/table_list_view.rs @@ -1,12 +1,12 @@ +use crate::component::Component; +use crate::ui_components::table_list_item::TableListItem; +use crate::ui_components::treemap_layout::TreeMapLayout; use crossterm::event::{KeyCode, KeyEvent}; +use num_format::SystemLocale; use ratatui::prelude::*; use ratatui::symbols::border; use ratatui::widgets::Block; use std::sync::{Arc, RwLock}; - -use crate::component::Component; -use crate::ui_components::table_list_item::TableListItem; -use crate::ui_components::treemap_layout::TreeMapLayout; use tanic_svc::state::{ RetrievedIcebergMetadata, TanicIcebergState, TanicUiState, ViewingTablesListState, }; @@ -33,7 +33,7 @@ impl Component for &TableListView { } } - fn render(&self, area: Rect, buf: &mut Buffer) { + fn render(&self, area: Rect, buf: &mut Buffer, locale: &SystemLocale) { tracing::debug!("self.state.read"); let state = self.state.read().unwrap(); tracing::debug!("self.state.read done"); @@ -69,7 +69,7 @@ impl Component for &TableListView { let layout = TreeMapLayout::new(children); block.render(area, buf); - (&layout).render(block_inner_area, buf); + (&layout).render(block_inner_area, buf, locale); } } diff --git a/tanic-tui/ui_components/treemap_layout.rs b/tanic-tui/ui_components/treemap_layout.rs index d7dea73..34d6214 100644 --- a/tanic-tui/ui_components/treemap_layout.rs +++ b/tanic-tui/ui_components/treemap_layout.rs @@ -1,3 +1,4 @@ +use num_format::SystemLocale; use ratatui::prelude::*; use treemap::{MapItem, Mappable, Rect as TreeMapRect, TreemapLayout}; @@ -14,7 +15,7 @@ impl TreeMapLayout { } impl Component for &TreeMapLayout { - fn render(&self, area: Rect, buf: &mut Buffer) { + fn render(&self, area: Rect, buf: &mut Buffer, locale: &SystemLocale) { let layout = TreemapLayout::new(); let bounds = TreeMapRect::from_points( area.x as f64, @@ -44,7 +45,7 @@ impl Component for &TreeMapLayout { height: region_bounds.h as u16, }; - child.render(rect, buf); + child.render(rect, buf, locale); } } }