diff --git a/Cargo.toml b/Cargo.toml index dd017a5..54abbfb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dashboard" -version = "0.1.3" +version = "0.2.0" edition = "2021" [[bin]] @@ -24,4 +24,4 @@ serde_json = "1" rusqlite = "0.27" jql = { version = "4", default-features = false } ureq = { version = "2", features = ["json"] } -eframe = "0.18" \ No newline at end of file +eframe = "0.18" diff --git a/src/app/data/mod.rs b/src/app/data/mod.rs index 16000ec..ed0806f 100644 --- a/src/app/data/mod.rs +++ b/src/app/data/mod.rs @@ -99,11 +99,13 @@ impl ApplicationState { .new_source( source.panel_id, source.name.as_str(), + source.enabled, source.url.as_str(), + source.interval, source.query_x.as_str(), source.query_y.as_str(), source.color, - source.visible, + self.sources.read().expect("Sources RwLock poisoned").len() as i32, )?; self.sources .write() diff --git a/src/app/data/source.rs b/src/app/data/source.rs index 0bd7c67..f040de1 100644 --- a/src/app/data/source.rs +++ b/src/app/data/source.rs @@ -33,10 +33,10 @@ impl Default for Panel { pub struct Source { pub(crate) id: i32, pub name: String, + pub enabled: bool, pub url: String, pub interval: i32, pub color: Color32, - pub visible: bool, pub(crate) last_fetch: RwLock>, pub query_x: String, // pub(crate) compiled_query_x: Arc>, @@ -51,10 +51,10 @@ impl Default for Source { Source { id: -1, name: "".to_string(), + enabled: false, url: "".to_string(), interval: 60, color: Color32::TRANSPARENT, - visible: false, last_fetch: RwLock::new(Utc::now()), query_x: "".to_string(), query_y: "".to_string(), diff --git a/src/app/data/store.rs b/src/app/data/store.rs index a24e1c8..2dcd365 100644 --- a/src/app/data/store.rs +++ b/src/app/data/store.rs @@ -39,13 +39,14 @@ impl SQLiteDataStore { "CREATE TABLE IF NOT EXISTS sources ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, + enabled BOOL NOT NULL, url TEXT NOT NULL, interval INT NOT NULL, query_x TEXT NOT NULL, query_y TEXT NOT NULL, panel_id INT NOT NULL, color INT NULL, - visible BOOL NOT NULL + position INT NOT NULL );", [], )?; @@ -93,19 +94,19 @@ impl SQLiteDataStore { pub fn load_sources(&self) -> rusqlite::Result> { let mut sources: Vec = Vec::new(); - let mut statement = self.conn.prepare("SELECT * FROM sources")?; + let mut statement = self.conn.prepare("SELECT * FROM sources ORDER BY position")?; let sources_iter = statement.query_map([], |row| { Ok(Source { id: row.get(0)?, name: row.get(1)?, - url: row.get(2)?, - interval: row.get(3)?, + enabled: row.get(2)?, + url: row.get(3)?, + interval: row.get(4)?, last_fetch: RwLock::new(Utc.ymd(1970, 1, 1).and_hms(0, 0, 0)), - query_x: row.get(4)?, - query_y: row.get(5)?, - panel_id: row.get(6)?, - color: unpack_color(row.get(7).unwrap_or(0)), - visible: row.get(8)?, + 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()), }) })?; @@ -125,11 +126,13 @@ impl SQLiteDataStore { &self, panel_id: i32, name: &str, + enabled: bool, url: &str, + interval: i32, query_x: &str, query_y: &str, color: Color32, - visible: bool, + position: i32, ) -> rusqlite::Result { let color_u32: Option = if color == Color32::TRANSPARENT { None @@ -137,8 +140,8 @@ impl SQLiteDataStore { Some(repack_color(color)) }; self.conn.execute( - "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], + "INSERT INTO sources(name, enabled, url, interval, query_x, query_y, panel_id, color, position) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", + params![name, enabled, url, interval, query_x, query_y, panel_id, color_u32, position], )?; let mut statement = self .conn @@ -147,14 +150,14 @@ impl SQLiteDataStore { Ok(Source { id: row.get(0)?, name: row.get(1)?, - url: row.get(2)?, - interval: row.get(3)?, + enabled: row.get(2)?, + url: row.get(3)?, + interval: row.get(4)?, last_fetch: RwLock::new(Utc.ymd(1970, 1, 1).and_hms(0, 0, 0)), - query_x: row.get(4)?, - query_y: row.get(5)?, - panel_id: row.get(6)?, - color: unpack_color(row.get(7).unwrap_or(0)), - visible: row.get(8)?, + 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()), }) })? { @@ -171,12 +174,13 @@ impl SQLiteDataStore { source_id: i32, panel_id: i32, name: &str, + enabled: bool, url: &str, interval: i32, query_x: &str, query_y: &str, color: Color32, - visible: bool, + position: i32, ) -> rusqlite::Result { let color_u32: Option = if color == Color32::TRANSPARENT { None @@ -184,8 +188,8 @@ impl SQLiteDataStore { Some(repack_color(color)) }; self.conn.execute( - "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], + "UPDATE sources SET name = ?, enabled = ?, url = ?, interval = ?, query_x = ?, query_y = ?, panel_id = ?, color = ?, position = ? WHERE id = ?", + params![name, enabled, url, interval, query_x, query_y, panel_id, color_u32, position, source_id], ) } @@ -271,7 +275,7 @@ impl SQLiteDataStore { ) } - // pub fn delete_panel(&self, id:i32) -> rusqlite::Result { - // self.conn.execute("DELETE FROM panels WHERE id = ?", params![id]) - // } + pub fn delete_panel(&self, id:i32) -> rusqlite::Result { + self.conn.execute("DELETE FROM panels WHERE id = ?", params![id]) + } } diff --git a/src/app/gui/panel.rs b/src/app/gui/panel.rs index 310acb4..5146105 100644 --- a/src/app/gui/panel.rs +++ b/src/app/gui/panel.rs @@ -47,15 +47,16 @@ pub fn panel_body_ui(ui: &mut Ui, panel: &mut Panel, sources: &Vec) { .allow_scroll(false) .legend(Legend::default().position(Corner::LeftTop)); + if panel.limit { + p = p.set_margin_fraction(Vec2 { x: 0.0, y: 0.1 }); + } + if panel.view_scroll { p = p.include_x(Utc::now().timestamp() as f64); if panel.limit { p = p - .set_margin_fraction(Vec2 { x: 0.0, y: 0.1 }) - .include_x((Utc::now().timestamp() + (panel.view_size as i64 * 3)) as f64); - } - if panel.limit { - p = p.include_x((Utc::now().timestamp() - (panel.view_size as i64 * 60)) as f64); + .include_x((Utc::now().timestamp() + (panel.view_size as i64 * 3)) as f64) + .include_x((Utc::now().timestamp() - (panel.view_size as i64 * 60)) as f64); // ??? TODO } } @@ -106,7 +107,7 @@ pub fn panel_body_ui(ui: &mut Ui, panel: &mut Panel, sources: &Vec) { p.show(ui, |plot_ui| { for source in &*sources { - if source.visible && source.panel_id == panel.id { + if source.panel_id == panel.id { let line = if panel.limit { Line::new(source.values_filter( (Utc::now().timestamp() - (panel.view_size as i64 * 60)) as f64, diff --git a/src/app/gui/source.rs b/src/app/gui/source.rs index 60ec0a5..2a1ae8b 100644 --- a/src/app/gui/source.rs +++ b/src/app/gui/source.rs @@ -28,51 +28,54 @@ pub fn source_edit_inline_ui(ui: &mut Ui, source: &mut Source, panels: &Vec, width: f32) { ui.group(|ui| { - ui.horizontal(|ui| { - let text_width = width - 25.0; - ui.checkbox(&mut source.visible, ""); - 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| { - 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); + ui.vertical(|ui| { + ui.horizontal(|ui| { + let text_width = width - 25.0; + ui.checkbox(&mut source.enabled, ""); + TextEdit::singleline(&mut source.name) + .desired_width(text_width / 4.0) + .hint_text("name") + .show(ui); + TextEdit::singleline(&mut source.url) + .desired_width(text_width * 3.0 / 4.0) + .hint_text("url") + .show(ui); + }); + ui.horizontal(|ui| { + let text_width : f32 ; + if width > 400.0 { + ui.add(Slider::new(&mut source.interval, 1..=120)); + text_width = width - 330.0 + } else { + ui.add(DragValue::new(&mut source.interval).clamp_range(1..=120)); + text_width = width - 225.0 + } + TextEdit::singleline(&mut source.query_x) + .desired_width(text_width / 2.0) + .hint_text("x") + .show(ui); + TextEdit::singleline(&mut source.query_y) + .desired_width(text_width / 2.0) + .hint_text("y") + .show(ui); + ComboBox::from_id_source(format!("panel-{}", source.id)) + .width(60.0) + .selected_text(format!("panel: {}", source.panel_id)) + .show_ui(ui, |ui| { + ui.selectable_value(&mut source.panel_id, -1, "None"); + for p in panels { + ui.selectable_value(&mut source.panel_id, p.id, p.name.as_str()); + } + }); + ui.color_edit_button_srgba(&mut source.color); + }); }); }); } diff --git a/src/app/mod.rs b/src/app/mod.rs index 01c0faf..8180464 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -78,6 +78,7 @@ impl eframe::App for App { ) .show_header(ui, |ui| { ui.horizontal(|ui| { + ui.separator(); ui.label(self.data.file_path.to_str().unwrap()); // TODO maybe calculate it just once? ui.separator(); ui.label(human_size( @@ -118,17 +119,36 @@ impl eframe::App for App { }); }); if self.edit { + let mut to_swap: Option = None; + // let mut to_delete: Option = None; SidePanel::left("sources-bar") - .width_range(240.0..=800.0) + .width_range(280.0..=800.0) .default_width(500.0) .show(ctx, |ui| { let panels = self.data.panels.read().expect("Panels RwLock poisoned"); ScrollArea::vertical().show(ui, |ui| { - let width = ui.available_width(); + let panel_width = ui.available_width(); { let mut sources = self.data.sources.write().expect("Sources RwLock poisoned"); - for source in &mut *sources { - source_edit_ui(ui, source, &panels, width); + let sources_count = sources.len(); + for (index, source) in sources.iter_mut().enumerate() { + ui.horizontal(|ui| { + ui.vertical(|ui| { + ui.add_space(10.0); + if ui.small_button("+").clicked() { + if index > 0 { + to_swap = Some(index); // TODO kinda jank but is there a better way? + } + } + if ui.small_button("−").clicked() { + if index < sources_count - 1 { + to_swap = Some(index + 1); // TODO kinda jank but is there a better way? + } + } + }); + let remaining_width = ui.available_width(); + source_edit_ui(ui, source, &panels, remaining_width); + }); } } ui.add_space(20.0); @@ -148,14 +168,25 @@ impl eframe::App for App { }); }); }); - source_edit_ui(ui, &mut self.input_source, &panels, width); + source_edit_ui(ui, &mut self.input_source, &panels, panel_width); if self.padding { ui.add_space(300.0); } }); }); + //if let Some(i) = to_delete { + // // TODO can this be done in background? idk + // let mut panels = self.data.panels.write().expect("Panels RwLock poisoned"); + // panels.remove(i); + // } else + if let Some(i) = to_swap { + // TODO can this be done in background? idk + let mut sources = self.data.sources.write().expect("Sources RwLock poisoned"); + sources.swap(i - 1, i); + } } - let mut to_swap: Vec = Vec::new(); + let mut to_swap: Option = None; + let mut to_delete: Option = None; CentralPanel::default().show(ctx, |ui| { ScrollArea::vertical().show(ui, |ui| { let mut panels = self.data.panels.write().expect("Panels RwLock poisoned"); // TODO only lock as write when editing @@ -174,14 +205,17 @@ impl eframe::App for App { if self.edit { if ui.small_button(" + ").clicked() { if index > 0 { - to_swap.push(index); // TODO kinda jank but is there a better way? + to_swap = Some(index); // TODO kinda jank but is there a better way? } } - if ui.small_button(" - ").clicked() { + if ui.small_button(" − ").clicked() { if index < panels_count - 1 { - to_swap.push(index + 1); // TODO kinda jank but is there a better way? + to_swap = Some(index + 1); // TODO kinda jank but is there a better way? } } + if ui.small_button(" × ").clicked() { + to_delete = Some(index); // TODO kinda jank but is there a better way? + } ui.separator(); } panel_title_ui(ui, panel, self.edit); @@ -190,12 +224,18 @@ impl eframe::App for App { } }); }); - if !to_swap.is_empty() { + if let Some(i) = to_delete { // TODO can this be done in background? idk let mut panels = self.data.panels.write().expect("Panels RwLock poisoned"); - for index in to_swap { - panels.swap(index - 1, index); + if let Err(e) = self.data.storage.lock().expect("Storage Mutex poisoned").delete_panel(panels[i].id) { + error!(target: "ui", "Could not delete panel : {:?}", e); + } else { + panels.remove(i); } + } else if let Some(i) = to_swap { + // TODO can this be done in background? idk + let mut panels = self.data.panels.write().expect("Panels RwLock poisoned"); + panels.swap(i - 1, i); } } } diff --git a/src/app/worker.rs b/src/app/worker.rs index 0887ea3..fc28b49 100644 --- a/src/app/worker.rs +++ b/src/app/worker.rs @@ -23,17 +23,18 @@ pub fn native_save(state: Arc) { 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 (index, source) in sources.iter().enumerate() { if let Err(e) = storage.update_source( source.id, source.panel_id, source.name.as_str(), + source.enabled, source.url.as_str(), source.interval, source.query_x.as_str(), source.query_y.as_str(), source.color, - source.visible, + index as i32, ) { warn!(target: "native-save", "Could not update source #{} : {:?}", source.id, e); } @@ -65,7 +66,7 @@ impl BackgroundWorker for NativeBackgroundWorker { let sources = state.sources.read().expect("Sources RwLock poisoned"); for j in 0..sources.len() { let s_id = sources[j].id; - if !sources[j].valid() { + if sources[j].enabled && !sources[j].valid() { let mut last_update = sources[j] .last_fetch .write()