mirror of
https://git.alemi.dev/dashboard.git
synced 2024-11-14 03:49:19 +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
bd3acb188b
commit
f249ca372a
11 changed files with 495 additions and 241 deletions
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "dashboard"
|
name = "dashboard"
|
||||||
version = "0.2.1"
|
version = "0.3.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
pub mod source;
|
pub mod source;
|
||||||
pub mod store;
|
pub mod store;
|
||||||
|
|
||||||
use self::source::{Panel, Source};
|
use self::source::{Panel, Source, Metric};
|
||||||
use self::store::SQLiteDataStore;
|
use self::store::SQLiteDataStore;
|
||||||
use std::num::ParseFloatError;
|
use std::num::ParseFloatError;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
@ -50,6 +50,7 @@ pub struct ApplicationState {
|
||||||
pub file_size: RwLock<u64>,
|
pub file_size: RwLock<u64>,
|
||||||
pub panels: RwLock<Vec<Panel>>,
|
pub panels: RwLock<Vec<Panel>>,
|
||||||
pub sources: RwLock<Vec<Source>>,
|
pub sources: RwLock<Vec<Source>>,
|
||||||
|
pub metrics: RwLock<Vec<Metric>>,
|
||||||
pub storage: Mutex<SQLiteDataStore>,
|
pub storage: Mutex<SQLiteDataStore>,
|
||||||
pub diagnostics: RwLock<Vec<String>>,
|
pub diagnostics: RwLock<Vec<String>>,
|
||||||
}
|
}
|
||||||
|
@ -60,6 +61,7 @@ impl ApplicationState {
|
||||||
|
|
||||||
let panels = storage.load_panels()?;
|
let panels = storage.load_panels()?;
|
||||||
let sources = storage.load_sources()?;
|
let sources = storage.load_sources()?;
|
||||||
|
let metrics = storage.load_metrics()?;
|
||||||
|
|
||||||
return Ok(ApplicationState {
|
return Ok(ApplicationState {
|
||||||
run: true,
|
run: true,
|
||||||
|
@ -67,6 +69,7 @@ impl ApplicationState {
|
||||||
file_path: path,
|
file_path: path,
|
||||||
panels: RwLock::new(panels),
|
panels: RwLock::new(panels),
|
||||||
sources: RwLock::new(sources),
|
sources: RwLock::new(sources),
|
||||||
|
metrics: RwLock::new(metrics),
|
||||||
storage: Mutex::new(storage),
|
storage: Mutex::new(storage),
|
||||||
diagnostics: RwLock::new(Vec::new()),
|
diagnostics: RwLock::new(Vec::new()),
|
||||||
});
|
});
|
||||||
|
@ -104,14 +107,10 @@ impl ApplicationState {
|
||||||
.lock()
|
.lock()
|
||||||
.expect("Storage Mutex poisoned")
|
.expect("Storage Mutex poisoned")
|
||||||
.new_source(
|
.new_source(
|
||||||
source.panel_id,
|
|
||||||
source.name.as_str(),
|
source.name.as_str(),
|
||||||
source.enabled,
|
source.enabled,
|
||||||
source.url.as_str(),
|
source.url.as_str(),
|
||||||
source.interval,
|
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.read().expect("Sources RwLock poisoned").len() as i32,
|
||||||
)?;
|
)?;
|
||||||
self.sources
|
self.sources
|
||||||
|
@ -120,4 +119,25 @@ impl ApplicationState {
|
||||||
.push(verified_source);
|
.push(verified_source);
|
||||||
return Ok(());
|
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::egui::plot::{Value, Values};
|
||||||
use eframe::epaint::Color32;
|
use eframe::epaint::Color32;
|
||||||
use std::sync::RwLock;
|
use std::sync::RwLock;
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Panel {
|
pub struct Panel {
|
||||||
|
@ -46,14 +47,7 @@ pub struct Source {
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
pub url: String,
|
pub url: String,
|
||||||
pub interval: i32,
|
pub interval: i32,
|
||||||
pub color: Color32,
|
|
||||||
pub(crate) last_fetch: RwLock<DateTime<Utc>>,
|
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 {
|
impl Default for Source {
|
||||||
|
@ -64,12 +58,7 @@ impl Default for Source {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
url: "".to_string(),
|
url: "".to_string(),
|
||||||
interval: 60,
|
interval: 60,
|
||||||
color: Color32::TRANSPARENT,
|
|
||||||
last_fetch: RwLock::new(Utc::now()),
|
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;
|
x += v.x;
|
||||||
y += v.y;
|
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 {
|
impl Source {
|
||||||
|
@ -90,8 +82,64 @@ impl Source {
|
||||||
return (Utc::now() - *last_fetch).num_seconds() < self.interval as i64;
|
return (Utc::now() - *last_fetch).num_seconds() < self.interval as i64;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO optimize this with caching!
|
pub fn fetch(&self) -> Result<serde_json::Value, FetchError> {
|
||||||
pub fn values(&self, min_x: Option<f64>, max_x: Option<f64>, chunk_size: Option<u32>) -> Values {
|
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();
|
let mut values = self.data.read().expect("Values RwLock poisoned").clone();
|
||||||
if let Some(min_x) = min_x {
|
if let Some(min_x) = min_x {
|
||||||
values.retain(|x| x.x > min_x);
|
values.retain(|x| x.x > min_x);
|
||||||
|
@ -100,27 +148,12 @@ impl Source {
|
||||||
values.retain(|x| x.x < max_x);
|
values.retain(|x| x.x < max_x);
|
||||||
}
|
}
|
||||||
if let Some(chunk_size) = chunk_size {
|
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);
|
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)
|
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 rusqlite::{params, Connection};
|
||||||
use std::sync::RwLock;
|
use std::sync::RwLock;
|
||||||
|
|
||||||
|
use super::source::Metric;
|
||||||
|
|
||||||
pub trait DataStorage {
|
pub trait DataStorage {
|
||||||
fn add_panel(&self, name: &str);
|
fn add_panel(&self, name: &str);
|
||||||
}
|
}
|
||||||
|
@ -46,19 +48,38 @@ impl SQLiteDataStore {
|
||||||
enabled BOOL NOT NULL,
|
enabled BOOL NOT NULL,
|
||||||
url TEXT NOT NULL,
|
url TEXT NOT NULL,
|
||||||
interval INT 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
|
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(
|
conn.execute(
|
||||||
"CREATE TABLE IF NOT EXISTS points (
|
"CREATE TABLE IF NOT EXISTS points (
|
||||||
id INTEGER PRIMARY KEY,
|
id INTEGER PRIMARY KEY,
|
||||||
source_id INT NOT NULL,
|
metric_id INT NOT NULL,
|
||||||
x FLOAT NOT NULL,
|
x FLOAT NOT NULL,
|
||||||
y FLOAT NOT NULL
|
y FLOAT NOT NULL
|
||||||
);",
|
);",
|
||||||
|
@ -68,12 +89,12 @@ impl SQLiteDataStore {
|
||||||
Ok(SQLiteDataStore { conn })
|
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 values: Vec<Value> = Vec::new();
|
||||||
let mut statement = self
|
let mut statement = self
|
||||||
.conn
|
.conn
|
||||||
.prepare("SELECT x, y FROM points WHERE source_id = ?")?;
|
.prepare("SELECT x, y FROM points WHERE metric_id = ?")?;
|
||||||
let values_iter = statement.query_map(params![source_id], |row| {
|
let values_iter = statement.query_map(params![metric_id], |row| {
|
||||||
Ok(Value {
|
Ok(Value {
|
||||||
x: row.get(0)?,
|
x: row.get(0)?,
|
||||||
y: row.get(1)?,
|
y: row.get(1)?,
|
||||||
|
@ -89,10 +110,10 @@ impl SQLiteDataStore {
|
||||||
Ok(values)
|
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(
|
self.conn.execute(
|
||||||
"INSERT INTO points(source_id, x, y) VALUES (?, ?, ?)",
|
"INSERT INTO points(metric_id, x, y) VALUES (?, ?, ?)",
|
||||||
params![source_id, v.x, v.y],
|
params![metric_id, v.x, v.y],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -107,17 +128,11 @@ impl SQLiteDataStore {
|
||||||
url: row.get(3)?,
|
url: row.get(3)?,
|
||||||
interval: row.get(4)?,
|
interval: row.get(4)?,
|
||||||
last_fetch: RwLock::new(Utc.ymd(1970, 1, 1).and_hms(0, 0, 0)),
|
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 {
|
for source in sources_iter {
|
||||||
if let Ok(mut s) = source {
|
if let Ok(s) = source {
|
||||||
s.data = RwLock::new(self.load_values(s.id)?);
|
|
||||||
sources.push(s);
|
sources.push(s);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -128,41 +143,28 @@ impl SQLiteDataStore {
|
||||||
// jank! TODO make it not jank!
|
// jank! TODO make it not jank!
|
||||||
pub fn new_source(
|
pub fn new_source(
|
||||||
&self,
|
&self,
|
||||||
panel_id: i32,
|
|
||||||
name: &str,
|
name: &str,
|
||||||
enabled: bool,
|
enabled: bool,
|
||||||
url: &str,
|
url: &str,
|
||||||
interval: i32,
|
interval: i32,
|
||||||
query_x: &str,
|
|
||||||
query_y: &str,
|
|
||||||
color: Color32,
|
|
||||||
position: i32,
|
position: i32,
|
||||||
) -> rusqlite::Result<Source> {
|
) -> rusqlite::Result<Source> {
|
||||||
let color_u32: Option<u32> = if color == Color32::TRANSPARENT {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(repack_color(color))
|
|
||||||
};
|
|
||||||
self.conn.execute(
|
self.conn.execute(
|
||||||
"INSERT INTO sources(name, enabled, url, interval, query_x, query_y, panel_id, color, position) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
"INSERT INTO sources(name, enabled, url, interval, position) VALUES (?, ?, ?, ?, ?)",
|
||||||
params![name, enabled, url, interval, query_x, query_y, panel_id, color_u32, position],
|
params![name, enabled, url, interval, position],
|
||||||
)?;
|
)?;
|
||||||
let mut statement = self
|
let mut statement = self
|
||||||
.conn
|
.conn
|
||||||
.prepare("SELECT * FROM sources WHERE name = ? AND panel_id = ?")?;
|
.prepare("SELECT * FROM sources WHERE name = ? AND url = ? ORDER BY id DESC")?;
|
||||||
for panel in statement.query_map(params![name, panel_id], |row| {
|
for panel in statement.query_map(params![name, url], |row| {
|
||||||
Ok(Source {
|
Ok(Source {
|
||||||
id: row.get(0)?,
|
id: row.get(0)?,
|
||||||
name: row.get(1)?,
|
name: row.get(1)?,
|
||||||
enabled: row.get(2)?,
|
enabled: row.get(2)?,
|
||||||
url: row.get(3)?,
|
url: row.get(3)?,
|
||||||
interval: row.get(4)?,
|
interval: row.get(4)?,
|
||||||
|
// position: row.get(5)?,
|
||||||
last_fetch: RwLock::new(Utc.ymd(1970, 1, 1).and_hms(0, 0, 0)),
|
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 {
|
if let Ok(p) = panel {
|
||||||
|
@ -175,25 +177,16 @@ impl SQLiteDataStore {
|
||||||
|
|
||||||
pub fn update_source(
|
pub fn update_source(
|
||||||
&self,
|
&self,
|
||||||
source_id: i32,
|
id: i32,
|
||||||
panel_id: i32,
|
|
||||||
name: &str,
|
name: &str,
|
||||||
enabled: bool,
|
enabled: bool,
|
||||||
url: &str,
|
url: &str,
|
||||||
interval: i32,
|
interval: i32,
|
||||||
query_x: &str,
|
|
||||||
query_y: &str,
|
|
||||||
color: Color32,
|
|
||||||
position: i32,
|
position: i32,
|
||||||
) -> rusqlite::Result<usize> {
|
) -> rusqlite::Result<usize> {
|
||||||
let color_u32: Option<u32> = if color == Color32::TRANSPARENT {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(repack_color(color))
|
|
||||||
};
|
|
||||||
self.conn.execute(
|
self.conn.execute(
|
||||||
"UPDATE sources SET name = ?, enabled = ?, url = ?, interval = ?, query_x = ?, query_y = ?, panel_id = ?, color = ?, position = ? WHERE id = ?",
|
"UPDATE sources SET name = ?, enabled = ?, url = ?, interval = ?, position = ? WHERE id = ?",
|
||||||
params![name, enabled, url, interval, query_x, query_y, panel_id, color_u32, position, source_id],
|
params![name, enabled, url, interval, position, id],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -201,6 +194,93 @@ impl SQLiteDataStore {
|
||||||
// self.conn.execute("DELETE FROM sources WHERE id = ?", params![id])
|
// 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>> {
|
pub fn load_panels(&self) -> rusqlite::Result<Vec<Panel>> {
|
||||||
let mut panels: Vec<Panel> = Vec::new();
|
let mut panels: Vec<Panel> = Vec::new();
|
||||||
let mut statement = self
|
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 panel;
|
||||||
pub mod source;
|
pub mod source;
|
||||||
|
pub mod metric;
|
||||||
|
|
|
@ -5,7 +5,7 @@ use eframe::{egui::{
|
||||||
}, emath::Vec2};
|
}, emath::Vec2};
|
||||||
|
|
||||||
use crate::app::{
|
use crate::app::{
|
||||||
data::source::{Panel, Source},
|
data::source::{Panel, Metric},
|
||||||
util::timestamp_to_str,
|
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.toggle_value(&mut panel.view_scroll, "🔒");
|
||||||
ui.separator();
|
ui.separator();
|
||||||
if panel.limit {
|
if panel.limit {
|
||||||
ui.label("min"); // TODO makes no sense if it's not a timeserie
|
|
||||||
ui.add(
|
ui.add(
|
||||||
DragValue::new(&mut panel.view_size)
|
DragValue::new(&mut panel.view_size)
|
||||||
.speed(10)
|
.speed(10)
|
||||||
|
.suffix(" min")
|
||||||
.clamp_range(0..=2147483647i32),
|
.clamp_range(0..=2147483647i32),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
ui.toggle_value(&mut panel.limit, "limit");
|
ui.toggle_value(&mut panel.limit, "limit");
|
||||||
ui.separator();
|
ui.separator();
|
||||||
if panel.shift {
|
if panel.shift {
|
||||||
ui.label("min");
|
|
||||||
ui.add(
|
ui.add(
|
||||||
DragValue::new(&mut panel.view_offset)
|
DragValue::new(&mut panel.view_offset)
|
||||||
.speed(10)
|
.speed(10)
|
||||||
|
.suffix(" min")
|
||||||
.clamp_range(0..=2147483647i32),
|
.clamp_range(0..=2147483647i32),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
ui.toggle_value(&mut panel.shift, "offset");
|
ui.toggle_value(&mut panel.shift, "offset");
|
||||||
ui.separator();
|
ui.separator();
|
||||||
if panel.reduce {
|
if panel.reduce {
|
||||||
ui.label("x");
|
|
||||||
ui.add(
|
ui.add(
|
||||||
DragValue::new(&mut panel.view_chunks)
|
DragValue::new(&mut panel.view_chunks)
|
||||||
.speed(1)
|
.speed(1)
|
||||||
|
.prefix("x")
|
||||||
.clamp_range(1..=1000), // TODO allow to average larger spans maybe?
|
.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))
|
let mut p = Plot::new(format!("plot-{}", panel.name))
|
||||||
.height(panel.height as f32)
|
.height(panel.height as f32)
|
||||||
.allow_scroll(false)
|
.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 min_x = if panel.limit { Some(_now - _size - _off) } else { None };
|
||||||
let max_x = if panel.shift { Some(_now - _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 };
|
let chunk_size = if panel.reduce { Some(panel.view_chunks) } else { None };
|
||||||
for source in sources {
|
for metric in metrics {
|
||||||
if source.panel_id == panel.id {
|
if metric.panel_id == panel.id {
|
||||||
// let chunks = None;
|
// let chunks = None;
|
||||||
let line = Line::new(source.values(min_x, max_x, chunk_size)).name(source.name.as_str());
|
let line = Line::new(metric.values(min_x, max_x, chunk_size))
|
||||||
plot_ui.line(line.color(source.color));
|
.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)]
|
use super::metric::{metric_edit_ui, metric_display_ui};
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn source_edit_ui(ui: &mut Ui, source: &mut Source, panels: &Vec<Panel>, width: f32) {
|
pub fn source_display_ui(ui: &mut Ui, source: &mut Source, metrics: &Vec<Metric>, _width: f32) {
|
||||||
ui.group(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.vertical(|ui| {
|
ui.add_enabled(false, Checkbox::new(&mut source.enabled, ""));
|
||||||
ui.horizontal(|ui| {
|
ui.add_enabled(false, DragValue::new(&mut source.interval).clamp_range(1..=120));
|
||||||
let text_width = width - 25.0;
|
ui.heading(&source.name).on_hover_text(&source.url);
|
||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
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 std::sync::Arc;
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
use self::data::source::{Panel, Source};
|
use self::data::source::{Metric, Panel, Source};
|
||||||
use self::data::ApplicationState;
|
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::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::util::human_size;
|
||||||
use self::worker::native_save;
|
use self::worker::native_save;
|
||||||
|
|
||||||
pub struct App {
|
pub struct App {
|
||||||
data: Arc<ApplicationState>,
|
data: Arc<ApplicationState>,
|
||||||
|
input_metric: Metric,
|
||||||
input_source: Source,
|
input_source: Source,
|
||||||
input_panel: Panel,
|
input_panel: Panel,
|
||||||
edit: bool,
|
edit: bool,
|
||||||
|
sources: bool,
|
||||||
padding: bool,
|
padding: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,9 +33,11 @@ impl App {
|
||||||
pub fn new(_cc: &eframe::CreationContext, data: Arc<ApplicationState>) -> Self {
|
pub fn new(_cc: &eframe::CreationContext, data: Arc<ApplicationState>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
data,
|
data,
|
||||||
|
input_metric: Metric::default(),
|
||||||
input_panel: Panel::default(),
|
input_panel: Panel::default(),
|
||||||
input_source: Source::default(),
|
input_source: Source::default(),
|
||||||
edit: false,
|
edit: false,
|
||||||
|
sources: true,
|
||||||
padding: false,
|
padding: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -45,6 +50,8 @@ impl eframe::App for App {
|
||||||
global_dark_light_mode_switch(ui);
|
global_dark_light_mode_switch(ui);
|
||||||
ui.heading("dashboard");
|
ui.heading("dashboard");
|
||||||
ui.separator();
|
ui.separator();
|
||||||
|
ui.checkbox(&mut self.sources, "sources");
|
||||||
|
ui.separator();
|
||||||
ui.checkbox(&mut self.edit, "edit");
|
ui.checkbox(&mut self.edit, "edit");
|
||||||
if self.edit {
|
if self.edit {
|
||||||
if ui.button("save").clicked() {
|
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_swap: Option<usize> = None;
|
||||||
// let mut to_delete: Option<usize> = None;
|
// let mut to_delete: Option<usize> = None;
|
||||||
SidePanel::left("sources-bar")
|
SidePanel::left("sources-bar")
|
||||||
.width_range(280.0..=800.0)
|
.width_range(280.0..=800.0)
|
||||||
.default_width(500.0)
|
.default_width(350.0)
|
||||||
.show(ctx, |ui| {
|
.show(ctx, |ui| {
|
||||||
let panels = self.data.panels.read().expect("Panels RwLock poisoned");
|
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();
|
let sources_count = sources.len();
|
||||||
|
ui.heading("Sources");
|
||||||
|
ui.separator();
|
||||||
for (index, source) in sources.iter_mut().enumerate() {
|
for (index, source) in sources.iter_mut().enumerate() {
|
||||||
ui.horizontal(|ui| {
|
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.vertical(|ui| {
|
||||||
ui.add_space(10.0);
|
let remaining_width = ui.available_width();
|
||||||
if ui.small_button("+").clicked() {
|
if self.edit {
|
||||||
if index > 0 {
|
ui.group(|ui| {
|
||||||
to_swap = Some(index); // TODO kinda jank but is there a better way?
|
source_edit_ui(
|
||||||
}
|
ui,
|
||||||
}
|
source,
|
||||||
if ui.small_button("−").clicked() {
|
Some(&mut *self.data.metrics.write().expect("Metrics RwLock poisoned")),
|
||||||
if index < sources_count - 1 {
|
&panels,
|
||||||
to_swap = Some(index + 1); // TODO kinda jank but is there a better way?
|
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);
|
if self.edit {
|
||||||
ui.separator();
|
ui.add_space(20.0);
|
||||||
ui.horizontal(|ui| {
|
ui.separator();
|
||||||
ui.heading("new source");
|
ui.horizontal(|ui| {
|
||||||
|
ui.heading("new source");
|
||||||
ui.with_layout(Layout::top_down(Align::RIGHT), |ui| {
|
ui.with_layout(Layout::top_down(Align::RIGHT), |ui| {
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
if ui.button("add").clicked() {
|
if ui.button("add").clicked() {
|
||||||
if let Err(e) = self.data.add_source(&self.input_source) {
|
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 {
|
} else {
|
||||||
self.input_source.id += 1;
|
self.input_source.id += 1;
|
||||||
}
|
}
|
||||||
|
@ -167,10 +224,17 @@ impl eframe::App for App {
|
||||||
ui.toggle_value(&mut self.padding, "#");
|
ui.toggle_value(&mut self.padding, "#");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
source_edit_ui(ui, &mut self.input_source, &panels, panel_width);
|
source_edit_ui(
|
||||||
if self.padding {
|
ui,
|
||||||
ui.add_space(300.0);
|
&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| {
|
ScrollArea::vertical().show(ui, |ui| {
|
||||||
let mut panels = self.data.panels.write().expect("Panels RwLock poisoned"); // TODO only lock as write when editing
|
let mut panels = self.data.panels.write().expect("Panels RwLock poisoned"); // TODO only lock as write when editing
|
||||||
let panels_count = panels.len();
|
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() {
|
for (index, panel) in panels.iter_mut().enumerate() {
|
||||||
if index > 0 {
|
if index > 0 {
|
||||||
ui.separator();
|
ui.separator();
|
||||||
|
@ -220,19 +284,31 @@ impl eframe::App for App {
|
||||||
}
|
}
|
||||||
panel_title_ui(ui, panel, self.edit);
|
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 {
|
if let Some(i) = to_delete {
|
||||||
// TODO can this be done in background? idk
|
// TODO can this be done in background? idk
|
||||||
let mut panels = self.data.panels.write().expect("Panels RwLock poisoned");
|
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);
|
error!(target: "ui", "Could not delete panel : {:?}", e);
|
||||||
} else {
|
} else {
|
||||||
for source in self.data.sources.write().expect("Sources RwLock poisoned").iter_mut() {
|
for metric in self
|
||||||
if source.panel_id == panels[i].id {
|
.data
|
||||||
source.panel_id = -1;
|
.metrics
|
||||||
|
.write()
|
||||||
|
.expect("Sources RwLock poisoned")
|
||||||
|
.iter_mut()
|
||||||
|
{
|
||||||
|
if metric.panel_id == panels[i].id {
|
||||||
|
metric.panel_id = -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
panels.remove(i);
|
panels.remove(i);
|
||||||
|
|
|
@ -46,6 +46,7 @@ pub fn unpack_color(c: u32) -> Color32 {
|
||||||
return Color32::from_rgba_unmultiplied(r, g, b, a);
|
return Color32::from_rgba_unmultiplied(r, g, b, a);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
pub fn repack_color(c: Color32) -> u32 {
|
pub fn repack_color(c: Color32) -> u32 {
|
||||||
let mut out: u32 = 0;
|
let mut out: u32 = 0;
|
||||||
let mut offset = 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);
|
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() {
|
let sources = state.sources.read().expect("Sources RwLock poisoned");
|
||||||
if let Err(e) = storage.update_source(
|
for (index, source) in sources.iter().enumerate() {
|
||||||
source.id,
|
if let Err(e) = storage.update_source(
|
||||||
source.panel_id,
|
source.id,
|
||||||
source.name.as_str(),
|
source.name.as_str(),
|
||||||
source.enabled,
|
source.enabled,
|
||||||
source.url.as_str(),
|
source.url.as_str(),
|
||||||
source.interval,
|
source.interval,
|
||||||
source.query_x.as_str(),
|
index as i32,
|
||||||
source.query_y.as_str(),
|
) {
|
||||||
source.color,
|
warn!(target: "native-save", "Could not update source #{} : {:?}", source.id, e);
|
||||||
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();
|
*last_update = Utc::now();
|
||||||
let state2 = state.clone();
|
let state2 = state.clone();
|
||||||
let url = sources[j].url.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 || {
|
std::thread::spawn(move || {
|
||||||
// TODO this can overspawn if a request takes longer than the refresh interval!
|
// 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()) {
|
match fetch(url.as_str()) {
|
||||||
Ok(v) => {
|
Ok(res) => {
|
||||||
let store =
|
let store =
|
||||||
state2.storage.lock().expect("Storage mutex poisoned");
|
state2.storage.lock().expect("Storage mutex poisoned");
|
||||||
if let Err(e) = store.put_value(s_id, v) {
|
for metric in state2.metrics.read().expect("Metrics RwLock poisoned").iter() {
|
||||||
warn!(target:"background-worker", "Could not put sample for source #{} in db: {:?}", s_id, e);
|
if metric.source_id == s_id {
|
||||||
} else {
|
match metric.extract(&res) {
|
||||||
let sources =
|
Ok(v) => {
|
||||||
state2.sources.read().expect("Sources RwLock poisoned");
|
metric.data.write().expect("Data RwLock poisoned").push(v);
|
||||||
sources[j]
|
if let Err(e) = store.put_value(metric.id, v) {
|
||||||
.data
|
warn!(target:"background-worker", "Could not put sample for source #{} in db: {:?}", s_id, e);
|
||||||
.write()
|
}
|
||||||
.expect("Source data RwLock poisoned")
|
}
|
||||||
.push(v);
|
Err(e) => {
|
||||||
let mut last_update = sources[j]
|
warn!(target:"background-worker", "[{}] Could not extract value from result: {:?}", metric.name, e); // TODO: more info!
|
||||||
.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
|
|
||||||
}
|
}
|
||||||
|
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) => {
|
Err(e) => {
|
||||||
warn!(target:"background-worker", "Could not fetch value from {} : {:?}", url, e);
|
warn!(target:"background-worker", "Could not fetch value from {} : {:?}", url, e);
|
||||||
|
|
Loading…
Reference in a new issue