mirror of
https://git.alemi.dev/dashboard.git
synced 2024-11-22 23:44:55 +01:00
feat: improved logs, added log panels, added up btn
improved a lot logging, using tracing from tokio. Now there's a subscriber which tracks log messages and makes them available inside the state, so that we can show them in the gui. Made a jank logs side panel. Made panels reorderable in a kinda weird but meh way
This commit is contained in:
parent
0a0ee7077d
commit
f4cca35816
8 changed files with 180 additions and 56 deletions
|
@ -17,6 +17,8 @@ rand = "0.8"
|
||||||
dirs = "4"
|
dirs = "4"
|
||||||
git-version = "0.3.5"
|
git-version = "0.3.5"
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
|
tracing = "0.1" # egui / eframe use tracing
|
||||||
|
tracing-subscriber = "0.3"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
rusqlite = "0.27"
|
rusqlite = "0.27"
|
||||||
|
|
|
@ -41,27 +41,36 @@ pub struct ApplicationState {
|
||||||
pub panels: RwLock<Vec<Panel>>,
|
pub panels: RwLock<Vec<Panel>>,
|
||||||
pub sources: RwLock<Vec<Source>>,
|
pub sources: RwLock<Vec<Source>>,
|
||||||
pub storage: Mutex<SQLiteDataStore>,
|
pub storage: Mutex<SQLiteDataStore>,
|
||||||
|
pub diagnostics: RwLock<Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ApplicationState {
|
impl ApplicationState {
|
||||||
pub fn new(path:PathBuf) -> Self {
|
pub fn new(path:PathBuf) -> Result<ApplicationState, FetchError> {
|
||||||
let storage = SQLiteDataStore::new(path.clone()).unwrap();
|
let storage = SQLiteDataStore::new(path.clone())?;
|
||||||
|
|
||||||
let panels = storage.load_panels().unwrap();
|
let panels = storage.load_panels()?;
|
||||||
let sources = storage.load_sources().unwrap();
|
let sources = storage.load_sources()?;
|
||||||
|
|
||||||
return ApplicationState{
|
return Ok(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())?.len()),
|
||||||
file_path: path,
|
file_path: path,
|
||||||
panels: RwLock::new(panels),
|
panels: RwLock::new(panels),
|
||||||
sources: RwLock::new(sources),
|
sources: RwLock::new(sources),
|
||||||
storage: Mutex::new(storage),
|
storage: Mutex::new(storage),
|
||||||
};
|
diagnostics: RwLock::new(Vec::new()),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_panel(&self, name:&str) -> Result<(), FetchError> {
|
pub fn add_panel(&self, name:&str) -> Result<(), FetchError> {
|
||||||
let panel = self.storage.lock().expect("Storage Mutex poisoned").new_panel(name, 100, 200, 280)?; // TODO make values customizable and useful
|
let panel = self.storage.lock().expect("Storage Mutex poisoned")
|
||||||
|
.new_panel(
|
||||||
|
name,
|
||||||
|
100,
|
||||||
|
200,
|
||||||
|
280,
|
||||||
|
self.panels.read().expect("Panels RwLock poisoned").len() as i32 // todo can this be made more compact and without acquisition?
|
||||||
|
)?; // TODO make values customizable and useful
|
||||||
self.panels.write().expect("Panels RwLock poisoned").push(panel);
|
self.panels.write().expect("Panels RwLock poisoned").push(panel);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,10 +52,10 @@ pub fn fetch(url:&str, query_x:&str, query_y:&str) -> Result<Value, FetchError>
|
||||||
let res = ureq::get(url).call()?.into_json()?;
|
let res = ureq::get(url).call()?.into_json()?;
|
||||||
let x : f64;
|
let x : f64;
|
||||||
if query_x.len() > 0 {
|
if query_x.len() > 0 {
|
||||||
x = jql::walker(&res, query_x)?.as_f64().unwrap(); // TODO what if it's given to us as a string?
|
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 {
|
} else {
|
||||||
x = Utc::now().timestamp() as f64;
|
x = Utc::now().timestamp() as f64;
|
||||||
}
|
}
|
||||||
let y = jql::walker(&res, query_y)?.as_f64().unwrap();
|
let y = jql::walker(&res, query_y)?.as_f64().ok_or(FetchError::JQLError("Y query is null".to_string()))?;
|
||||||
return Ok( Value { x, y } );
|
return Ok( Value { x, y } );
|
||||||
}
|
}
|
|
@ -26,7 +26,8 @@ impl SQLiteDataStore {
|
||||||
timeserie BOOL NOT NULL,
|
timeserie BOOL NOT NULL,
|
||||||
width INT NOT NULL,
|
width INT NOT NULL,
|
||||||
height INT NOT NULL,
|
height INT NOT NULL,
|
||||||
limit_view BOOL NOT NULL
|
limit_view BOOL NOT NULL,
|
||||||
|
position INT NOT NULL
|
||||||
);",
|
);",
|
||||||
[],
|
[],
|
||||||
)?;
|
)?;
|
||||||
|
@ -104,9 +105,7 @@ impl SQLiteDataStore {
|
||||||
interval: row.get(3)?,
|
interval: row.get(3)?,
|
||||||
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(4)?,
|
query_x: row.get(4)?,
|
||||||
// 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())),
|
|
||||||
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)?,
|
||||||
|
@ -151,9 +150,7 @@ impl SQLiteDataStore {
|
||||||
interval: row.get(3)?,
|
interval: row.get(3)?,
|
||||||
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(4)?,
|
query_x: row.get(4)?,
|
||||||
// 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())),
|
|
||||||
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)?,
|
||||||
|
@ -193,7 +190,7 @@ impl SQLiteDataStore {
|
||||||
|
|
||||||
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.conn.prepare("SELECT * FROM panels")?;
|
let mut statement = self.conn.prepare("SELECT * FROM panels ORDER BY position")?;
|
||||||
let panels_iter = statement.query_map([], |row| {
|
let panels_iter = statement.query_map([], |row| {
|
||||||
Ok(Panel {
|
Ok(Panel {
|
||||||
id: row.get(0)?,
|
id: row.get(0)?,
|
||||||
|
@ -217,10 +214,10 @@ impl SQLiteDataStore {
|
||||||
}
|
}
|
||||||
|
|
||||||
// jank! TODO make it not jank!
|
// jank! TODO make it not jank!
|
||||||
pub fn new_panel(&self, name: &str, view_size:i32, width: i32, height: i32) -> rusqlite::Result<Panel> {
|
pub fn new_panel(&self, name: &str, view_size:i32, width: i32, height: i32, position: i32) -> rusqlite::Result<Panel> {
|
||||||
self.conn.execute(
|
self.conn.execute(
|
||||||
"INSERT INTO panels (name, view_scroll, view_size, timeserie, width, height, limit_view) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
"INSERT INTO panels (name, view_scroll, view_size, timeserie, width, height, limit_view, position) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||||
params![name, true, view_size, true, width, height, false]
|
params![name, true, view_size, true, width, height, false, position]
|
||||||
)?;
|
)?;
|
||||||
let mut statement = self.conn.prepare("SELECT * FROM panels WHERE name = ?")?;
|
let mut statement = self.conn.prepare("SELECT * FROM panels WHERE name = ?")?;
|
||||||
for panel in statement.query_map(params![name], |row| {
|
for panel in statement.query_map(params![name], |row| {
|
||||||
|
@ -252,10 +249,11 @@ impl SQLiteDataStore {
|
||||||
width: i32,
|
width: i32,
|
||||||
height: i32,
|
height: i32,
|
||||||
limit: bool,
|
limit: bool,
|
||||||
|
position: i32,
|
||||||
) -> rusqlite::Result<usize> {
|
) -> rusqlite::Result<usize> {
|
||||||
self.conn.execute(
|
self.conn.execute(
|
||||||
"UPDATE panels SET name = ?, view_scroll = ?, view_size = ?, timeserie = ?, width = ?, height = ?, limit_view = ? WHERE id = ?",
|
"UPDATE panels SET name = ?, view_scroll = ?, view_size = ?, timeserie = ?, width = ?, height = ?, limit_view = ?, position = ? WHERE id = ?",
|
||||||
params![name, view_scroll, view_size, timeserie, width, height, limit, id],
|
params![name, view_scroll, view_size, timeserie, width, height, limit, position, id],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ pub mod util;
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use chrono::{Utc, Local};
|
use chrono::{Utc, Local};
|
||||||
|
use tracing::error;
|
||||||
use eframe::egui;
|
use eframe::egui;
|
||||||
use eframe::egui::plot::GridMark;
|
use eframe::egui::plot::GridMark;
|
||||||
use eframe::egui::{RichText, plot::{Line, Plot}, Color32};
|
use eframe::egui::{RichText, plot::{Line, Plot}, Color32};
|
||||||
|
@ -44,11 +45,12 @@ pub struct App {
|
||||||
data: Arc<ApplicationState>,
|
data: Arc<ApplicationState>,
|
||||||
input: InputBuffer,
|
input: InputBuffer,
|
||||||
edit: bool,
|
edit: bool,
|
||||||
|
show_log: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
pub fn new(_cc: &eframe::CreationContext, data: Arc<ApplicationState>) -> Self {
|
pub fn new(_cc: &eframe::CreationContext, data: Arc<ApplicationState>) -> Self {
|
||||||
Self { data, input: InputBuffer::default(), edit: false }
|
Self { data, input: InputBuffer::default(), edit: false, show_log: false }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,7 +71,9 @@ impl eframe::App for App {
|
||||||
ui.label("+ panel");
|
ui.label("+ panel");
|
||||||
eframe::egui::TextEdit::singleline(&mut self.input.panel_name).hint_text("name").desired_width(50.0).show(ui);
|
eframe::egui::TextEdit::singleline(&mut self.input.panel_name).hint_text("name").desired_width(50.0).show(ui);
|
||||||
if ui.button("add").clicked() {
|
if ui.button("add").clicked() {
|
||||||
self.data.add_panel(self.input.panel_name.as_str()).unwrap();
|
if let Err(e) = self.data.add_panel(self.input.panel_name.as_str()) {
|
||||||
|
error!(target: "ui", "Failed to add panel: {:?}", e);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
ui.separator();
|
ui.separator();
|
||||||
ui.label("+ source");
|
ui.label("+ source");
|
||||||
|
@ -81,7 +85,7 @@ impl eframe::App for App {
|
||||||
.selected_text(format!("panel [{}]", self.input.panel_id))
|
.selected_text(format!("panel [{}]", self.input.panel_id))
|
||||||
.width(70.0)
|
.width(70.0)
|
||||||
.show_ui(ui, |ui| {
|
.show_ui(ui, |ui| {
|
||||||
let pnls = self.data.panels.write().unwrap();
|
let pnls = self.data.panels.write().expect("Panels RwLock poisoned");
|
||||||
for p in &*pnls {
|
for p in &*pnls {
|
||||||
ui.selectable_value(&mut self.input.panel_id, p.id, p.name.as_str());
|
ui.selectable_value(&mut self.input.panel_id, p.id, p.name.as_str());
|
||||||
}
|
}
|
||||||
|
@ -91,7 +95,7 @@ impl eframe::App for App {
|
||||||
ui.add(egui::Slider::new(&mut self.input.interval, 1..=60));
|
ui.add(egui::Slider::new(&mut self.input.interval, 1..=60));
|
||||||
ui.color_edit_button_srgba(&mut self.input.color);
|
ui.color_edit_button_srgba(&mut self.input.color);
|
||||||
if ui.button("add").clicked() {
|
if ui.button("add").clicked() {
|
||||||
self.data.add_source(
|
if let Err(e) = self.data.add_source(
|
||||||
self.input.panel_id,
|
self.input.panel_id,
|
||||||
self.input.name.as_str(),
|
self.input.name.as_str(),
|
||||||
self.input.url.as_str(),
|
self.input.url.as_str(),
|
||||||
|
@ -99,22 +103,32 @@ impl eframe::App for App {
|
||||||
self.input.query_y.as_str(),
|
self.input.query_y.as_str(),
|
||||||
self.input.color,
|
self.input.color,
|
||||||
self.input.visible,
|
self.input.visible,
|
||||||
).unwrap();
|
) {
|
||||||
|
error!(target: "ui", "Error adding souce : {:?}", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
ui.separator();
|
ui.separator();
|
||||||
}
|
}
|
||||||
ui.with_layout(egui::Layout::top_down(egui::Align::RIGHT), |ui| {
|
ui.with_layout(egui::Layout::top_down(egui::Align::RIGHT), |ui| {
|
||||||
if ui.button("×").clicked() {
|
ui.horizontal(|ui| {
|
||||||
|
if ui.small_button("×").clicked() {
|
||||||
frame.quit();
|
frame.quit();
|
||||||
}
|
}
|
||||||
|
ui.checkbox(&mut self.show_log, "log");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
egui::TopBottomPanel::bottom("footer").show(ctx, |ui| {
|
egui::TopBottomPanel::bottom("footer").show(ctx, |ui| {
|
||||||
ui.horizontal(|ui|{
|
ui.horizontal(|ui|{
|
||||||
ui.label(self.data.file_path.to_str().unwrap());
|
ui.label(self.data.file_path.to_str().unwrap()); // TODO maybe calculate it just once?
|
||||||
ui.separator();
|
ui.separator();
|
||||||
ui.label(human_size(*self.data.file_size.read().unwrap()));
|
ui.label(human_size(*self.data.file_size.read().expect("Filesize RwLock poisoned")));
|
||||||
|
let diags = self.data.diagnostics.read().expect("Diagnostics RwLock poisoned");
|
||||||
|
if diags.len() > 0 {
|
||||||
|
ui.separator();
|
||||||
|
ui.label(diags.last().unwrap_or(&"".to_string()));
|
||||||
|
}
|
||||||
ui.with_layout(egui::Layout::top_down(egui::Align::RIGHT), |ui| {
|
ui.with_layout(egui::Layout::top_down(egui::Align::RIGHT), |ui| {
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.label(format!("v{}-{}", env!("CARGO_PKG_VERSION"), git_version::git_version!()));
|
ui.label(format!("v{}-{}", env!("CARGO_PKG_VERSION"), git_version::git_version!()));
|
||||||
|
@ -160,16 +174,37 @@ impl eframe::App for App {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if self.show_log {
|
||||||
|
egui::SidePanel::right("logs-panel").show(ctx, |ui| {
|
||||||
|
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||||
|
ui.heading("logs");
|
||||||
|
ui.separator();
|
||||||
|
let msgs = self.data.diagnostics.read().expect("Diagnostics RwLock poisoned");
|
||||||
|
ui.group(|ui| {
|
||||||
|
for msg in msgs.iter() {
|
||||||
|
ui.label(msg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let mut to_swap : Vec<usize> = Vec::new();
|
||||||
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().expect("Panels RwLock poisoned"); // TODO only lock as write when editing
|
||||||
let sources = self.data.sources.read().unwrap(); // TODO only lock as write when editing
|
let sources = self.data.sources.read().expect("Sources RwLock poisoned"); // TODO only lock as write when editing
|
||||||
for panel in &mut *panels {
|
for (index, panel) in panels.iter_mut().enumerate() {
|
||||||
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();
|
||||||
|
if self.edit && index > 0 {
|
||||||
|
if ui.small_button("up").clicked() {
|
||||||
|
to_swap.push(index); // TODO kinda jank but is there a better way?
|
||||||
|
}
|
||||||
|
ui.separator();
|
||||||
|
}
|
||||||
for source in &*sources {
|
for source in &*sources {
|
||||||
if source.panel_id == panel.id {
|
if source.panel_id == panel.id {
|
||||||
if source.visible {
|
if source.visible {
|
||||||
|
@ -249,5 +284,11 @@ impl eframe::App for App {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
if !to_swap.is_empty() { // TODO can this be done in background? idk
|
||||||
|
let mut panels = self.data.panels.write().expect("Panels RwLock poisoned");
|
||||||
|
for index in to_swap {
|
||||||
|
panels.swap(index-1, index);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
// if you're handling more than terabytes of data, it's the future and you ought to update this code!
|
use std::sync::Arc;
|
||||||
use chrono::{DateTime, NaiveDateTime, Utc, Local};
|
use chrono::{DateTime, NaiveDateTime, Utc, Local};
|
||||||
|
use tracing_subscriber::Layer;
|
||||||
use eframe::egui::Color32;
|
use eframe::egui::Color32;
|
||||||
|
|
||||||
|
use super::data::ApplicationState;
|
||||||
|
|
||||||
|
// if you're handling more than terabytes of data, it's the future and you ought to update this code!
|
||||||
const PREFIXES: &'static [&'static str] = &["", "k", "M", "G", "T"];
|
const PREFIXES: &'static [&'static str] = &["", "k", "M", "G", "T"];
|
||||||
|
|
||||||
pub fn human_size(size: u64) -> String {
|
pub fn human_size(size: u64) -> String {
|
||||||
|
@ -49,3 +53,44 @@ pub fn repack_color(c: Color32) -> u32 {
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct InternalLogger {
|
||||||
|
state: Arc<ApplicationState>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InternalLogger {
|
||||||
|
pub fn new(state: Arc<ApplicationState>) -> Self {
|
||||||
|
InternalLogger { state }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S> Layer<S> for InternalLogger where S: tracing::Subscriber {
|
||||||
|
fn on_event(
|
||||||
|
&self,
|
||||||
|
event: &tracing::Event<'_>,
|
||||||
|
_ctx: tracing_subscriber::layer::Context<'_, S>,
|
||||||
|
) {
|
||||||
|
let mut msg_visitor = LogMessageVisitor { msg: "".to_string() };
|
||||||
|
event.record(&mut msg_visitor);
|
||||||
|
let out = format!("{} [{}] {}: {}", Local::now().format("%H:%M:%S"), event.metadata().level(), event.metadata().target(), msg_visitor.msg);
|
||||||
|
self.state.diagnostics.write().expect("Diagnostics RwLock poisoned").push(out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LogMessageVisitor {
|
||||||
|
msg : String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl tracing::field::Visit for LogMessageVisitor {
|
||||||
|
fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
|
||||||
|
if field.name() == "message" {
|
||||||
|
self.msg = format!("{}: '{:?}' ", field.name(), &value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
|
||||||
|
if field.name() == "message" {
|
||||||
|
self.msg = value.to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,14 +1,15 @@
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use tracing::warn;
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use eframe::egui::Context;
|
use eframe::egui::Context;
|
||||||
use crate::app::data::{ApplicationState, source::fetch};
|
use crate::app::data::{ApplicationState, source::fetch};
|
||||||
|
|
||||||
pub fn native_save(state:Arc<ApplicationState>) {
|
pub fn native_save(state:Arc<ApplicationState>) {
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
let storage = state.storage.lock().unwrap();
|
let storage = state.storage.lock().expect("Storage Mutex poisoned");
|
||||||
let panels = state.panels.read().unwrap();
|
let panels = state.panels.read().expect("Panels RwLock poisoned");
|
||||||
for panel in &*panels {
|
for (index, panel) in panels.iter().enumerate() {
|
||||||
storage.update_panel(
|
if let Err(e) = storage.update_panel(
|
||||||
panel.id,
|
panel.id,
|
||||||
panel.name.as_str(),
|
panel.name.as_str(),
|
||||||
panel.view_scroll,
|
panel.view_scroll,
|
||||||
|
@ -17,10 +18,13 @@ pub fn native_save(state:Arc<ApplicationState>) {
|
||||||
panel.width,
|
panel.width,
|
||||||
panel.height,
|
panel.height,
|
||||||
panel.limit,
|
panel.limit,
|
||||||
).unwrap();
|
index as i32,
|
||||||
let sources = state.sources.read().unwrap();
|
) {
|
||||||
|
warn!(target: "native-save", "Could not update panel #{} : {:?}", panel.id, e);
|
||||||
|
}
|
||||||
|
let sources = state.sources.read().expect("Sources RwLock poisoned");
|
||||||
for source in &*sources {
|
for source in &*sources {
|
||||||
storage.update_source(
|
if let Err(e) = storage.update_source(
|
||||||
source.id,
|
source.id,
|
||||||
source.panel_id,
|
source.panel_id,
|
||||||
source.name.as_str(),
|
source.name.as_str(),
|
||||||
|
@ -30,7 +34,9 @@ pub fn native_save(state:Arc<ApplicationState>) {
|
||||||
source.query_y.as_str(),
|
source.query_y.as_str(),
|
||||||
source.color,
|
source.color,
|
||||||
source.visible,
|
source.visible,
|
||||||
).unwrap();
|
) {
|
||||||
|
warn!(target: "native-save", "Could not update source #{} : {:?}", source.id, e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -56,30 +62,41 @@ impl BackgroundWorker for NativeBackgroundWorker {
|
||||||
}
|
}
|
||||||
last_check = Utc::now().timestamp_millis();
|
last_check = Utc::now().timestamp_millis();
|
||||||
|
|
||||||
let sources = state.sources.read().unwrap();
|
let sources = state.sources.read().expect("Sources RwLock poisoned");
|
||||||
for j in 0..sources.len() {
|
for j in 0..sources.len() {
|
||||||
let s_id = sources[j].id;
|
let s_id = sources[j].id;
|
||||||
if !sources[j].valid() {
|
if !sources[j].valid() {
|
||||||
let mut last_update = sources[j].last_fetch.write().unwrap();
|
let mut last_update = sources[j].last_fetch.write().expect("Sources RwLock poisoned");
|
||||||
*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_x = sources[j].query_x.clone();
|
||||||
let query_y = sources[j].query_y.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!
|
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();
|
match fetch(url.as_str(), query_x.as_str(), query_y.as_str()) {
|
||||||
let store = state2.storage.lock().unwrap();
|
Ok(v) => {
|
||||||
store.put_value(s_id, v).unwrap();
|
let store = state2.storage.lock().expect("Storage mutex poisoned");
|
||||||
let sources = state2.sources.read().unwrap();
|
if let Err(e) = store.put_value(s_id, v) {
|
||||||
sources[j].data.write().unwrap().push(v);
|
warn!(target:"background-worker", "Could not put sample for source #{} in db: {:?}", s_id, e);
|
||||||
let mut last_update = sources[j].last_fetch.write().unwrap();
|
} 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
|
*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);
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Ok(meta) = std::fs::metadata(state.file_path.clone()) {
|
||||||
let mut fsize = state.file_size.write().expect("File Size RwLock poisoned");
|
let mut fsize = state.file_size.write().expect("File Size RwLock poisoned");
|
||||||
*fsize = std::fs::metadata(state.file_path.clone()).unwrap().len();
|
*fsize = meta.len();
|
||||||
|
} // ignore errors
|
||||||
|
|
||||||
ctx.request_repaint();
|
ctx.request_repaint();
|
||||||
}
|
}
|
||||||
|
|
16
src/main.rs
16
src/main.rs
|
@ -1,17 +1,29 @@
|
||||||
mod app;
|
mod app;
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use crate::app::{App, data::ApplicationState, worker::{BackgroundWorker, NativeBackgroundWorker}};
|
use tracing_subscriber::prelude::*;
|
||||||
|
use crate::app::{App, util::InternalLogger, data::ApplicationState, worker::{BackgroundWorker, NativeBackgroundWorker}};
|
||||||
|
|
||||||
// When compiling natively:
|
// When compiling natively:
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
fn main() -> ! {
|
fn main() -> ! {
|
||||||
|
use tracing::metadata::LevelFilter;
|
||||||
|
|
||||||
|
|
||||||
let native_options = eframe::NativeOptions::default();
|
let native_options = eframe::NativeOptions::default();
|
||||||
|
|
||||||
let mut store_path = dirs::data_dir().unwrap_or(std::path::PathBuf::from(".")); // TODO get cwd more consistently?
|
let mut store_path = dirs::data_dir().unwrap_or(std::path::PathBuf::from(".")); // TODO get cwd more consistently?
|
||||||
store_path.push("dashboard.db");
|
store_path.push("dashboard.db");
|
||||||
|
|
||||||
let store = Arc::new(ApplicationState::new(store_path));
|
let store = Arc::new(
|
||||||
|
ApplicationState::new(store_path).expect("Failed creating application state")
|
||||||
|
);
|
||||||
|
|
||||||
|
tracing_subscriber::registry()
|
||||||
|
.with(LevelFilter::INFO)
|
||||||
|
.with(tracing_subscriber::fmt::Layer::new())
|
||||||
|
.with(InternalLogger::new(store.clone()))
|
||||||
|
.init();
|
||||||
|
|
||||||
eframe::run_native( // TODO replace this with a loop that ends so we can cleanly exit the background worker
|
eframe::run_native( // TODO replace this with a loop that ends so we can cleanly exit the background worker
|
||||||
"dashboard",
|
"dashboard",
|
||||||
|
|
Loading…
Reference in a new issue