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",
);
}
}