From 7f30d87d757fb6c2cc0ccbf1b476047c8eec3195 Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Wed, 14 Feb 2024 19:45:28 -0600 Subject: [PATCH] ux: end is better than start --- examples/example_dashboards.yaml | 6 +-- src/dashboard.rs | 20 +++++--- src/query.rs | 14 +++--- src/routes.rs | 30 ++++++------ static/lib.js | 78 ++++++++++++++++++++++++++++---- 5 files changed, 108 insertions(+), 40 deletions(-) diff --git a/examples/example_dashboards.yaml b/examples/example_dashboards.yaml index cd0de21..f5c64c2 100644 --- a/examples/example_dashboards.yaml +++ b/examples/example_dashboards.yaml @@ -6,13 +6,13 @@ query: 'sum by (instance)(irate(node_cpu_seconds_total{mode="system",job="nodestats"}[5m])) * 100' query_type: Range span: - start: 2024-02-10T00:00:00.00Z - duration: 2d + end: 2024-02-10T00:00:00.00Z + duration: 1d step_duration: 1min name_label: instance - title: Test Dasbboard 2 span: - start: 2024-02-10T00:00:00.00Z + end: 2024-02-10T00:00:00.00Z duration: 2d step_duration: 1min graphs: diff --git a/src/dashboard.rs b/src/dashboard.rs index 1dca17e..6aacba6 100644 --- a/src/dashboard.rs +++ b/src/dashboard.rs @@ -23,7 +23,7 @@ use crate::query::{QueryConn, QueryType}; #[derive(Deserialize)] pub struct GraphSpan { - pub start: DateTime, + pub end: String, pub duration: String, pub step_duration: String, } @@ -84,7 +84,15 @@ fn graph_span_to_tuple(span: &Option) -> Option<(DateTime, Durat return None; } }; - Some((span.start.clone(), duration, step_duration)) + let end = if span.end == "now" { + Utc::now() + } else if let Ok(end) = DateTime::parse_from_rfc3339(&span.end) { + end.to_utc() + } else { + error!(?span.end, "Invalid DateTime using current time."); + Utc::now() + }; + Some((end, duration, step_duration)) } impl Graph { @@ -95,10 +103,10 @@ impl Graph { "Getting query connection for graph" ); let mut conn = QueryConn::new(&self.source, &self.query, self.query_type.clone()); - if let Some((start, duration, step_duration)) = graph_span_to_tuple(&self.span) { - conn = conn.with_span(start, duration, step_duration); - } else if let Some((start, duration, step_duration)) = graph_span_to_tuple(graph_span) { - conn = conn.with_span(start, duration, step_duration); + if let Some((end, duration, step_duration)) = graph_span_to_tuple(&self.span) { + conn = conn.with_span(end, duration, step_duration); + } else if let Some((end, duration, step_duration)) = graph_span_to_tuple(graph_span) { + conn = conn.with_span(end, duration, step_duration); } conn } diff --git a/src/query.rs b/src/query.rs index bfeb92c..8a8cc15 100644 --- a/src/query.rs +++ b/src/query.rs @@ -28,7 +28,7 @@ pub enum QueryType { } pub struct TimeSpan { - pub start: DateTime, + pub end: DateTime, pub duration: chrono::Duration, pub step_seconds: i64, } @@ -50,25 +50,25 @@ impl<'conn> QueryConn<'conn> { } } - pub fn with_span(mut self, start: DateTime, duration: chrono::Duration, step: chrono::Duration) -> Self { - self.span = Some(TimeSpan { start, duration, step_seconds: step.num_seconds() , }); + pub fn with_span(mut self, end: DateTime, duration: chrono::Duration, step: chrono::Duration) -> Self { + self.span = Some(TimeSpan { end, duration, step_seconds: step.num_seconds() , }); self } pub async fn get_results(&self) -> anyhow::Result { debug!("Getting results for query"); let client = Client::try_from(self.source)?; - let (end, start, step_resolution) = if let Some(TimeSpan { - start: st, + let (start, end, step_resolution) = if let Some(TimeSpan { + end: e, duration: du, step_seconds, }) = self.span { - ((st + du).timestamp(), st.timestamp(), step_seconds as f64) + ((e -du).timestamp(), e.timestamp(), step_seconds as f64) } else { let end = Utc::now().timestamp(); let start = end - (60 * 10); - (end, start, 30 as f64) + (start, end, 30 as f64) }; debug!(start, end, step_resolution, "Running Query with range values"); match self.query_type { diff --git a/src/routes.rs b/src/routes.rs index f20fea9..e061598 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -11,7 +11,7 @@ // 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 std::{collections::HashMap, sync::Arc}; use axum::{ extract::{Path, Query, State}, @@ -23,7 +23,6 @@ use axum::{ // 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}; @@ -42,25 +41,27 @@ pub async fn graph_query( .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 query.contains_key("end") + && 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 - } + // TODO(jwall): handle the now case. + Some(GraphSpan { + end: query["end"].clone(), + duration: query["duration"].clone(), + step_duration: query["step_duration"].clone(), + }) } else { None } }; let data = to_samples( graph - .get_query_connection(if query_span.is_some() { &query_span } else { &dash.span }) + .get_query_connection(if query_span.is_some() { + &query_span + } else { + &dash.span + }) .get_results() .await .expect("Unable to get query results") @@ -113,6 +114,7 @@ pub async fn dash_ui(State(config): State, Path(dash_idx): Path) .collect::>(); html!( h1 { (dash.title) } + span-selector {} @for (idx, graph) in &graph_iter { (graph_component(dash_idx, *idx, *graph)) } diff --git a/static/lib.js b/static/lib.js index 40e155a..854ab7c 100644 --- a/static/lib.js +++ b/static/lib.js @@ -11,6 +11,7 @@ // 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. + class TimeseriesGraph extends HTMLElement { #uri; #width; @@ -18,7 +19,7 @@ class TimeseriesGraph extends HTMLElement { #intervalId; #pollSeconds; #label; - #start; + #end; #duration; #step_duration; #targetNode = null; @@ -30,9 +31,9 @@ class TimeseriesGraph extends HTMLElement { this.#targetNode = this.appendChild(document.createElement("div")); } - static observedAttributes = ['uri', 'width', 'height', 'poll-seconds']; + static observedAttributes = ['uri', 'width', 'height', 'poll-seconds', 'end', 'duration', 'step-duration']; - attributeChanged(name, _oldValue, newValue) { + attributeChangedCallback(name, _oldValue, newValue) { switch (name) { case 'uri': this.#uri = newValue; @@ -49,8 +50,8 @@ class TimeseriesGraph extends HTMLElement { case 'label': this.#label = newValue; break; - case 'start': - this.#start = newValue; + case 'end': + this.#end = newValue; break; case 'duration': this.#duration = newValue; @@ -70,7 +71,7 @@ class TimeseriesGraph extends HTMLElement { this.#height = this.getAttribute('height') || this.#height; this.#pollSeconds = this.getAttribute('poll-seconds') || this.#pollSeconds; this.#label = this.getAttribute('label') || null; - this.#start = this.getAttribute('start') || null; + this.#end = this.getAttribute('end') || null; this.#duration = this.getAttribute('duration') || null; this.#step_duration = this.getAttribute('step-duration') || null; this.resetInterval() @@ -109,8 +110,8 @@ class TimeseriesGraph extends HTMLElement { } getUri() { - if (this.#start && this.#duration && this.#step_duration) { - return this.#uri + "?start=" + this.#start + "&duration=" + this.#duration + "&step_duration=" + this.#step_duration; + if (this.#end && this.#duration && this.#step_duration) { + return this.#uri + "?end=" + this.#end + "&duration=" + this.#duration + "&step_duration=" + this.#step_duration; } else { return this.#uri; } @@ -155,7 +156,8 @@ class TimeseriesGraph extends HTMLElement { } traces.push(trace); } - console.log("Traces: ", traces); + // TODO(jwall): If this has modified the trace length or anything we should + // do newPlot instead. // https://plotly.com/javascript/plotlyjs-function-reference/#plotlyreact Plotly.react(this.getTargetNode(), traces, config, layout); } else if (data.Scalar) { @@ -177,10 +179,66 @@ class TimeseriesGraph extends HTMLElement { trace.y.push(series.value); traces.push(trace); } - console.log("Traces: ", traces); Plotly.react(this.getTargetNode(), traces, config, layout); } } } TimeseriesGraph.registerElement(); + +class SpanSelector extends HTMLElement { + #targetNode = null; + #endInput = null; + #durationInput = null; + #stepDurationInput = null; + #updateInput = null + + constructor() { + super(); + this.#targetNode = this.appendChild(document.createElement('div')); + + this.#targetNode.appendChild(document.createElement('span')).innerText = "end: "; + this.#endInput = this.#targetNode.appendChild(document.createElement('input')); + + this.#targetNode.appendChild(document.createElement('span')).innerText = "duration: "; + this.#durationInput = this.#targetNode.appendChild(document.createElement('input')); + + this.#targetNode.appendChild(document.createElement('span')).innerText = "step duration: "; + this.#stepDurationInput = this.#targetNode.appendChild(document.createElement('input')); + + this.#updateInput = this.#targetNode.appendChild(document.createElement('button')); + this.#updateInput.innerText = "Update"; + } + + connectedCallback() { + const self = this; + self.#updateInput.onclick = function(_evt) { + self.updateGraphs() + }; + } + + disconnectedCallback() { + this.#updateInput.onclick = undefined; + } + + updateGraphs() { + for (var node of document.getElementsByTagName(TimeseriesGraph.elementName)) { + console.log("endInput: ", this.#endInput.value); + node.setAttribute('end', this.#endInput.value); + console.log("durationInput: ", this.#durationInput.value); + node.setAttribute('duration', this.#durationInput.value); + console.log("stepDurationInput: ", this.#stepDurationInput.value); + node.setAttribute('step-duration', this.#stepDurationInput.value); + } + } + + static elementName = "span-selector"; + + static registerElement() { + if (!customElements.get(SpanSelector.elementName)) { + customElements.define(SpanSelector.elementName, SpanSelector); + } + } +} + +SpanSelector.registerElement();