Heracles/src/routes.rs

197 lines
5.9 KiB
Rust
Raw Normal View History

2024-02-12 15:50:35 -06:00
// Copyright 2023 Jeremy Wall
2024-02-01 15:25:51 -05:00
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::{sync::Arc, collections::HashMap};
use axum::{
extract::{Path, Query, State},
response::Response,
routing::get,
Json, Router,
};
// https://maud.lambda.xyz/getting-started.html
use maud::{html, Markup};
use tracing::{debug, error};
use chrono::prelude::*;
use crate::dashboard::{Dashboard, Graph, GraphSpan};
use crate::query::{to_samples, QueryResult};
type Config = State<Arc<Vec<Dashboard>>>;
2024-02-01 15:25:51 -05:00
pub async fn graph_query(
State(config): Config,
Path((dash_idx, graph_idx)): Path<(usize, usize)>,
Query(query): Query<HashMap<String, String>>,
) -> Json<QueryResult> {
debug!("Getting data for query");
let dash = config.get(dash_idx).expect("No such dashboard index");
let graph = dash
.graphs
.get(graph_idx)
.expect(&format!("No such graph in dasboard {}", dash_idx));
let query_span = {
if query.contains_key("start") && query.contains_key("duration") && query.contains_key("step_duration")
{
if let Ok(start) = DateTime::parse_from_rfc3339(&query["start"]) {
Some(GraphSpan {
start: start.to_utc(),
duration: query["duration"].clone(),
step_duration: query["step_duration"].clone(),
})
} else {
error!(?query, "Invalid date time in start for query string");
None
}
} else {
None
}
};
let data = to_samples(
graph
.get_query_connection(if query_span.is_some() { &query_span } else { &dash.span })
.get_results()
.await
.expect("Unable to get query results")
.data()
.clone(),
);
Json(data)
}
pub fn mk_api_routes(config: Arc<Vec<Dashboard>>) -> Router<Config> {
// Query routes
2024-02-13 16:15:19 -06:00
// TODO(zaphar): Allow passing the timespan in via query
Router::new().route(
"/dash/:dash_idx/graph/:graph_idx",
get(graph_query).with_state(config),
)
2024-02-01 15:25:51 -05:00
}
pub fn graph_component(dash_idx: usize, graph_idx: usize, graph: &Graph) -> Markup {
let graph_id = format!("graph-{}-{}", dash_idx, graph_idx);
2024-02-07 19:18:30 -06:00
let graph_data_uri = format!("/api/dash/{}/graph/{}", dash_idx, graph_idx);
html!(
div {
h2 { (graph.title) }
timeseries-graph uri=(graph_data_uri) id=(graph_id) label=(graph.name_label) { }
}
)
}
pub async fn graph_ui(
State(config): State<Config>,
Path((dash_idx, graph_idx)): Path<(usize, usize)>,
) -> Markup {
let graph = config
.get(dash_idx)
.expect("No such dashboard")
.graphs
.get(graph_idx)
.expect("No such graph");
graph_component(dash_idx, graph_idx, graph)
}
pub async fn dash_ui(State(config): State<Config>, Path(dash_idx): Path<usize>) -> Markup {
// TODO(zaphar): Should do better http error reporting here.
let dash = config.get(dash_idx).expect("No such dashboard");
let graph_iter = dash
.graphs
.iter()
.enumerate()
.collect::<Vec<(usize, &Graph)>>();
html!(
h1 { (dash.title) }
@for (idx, graph) in &graph_iter {
(graph_component(dash_idx, *idx, *graph))
}
)
}
pub fn mk_ui_routes(config: Arc<Vec<Dashboard>>) -> Router<Config> {
Router::new()
.route(
"/dash/:dash_idx",
get(dash_ui).with_state(State(config.clone())),
)
.route(
"/dash/:dash_idx/graph/:graph_idx",
get(graph_ui).with_state(State(config)),
)
2024-02-01 15:25:51 -05:00
}
pub async fn index(State(config): State<Config>) -> Markup {
2024-02-01 15:25:51 -05:00
html! {
html {
head {
title { ("Heracles - Prometheus Unshackled") }
}
body {
script src="/js/plotly.js" { }
script src="/js/htmx.js" { }
2024-02-07 19:18:30 -06:00
script src="/js/lib.js" { }
(app(State(config.clone())).await)
2024-02-01 15:25:51 -05:00
}
}
}
}
pub async fn app(State(config): State<Config>) -> Markup {
let titles = config
.iter()
.map(|d| d.title.clone())
.enumerate()
.collect::<Vec<(usize, String)>>();
html! {
div {
// Header menu
ul {
@for title in &titles {
li hx-get=(format!("/ui/dash/{}", title.0)) hx-target="#dashboard" { (title.1) }
}
}
// dashboard display
div id="dashboard" { }
}
}
}
pub fn javascript_response(content: &str) -> Response<String> {
Response::builder()
.header("Content-Type", "text/javascript")
.body(content.to_string())
.expect("Invalid javascript response")
}
2024-02-07 19:18:30 -06:00
// TODO(jwall): Should probably hook in one of the axum directory serving crates here.
pub async fn htmx() -> Response<String> {
javascript_response(include_str!("../static/htmx.min.js"))
}
pub async fn plotly() -> Response<String> {
javascript_response(include_str!("../static/plotly-2.27.0.min.js"))
}
2024-02-07 19:18:30 -06:00
pub async fn lib() -> Response<String> {
javascript_response(include_str!("../static/lib.js"))
}
pub fn mk_js_routes(config: Arc<Vec<Dashboard>>) -> Router<Config> {
Router::new()
.route("/plotly.js", get(plotly))
2024-02-07 19:18:30 -06:00
.route("/lib.js", get(lib))
.route("/htmx.js", get(htmx))
.with_state(State(config))
}