style: rustfmt whole project

This commit is contained in:
əlemi 2022-06-14 00:54:46 +02:00
parent c53abb2eed
commit 8df10ec5ed
Signed by: alemi
GPG key ID: A4895B84D311642C
9 changed files with 184 additions and 137 deletions

View file

@ -1,12 +1,11 @@
pub mod source; pub mod source;
pub mod store; pub mod store;
use std::path::PathBuf;
use std::sync::{RwLock, Mutex};
use std::num::ParseFloatError;
use eframe::epaint::Color32;
use self::store::SQLiteDataStore;
use self::source::{Panel, Source}; use self::source::{Panel, Source};
use self::store::SQLiteDataStore;
use std::num::ParseFloatError;
use std::path::PathBuf;
use std::sync::{Mutex, RwLock};
#[derive(Debug)] #[derive(Debug)]
pub enum FetchError { pub enum FetchError {
@ -18,20 +17,31 @@ pub enum FetchError {
ParseFloatError(ParseFloatError), ParseFloatError(ParseFloatError),
} }
impl From::<ureq::Error> for FetchError { impl From<ureq::Error> for FetchError {
fn from(e: ureq::Error) -> Self { FetchError::UreqError(e) } fn from(e: ureq::Error) -> Self {
FetchError::UreqError(e)
} }
impl From::<std::io::Error> for FetchError {
fn from(e: std::io::Error) -> Self { FetchError::IoError(e) }
} }
impl From::<String> for FetchError { // TODO wtf? why does JQL error as a String? impl From<std::io::Error> for FetchError {
fn from(e: String) -> Self { FetchError::JQLError(e) } fn from(e: std::io::Error) -> Self {
FetchError::IoError(e)
} }
impl From::<ParseFloatError> for FetchError {
fn from(e: ParseFloatError) -> Self { FetchError::ParseFloatError(e) }
} }
impl From::<rusqlite::Error> for FetchError { impl From<String> for FetchError {
fn from(e: rusqlite::Error) -> Self { FetchError::RusqliteError(e) } // TODO wtf? why does JQL error as a String?
fn from(e: String) -> Self {
FetchError::JQLError(e)
}
}
impl From<ParseFloatError> for FetchError {
fn from(e: ParseFloatError) -> Self {
FetchError::ParseFloatError(e)
}
}
impl From<rusqlite::Error> for FetchError {
fn from(e: rusqlite::Error) -> Self {
FetchError::RusqliteError(e)
}
} }
pub struct ApplicationState { pub struct ApplicationState {
@ -63,22 +73,42 @@ impl ApplicationState {
} }
pub fn add_panel(&self, panel: &Panel) -> Result<(), FetchError> { pub fn add_panel(&self, panel: &Panel) -> Result<(), FetchError> {
let verified_panel = self.storage.lock().expect("Storage Mutex poisoned") let verified_panel = self
.storage
.lock()
.expect("Storage Mutex poisoned")
.new_panel( .new_panel(
panel.name.as_str(), panel.name.as_str(),
panel.view_size, panel.view_size,
panel.width, panel.width,
panel.height, panel.height,
self.panels.read().expect("Panels RwLock poisoned").len() as i32 // todo can this be made more compact and without acquisition? 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 )?; // TODO make values customizable and useful
self.panels.write().expect("Panels RwLock poisoned").push(verified_panel); self.panels
.write()
.expect("Panels RwLock poisoned")
.push(verified_panel);
Ok(()) Ok(())
} }
pub fn add_source(&self, source: &Source) -> Result<(), FetchError> { pub fn add_source(&self, source: &Source) -> Result<(), FetchError> {
let verified_source = self.storage.lock().expect("Storage Mutex poisoned") let verified_source = self
.new_source(source.panel_id, source.name.as_str(), source.url.as_str(), source.query_x.as_str(), source.query_y.as_str(), source.color, source.visible)?; .storage
self.sources.write().expect("Sources RwLock poisoned").push(verified_source); .lock()
.expect("Storage Mutex poisoned")
.new_source(
source.panel_id,
source.name.as_str(),
source.url.as_str(),
source.query_x.as_str(),
source.query_y.as_str(),
source.color,
source.visible,
)?;
self.sources
.write()
.expect("Sources RwLock poisoned")
.push(verified_source);
return Ok(()); return Ok(());
} }
} }

View file

@ -1,8 +1,8 @@
use std::sync::RwLock;
use chrono::{DateTime, Utc};
use eframe::egui::plot::{Values, Value};
use eframe::epaint::Color32;
use super::FetchError; use super::FetchError;
use chrono::{DateTime, Utc};
use eframe::egui::plot::{Value, Values};
use eframe::epaint::Color32;
use std::sync::RwLock;
pub struct Panel { pub struct Panel {
pub(crate) id: i32, pub(crate) id: i32,
@ -59,7 +59,7 @@ impl Default for Source {
query_x: "".to_string(), query_x: "".to_string(),
query_y: "".to_string(), query_y: "".to_string(),
panel_id: -1, panel_id: -1,
data: RwLock::new(Vec::new()) data: RwLock::new(Vec::new()),
} }
} }
} }
@ -85,10 +85,14 @@ 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().ok_or(FetchError::JQLError("X query is null".to_string()))?; // 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().ok_or(FetchError::JQLError("Y query is null".to_string()))?; 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 });
} }

