Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/knut

crates/knut-popviz/src/app.rs

//! **The popviz desktop UI itself** (feature `gui`).
//!
//! This used to live inside `src/main.rs`, which made it unreachable from any
//! test: an integration test can link the *library*, never the binary. The
//! consequence was that `knut-popviz`'s "robot" tests drove [`PopvizState`]
//! through `update_json` and asserted `state_json` — a state round-trip that
//! never touched a widget, so nothing in this crate had ever asserted that a
//! click reaches the pixels, or that the progress bar's *fill* tracks
//! [`PopvizState::fraction`].
//!
//! So the app lives here and `main.rs` is the thin `eframe::run_native` shell
//! over it — one writer, both paths. [`PopvizApp::ui`] takes a plain
//! `&mut egui::Ui` (no `eframe::Frame`) so an `egui_kittest` harness can drive
//! the identical widget tree the desktop window shows; the `eframe::App` impl
//! just forwards to it.

use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};

use crate::{LoadParams, Phase, PopvizState, Target, run_load};

/// The popviz application: the shared, introspectable [`PopvizState`] plus the
/// UI-local widget bindings (the text fields and the target toggle).
pub struct PopvizApp {
    /// The load state the background thread writes and the UI renders. Shared
    /// so a test can pose the UI at any phase without running a load.
    pub state: Arc<Mutex<PopvizState>>,
    pub params: LoadParams,
    pub scale_str: String,
    /// UI target toggle + warehouse path (assembled into `params.target` at start).
    pub to_iceberg: bool,
    pub warehouse: String,
    pub running: bool,
    /// Kick off the load on the first frame (`--autostart` / `KNUT_POPVIZ_AUTOSTART`)
    /// so the GUI can be driven headlessly by a robot test.
    pub autostart: bool,
    pub state_path: std::path::PathBuf,
    last_dump: Instant,
}

impl PopvizApp {
    pub fn new(params: LoadParams) -> Self {
        let state = Arc::new(Mutex::new(PopvizState::new(params.scale)));
        let (to_iceberg, warehouse) = match &params.target {
            Target::Iceberg { warehouse } => (true, warehouse.clone()),
            // The GUI drives a local warehouse; a catalog lake is knut-popload's.
            Target::Falkor | Target::IcebergRest(_) => (false, "/tmp/knut_popviz_warehouse".to_string()),
        };
        Self {
            scale_str: params.scale.to_string(),
            to_iceberg,
            warehouse,
            state,
            params,
            running: false,
            autostart: std::env::var("KNUT_POPVIZ_AUTOSTART").is_ok()
                || std::env::args().any(|a| a == "--autostart"),
            state_path: std::env::var("KNUT_POPVIZ_STATE")
                .map(Into::into)
                .unwrap_or_else(|_| std::env::temp_dir().join("knut_popviz_state.json")),
            last_dump: Instant::now(),
        }
    }

    /// Always-on introspection: overwrite the state JSON (~300 ms throttle).
    fn dump_if_due(&mut self) {
        if self.last_dump.elapsed() < Duration::from_millis(300) {
            return;
        }
        self.last_dump = Instant::now();
        let json = self.state.lock().unwrap().state_json();
        if let Ok(bytes) = serde_json::to_vec_pretty(&json) {
            let _ = std::fs::write(&self.state_path, bytes);
        }
    }

    /// Reset the state and spawn the load on a background thread (which wakes the
    /// UI after each batch). Shared by the Start button and `--autostart`.
    pub fn start_load(&mut self, ctx: &egui::Context) {
        self.params.target = if self.to_iceberg {
            Target::Iceberg {
                warehouse: self.warehouse.clone(),
            }
        } else {
            Target::Falkor
        };
        *self.state.lock().unwrap() = PopvizState::new(self.params.scale);
        self.running = true;
        let st = self.state.clone();
        let params = self.params.clone();
        let ctx2 = ctx.clone();
        std::thread::spawn(move || run_load(params, st, move || ctx2.request_repaint()));
    }

