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

nordisk/nornir

nornir-demo/src/zone_demo.rs

//! **The nornir zone/jera showcase surface** — nornir's GUI dropped onto the SAME
//! common framework korp now runs (L5 — consume the shared widgets, never
//! hand-roll):
//!
//! * a **central [`JeraCentral`] panel** — the shared jobs/zones/infra surface
//!   (`facett-jobview` over the jera ledger + the per-job **zone column**), seeded
//!   here with representative nornir jobs (release wave · bench · docs) so the panel
//!   lists jobs WITH their zone; and
//! * a **🧰 Tools menu → 🎬 Demo / Zone…** entry that opens the shared
//!   [`ZonePane`] (default [`Zone::Demo`]) — pick a zone, see the constellation's
//!   default infra, press **Generate**, watch inline status.
//!
//! There is deliberately **NO bare "demo" button** anywhere (`state_json` pins
//! `has_demo_button:false`); the only zone/generate affordance is the Tools-menu
//! pane, exactly like korp. Because the whole surface is one [`Facet`], the
//! `facett-wasm-demo` harness mounts it on a native window and a wasm canvas from
//! the same source, and a headless robot drives it through `update_json` /
//! `state_json` (see `tests/zone_demo_robot.rs`).
//!
//! ## nornir's zone infra is a no-op (a question logged for Rickard)
//! korp's Generate brings up the 2 default containers (FalkorDB + Spark) as jera
//! jobs. **nornir has no containerized zone infra of its own** — its warehouse
//! (skade) and knowledge/catalog are *embedded* (redb), not services a zone brings
//! up. So the wired [`ZoneGenerator`](facett_zoneui::ZoneGenerator) here is an
//! honest **no-op** that states so IN the pane, rather than faking a bring-up. If
//! nornir should instead share the constellation's FalkorDB/Spark default infra
//! (e.g. to host its knowledge graph), that is a follow-up wiring decision — logged
//! in `workspace_nordisk/nornir_agents_problems.md`.

use facett_zoneui::{
    ContainerStatus, Facet, JeraCentral, JobEntry, JobStatus, Step, Zone, ZonePane, ZonedJob,
};
use serde_json::{json, Value};

use crate::panel::ReleaseDepDag;

/// Which surface the central area shows. Defaults to [`View::Jera`] so the shared
/// [`JeraCentral`] IS the central panel (the framework requirement); the existing
/// release dep-DAG demo stays reachable (additive, no regression).
#[derive(Clone, Copy, PartialEq, Eq)]
enum View {
    /// The shared jera-central jobs/zones/infra panel.
    Jera,
    /// nornir's release dependency-DAG demo panel (the pre-existing surface).
    ReleaseDag,
}

impl View {
    fn key(self) -> &'static str {
        match self {
            View::Jera => "jera",
            View::ReleaseDag => "release_dag",
        }
    }
}

/// The composite nornir GUI surface: a menu bar (🧰 Tools → Demo/Zone · View
/// switch), a central [`JeraCentral`] panel, and the Tools-menu [`ZonePane`]. One
/// [`Facet`] so the wasm/native harness mounts it and a robot drives it.
pub struct ZoneDemo {
    /// The shared central jobs/zones/infra panel (jera ledger + zone column).
    jera: JeraCentral,
    /// The shared zone-populate pane the Tools menu opens.
    zone_pane: ZonePane,
    /// nornir's release dep-DAG demo (kept — additive, reachable via the View menu).
    dag: ReleaseDepDag,
    /// Whether the Tools-menu zone pane is open.
    show_zone_pane: bool,
    /// Which surface the central area shows.
    view: View,
}

impl Default for ZoneDemo {
    fn default() -> Self {
        Self::new()
    }
}

impl ZoneDemo {
    /// Build the surface: seed [`JeraCentral`] with representative nornir jobs and
    /// wire the [`ZonePane`]'s Generate to nornir's honest no-op generator.
    pub fn new() -> Self {
        Self {
            jera: JeraCentral::from_jobs("Jera", sample_nornir_jobs()),
            zone_pane: ZonePane::new("Populate a zone").with_generator(nornir_no_infra_generator),
            dag: ReleaseDepDag::from_fixture(),
            show_zone_pane: false,
            view: View::Jera,
        }
    }
}

