use crate::tui::model::{packs::PacksModel, Model};
use ratatui::{
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, Cell, Paragraph, Row, Table, Wrap},
Frame,
};
pub fn render(f: &mut Frame, model: &mut Model, area: Rect) {
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(40), Constraint::Percentage(60)])
.split(area);
render_pack_list(f, model, chunks[0]);
render_pack_details(f, &model.packs, chunks[1]);
}
fn render_pack_list(f: &mut Frame, model: &mut Model, area: Rect) {
let packs_model = &mut model.packs;
let loaded_pack_path = model
.query
.pack_context
.as_ref()
.map(|ctx| ctx.pack_path.as_str());
if packs_model.loading {
let loading_paragraph = Paragraph::new("Loading query packs...")
.block(Block::default().borders(Borders::ALL).title("Query Packs"))
.style(Style::default().fg(Color::Yellow));
f.render_widget(loading_paragraph, area);
return;
}
if let Some(error) = &packs_model.error {
let error_paragraph = Paragraph::new(format!("Error: {}", error))
.block(Block::default().borders(Borders::ALL).title("Query Packs"))
.style(Style::default().fg(Color::Red));
f.render_widget(error_paragraph, area);
return;
}
if packs_model.packs.is_empty() {
let empty_lines = vec![
Line::from(""),
Line::from("No query packs found"),
Line::from(""),
Line::from(vec![
Span::raw("Create packs in: "),
Span::styled("~/.kql-panopticon/packs/", Style::default().fg(Color::Cyan)),
]),
Line::from(""),
Line::from("Press 'r' to refresh"),
];
let empty_paragraph = Paragraph::new(empty_lines)
.block(Block::default().borders(Borders::ALL).title("Query Packs"))
.style(Style::default().fg(Color::Gray))
.wrap(Wrap { trim: true });
f.render_widget(empty_paragraph, area);
return;
}
let header = Row::new(vec!["Pack", "Status", "Queries"])
.style(
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
)
.bottom_margin(1);
let rows: Vec<Row> = packs_model
.packs
.iter()
.map(|entry| {
let name = entry.get_display_name();
let query_count = entry
.get_query_count()
.map(|c| c.to_string())
.unwrap_or_else(|| "?".to_string());
let name_with_indicator = if entry.load_error.is_some() {
format!("âš {}", name)
} else {
name
};
let is_loaded = loaded_pack_path
.map(|loaded| loaded == entry.relative_path)
.unwrap_or(false);
let status = if is_loaded {
Cell::from("[LOADED]").style(Style::default().fg(Color::Green))
} else {
Cell::from("")
};
Row::new(vec![
Cell::from(name_with_indicator),
status,
Cell::from(query_count),
])
})
.collect();
let widths = [
Constraint::Percentage(55),
Constraint::Percentage(20),
Constraint::Percentage(25),
];
let table = Table::new(rows, widths)
.header(header)
.block(
Block::default()
.borders(Borders::ALL)
.title(format!("Query Packs ({})", packs_model.pack_count())),
)
.highlight_style(
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
)
.highlight_symbol(">> ");
f.render_stateful_widget(table, area, &mut packs_model.table_state);
}
fn render_pack_details(f: &mut Frame, model: &PacksModel, area: Rect) {
let selected_entry = model.get_selected_entry();
if selected_entry.is_none() {
let help_paragraph = Paragraph::new(vec![
Line::from(""),
Line::from("No pack selected"),
Line::from(""),
Line::from("Use Up/Down to select a pack"),
])
.block(
Block::default()
.borders(Borders::TOP | Borders::RIGHT | Borders::BOTTOM)
.title("Pack Details"),
)
.style(Style::default().fg(Color::Gray));
f.render_widget(help_paragraph, area);
return;
}
let entry = selected_entry.unwrap();
if let Some(error) = &entry.load_error {
let error_paragraph = Paragraph::new(vec![
Line::from(""),
Line::from(Span::styled(
"Failed to load pack",
Style::default().fg(Color::Red).add_modifier(Modifier::BOLD),
)),
Line::from(""),
Line::from(error.as_str()),
Line::from(""),
Line::from(Span::styled(
format!("File: {}", entry.relative_path),
Style::default().fg(Color::Gray),
)),
])
.block(
Block::default()
.borders(Borders::TOP | Borders::RIGHT | Borders::BOTTOM)
.title("Pack Details"),
)
.wrap(Wrap { trim: true });
f.render_widget(error_paragraph, area);
return;
}
if entry.pack.is_none() {
let loading_paragraph = Paragraph::new("Press Enter to load pack details...")
.block(
Block::default()
.borders(Borders::TOP | Borders::RIGHT | Borders::BOTTOM)
.title("Pack Details"),
)
.style(Style::default().fg(Color::Yellow));
f.render_widget(loading_paragraph, area);
return;
}
let pack = entry.pack.as_ref().unwrap();
let mut lines = vec![
Line::from(vec![
Span::styled("Name: ", Style::default().add_modifier(Modifier::BOLD)),
Span::raw(&pack.name),
]),
Line::from(""),
];
if let Some(description) = &pack.description {
lines.push(Line::from(vec![
Span::styled(
"Description: ",
Style::default().add_modifier(Modifier::BOLD),
),
Span::raw(description),
]));
lines.push(Line::from(""));
}
if let Some(author) = &pack.author {
lines.push(Line::from(vec![
Span::styled("Author: ", Style::default().add_modifier(Modifier::BOLD)),
Span::raw(author),
]));
}
if let Some(version) = &pack.version {
lines.push(Line::from(vec![
Span::styled("Version: ", Style::default().add_modifier(Modifier::BOLD)),
Span::raw(version),
]));
}
lines.push(Line::from(""));
let queries = pack.get_queries();
lines.push(Line::from(vec![
Span::styled("Queries: ", Style::default().add_modifier(Modifier::BOLD)),
Span::raw(format!("{}", queries.len())),
]));
lines.push(Line::from(""));
for (i, query) in queries.iter().enumerate() {
lines.push(Line::from(vec![
Span::styled(format!(" {}. ", i + 1), Style::default().fg(Color::Yellow)),
Span::raw(&query.name),
]));
if let Some(description) = &query.description {
lines.push(Line::from(vec![
Span::raw(" "),
Span::styled(description, Style::default().fg(Color::Gray)),
]));
}
}
lines.push(Line::from(""));
lines.push(Line::from(""));
lines.push(Line::from(Span::styled(
"Controls:",
Style::default().add_modifier(Modifier::BOLD),
)));
lines.push(Line::from(" Enter - Load first query into editor"));
lines.push(Line::from(" s - Save current query changes to pack"));
lines.push(Line::from(" e - Execute pack on selected workspaces"));
lines.push(Line::from(" r - Refresh pack list"));
let details_paragraph = Paragraph::new(lines)
.block(
Block::default()
.borders(Borders::TOP | Borders::RIGHT | Borders::BOTTOM)
.title("Pack Details"),
)
.wrap(Wrap { trim: true });
f.render_widget(details_paragraph, area);
}