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

nordisk/holger-ui

src/app.rs

//! The console shell: topbar (brand, nav, session, health) and the routed
//! views. Every view is a URL you can share:
//!
//! ```text
//! /                                   the overview again (the console's home)
//! /repositories                       repository list
//! /repo/:name                         one repository: browse · archive · upload
//! /artifact?repo=&ns=&name=&version=  artifact detail (query-addressed:
//!                                     raw artifact names contain slashes)
//! /search?name=&…&repo=&prop=k=v      search — axes in the query, auto-run
//! /security?queue=                    the intake queues (the security dashboard)
//! /security/artifact?ns=&name=&version=  one artifact's state, evidence and buttons
//! /replicate                          replication (preview-first)
//! /migrate                            migrate a Nexus/Artifactory in (preview-first)
//! /admin                              the admin view (an admin's session only;
//!                                     the server refuses the data to others)
//! /overview                           the estate at a glance: where a login lands
//!                                     (`/` itself is holger's front page on a
//!                                     served console)
//! /flow?open=                         the artifact flow, the console's only picture
//! /apps                               the watched applications (a Security tab)
//! /apps/:name                         one application's lamp, its reasons and call paths
//! /mine                               my repositories: list, make a private one,
//!                                     client setup (cargo, pip, npm, maven)
//! /tokens                             my tokens: mint (shown once), list, revoke
//! /account                            who holger says this session is; token
//!                                     paste for a holger without passkeys
//! /login                              NOT a route here: the static passkey page
//!                                     (`assets/login.html`) the server serves
//! ```
//!
//! A metered console (feature `metered`, A28 M13) has one address more, reached through the
//! router's fallback ([`unmatched`]) and not as a route, so an open console has no such
//! address: `pages::plan::PATH`, the person's room, plan and orders.
//!
//! A directly-opened URL carries no view state, so each view fetches what it
//! needs from its address alone. Serving note: any host must fall back to
//! index.html for unknown paths (Trunk's dev server does; a reverse proxy
//! needs the same rule).

use leptos::prelude::*;
use leptos::task::spawn_local;
use leptos_router::components::{Route, Router, Routes};
use leptos_router::path;

use crate::grpc;
use crate::pages::{
    admin::Admin, artifact_detail::ArtifactDetail, dashboard::Dashboard, estate_board::Flow, login::Login,
    migrate::Migrate, mine::{MyRepositories, MyTokens}, replication::Replicate,
    repositories::Repositories,
    repository_detail::RepositoryDetail, search::Search,
    security::{SecurityArtifact, SecurityDashboard},
    watched_app::WatchedApp, watched_apps::WatchedApps,
};

