mirror of
https://git.alemi.dev/dashboard.git
synced 2024-11-14 03:49:19 +01:00
feat: allow to move sources, side panel
now sources are all edited in one side panel, which opens in edit mode. Easily move sources across panels. Implementation has a lot of room for improvement (too many loops) but it works for now
This commit is contained in:
parent
83a49f07c5
commit
649b0be848
7 changed files with 102 additions and 110 deletions
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "dashboard"
|
name = "dashboard"
|
||||||
version = "0.1.1"
|
version = "0.1.2"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
|
@ -17,4 +17,4 @@ serde_json = "1"
|
||||||
rusqlite = { version = "0.27" }
|
rusqlite = { version = "0.27" }
|
||||||
jql = { version = "4", default-features = false }
|
jql = { version = "4", default-features = false }
|
||||||
ureq = { version = "2", features = ["json"] }
|
ureq = { version = "2", features = ["json"] }
|
||||||
eframe = { version = "0.18", features = ["persistence"] }
|
eframe = { version = "0.18", features = ["persistence"] }
|
14
README.md
14
README.md
|
@ -1,2 +1,14 @@
|
||||||
# dashboard
|
# dashboard
|
||||||
My custom dashboard, displaying stats and controls
|
A data aggregating dashboard, capable of periodically fetching, parsing, archiving and plotting data.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
This program will work on a database stored in `$HOME/.local/share/dashboard.db`. By default, nothing will be shown.
|
||||||
|
Start editing your dashboard by toggling edit mode on, and add one or more panels (from top bar).
|
||||||
|
You can now add sources to your panel(s): put an URL pointing to any REST api, dashboard will make a periodic GET request.
|
||||||
|
Specify how to access data with "y" fields. A JQL query will be used to parse the json data. A value to fetch X data can also be given, if not specified, current time will be used as X when inserting values.
|
||||||
|
Done! Edit anything to your pleasure, remember to save after editing to make your changes persist, and leave the dashboard hoarding data.
|
||||||
|
## Install
|
||||||
|
idk, `cargo build --release`
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,6 @@ pub enum FetchError {
|
||||||
JQLError(String),
|
JQLError(String),
|
||||||
RusqliteError(rusqlite::Error),
|
RusqliteError(rusqlite::Error),
|
||||||
ParseFloatError(ParseFloatError),
|
ParseFloatError(ParseFloatError),
|
||||||
NoPanelWithThatIdError,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From::<ureq::Error> for FetchError {
|
impl From::<ureq::Error> for FetchError {
|
||||||
|
@ -42,6 +41,7 @@ pub struct ApplicationState {
|
||||||
pub file_path: PathBuf,
|
pub file_path: PathBuf,
|
||||||
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 storage: Mutex<SQLiteDataStore>,
|
pub storage: Mutex<SQLiteDataStore>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,12 +50,14 @@ impl ApplicationState {
|
||||||
let storage = SQLiteDataStore::new(path.clone()).unwrap();
|
let storage = SQLiteDataStore::new(path.clone()).unwrap();
|
||||||
|
|
||||||
let panels = storage.load_panels().unwrap();
|
let panels = storage.load_panels().unwrap();
|
||||||
|
let sources = storage.load_sources().unwrap();
|
||||||
|
|
||||||
return ApplicationState{
|
return ApplicationState{
|
||||||
run: true,
|
run: true,
|
||||||
file_size: RwLock::new(std::fs::metadata(path.clone()).unwrap().len()),
|
file_size: RwLock::new(std::fs::metadata(path.clone()).unwrap().len()),
|
||||||
file_path: path,
|
file_path: path,
|
||||||
panels: RwLock::new(panels),
|
panels: RwLock::new(panels),
|
||||||
|
sources: RwLock::new(sources),
|
||||||
storage: Mutex::new(storage),
|
storage: Mutex::new(storage),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -69,14 +71,8 @@ impl ApplicationState {
|
||||||
pub fn add_source(&self, panel_id:i32, name:&str, url:&str, query_x:&str, query_y:&str, color:Color32, visible:bool) -> Result<(), FetchError> {
|
pub fn add_source(&self, panel_id:i32, name:&str, url:&str, query_x:&str, query_y:&str, color:Color32, visible:bool) -> Result<(), FetchError> {
|
||||||
let source = self.storage.lock().expect("Storage Mutex poisoned")
|
let source = self.storage.lock().expect("Storage Mutex poisoned")
|
||||||
.new_source(panel_id, name, url, query_x, query_y, color, visible)?;
|
.new_source(panel_id, name, url, query_x, query_y, color, visible)?;
|
||||||
let panels = self.panels.read().expect("Panels RwLock poisoned");
|
self.sources.write().expect("Sources RwLock poisoned").push(source);
|
||||||
for panel in &*panels {
|
return Ok(());
|
||||||
if panel.id == panel_id {
|
|
||||||
panel.sources.write().expect("Sources RwLock poisoned").push(source);
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(FetchError::NoPanelWithThatIdError)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,8 +84,6 @@ pub struct Panel {
|
||||||
pub timeserie: bool,
|
pub timeserie: bool,
|
||||||
pub(crate) width: i32,
|
pub(crate) width: i32,
|
||||||
pub(crate) height: i32,
|
pub(crate) height: i32,
|
||||||
pub(crate) sources: RwLock<Vec<Source>>,
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Panel {
|
impl Panel {
|
||||||
|
@ -107,7 +101,7 @@ pub struct Source {
|
||||||
// pub(crate) compiled_query_x: Arc<Mutex<jq_rs::JqProgram>>,
|
// pub(crate) compiled_query_x: Arc<Mutex<jq_rs::JqProgram>>,
|
||||||
pub query_y: String,
|
pub query_y: String,
|
||||||
// pub(crate) compiled_query_y: Arc<Mutex<jq_rs::JqProgram>>,
|
// pub(crate) compiled_query_y: Arc<Mutex<jq_rs::JqProgram>>,
|
||||||
// pub(crate) panel_id: i32,
|
pub(crate) panel_id: i32,
|
||||||
pub(crate) data: RwLock<Vec<Value>>,
|
pub(crate) data: RwLock<Vec<Value>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -48,7 +48,6 @@ impl SQLiteDataStore {
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"CREATE TABLE IF NOT EXISTS points (
|
"CREATE TABLE IF NOT EXISTS points (
|
||||||
id INTEGER PRIMARY KEY,
|
id INTEGER PRIMARY KEY,
|
||||||
panel_id INT NOT NULL,
|
|
||||||
source_id INT NOT NULL,
|
source_id INT NOT NULL,
|
||||||
x FLOAT NOT NULL,
|
x FLOAT NOT NULL,
|
||||||
y FLOAT NOT NULL
|
y FLOAT NOT NULL
|
||||||
|
@ -61,12 +60,12 @@ impl SQLiteDataStore {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
pub fn load_values(&self, panel_id: i32, source_id: i32) -> rusqlite::Result<Vec<Value>> {
|
pub fn load_values(&self, source_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 panel_id = ? AND source_id = ?")?;
|
.prepare("SELECT x, y FROM points WHERE source_id = ?")?;
|
||||||
let values_iter = statement.query_map(params![panel_id, source_id], |row| {
|
let values_iter = statement.query_map(params![source_id], |row| {
|
||||||
Ok(Value {
|
Ok(Value {
|
||||||
x: row.get(0)?,
|
x: row.get(0)?,
|
||||||
y: row.get(1)?,
|
y: row.get(1)?,
|
||||||
|
@ -82,21 +81,21 @@ impl SQLiteDataStore {
|
||||||
Ok(values)
|
Ok(values)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn put_value(&self, panel_id: i32, source_id: i32, v: Value) -> rusqlite::Result<usize> {
|
pub fn put_value(&self, source_id: i32, v: Value) -> rusqlite::Result<usize> {
|
||||||
self.conn.execute(
|
self.conn.execute(
|
||||||
"INSERT INTO points(panel_id, source_id, x, y) VALUES (?, ?, ?, ?)",
|
"INSERT INTO points(source_id, x, y) VALUES (?, ?, ?)",
|
||||||
params![panel_id, source_id, v.x, v.y],
|
params![source_id, v.x, v.y],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
pub fn load_sources(&self, panel_id: i32) -> rusqlite::Result<Vec<Source>> {
|
pub fn load_sources(&self) -> rusqlite::Result<Vec<Source>> {
|
||||||
let mut sources: Vec<Source> = Vec::new();
|
let mut sources: Vec<Source> = Vec::new();
|
||||||
let mut statement = self
|
let mut statement = self
|
||||||
.conn
|
.conn
|
||||||
.prepare("SELECT * FROM sources WHERE panel_id = ?")?;
|
.prepare("SELECT * FROM sources")?;
|
||||||
let sources_iter = statement.query_map(params![panel_id], |row| {
|
let sources_iter = statement.query_map([], |row| {
|
||||||
Ok(Source {
|
Ok(Source {
|
||||||
id: row.get(0)?,
|
id: row.get(0)?,
|
||||||
name: row.get(1)?,
|
name: row.get(1)?,
|
||||||
|
@ -107,7 +106,7 @@ impl SQLiteDataStore {
|
||||||
// compiled_query_x: Arc::new(Mutex::new(jq_rs::compile(row.get::<usize, String>(4)?.as_str()).unwrap())),
|
// compiled_query_x: Arc::new(Mutex::new(jq_rs::compile(row.get::<usize, String>(4)?.as_str()).unwrap())),
|
||||||
query_y: row.get(5)?,
|
query_y: row.get(5)?,
|
||||||
// compiled_query_y: Arc::new(Mutex::new(jq_rs::compile(row.get::<usize, String>(5)?.as_str()).unwrap())),
|
// compiled_query_y: Arc::new(Mutex::new(jq_rs::compile(row.get::<usize, String>(5)?.as_str()).unwrap())),
|
||||||
// panel_id: row.get(6)?,
|
panel_id: row.get(6)?,
|
||||||
color: unpack_color(row.get(7).unwrap_or(0)),
|
color: unpack_color(row.get(7).unwrap_or(0)),
|
||||||
visible: row.get(8)?,
|
visible: row.get(8)?,
|
||||||
data: RwLock::new(Vec::new()),
|
data: RwLock::new(Vec::new()),
|
||||||
|
@ -116,7 +115,7 @@ impl SQLiteDataStore {
|
||||||
|
|
||||||
for source in sources_iter {
|
for source in sources_iter {
|
||||||
if let Ok(mut s) = source {
|
if let Ok(mut s) = source {
|
||||||
s.data = RwLock::new(self.load_values(panel_id, s.id)?);
|
s.data = RwLock::new(self.load_values(s.id)?);
|
||||||
sources.push(s);
|
sources.push(s);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -154,7 +153,7 @@ impl SQLiteDataStore {
|
||||||
// compiled_query_x: Arc::new(Mutex::new(jq_rs::compile(row.get::<usize, String>(4)?.as_str()).unwrap())),
|
// compiled_query_x: Arc::new(Mutex::new(jq_rs::compile(row.get::<usize, String>(4)?.as_str()).unwrap())),
|
||||||
query_y: row.get(5)?,
|
query_y: row.get(5)?,
|
||||||
// compiled_query_y: Arc::new(Mutex::new(jq_rs::compile(row.get::<usize, String>(5)?.as_str()).unwrap())),
|
// compiled_query_y: Arc::new(Mutex::new(jq_rs::compile(row.get::<usize, String>(5)?.as_str()).unwrap())),
|
||||||
// panel_id: row.get(6)?,
|
panel_id: row.get(6)?,
|
||||||
color: unpack_color(row.get(7).unwrap_or(0)),
|
color: unpack_color(row.get(7).unwrap_or(0)),
|
||||||
visible: row.get(8)?,
|
visible: row.get(8)?,
|
||||||
data: RwLock::new(Vec::new()),
|
data: RwLock::new(Vec::new()),
|
||||||
|
@ -171,6 +170,7 @@ impl SQLiteDataStore {
|
||||||
pub fn update_source(
|
pub fn update_source(
|
||||||
&self,
|
&self,
|
||||||
source_id: i32,
|
source_id: i32,
|
||||||
|
panel_id: i32,
|
||||||
name: &str,
|
name: &str,
|
||||||
url: &str,
|
url: &str,
|
||||||
interval: i32,
|
interval: i32,
|
||||||
|
@ -181,8 +181,8 @@ impl SQLiteDataStore {
|
||||||
) -> rusqlite::Result<usize> {
|
) -> rusqlite::Result<usize> {
|
||||||
let color_u32 : Option<u32> = if color == Color32::TRANSPARENT { None } else { Some(repack_color(color)) };
|
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 = ?, url = ?, interval = ?, query_x = ?, query_y = ?, color = ?, visible = ? WHERE id = ?",
|
"UPDATE sources SET name = ?, url = ?, interval = ?, query_x = ?, query_y = ?, panel_id = ?, color = ?, visible = ? WHERE id = ?",
|
||||||
params![name, url, interval, query_x, query_y, color_u32, visible, source_id],
|
params![name, url, interval, query_x, query_y, panel_id, color_u32, visible, source_id],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -202,13 +202,11 @@ impl SQLiteDataStore {
|
||||||
timeserie: row.get(4)?,
|
timeserie: row.get(4)?,
|
||||||
width: row.get(5)?,
|
width: row.get(5)?,
|
||||||
height: row.get(6)?,
|
height: row.get(6)?,
|
||||||
sources: RwLock::new(Vec::new()),
|
|
||||||
})
|
})
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
for panel in panels_iter {
|
for panel in panels_iter {
|
||||||
if let Ok(mut p) = panel {
|
if let Ok(p) = panel {
|
||||||
p.sources = RwLock::new(self.load_sources(p.id)?);
|
|
||||||
panels.push(p);
|
panels.push(p);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -232,7 +230,6 @@ impl SQLiteDataStore {
|
||||||
timeserie: row.get(4)?,
|
timeserie: row.get(4)?,
|
||||||
width: row.get(5)?,
|
width: row.get(5)?,
|
||||||
height: row.get(6)?,
|
height: row.get(6)?,
|
||||||
sources: RwLock::new(Vec::new()),
|
|
||||||
})
|
})
|
||||||
})? {
|
})? {
|
||||||
if let Ok(p) = panel {
|
if let Ok(p) = panel {
|
||||||
|
|
|
@ -126,29 +126,59 @@ impl eframe::App for App {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
if self.edit {
|
||||||
|
egui::SidePanel::left("sources-bar").show(ctx, |ui| {
|
||||||
|
let mut sources = self.data.sources.write().expect("Sources RwLock poisoned");
|
||||||
|
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||||
|
for source in &mut *sources {
|
||||||
|
ui.group(|ui| {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.checkbox(&mut source.visible, "");
|
||||||
|
eframe::egui::TextEdit::singleline(&mut source.name).hint_text("name").desired_width(80.0).show(ui);
|
||||||
|
eframe::egui::TextEdit::singleline(&mut source.url).hint_text("url").desired_width(300.0).show(ui);
|
||||||
|
});
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.add(egui::Slider::new(&mut source.interval, 1..=60));
|
||||||
|
eframe::egui::TextEdit::singleline(&mut source.query_x).hint_text("x").desired_width(50.0).show(ui);
|
||||||
|
eframe::egui::TextEdit::singleline(&mut source.query_y).hint_text("y").desired_width(50.0).show(ui);
|
||||||
|
egui::ComboBox::from_id_source(format!("panel-{}", source.id))
|
||||||
|
.selected_text(format!("panel [{}]", source.panel_id))
|
||||||
|
.width(70.0)
|
||||||
|
.show_ui(ui, |ui| {
|
||||||
|
let pnls = self.data.panels.read().expect("Panels RwLock poisoned");
|
||||||
|
for p in &*pnls {
|
||||||
|
ui.selectable_value(&mut source.panel_id, p.id, p.name.as_str());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ui.color_edit_button_srgba(&mut source.color);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
egui::CentralPanel::default().show(ctx, |ui| {
|
egui::CentralPanel::default().show(ctx, |ui| {
|
||||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||||
let mut panels = self.data.panels.write().unwrap(); // TODO only lock as write when editing
|
let mut panels = self.data.panels.write().unwrap(); // TODO only lock as write when editing
|
||||||
|
let sources = self.data.sources.read().unwrap(); // TODO only lock as write when editing
|
||||||
for panel in &mut *panels {
|
for panel in &mut *panels {
|
||||||
let mut sources = panel.sources.write().unwrap(); // TODO only lock as write when editing
|
|
||||||
ui.group(|ui| {
|
ui.group(|ui| {
|
||||||
ui.vertical(|ui| {
|
ui.vertical(|ui| {
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.heading(panel.name.as_str());
|
ui.heading(panel.name.as_str());
|
||||||
ui.separator();
|
ui.separator();
|
||||||
for source in &mut *sources {
|
for source in &*sources {
|
||||||
if self.edit {
|
if source.panel_id == panel.id {
|
||||||
ui.checkbox(&mut source.visible, "");
|
if source.visible {
|
||||||
|
ui.label(
|
||||||
|
RichText::new(source.name.as_str())
|
||||||
|
.color(if source.color == Color32::TRANSPARENT { Color32::GRAY } else { source.color })
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
ui.label(RichText::new(source.name.as_str()).color(Color32::BLACK));
|
||||||
|
}
|
||||||
|
ui.separator();
|
||||||
}
|
}
|
||||||
if source.visible {
|
|
||||||
ui.label(
|
|
||||||
RichText::new(source.name.as_str())
|
|
||||||
.color(if source.color == Color32::TRANSPARENT { Color32::GRAY } else { source.color })
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
ui.label(RichText::new(source.name.as_str()).color(Color32::BLACK));
|
|
||||||
}
|
|
||||||
ui.separator();
|
|
||||||
}
|
}
|
||||||
if self.filter {
|
if self.filter {
|
||||||
ui.add(egui::Slider::new(&mut panel.view_size, 1..=1440).text("samples"));
|
ui.add(egui::Slider::new(&mut panel.view_size, 1..=1440).text("samples"));
|
||||||
|
@ -161,23 +191,6 @@ impl eframe::App for App {
|
||||||
ui.separator();
|
ui.separator();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
if self.edit {
|
|
||||||
for source in &mut *sources {
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
ui.add(egui::Slider::new(&mut source.interval, 1..=60));
|
|
||||||
eframe::egui::TextEdit::singleline(&mut source.url).hint_text("url").desired_width(300.0).show(ui);
|
|
||||||
if !panel.timeserie {
|
|
||||||
eframe::egui::TextEdit::singleline(&mut source.query_x).hint_text("x").desired_width(50.0).show(ui);
|
|
||||||
}
|
|
||||||
eframe::egui::TextEdit::singleline(&mut source.query_y).hint_text("y").desired_width(50.0).show(ui);
|
|
||||||
ui.color_edit_button_srgba(&mut source.color);
|
|
||||||
ui.separator();
|
|
||||||
ui.label(source.name.as_str());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
||||||
|
@ -201,8 +214,8 @@ impl eframe::App for App {
|
||||||
}
|
}
|
||||||
|
|
||||||
p.show(ui, |plot_ui| {
|
p.show(ui, |plot_ui| {
|
||||||
for source in &mut *sources {
|
for source in &*sources {
|
||||||
if source.visible {
|
if source.visible && source.panel_id == panel.id {
|
||||||
let line = if self.filter {
|
let line = if self.filter {
|
||||||
Line::new(source.values_filter((Utc::now().timestamp() - (panel.view_size as i64 * 60)) as f64)).name(source.name.as_str())
|
Line::new(source.values_filter((Utc::now().timestamp() - (panel.view_size as i64 * 60)) as f64)).name(source.name.as_str())
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -17,10 +17,11 @@ pub fn native_save(state:Arc<ApplicationState>) {
|
||||||
panel.width,
|
panel.width,
|
||||||
panel.height
|
panel.height
|
||||||
).unwrap();
|
).unwrap();
|
||||||
let sources = panel.sources.read().unwrap();
|
let sources = state.sources.read().unwrap();
|
||||||
for source in &*sources {
|
for source in &*sources {
|
||||||
storage.update_source(
|
storage.update_source(
|
||||||
source.id,
|
source.id,
|
||||||
|
source.panel_id,
|
||||||
source.name.as_str(),
|
source.name.as_str(),
|
||||||
source.url.as_str(),
|
source.url.as_str(),
|
||||||
source.interval,
|
source.interval,
|
||||||
|
@ -54,30 +55,25 @@ impl BackgroundWorker for NativeBackgroundWorker {
|
||||||
}
|
}
|
||||||
last_check = Utc::now().timestamp_millis();
|
last_check = Utc::now().timestamp_millis();
|
||||||
|
|
||||||
let panels = state.panels.read().unwrap();
|
let sources = state.sources.read().unwrap();
|
||||||
for i in 0..panels.len() {
|
for j in 0..sources.len() {
|
||||||
let sources = panels[i].sources.read().unwrap();
|
let s_id = sources[j].id;
|
||||||
let p_id = panels[i].id;
|
if !sources[j].valid() {
|
||||||
for j in 0..sources.len() {
|
let mut last_update = sources[j].last_fetch.write().unwrap();
|
||||||
let s_id = sources[j].id;
|
*last_update = Utc::now();
|
||||||
if !sources[j].valid() {
|
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!
|
||||||
|
let v = fetch(url.as_str(), query_x.as_str(), query_y.as_str()).unwrap();
|
||||||
|
let store = state2.storage.lock().unwrap();
|
||||||
|
store.put_value(s_id, v).unwrap();
|
||||||
|
let sources = state2.sources.read().unwrap();
|
||||||
|
sources[j].data.write().unwrap().push(v);
|
||||||
let mut last_update = sources[j].last_fetch.write().unwrap();
|
let mut last_update = sources[j].last_fetch.write().unwrap();
|
||||||
*last_update = Utc::now();
|
*last_update = Utc::now(); // overwrite it so fetches comply with API slowdowns and get desynched among them
|
||||||
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!
|
|
||||||
let v = fetch(url.as_str(), query_x.as_str(), query_y.as_str()).unwrap();
|
|
||||||
let store = state2.storage.lock().unwrap();
|
|
||||||
store.put_value(p_id, s_id, v).unwrap();
|
|
||||||
let panels = state2.panels.read().unwrap();
|
|
||||||
let sources = panels[i].sources.read().unwrap();
|
|
||||||
sources[j].data.write().unwrap().push(v);
|
|
||||||
let mut last_update = sources[j].last_fetch.write().unwrap();
|
|
||||||
*last_update = Utc::now(); // overwrite it so fetches comply with API slowdowns and get desynched among them
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
20
src/lib.rs
20
src/lib.rs
|
@ -1,20 +0,0 @@
|
||||||
mod app;
|
|
||||||
|
|
||||||
pub use app::App;
|
|
||||||
|
|
||||||
// When compiling for web:
|
|
||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
|
||||||
use eframe::wasm_bindgen::{self, prelude::*};
|
|
||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
|
||||||
#[wasm_bindgen]
|
|
||||||
pub fn start(canvas_id: &str) -> Result<(), eframe::wasm_bindgen::JsValue> {
|
|
||||||
// Make sure panics are logged using `console.error`.
|
|
||||||
console_error_panic_hook::set_once();
|
|
||||||
|
|
||||||
// Redirect tracing to console.log and friends:
|
|
||||||
tracing_wasm::set_as_global_default();
|
|
||||||
|
|
||||||
eframe::start_web(canvas_id, Box::new(|cc| Box::new(App::new(cc))))
|
|
||||||
}
|
|
Loading…
Reference in a new issue