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