#[component]
pub fn App() -> impl IntoView {
    // Flipped on login/out so the topbar asks holger again (the session itself is the console
    // server's HttpOnly cookie, A34-38: no script here can read it).
    let logged_in = RwSignal::new(false);

    // holger's view of who is logged in — its passkey door's session answer
    // for this tab's bearer — refetched whenever login state flips. ONE answer
    // for the topbar and /account (`crate::session`).
    let status = LocalResource::new(move || {
        let _ = logged_in.get();
        async move { crate::session::fetch_status().await }
    });
    let shown = move || {
        let s = status.get().flatten();
        crate::session::shown(s.as_ref(), s.as_ref().is_some_and(|s| s.token_held))
    };
    // The way to buy (A11 fix 5), a metered console's only: holger's sign-up door, asked with
    // this tab's session.
    let buy = buy_slot(logged_in);

    // Which environment this console is pointed at — the gRPC door's host.
    let environment = crate::util::environment_label(&grpc::base_url());
    // Which security domain that Holger serves (its RON `domain:`, lane HOL-CDS), as holger
    // says it on every answer (`X-Holger-Domain`).
    let domain = LocalResource::new(|| async { grpc::domain().await });

    view! {
        <Router>
            <a class="skip" href="#main">"Skip to content"</a>
            <header class="topbar">
                <a class="brand" href="/">
                    // holger's mark: the helmeted head, a 124×128 lossless webp
                    // that trunk copies into the bundle (`assets/holger-mark.webp`). Decorative:
                    // the word beside it names holger, once (HCON-3).
                    <img class="mark" src="/holger-mark.webp" width="124" height="128" alt="" aria-hidden="true"/>
                    <span class="brand-product">"holger"</span>
                    <span class="brand-sub">"console"</span>
                </a>
                // One entry per job (lane HOL-UI-FIX): the page's entry is marked wherever in it
                // the person is (`util::menu_entry`). Admin is offered to an admin's session only;
                // the role is the SERVER's, and holger refuses the data to everyone else whatever
                // this link does (`crate::admin`).
                <Menu admin=Signal::derive(move || crate::admin::admin_visible(&shown()))/>
                // Search lives in the header: a plain GET of the Search page, which runs it.
                <form class="topsearch" role="search" action="/search" method="get">
                    <label class="vh" for="topsearch">"Search artifacts by name"</label>
                    <input id="topsearch" type="search" name="name" placeholder="Search artifacts…" autocomplete="off"/>
                </form>
                <span class="envchip" title="the server this console talks to">{environment}</span>
                {move || domain.get().flatten().map(|d| view! {
                    <span class="domainchip" title="the security domain this Holger serves (its domain: label)">{d}</span>
                })}
                <span class="authbox">
                    {move || {
                        use crate::session::Shown;
                        let out = move |ev: leptos::ev::MouseEvent| {
                            ev.prevent_default();
                            spawn_local(async move {
                                match crate::session::logout().await {
                                    Ok(()) => {
                                        logged_in.set(false);
                                        // A passkey console's way back in is its login page.
                                        if let Some(w) = web_sys::window() {
                                            let _ = w.location().assign(crate::session::LOGIN_PAGE);
                                        }
                                    }
                                    // holger did not end the session (RA5-11): said, and still signed in.
                                    Err(why) => {
                                        if let Some(w) = web_sys::window() {
                                            let _ = w.alert_with_message(&format!("You are still signed in: holger did not end your session ({why}). Try again."));
                                        }
                                    }
                                }
                            });
                        };
                        match shown() {
                            Shown::Session { user, role } => view! {
                                {buy}
                                <a class="ident" href="/account" title="session details">
                                    <span class="ok">{user}</span>
                                    <span class="count">{format!(" · {role}")}</span>
                                </a>
                                <a href="#" on:click=out>"Log out"</a>
                            }.into_any(),
                            // ONE road in on a passkey console: the server's page.
                            Shown::LoggedOut { login } => view! { <a href=login rel="external">"Log in"</a> }.into_any(),
                            Shown::TokenOnly { has_token: true } => view! {
                                <a class="ident" href="/account" title="session details"><span class="ok">"token"</span></a>
                                <a href="#" on:click=move |ev| {
                                    ev.prevent_default();
                                    spawn_local(async move {
                                        let _ = crate::session::hold_token("").await;
                                        logged_in.set(false);
                                    });
                                }>"Log out"</a>
                            }.into_any(),
                            Shown::TokenOnly { has_token: false } => view! { <a href="/account">"Log in"</a> }.into_any(),
                            // Not answered yet: say nothing rather than guess.
                            Shown::Unknown => view! { <span class="count">"…"</span> }.into_any(),
                        }
                    }}
                </span>
                <HealthBadge/>
            </header>
            <main id="main">
                <Routes fallback=unmatched>
                    // The console's home is the Overview (HOL-UI-FIX: one page, not a second one
                    // with the same cards). On a box `/` is holger's front page anyway.
                    <Route path=path!("/") view=Dashboard/>
                    <Route path=path!("/overview") view=Dashboard/>
                    // The artifact flow: the console's ONLY picture (HOL-SVG-RON, HOL-FLOW-SMART).
                    <Route path=path!("/flow") view=Flow/>
                    // Where the flow's lamp on a watched application leads (H32-BOARD-LAMP).
                    <Route path=path!("/apps/:name") view=WatchedApp/>
                    // The watched applications: the list, the declaration, each OpenVEX (lane hui-audit).
                    <Route path=path!("/apps") view=WatchedApps/>
                    <Route path=path!("/repositories") view=Repositories/>
                    <Route path=path!("/repo/:name") view=RepositoryDetail/>
                    <Route path=path!("/artifact") view=ArtifactDetail/>
                    <Route path=path!("/search") view=Search/>
                    // The security workflow: holger's intake queues, and one
                    // artifact's evidence with the four buttons.
                    <Route path=path!("/security") view=SecurityDashboard/>
                    <Route path=path!("/security/artifact") view=SecurityArtifact/>
                    // The cross-repo verbs, replication and migration: tabs of the
                    // Repositories entry (HOL-UI-FIX), one address each (lane hui-audit).
                    <Route path=path!("/replicate") view=Replicate/>
                    // Migrate: the one-time registry pull. Preview-first like
                    // its neighbour, and it cannot create repositories -- see
                    // pages/migrate.rs for why that shapes the whole page.
                    <Route path=path!("/migrate") view=Migrate/>
                    // The admin view: its data is refused by the server to every
                    // session below admin; this route only renders the answer.
                    <Route path=path!("/admin") view=Admin/>
                    <Route path=path!("/account") view=move || view! { <Login logged_in=logged_in/> }/>
                    // The buyer's pages: the repositories holger says this person
                    // has, and their tokens for package clients. (A metered console's
                    // third is reached through the fallback: `unmatched`.)
                    <Route path=path!("/mine") view=MyRepositories/>
                    <Route path=path!("/tokens") view=MyTokens/>
                </Routes>
            </main>
        </Router>
    }
}

