use std::path::PathBuf;
use eframe::egui::{self, Color32, ScrollArea};
use crate::warehouse::iceberg::{IcebergWarehouse, TablePreview};
const PREVIEW_LIMIT: usize = 500;
const CELL_MAX_CHARS: usize = 80;
enum WhSource {
Local(PathBuf),
Remote { endpoint: String, token: String },
}
pub struct WarehouseBrowser {
source: WhSource,
tables: Vec<String>,
selected: Option<String>,
preview: Option<Result<TablePreview, String>>,
err: Option<String>,
listed: bool,
}
impl WarehouseBrowser {
pub fn local(root: PathBuf) -> Self {
Self::with(WhSource::Local(root))
}
pub fn remote(endpoint: String, token: String) -> Self {
Self::with(WhSource::Remote { endpoint, token })
}
fn with(source: WhSource) -> Self {
Self { source, tables: Vec::new(), selected: None, preview: None, err: None, listed: false }
}
pub(crate) fn refresh_tables(&mut self) {
self.listed = true;
let res = match &self.source {
WhSource::Local(root) => {
IcebergWarehouse::open(root).and_then(|wh| wh.table_names())
}
WhSource::Remote { endpoint, token } => super::remote::fetch_tables(endpoint, token),
};
match res {
Ok(names) => {
self.tables = names;
self.err = None;
}
Err(e) => {
self.err = Some(match &self.source {
WhSource::Local(_) => format!(
"open warehouse failed: {e:#}\n(a running nornir-server holds the redb \
lock — stop it to browse locally, or point the viz at the server)"
),
WhSource::Remote { .. } => format!("Warehouse.Tables failed: {e:#}"),
});
}
}
}
fn load_preview(&mut self, table: &str) {
let res = match &self.source {
WhSource::Local(root) => {
IcebergWarehouse::open(root).and_then(|wh| wh.scan_preview(table, PREVIEW_LIMIT))
}
WhSource::Remote { endpoint, token } => {
super::remote::scan_table(endpoint, token, table, PREVIEW_LIMIT as u32)
}
};
self.preview = Some(res.map_err(|e| format!("{e:#}")));
}
pub fn draw(&mut self, ui: &mut egui::Ui) {
if !self.listed {
self.refresh_tables();
}
egui::SidePanel::left("wh_tables").default_width(240.0).show_inside(ui, |ui| {
ui.horizontal(|ui| {
ui.heading(format!("tables ({})", self.tables.len()));
if ui.button("↻").clicked() {
self.refresh_tables();
}
});
ui.separator();
ScrollArea::vertical().auto_shrink([false, false]).show(ui, |ui| {
let tables = self.tables.clone();
for name in &tables {
let selected = self.selected.as_deref() == Some(name.as_str());
if ui.selectable_label(selected, name).clicked() {
self.selected = Some(name.clone());
self.load_preview(name);
}
}
});
});
egui::CentralPanel::default().show_inside(ui, |ui| {
if let Some(err) = &self.err {
ui.colored_label(Color32::RED, err);
return;
}
let Some(table) = self.selected.clone() else {
ui.label("select a table on the left to view its rows");
return;
};
match &self.preview {
Some(Ok(p)) => {
ui.horizontal(|ui| {
ui.heading(&table);
ui.label(format!(
"· {} rows shown (cap {PREVIEW_LIMIT}) · {} columns",
p.rows.len(),
p.columns.len()
));
});
ui.separator();
ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| {
egui::Grid::new("wh_grid")
.striped(true)
.num_columns(p.columns.len())
.show(ui, |ui| {
for c in &p.columns {
ui.strong(c);
}
ui.end_row();
for row in &p.rows {
for cell in row {
ui.label(truncate(cell));
}
ui.end_row();
}
});
});
}
Some(Err(e)) => {
ui.colored_label(Color32::RED, e);
}
None => {
ui.label("loading…");
}
}
});
}
}
fn truncate(s: &str) -> String {
if s.chars().count() > CELL_MAX_CHARS {
let head: String = s.chars().take(CELL_MAX_CHARS).collect();
format!("{head}…")
} else {
s.to_string()
}
}