rok-cli 0.6.1

Developer CLI for rok-based Axum applications
use crossterm::event::{KeyCode, KeyEvent};
use ratatui::{
    layout::{Constraint, Rect},
    style::{Color, Modifier, Style},
    widgets::{Block, Borders, Cell, Row, Table, TableState},
    Frame,
};
use sqlx::PgPool;

use crate::tui::app::AppAction;

pub struct FeatureEntry {
    pub name: String,
    pub enabled: bool,
    pub rollout: String,
}

pub struct FeaturesTab {
    pub items: Vec<FeatureEntry>,
    pub state: TableState,
    pub status: String,
}

impl Default for FeaturesTab {
    fn default() -> Self {
        Self {
            items: Vec::new(),
            state: TableState::default(),
            status: "Loading...".into(),
        }
    }
}

impl FeaturesTab {
    pub async fn load(&mut self, pool: &PgPool) {
        match fetch_features(pool).await {
            Ok(items) => {
                self.status = format!("{} flags", items.len());
                self.items = items;
                if self.state.selected().is_none() && !self.items.is_empty() {
                    self.state.select(Some(0));
                }
            }
            Err(e) => {
                self.status = format!("Error: {e}");
                self.items.clear();
            }
        }
    }

    pub fn render(&mut self, frame: &mut Frame, area: Rect) {
        let header = Row::new(vec![
            Cell::from("Status").style(Style::default().add_modifier(Modifier::BOLD)),
            Cell::from("Name").style(Style::default().add_modifier(Modifier::BOLD)),
            Cell::from("Rollout").style(Style::default().add_modifier(Modifier::BOLD)),
        ])
        .height(1);

        let rows: Vec<Row> = self
            .items
            .iter()
            .map(|f| {
                let (icon, color) = if f.enabled {
                    ("🟢 ON ", Color::Green)
                } else {
                    ("🔴 OFF", Color::Red)
                };
                Row::new(vec![
                    Cell::from(icon).style(Style::default().fg(color)),
                    Cell::from(f.name.as_str()),
                    Cell::from(f.rollout.as_str()),
                ])
            })
            .collect();

        let widths = [
            Constraint::Length(8),
            Constraint::Min(25),
            Constraint::Length(10),
        ];

        let title = format!(" Feature Flags — {} ", self.status);
        let table = Table::new(rows, widths)
            .header(header)
            .block(Block::default().title(title).borders(Borders::ALL))
            .highlight_style(Style::default().bg(Color::DarkGray))
            .highlight_symbol("â–¶ ");

        frame.render_stateful_widget(table, area, &mut self.state);
    }

    pub fn handle_key(&mut self, key: KeyEvent) -> AppAction {
        match key.code {
            KeyCode::Up => {
                let i = self.state.selected().unwrap_or(0).saturating_sub(1);
                self.state.select(Some(i));
                AppAction::None
            }
            KeyCode::Down => {
                let max = self.items.len().saturating_sub(1);
                let i = (self.state.selected().unwrap_or(0) + 1).min(max);
                self.state.select(Some(i));
                AppAction::None
            }
            KeyCode::Char(' ') => {
                if let Some(idx) = self.state.selected() {
                    if let Some(item) = self.items.get_mut(idx) {
                        item.enabled = !item.enabled;
                    }
                }
                AppAction::None
            }
            KeyCode::F(5) | KeyCode::Char('r') | KeyCode::Char('R') => AppAction::Reload,
            _ => AppAction::None,
        }
    }
}

async fn fetch_features(pool: &PgPool) -> anyhow::Result<Vec<FeatureEntry>> {
    use sqlx::Row as _;

    let result = sqlx::query("SELECT name, enabled, rollout FROM feature_flags ORDER BY name")
        .fetch_all(pool)
        .await;

    match result {
        Ok(rows) => Ok(rows
            .into_iter()
            .map(|r| FeatureEntry {
                name: r.try_get("name").unwrap_or_default(),
                enabled: r.try_get("enabled").unwrap_or(false),
                rollout: r.try_get("rollout").unwrap_or_else(|_| "100%".to_string()),
            })
            .collect()),
        Err(_) => Ok(vec![FeatureEntry {
            name: "(no feature_flags table found)".into(),
            enabled: false,
            rollout: "-".into(),
        }]),
    }
}