nornir-demo/src/panel.rs
//! The **nornir release dependency-DAG** panel, as a [`facett_core::Facet`].
//!
//! This mirrors nornir's real release dep-DAG view (`src/viz/release_depdag.rs`), which
//! renders the crate publish-order graph — nodes = crates, edges = depends-on, coloured
//! by per-crate registry state and badged with the publish **wave** index — with the
//! shared [`facett_graphview::DecoratedGraphView`] pane (rings / badges / pan-drag /
//! scroll-zoom / click-to-light-downstream). Here that SAME component is fed a
//! **nornir-shaped fixture** (a sample constellation publish graph) so the panel runs
//! with no backend, no network and no filesystem — browser-safe, exactly what the wasm
//! demo needs.
//!
//! ## INTERIM
//! nornir's real dep-DAG is assembled by `crate::release::depdag::ReleaseDepDag` from
//! live `nornir doctor` data, wrapped by `ReleaseDepDagPane` — inside the monolithic
//! `nornir` crate (server + MCP + reqwest + …), not yet a standalone, backend-free wasm
//! **component**. Until it is, this demo ships facett's own render of the graph — the
//! identical `DecoratedGraphView` component nornir uses, over a fixture — **labelled
//! interim**. When `ReleaseDepDagPane` becomes a mountable wasm component, swap this
//! `ReleaseDepDag` panel for it: the harness API (`FacetApp` → `NativeDemo`/`WebDemo`)
//! is unchanged. The colour + badge mapping below is copied 1:1 from
//! `release_depdag.rs::state_color` so the demo looks like the real thing.
use std::collections::HashMap;
use facett_core::Facet;
use facett_graphview::{
Color, DecoratedGraphView, Decorations, GraphEdge, GraphModel, GraphNode, NodeDecoration, Pos,
};
use serde_json::json;
/// World-space column / row gaps — the same spacing constants
/// `src/viz/release_depdag.rs` lays the real graph out with.
const COL_GAP: f32 = 230.0;
const ROW_GAP: f32 = 74.0;
/// A crate's registry state — the sample-fixture stand-in for nornir's real
/// `crate::release::registry::RegistryState`. Drives the node fill colour exactly as
/// `release_depdag.rs::state_color` does.
#[derive(Clone, Copy, PartialEq, Eq)]
enum State {
/// On the current publish plan (green).
WillPublish,
/// Live registry == local (grey).
AlreadyPublished,
/// Local is behind / diverged from the registry (red).
Desync,
/// Never published (blue).
Unpublished,
}
impl State {
fn as_str(self) -> &'static str {
match self {
State::WillPublish => "will_publish",
State::AlreadyPublished => "already_published",
State::Desync => "desync",
State::Unpublished => "unpublished",
}
}
/// Registry-state → chip fill colour — 1:1 with
/// `src/viz/release_depdag.rs::state_color`.
fn color(self) -> Color {
match self {
State::WillPublish => Color::rgb(90, 200, 140),
State::AlreadyPublished => Color::rgb(120, 130, 140),
State::Desync => Color::rgb(230, 90, 90),
State::Unpublished => Color::rgb(90, 150, 230),
}
}
}
/// One fixture crate node: name, registry state, publish **wave** (badge), and its
/// laid-out `(col, row)` — the column rank IS the reverse publish order (deps to the
/// right), matching the real view.
struct CrateNode {
name: &'static str,
state: State,
wave: u32,
col: i32,
row: i32,
}
/// A sample nordisk-constellation publish graph: the crates a `nornir release` would
/// stage, coloured by registry state and layered into publish waves (leaves / already-
/// published deps on the right, the top-level consumers on the left).
fn fixture_nodes() -> Vec<CrateNode> {
use State::*;
vec![
// col 0 — the top-level consumer.
CrateNode {
name: "nornir",
state: WillPublish,
wave: 5,
col: 0,
row: 2,
},
// col 1 — nornir's sibling workspace crates.
CrateNode {
name: "brokkr",
state: WillPublish,
wave: 4,
col: 1,
row: 0,
},
CrateNode {
name: "dwarfs",
state: WillPublish,
wave: 4,
col: 1,
row: 4,
},
// col 2 — the mid-tier libraries.
CrateNode {
name: "edda",
state: Desync,
wave: 3,
col: 2,
row: 1,
},
CrateNode {
name: "skade",
state: Unpublished,
wave: 3,
col: 2,
row: 3,
},
// col 3 — reusable facett facets + the test-matrix engine.
CrateNode {
name: "facett-wasm-demo",
state: WillPublish,
wave: 2,
col: 3,
row: 0,
},
CrateNode {
name: "facett-jobview",
state: AlreadyPublished,
wave: 2,
col: 3,
row: 2,
},
CrateNode {
name: "nornir-testmatrix",
state: WillPublish,
wave: 2,
col: 3,
row: 4,
},
// col 4 — the leaves (published first).
CrateNode {
name: "facett-graphview",
state: AlreadyPublished,
wave: 1,
col: 4,
row: 1,
},
CrateNode {
name: "facett-core",
state: AlreadyPublished,
wave: 0,
col: 4,
row: 2,
},
CrateNode {
name: "znippy-common",
state: AlreadyPublished,
wave: 0,
col: 4,
row: 4,
},
]
}
/// Fixture depends-on edges (`from` consumes `to`), including one **dashed**
/// transitive-closure edge (`nornir → facett-core`) to exercise the dashed-edge render
/// the real view uses for its `show_transitive` toggle.
fn fixture_edges() -> Vec<(&'static str, &'static str, bool)> {
vec![
("nornir", "brokkr", false),
("nornir", "dwarfs", false),
("nornir", "edda", false),
("nornir", "facett-graphview", false),
("nornir", "nornir-testmatrix", false),
("nornir", "facett-wasm-demo", false),
("nornir", "facett-core", true), // transitive (dashed)
("brokkr", "facett-jobview", false),
("brokkr", "facett-core", false),
("dwarfs", "skade", false),
("dwarfs", "facett-core", false),
("edda", "facett-core", false),
("edda", "znippy-common", false),
("skade", "znippy-common", false),
("facett-wasm-demo", "facett-core", false),
("facett-jobview", "facett-core", false),
("nornir-testmatrix", "facett-core", false),
("facett-graphview", "facett-core", false),
]
}
/// The nornir release dependency-DAG panel: the crate publish graph rendered with
/// `facett_graphview::DecoratedGraphView` (nornir's own component). A single [`Facet`]
/// the [`facett_wasm_demo`](facett_wasm_demo) harness mounts on a native window and a
/// wasm canvas from the same code.
pub struct ReleaseDepDag {
/// The decorated pan/zoom graph pane.
view: DecoratedGraphView,
/// The publish waves (`waves[i]` = the crates published in wave `i`).
waves: Vec<Vec<String>>,
/// Per-crate `{ name, state, wave }` summary — the robot-observable registry map.
node_states: Vec<serde_json::Value>,
node_count: usize,
edge_count: usize,
}
impl Default for ReleaseDepDag {
fn default() -> Self {
Self::from_fixture()
}
}
impl ReleaseDepDag {
/// Build the panel from the built-in nornir fixture (a sample constellation publish
/// graph). No backend, no network, no filesystem — browser-safe.
pub fn from_fixture() -> Self {
let nodes = fixture_nodes();
let mut model = GraphModel::default();
let mut deco: HashMap<String, NodeDecoration> = HashMap::new();
// Publish waves, gathered in index order for the observable state.
let mut waves: Vec<Vec<String>> = Vec::new();
let mut node_states: Vec<serde_json::Value> = Vec::new();
for n in &nodes {
node_states.push(json!({
"name": n.name,
"state": n.state.as_str(),
"wave": n.wave,
}));
let fill = n.state.color();
model.nodes.push(GraphNode {
id: n.name.to_string(),
label: n.name.to_string(),
fill,
stroke: Color::WHITE,
pos: Pos::new(n.col as f32 * COL_GAP, n.row as f32 * ROW_GAP),
});
// The publish-wave index as a corner badge, tinted with the fill (exactly
// what release_depdag decorates each node with).
deco.insert(
n.name.to_string(),
NodeDecoration {
ring: None,
badge: Some(n.wave.to_string()),
badge_color: Some(fill),
scale: None,
},
);
let w = n.wave as usize;
if waves.len() <= w {
waves.resize(w + 1, Vec::new());
}
waves[w].push(n.name.to_string());
}
for (from, to, transitive) in fixture_edges() {
model.edges.push(GraphEdge {
from: from.to_string(),
to: to.to_string(),
color: if transitive {
Color::rgb(120, 120, 140)
} else {
Color::rgb(180, 185, 200)
},
dashed: transitive,
label: None,
});
}
let node_count = model.nodes.len();
let edge_count = model.edges.len();
let view = DecoratedGraphView::new("release-depdag")
.with_model(model)
.with_decorations(Decorations {
nodes: deco,
edges: Vec::new(),
});
Self {
view,
waves,
node_states,
node_count,
edge_count,
}
}
}
impl Facet for ReleaseDepDag {
fn title(&self) -> &str {
"nornir — release dependency-DAG"
}
fn ui(&mut self, ui: &mut egui::Ui) {
ui.heading("nornir — release dependency-DAG");
ui.label(
egui::RichText::new(
"INTERIM demo: facett's own render of nornir's release dep-DAG view \
(src/viz/release_depdag.rs), over a sample constellation publish graph \
(no backend). Node fill = registry state (green will-publish / grey \
already-published / red desync / blue unpublished); the corner badge is \
the publish wave. Drag to pan, scroll to zoom, click a node to light its \
downstream. Upgrades to nornir's real panel when it becomes a wasm \
component.",
)
.italics()
.weak(),
);
ui.separator();
self.view.ui(ui);
}
fn state_json(&self) -> serde_json::Value {
json!({
"panel": "nornir-release-depdag",
"interim": true,
"node_count": self.node_count,
"edge_count": self.edge_count,
"waves": self.waves,
"nodes": self.node_states,
// The DecoratedGraphView's own observable state (model + pan/zoom/selection).
"view": self.view.state_json(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dag_reports_nornir_shaped_state() {
let d = ReleaseDepDag::from_fixture();
let s = d.state_json();
assert_eq!(s["panel"], "nornir-release-depdag");
assert_eq!(s["interim"], true);
// The fixture graph is fully wired.
assert_eq!(s["node_count"], 11);
assert_eq!(s["edge_count"], 18);
// wave 0 carries the leaves published first (facett-core / znippy-common).
let wave0 = s["waves"][0].as_array().unwrap();
assert!(wave0.iter().any(|c| c == "facett-core"));
}
#[test]
fn state_color_matches_the_real_view() {
// 1:1 with src/viz/release_depdag.rs::state_color.
assert_eq!(State::WillPublish.color(), Color::rgb(90, 200, 140));
assert_eq!(State::Desync.color(), Color::rgb(230, 90, 90));
assert_eq!(State::Unpublished.color(), Color::rgb(90, 150, 230));
assert_eq!(State::AlreadyPublished.color(), Color::rgb(120, 130, 140));
}
#[test]
fn every_edge_endpoint_is_a_real_node() {
let names: std::collections::HashSet<&str> =
fixture_nodes().iter().map(|n| n.name).collect();
for (from, to, _) in fixture_edges() {
assert!(names.contains(from), "edge from unknown node {from}");
assert!(names.contains(to), "edge to unknown node {to}");
}
}
}