feat: made plots collapsable, moved log in footer

footer is also collapsing
This commit is contained in:
əlemi 2022-06-13 02:37:20 +02:00
parent 4c40041048
commit a75d6b432f
No known key found for this signature in database
GPG key ID: BBCBFE5D7244634E
2 changed files with 250 additions and 142 deletions

View file

@ -1,17 +1,20 @@
pub mod data; pub mod data;
pub mod worker;
pub mod util; pub mod util;
pub mod worker;
use std::sync::Arc; use chrono::{Local, Utc};
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::{
plot::{Line, Plot},
Color32,
};
use std::sync::Arc;
use tracing::error;
use self::data::ApplicationState; use self::data::ApplicationState;
use self::worker::native_save;
use self::util::{human_size, timestamp_to_str}; use self::util::{human_size, timestamp_to_str};
use self::worker::native_save;
struct InputBuffer { struct InputBuffer {
panel_name: String, panel_name: String,
@ -45,12 +48,15 @@ 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, show_log: false } Self {
data,
input: InputBuffer::default(),
edit: false,
}
} }
} }
@ -69,7 +75,10 @@ impl eframe::App for App {
} }
ui.separator(); ui.separator();
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() {
if let Err(e) = self.data.add_panel(self.input.panel_name.as_str()) { if let Err(e) = self.data.add_panel(self.input.panel_name.as_str()) {
error!(target: "ui", "Failed to add panel: {:?}", e); error!(target: "ui", "Failed to add panel: {:?}", e);
@ -77,20 +86,35 @@ impl eframe::App for App {
} }
ui.separator(); ui.separator();
ui.label("+ source"); ui.label("+ source");
eframe::egui::TextEdit::singleline(&mut self.input.name).hint_text("name").desired_width(50.0).show(ui); eframe::egui::TextEdit::singleline(&mut self.input.name)
eframe::egui::TextEdit::singleline(&mut self.input.url).hint_text("url").desired_width(160.0).show(ui); .hint_text("name")
eframe::egui::TextEdit::singleline(&mut self.input.query_x).hint_text("x").desired_width(30.0).show(ui); .desired_width(50.0)
eframe::egui::TextEdit::singleline(&mut self.input.query_y).hint_text("y").desired_width(30.0).show(ui); .show(ui);
eframe::egui::TextEdit::singleline(&mut self.input.url)
.hint_text("url")
.desired_width(160.0)
.show(ui);
eframe::egui::TextEdit::singleline(&mut self.input.query_x)
.hint_text("x")
.desired_width(30.0)
.show(ui);
eframe::egui::TextEdit::singleline(&mut self.input.query_y)
.hint_text("y")
.desired_width(30.0)
.show(ui);
egui::ComboBox::from_id_source("panel") egui::ComboBox::from_id_source("panel")
.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().expect("Panels RwLock poisoned"); 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(),
);
} }
} });
);
ui.checkbox(&mut self.input.visible, "visible"); ui.checkbox(&mut self.input.visible, "visible");
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);
@ -114,29 +138,53 @@ impl eframe::App for App {
if ui.small_button("×").clicked() { 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|{ egui::collapsing_header::CollapsingState::load_with_default_open(
ui.label(self.data.file_path.to_str().unwrap()); // TODO maybe calculate it just once? ctx,
ui.separator(); ui.make_persistent_id("footer-logs"),
ui.label(human_size(*self.data.file_size.read().expect("Filesize RwLock poisoned"))); false,
let diags = self.data.diagnostics.read().expect("Diagnostics RwLock poisoned"); )
if diags.len() > 0 { .show_header(ui, |ui| {
ui.horizontal(|ui| {
ui.label(self.data.file_path.to_str().unwrap()); // TODO maybe calculate it just once?
ui.separator(); ui.separator();
ui.label(diags.last().unwrap_or(&"".to_string())); ui.label(human_size(
} *self
ui.with_layout(egui::Layout::top_down(egui::Align::RIGHT), |ui| { .data
ui.horizontal(|ui| { .file_size
ui.label(format!("v{}-{}", env!("CARGO_PKG_VERSION"), git_version::git_version!())); .read()
ui.separator(); .expect("Filesize RwLock poisoned"),
ui.hyperlink_to("<me@alemi.dev>", "mailto:me@alemi.dev"); ));
ui.label("alemi"); ui.with_layout(egui::Layout::top_down(egui::Align::RIGHT), |ui| {
ui.horizontal(|ui| {
ui.label(format!(
"v{}-{}",
env!("CARGO_PKG_VERSION"),
git_version::git_version!()
));
ui.separator();
ui.hyperlink_to("<me@alemi.dev>", "mailto:me@alemi.dev");
ui.label("alemi");
});
}); });
}); });
})
.body(|ui| {
ui.set_height(200.0);
egui::ScrollArea::vertical().show(ui, |ui| {
let msgs = self
.data
.diagnostics
.read()
.expect("Diagnostics RwLock poisoned");
for msg in msgs.iter() {
ui.label(msg);
}
});
}); });
}); });
if self.edit { if self.edit {
@ -147,20 +195,40 @@ impl eframe::App for App {
ui.group(|ui| { ui.group(|ui| {
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.checkbox(&mut source.visible, ""); 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.name)
eframe::egui::TextEdit::singleline(&mut source.url).hint_text("url").desired_width(300.0).show(ui); .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.horizontal(|ui| {
ui.add(egui::Slider::new(&mut source.interval, 1..=60)); 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_x)
eframe::egui::TextEdit::singleline(&mut source.query_y).hint_text("y").desired_width(50.0).show(ui); .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)) egui::ComboBox::from_id_source(format!("panel-{}", source.id))
.selected_text(format!("panel [{}]", source.panel_id)) .selected_text(format!("panel [{}]", source.panel_id))
.width(70.0) .width(70.0)
.show_ui(ui, |ui| { .show_ui(ui, |ui| {
let pnls = self.data.panels.read().expect("Panels RwLock poisoned"); let pnls = self
.data
.panels
.read()
.expect("Panels RwLock poisoned");
for p in &*pnls { for p in &*pnls {
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.color_edit_button_srgba(&mut source.color); ui.color_edit_button_srgba(&mut source.color);
@ -174,120 +242,143 @@ impl eframe::App for App {
}); });
}); });
} }
if self.show_log { let mut to_swap: Vec<usize> = Vec::new();
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().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
let sources = self.data.sources.read().expect("Sources RwLock poisoned"); // 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 (index, panel) in panels.iter_mut().enumerate() { for (index, panel) in panels.iter_mut().enumerate() {
ui.group(|ui| { if index > 0 {
ui.vertical(|ui| { ui.separator();
ui.horizontal(|ui| { }
ui.heading(panel.name.as_str()); egui::collapsing_header::CollapsingState::load_with_default_open(
ui.separator(); ctx,
if self.edit && index > 0 { ui.make_persistent_id(format!("panel-{}-compressable", panel.id)),
if ui.small_button("up").clicked() { true,
)
.show_header(ui, |ui| {
ui.horizontal(|ui| {
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.push(index); // TODO kinda jank but is there a better way?
} }
ui.separator();
} }
for source in &*sources {
if source.panel_id == panel.id {
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();
}
}
ui.add(egui::Slider::new(&mut panel.height, 0..=500).text("height"));
ui.separator(); ui.separator();
ui.checkbox(&mut panel.limit, "limit"); }
ui.add(egui::DragValue::new(&mut panel.view_size).speed(10).clamp_range(0..=2147483647)); ui.heading(panel.name.as_str());
ui.label("mins"); if self.edit {
ui.separator();
ui.add(
egui::Slider::new(&mut panel.height, 0..=500).text("height"),
);
ui.separator(); ui.separator();
ui.checkbox(&mut panel.timeserie, "timeserie"); ui.checkbox(&mut panel.timeserie, "timeserie");
ui.checkbox(&mut panel.view_scroll, "autoscroll");
ui.separator();
});
let mut p = Plot::new(format!("plot-{}", panel.name))
.height(panel.height as f32)
.allow_scroll(false);
if panel.view_scroll {
p = p.include_x(Utc::now().timestamp() as f64);
if panel.limit {
p = p.include_x((Utc::now().timestamp() - (panel.view_size as i64 * 60)) as f64);
}
} }
ui.with_layout(egui::Layout::top_down(egui::Align::RIGHT), |ui| {
if panel.timeserie { ui.horizontal(|ui| {
p = p.x_axis_formatter(|x, _range| timestamp_to_str(x as i64, true, false)); ui.toggle_value(&mut panel.view_scroll, "");
p = p.label_formatter(|name, value| { ui.separator();
if !name.is_empty() { ui.label("m");
return format!("{}\nx = {}\ny = {:.1}", name, timestamp_to_str(value.x as i64, false, true), value.y) ui.add(
} else { egui::DragValue::new(&mut panel.view_size)
return format!("x = {}\ny = {:.1}", timestamp_to_str(value.x as i64, false, true), value.y); .speed(10)
} .clamp_range(0..=2147483647i32),
);
ui.checkbox(&mut panel.limit, "limit");
}); });
p = p.x_grid_spacer(|grid| { });
});
})
.body(|ui| {
let mut p = Plot::new(format!("plot-{}", panel.name))
.height(panel.height as f32)
.allow_scroll(false)
.legend(egui::plot::Legend::default().position(egui::plot::Corner::LeftTop));
if panel.view_scroll {
p = p
.include_x(Utc::now().timestamp() as f64);
if panel.limit {
p = p.include_x(
(Utc::now().timestamp() - (panel.view_size as i64 * 60))
as f64,
);
}
}
if panel.timeserie {
p = p
.x_axis_formatter(|x, _range| {
timestamp_to_str(x as i64, true, false)
})
.label_formatter(|name, value| {
if !name.is_empty() {
return format!(
"{}\nx = {}\ny = {:.1}",
name,
timestamp_to_str(value.x as i64, false, true),
value.y
);
} else {
return format!(
"x = {}\ny = {:.1}",
timestamp_to_str(value.x as i64, false, true),
value.y
);
}
})
.x_grid_spacer(|grid| {
let offset = Local::now().offset().local_minus_utc() as i64; let offset = Local::now().offset().local_minus_utc() as i64;
let (start, end) = grid.bounds; let (start, end) = grid.bounds;
let mut counter = (start as i64) - ((start as i64) % 3600); let mut counter = (start as i64) - ((start as i64) % 3600);
let mut out : Vec<GridMark> = Vec::new(); let mut out: Vec<GridMark> = Vec::new();
loop { loop {
counter += 3600; counter += 3600;
if counter > end as i64 { break; } if counter > end as i64 {
break;
}
if (counter + offset) % 86400 == 0 { if (counter + offset) % 86400 == 0 {
out.push(GridMark { value: counter as f64, step_size: 86400 as f64 }) out.push(GridMark {
value: counter as f64,
step_size: 86400 as f64,
})
} else if counter % 3600 == 0 { } else if counter % 3600 == 0 {
out.push(GridMark { value: counter as f64, step_size: 3600 as f64 }); out.push(GridMark {
value: counter as f64,
step_size: 3600 as f64,
});
} }
} }
return out; return out;
}); });
} }
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.visible && source.panel_id == panel.id {
let line = if panel.limit { let line = if panel.limit {
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(
} else { (Utc::now().timestamp()
Line::new(source.values()).name(source.name.as_str()) - (panel.view_size as i64 * 60)) as f64,
}; ))
plot_ui.line(line.color(source.color)); .name(source.name.as_str())
} } else {
Line::new(source.values()).name(source.name.as_str())
};
plot_ui.line(line.color(source.color));
} }
}); }
}); });
}); });
} }
}); });
}); });
if !to_swap.is_empty() { // TODO can this be done in background? idk if !to_swap.is_empty() {
// 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 { for index in to_swap {
panels.swap(index-1, index); panels.swap(index - 1, index);
} }
} }
} }

