rusticity-term 0.1.8

Terminal UI components for Rusticity
use crate::app::App;
use crate::common::{render_pagination_text, ColumnId, InputFocus, SortDirection};
use crate::keymap::Mode;
use crate::rds::instance::Column;
use crate::table::{TableState, TableWindow};
use crate::ui::filter::{render_simple_filter, SimpleFilterConfig};
use crate::ui::table::{expanded_from_columns, render_table, TableConfig};
use crate::ui::{format_title, titled_block};
use ratatui::{prelude::*, widgets::Paragraph};
use rusticity_core::rds::RdsInstance;

pub const FILTER_CONTROLS: [InputFocus; 2] = [InputFocus::Filter, InputFocus::Pagination];

pub struct State {
    pub instances: TableState<RdsInstance>,
    pub input_focus: InputFocus,
    pub column_ids: Vec<ColumnId>,
    pub visible_column_ids: Vec<ColumnId>,
}

impl Default for State {
    fn default() -> Self {
        Self {
            instances: TableState::new(),
            input_focus: InputFocus::Filter,
            column_ids: Column::ids(),
            visible_column_ids: Column::default_visible_ids(),
        }
    }
}

pub fn filtered_instances(app: &App) -> Vec<&RdsInstance> {
    let filter = app.rds_state.instances.filter.to_lowercase();
    let mut instances = app.rds_state.instances.filtered(|instance| {
        [
            &instance.identifier,
            &instance.engine,
            &instance.engine_version,
            &instance.status,
            &instance.instance_class,
            &instance.region,
            &instance.availability_zone,
            &instance.endpoint,
            &instance.arn,
            &instance.cluster_identifier,
            &instance.vpc,
            &instance.subnet_group,
        ]
        .iter()
        .any(|value| value.to_lowercase().contains(&filter))
    });
    instances.sort_by(|a, b| a.identifier.cmp(&b.identifier));
    instances
}

pub fn render_instances(frame: &mut Frame, app: &App, area: Rect) {
    let chunks = Layout::vertical([Constraint::Length(3), Constraint::Min(0)]).split(area);
    let state = &app.rds_state;
    let view = state
        .instances
        .prepare_view(filtered_instances(app), TableWindow::SelectedPage);
    let pagination = render_pagination_text(view.current_page, view.total_pages);
    render_simple_filter(
        frame,
        chunks[0],
        SimpleFilterConfig {
            filter_text: &state.instances.filter,
            placeholder: "Search by DB identifier, engine, status, class, AZ, or endpoint",
            pagination: &pagination,
            mode: app.mode,
            is_input_focused: state.input_focus == InputFocus::Filter,
            is_pagination_focused: state.input_focus == InputFocus::Pagination,
        },
    );
    if state.instances.loading && state.instances.items.is_empty() {
        frame.render_widget(
            Paragraph::new("Loading databases...")
                .block(titled_block("Databases"))
                .alignment(Alignment::Center),
            chunks[1],
        );
        return;
    }
    let columns: Vec<_> = state
        .visible_column_ids
        .iter()
        .filter_map(|id| Column::from_id(id))
        .map(Column::into_table_column)
        .collect();
    render_table(
        frame,
        TableConfig {
            items: view.items,
            selected_index: view.selected_index,
            expanded_index: view.expanded_index,
            columns: &columns,
            sort_column: &Column::Identifier.name(),
            sort_direction: SortDirection::Asc,
            title: format_title(&format!("Databases ({})", view.total_items)),
            area: chunks[1],
            get_expanded_content: Some(Box::new(|instance| {
                expanded_from_columns(&columns, instance)
            })),
            is_active: app.mode == Mode::Normal,
        },
    );
}