diff --git a/src/app/model/config/mod.rs b/src/app/model/config/mod.rs index fbbf0b60..c6809146 100644 --- a/src/app/model/config/mod.rs +++ b/src/app/model/config/mod.rs @@ -31,7 +31,7 @@ impl ConfigModel { let mut table = FilterableTable::new(); table.set_items(configs.to_vec()); let config_names: Vec = configs.iter().map(|c| c.name.clone()).collect(); - table.filter.set_primary_values("name", config_names); + table.filter_mut().set_primary_values("name", config_names); Self { table, @@ -52,12 +52,10 @@ impl ConfigModel { fn handle_keys(&mut self, key_event: &KeyEvent) -> KeyResult { match key_event.code { KeyCode::Char('o') => { - if let Some(idx) = self.table.filtered.state.selected() { - if let Some(item) = self.table.filtered.items.get(idx) { - return KeyResult::PassWith(vec![WorkerMessage::OpenItem( - OpenItem::Config(item.endpoint.clone()), - )]); - } + if let Some(item) = self.table.current() { + return KeyResult::PassWith(vec![WorkerMessage::OpenItem(OpenItem::Config( + item.endpoint.clone(), + ))]); } KeyResult::PassThrough } @@ -66,8 +64,8 @@ impl ConfigModel { KeyResult::Consumed } KeyCode::Enter => { - if let Some(idx) = self.table.filtered.state.selected() { - if let Some(item) = self.table.filtered.items.get(idx) { + if let Some(idx) = self.table.selected_position() { + if let Some(item) = self.table.item_at(idx) { debug!("Selected config: {}", item.name); return KeyResult::PassWith(vec![WorkerMessage::ConfigSelected(idx)]); } @@ -116,16 +114,14 @@ impl Widget for &mut ConfigModel { let header_row = create_headers(headers); let header = Row::new(header_row).style(t.table_header_style); - let rows = self + let rows: Vec = self .table - .filtered - .items - .iter() + .items() .enumerate() .map(|(idx, item)| { Row::new(vec![ - Line::from(item.name.as_str()), - Line::from(item.endpoint.as_str()), + Line::from(item.name.clone()), + Line::from(item.endpoint.clone()), Line::from( item.managed .as_ref() @@ -137,7 +133,8 @@ impl Widget for &mut ConfigModel { }), ]) .style(self.table.row_style(idx)) - }); + }) + .collect(); let t = Table::new( rows, @@ -162,7 +159,7 @@ impl Widget for &mut ConfigModel { } }) .row_highlight_style(t.selected_row_style); - StatefulWidget::render(t, content_area, buf, &mut self.table.filtered.state); + StatefulWidget::render(t, content_area, buf, self.table.state_mut()); // Render any active popup (error or commands) (&self.popup).render(area, buf); diff --git a/src/app/model/dagruns/mod.rs b/src/app/model/dagruns/mod.rs index 6deea55d..ed8b4fe1 100644 --- a/src/app/model/dagruns/mod.rs +++ b/src/app/model/dagruns/mod.rs @@ -98,7 +98,7 @@ impl DagRunModel { pub fn sort_dag_runs(&mut self) { // Sort by logical_date (execution date) descending, with fallback to start_date // This ensures queued runs (which have no start_date yet) appear in chronological order - self.table.filtered.items.sort_by(|a, b| { + self.table.sort_by(|a, b| { b.logical_date .or(b.start_date) .cmp(&a.logical_date.or(a.start_date)) @@ -117,15 +117,11 @@ impl DagRunModel { /// Mark a DAG run with a new status (optimistic update) pub fn mark_dag_run(&mut self, dag_run_id: &DagRunId, status: DagRunState) { - if let Some(dag_run) = self - .table - .filtered - .items - .iter_mut() - .find(|dr| dr.dag_run_id == *dag_run_id) - { - dag_run.state = status; - } + self.table.update_all(|all| { + if let Some(dag_run) = all.iter_mut().find(|dr| dr.dag_run_id == *dag_run_id) { + dag_run.state = status; + } + }); } } @@ -165,7 +161,7 @@ impl DagRunModel { matches!(custom_popup, DagRunPopUp::Clear(_) | DagRunPopUp::Mark(_)); self.popup.close(); if exit_visual { - self.table.visual_anchor = None; + self.table.clear_visual_mode(); } } } @@ -396,14 +392,16 @@ mod tests { ..Default::default() }; - model.table.all = vec![oldest_run, newest_run, queued_run]; + model + .table + .set_items(vec![oldest_run, newest_run, queued_run]); model.table.apply_filter(); model.sort_dag_runs(); - assert_eq!(model.table.filtered.items.len(), 3); - assert_eq!(model.table.filtered.items[0].dag_run_id, "run_3"); - assert_eq!(model.table.filtered.items[1].dag_run_id, "run_2"); - assert_eq!(model.table.filtered.items[2].dag_run_id, "run_1"); + assert_eq!(model.table.len(), 3); + assert_eq!(model.table.items().next().unwrap().dag_run_id, "run_3"); + assert_eq!(model.table.items().nth(1).unwrap().dag_run_id, "run_2"); + assert_eq!(model.table.items().nth(2).unwrap().dag_run_id, "run_1"); } #[test] @@ -430,13 +428,13 @@ mod tests { ..Default::default() }; - model.table.all = vec![run_with_both, run_with_start]; + model.table.set_items(vec![run_with_both, run_with_start]); model.table.apply_filter(); model.sort_dag_runs(); - assert_eq!(model.table.filtered.items.len(), 2); - assert_eq!(model.table.filtered.items[0].dag_run_id, "run_1"); - assert_eq!(model.table.filtered.items[1].dag_run_id, "run_2"); + assert_eq!(model.table.len(), 2); + assert_eq!(model.table.items().next().unwrap().dag_run_id, "run_1"); + assert_eq!(model.table.items().nth(1).unwrap().dag_run_id, "run_2"); } #[test] @@ -461,11 +459,11 @@ mod tests { ..Default::default() }; - model.table.all = vec![run_manual, run_scheduled]; + model.table.set_items(vec![run_manual, run_scheduled]); - model.table.filter.activate(); + model.table.filter_mut().activate(); for c in "manual".chars() { - model.table.filter.update( + model.table.filter_mut().update( &KeyEvent::new(crossterm::event::KeyCode::Char(c), KeyModifiers::empty()), &DagRun::filterable_fields(), ); @@ -473,7 +471,10 @@ mod tests { model.table.apply_filter(); model.sort_dag_runs(); - assert_eq!(model.table.filtered.items.len(), 1); - assert_eq!(model.table.filtered.items[0].dag_run_id, "manual_run_1"); + assert_eq!(model.table.len(), 1); + assert_eq!( + model.table.items().next().unwrap().dag_run_id, + "manual_run_1" + ); } } diff --git a/src/app/model/dagruns/render.rs b/src/app/model/dagruns/render.rs index 28ff48e5..95700347 100644 --- a/src/app/model/dagruns/render.rs +++ b/src/app/model/dagruns/render.rs @@ -40,9 +40,7 @@ impl Widget for &mut DagRunModel { // Calculate max duration for normalization let max_duration = self .table - .filtered - .items - .iter() + .items() .filter_map(calculate_duration) .max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)) .unwrap_or(1.0); @@ -56,11 +54,9 @@ impl Widget for &mut DagRunModel { .saturating_sub(fixed_columns_width + dag_run_id_width) .max(10) as usize; - let rows = self + let rows: Vec = self .table - .filtered - .items - .iter() + .items() .enumerate() .map(|(idx, item)| { let state_color: Color = AirflowStateColor::from(&item.state).into(); @@ -82,7 +78,7 @@ impl Widget for &mut DagRunModel { Row::new(vec![ Line::from(Span::styled("■", Style::default().fg(state_color))), Line::from(Span::styled( - &*item.dag_run_id, + item.dag_run_id.to_string(), Style::default().add_modifier(Modifier::BOLD), )), Line::from(if let Some(date) = item.logical_date { @@ -96,7 +92,8 @@ impl Widget for &mut DagRunModel { time_cell, ]) .style(self.table.row_style(idx)) - }); + }) + .collect(); let t = Table::new( rows, &[ @@ -122,7 +119,7 @@ impl Widget for &mut DagRunModel { } }) .row_highlight_style(t.selected_row_style); - StatefulWidget::render(t, content_area, buf, &mut self.table.filtered.state); + StatefulWidget::render(t, content_area, buf, self.table.state_mut()); if let Some(view) = &mut self.dag_code { view.render(area, buf); diff --git a/src/app/model/dags/render.rs b/src/app/model/dags/render.rs index 5c139b30..d68c02ab 100644 --- a/src/app/model/dags/render.rs +++ b/src/app/model/dags/render.rs @@ -20,11 +20,9 @@ impl Widget for &mut DagModel { let headers = ["Active", "Name", "Owners", "Schedule", "Next Run", "Stats"]; let header_row = create_headers(headers); let header = Row::new(header_row).style(theme.table_header_style); - let rows = self + let rows: Vec = self .table - .filtered - .items - .iter() + .items() .enumerate() .map(|(idx, item)| { Row::new(vec![ @@ -34,12 +32,16 @@ impl Widget for &mut DagModel { Line::from(Span::styled("𖣘", Style::default().fg(theme.dag_active))) }, Line::from(Span::styled( - &*item.dag_id, + item.dag_id.to_string(), Style::default().add_modifier(Modifier::BOLD), )), Line::from(item.owners.join(", ")), - Line::from(item.timetable_description.as_deref().unwrap_or("None")) - .style(Style::default().fg(theme.schedule_fg)), + Line::from( + item.timetable_description + .clone() + .unwrap_or_else(|| "None".to_string()), + ) + .style(Style::default().fg(theme.schedule_fg)), Line::from(item.next_dagrun_create_after.map_or_else( || "None".to_string(), convert_datetimeoffset_to_human_readable_remaining_time, @@ -66,7 +68,8 @@ impl Widget for &mut DagModel { )), ]) .style(self.table.row_style(idx)) - }); + }) + .collect(); let table = Table::new( rows, &[ @@ -93,7 +96,7 @@ impl Widget for &mut DagModel { }) .row_highlight_style(theme.selected_row_style); - StatefulWidget::render(table, content_area, buf, &mut self.table.filtered.state); + StatefulWidget::render(table, content_area, buf, self.table.state_mut()); if let Some(view) = &mut self.dag_code { view.render(area, buf); diff --git a/src/app/model/filter/matching.rs b/src/app/model/filter/matching.rs index 3848cdbd..fecfe70b 100644 --- a/src/app/model/filter/matching.rs +++ b/src/app/model/filter/matching.rs @@ -21,16 +21,18 @@ pub fn matches(item: &T, conditions: &[FilterCondition]) -> bool item_matches(item, conditions) } -/// Filter a collection of items by conditions -pub fn filter_items(items: &[T], conditions: &[FilterCondition]) -> Vec { +/// Filter a collection of items by conditions, returning the indices of the +/// matches so callers can reference the originals instead of cloning them. +pub fn filter_items(items: &[T], conditions: &[FilterCondition]) -> Vec { if conditions.is_empty() { - return items.to_vec(); + return (0..items.len()).collect(); } items .iter() - .filter(|item| item_matches(*item, conditions)) - .cloned() + .enumerate() + .filter(|&(_, item)| item_matches(item, conditions)) + .map(|(i, _)| i) .collect() } @@ -149,7 +151,7 @@ mod tests { ]; let filtered = filter_items(&items, &conditions); assert_eq!(filtered.len(), 1); - assert_eq!(filtered[0].id, "item_1"); + assert_eq!(items[filtered[0]].id, "item_1"); } #[test] diff --git a/src/app/model/filterable_table/mod.rs b/src/app/model/filterable_table/mod.rs index e3f52110..2420a1de 100644 --- a/src/app/model/filterable_table/mod.rs +++ b/src/app/model/filterable_table/mod.rs @@ -9,50 +9,136 @@ mod render; +use std::cmp::Ordering; use std::ops::RangeInclusive; use crossterm::event::{KeyCode, KeyEvent}; +use ratatui::widgets::TableState; use super::filter::{filter_items, FilterStateMachine, Filterable}; use super::{KeyResult, StatefulTable}; -/// A generic filterable table that combines data storage, filtering, and visual selection. +/// A generic filterable, selectable table shared by the Dags, `DagRuns`, and +/// `TaskInstances` panels. /// -/// This widget is designed to work with any type that implements `Filterable + Clone`. +/// It owns the canonical `all` items and derives a filtered, ordered *view* of +/// indices into them, so matched items are never cloned. Callers interact +/// through the item-oriented API (`items`, `current`, `sort_by`, ...) and never +/// see the index representation, which is free to change (e.g. to shared `Arc` +/// data) without touching them. #[derive(Debug, Clone, Default)] pub struct FilterableTable { - /// All items from the API (unfiltered) - pub all: Vec, - /// Filtered items with table state for rendering - pub filtered: StatefulTable, - /// Filter state machine with autocomplete - pub filter: FilterStateMachine, - /// Anchor index for visual selection; `Some` means visual mode is active - pub visual_anchor: Option, + /// Canonical items, kept in display/sort order. + all: Vec, + /// Indices into `all` that pass the filter, with the table selection state. + view: StatefulTable, + /// Filter state machine with autocomplete. + filter: FilterStateMachine, + /// Anchor position for visual selection; `Some` means visual mode is active. + visual_anchor: Option, } -impl FilterableTable { +impl FilterableTable { /// Creates a new empty filterable table pub fn new() -> Self { Self { all: Vec::new(), - filtered: StatefulTable::new(Vec::new()), + view: StatefulTable::new(Vec::new()), filter: FilterStateMachine::default(), visual_anchor: None, } } - /// Sets the items and applies the current filter + // ── Data ──────────────────────────────────────────────── + + /// Replace all items and recompute the filtered view. pub fn set_items(&mut self, items: Vec) { self.all = items; self.apply_filter(); } - /// Applies the current filter conditions to the items + /// Reorder or mutate the canonical items in place, then refresh the view. + /// Used for sorting and optimistic updates; the view stays in `all`'s order. + pub fn update_all(&mut self, f: impl FnOnce(&mut Vec)) { + f(&mut self.all); + self.apply_filter(); + } + + /// Sort the items with `cmp` and refresh the view. + pub fn sort_by(&mut self, cmp: impl FnMut(&T, &T) -> Ordering) { + self.update_all(|all| all.sort_by(cmp)); + } + + /// Remove all items (and any selection). + pub fn clear(&mut self) { + self.all.clear(); + self.apply_filter(); + } + + /// Read-only access to the canonical items. + pub fn all(&self) -> &[T] { + &self.all + } + + /// Recompute the filtered view from the current filter conditions. pub fn apply_filter(&mut self) { let conditions = self.filter.active_conditions(); - let filtered = filter_items(&self.all, &conditions); - self.filtered.items = filtered; + self.view.items = filter_items(&self.all, &conditions); + } + + // ── The filtered view ─────────────────────────────────── + + /// Iterate the filtered items in display order. + pub fn items(&self) -> impl Iterator { + self.view.items.iter().filter_map(|&i| self.all.get(i)) + } + + /// Number of items in the filtered view. + pub fn len(&self) -> usize { + self.view.items.len() + } + + /// Whether the filtered view is empty. + pub fn is_empty(&self) -> bool { + self.view.items.is_empty() + } + + /// Resolve a position in the filtered view to its item. + pub fn item_at(&self, pos: usize) -> Option<&T> { + self.all.get(*self.view.items.get(pos)?) + } + + /// The currently selected item, if any. + pub fn current(&self) -> Option<&T> { + self.item_at(self.view.state.selected()?) + } + + /// The currently selected item, mutably. + pub fn current_mut(&mut self) -> Option<&mut T> { + let index = *self.view.items.get(self.view.state.selected()?)?; + self.all.get_mut(index) + } + + /// The selected position within the filtered view. + pub fn selected_position(&self) -> Option { + self.view.state.selected() + } + + /// Mutable table state, for driving the ratatui `StatefulWidget` render. + pub fn state_mut(&mut self) -> &mut TableState { + &mut self.view.state + } + + // ── Filter ────────────────────────────────────────────── + + /// Read-only access to the filter state (active flag, cursor, display). + pub fn filter(&self) -> &FilterStateMachine { + &self.filter + } + + /// Mutable access to the filter state (e.g. to set autocomplete values). + pub fn filter_mut(&mut self) -> &mut FilterStateMachine { + &mut self.filter } /// Activates filter mode @@ -74,32 +160,18 @@ impl FilterableTable { } } - /// Returns the currently selected item - pub fn current(&self) -> Option<&T> { - self.filtered - .state - .selected() - .and_then(|i| self.filtered.items.get(i)) - } - - /// Returns the currently selected item mutably - pub fn current_mut(&mut self) -> Option<&mut T> { - self.filtered - .state - .selected() - .and_then(|i| self.filtered.items.get_mut(i)) - } + // ── Selection / visual mode ───────────────────────────── - /// Returns the inclusive range of selected indices, if in visual mode + /// Returns the inclusive range of selected positions, if in visual mode pub fn visual_selection(&self) -> Option> { let anchor = self.visual_anchor?; - let cursor = self.filtered.state.selected()?; - let (start, end) = if anchor <= cursor { - (anchor, cursor) - } else { - (cursor, anchor) - }; - Some(start..=end) + let cursor = self.view.state.selected()?; + Some(anchor.min(cursor)..=anchor.max(cursor)) + } + + /// Exit visual selection mode. + pub fn clear_visual_mode(&mut self) { + self.visual_anchor = None; } /// Returns count of selected items (for bottom border display) @@ -116,14 +188,11 @@ impl FilterableTable { { match self.visual_selection() { Some(range) => range - .filter_map(|i| self.filtered.items.get(i)) + .filter_map(|pos| self.item_at(pos)) .map(&key_fn) .collect(), None => self - .filtered - .state - .selected() - .and_then(|i| self.filtered.items.get(i)) + .current() .map(|item| vec![key_fn(item)]) .unwrap_or_default(), } @@ -137,25 +206,23 @@ impl FilterableTable { ) -> KeyResult { match key_code { KeyCode::Down | KeyCode::Char('j') => { - self.filtered.next(); + self.view.next(); KeyResult::Consumed } KeyCode::Up | KeyCode::Char('k') => { - self.filtered.previous(); + self.view.previous(); KeyResult::Consumed } KeyCode::Char('G') => { - if !self.filtered.items.is_empty() { - self.filtered - .state - .select(Some(self.filtered.items.len() - 1)); + if !self.view.items.is_empty() { + self.view.state.select(Some(self.view.items.len() - 1)); } KeyResult::Consumed } KeyCode::Char('g') => { if let Some(last_key) = event_buffer.pop() { if last_key == KeyCode::Char('g') { - self.filtered.state.select_first(); + self.view.state.select_first(); } else { event_buffer.push(last_key); event_buffer.push(key_code); @@ -173,7 +240,7 @@ impl FilterableTable { pub fn handle_visual_mode_key(&mut self, key_code: KeyCode) -> KeyResult { match key_code { KeyCode::Char('V') => { - if let Some(cursor) = self.filtered.state.selected() { + if let Some(cursor) = self.view.state.selected() { self.visual_anchor = Some(cursor); } KeyResult::Consumed @@ -222,8 +289,8 @@ mod tests { #[test] fn test_new_table_is_empty() { let table: FilterableTable = FilterableTable::new(); - assert!(table.filtered.items.is_empty()); - assert_eq!(table.filtered.items.len(), 0); + assert!(table.view.items.is_empty()); + assert_eq!(table.view.items.len(), 0); assert!(table.current().is_none()); } @@ -241,8 +308,8 @@ mod tests { }, ]; table.set_items(items); - assert_eq!(table.filtered.items.len(), 2); - assert!(!table.filtered.items.is_empty()); + assert_eq!(table.view.items.len(), 2); + assert!(!table.view.items.is_empty()); } #[test] @@ -267,22 +334,19 @@ mod tests { assert!(table.current().is_none()); // Move next, should select first - table.filtered.next(); + table.view.next(); assert_eq!(table.current().map(|i| i.id.as_str()), Some("1")); // Move next - table.filtered.next(); + table.view.next(); assert_eq!(table.current().map(|i| i.id.as_str()), Some("2")); // Select last - table - .filtered - .state - .select(Some(table.filtered.items.len() - 1)); + table.view.state.select(Some(table.view.items.len() - 1)); assert_eq!(table.current().map(|i| i.id.as_str()), Some("3")); // Select first - table.filtered.state.select_first(); + table.view.state.select_first(); assert_eq!(table.current().map(|i| i.id.as_str()), Some("1")); } @@ -305,18 +369,18 @@ mod tests { ]); // Select first item - table.filtered.next(); + table.view.next(); assert!(table.visual_selection().is_none()); // Enter visual mode - table.visual_anchor = table.filtered.state.selected(); + table.visual_anchor = table.view.state.selected(); assert!(table.visual_anchor.is_some()); assert_eq!(table.visual_anchor, Some(0)); assert_eq!(table.visual_selection_count(), 1); // Move down to expand selection - table.filtered.next(); - table.filtered.next(); + table.view.next(); + table.view.next(); assert_eq!(table.visual_selection_count(), 3); // Get selected IDs @@ -347,7 +411,7 @@ mod tests { assert!(table.selected_ids(|item| item.id.clone()).is_empty()); // Select first item - table.filtered.next(); + table.view.next(); let ids = table.selected_ids(|item| item.id.clone()); assert_eq!(ids, vec!["1"]); } @@ -421,7 +485,7 @@ mod tests { ]); // Select first item - table.filtered.next(); + table.view.next(); assert!(table.visual_anchor.is_none()); // V key enters visual mode diff --git a/src/app/model/filterable_table/render.rs b/src/app/model/filterable_table/render.rs index e118d574..5d97fa11 100644 --- a/src/app/model/filterable_table/render.rs +++ b/src/app/model/filterable_table/render.rs @@ -8,7 +8,7 @@ use crate::ui::theme::theme; use super::FilterableTable; -impl FilterableTable { +impl FilterableTable { /// Renders the filter widget if active and returns the content area. /// /// This handles the common pattern of splitting the area for filter input. diff --git a/src/app/model/taskinstances/mod.rs b/src/app/model/taskinstances/mod.rs index 186d6e27..0bd9dc86 100644 --- a/src/app/model/taskinstances/mod.rs +++ b/src/app/model/taskinstances/mod.rs @@ -73,21 +73,17 @@ impl TaskInstanceModel { /// Sort task instances by topological order (or timestamp fallback) pub fn sort_task_instances(&mut self) { if let Some(graph) = &self.task_graph { - sort_task_instances(&mut self.table.all, graph); + self.table.update_all(|all| sort_task_instances(all, graph)); } } /// Mark a task instance with a new status (optimistic update) pub fn mark_task_instance(&mut self, task_id: &TaskId, status: TaskInstanceState) { - if let Some(task_instance) = self - .table - .filtered - .items - .iter_mut() - .find(|ti| ti.task_id == *task_id) - { - task_instance.state = Some(status); - } + self.table.update_all(|all| { + if let Some(task_instance) = all.iter_mut().find(|ti| ti.task_id == *task_id) { + task_instance.state = Some(status); + } + }); } /// Returns selected task IDs for passing to mark/clear popups @@ -117,7 +113,7 @@ impl TaskInstanceModel { KeyCode::Enter | KeyCode::Esc | KeyCode::Char('q') ) { self.popup.close(); - self.table.visual_anchor = None; + self.table.clear_visual_mode(); } } Some(messages) @@ -171,7 +167,7 @@ impl TaskInstanceModel { KeyCode::Char('d') => { if let Some(graph) = &self.task_graph { if !graph.is_empty() { - let popup = DagGraphPopup::new(graph, &self.table.all); + let popup = DagGraphPopup::new(graph, self.table.all()); self.popup.show_custom(TaskInstancePopUp::Graph(popup)); } } diff --git a/src/app/model/taskinstances/render.rs b/src/app/model/taskinstances/render.rs index 20119ec5..7294e925 100644 --- a/src/app/model/taskinstances/render.rs +++ b/src/app/model/taskinstances/render.rs @@ -42,15 +42,13 @@ impl Widget for &mut TaskInstanceModel { let table_inner_width = content_area.width.saturating_sub(2); // Subtract borders let gantt_width = (table_inner_width / 2).max(10); - let rows = self + let rows: Vec = self .table - .filtered - .items - .iter() + .items() .enumerate() .map(|(idx, item)| { Row::new(vec![ - Line::from(item.task_id.as_ref()), + Line::from(item.task_id.to_string()), Line::from( calculate_duration(item).map_or_else(|| "-".to_string(), format_duration), ), @@ -63,7 +61,8 @@ impl Widget for &mut TaskInstanceModel { create_gantt_bar(&self.gantt_data, &item.task_id, gantt_width.into()), ]) .style(self.table.row_style(idx)) - }); + }) + .collect(); let t = Table::new( rows, &[ @@ -89,7 +88,7 @@ impl Widget for &mut TaskInstanceModel { }) .row_highlight_style(t.selected_row_style); - StatefulWidget::render(t, content_area, buffer, &mut self.table.filtered.state); + StatefulWidget::render(t, content_area, buffer, self.table.state_mut()); legend.render(legend_area, buffer); diff --git a/src/app/state/breadcrumb.rs b/src/app/state/breadcrumb.rs index 458e931d..15d8715d 100644 --- a/src/app/state/breadcrumb.rs +++ b/src/app/state/breadcrumb.rs @@ -26,7 +26,7 @@ impl App { } if self.active_panel == Panel::TaskInstance || self.active_panel == Panel::Logs { - if let Some(task_instance) = self.task_instances.table.filtered.items.first() { + if let Some(task_instance) = self.task_instances.table.items().next() { if let Some(logical_date) = task_instance.logical_date { let formatted = logical_date .format(&BREADCRUMB_DATE_FORMAT) diff --git a/src/app/state/mod.rs b/src/app/state/mod.rs index 80c11170..4db55114 100644 --- a/src/app/state/mod.rs +++ b/src/app/state/mod.rs @@ -152,9 +152,9 @@ impl App { pub fn clear_state(&mut self) { self.loading = true; self.nav_context.reset_to_environment(); - self.dags.table.all.clear(); - self.dagruns.table.all.clear(); - self.task_instances.table.all.clear(); + self.dags.table.clear(); + self.dagruns.table.clear(); + self.task_instances.table.clear(); self.logs.all.clear(); } } diff --git a/src/app/state/sync.rs b/src/app/state/sync.rs index 53899206..e2ca94ea 100644 --- a/src/app/state/sync.rs +++ b/src/app/state/sync.rs @@ -5,36 +5,41 @@ impl App { pub fn sync_panel(&mut self, panel: &Panel) { match panel { Panel::Dag => { - self.dags.table.all = self.environment_state.get_active_dags(); + self.dags + .table + .set_items(self.environment_state.get_active_dags()); self.dags.dag_stats = self.environment_state.get_active_dag_stats(); let dag_ids: Vec = self .dags .table - .all + .all() .iter() .map(|d| d.dag_id.to_string()) .collect(); - self.dags.table.filter.set_primary_values("dag_id", dag_ids); - self.dags.table.apply_filter(); + self.dags + .table + .filter_mut() + .set_primary_values("dag_id", dag_ids); } Panel::DAGRun => { if let Some(dag_id) = self.nav_context.dag_id() { - self.dagruns.table.all = self.environment_state.get_active_dag_runs(dag_id); + self.dagruns + .table + .set_items(self.environment_state.get_active_dag_runs(dag_id)); let dag_run_ids: Vec = self .dagruns .table - .all + .all() .iter() .map(|dr| dr.dag_run_id.to_string()) .collect(); self.dagruns .table - .filter + .filter_mut() .set_primary_values("dag_run_id", dag_run_ids); - self.dagruns.table.apply_filter(); self.dagruns.sort_dag_runs(); } else { - self.dagruns.table.all.clear(); + self.dagruns.table.clear(); } } Panel::TaskInstance => { @@ -42,24 +47,24 @@ impl App { (self.nav_context.dag_id(), self.nav_context.dag_run_id()) { self.task_instances.set_gantt_context(dag_id, dag_run_id); - self.task_instances.table.all = self - .environment_state - .get_active_task_instances(dag_id, dag_run_id); + self.task_instances.table.set_items( + self.environment_state + .get_active_task_instances(dag_id, dag_run_id), + ); self.task_instances.sort_task_instances(); let task_ids: Vec = self .task_instances .table - .all + .all() .iter() .map(|ti| ti.task_id.to_string()) .collect(); self.task_instances .table - .filter + .filter_mut() .set_primary_values("task_id", task_ids); - self.task_instances.table.apply_filter(); } else { - self.task_instances.table.all.clear(); + self.task_instances.table.clear(); } } Panel::Logs => { @@ -80,13 +85,13 @@ impl App { let config_names: Vec = self .configs .table - .all + .all() .iter() .map(|c| c.name.clone()) .collect(); self.configs .table - .filter + .filter_mut() .set_primary_values("name", config_names); } } diff --git a/src/app/worker/config.rs b/src/app/worker/config.rs index e00d1edb..92ca8512 100644 --- a/src/app/worker/config.rs +++ b/src/app/worker/config.rs @@ -14,10 +14,10 @@ pub fn handle_config_selected(app: &Arc>, idx: usize) -> Result<()> { .lock() .map_err(|_| anyhow::anyhow!("Failed to acquire app lock"))?; - let Some(selected_config) = app.configs.table.filtered.items.get(idx).cloned() else { + let Some(selected_config) = app.configs.table.item_at(idx).cloned() else { log::error!( "Config index {idx} out of bounds (total: {})", - app.configs.table.filtered.items.len() + app.configs.table.len() ); app.configs .popup diff --git a/src/ui.rs b/src/ui.rs index b46bb8d5..ea5a156e 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -98,26 +98,26 @@ pub fn draw_ui(f: &mut Frame, app: &Arc>) { match app.active_panel { Panel::Config => { app.configs.render(panel_area, f.buffer_mut()); - if app.configs.table.filter.is_active() { - f.set_cursor_position(app.configs.table.filter.cursor_position); + if app.configs.table.filter().is_active() { + f.set_cursor_position(app.configs.table.filter().cursor_position); } } Panel::Dag => { app.dags.render(panel_area, f.buffer_mut()); - if app.dags.table.filter.is_active() { - f.set_cursor_position(app.dags.table.filter.cursor_position); + if app.dags.table.filter().is_active() { + f.set_cursor_position(app.dags.table.filter().cursor_position); } } Panel::DAGRun => { app.dagruns.render(panel_area, f.buffer_mut()); - if app.dagruns.table.filter.is_active() { - f.set_cursor_position(app.dagruns.table.filter.cursor_position); + if app.dagruns.table.filter().is_active() { + f.set_cursor_position(app.dagruns.table.filter().cursor_position); } } Panel::TaskInstance => { app.task_instances.render(panel_area, f.buffer_mut()); - if app.task_instances.table.filter.is_active() { - f.set_cursor_position(app.task_instances.table.filter.cursor_position); + if app.task_instances.table.filter().is_active() { + f.set_cursor_position(app.task_instances.table.filter().cursor_position); } } Panel::Logs => app.logs.render(panel_area, f.buffer_mut()),