diff --git a/src/main.rs b/src/main.rs index eab586b..cdb66b6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -64,6 +64,7 @@ async fn main() -> anyhow::Result<()> { let router = Router::new() // JSON api endpoints .nest("/js", routes::mk_js_routes(config.clone())) + .nest("/static", routes::mk_static_routes(config.clone())) .nest("/api", routes::mk_api_routes(config.clone())) // HTMX ui component endpoints .nest("/ui", routes::mk_ui_routes(config.clone())) diff --git a/src/query.rs b/src/query.rs index ba246fc..7185a63 100644 --- a/src/query.rs +++ b/src/query.rs @@ -21,18 +21,20 @@ use prometheus_http_query::{ use serde::{Deserialize, Serialize}; use tracing::debug; -#[derive(Deserialize, Clone)] +#[derive(Deserialize, Clone, Debug)] pub enum QueryType { Range, Scalar, } +#[derive(Debug)] pub struct TimeSpan { pub end: DateTime, pub duration: chrono::Duration, pub step_seconds: i64, } +#[derive(Debug)] pub struct QueryConn<'conn> { source: &'conn str, query: &'conn str, diff --git a/src/routes.rs b/src/routes.rs index ae052bc..0ca1186 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -115,7 +115,7 @@ pub async fn dash_ui(State(config): State, Path(dash_idx): Path) .collect::>(); html!( h1 { (dash.title) } - span-selector {} + span-selector class="row-flex" {} @for (idx, graph) in &graph_iter { (graph_component(dash_idx, *idx, *graph)) } @@ -144,6 +144,7 @@ pub async fn index(State(config): State) -> Markup { script src="/js/plotly.js" { } script src="/js/htmx.js" { } script src="/js/lib.js" { } + link rel="stylesheet" href="/static/site.css" { } (app(State(config.clone())).await) } } @@ -157,15 +158,16 @@ pub async fn app(State(config): State) -> Markup { .enumerate() .collect::>(); html! { - div { - // Header menu - ul { - @for title in &titles { - li hx-get=(format!("/ui/dash/{}", title.0)) hx-target="#dashboard" { (title.1) } + div class="row-flex" { + div class="flex-item-shrink" { + // 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" { } + div class="flex-item-grow" id="dashboard" { } } } } @@ -197,3 +199,10 @@ pub fn mk_js_routes(config: Arc>) -> Router { .route("/htmx.js", get(htmx)) .with_state(State(config)) } + +pub fn mk_static_routes(config: Arc>) -> Router { + Router::new() + .route("/site.css", get(|| async { return include_str!("../static/site.css"); })) + .with_state(State(config)) +} + diff --git a/static/lib.js b/static/lib.js index 478f5b2..16ea915 100644 --- a/static/lib.js +++ b/static/lib.js @@ -12,6 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. +function getCssVariableValue(variableName) { + return getComputedStyle(document.documentElement).getPropertyValue(variableName); +} + class TimeseriesGraph extends HTMLElement { #uri; #width; @@ -33,10 +37,13 @@ class TimeseriesGraph extends HTMLElement { this.#height = 600; this.#pollSeconds = 30; this.#menuContainer = this.appendChild(document.createElement('div')); + // TODO(jwall): These should probably be done as template clones so we have less places + // to look for class attributes. + this.#menuContainer.setAttribute("class", "row-flex"); this.#targetNode = this.appendChild(document.createElement("div")); } - static observedAttributes = ['uri', 'width', 'height', 'poll-seconds', 'end', 'duration', 'step-duration']; + static observedAttributes = ['uri', 'width', 'height', 'poll-seconds', 'end', 'duration', 'step-duration', 'd3-tick-format']; attributeChangedCallback(name, _oldValue, newValue) { switch (name) { @@ -181,6 +188,7 @@ class TimeseriesGraph extends HTMLElement { for (var opt of this.#filterLabels[key]) { const optElement = document.createElement("option"); optElement.setAttribute("value", opt); + optElement.setAttribute("selected", true); optElement.innerText = opt; select.appendChild(optElement); } @@ -217,6 +225,12 @@ class TimeseriesGraph extends HTMLElement { this.populateFilterData(labels); } } + if (subplot.Scalar) { + for (const triple of subplot.Scalar) { + const labels = triple[0]; + this.populateFilterData(labels); + } + } } } @@ -233,6 +247,14 @@ class TimeseriesGraph extends HTMLElement { var layout = { displayModeBar: false, responsive: true, + plot_bgcolor: getCssVariableValue('--paper-background-color').trim(), + paper_bgcolor: getCssVariableValue('--paper-background-color').trim(), + font: { + color: getCssVariableValue('--text-color').trim() + }, + xaxis: { + gridcolor: getCssVariableValue("--accent-color") + } }; var traces = []; for (var subplot_idx in data) { @@ -254,6 +276,7 @@ class TimeseriesGraph extends HTMLElement { // https://plotly.com/javascript/reference/layout/yaxis/ layout["yaxis" + subplotCount] = { anchor: yaxis, + gridcolor: getCssVariableValue("--accent-color"), tickformat: meta["d3_tick_format"] || this.#d3TickFormat }; const series = triple[2]; @@ -277,8 +300,18 @@ class TimeseriesGraph extends HTMLElement { } } else if (subplot.Scalar) { // https://plotly.com/javascript/reference/bar/ - for (const triple of subplot.Scalar) { + layout["yaxis"] = { + tickformat: this.#d3TickFormat, + gridcolor: getCssVariableValue("--accent-color") + }; + loopScalar: for (const triple of subplot.Scalar) { const labels = triple[0]; + for (var label in labels) { + var show = this.#filteredLabelSets[label]; + if (show && !show.includes(labels[label])) { + continue loopScalar; + } + } const meta = triple[1]; const series = triple[2]; var trace = { @@ -328,6 +361,7 @@ class SpanSelector extends HTMLElement { connectedCallback() { const self = this; + // TODO(jwall): We should probably show a loading indicator of some kind. self.#updateInput.onclick = function(_evt) { self.updateGraphs() }; diff --git a/static/site.css b/static/site.css new file mode 100644 index 0000000..b7e6922 --- /dev/null +++ b/static/site.css @@ -0,0 +1,92 @@ +:root { + /* Base colors */ + --background-color: #FFFFFF; /* Light background */ + --text-color: #333333; /* Dark text for contrast */ + --paper-background-color: #F0F0F0; + --accent-color: #6200EE; /* For buttons and interactive elements */ + + /* Graph colors */ + --graph-color-1: #007BFF; /* Blue */ + --graph-color-2: #28A745; /* Green */ + --graph-color-3: #DC3545; /* Red */ + --graph-color-4: #FFC107; /* Yellow */ + --graph-color-5: #17A2B8; /* Cyan */ + --graph-color-6: #6C757D; /* Gray */ + + /* Axis and grid lines */ + --axis-color: #CCCCCC; + --grid-line-color: #EEEEEE; + + /* Tooltip background */ + --tooltip-background-color: #FFFFFF; + --tooltip-text-color: #000000; +} + +@media (prefers-color-scheme: dark) { + :root { + /* Solarized Dark Base Colors */ + --background-color: #002b36; /* base03 */ + --paper-background-color: #003c4a; + --text-color: #839496; /* base0 */ + --accent-color: #268bd2; /* blue */ + + /* Graph colors - Solarized Accent Colors */ + --graph-color-1: #b58900; /* yellow */ + --graph-color-2: #cb4b16; /* orange */ + --graph-color-3: #dc322f; /* red */ + --graph-color-4: #d33682; /* magenta */ + --graph-color-5: #6c71c4; /* violet */ + --graph-color-6: #2aa198; /* cyan */ + + /* Axis and grid lines */ + --axis-color: #586e75; /* base01 */ + --grid-line-color: #073642; /* base02 */ + + /* Tooltip background */ + --tooltip-background-color: #002b36; /* base03 */ + --tooltip-text-color: #839496; /* base0 */ + } +} + +body { + background-color: var(--background-color); + color: var(--text-color); +} + +input, textarea, select, option, button { + background-color: var(--paper-background-color); + border: 1px solid var(--accent-color); + color: var(--text-color); + padding: 8px; + border-radius: 4px; +} + +body * { + padding-left: .3em; + padding-right: .3em; +} + +.column-flex { + display: flex; + flex-direction: column; +} + +.row-flex { + display: flex; + flex-direction: row; +} + +.flex-item-grow { + flex: 1 0 auto; +} + +.flex-item-shrink { + flex: 0 1 auto; +} + +timeseries-graph { + background-color: var(--paper-background-color); + border-radius: 4px; + display: flex; + flex-direction: column; +}