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

nordisk/holger-ui

src/pages/admin_queue.rs

//! Admin → **Queue** and **Admission** panes (holger `admit-queue`).
//!
//! Each pane reads its own answer from this console's server, which refuses
//! every session below admin before holger is asked (`ADMIN_READS`). A server
//! built without the job queue answers 501 and the pane says so; nothing is
//! drawn from a guess. The decisions (words, times, the pass-through rule) are
//! in [`crate::admin_queue`], host-tested.

use leptos::prelude::*;

use crate::admin_queue::{
    attempts_words, consumer_lines, door_words, job_state, next_attempt_words, priority_class, stage_words, utc_micros,
    work_words, AdmissionView, DEFAULT_JOBS, JOB_STATES, PASS_THROUGH,
};
use crate::components::feedback::{Badge, ErrorText, Loading, Tone};
use crate::grpc::pb;

fn now_micros() -> i64 {
    (js_sys::Date::now() * 1_000.0) as i64
}

fn status_tone(status: &str) -> Tone {
    match status {
        "done" => Tone::Good,
        "failed" | "interrupted" => Tone::Bad,
        "retry_wait" | "queued" => Tone::Warn,
        _ => Tone::Neutral,
    }
}

/// The Queue pane: a state filter over `JobService.ListJobs`.
#[component]
pub fn QueuePane(generation: RwSignal<u32>) -> impl IntoView {
    let state = RwSignal::new("");
    let jobs = LocalResource::new(move || {
        let _ = generation.get();
        let state = job_state(state.get());
        async move { crate::grpc::answer_or_none(crate::grpc::jobs().list_jobs(pb::ListJobsRequest { kind: String::new(), limit: DEFAULT_JOBS, state }).await) }
    });
    view! {
        <div>
            <nav class="tabs" aria-label="Job state">
                {JOB_STATES.iter().map(|(word, label)| {
                    let word: &'static str = word;
                    view! {
                        <button
                            class=move || if state.get() == word { "tab active" } else { "tab" }
                            aria-pressed=move || (state.get() == word).to_string()
                            on:click=move |_| state.set(word)
                        >{*label}</button>
                    }
                }).collect_view()}
            </nav>
            {move || match jobs.get() {
                None => view! { <Loading what="the job queue"/> }.into_any(),
                Some(Err(e)) => view! { <ErrorText context="job queue" message=e/> }.into_any(),
                Some(Ok(None)) => view! {
                    <p class="count" role="status">{crate::util::not_in_build("Job queue")}</p>
                }.into_any(),
                Some(Ok(Some(r))) => jobs_table(r.jobs, now_micros()),
            }}
        </div>
    }
}

fn jobs_table(jobs: Vec<pb::JobRow>, now: i64) -> AnyView {
    if jobs.is_empty() {
        return view! { <p class="count">"No job in this state."</p> }.into_any();
    }
    let count = format!("{} jobs, newest first", jobs.len());
    view! {
        <div>
            <p class="count">{count}</p>
            <table>
                <thead><tr>
                    <th>"job"</th><th>"kind"</th><th>"target"</th><th>"status · stage"</th><th>"attempts"</th>
                    <th>"reason"</th><th>"next attempt"</th><th>"priority"</th><th>"requested by"</th><th>"started"</th>
                </tr></thead>
                <tbody>
                    {jobs.into_iter().map(|j| view! {
                        <tr>
                            <td class="mono">{j.job_id}</td>
                            <td class="mono">{j.kind}</td>
                            <td class="mono">{j.target}</td>
                            <td><Badge tone=status_tone(&j.status) label=stage_words(&j.status, &j.stage)/></td>
                            <td>{attempts_words(j.attempts, j.max_attempts)}</td>
                            <td>{if j.reason.is_empty() { "—".to_string() } else { j.reason }}</td>
                            <td class="mono">{next_attempt_words(j.next_attempt_micros, now)}</td>
                            <td>{priority_class(j.priority)}</td>
                            <td class="mono">{if j.requested_by.is_empty() { "—".to_string() } else { j.requested_by }}</td>
                            <td class="mono">{utc_micros(j.ts_start_micros)}</td>
                        </tr>
                    }).collect_view()}
                </tbody>
            </table>
        </div>
    }
    .into_any()
}

/// The Admission pane: per consumer, used / limit over the window, the class
/// and the plan's sentence.
#[component]
pub fn AdmissionPane(generation: RwSignal<u32>) -> impl IntoView {
    let adm = LocalResource::new(move || {
        let _ = generation.get();
        async move { crate::admin_queue::admission_view().await }
    });
    view! {
        <div>
            {move || match adm.get() {
                None => view! { <Loading what="admission"/> }.into_any(),
                Some(Err(e)) => view! { <ErrorText context="admission" message=e/> }.into_any(),
                Some(Ok(None)) => view! {
                    <p class="count" role="status">{crate::util::not_in_build("Admission")}</p>
                }.into_any(),
                Some(Ok(Some(v))) => admission_view(v),
            }}
        </div>
    }
}

fn admission_view(v: AdmissionView) -> AnyView {
    let today = now_micros().div_euclid(86_400_000_000);
    let lines = consumer_lines(&v, today);
    let pass = v.usage.policy == PASS_THROUGH;
    let door = door_words(&v.usage);
    let details: Vec<(String, Vec<String>)> = v
        .usage
        .consumers
        .iter()
        .map(|r| (r.consumer.clone(), r.by_work.iter().map(work_words).collect()))
        .collect();
    view! {
        <div>
            <p class="count mono">{door}</p>
            {pass.then(|| view! {
                <p class="banner">"This server runs pass-through: nothing is refused and no limit is enforced, so used / limit is N/A."</p>
            })}
            {v.plan_errors.into_iter().map(|e| view! { <p class="banner banner-warn">{e}</p> }).collect_view()}
            {lines.is_empty().then(|| view! { <p class="count">"The usage book holds no consumer yet."</p> })}
            <table>
                <thead><tr>
                    <th>"consumer"</th><th>"kind"</th><th>"used / limit (30-day window)"</th><th>"class"</th>
                    <th>"admission plan"</th><th>"in flight"</th><th>"by work"</th>
                </tr></thead>
                <tbody>
                    {lines.into_iter().map(|l| {
                        let work = details.iter().find(|(k, _)| *k == l.key).map(|(_, w)| w.join(" · ")).unwrap_or_default();
                        view! {
                            <tr>
                                <td><span>{l.label}</span><br/><span class="count mono">{l.key}</span></td>
                                <td>{l.kind}</td>
                                <td>{l.used_limit}</td>
                                <td>{l.class}</td>
                                <td>{l.why}</td>
                                <td>{l.in_flight.to_string()}</td>
                                <td class="count">{work}</td>
                            </tr>
                        }
                    }).collect_view()}
                </tbody>
            </table>
        </div>
    }
    .into_any()
}