From 5d304a93413885c14fff212e783301587f33e4ff Mon Sep 17 00:00:00 2001 From: metamuffin Date: Sat, 17 May 2025 19:07:50 +0200 Subject: webu --- src/helper.rs | 21 ++++++++++++++ src/main.rs | 6 ++-- src/style.css | 63 +++++++++++++++++++++++++++++++++++++++++ src/webui.rs | 91 ++++++++++++++++++++++++++++++++++++++--------------------- 4 files changed, 147 insertions(+), 34 deletions(-) create mode 100644 src/helper.rs create mode 100644 src/style.css diff --git a/src/helper.rs b/src/helper.rs new file mode 100644 index 0000000..22ccd0d --- /dev/null +++ b/src/helper.rs @@ -0,0 +1,21 @@ +use axum::{ + http::{HeaderValue, header}, + response::{IntoResponse, Response}, +}; + +#[derive(Clone, Copy, Debug)] +#[must_use] +pub struct Css(pub T); + +impl IntoResponse for Css +where + T: IntoResponse, +{ + fn into_response(self) -> Response { + ( + [(header::CONTENT_TYPE, HeaderValue::from_static("text/css"))], + self.0, + ) + .into_response() + } +} diff --git a/src/main.rs b/src/main.rs index 5096ae8..354d303 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,4 @@ +pub mod helper; pub mod webui; pub mod worker_ws; @@ -14,10 +15,10 @@ use tokio::{ net::TcpListener, sync::{RwLock, mpsc::Sender}, }; -use webui::webui; +use webui::{webui, webui_style}; use worker_ws::{WorkerID, WorkerResponse, worker_websocket}; -struct Worker { +pub struct Worker { accept: usize, name: String, sources: Vec, @@ -42,6 +43,7 @@ async fn main() -> Result<()> { state.load().await?; let router = Router::new() .route("/", get(webui)) + .route("/style.css", get(webui_style)) .route("/worker_ws", get(worker_websocket)) .with_state(Arc::new(RwLock::new(state))); let listener = TcpListener::bind("127.0.0.1:8080").await?; diff --git a/src/style.css b/src/style.css new file mode 100644 index 0000000..b0baf42 --- /dev/null +++ b/src/style.css @@ -0,0 +1,63 @@ +:root { + background-color: #111111; + color: white; +} + +.task { + margin: 2px; + padding: 2px; + border-radius: 10px; + border: 2px solid white; +} +.task.queue { + border-color: #6e6eff; +} +.task.loading { + border-color: #ffc36e; +} +.task.complete { + border-color: #6eff70; +} + +.worker { + margin: 2px; + padding: 2px; + border-radius: 10px; + border: 2px solid white; +} +.worker.accepting { + border-color: #6eff70; +} +.worker.busy { + border-color: #ffc36e; +} + +section.tasks { + display: flex; + width: 100%; +} +section h2 { + text-align: center; + margin-bottom: 5px; +} +section.tasks .count { + margin-top: 5px; + text-align: center; + color: #9b9b9b; +} +section.tasks > div { + flex: 1; +} + +ul { + list-style: none; +} + +span.key { + color: #9b9b9b; + font-size: small; +} + +h3 { + margin: 0px; +} diff --git a/src/webui.rs b/src/webui.rs index 9e8b9c3..9b21ab0 100644 --- a/src/webui.rs +++ b/src/webui.rs @@ -1,51 +1,59 @@ -use crate::State; +use crate::{State, helper::Css}; use axum::extract::State as S; use axum::response::Html; +use markup::doctype; use serde_json::{Map, Value}; use std::sync::Arc; -use tokio::sync::RwLock; +use tokio::{fs::read_to_string, sync::RwLock}; + +pub(crate) async fn webui_style() -> Css { + Css(if cfg!(debug_assertions) { + read_to_string("src/style.css").await.unwrap() + } else { + include_str!("style.css").to_string() + }) +} pub(crate) async fn webui(S(state): S>>) -> Html { let g = state.read().await; let doc = markup::new! { + @doctype() html { head { meta[charset="UTF-8"]; - title { "" } + link[rel="stylesheet", href="/style.css"]; + title { "Queue-Server" } } body { section { h2 { "Workers"} ul { @for (id, w) in &g.workers { - li { div { - h3 { @w.name } - span { "ID: " @id } " " - @if w.accept > 0 { - span { "Accepting Jobs (" @w.accept ")" } - } else { - span { "Idle" } - } - }} - }} - } - section { - h2 { "Queued" } - ul { @for key in &g.queue { - li { @key } + li { @Worker { id: *id, w } } }} } - section { - h2 { "Loading" } - ul { @for key in &g.loading { - li { @key } - }} - } - section { - h2 { "Completed" } - ul { @for key in &g.complete { - li { @key } - }} + section.tasks { + div { + h2 { "Queued" } + p.count { @g.queue.len() " tasks" } + ul { @for key in &g.queue { + li { @Task { key, data: g.metadata.get(key).unwrap(), class: "task queue" } } + }} + } + div { + h2 { "Loading" } + p.count { @g.loading.len() " tasks" } + ul { @for key in &g.loading { + li { @Task { key, data: g.metadata.get(key).unwrap(), class: "task loading" } } + }} + } + div { + h2 { "Completed" } + p.count { @g.complete.len() " tasks" } + ul { @for key in &g.complete { + li { @Task { key, data: g.metadata.get(key).unwrap(), class: "task complete" } } + }} + } } } } @@ -54,10 +62,29 @@ pub(crate) async fn webui(S(state): S>>) -> Html { } markup::define!( - Task<'a>(key: &'a str, data: &'a Map) { - div.task { + Worker<'a>(id: u64, w: &'a crate::Worker) { + div[class=worker_class(w)] { + h3 { @w.name } + span { "ID: " @id } " " + @if w.accept > 0 { + span { "Accepting Jobs (" @w.accept ")" } + } else { + span { "Idle" } + } + } + } + Task<'a>(key: &'a str, data: &'a Map, class: &'a str) { + div[class=class] { h3 { @data["title"].as_str().unwrap_or(key) } - spawn.key { @key } + span.key { @key } } } ); + +fn worker_class(w: &crate::Worker) -> &'static str { + if w.accept > 0 { + "worker accepting" + } else { + "worker busy" + } +} -- cgit v1.2.3-70-g09d2