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(),
}]),
}
}