View file

@ -1,9 +1,12 @@
use crate::app::{data::source::{Panel, Source}, util::repack_color}; use crate::app::util::unpack_color;
use crate::app::{
data::source::{Panel, Source},
util::repack_color,
};
use chrono::{TimeZone, Utc}; use chrono::{TimeZone, Utc};
use eframe::egui::{Color32, plot::Value}; use eframe::egui::{plot::Value, Color32};
use rusqlite::{params, Connection}; use rusqlite::{params, Connection};
use std::sync::RwLock; use std::sync::RwLock;
use crate::app::util::unpack_color;
pub trait DataStorage { pub trait DataStorage {
fn add_panel(&self, name: &str); fn add_panel(&self, name: &str);
@ -60,8 +63,6 @@ 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, 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
@ -90,13 +91,9 @@ impl SQLiteDataStore {
) )
} }
pub fn load_sources(&self) -> 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.prepare("SELECT * FROM sources")?;
.conn
.prepare("SELECT * FROM sources")?;
let sources_iter = statement.query_map([], |row| { let sources_iter = statement.query_map([], |row| {
Ok(Source { Ok(Source {
id: row.get(0)?, id: row.get(0)?,
@ -134,7 +131,11 @@ impl SQLiteDataStore {
color: Color32, color: Color32,
visible: bool, visible: bool,
) -> rusqlite::Result<Source> { ) -> rusqlite::Result<Source> {
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(
"INSERT INTO sources(name, url, interval, query_x, query_y, panel_id, color, visible) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", "INSERT INTO sources(name, url, interval, query_x, query_y, panel_id, color, visible) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
params![name, url, 60i32, query_x, query_y, panel_id, color_u32, visible], params![name, url, 60i32, query_x, query_y, panel_id, color_u32, visible],
@ -177,7 +178,11 @@ impl SQLiteDataStore {
color: Color32, color: Color32,
visible: bool, visible: bool,
) -> 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 = ?, panel_id = ?, 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, panel_id, color_u32, visible, source_id], params![name, url, interval, query_x, query_y, panel_id, color_u32, visible, source_id],
@ -190,7 +195,9 @@ 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 ORDER BY position")?; 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)?,
@ -214,7 +221,14 @@ 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, position: 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, position) 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, position] params![name, true, view_size, true, width, height, false, position]
@ -260,7 +274,4 @@ impl SQLiteDataStore {
// pub fn delete_panel(&self, id:i32) -> rusqlite::Result<usize> { // pub fn delete_panel(&self, id:i32) -> rusqlite::Result<usize> {
// self.conn.execute("DELETE FROM panels WHERE id = ?", params![id]) // self.conn.execute("DELETE FROM panels WHERE id = ?", params![id])
// } // }
} }

View file

@ -1,2 +1,2 @@
pub mod source;
pub mod panel; pub mod panel;
pub mod source;

View file

@ -1,7 +1,13 @@
use chrono::{Utc, Local}; use chrono::{Local, Utc};
use eframe::egui::{Ui, Layout, Align, plot::{Plot, Legend, Corner, Line, GridMark}, Slider, DragValue}; use eframe::egui::{
plot::{Corner, GridMark, Legend, Line, Plot},
DragValue, Layout, Slider, Ui,
};
use crate::app::{data::source::{Panel, Source}, util::timestamp_to_str}; use crate::app::{
data::source::{Panel, Source},
util::timestamp_to_str,
};
pub fn panel_edit_inline_ui(ui: &mut Ui, panel: &mut Panel) { pub fn panel_edit_inline_ui(ui: &mut Ui, panel: &mut Panel) {
eframe::egui::TextEdit::singleline(&mut panel.name) eframe::egui::TextEdit::singleline(&mut panel.name)
@ -27,16 +33,13 @@ pub fn panel_title_ui(ui: &mut Ui, panel: &mut Panel) {
ui.separator(); ui.separator();
ui.checkbox(&mut panel.timeserie, "timeserie"); ui.checkbox(&mut panel.timeserie, "timeserie");
ui.separator(); ui.separator();
ui.add( ui.add(Slider::new(&mut panel.height, 0..=500).text("height"));
Slider::new(&mut panel.height, 0..=500).text("height"),
);
ui.separator(); ui.separator();
}); });
}); });
}); });
} }
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, sources: &Vec<Source>) {
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)
@ -44,26 +47,20 @@ pub fn panel_body_ui(ui: &mut Ui, panel: &mut Panel, sources: &Vec<Source>) {
.legend(Legend::default().position(Corner::LeftTop)); .legend(Legend::default().position(Corner::LeftTop));
if panel.view_scroll { if panel.view_scroll {
p = p p = p.include_x(Utc::now().timestamp() as f64);
.include_x(Utc::now().timestamp() as f64);
if panel.limit { if panel.limit {
p = p p = p
.set_margin_fraction(eframe::emath::Vec2 { x: 0.0, y: 0.1 }) .set_margin_fraction(eframe::emath::Vec2 { x: 0.0, y: 0.1 })
.include_x((Utc::now().timestamp() + (panel.view_size as i64 * 3)) as f64); .include_x((Utc::now().timestamp() + (panel.view_size as i64 * 3)) as f64);
} }
if panel.limit { if panel.limit {
p = p.include_x( p = p.include_x((Utc::now().timestamp() - (panel.view_size as i64 * 60)) as f64);
(Utc::now().timestamp() - (panel.view_size as i64 * 60))
as f64,
);
} }
} }
if panel.timeserie { if panel.timeserie {
p = p p = p
.x_axis_formatter(|x, _range| { .x_axis_formatter(|x, _range| timestamp_to_str(x as i64, true, false))
timestamp_to_str(x as i64, true, false)
})
.label_formatter(|name, value| { .label_formatter(|name, value| {
if !name.is_empty() { if !name.is_empty() {
return format!( return format!(
@ -111,8 +108,7 @@ pub fn panel_body_ui(ui: &mut Ui, panel: &mut Panel, sources: &Vec<Source>) {
if source.visible && source.panel_id == panel.id { if source.visible && source.panel_id == panel.id {
let line = if panel.limit { let line = if panel.limit {
Line::new(source.values_filter( Line::new(source.values_filter(
(Utc::now().timestamp() (Utc::now().timestamp() - (panel.view_size as i64 * 60)) as f64,
- (panel.view_size as i64 * 60)) as f64,
)) ))
.name(source.name.as_str()) .name(source.name.as_str())
} else { } else {

View file

@ -1,7 +1,7 @@
use eframe::egui; use eframe::egui;
use eframe::egui::Ui; use eframe::egui::Ui;
use crate::app::data::source::{Source, Panel}; use crate::app::data::source::{Panel, Source};
pub fn source_edit_inline_ui(ui: &mut Ui, source: &mut Source, panels: &Vec<Panel>) { pub fn source_edit_inline_ui(ui: &mut Ui, source: &mut Source, panels: &Vec<Panel>) {
eframe::egui::TextEdit::singleline(&mut source.name) eframe::egui::TextEdit::singleline(&mut source.name)
@ -25,11 +25,7 @@ pub fn source_edit_inline_ui(ui: &mut Ui, source: &mut Source, panels: &Vec<Pane
.width(70.0) .width(70.0)
.show_ui(ui, |ui| { .show_ui(ui, |ui| {
for p in panels { for p in panels {
ui.selectable_value( ui.selectable_value(&mut source.panel_id, p.id, p.name.as_str());
&mut source.panel_id,
p.id,
p.name.as_str(),
);
} }
}); });
ui.checkbox(&mut source.visible, "visible"); ui.checkbox(&mut source.visible, "visible");
@ -37,7 +33,6 @@ pub fn source_edit_inline_ui(ui: &mut Ui, source: &mut Source, panels: &Vec<Pane
ui.color_edit_button_srgba(&mut source.color); ui.color_edit_button_srgba(&mut source.color);
} }
pub fn source_ui(ui: &mut Ui, source: &mut Source, panels: &Vec<Panel>) { pub fn source_ui(ui: &mut Ui, source: &mut Source, panels: &Vec<Panel>) {
ui.group(|ui| { ui.group(|ui| {
ui.horizontal(|ui| { ui.horizontal(|ui| {
@ -66,11 +61,7 @@ pub fn source_ui(ui: &mut Ui, source: &mut Source, panels: &Vec<Panel>) {
.width(70.0) .width(70.0)
.show_ui(ui, |ui| { .show_ui(ui, |ui| {
for p in panels { for p in panels {
ui.selectable_value( ui.selectable_value(&mut source.panel_id, p.id, p.name.as_str());
&mut source.panel_id,
p.id,
p.name.as_str(),
);
} }
}); });
ui.color_edit_button_srgba(&mut source.color); ui.color_edit_button_srgba(&mut source.color);

View file

@ -3,21 +3,15 @@ pub mod gui;
pub mod util; pub mod util;
pub mod worker; pub mod worker;
use chrono::{Local, Utc};
use eframe::egui; use eframe::egui;
use eframe::egui::plot::GridMark;
use eframe::egui::{
plot::{Line, Plot},
Color32,
};
use std::sync::Arc; use std::sync::Arc;
use tracing::error; use tracing::error;
use self::data::ApplicationState;
use self::data::source::{Panel, Source}; use self::data::source::{Panel, Source};
use self::gui::panel::{panel_edit_inline_ui, panel_title_ui, panel_body_ui}; use self::data::ApplicationState;
use self::gui::source::{source_ui, source_edit_inline_ui}; use self::gui::panel::{panel_body_ui, panel_edit_inline_ui, panel_title_ui};
use self::util::{human_size, timestamp_to_str}; use self::gui::source::{source_edit_inline_ui, source_ui};
use self::util::human_size;
use self::worker::native_save; use self::worker::native_save;
pub struct App { pub struct App {
@ -61,7 +55,11 @@ impl eframe::App for App {
} }
ui.separator(); ui.separator();
ui.label("+ source"); ui.label("+ source");
source_edit_inline_ui(ui, &mut self.input_source, &self.data.panels.read().expect("Panels RwLock poisoned")); source_edit_inline_ui(
ui,
&mut self.input_source,
&self.data.panels.read().expect("Panels RwLock poisoned"),
);
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 souce : {:?}", e);

View file

@ -1,8 +1,8 @@
use std::sync::Arc; use crate::app::data::{source::fetch, ApplicationState};
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 std::sync::Arc;
use tracing::warn;
pub fn native_save(state: Arc<ApplicationState>) { pub fn native_save(state: Arc<ApplicationState>) {
std::thread::spawn(move || { std::thread::spawn(move || {
@ -66,25 +66,38 @@ impl BackgroundWorker for NativeBackgroundWorker {
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().expect("Sources RwLock poisoned"); 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!
match fetch(url.as_str(), query_x.as_str(), query_y.as_str()) { match fetch(url.as_str(), query_x.as_str(), query_y.as_str()) {
Ok(v) => { Ok(v) => {
let store = state2.storage.lock().expect("Storage mutex poisoned"); let store =
state2.storage.lock().expect("Storage mutex poisoned");
if let Err(e) = store.put_value(s_id, v) { if let Err(e) = store.put_value(s_id, v) {
warn!(target:"background-worker", "Could not put sample for source #{} in db: {:?}", s_id, e); warn!(target:"background-worker", "Could not put sample for source #{} in db: {:?}", s_id, e);
} else { } else {
let sources = state2.sources.read().expect("Sources RwLock poisoned"); let sources =
sources[j].data.write().expect("Source data RwLock poisoned").push(v); state2.sources.read().expect("Sources RwLock poisoned");
let mut last_update = sources[j].last_fetch.write().expect("Source last update 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) => { Err(e) => {
warn!(target:"background-worker", "Could not fetch value from {} : {:?}", url, e); warn!(target:"background-worker", "Could not fetch value from {} : {:?}", url, e);
} }
@ -102,12 +115,12 @@ impl BackgroundWorker for NativeBackgroundWorker {
} }
}); });
return NativeBackgroundWorker { return NativeBackgroundWorker { worker };
worker
};
} }
fn stop(self) { fn stop(self) {
self.worker.join().expect("Failed joining main worker thread"); self.worker
.join()
.expect("Failed joining main worker thread");
} }
} }

View file

@ -1,23 +1,26 @@
mod app; mod app;
use crate::app::{
data::ApplicationState,
util::InternalLogger,
worker::{BackgroundWorker, NativeBackgroundWorker},
App,
};
use std::sync::Arc; use std::sync::Arc;
use tracing_subscriber::prelude::*; 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; 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( let store =
ApplicationState::new(store_path).expect("Failed creating application state") Arc::new(ApplicationState::new(store_path).expect("Failed creating application state"));
);
tracing_subscriber::registry() tracing_subscriber::registry()
.with(LevelFilter::INFO) .with(LevelFilter::INFO)
@ -25,7 +28,8 @@ fn main() -> ! {
.with(InternalLogger::new(store.clone())) .with(InternalLogger::new(store.clone()))
.init(); .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",
native_options, native_options,
Box::new(move |cc| { Box::new(move |cc| {