assay_workflow/api/
dashboard.rs1use axum::http::{header, StatusCode};
2use axum::response::{IntoResponse, Redirect};
3use axum::routing::get;
4use axum::Router;
5use std::sync::{Arc, LazyLock};
6use std::time::{SystemTime, UNIX_EPOCH};
7
8use crate::api::whitelabel::{render_index, WHITELABEL};
9use crate::api::AppState;
10use crate::store::WorkflowStore;
11
12const INDEX_HTML: &str = include_str!("../dashboard/index.html");
13const THEME_CSS: &str = include_str!("../dashboard/theme.css");
14const STYLE_CSS: &str = include_str!("../dashboard/style.css");
15const APP_JS: &str = include_str!("../dashboard/app.js");
16const WORKFLOWS_JS: &str = include_str!("../dashboard/components/workflows.js");
17const DETAIL_JS: &str = include_str!("../dashboard/components/detail.js");
18const SCHEDULES_JS: &str = include_str!("../dashboard/components/schedules.js");
19const WORKERS_JS: &str = include_str!("../dashboard/components/workers.js");
20const QUEUES_JS: &str = include_str!("../dashboard/components/queues.js");
21const SETTINGS_JS: &str = include_str!("../dashboard/components/settings.js");
22
23const FAVICON_SVG: &str = r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" rx="12" fill="#0d1117"/><text x="32" y="46" font-family="-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif" font-size="44" font-weight="800" fill="#e6662a" text-anchor="middle">A</text></svg>"##;
27
28pub fn router<S: WorkflowStore + 'static>() -> Router<Arc<AppState<S>>> {
29 Router::new()
30 .route("/", get(redirect_to_dashboard))
31 .route("/workflow", get(redirect_to_dashboard))
32 .route("/workflow/", get(index))
33 .route("/workflow/schedules", get(index))
34 .route("/workflow/workers", get(index))
35 .route("/workflow/queues", get(index))
36 .route("/workflow/settings", get(index))
37 .route("/workflow/theme.css", get(theme_css))
38 .route("/workflow/style.css", get(style_css))
39 .route("/workflow/app.js", get(app_js))
40 .route("/workflow/components/workflows.js", get(workflows_js))
41 .route("/workflow/components/detail.js", get(detail_js))
42 .route("/workflow/components/schedules.js", get(schedules_js))
43 .route("/workflow/components/workers.js", get(workers_js))
44 .route("/workflow/components/queues.js", get(queues_js))
45 .route("/workflow/components/settings.js", get(settings_js))
46 .route("/workflow/favicon.svg", get(favicon))
47 .route("/favicon.ico", get(favicon))
48}
49
50const NO_CACHE: &str = "no-cache, no-store, must-revalidate";
54
55static ASSET_VERSION: LazyLock<String> = LazyLock::new(|| {
61 SystemTime::now()
62 .duration_since(UNIX_EPOCH)
63 .map(|d| d.as_secs().to_string())
64 .unwrap_or_else(|_| env!("CARGO_PKG_VERSION").to_string())
65});
66
67fn asset(content_type: &'static str, body: &'static str) -> impl IntoResponse {
68 (
69 StatusCode::OK,
70 [
71 (header::CONTENT_TYPE, content_type),
72 (header::CACHE_CONTROL, NO_CACHE),
73 ],
74 body,
75 )
76}
77
78async fn index() -> impl IntoResponse {
79 let body = render_index(INDEX_HTML, ASSET_VERSION.as_str(), &WHITELABEL);
80 (
81 StatusCode::OK,
82 [
83 (header::CONTENT_TYPE, "text/html; charset=utf-8"),
84 (header::CACHE_CONTROL, NO_CACHE),
85 ],
86 body,
87 )
88}
89
90async fn redirect_to_dashboard() -> Redirect {
91 Redirect::permanent("/workflow/")
92}
93
94async fn theme_css() -> impl IntoResponse { asset("text/css", THEME_CSS) }
95async fn style_css() -> impl IntoResponse { asset("text/css", STYLE_CSS) }
96async fn app_js() -> impl IntoResponse { asset("application/javascript", APP_JS) }
97async fn workflows_js() -> impl IntoResponse { asset("application/javascript", WORKFLOWS_JS) }
98async fn detail_js() -> impl IntoResponse { asset("application/javascript", DETAIL_JS) }
99async fn schedules_js() -> impl IntoResponse { asset("application/javascript", SCHEDULES_JS) }
100async fn workers_js() -> impl IntoResponse { asset("application/javascript", WORKERS_JS) }
101async fn queues_js() -> impl IntoResponse { asset("application/javascript", QUEUES_JS) }
102async fn settings_js() -> impl IntoResponse { asset("application/javascript", SETTINGS_JS) }
103async fn favicon() -> impl IntoResponse { asset("image/svg+xml", FAVICON_SVG) }