/// nornir's honest **no-op** zone generator: nornir has no containerized zone infra,
/// so Generate brings nothing up — it returns a single, clearly-labelled status
/// saying so (fail-*clear*, never a faked bring-up). Wired in place of korp's jera
/// `submit_container_job`.
fn nornir_no_infra_generator(zone: Zone) -> Vec<ContainerStatus> {
    vec![
        ContainerStatus::new("(no infra)", "nornir has no default zone infra", 0)
            .step(Step::Done)
            .detail(format!(
                "nothing to bring up for the {} zone — nornir's warehouse (skade) + \
                 catalog are embedded (redb), not containers",
                zone.key(),
            )),
    ]
}

/// A few representative nornir jobs for the seeded [`JeraCentral`] — nornir's real
/// job vocabulary (release wave · bench · docs), namespaced into the demo zone so
/// the panel shows the zone column populated (one job also in the test zone to prove
/// the column varies).
fn sample_nornir_jobs() -> Vec<ZonedJob> {
    vec![
        ZonedJob::new(
            JobEntry {
                target: "constellation".into(),
                started_micros: 3_000_000,
                detail: "publish wave 3 — edda + facett leaves".into(),
                ..JobEntry::new("release-wave-3", "release_wave", JobStatus::Running)
            },
            Zone::Demo,
        ),
        ZonedJob::new(
            JobEntry {
                target: "znippy-zoomies".into(),
                started_micros: 2_900_000,
                elapsed_ms: Some(41_200),
                detail: "HEAVY bench — 808 MB/s multicore".into(),
                ..JobEntry::new("bench-heavy", "bench_run", JobStatus::Done)
            },
            Zone::Demo,
        ),
        ZonedJob::new(
            JobEntry {
                target: "nornir".into(),
                started_micros: 2_800_000,
                detail: "docs book — skade-flow SVG → book.pdf".into(),
                ..JobEntry::new("docs-book", "docs_book_svg", JobStatus::Queued)
            },
            Zone::Demo,
        ),
        ZonedJob::new(
            JobEntry {
                target: "nornir".into(),
                started_micros: 2_700_000,
                detail: "release gate — edition/lint/test (isolated ports/namespace)".into(),
                ..JobEntry::new("gate-test", "release_gate", JobStatus::Queued)
            },
            Zone::Test,
        ),
    ]
}

impl Facet for ZoneDemo {
    fn title(&self) -> &str {
        "nornir — zones & jera"
    }