    /// **The whole popviz UI**, against a plain `egui::Ui`.
    ///
    /// This is the single widget-tree writer: the desktop window (through the
    /// `eframe::App` impl below) and the headless `egui_kittest` robot render
    /// exactly these widgets, so a robot assertion about a click or a fill width
    /// is an assertion about the shipped app.
    pub fn ui(&mut self, ui: &mut egui::Ui) {
        let ctx = ui.ctx().clone();
        self.dump_if_due();
        if self.autostart && !self.running {
            self.autostart = false;
            self.start_load(&ctx);
        }
        let s = self.state.lock().unwrap().clone();

        // While loading, keep ticking so the bars animate even between wakeups.
        if self.running && !matches!(s.phase, Phase::Done | Phase::Failed) {
            ctx.request_repaint_after(Duration::from_millis(200));
        }

        egui::CentralPanel::default().show_inside(ui, |ui| {
            ui.heading("knut · population bulk loader");
            ui.label("knut-native bulk insert — no falkordb_bulk_loader");
            ui.add_space(6.0);

            ui.horizontal(|ui| {
                ui.label("FalkorDB:");
                ui.text_edit_singleline(&mut self.params.url);
                ui.label("graph:");
                ui.add(egui::TextEdit::singleline(&mut self.params.graph).desired_width(80.0));
                ui.label("scale:");
                if ui
                    .add(egui::TextEdit::singleline(&mut self.scale_str).desired_width(70.0))
                    .changed()
                    && let Ok(v) = self.scale_str.parse()
                {
                    self.params.scale = v;
                }
                ui.checkbox(&mut self.params.drop, "drop first");
            });
            ui.horizontal(|ui| {
                ui.label("target:");
                ui.selectable_value(&mut self.to_iceberg, false, "FalkorDB");
                ui.selectable_value(&mut self.to_iceberg, true, "Iceberg");
                if self.to_iceberg {
                    ui.label("warehouse:");
                    ui.add(egui::TextEdit::singleline(&mut self.warehouse).desired_width(220.0));
                }
            });

            let idle_or_done = !self.running || matches!(s.phase, Phase::Done | Phase::Failed);
            if ui
                .add_enabled(idle_or_done, egui::Button::new("▶  Start load"))
                .clicked()
            {
                self.start_load(&ctx);
            }

            ui.separator();
            ui.label(format!(
                "→ {}   ·   phase: {}   ·   {:.0} rows/s   ·   {:.1}s",
                s.target,
                s.phase.label(),
                s.rows_per_s(),
                s.elapsed_s
            ));
            // The overall progress bar. Its FILL is the only pixel-level
            // projection of `PopvizState::fraction()` in the app, which is why
            // the robot suite measures its fill edge rather than re-reading the
            // fraction it was handed.
            ui.add(
                egui::ProgressBar::new(s.fraction())
                    .show_percentage()
                    .text(format!("{} / {}", s.loaded(), s.total())),
            );
            if let Some(e) = &s.error {
                ui.colored_label(egui::Color32::from_rgb(220, 80, 80), e);
            }

            ui.separator();
            // Per-label progress via the shared facett-pop component (cut-over).
            use facett_pop::Facet;
            let mut pv = facett_pop::ProgressView::new("ingest");
            pv.phase = s.phase.label().to_string();
            pv.items = s
                .labels
                .iter()
                .map(|l| facett_pop::ProgressItem {
                    label: l.name.clone(),
                    done: l.loaded as u64,
                    total: l.total as u64,
                })
                .collect();
            pv.ui(ui);
        });
    }
}

impl eframe::App for PopvizApp {
    // eframe 0.34: `ui` is the required entry; `update(ctx)` is deprecated.
    fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
        PopvizApp::ui(self, ui);
    }
}