use std::path::PathBuf;
use arrow::array::{Array, StringArray};
use eframe::egui;
use facett_warehousedeck::{
ChartSpec, Facet, GraphLayout, GraphSpec, PaneData, WarehouseDeck, WarehouseRegistry,
};
use crate::warehouse::iceberg::IcebergWarehouse;
use crate::warehouse::Warehouse;
use super::facett_theme::{Theme, RED};
const GRAPH_ROW_CAP: usize = 1500;
const GRID_ROW_CAP: usize = 500;
const CHART_ROW_CAP: usize = 200;
enum DeckSource {
Local(PathBuf),
Remote,
}
pub struct WarehouseDeckPane {
source: DeckSource,
registry: WarehouseRegistry,
deck: Option<WarehouseDeck>,
built: bool,
err: Option<String>,
tables: Vec<String>,
theme: Theme,
}
impl WarehouseDeckPane {
pub fn local(root: PathBuf) -> Self {
Self::with(DeckSource::Local(root))
}
pub fn remote() -> Self {
Self::with(DeckSource::Remote)
}
fn with(source: DeckSource) -> Self {
Self {
source,
registry: WarehouseRegistry::default_nordisk(),
deck: None,
built: false,
err: None,
tables: Vec::new(),
theme: Theme::default(),
}
}
pub fn set_palette(&mut self, t: Theme) {
self.theme = t;
}
pub fn inject_for_test(&mut self, deck: WarehouseDeck) {
self.tables = deck.titles().iter().map(|s| s.to_string()).collect();
self.deck = Some(deck);
self.err = None;
self.built = true;
}
pub fn registry_mut(&mut self) -> &mut WarehouseRegistry {
&mut self.registry
}
pub fn state_json(&self) -> serde_json::Value {
match &self.deck {
Some(d) => {
let mut v = d.state_json();
if let serde_json::Value::Object(map) = &mut v {
map.insert("error".into(), serde_json::json!(self.err));
map.insert("tables_in_warehouse".into(), serde_json::json!(self.tables.len()));
map.insert("built".into(), serde_json::json!(self.built));
}
v
}
None => serde_json::json!({
"built": self.built,
"error": self.err,
"pane_count": 0,
"graph3d_count": 0,
"grid_count": 0,
"chart_count": 0,
"tables_in_warehouse": self.tables.len(),
}),
}
}
fn build(&mut self) {
self.built = true;
let root = match &self.source {
DeckSource::Local(p) => p.clone(),
DeckSource::Remote => {
self.err = Some(
"the 🏢 Warehouse Deck (3D) needs local warehouse access — use the \
🗄 Warehouse browser when pointed at a remote server"
.into(),
);
return;
}
};
let wh = match IcebergWarehouse::open_read_only(&root) {
Ok(w) => w,
Err(e) => {
self.err = Some(format!(
"open warehouse failed: {e:#}\n(a running nornir-server holds the redb lock; \
a snapshot fallback is used when possible)"
));
return;
}
};
let tables = wh.table_names().unwrap_or_default();
self.tables = tables.clone();
let has = |t: &str| tables.iter().any(|x| x == t);
let mut deck = WarehouseDeck::new("🏢 Warehouse Deck").with_cols(2);
if has("call_edges") {
if let Some(g) = edge_graph(&wh, "call_edges", "caller_path", "callee_ident", GraphLayout::Force) {
deck.add_for("call graph", "call_edges", PaneData::Graph(g));
}
}
if has("dep_graph_edges") {
if let Some(g) = edge_graph(&wh, "dep_graph_edges", "from_repo", "to_repo", GraphLayout::Sphere) {
deck.add_for("dependency graph", "dep_graph_edges", PaneData::Graph(g));
}
}
if has("scip_call_edges") {
if let Some(g) = edge_graph(&wh, "scip_call_edges", "caller_symbol", "callee_symbol", GraphLayout::Force) {
deck.add_for("resolved call graph", "scip_call_edges", PaneData::Graph(g));
}
}
for t in ["bench_runs", "test_outcomes", "vuln_findings", "sbom_components"] {
if !has(t) {
continue;
}
if let Ok(batches) = wh.scan_limited(t, GRID_ROW_CAP) {
let rows: usize = batches.iter().map(|b| b.num_rows()).sum();
if rows > 0 {
deck.add_for(t, t, PaneData::Grid(batches));
}
}
}
for t in ["git_heat_facts", "bench_telemetry", "release_lineage"] {
if !has(t) {
continue;
}
if let Some(c) = numeric_chart(&wh, t) {
deck.add_for(t, t, PaneData::Chart(c));
break;
}
}
if deck.pane_count() == 0 {
self.err = Some(
"no renderable tables yet — populate the workspace (nornir populate) so \
call_edges / dep_graph_edges / bench_runs accrue rows"
.into(),
);
}
self.deck = Some(deck);
}
pub fn draw(&mut self, ui: &mut egui::Ui) {
if !self.built {
self.build();
}
ui.horizontal(|ui| {
ui.heading("🏢 Warehouse Deck");
ui.weak(format!("· {} tables in warehouse", self.tables.len()));
if ui.button("↻ rebuild").clicked() {
self.built = false;
self.deck = None;
}
});
ui.separator();
if let Some(err) = &self.err {
ui.colored_label(RED, err);
}
if let Some(deck) = &mut self.deck {
deck.ui(ui);
}
}
}
fn edge_graph(wh: &IcebergWarehouse, table: &str, src: &str, dst: &str, layout: GraphLayout) -> Option<GraphSpec> {
let batches = wh.scan_limited(table, GRAPH_ROW_CAP).ok()?;
let mut idx: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
let mut nodes: Vec<(String, egui::Color32)> = Vec::new();
let mut edges: Vec<(usize, usize)> = Vec::new();
for b in &batches {
let (Ok(si), Ok(di)) = (b.schema().index_of(src), b.schema().index_of(dst)) else {
continue;
};
let (Some(sc), Some(dc)) = (
b.column(si).as_any().downcast_ref::<StringArray>(),
b.column(di).as_any().downcast_ref::<StringArray>(),
) else {
continue;
};
for r in 0..b.num_rows() {
if sc.is_null(r) || dc.is_null(r) {
continue;
}
let s = intern(sc.value(r), &mut nodes, &mut idx);
let d = intern(dc.value(r), &mut nodes, &mut idx);
edges.push((s, d));
}
}
if nodes.is_empty() {
return None;
}
Some(GraphSpec::new(nodes, edges).with_layout(layout))
}
fn intern(
label: &str,
nodes: &mut Vec<(String, egui::Color32)>,
idx: &mut std::collections::HashMap<String, usize>,
) -> usize {
if let Some(&i) = idx.get(label) {
return i;
}
let i = nodes.len();
nodes.push((label.to_string(), facett_warehousedeck::hash_color(label)));
idx.insert(label.to_string(), i);
i
}
fn numeric_chart(wh: &IcebergWarehouse, table: &str) -> Option<ChartSpec> {
use arrow::array::{Float64Array, Int32Array, Int64Array};
let batches = wh.scan_limited(table, CHART_ROW_CAP).ok()?;
let first = batches.first()?;
let (col, name) = first.schema().fields().iter().enumerate().find_map(|(i, f)| {
use arrow::datatypes::DataType::*;
matches!(f.data_type(), Int64 | Int32 | Float64).then(|| (i, f.name().clone()))
})?;
let mut points: Vec<(f64, f64)> = Vec::new();
let mut x = 0.0;
for b in &batches {
if b.num_columns() <= col {
continue;
}
let column = b.column(col);
for r in 0..b.num_rows() {
let y = if let Some(a) = column.as_any().downcast_ref::<Int64Array>() {
a.is_valid(r).then(|| a.value(r) as f64)
} else if let Some(a) = column.as_any().downcast_ref::<Int32Array>() {
a.is_valid(r).then(|| a.value(r) as f64)
} else if let Some(a) = column.as_any().downcast_ref::<Float64Array>() {
a.is_valid(r).then(|| a.value(r))
} else {
None
};
if let Some(y) = y {
points.push((x, y));
x += 1.0;
}
}
}
if points.is_empty() {
return None;
}
Some(ChartSpec::bars(vec![(name, points)]))
}