/// **An address no route names.** One exception, and only in a metered console: the
/// person's room, plan and orders (`pages::plan`, the money scanner's `METERED_MODULES`),
/// so an open console has no such address. gunnar-ui reaches its vendor's return page the
/// same way. Reactive on the path, so moving between two such addresses redraws.
fn unmatched() -> impl IntoView {
    #[cfg(feature = "metered")]
    let path = leptos_router::hooks::use_location().pathname;
    move || {
        #[cfg(feature = "metered")]
        if let Some(page) = crate::pages::plan::at_its_address(&path.get()) {
            return page;
        }
        view! { <p class="err">"no such page"</p> }.into_any()
    }
}

/// **The way to buy in the topbar** (A11 fix 5, Gunnar's Account page "Buy a plan" / "Grow
/// your account"): holger's sign-up door, asked with this tab's session whenever the login
/// state flips. A metered console's only.
#[cfg(feature = "metered")]
fn buy_slot(logged_in: RwSignal<bool>) -> impl Fn() -> AnyView + Copy + Send + Sync + 'static {
    let buy = LocalResource::new(move || {
        let _ = logged_in.get();
        async move { crate::session::fetch_buy_link().await }
    });
    move || match buy.get().flatten() {
        Some((href, words)) => view! { <a class="btn primary" rel="external" href=href>{words}</a> }.into_any(),
        None => ().into_any(),
    }
}

/// An open console's topbar has no way to buy: nothing is asked and nothing is drawn.
#[cfg(not(feature = "metered"))]
fn buy_slot(_logged_in: RwSignal<bool>) -> impl Fn() -> AnyView + Copy + Send + Sync + 'static {
    || ().into_any()
}

/// **The menu** (lane HOL-UI-FIX): [`crate::util::MENU`], the entry of the page the person is
/// on marked (`aria-current`), Admin last for an admin's session.
#[component]
fn Menu(admin: Signal<bool>) -> impl IntoView {
    let location = leptos_router::hooks::use_location();
    let here = Memo::new(move |_| crate::util::menu_entry(&location.pathname.get()));
    let entry = move |href: &'static str, label: &'static str| {
        view! { <a href=href aria-current=move || (here.get() == Some(href)).then_some("page")>{label}</a> }
    };
    view! {
        <nav class="nav" aria-label="Main">
            {crate::util::MENU.iter().map(|(href, label)| entry(href, label)).collect_view()}
            {move || admin.get().then(|| entry(crate::admin::PAGE, "Admin"))}
        </nav>
    }
}

#[component]
fn HealthBadge() -> impl IntoView {
    let health = LocalResource::new(|| async {
        grpc::admin()
            .health(grpc::pb::HealthRequest {})
            .await
            .map(|r| r.into_inner())
            .map_err(|s| grpc::status_text(&s))
    });

    view! {
        <span class="health">
            {move || match health.get() {
                None => view! { <span class="pending">"connecting…"</span> }.into_any(),
                // One word in the bar; the version and the uptime ride its title (the Overview's
                // "This server" card says them in full).
                Some(Ok(h)) => view! {
                    <span class="ok" title=format!("{} · {} · up {}", h.status, h.version, crate::util::uptime_words(h.uptime_seconds))>
                        <span class="dot" aria-hidden="true"></span>{h.status.clone()}
                    </span>
                }.into_any(),
                Some(Err(e)) => view! {
                    <span class="err" title=format!("unreachable: {e}")><span class="dot" aria-hidden="true"></span>"unreachable"</span>
                }.into_any(),
            }}
        </span>
    }
}