    fn ui(&mut self, ui: &mut egui::Ui) {
        // ── menu bar: 🧰 Tools → Demo/Zone · View switch (NO bare demo button) ──
        // `menu_button` works in any Ui; a horizontal row is the menu bar (avoids the
        // deprecated `egui::menu::bar` wrapper — same idiom facett uses).
        ui.horizontal(|ui| {
            ui.menu_button("🧰 Tools", |ui| {
                if ui
                    .button("🎬 Demo / Zone…")
                    .on_hover_text("Pick a zone and bring up its default infra")
                    .clicked()
                {
                    self.show_zone_pane = true;
                    ui.close();
                }
            });
            ui.menu_button("View", |ui| {
                if ui
                    .radio(self.view == View::Jera, "Jera (jobs · zones · infra)")
                    .clicked()
                {
                    self.view = View::Jera;
                    ui.close();
                }
                if ui
                    .radio(self.view == View::ReleaseDag, "Release dependency-DAG")
                    .clicked()
                {
                    self.view = View::ReleaseDag;
                    ui.close();
                }
            });
            ui.separator();
            ui.label(egui::RichText::new("nornir — zones & jera").strong());
        });
        ui.separator();

        // ── the Tools-menu zone pane (shared ZonePane), when open ──────────────
        if self.show_zone_pane {
            egui::Frame::group(ui.style()).show(ui, |ui| {
                ui.horizontal(|ui| {
                    ui.label(egui::RichText::new("🎬 Demo / Zone").strong());
                    ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
                        if ui.button("✖ Close").clicked() {
                            self.show_zone_pane = false;
                        }
                    });
                });
                // Honest note: nornir's Generate is a no-op (no containerized infra).
                ui.label(
                    egui::RichText::new(
                        "nornir has no containerized zone infra of its own — Generate is a \
                         no-op here (the infra list below is the constellation's shared default).",
                    )
                    .italics()
                    .weak(),
                );
                ui.add_space(4.0);
                // The SHARED widget renders + drives itself (view + update via the Elm bridge).
                Facet::ui(&mut self.zone_pane, ui);
            });
            ui.separator();
        }

        // ── central surface ────────────────────────────────────────────────────
        match self.view {
            View::Jera => self.jera.ui(ui),
            View::ReleaseDag => self.dag.ui(ui),
        }
    }

    fn state_json(&self) -> Value {
        json!({
            "app": "nornir-zone-demo",
            // The framework guard: NO bare demo button — the only generate path is
            // the Tools-menu zone pane (RED if a demo button reappears).
            "has_demo_button": false,
            "view": self.view.key(),
            "zone_pane_open": self.show_zone_pane,
            // The shared widgets' own observable state (consumed verbatim).
            "zone_pane": self.zone_pane.state_json(),
            "jera": self.jera.state_json(),
        })
    }

    /// Route a headless-drive message: app-level control (`{"app":"open_zone_pane"}`
    /// / `"close_zone_pane"` / `"view_jera"` / `"view_dag"`), else forward straight
    /// to the shared [`ZonePane`] (`{"select_zone":"test"}`, `"generate"`, …) so a
    /// robot drives the exact live surface.
    fn update_json(&mut self, msg_json: &str) {
        if let Ok(v) = serde_json::from_str::<Value>(msg_json) {
            if let Some(app) = v.get("app").and_then(|a| a.as_str()) {
                match app {
                    "open_zone_pane" => self.show_zone_pane = true,
                    "close_zone_pane" => self.show_zone_pane = false,
                    "view_jera" => self.view = View::Jera,
                    "view_dag" => self.view = View::ReleaseDag,
                    _ => {}
                }
                return;
            }
        }
        // Not an app-control message → drive the shared zone pane.
        self.zone_pane.update_json(msg_json);
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    /// The surface defaults to the Jera central panel, the pane closed, NO demo button.
    #[test]
    fn defaults_to_jera_central_with_no_demo_button() {
        let d = ZoneDemo::new();
        let s = d.state_json();
        assert_eq!(s["app"], "nornir-zone-demo");
        assert_eq!(s["view"], "jera");
        assert_eq!(s["zone_pane_open"], false);
        assert_eq!(
            s["has_demo_button"], false,
            "no bare demo button — the guard"
        );
        // The seeded jera-central panel lists nornir jobs WITH their zone.
        assert_eq!(s["jera"]["zones"]["release-wave-3"], "demo");
        assert_eq!(s["jera"]["zones"]["gate-test"], "test");
        assert_eq!(s["jera"]["zone_summary"]["demo"], 3);
        assert_eq!(s["jera"]["zone_summary"]["test"], 1);
    }

    /// Opening the pane + selecting a zone lands in `state_json`; the default zone is demo.
    #[test]
    fn tools_menu_opens_the_pane_and_zone_selects() {
        let mut d = ZoneDemo::new();
        assert_eq!(
            d.state_json()["zone_pane"]["zone"],
            "demo",
            "default zone is demo"
        );
        d.update_json(r#"{"app":"open_zone_pane"}"#);
        assert_eq!(d.state_json()["zone_pane_open"], true);
        d.update_json(r#"{"select_zone":"test"}"#);
        assert_eq!(
            d.state_json()["zone_pane"]["zone"],
            "test",
            "select pins the zone"
        );
    }

    /// **RED-when-broken: Generate is nornir's honest no-op** — it seeds the "(no
    /// infra)" status, never a faked FalkorDB/Spark bring-up. If Generate silently
    /// started faking infra (or stopped driving the pane), this fails.
    #[test]
    fn generate_is_a_clear_no_op() {
        let mut d = ZoneDemo::new();
        d.update_json(r#""generate""#);
        let containers = d.state_json()["zone_pane"]["containers"]
            .as_array()
            .expect("containers[] present")
            .clone();
        assert_eq!(
            containers.len(),
            1,
            "the single no-op status, not 2 faked containers"
        );
        assert_eq!(containers[0]["name"], "(no infra)");
        assert!(
            containers[0]["detail"]
                .as_str()
                .unwrap()
                .contains("nothing to bring up"),
            "the no-op states nornir has no default zone infra",
        );
    }
}