mirror of
https://git.alemi.dev/dashboard.git
synced 2024-11-22 23:44:55 +01:00
feat!: toggle sources/edit, many lines per source
Some UI tweaks (like suffixes in sliders) Restructured data and storage: many Metrics per one Source. Each Metric is a Line on a Panel. Fetches are handled per Source. Metrics cannot be moved across Sources. Data points are linked to a Metric. Also made "show sources" and "edit mode" two distinct toggles, and thus implemented a non-edit view for sources. Also bumped version to 0.3.0
This commit is contained in:
parent
375c8d5e5e
commit
37d00b471a
11 changed files with 495 additions and 241 deletions
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "dashboard"
|
||||
version = "0.2.1"
|
||||
version = "0.3.0"
|
||||
edition = "2021"
|
||||
|
||||
[[bin]]
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
pub mod source;
|
||||
pub mod store;
|
||||
|
||||
use self::source::{Panel, Source};
|
||||
use self::source::{Panel, Source, Metric};
|
||||
use self::store::SQLiteDataStore;
|
||||
use std::num::ParseFloatError;
|
||||
use std::path::PathBuf;
|
||||
|
@ -50,6 +50,7 @@ pub struct ApplicationState {
|
|||
pub file_size: RwLock<u64>,
|
||||
pub panels: RwLock<Vec<Panel>>,
|
||||
pub sources: RwLock<Vec<Source>>,
|
||||
pub metrics: RwLock<Vec<Metric>>,
|
||||
pub storage: Mutex<SQLiteDataStore>,
|
||||
pub diagnostics: RwLock<Vec<String>>,
|
||||
}
|
||||
|
@ -60,6 +61,7 @@ impl ApplicationState {
|
|||
|
||||
let panels = storage.load_panels()?;
|
||||
let sources = storage.load_sources()?;
|
||||
let metrics = storage.load_metrics()?;
|
||||
|
||||
return Ok(ApplicationState {
|
||||
run: true,
|
||||
|
@ -67,6 +69,7 @@ impl ApplicationState {
|
|||
file_path: path,
|
||||
panels: RwLock::new(panels),
|
||||
sources: RwLock::new(sources),
|
||||
metrics: RwLock::new(metrics),
|
||||
storage: Mutex::new(storage),
|
||||
diagnostics: RwLock::new(Vec::new()),
|
||||
});
|
||||
|
@ -104,14 +107,10 @@ impl ApplicationState {
|
|||
.lock()
|
||||
.expect("Storage Mutex poisoned")
|
||||
.new_source(
|
||||
source.panel_id,
|
||||
source.name.as_str(),
|
||||
source.enabled,
|
||||
source.url.as_str(),
|
||||
source.interval,
|
||||
source.query_x.as_str(),
|
||||
source.query_y.as_str(),
|
||||
source.color,
|
||||
self.sources.read().expect("Sources RwLock poisoned").len() as i32,
|
||||
)?;
|
||||
self.sources
|
||||
|
@ -120,4 +119,25 @@ impl ApplicationState {
|
|||
.push(verified_source);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
pub fn add_metric(&self, metric: &Metric, source: &Source) -> Result<(), FetchError> {
|
||||
let verified_metric = self
|
||||
.storage
|
||||
.lock()
|
||||
.expect("Storage Mutex poisoned")
|
||||
.new_metric(
|
||||
metric.name.as_str(),
|
||||
source.id,
|
||||
metric.query_x.as_str(),
|
||||
metric.query_y.as_str(),
|
||||
metric.panel_id,
|
||||
metric.color,
|
||||
self.metrics.read().expect("Sources RwLock poisoned").len() as i32, // TODO use source.metrics.len()
|
||||
)?;
|
||||
self.metrics
|
||||
.write()
|
||||
.expect("Sources RwLock poisoned")
|
||||
.push(verified_metric);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ use chrono::{DateTime, Utc};
|
|||
use eframe::egui::plot::{Value, Values};
|
||||
use eframe::epaint::Color32;
|
||||
use std::sync::RwLock;
|
||||
use tracing::info;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Panel {
|
||||
|
@ -46,14 +47,7 @@ pub struct Source {
|
|||
pub enabled: bool,
|
||||
pub url: String,
|
||||
pub interval: i32,
|
||||
pub color: Color32,
|
||||
pub(crate) last_fetch: RwLock<DateTime<Utc>>,
|
||||
pub query_x: String,
|
||||
// pub(crate) compiled_query_x: Arc<Mutex<jq_rs::JqProgram>>,
|
||||
pub query_y: String,
|
||||
// pub(crate) compiled_query_y: Arc<Mutex<jq_rs::JqProgram>>,
|
||||
pub(crate) panel_id: i32,
|
||||
pub(crate) data: RwLock<Vec<Value>>,
|
||||
}
|
||||
|
||||
impl Default for Source {
|
||||
|
@ -64,12 +58,7 @@ impl Default for Source {
|
|||
enabled: false,
|
||||
url: "".to_string(),
|
||||
interval: 60,
|
||||
color: Color32::TRANSPARENT,
|
||||
last_fetch: RwLock::new(Utc::now()),
|
||||
query_x: "".to_string(),
|
||||
query_y: "".to_string(),
|
||||
panel_id: -1,
|
||||
data: RwLock::new(Vec::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -81,7 +70,10 @@ fn avg_value(values: &[Value]) -> Value {
|
|||
x += v.x;
|
||||
y += v.y;
|
||||
}
|
||||
return Value { x: x / values.len() as f64, y: y / values.len() as f64 };
|
||||
return Value {
|
||||
x: x / values.len() as f64,
|
||||
y: y / values.len() as f64,
|
||||
};
|
||||
}
|
||||
|
||||
impl Source {
|
||||
|
@ -90,8 +82,64 @@ impl Source {
|
|||
return (Utc::now() - *last_fetch).num_seconds() < self.interval as i64;
|
||||
}
|
||||
|
||||
// TODO optimize this with caching!
|
||||
pub fn values(&self, min_x: Option<f64>, max_x: Option<f64>, chunk_size: Option<u32>) -> Values {
|
||||
pub fn fetch(&self) -> Result<serde_json::Value, FetchError> {
|
||||
fetch(self.url.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fetch(url: &str) -> Result<serde_json::Value, FetchError> {
|
||||
return Ok(ureq::get(url).call()?.into_json()?);
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Metric {
|
||||
pub(crate) id: i32,
|
||||
pub name: String,
|
||||
pub source_id: i32,
|
||||
pub color: Color32,
|
||||
pub query_x: String,
|
||||
pub query_y: String,
|
||||
pub(crate) panel_id: i32,
|
||||
pub(crate) data: RwLock<Vec<Value>>,
|
||||
}
|
||||
|
||||
impl Default for Metric {
|
||||
fn default() -> Self {
|
||||
Metric {
|
||||
id: -1,
|
||||
name: "".to_string(),
|
||||
source_id: -1,
|
||||
color: Color32::TRANSPARENT,
|
||||
query_x: "".to_string(),
|
||||
query_y: "".to_string(),
|
||||
panel_id: -1,
|
||||
data: RwLock::new(Vec::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Metric {
|
||||
pub fn extract(&self, value: &serde_json::Value) -> Result<Value, FetchError> {
|
||||
let x: f64;
|
||||
if self.query_x.len() > 0 {
|
||||
x = jql::walker(value, self.query_x.as_str())?
|
||||
.as_f64()
|
||||
.ok_or(FetchError::JQLError("X query is null".to_string()))?; // TODO what if it's given to us as a string?
|
||||
} else {
|
||||
x = Utc::now().timestamp() as f64;
|
||||
}
|
||||
let y = jql::walker(value, self.query_y.as_str())?
|
||||
.as_f64()
|
||||
.ok_or(FetchError::JQLError("Y query is null".to_string()))?;
|
||||
Ok(Value { x, y })
|
||||
}
|
||||
|
||||
pub fn values(
|
||||
&self,
|
||||
min_x: Option<f64>,
|
||||
max_x: Option<f64>,
|
||||
chunk_size: Option<u32>,
|
||||
) -> Values {
|
||||
let mut values = self.data.read().expect("Values RwLock poisoned").clone();
|
||||
if let Some(min_x) = min_x {
|
||||
values.retain(|x| x.x > min_x);
|
||||
|
@ -100,27 +148,12 @@ impl Source {
|
|||
values.retain(|x| x.x < max_x);
|
||||
}
|
||||
if let Some(chunk_size) = chunk_size {
|
||||
if chunk_size > 0 { // TODO make this nested if prettier
|
||||
if chunk_size > 0 {
|
||||
// TODO make this nested if prettier
|
||||
let iter = values.chunks(chunk_size as usize);
|
||||
values = iter.map(|x| avg_value(x) ).collect();
|
||||
values = iter.map(|x| avg_value(x)).collect();
|
||||
}
|
||||
}
|
||||
Values::from_values(values)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fetch(url: &str, query_x: &str, query_y: &str) -> Result<Value, FetchError> {
|
||||
let res = ureq::get(url).call()?.into_json()?;
|
||||
let x: f64;
|
||||
if query_x.len() > 0 {
|
||||
x = jql::walker(&res, query_x)?
|
||||
.as_f64()
|
||||
.ok_or(FetchError::JQLError("X query is null".to_string()))?; // TODO what if it's given to us as a string?
|
||||
} else {
|
||||
x = Utc::now().timestamp() as f64;
|
||||
}
|
||||
let y = jql::walker(&res, query_y)?
|
||||
.as_f64()
|
||||
.ok_or(FetchError::JQLError("Y query is null".to_string()))?;
|
||||
return Ok(Value { x, y });
|
||||
}
|
||||
|
|
|
@ -8,6 +8,8 @@ use eframe::egui::{plot::Value, Color32};
|
|||
use rusqlite::{params, Connection};
|
||||
use std::sync::RwLock;
|
||||
|
||||
use super::source::Metric;
|
||||
|
||||
pub trait DataStorage {
|
||||
fn add_panel(&self, name: &str);
|
||||
}
|
||||
|
@ -46,19 +48,38 @@ impl SQLiteDataStore {
|
|||
enabled BOOL NOT NULL,
|
||||
url TEXT NOT NULL,
|
||||
interval INT NOT NULL,
|
||||
query_x TEXT NOT NULL,
|
||||
query_y TEXT NOT NULL,
|
||||
panel_id INT NOT NULL,
|
||||
color INT NULL,
|
||||
position INT NOT NULL
|
||||
);",
|
||||
[],
|
||||
)?;
|
||||
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS metrics (
|
||||
id INTEGER PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
source_id INT NOT NULL,
|
||||
query_x TEXT NOT NULL,
|
||||
query_y TEXT NOT NULL,
|
||||
panel_id INT NOT NULL,
|
||||
color INT NOT NULL,
|
||||
position INT NOT NULL
|
||||
);",
|
||||
[],
|
||||
)?;
|
||||
|
||||
// BEGIN TRANSACTION;
|
||||
// CREATE TEMPORARY TABLE t1_backup(a,b);
|
||||
// INSERT INTO t1_backup SELECT a,b FROM t1;
|
||||
// DROP TABLE t1;
|
||||
// CREATE TABLE t1(a,b);
|
||||
// INSERT INTO t1 SELECT a,b FROM t1_backup;
|
||||
// DROP TABLE t1_backup;
|
||||
// COMMIT;
|
||||
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS points (
|
||||
id INTEGER PRIMARY KEY,
|
||||
source_id INT NOT NULL,
|
||||
metric_id INT NOT NULL,
|
||||
x FLOAT NOT NULL,
|
||||
y FLOAT NOT NULL
|
||||
);",
|
||||
|
@ -68,12 +89,12 @@ impl SQLiteDataStore {
|
|||
Ok(SQLiteDataStore { conn })
|
||||
}
|
||||
|
||||
pub fn load_values(&self, source_id: i32) -> rusqlite::Result<Vec<Value>> {
|
||||
pub fn load_values(&self, metric_id: i32) -> rusqlite::Result<Vec<Value>> {
|
||||
let mut values: Vec<Value> = Vec::new();
|
||||
let mut statement = self
|
||||
.conn
|
||||
.prepare("SELECT x, y FROM points WHERE source_id = ?")?;
|
||||
let values_iter = statement.query_map(params![source_id], |row| {
|
||||
.prepare("SELECT x, y FROM points WHERE metric_id = ?")?;
|
||||
let values_iter = statement.query_map(params![metric_id], |row| {
|
||||
Ok(Value {
|
||||
x: row.get(0)?,
|
||||
y: row.get(1)?,
|
||||
|
@ -89,10 +110,10 @@ impl SQLiteDataStore {
|
|||
Ok(values)
|
||||
}
|
||||
|
||||
pub fn put_value(&self, source_id: i32, v: Value) -> rusqlite::Result<usize> {
|
||||
pub fn put_value(&self, metric_id: i32, v: Value) -> rusqlite::Result<usize> {
|
||||
self.conn.execute(
|
||||
"INSERT INTO points(source_id, x, y) VALUES (?, ?, ?)",
|
||||
params![source_id, v.x, v.y],
|
||||
"INSERT INTO points(metric_id, x, y) VALUES (?, ?, ?)",
|
||||
params![metric_id, v.x, v.y],
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -107,17 +128,11 @@ impl SQLiteDataStore {
|
|||
url: row.get(3)?,
|
||||
interval: row.get(4)?,
|
||||
last_fetch: RwLock::new(Utc.ymd(1970, 1, 1).and_hms(0, 0, 0)),
|
||||
query_x: row.get(5)?,
|
||||
query_y: row.get(6)?,
|
||||
panel_id: row.get(7)?,
|
||||
color: unpack_color(row.get(8).unwrap_or(0)),
|
||||
data: RwLock::new(Vec::new()),
|
||||
})
|
||||
})?;
|
||||
|
||||
for source in sources_iter {
|
||||
if let Ok(mut s) = source {
|
||||
s.data = RwLock::new(self.load_values(s.id)?);
|
||||
if let Ok(s) = source {
|
||||
sources.push(s);
|
||||
}
|
||||
}
|
||||
|
@ -128,41 +143,28 @@ impl SQLiteDataStore {
|
|||
// jank! TODO make it not jank!
|
||||
pub fn new_source(
|
||||
&self,
|
||||
panel_id: i32,
|
||||
name: &str,
|
||||
enabled: bool,
|
||||
url: &str,
|
||||
interval: i32,
|
||||
query_x: &str,
|
||||
query_y: &str,
|
||||
color: Color32,
|
||||
position: i32,
|
||||
) -> rusqlite::Result<Source> {
|
||||
let color_u32: Option<u32> = if color == Color32::TRANSPARENT {
|
||||
None
|
||||
} else {
|
||||
Some(repack_color(color))
|
||||
};
|
||||
self.conn.execute(
|
||||
"INSERT INTO sources(name, enabled, url, interval, query_x, query_y, panel_id, color, position) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
params![name, enabled, url, interval, query_x, query_y, panel_id, color_u32, position],
|
||||
"INSERT INTO sources(name, enabled, url, interval, position) VALUES (?, ?, ?, ?, ?)",
|
||||
params![name, enabled, url, interval, position],
|
||||
)?;
|
||||
let mut statement = self
|
||||
.conn
|
||||
.prepare("SELECT * FROM sources WHERE name = ? AND panel_id = ?")?;
|
||||
for panel in statement.query_map(params![name, panel_id], |row| {
|
||||
.prepare("SELECT * FROM sources WHERE name = ? AND url = ? ORDER BY id DESC")?;
|
||||
for panel in statement.query_map(params![name, url], |row| {
|
||||
Ok(Source {
|
||||
id: row.get(0)?,
|
||||
name: row.get(1)?,
|
||||
enabled: row.get(2)?,
|
||||
url: row.get(3)?,
|
||||
interval: row.get(4)?,
|
||||
// position: row.get(5)?,
|
||||
last_fetch: RwLock::new(Utc.ymd(1970, 1, 1).and_hms(0, 0, 0)),
|
||||
query_x: row.get(5)?,
|
||||
query_y: row.get(6)?,
|
||||
panel_id: row.get(7)?,
|
||||
color: unpack_color(row.get(8).unwrap_or(0)),
|
||||
data: RwLock::new(Vec::new()),
|
||||
})
|
||||
})? {
|
||||
if let Ok(p) = panel {
|
||||
|
@ -175,25 +177,16 @@ impl SQLiteDataStore {
|
|||
|
||||
pub fn update_source(
|
||||
&self,
|
||||
source_id: i32,
|
||||
panel_id: i32,
|
||||
id: i32,
|
||||
name: &str,
|
||||
enabled: bool,
|
||||
url: &str,
|
||||
interval: i32,
|
||||
query_x: &str,
|
||||
query_y: &str,
|
||||
color: Color32,
|
||||
position: i32,
|
||||
) -> rusqlite::Result<usize> {
|
||||
let color_u32: Option<u32> = if color == Color32::TRANSPARENT {
|
||||
None
|
||||
} else {
|
||||
Some(repack_color(color))
|
||||
};
|
||||
self.conn.execute(
|
||||
"UPDATE sources SET name = ?, enabled = ?, url = ?, interval = ?, query_x = ?, query_y = ?, panel_id = ?, color = ?, position = ? WHERE id = ?",
|
||||
params![name, enabled, url, interval, query_x, query_y, panel_id, color_u32, position, source_id],
|
||||
"UPDATE sources SET name = ?, enabled = ?, url = ?, interval = ?, position = ? WHERE id = ?",
|
||||
params![name, enabled, url, interval, position, id],
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -201,6 +194,93 @@ impl SQLiteDataStore {
|
|||
// self.conn.execute("DELETE FROM sources WHERE id = ?", params![id])
|
||||
// }
|
||||
|
||||
pub fn load_metrics(&self) -> rusqlite::Result<Vec<Metric>> {
|
||||
let mut metrics: Vec<Metric> = Vec::new();
|
||||
let mut statement = self.conn.prepare("SELECT * FROM metrics ORDER BY position")?;
|
||||
let metrics_iter = statement.query_map([], |row| {
|
||||
Ok(Metric {
|
||||
id: row.get(0)?,
|
||||
name: row.get(1)?,
|
||||
source_id: row.get(2)?,
|
||||
query_x: row.get(3)?,
|
||||
query_y: row.get(4)?,
|
||||
panel_id: row.get(5)?,
|
||||
color: unpack_color(row.get(6).unwrap_or(0)),
|
||||
// position: row.get(7)?,
|
||||
data: RwLock::new(Vec::new()),
|
||||
})
|
||||
})?;
|
||||
|
||||
for metric in metrics_iter {
|
||||
if let Ok(m) = metric {
|
||||
*m.data.write().expect("Points RwLock poisoned") = self.load_values(m.id)?;
|
||||
metrics.push(m);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(metrics)
|
||||
}
|
||||
|
||||
// jank! TODO make it not jank!
|
||||
pub fn new_metric(
|
||||
&self,
|
||||
name: &str,
|
||||
source_id: i32,
|
||||
query_x: &str,
|
||||
query_y: &str,
|
||||
panel_id: i32,
|
||||
color: Color32,
|
||||
position: i32,
|
||||
) -> rusqlite::Result<Metric> {
|
||||
self.conn.execute(
|
||||
"INSERT INTO metrics(name, source_id, query_x, query_y, panel_id, color, position) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||
params![name, source_id, query_x, query_y, panel_id, repack_color(color), position],
|
||||
)?;
|
||||
let mut statement = self
|
||||
.conn
|
||||
.prepare("SELECT * FROM metrics WHERE source_id = ? AND panel_id = ? AND name = ? ORDER BY id DESC")?;
|
||||
for metric in statement.query_map(params![source_id, panel_id, name], |row| {
|
||||
Ok(Metric {
|
||||
id: row.get(0)?,
|
||||
name: row.get(1)?,
|
||||
source_id: row.get(2)?,
|
||||
query_x: row.get(3)?,
|
||||
query_y: row.get(4)?,
|
||||
panel_id: row.get(5)?,
|
||||
color: unpack_color(row.get(6).unwrap_or(0)),
|
||||
// position: row.get(7)?,
|
||||
data: RwLock::new(Vec::new()),
|
||||
})
|
||||
})? {
|
||||
if let Ok(m) = metric {
|
||||
return Ok(m);
|
||||
}
|
||||
}
|
||||
|
||||
Err(rusqlite::Error::QueryReturnedNoRows)
|
||||
}
|
||||
|
||||
pub fn update_metric(
|
||||
&self,
|
||||
id: i32,
|
||||
name: &str,
|
||||
source_id: i32,
|
||||
query_x: &str,
|
||||
query_y: &str,
|
||||
panel_id: i32,
|
||||
color: Color32,
|
||||
position: i32,
|
||||
) -> rusqlite::Result<usize> {
|
||||
self.conn.execute(
|
||||
"UPDATE metrics SET name = ?, query_x = ?, query_y = ?, panel_id = ?, color = ?, position = ? WHERE id = ? AND source_id = ?",
|
||||
params![name, query_x, query_y, panel_id, repack_color(color), position, id, source_id],
|
||||
)
|
||||
}
|
||||
|
||||
// pub fn delete_metric(&self, id:i32) -> rusqlite::Result<usize> {
|
||||
// self.conn.execute("DELETE FROM metrics WHERE id = ?", params![id])
|
||||
// }
|
||||
|
||||
pub fn load_panels(&self) -> rusqlite::Result<Vec<Panel>> {
|
||||
let mut panels: Vec<Panel> = Vec::new();
|
||||
let mut statement = self
|
||||
|
|
67
src/app/gui/metric.rs
Normal file
67
src/app/gui/metric.rs
Normal file
|
@ -0,0 +1,67 @@
|
|||
use eframe::{egui::{Ui, TextEdit, ComboBox, Layout, Sense, color_picker::show_color_at}, emath::Align, epaint::Color32};
|
||||
|
||||
use crate::app::data::source::{Panel, Metric};
|
||||
|
||||
fn color_square(ui: &mut Ui, color:Color32) {
|
||||
let size = ui.spacing().interact_size;
|
||||
let (rect, response) = ui.allocate_exact_size(size, Sense::click());
|
||||
if ui.is_rect_visible(rect) {
|
||||
let visuals = ui.style().interact(&response);
|
||||
let rect = rect.expand(visuals.expansion);
|
||||
|
||||
show_color_at(ui.painter(), color, rect);
|
||||
|
||||
let rounding = visuals.rounding.at_most(2.0);
|
||||
ui.painter()
|
||||
.rect_stroke(rect, rounding, (2.0, visuals.bg_fill)); // fill is intentional, because default style has no border
|
||||
}
|
||||
}
|
||||
|
||||
pub fn metric_display_ui(ui: &mut Ui, metric: &Metric, _width: f32) {
|
||||
ui.horizontal(|ui| {
|
||||
color_square(ui, metric.color);
|
||||
ui.label(&metric.name);
|
||||
ui.with_layout(Layout::top_down(Align::RIGHT), |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(format!("panel: {}", metric.panel_id));
|
||||
if metric.query_y.len() > 0 {
|
||||
ui.label(format!("y: {}", metric.query_y));
|
||||
}
|
||||
if metric.query_x.len() > 0 {
|
||||
ui.label(format!("x: {}", metric.query_x));
|
||||
}
|
||||
})
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
pub fn metric_edit_ui(ui: &mut Ui, metric: &mut Metric, panels: Option<&Vec<Panel>>, width: f32) {
|
||||
let text_width = width - 190.0;
|
||||
ui.horizontal(|ui| {
|
||||
ui.color_edit_button_srgba(&mut metric.color);
|
||||
TextEdit::singleline(&mut metric.name)
|
||||
.desired_width(text_width / 2.0)
|
||||
.hint_text("name")
|
||||
.show(ui);
|
||||
ui.separator();
|
||||
TextEdit::singleline(&mut metric.query_x)
|
||||
.desired_width(text_width / 4.0)
|
||||
.hint_text("x")
|
||||
.show(ui);
|
||||
TextEdit::singleline(&mut metric.query_y)
|
||||
.desired_width(text_width / 4.0)
|
||||
.hint_text("y")
|
||||
.show(ui);
|
||||
if let Some(panels) = panels {
|
||||
ComboBox::from_id_source(format!("panel-selector-{}", metric.id))
|
||||
.width(60.0)
|
||||
.selected_text(format!("panel: {}", metric.panel_id))
|
||||
.show_ui(ui, |ui| {
|
||||
ui.selectable_value(&mut metric.panel_id, -1, "None");
|
||||
for p in panels {
|
||||
ui.selectable_value(&mut metric.panel_id, p.id, p.name.as_str());
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
|
@ -1,2 +1,3 @@
|
|||
pub mod panel;
|
||||
pub mod source;
|
||||
pub mod metric;
|
||||
|
|
|
@ -5,7 +5,7 @@ use eframe::{egui::{
|
|||
}, emath::Vec2};
|
||||
|
||||
use crate::app::{
|
||||
data::source::{Panel, Source},
|
||||
data::source::{Panel, Metric},
|
||||
util::timestamp_to_str,
|
||||
};
|
||||
|
||||
|
@ -24,30 +24,30 @@ pub fn panel_title_ui(ui: &mut Ui, panel: &mut Panel, extra: bool) {
|
|||
ui.toggle_value(&mut panel.view_scroll, "🔒");
|
||||
ui.separator();
|
||||
if panel.limit {
|
||||
ui.label("min"); // TODO makes no sense if it's not a timeserie
|
||||
ui.add(
|
||||
DragValue::new(&mut panel.view_size)
|
||||
.speed(10)
|
||||
.suffix(" min")
|
||||
.clamp_range(0..=2147483647i32),
|
||||
);
|
||||
}
|
||||
ui.toggle_value(&mut panel.limit, "limit");
|
||||
ui.separator();
|
||||
if panel.shift {
|
||||
ui.label("min");
|
||||
ui.add(
|
||||
DragValue::new(&mut panel.view_offset)
|
||||
.speed(10)
|
||||
.suffix(" min")
|
||||
.clamp_range(0..=2147483647i32),
|
||||
);
|
||||
}
|
||||
ui.toggle_value(&mut panel.shift, "offset");
|
||||
ui.separator();
|
||||
if panel.reduce {
|
||||
ui.label("x");
|
||||
ui.add(
|
||||
DragValue::new(&mut panel.view_chunks)
|
||||
.speed(1)
|
||||
.prefix("x")
|
||||
.clamp_range(1..=1000), // TODO allow to average larger spans maybe?
|
||||
);
|
||||
}
|
||||
|
@ -63,7 +63,7 @@ pub fn panel_title_ui(ui: &mut Ui, panel: &mut Panel, extra: bool) {
|
|||
});
|
||||
}
|
||||
|
||||
pub fn panel_body_ui(ui: &mut Ui, panel: &mut Panel, sources: &Vec<Source>) {
|
||||
pub fn panel_body_ui(ui: &mut Ui, panel: &mut Panel, metrics: &Vec<Metric>) {
|
||||
let mut p = Plot::new(format!("plot-{}", panel.name))
|
||||
.height(panel.height as f32)
|
||||
.allow_scroll(false)
|
||||
|
@ -135,11 +135,13 @@ pub fn panel_body_ui(ui: &mut Ui, panel: &mut Panel, sources: &Vec<Source>) {
|
|||
let min_x = if panel.limit { Some(_now - _size - _off) } else { None };
|
||||
let max_x = if panel.shift { Some(_now - _off) } else { None };
|
||||
let chunk_size = if panel.reduce { Some(panel.view_chunks) } else { None };
|
||||
for source in sources {
|
||||
if source.panel_id == panel.id {
|
||||
for metric in metrics {
|
||||
if metric.panel_id == panel.id {
|
||||
// let chunks = None;
|
||||
let line = Line::new(source.values(min_x, max_x, chunk_size)).name(source.name.as_str());
|
||||
plot_ui.line(line.color(source.color));
|
||||
let line = Line::new(metric.values(min_x, max_x, chunk_size))
|
||||
.name(metric.name.as_str())
|
||||
.color(metric.color);
|
||||
plot_ui.line(line);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,81 +1,41 @@
|
|||
use eframe::egui::{Ui, TextEdit, ComboBox, Slider, DragValue};
|
||||
use eframe::{egui::{Ui, TextEdit, DragValue, Checkbox}};
|
||||
|
||||
use crate::app::data::source::{Panel, Source};
|
||||
use crate::app::data::source::{Panel, Source, Metric};
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn source_edit_inline_ui(ui: &mut Ui, source: &mut Source, panels: &Vec<Panel>) {
|
||||
TextEdit::singleline(&mut source.name)
|
||||
.hint_text("name")
|
||||
.desired_width(50.0)
|
||||
.show(ui);
|
||||
TextEdit::singleline(&mut source.url)
|
||||
.hint_text("url")
|
||||
.desired_width(160.0)
|
||||
.show(ui);
|
||||
TextEdit::singleline(&mut source.query_x)
|
||||
.hint_text("x")
|
||||
.desired_width(30.0)
|
||||
.show(ui);
|
||||
TextEdit::singleline(&mut source.query_y)
|
||||
.hint_text("y")
|
||||
.desired_width(30.0)
|
||||
.show(ui);
|
||||
ComboBox::from_id_source("panel")
|
||||
.selected_text(format!("panel [{}]", source.panel_id))
|
||||
.width(70.0)
|
||||
.show_ui(ui, |ui| {
|
||||
for p in panels {
|
||||
ui.selectable_value(&mut source.panel_id, p.id, p.name.as_str());
|
||||
}
|
||||
});
|
||||
ui.checkbox(&mut source.enabled, "enabled");
|
||||
ui.add(Slider::new(&mut source.interval, 1..=60));
|
||||
ui.color_edit_button_srgba(&mut source.color);
|
||||
}
|
||||
use super::metric::{metric_edit_ui, metric_display_ui};
|
||||
|
||||
pub fn source_edit_ui(ui: &mut Ui, source: &mut Source, panels: &Vec<Panel>, width: f32) {
|
||||
ui.group(|ui| {
|
||||
ui.vertical(|ui| {
|
||||
ui.horizontal(|ui| {
|
||||
let text_width = width - 25.0;
|
||||
ui.checkbox(&mut source.enabled, "");
|
||||
TextEdit::singleline(&mut source.name)
|
||||
.desired_width(text_width / 4.0)
|
||||
.hint_text("name")
|
||||
.show(ui);
|
||||
TextEdit::singleline(&mut source.url)
|
||||
.desired_width(text_width * 3.0 / 4.0)
|
||||
.hint_text("url")
|
||||
.show(ui);
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
let text_width : f32 ;
|
||||
if width > 400.0 {
|
||||
ui.add(Slider::new(&mut source.interval, 1..=120));
|
||||
text_width = width - 330.0
|
||||
} else {
|
||||
ui.add(DragValue::new(&mut source.interval).clamp_range(1..=120));
|
||||
text_width = width - 225.0
|
||||
}
|
||||
TextEdit::singleline(&mut source.query_x)
|
||||
.desired_width(text_width / 2.0)
|
||||
.hint_text("x")
|
||||
.show(ui);
|
||||
TextEdit::singleline(&mut source.query_y)
|
||||
.desired_width(text_width / 2.0)
|
||||
.hint_text("y")
|
||||
.show(ui);
|
||||
ComboBox::from_id_source(format!("panel-{}", source.id))
|
||||
.width(60.0)
|
||||
.selected_text(format!("panel: {}", source.panel_id))
|
||||
.show_ui(ui, |ui| {
|
||||
ui.selectable_value(&mut source.panel_id, -1, "None");
|
||||
for p in panels {
|
||||
ui.selectable_value(&mut source.panel_id, p.id, p.name.as_str());
|
||||
}
|
||||
});
|
||||
ui.color_edit_button_srgba(&mut source.color);
|
||||
});
|
||||
});
|
||||
pub fn source_display_ui(ui: &mut Ui, source: &mut Source, metrics: &Vec<Metric>, _width: f32) {
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_enabled(false, Checkbox::new(&mut source.enabled, ""));
|
||||
ui.add_enabled(false, DragValue::new(&mut source.interval).clamp_range(1..=120));
|
||||
ui.heading(&source.name).on_hover_text(&source.url);
|
||||
});
|
||||
for metric in metrics.iter() {
|
||||
if metric.source_id == source.id {
|
||||
metric_display_ui(ui, metric, ui.available_width());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn source_edit_ui(ui: &mut Ui, source: &mut Source, metrics: Option<&mut Vec<Metric>>, panels: &Vec<Panel>, width: f32) {
|
||||
ui.horizontal(|ui| {
|
||||
let text_width = width - 100.0;
|
||||
ui.checkbox(&mut source.enabled, "");
|
||||
ui.add(DragValue::new(&mut source.interval).clamp_range(1..=3600));
|
||||
TextEdit::singleline(&mut source.name)
|
||||
.desired_width(text_width / 4.0)
|
||||
.hint_text("name")
|
||||
.show(ui);
|
||||
TextEdit::singleline(&mut source.url)
|
||||
.desired_width(text_width * 3.0 / 4.0)
|
||||
.hint_text("url")
|
||||
.show(ui);
|
||||
});
|
||||
if let Some(metrics) = metrics {
|
||||
for metric in metrics.iter_mut() {
|
||||
if metric.source_id == source.id {
|
||||
metric_edit_ui(ui, metric, Some(panels), width - 10.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
144
src/app/mod.rs
144
src/app/mod.rs
|
@ -11,18 +11,21 @@ use eframe::emath::Align;
|
|||
use std::sync::Arc;
|
||||
use tracing::error;
|
||||
|
||||
use self::data::source::{Panel, Source};
|
||||
use self::data::source::{Metric, Panel, Source};
|
||||
use self::data::ApplicationState;
|
||||
use self::gui::metric::metric_edit_ui;
|
||||
use self::gui::panel::{panel_body_ui, panel_edit_inline_ui, panel_title_ui};
|
||||
use self::gui::source::source_edit_ui;
|
||||
use self::gui::source::{source_display_ui, source_edit_ui};
|
||||
use self::util::human_size;
|
||||
use self::worker::native_save;
|
||||
|
||||
pub struct App {
|
||||
data: Arc<ApplicationState>,
|
||||
input_metric: Metric,
|
||||
input_source: Source,
|
||||
input_panel: Panel,
|
||||
edit: bool,
|
||||
sources: bool,
|
||||
padding: bool,
|
||||
}
|
||||
|
||||
|
@ -30,9 +33,11 @@ impl App {
|
|||
pub fn new(_cc: &eframe::CreationContext, data: Arc<ApplicationState>) -> Self {
|
||||
Self {
|
||||
data,
|
||||
input_metric: Metric::default(),
|
||||
input_panel: Panel::default(),
|
||||
input_source: Source::default(),
|
||||
edit: false,
|
||||
sources: true,
|
||||
padding: false,
|
||||
}
|
||||
}
|
||||
|
@ -45,6 +50,8 @@ impl eframe::App for App {
|
|||
global_dark_light_mode_switch(ui);
|
||||
ui.heading("dashboard");
|
||||
ui.separator();
|
||||
ui.checkbox(&mut self.sources, "sources");
|
||||
ui.separator();
|
||||
ui.checkbox(&mut self.edit, "edit");
|
||||
if self.edit {
|
||||
if ui.button("save").clicked() {
|
||||
|
@ -118,48 +125,98 @@ impl eframe::App for App {
|
|||
});
|
||||
});
|
||||
});
|
||||
if self.edit {
|
||||
if self.sources {
|
||||
let mut to_swap: Option<usize> = None;
|
||||
// let mut to_delete: Option<usize> = None;
|
||||
SidePanel::left("sources-bar")
|
||||
.width_range(280.0..=800.0)
|
||||
.default_width(500.0)
|
||||
.default_width(350.0)
|
||||
.show(ctx, |ui| {
|
||||
let panels = self.data.panels.read().expect("Panels RwLock poisoned");
|
||||
ScrollArea::vertical().show(ui, |ui| {
|
||||
let panel_width = ui.available_width();
|
||||
let panel_width = ui.available_width();
|
||||
ScrollArea::both().max_width(panel_width).show(ui, |ui| {
|
||||
// TODO only vertical!
|
||||
{
|
||||
let mut sources = self.data.sources.write().expect("Sources RwLock poisoned");
|
||||
let mut sources =
|
||||
self.data.sources.write().expect("Sources RwLock poisoned");
|
||||
let sources_count = sources.len();
|
||||
ui.heading("Sources");
|
||||
ui.separator();
|
||||
for (index, source) in sources.iter_mut().enumerate() {
|
||||
ui.horizontal(|ui| {
|
||||
if self.edit {
|
||||
ui.vertical(|ui| {
|
||||
ui.add_space(10.0);
|
||||
if ui.small_button("+").clicked() {
|
||||
if index > 0 {
|
||||
to_swap = Some(index); // TODO kinda jank but is there a better way?
|
||||
}
|
||||
}
|
||||
if ui.small_button("−").clicked() {
|
||||
if index < sources_count - 1 {
|
||||
to_swap = Some(index + 1); // TODO kinda jank but is there a better way?
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
ui.vertical(|ui| {
|
||||
ui.add_space(10.0);
|
||||
if ui.small_button("+").clicked() {
|
||||
if index > 0 {
|
||||
to_swap = Some(index); // TODO kinda jank but is there a better way?
|
||||
}
|
||||
}
|
||||
if ui.small_button("−").clicked() {
|
||||
if index < sources_count - 1 {
|
||||
to_swap = Some(index + 1); // TODO kinda jank but is there a better way?
|
||||
}
|
||||
let remaining_width = ui.available_width();
|
||||
if self.edit {
|
||||
ui.group(|ui| {
|
||||
source_edit_ui(
|
||||
ui,
|
||||
source,
|
||||
Some(&mut *self.data.metrics.write().expect("Metrics RwLock poisoned")),
|
||||
&panels,
|
||||
remaining_width,
|
||||
);
|
||||
ui.horizontal(|ui| {
|
||||
metric_edit_ui(
|
||||
ui,
|
||||
&mut self.input_metric,
|
||||
None,
|
||||
remaining_width - 10.0,
|
||||
);
|
||||
ui.add_space(5.0);
|
||||
if ui.button(" × ").clicked() {
|
||||
self.input_metric = Metric::default();
|
||||
}
|
||||
ui.separator();
|
||||
if ui.button(" + ").clicked() {
|
||||
if let Err(e) = self
|
||||
.data
|
||||
.add_metric(&self.input_metric, source)
|
||||
{
|
||||
error!(target: "ui", "Error adding metric : {:?}", e);
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
} else {
|
||||
let metrics =
|
||||
self.data.metrics.read().expect("Metrics RwLock poisoned");
|
||||
source_display_ui(
|
||||
ui,
|
||||
source,
|
||||
&metrics,
|
||||
remaining_width,
|
||||
);
|
||||
ui.separator();
|
||||
}
|
||||
});
|
||||
let remaining_width = ui.available_width();
|
||||
source_edit_ui(ui, source, &panels, remaining_width);
|
||||
});
|
||||
}
|
||||
}
|
||||
ui.add_space(20.0);
|
||||
ui.separator();
|
||||
ui.horizontal(|ui| {
|
||||
ui.heading("new source");
|
||||
if self.edit {
|
||||
ui.add_space(20.0);
|
||||
ui.separator();
|
||||
ui.horizontal(|ui| {
|
||||
ui.heading("new source");
|
||||
ui.with_layout(Layout::top_down(Align::RIGHT), |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("add").clicked() {
|
||||
if let Err(e) = self.data.add_source(&self.input_source) {
|
||||
error!(target: "ui", "Error adding souce : {:?}", e);
|
||||
error!(target: "ui", "Error adding source : {:?}", e);
|
||||
} else {
|
||||
self.input_source.id += 1;
|
||||
}
|
||||
|
@ -167,10 +224,17 @@ impl eframe::App for App {
|
|||
ui.toggle_value(&mut self.padding, "#");
|
||||
});
|
||||
});
|
||||
});
|
||||
source_edit_ui(ui, &mut self.input_source, &panels, panel_width);
|
||||
if self.padding {
|
||||
ui.add_space(300.0);
|
||||
});
|
||||
source_edit_ui(
|
||||
ui,
|
||||
&mut self.input_source,
|
||||
None,
|
||||
&panels,
|
||||
panel_width,
|
||||
);
|
||||
if self.padding {
|
||||
ui.add_space(300.0);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -191,7 +255,7 @@ impl eframe::App for App {
|
|||
ScrollArea::vertical().show(ui, |ui| {
|
||||
let mut panels = self.data.panels.write().expect("Panels RwLock poisoned"); // TODO only lock as write when editing
|
||||
let panels_count = panels.len();
|
||||
let sources = self.data.sources.read().expect("Sources RwLock poisoned"); // TODO only lock as write when editing
|
||||
let metrics = self.data.metrics.read().expect("Metrics RwLock poisoned"); // TODO only lock as write when editing
|
||||
for (index, panel) in panels.iter_mut().enumerate() {
|
||||
if index > 0 {
|
||||
ui.separator();
|
||||
|
@ -220,19 +284,31 @@ impl eframe::App for App {
|
|||
}
|
||||
panel_title_ui(ui, panel, self.edit);
|
||||
})
|
||||
.body(|ui| panel_body_ui(ui, panel, &sources));
|
||||
.body(|ui| panel_body_ui(ui, panel, &metrics));
|
||||
}
|
||||
});
|
||||
});
|
||||
if let Some(i) = to_delete {
|
||||
// TODO can this be done in background? idk
|
||||
let mut panels = self.data.panels.write().expect("Panels RwLock poisoned");
|
||||
if let Err(e) = self.data.storage.lock().expect("Storage Mutex poisoned").delete_panel(panels[i].id) {
|
||||
if let Err(e) = self
|
||||
.data
|
||||
.storage
|
||||
.lock()
|
||||
.expect("Storage Mutex poisoned")
|
||||
.delete_panel(panels[i].id)
|
||||
{
|
||||
error!(target: "ui", "Could not delete panel : {:?}", e);
|
||||
} else {
|
||||
for source in self.data.sources.write().expect("Sources RwLock poisoned").iter_mut() {
|
||||
if source.panel_id == panels[i].id {
|
||||
source.panel_id = -1;
|
||||
for metric in self
|
||||
.data
|
||||
.metrics
|
||||
.write()
|
||||
.expect("Sources RwLock poisoned")
|
||||
.iter_mut()
|
||||
{
|
||||
if metric.panel_id == panels[i].id {
|
||||
metric.panel_id = -1;
|
||||
}
|
||||
}
|
||||
panels.remove(i);
|
||||
|
|
|
@ -46,6 +46,7 @@ pub fn unpack_color(c: u32) -> Color32 {
|
|||
return Color32::from_rgba_unmultiplied(r, g, b, a);
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn repack_color(c: Color32) -> u32 {
|
||||
let mut out: u32 = 0;
|
||||
let mut offset = 0;
|
||||
|
|
|
@ -26,22 +26,33 @@ pub fn native_save(state: Arc<ApplicationState>) {
|
|||
) {
|
||||
warn!(target: "native-save", "Could not update panel #{} : {:?}", panel.id, e);
|
||||
}
|
||||
let sources = state.sources.read().expect("Sources RwLock poisoned");
|
||||
for (index, source) in sources.iter().enumerate() {
|
||||
if let Err(e) = storage.update_source(
|
||||
source.id,
|
||||
source.panel_id,
|
||||
source.name.as_str(),
|
||||
source.enabled,
|
||||
source.url.as_str(),
|
||||
source.interval,
|
||||
source.query_x.as_str(),
|
||||
source.query_y.as_str(),
|
||||
source.color,
|
||||
index as i32,
|
||||
) {
|
||||
warn!(target: "native-save", "Could not update source #{} : {:?}", source.id, e);
|
||||
}
|
||||
}
|
||||
let sources = state.sources.read().expect("Sources RwLock poisoned");
|
||||
for (index, source) in sources.iter().enumerate() {
|
||||
if let Err(e) = storage.update_source(
|
||||
source.id,
|
||||
source.name.as_str(),
|
||||
source.enabled,
|
||||
source.url.as_str(),
|
||||
source.interval,
|
||||
index as i32,
|
||||
) {
|
||||
warn!(target: "native-save", "Could not update source #{} : {:?}", source.id, e);
|
||||
}
|
||||
}
|
||||
let metrics = state.metrics.read().expect("Metrics RwLock poisoned");
|
||||
for (index, metric) in metrics.iter().enumerate() {
|
||||
if let Err(e) = storage.update_metric(
|
||||
metric.id,
|
||||
metric.name.as_str(),
|
||||
metric.source_id,
|
||||
metric.query_x.as_str(),
|
||||
metric.query_y.as_str(),
|
||||
metric.panel_id,
|
||||
metric.color,
|
||||
index as i32,
|
||||
) {
|
||||
warn!(target: "native-save", "Could not update metric #{} : {:?}", metric.id, e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -78,30 +89,33 @@ impl BackgroundWorker for NativeBackgroundWorker {
|
|||
*last_update = Utc::now();
|
||||
let state2 = state.clone();
|
||||
let url = sources[j].url.clone();
|
||||
let query_x = sources[j].query_x.clone();
|
||||
let query_y = sources[j].query_y.clone();
|
||||
std::thread::spawn(move || {
|
||||
// TODO this can overspawn if a request takes longer than the refresh interval!
|
||||
match fetch(url.as_str(), query_x.as_str(), query_y.as_str()) {
|
||||
Ok(v) => {
|
||||
match fetch(url.as_str()) {
|
||||
Ok(res) => {
|
||||
let store =
|
||||
state2.storage.lock().expect("Storage mutex poisoned");
|
||||
if let Err(e) = store.put_value(s_id, v) {
|
||||
warn!(target:"background-worker", "Could not put sample for source #{} in db: {:?}", s_id, e);
|
||||
} else {
|
||||
let sources =
|
||||
state2.sources.read().expect("Sources RwLock poisoned");
|
||||
sources[j]
|
||||
.data
|
||||
.write()
|
||||
.expect("Source data RwLock poisoned")
|
||||
.push(v);
|
||||
let mut last_update = sources[j]
|
||||
.last_fetch
|
||||
.write()
|
||||
.expect("Source last update RwLock poisoned");
|
||||
*last_update = Utc::now(); // overwrite it so fetches comply with API slowdowns and get desynched among them
|
||||
for metric in state2.metrics.read().expect("Metrics RwLock poisoned").iter() {
|
||||
if metric.source_id == s_id {
|
||||
match metric.extract(&res) {
|
||||
Ok(v) => {
|
||||
metric.data.write().expect("Data RwLock poisoned").push(v);
|
||||
if let Err(e) = store.put_value(metric.id, v) {
|
||||
warn!(target:"background-worker", "Could not put sample for source #{} in db: {:?}", s_id, e);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(target:"background-worker", "[{}] Could not extract value from result: {:?}", metric.name, e); // TODO: more info!
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let sources = state2.sources.read().expect("Sources RwLock poisoned");
|
||||
let mut last_update = sources[j]
|
||||
.last_fetch
|
||||
.write()
|
||||
.expect("Source last update RwLock poisoned");
|
||||
*last_update = Utc::now(); // overwrite it so fetches comply with API slowdowns and get desynched among them
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(target:"background-worker", "Could not fetch value from {} : {:?}", url, e);
|
||||
|
|
Loading…
Reference in a new issue