mirror of
https://github.com/zaphar/Heracles.git
synced 2025-07-23 12:39:50 -04:00
ux: end is better than start
This commit is contained in:
parent
9529271b28
commit
7f30d87d75
@ -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:
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
14
src/query.rs
14
src/query.rs
@ -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 {
|
||||||
|
@ -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))
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
Loading…
x
Reference in New Issue
Block a user