feat!: made sources reorderable and panels deletable

Added in sources a position field which works just like the one in panels.
Added in sources an "enabled" flag which now governs wether or not data is fetched.
Added a button to delete panels.
Tweaked UI a little
This commit is contained in:
əlemi 2022-06-15 02:35:13 +02:00
parent 004a0c2b99
commit b05f8005e7
No known key found for this signature in database
GPG key ID: BBCBFE5D7244634E
8 changed files with 141 additions and 90 deletions

View file

@ -1,6 +1,6 @@
[package] [package]
name = "dashboard" name = "dashboard"
version = "0.1.3" version = "0.2.0"
edition = "2021" edition = "2021"
[[bin]] [[bin]]
@ -24,4 +24,4 @@ serde_json = "1"
rusqlite = "0.27" rusqlite = "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 = "0.18" eframe = "0.18"

View file

@ -99,11 +99,13 @@ impl ApplicationState {
.new_source( .new_source(
source.panel_id, source.panel_id,
source.name.as_str(), source.name.as_str(),
source.enabled,
source.url.as_str(), source.url.as_str(),
source.interval,
source.query_x.as_str(), source.query_x.as_str(),
source.query_y.as_str(), source.query_y.as_str(),
source.color, source.color,
source.visible, self.sources.read().expect("Sources RwLock poisoned").len() as i32,
)?; )?;
self.sources self.sources
.write() .write()

View file

@ -33,10 +33,10 @@ impl Default for Panel {
pub struct Source { pub struct Source {
pub(crate) id: i32, pub(crate) id: i32,
pub name: String, pub name: String,
pub enabled: bool,
pub url: String, pub url: String,
pub interval: i32, pub interval: i32,
pub color: Color32, pub color: Color32,
pub visible: bool,
pub(crate) last_fetch: RwLock<DateTime<Utc>>, pub(crate) last_fetch: RwLock<DateTime<Utc>>,
pub query_x: String, pub query_x: String,
// pub(crate) compiled_query_x: Arc<Mutex<jq_rs::JqProgram>>, // pub(crate) compiled_query_x: Arc<Mutex<jq_rs::JqProgram>>,
@ -51,10 +51,10 @@ impl Default for Source {
Source { Source {
id: -1, id: -1,
name: "".to_string(), name: "".to_string(),
enabled: false,
url: "".to_string(), url: "".to_string(),
interval: 60, interval: 60,
color: Color32::TRANSPARENT, color: Color32::TRANSPARENT,
visible: false,
last_fetch: RwLock::new(Utc::now()), last_fetch: RwLock::new(Utc::now()),
query_x: "".to_string(), query_x: "".to_string(),
query_y: "".to_string(), query_y: "".to_string(),

View file

@ -39,13 +39,14 @@ impl SQLiteDataStore {
"CREATE TABLE IF NOT EXISTS sources ( "CREATE TABLE IF NOT EXISTS sources (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
name TEXT NOT NULL, name TEXT 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_x TEXT NOT NULL,
query_y TEXT NOT NULL, query_y TEXT NOT NULL,
panel_id INT NOT NULL, panel_id INT NOT NULL,
color INT 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<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.conn.prepare("SELECT * FROM sources")?; let mut statement = self.conn.prepare("SELECT * FROM sources ORDER BY position")?;
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)?,
name: row.get(1)?, name: row.get(1)?,
url: row.get(2)?, enabled: row.get(2)?,
interval: row.get(3)?, url: row.get(3)?,
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(4)?, query_x: row.get(5)?,
query_y: row.get(5)?, query_y: row.get(6)?,
panel_id: row.get(6)?, panel_id: row.get(7)?,
color: unpack_color(row.get(7).unwrap_or(0)), color: unpack_color(row.get(8).unwrap_or(0)),
visible: row.get(8)?,
data: RwLock::new(Vec::new()), data: RwLock::new(Vec::new()),
}) })
})?; })?;
@ -125,11 +126,13 @@ impl SQLiteDataStore {
&self, &self,
panel_id: i32, panel_id: i32,
name: &str, name: &str,
enabled: bool,
url: &str, url: &str,
interval: i32,
query_x: &str, query_x: &str,
query_y: &str, query_y: &str,
color: Color32, color: Color32,
visible: bool, position: i32,
) -> rusqlite::Result<Source> { ) -> rusqlite::Result<Source> {
let color_u32: Option<u32> = if color == Color32::TRANSPARENT { let color_u32: Option<u32> = if color == Color32::TRANSPARENT {
None None
@ -137,8 +140,8 @@ impl SQLiteDataStore {
Some(repack_color(color)) 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, enabled, url, interval, query_x, query_y, panel_id, color, position) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
params![name, url, 60i32, query_x, query_y, panel_id, color_u32, visible], params![name, enabled, url, interval, query_x, query_y, panel_id, color_u32, position],
)?; )?;
let mut statement = self let mut statement = self
.conn .conn
@ -147,14 +150,14 @@ impl SQLiteDataStore {
Ok(Source { Ok(Source {
id: row.get(0)?, id: row.get(0)?,
name: row.get(1)?, name: row.get(1)?,
url: row.get(2)?, enabled: row.get(2)?,
interval: row.get(3)?, url: row.get(3)?,
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(4)?, query_x: row.get(5)?,
query_y: row.get(5)?, query_y: row.get(6)?,
panel_id: row.get(6)?, panel_id: row.get(7)?,
color: unpack_color(row.get(7).unwrap_or(0)), color: unpack_color(row.get(8).unwrap_or(0)),
visible: row.get(8)?,
data: RwLock::new(Vec::new()), data: RwLock::new(Vec::new()),
}) })
})? { })? {
@ -171,12 +174,13 @@ impl SQLiteDataStore {
source_id: i32, source_id: i32,
panel_id: i32, panel_id: i32,
name: &str, name: &str,
enabled: bool,
url: &str, url: &str,
interval: i32, interval: i32,
query_x: &str, query_x: &str,
query_y: &str, query_y: &str,
color: Color32, color: Color32,
visible: bool, position: i32,
) -> rusqlite::Result<usize> { ) -> rusqlite::Result<usize> {
let color_u32: Option<u32> = if color == Color32::TRANSPARENT { let color_u32: Option<u32> = if color == Color32::TRANSPARENT {
None None
@ -184,8 +188,8 @@ impl SQLiteDataStore {
Some(repack_color(color)) 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 = ?, enabled = ?, url = ?, interval = ?, query_x = ?, query_y = ?, panel_id = ?, color = ?, position = ? WHERE id = ?",
params![name, url, interval, query_x, query_y, panel_id, color_u32, visible, source_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<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

@ -47,15 +47,16 @@ pub fn panel_body_ui(ui: &mut Ui, panel: &mut Panel, sources: &Vec<Source>) {
.allow_scroll(false) .allow_scroll(false)
.legend(Legend::default().position(Corner::LeftTop)); .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 { if panel.view_scroll {
p = p.include_x(Utc::now().timestamp() as f64); p = p.include_x(Utc::now().timestamp() as f64);
if panel.limit { if panel.limit {
p = p 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)
.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
}
if panel.limit {
p = p.include_x((Utc::now().timestamp() - (panel.view_size as i64 * 60)) as f64);
} }
} }
@ -106,7 +107,7 @@ pub fn panel_body_ui(ui: &mut Ui, panel: &mut Panel, sources: &Vec<Source>) {
p.show(ui, |plot_ui| { p.show(ui, |plot_ui| {
for source in &*sources { for source in &*sources {
if source.visible && source.panel_id == panel.id { if 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() - (panel.view_size as i64 * 60)) as f64, (Utc::now().timestamp() - (panel.view_size as i64 * 60)) as f64,

View file

@ -28,51 +28,54 @@ pub fn source_edit_inline_ui(ui: &mut Ui, source: &mut Source, panels: &Vec<Pane
ui.selectable_value(&mut source.panel_id, p.id, p.name.as_str()); ui.selectable_value(&mut source.panel_id, p.id, p.name.as_str());
} }
}); });
ui.checkbox(&mut source.visible, "visible"); ui.checkbox(&mut source.enabled, "enabled");
ui.add(Slider::new(&mut source.interval, 1..=60)); ui.add(Slider::new(&mut source.interval, 1..=60));
ui.color_edit_button_srgba(&mut source.color); 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_edit_ui(ui: &mut Ui, source: &mut Source, panels: &Vec<Panel>, width: f32) {
ui.group(|ui| { ui.group(|ui| {
ui.horizontal(|ui| { ui.vertical(|ui| {
let text_width = width - 25.0; ui.horizontal(|ui| {
ui.checkbox(&mut source.visible, ""); let text_width = width - 25.0;
TextEdit::singleline(&mut source.name) ui.checkbox(&mut source.enabled, "");
.desired_width(text_width / 4.0) TextEdit::singleline(&mut source.name)
.hint_text("name") .desired_width(text_width / 4.0)
.show(ui); .hint_text("name")
TextEdit::singleline(&mut source.url) .show(ui);
.desired_width(text_width * 3.0 / 4.0) TextEdit::singleline(&mut source.url)
.hint_text("url") .desired_width(text_width * 3.0 / 4.0)
.show(ui); .hint_text("url")
}); .show(ui);
ui.horizontal(|ui| { });
let text_width : f32 ; ui.horizontal(|ui| {
if width > 400.0 { let text_width : f32 ;
ui.add(Slider::new(&mut source.interval, 1..=120)); if width > 400.0 {
text_width = width - 330.0 ui.add(Slider::new(&mut source.interval, 1..=120));
} else { text_width = width - 330.0
ui.add(DragValue::new(&mut source.interval).clamp_range(1..=120)); } else {
text_width = width - 225.0 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) TextEdit::singleline(&mut source.query_x)
.hint_text("x") .desired_width(text_width / 2.0)
.show(ui); .hint_text("x")
TextEdit::singleline(&mut source.query_y) .show(ui);
.desired_width(text_width / 2.0) TextEdit::singleline(&mut source.query_y)
.hint_text("y") .desired_width(text_width / 2.0)
.show(ui); .hint_text("y")
ComboBox::from_id_source(format!("panel-{}", source.id)) .show(ui);
.width(60.0) ComboBox::from_id_source(format!("panel-{}", source.id))
.selected_text(format!("panel [{}]", source.panel_id)) .width(60.0)
.show_ui(ui, |ui| { .selected_text(format!("panel: {}", source.panel_id))
for p in panels { .show_ui(ui, |ui| {
ui.selectable_value(&mut source.panel_id, p.id, p.name.as_str()); 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); }
});
ui.color_edit_button_srgba(&mut source.color);
});
}); });
}); });
} }

View file

@ -78,6 +78,7 @@ impl eframe::App for App {
) )
.show_header(ui, |ui| { .show_header(ui, |ui| {
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.separator();
ui.label(self.data.file_path.to_str().unwrap()); // TODO maybe calculate it just once? ui.label(self.data.file_path.to_str().unwrap()); // TODO maybe calculate it just once?
ui.separator(); ui.separator();
ui.label(human_size( ui.label(human_size(
@ -118,17 +119,36 @@ impl eframe::App for App {
}); });
}); });
if self.edit { if self.edit {
let mut to_swap: Option<usize> = None;
// let mut to_delete: Option<usize> = None;
SidePanel::left("sources-bar") SidePanel::left("sources-bar")
.width_range(240.0..=800.0) .width_range(280.0..=800.0)
.default_width(500.0) .default_width(500.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| { 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"); let mut sources = self.data.sources.write().expect("Sources RwLock poisoned");
for source in &mut *sources { let sources_count = sources.len();
source_edit_ui(ui, source, &panels, width); 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); 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 { if self.padding {
ui.add_space(300.0); 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<usize> = Vec::new(); let mut to_swap: Option<usize> = None;
let mut to_delete: Option<usize> = None;
CentralPanel::default().show(ctx, |ui| { CentralPanel::default().show(ctx, |ui| {
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
@ -174,14 +205,17 @@ impl eframe::App for App {
if self.edit { if self.edit {
if ui.small_button(" + ").clicked() { if ui.small_button(" + ").clicked() {
if index > 0 { 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 { 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(); ui.separator();
} }
panel_title_ui(ui, panel, self.edit); 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 // 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");
for index in to_swap { if let Err(e) = self.data.storage.lock().expect("Storage Mutex poisoned").delete_panel(panels[i].id) {
panels.swap(index - 1, index); 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);
} }
} }
} }

View file

@ -23,17 +23,18 @@ 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"); 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( 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(),
source.enabled,
source.url.as_str(), source.url.as_str(),
source.interval, source.interval,
source.query_x.as_str(), source.query_x.as_str(),
source.query_y.as_str(), source.query_y.as_str(),
source.color, source.color,
source.visible, index as i32,
) { ) {
warn!(target: "native-save", "Could not update source #{} : {:?}", source.id, e); 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"); 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].enabled && !sources[j].valid() {
let mut last_update = sources[j] let mut last_update = sources[j]
.last_fetch .last_fetch
.write() .write()