View file

@ -1,7 +1,7 @@
use std::sync::Arc; use chrono::{DateTime, Local, NaiveDateTime, Utc};
use chrono::{DateTime, NaiveDateTime, Utc, Local};
use tracing_subscriber::Layer;
use eframe::egui::Color32; use eframe::egui::Color32;
use std::sync::Arc;
use tracing_subscriber::Layer;
use super::data::ApplicationState; use super::data::ApplicationState;
@ -22,17 +22,19 @@ pub fn human_size(size: u64) -> String {
pub fn timestamp_to_str(t: i64, date: bool, time: bool) -> String { pub fn timestamp_to_str(t: i64, date: bool, time: bool) -> String {
format!( format!(
"{}", "{}",
DateTime::<Local>::from(DateTime::<Utc>::from_utc(NaiveDateTime::from_timestamp(t, 0), Utc)).format( DateTime::<Local>::from(DateTime::<Utc>::from_utc(
if date && time { NaiveDateTime::from_timestamp(t, 0),
"%Y/%m/%d %H:%M:%S" Utc
} else if date { ))
"%Y/%m/%d" .format(if date && time {
} else if time { "%Y/%m/%d %H:%M:%S"
"%H:%M:%S" } else if date {
} else { "%Y/%m/%d"
"%s" } else if time {
} "%H:%M:%S"
) } else {
"%s"
})
) )
} }
@ -64,21 +66,36 @@ impl InternalLogger {
} }
} }
impl<S> Layer<S> for InternalLogger where S: tracing::Subscriber { impl<S> Layer<S> for InternalLogger
where
S: tracing::Subscriber,
{
fn on_event( fn on_event(
&self, &self,
event: &tracing::Event<'_>, event: &tracing::Event<'_>,
_ctx: tracing_subscriber::layer::Context<'_, S>, _ctx: tracing_subscriber::layer::Context<'_, S>,
) { ) {
let mut msg_visitor = LogMessageVisitor { msg: "".to_string() }; let mut msg_visitor = LogMessageVisitor {
msg: "".to_string(),
};
event.record(&mut msg_visitor); event.record(&mut msg_visitor);
let out = format!("{} [{}] {}: {}", Local::now().format("%H:%M:%S"), event.metadata().level(), event.metadata().target(), msg_visitor.msg); let out = format!(
self.state.diagnostics.write().expect("Diagnostics RwLock poisoned").push(out); "{} [{}] {}: {}",
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 { struct LogMessageVisitor {
msg : String, msg: String,
} }
impl tracing::field::Visit for LogMessageVisitor { impl tracing::field::Visit for LogMessageVisitor {