ux: end is better than start

This commit is contained in:
Jeremy Wall 2024-02-14 19:45:28 -06:00
parent 9529271b28
commit 7f30d87d75
5 changed files with 108 additions and 40 deletions

View File

@ -6,13 +6,13 @@
query: 'sum by (instance)(irate(node_cpu_seconds_total{mode="system",job="nodestats"}[5m])) * 100' query: 'sum by (instance)(irate(node_cpu_seconds_total{mode="system",job="nodestats"}[5m])) * 100'
query_type: Range query_type: Range
span: span:
start: 2024-02-10T00:00:00.00Z end: 2024-02-10T00:00:00.00Z
duration: 2d duration: 1d
step_duration: 1min step_duration: 1min
name_label: instance name_label: instance
- title: Test Dasbboard 2 - title: Test Dasbboard 2
span: span:
start: 2024-02-10T00:00:00.00Z end: 2024-02-10T00:00:00.00Z
duration: 2d duration: 2d
step_duration: 1min step_duration: 1min
graphs: graphs:

View File

@ -23,7 +23,7 @@ use crate::query::{QueryConn, QueryType};
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct GraphSpan { pub struct GraphSpan {
pub start: DateTime<Utc>, pub end: String,
pub duration: String, pub duration: String,
pub step_duration: String, pub step_duration: String,
} }
@ -84,7 +84,15 @@ fn graph_span_to_tuple(span: &Option<GraphSpan>) -> Option<(DateTime<Utc>, Durat
return None; 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 { impl Graph {
@ -95,10 +103,10 @@ impl Graph {
"Getting query connection for graph" "Getting query connection for graph"
); );
let mut conn = QueryConn::new(&self.source, &self.query, self.query_type.clone()); 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) { if let Some((end, duration, step_duration)) = graph_span_to_tuple(&self.span) {
conn = conn.with_span(start, duration, step_duration); conn = conn.with_span(end, duration, step_duration);
} else if let Some((start, duration, step_duration)) = graph_span_to_tuple(graph_span) { } else if let Some((end, duration, step_duration)) = graph_span_to_tuple(graph_span) {
conn = conn.with_span(start, duration, step_duration); conn = conn.with_span(end, duration, step_duration);
} }
conn conn
} }

View File

@ -28,7 +28,7 @@ pub enum QueryType {
} }
pub struct TimeSpan { pub struct TimeSpan {
pub start: DateTime<Utc>, pub end: DateTime<Utc>,
pub duration: chrono::Duration, pub duration: chrono::Duration,
pub step_seconds: i64, pub step_seconds: i64,
} }
@ -50,25 +50,25 @@ impl<'conn> QueryConn<'conn> {
} }
} }
pub fn with_span(mut self, start: DateTime<Utc>, duration: chrono::Duration, step: chrono::Duration) -> Self { pub fn with_span(mut self, end: DateTime<Utc>, duration: chrono::Duration, step: chrono::Duration) -> Self {
self.span = Some(TimeSpan { start, duration, step_seconds: step.num_seconds() , }); self.span = Some(TimeSpan { end, duration, step_seconds: step.num_seconds() , });
self self
} }
pub async fn get_results(&self) -> anyhow::Result<PromqlResult> { pub async fn get_results(&self) -> anyhow::Result<PromqlResult> {
debug!("Getting results for query"); debug!("Getting results for query");
let client = Client::try_from(self.source)?; let client = Client::try_from(self.source)?;
let (end, start, step_resolution) = if let Some(TimeSpan { let (start, end, step_resolution) = if let Some(TimeSpan {
start: st, end: e,
duration: du, duration: du,
step_seconds, step_seconds,
}) = self.span }) = self.span
{ {
((st + du).timestamp(), st.timestamp(), step_seconds as f64) ((e -du).timestamp(), e.timestamp(), step_seconds as f64)
} else { } else {
let end = Utc::now().timestamp(); let end = Utc::now().timestamp();
let start = end - (60 * 10); 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"); debug!(start, end, step_resolution, "Running Query with range values");
match self.query_type { match self.query_type {

View File

@ -11,7 +11,7 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
use std::{sync::Arc, collections::HashMap}; use std::{collections::HashMap, sync::Arc};
use axum::{ use axum::{
extract::{Path, Query, State}, extract::{Path, Query, State},
@ -23,7 +23,6 @@ use axum::{
// https://maud.lambda.xyz/getting-started.html // https://maud.lambda.xyz/getting-started.html
use maud::{html, Markup}; use maud::{html, Markup};
use tracing::{debug, error}; use tracing::{debug, error};
use chrono::prelude::*;
use crate::dashboard::{Dashboard, Graph, GraphSpan}; use crate::dashboard::{Dashboard, Graph, GraphSpan};
use crate::query::{to_samples, QueryResult}; use crate::query::{to_samples, QueryResult};
@ -42,25 +41,27 @@ pub async fn graph_query(
.get(graph_idx) .get(graph_idx)
.expect(&format!("No such graph in dasboard {}", dash_idx)); .expect(&format!("No such graph in dasboard {}", dash_idx));
let query_span = { 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"]) { // TODO(jwall): handle the now case.
Some(GraphSpan { Some(GraphSpan {
start: start.to_utc(), end: query["end"].clone(),
duration: query["duration"].clone(), duration: query["duration"].clone(),
step_duration: query["step_duration"].clone(), step_duration: query["step_duration"].clone(),
}) })
} else {
error!(?query, "Invalid date time in start for query string");
None
}
} else { } else {
None None
} }
}; };
let data = to_samples( let data = to_samples(
graph 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() .get_results()
.await .await
.expect("Unable to get query results") .expect("Unable to get query results")
@ -113,6 +114,7 @@ pub async fn dash_ui(State(config): State<Config>, Path(dash_idx): Path<usize>)
.collect::<Vec<(usize, &Graph)>>(); .collect::<Vec<(usize, &Graph)>>();
html!( html!(
h1 { (dash.title) } h1 { (dash.title) }
span-selector {}
@for (idx, graph) in &graph_iter { @for (idx, graph) in &graph_iter {
(graph_component(dash_idx, *idx, *graph)) (graph_component(dash_idx, *idx, *graph))
} }

View File

@ -11,6 +11,7 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
class TimeseriesGraph extends HTMLElement { class TimeseriesGraph extends HTMLElement {
#uri; #uri;
#width; #width;
@ -18,7 +19,7 @@ class TimeseriesGraph extends HTMLElement {
#intervalId; #intervalId;
#pollSeconds; #pollSeconds;
#label; #label;
#start; #end;
#duration; #duration;
#step_duration; #step_duration;
#targetNode = null; #targetNode = null;
@ -30,9 +31,9 @@ class TimeseriesGraph extends HTMLElement {
this.#targetNode = this.appendChild(document.createElement("div")); 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) { switch (name) {
case 'uri': case 'uri':
this.#uri = newValue; this.#uri = newValue;
@ -49,8 +50,8 @@ class TimeseriesGraph extends HTMLElement {
case 'label': case 'label':
this.#label = newValue; this.#label = newValue;
break; break;
case 'start': case 'end':
this.#start = newValue; this.#end = newValue;
break; break;
case 'duration': case 'duration':
this.#duration = newValue; this.#duration = newValue;
@ -70,7 +71,7 @@ class TimeseriesGraph extends HTMLElement {
this.#height = this.getAttribute('height') || this.#height; this.#height = this.getAttribute('height') || this.#height;
this.#pollSeconds = this.getAttribute('poll-seconds') || this.#pollSeconds; this.#pollSeconds = this.getAttribute('poll-seconds') || this.#pollSeconds;
this.#label = this.getAttribute('label') || null; 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.#duration = this.getAttribute('duration') || null;
this.#step_duration = this.getAttribute('step-duration') || null; this.#step_duration = this.getAttribute('step-duration') || null;
this.resetInterval() this.resetInterval()
@ -109,8 +110,8 @@ class TimeseriesGraph extends HTMLElement {
} }
getUri() { getUri() {
if (this.#start && this.#duration && this.#step_duration) { if (this.#end && this.#duration && this.#step_duration) {
return this.#uri + "?start=" + this.#start + "&duration=" + this.#duration + "&step_duration=" + this.#step_duration; return this.#uri + "?end=" + this.#end + "&duration=" + this.#duration + "&step_duration=" + this.#step_duration;
} else { } else {
return this.#uri; return this.#uri;
} }
@ -155,7 +156,8 @@ class TimeseriesGraph extends HTMLElement {
} }
traces.push(trace); 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 // https://plotly.com/javascript/plotlyjs-function-reference/#plotlyreact
Plotly.react(this.getTargetNode(), traces, config, layout); Plotly.react(this.getTargetNode(), traces, config, layout);
} else if (data.Scalar) { } else if (data.Scalar) {
@ -177,10 +179,66 @@ class TimeseriesGraph extends HTMLElement {
trace.y.push(series.value); trace.y.push(series.value);
traces.push(trace); traces.push(trace);
} }
console.log("Traces: ", traces);
Plotly.react(this.getTargetNode(), traces, config, layout); Plotly.react(this.getTargetNode(), traces, config, layout);
} }
} }
} }
TimeseriesGraph.registerElement(); 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();