chore: boilerplate over async stuff

This commit is contained in:
əlemi 2022-11-01 04:32:46 +01:00
parent cbca9f99b3
commit bc94398e49
Signed by: alemi
GPG key ID: A4895B84D311642C
12 changed files with 695 additions and 456 deletions

View file

@ -2,8 +2,6 @@ pub mod entities;
use std::num::ParseFloatError; use std::num::ParseFloatError;
use sea_orm::{DatabaseConnection, EntityTrait};
#[derive(Debug)] #[derive(Debug)]
pub enum FetchError { pub enum FetchError {
ReqwestError(reqwest::Error), ReqwestError(reqwest::Error),
@ -39,39 +37,3 @@ impl From<sea_orm::DbErr> for FetchError {
FetchError::DbError(e) FetchError::DbError(e)
} }
} }
#[allow(dead_code)]
pub struct ApplicationState {
// pub run: bool,
db: DatabaseConnection,
pub panels: Vec<entities::panels::Model>,
pub sources: Vec<entities::sources::Model>,
pub metrics: Vec<entities::metrics::Model>,
last_fetch: i64,
// pub diagnostics: RwLock<Vec<String>>,
}
#[allow(dead_code)]
impl ApplicationState {
pub fn new(db: DatabaseConnection) -> Result<ApplicationState, FetchError> {
return Ok(ApplicationState {
db,
panels: vec![],
sources: vec![],
metrics: vec![],
last_fetch: 0,
});
}
pub async fn fetch(&mut self) -> Result<(), sea_orm::DbErr> {
self.panels = entities::panels::Entity::find().all(&self.db).await?;
self.sources = entities::sources::Entity::find().all(&self.db).await?;
self.metrics = entities::metrics::Entity::find().all(&self.db).await?;
self.last_fetch = chrono::Utc::now().timestamp();
Ok(())
}
pub fn age(&self) -> i64 {
chrono::Utc::now().timestamp() - self.last_fetch
}
}

View file

@ -1,8 +1,8 @@
use eframe::{egui::{Ui, Layout, Sense, color_picker::show_color_at}, emath::Align, epaint::Color32}; use eframe::{egui::{Ui, Layout, Sense, color_picker::show_color_at, ComboBox, TextEdit}, emath::Align, epaint::Color32};
use crate::{data::entities, util::unpack_color}; use crate::{data::entities, util::unpack_color};
fn color_square(ui: &mut Ui, color:Color32) { fn _color_square(ui: &mut Ui, color:Color32) {
let size = ui.spacing().interact_size; let size = ui.spacing().interact_size;
let (rect, response) = ui.allocate_exact_size(size, Sense::click()); let (rect, response) = ui.allocate_exact_size(size, Sense::click());
if ui.is_rect_visible(rect) { if ui.is_rect_visible(rect) {
@ -17,9 +17,9 @@ fn color_square(ui: &mut Ui, color:Color32) {
} }
} }
pub fn metric_display_ui(ui: &mut Ui, metric: &entities::metrics::Model, _width: f32) { pub fn _metric_display_ui(ui: &mut Ui, metric: &entities::metrics::Model, _width: f32) {
ui.horizontal(|ui| { ui.horizontal(|ui| {
color_square(ui, unpack_color(metric.color)); _color_square(ui, unpack_color(metric.color));
ui.label(&metric.name); ui.label(&metric.name);
ui.with_layout(Layout::top_down(Align::RIGHT), |ui| { ui.with_layout(Layout::top_down(Align::RIGHT), |ui| {
ui.horizontal(|ui| { ui.horizontal(|ui| {
@ -34,32 +34,42 @@ pub fn metric_display_ui(ui: &mut Ui, metric: &entities::metrics::Model, _width:
} }
pub fn metric_edit_ui(ui: &mut Ui, metric: &entities::metrics::Model, panels: Option<&Vec<entities::panels::Model>>, width: f32) { pub fn metric_edit_ui(ui: &mut Ui, metric: &entities::metrics::Model, panels: Option<&Vec<entities::panels::Model>>, width: f32) {
let _text_width = width - 195.0; let text_width = width - 195.0;
let mut name = metric.name.clone();
let def_str = "".into();
let mut query_x = metric.query_x.as_ref().unwrap_or(&def_str).clone();
let mut query_y = metric.query_y.clone();
let mut panel_id = 0;
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.color_edit_button_srgba(&mut unpack_color(metric.color)); ui.color_edit_button_srgba(&mut unpack_color(metric.color));
// TextEdit::singleline(&mut metric.name) TextEdit::singleline(&mut name)
// .desired_width(text_width / 2.0) .interactive(false)
// .hint_text("name") .desired_width(text_width / 2.0)
// .show(ui); .hint_text("name")
.show(ui);
ui.separator(); ui.separator();
// TextEdit::singleline(&mut metric.query_x.unwrap_or("".into())) if query_x.len() > 0 {
// .desired_width(text_width / 4.0) TextEdit::singleline(&mut query_x)
// .hint_text("x") .interactive(false)
// .show(ui); .desired_width(text_width / 4.0)
// TextEdit::singleline(&mut metric.query_y) .hint_text("x")
// .desired_width(text_width / 4.0) .show(ui);
// .hint_text("y") }
// .show(ui); TextEdit::singleline(&mut query_y)
if let Some(_panels) = panels { .interactive(false)
// ComboBox::from_id_source(format!("panel-selector-{}", metric.id)) .desired_width(if query_x.len() > 0 { 0.0 } else { 15.0 } + (text_width / if query_x.len() > 0 { 4.0 } else { 2.0 }))
// .width(60.0) .hint_text("y")
// .selected_text(format!("panel: {:02}", metric.panel_id)) .show(ui);
// .show_ui(ui, |ui| { if let Some(panels) = panels {
// ui.selectable_value(&mut metric.panel_id, -1, "None"); ComboBox::from_id_source(format!("panel-selector-{}", metric.id))
// for p in panels { .width(60.0)
// ui.selectable_value(&mut metric.panel_id, p.id, p.name.as_str()); .selected_text(format!("panel: {:02}", metric.panel_id))
// } .show_ui(ui, |ui| {
// }); ui.selectable_value(&mut panel_id, -1, "None");
for p in panels {
ui.selectable_value(&mut panel_id, p.id, p.name.as_str());
}
});
} }
}); });
} }

View file

@ -4,10 +4,11 @@ pub mod metric;
mod scaffold; mod scaffold;
use chrono::Utc;
use eframe::egui::{CentralPanel, Context, SidePanel, TopBottomPanel}; use eframe::egui::{CentralPanel, Context, SidePanel, TopBottomPanel};
use tokio::sync::watch; use tokio::sync::watch;
use crate::data::entities; use crate::{data::entities, worker::visualizer::AppStateView};
use panel::main_content; use panel::main_content;
use scaffold::{ use scaffold::{
// confirmation_popup_delete_metric, confirmation_popup_delete_source, footer, // confirmation_popup_delete_metric, confirmation_popup_delete_source, footer,
@ -15,14 +16,17 @@ use scaffold::{
}; };
use source::source_panel; use source::source_panel;
pub struct App { use self::scaffold::footer;
panels_rx: watch::Receiver<Vec<entities::panels::Model>>,
panels: Vec<entities::panels::Model>,
view_tx: watch::Sender<i64>,
sources: watch::Receiver<Vec<entities::sources::Model>>, pub struct App {
metrics: watch::Receiver<Vec<entities::metrics::Model>>, view: AppStateView,
points: watch::Receiver<Vec<entities::points::Model>>, db_path: String,
interval: i64,
last_redraw: i64,
panels: Vec<entities::panels::Model>,
width_tx: watch::Sender<i64>,
logger_view: watch::Receiver<Vec<String>>,
buffer_panel: entities::panels::Model, buffer_panel: entities::panels::Model,
buffer_source: entities::sources::Model, buffer_source: entities::sources::Model,
@ -36,24 +40,34 @@ pub struct App {
impl App { impl App {
pub fn new( pub fn new(
_cc: &eframe::CreationContext, _cc: &eframe::CreationContext,
panels_rx: watch::Receiver<Vec<entities::panels::Model>>, db_path: String,
sources: watch::Receiver<Vec<entities::sources::Model>>, interval: i64,
metrics: watch::Receiver<Vec<entities::metrics::Model>>, view: AppStateView,
points: watch::Receiver<Vec<entities::points::Model>>, width_tx: watch::Sender<i64>,
view_tx: watch::Sender<i64>, logger_view: watch::Receiver<Vec<String>>,
) -> Self { ) -> Self {
let panels = panels_rx.borrow().clone(); let panels = view.panels.borrow().clone();
Self { Self {
panels_rx, panels, view_tx, db_path, interval, panels, width_tx, view, logger_view,
sources, metrics, points,
buffer_panel: entities::panels::Model::default(), buffer_panel: entities::panels::Model::default(),
buffer_source: entities::sources::Model::default(), buffer_source: entities::sources::Model::default(),
buffer_metric: entities::metrics::Model::default(), buffer_metric: entities::metrics::Model::default(),
last_redraw: 0,
edit: false, edit: false,
sidebar: true, sidebar: true,
padding: false, padding: false,
} }
} }
pub fn save_all_panels(&self) {
self.view.op.blocking_send(
crate::worker::visualizer::BackgroundAction::UpdateAllPanels { panels: self.panels.clone() }
).unwrap();
}
pub fn refresh_data(&self) {
self.view.flush.blocking_send(()).unwrap();
}
} }
impl eframe::App for App { impl eframe::App for App {
@ -62,8 +76,8 @@ impl eframe::App for App {
header(self, ui, frame); header(self, ui, frame);
}); });
TopBottomPanel::bottom("footer").show(ctx, |_ui| { TopBottomPanel::bottom("footer").show(ctx, |ui| {
// footer(self.data.clone(), ctx, ui); footer(ctx, ui, self.logger_view.clone(), self.db_path.clone(), self.view.points.borrow().len());
}); });
// if let Some(index) = self.deleting_metric { // if let Some(index) = self.deleting_metric {
@ -75,6 +89,10 @@ impl eframe::App for App {
// .show(ctx, |ui| confirmation_popup_delete_source(self, ui, index)); // .show(ctx, |ui| confirmation_popup_delete_source(self, ui, index));
// } // }
// for window in self.windows {
// }
if self.sidebar { if self.sidebar {
SidePanel::left("sources-bar") SidePanel::left("sources-bar")
.width_range(if self.edit { 400.0..=1000.0 } else { 280.0..=680.0 }) .width_range(if self.edit { 400.0..=1000.0 } else { 280.0..=680.0 })
@ -87,7 +105,12 @@ impl eframe::App for App {
}); });
if let Some(viewsize) = self.panels.iter().map(|p| p.view_size).max() { if let Some(viewsize) = self.panels.iter().map(|p| p.view_size).max() {
self.view_tx.send(viewsize as i64).unwrap(); self.width_tx.send(viewsize as i64).unwrap();
}
if Utc::now().timestamp() > self.last_redraw + self.interval {
ctx.request_repaint();
self.last_redraw = Utc::now().timestamp();
} }
} }
} }

View file

@ -1,6 +1,6 @@
use chrono::{Local, Utc}; use chrono::{Local, Utc};
use eframe::{egui::{ use eframe::{egui::{
plot::{Corner, GridMark, Legend, Line, Plot, PlotPoints, PlotPoint}, plot::{Corner, GridMark, Legend, Line, Plot},
Ui, ScrollArea, collapsing_header::CollapsingState, Context, Layout, Slider, DragValue, Ui, ScrollArea, collapsing_header::CollapsingState, Context, Layout, Slider, DragValue,
}, emath::Vec2}; }, emath::Vec2};
@ -14,7 +14,7 @@ pub fn main_content(app: &mut App, ctx: &Context, ui: &mut Ui) {
ScrollArea::vertical().show(ui, |ui| { ScrollArea::vertical().show(ui, |ui| {
let panels = &mut app.panels; let panels = &mut app.panels;
let _panels_count = panels.len(); let _panels_count = panels.len();
let metrics = app.metrics.borrow(); let metrics = app.view.metrics.borrow();
for (index, panel) in panels.iter_mut().enumerate() { for (index, panel) in panels.iter_mut().enumerate() {
if index > 0 { if index > 0 {
ui.separator(); // only show this if there is at least one panel ui.separator(); // only show this if there is at least one panel
@ -41,7 +41,7 @@ pub fn main_content(app: &mut App, ctx: &Context, ui: &mut Ui) {
// ui.separator(); // ui.separator();
panel_title_ui(ui, panel, app.edit); panel_title_ui(ui, panel, app.edit);
}) })
.body(|ui| panel_body_ui(ui, panel, &metrics, &app.points.borrow())); .body(|ui| panel_body_ui(ui, panel, &metrics, &app.view.points.borrow()));
} }
}); });
} }

View file

@ -1,8 +1,6 @@
use std::sync::Arc;
use eframe::{Frame, egui::{collapsing_header::CollapsingState, Context, Ui, Layout, ScrollArea, global_dark_light_mode_switch}, emath::Align}; use eframe::{Frame, egui::{collapsing_header::CollapsingState, Context, Ui, Layout, ScrollArea, global_dark_light_mode_switch}, emath::Align};
use tokio::sync::watch;
use crate::data::ApplicationState;
use crate::gui::App; use crate::gui::App;
use super::panel::panel_edit_inline_ui; use super::panel::panel_edit_inline_ui;
@ -68,16 +66,19 @@ pub fn header(app: &mut App, ui: &mut Ui, frame: &mut Frame) {
ui.checkbox(&mut app.sidebar, "sources"); ui.checkbox(&mut app.sidebar, "sources");
ui.separator(); ui.separator();
if ui.button("reset").clicked() { if ui.button("reset").clicked() {
app.panels = app.panels_rx.borrow().clone(); app.panels = app.view.panels.borrow().clone();
}
ui.separator();
if ui.button("save").clicked() {
app.save_all_panels();
}
ui.separator();
if ui.button("refresh").clicked() {
app.refresh_data();
} }
ui.separator(); ui.separator();
ui.checkbox(&mut app.edit, "edit"); ui.checkbox(&mut app.edit, "edit");
if app.edit { if app.edit {
if ui.button("save").clicked() {
// native_save(app.data.clone());
app.edit = false;
}
ui.separator();
ui.label("+ panel"); ui.label("+ panel");
panel_edit_inline_ui(ui, &mut app.buffer_panel); panel_edit_inline_ui(ui, &mut app.buffer_panel);
if ui.button("add").clicked() { if ui.button("add").clicked() {
@ -97,7 +98,7 @@ pub fn header(app: &mut App, ui: &mut Ui, frame: &mut Frame) {
}); });
} }
pub fn _footer(_data: Arc<ApplicationState>, ctx: &Context, ui: &mut Ui) { pub fn footer(ctx: &Context, ui: &mut Ui, diagnostics: watch::Receiver<Vec<String>>, db_path: String, records: usize) {
CollapsingState::load_with_default_open( CollapsingState::load_with_default_open(
ctx, ctx,
ui.make_persistent_id("footer-logs"), ui.make_persistent_id("footer-logs"),
@ -106,8 +107,9 @@ pub fn _footer(_data: Arc<ApplicationState>, ctx: &Context, ui: &mut Ui) {
.show_header(ui, |ui| { .show_header(ui, |ui| {
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.separator(); ui.separator();
// ui.label(data.file_path.to_str().unwrap()); // TODO maybe calculate it just once? ui.label(db_path); // TODO maybe calculate it just once?
ui.separator(); ui.separator();
ui.label(format!("{} records loaded", records)); // TODO put thousands separator
// ui.label(human_size( // ui.label(human_size(
// *data // *data
// .file_size // .file_size
@ -131,14 +133,10 @@ pub fn _footer(_data: Arc<ApplicationState>, ctx: &Context, ui: &mut Ui) {
.body(|ui| { .body(|ui| {
ui.set_height(200.0); ui.set_height(200.0);
ScrollArea::vertical().show(ui, |ui| { ScrollArea::vertical().show(ui, |ui| {
// let msgs = data
// .diagnostics
// .read()
// .expect("Diagnostics RwLock poisoned");
ui.separator(); ui.separator();
//for msg in msgs.iter() { for msg in diagnostics.borrow().iter() {
// ui.label(msg); ui.label(msg);
//} }
ui.separator(); ui.separator();
}); });
}); });

View file

@ -1,5 +1,5 @@
use eframe::{ use eframe::{
egui::{Layout, ScrollArea, Ui}, egui::{Layout, ScrollArea, Ui, DragValue, TextEdit, Checkbox},
emath::Align, emath::Align,
}; };
use rfd::FileDialog; use rfd::FileDialog;
@ -9,7 +9,7 @@ use crate::util::deserialize_values;
use crate::gui::App; use crate::gui::App;
use crate::data::entities; use crate::data::entities;
use super::metric::{metric_display_ui, metric_edit_ui}; use super::metric::metric_edit_ui;
pub fn source_panel(app: &mut App, ui: &mut Ui) { pub fn source_panel(app: &mut App, ui: &mut Ui) {
let mut source_to_put_metric_on : Option<i32> = None; let mut source_to_put_metric_on : Option<i32> = None;
@ -23,138 +23,126 @@ pub fn source_panel(app: &mut App, ui: &mut Ui) {
.show(ui, |ui| { .show(ui, |ui| {
// TODO only vertical! // TODO only vertical!
{ {
let sources = app.sources.borrow(); let sources = app.view.sources.borrow();
let sources_count = sources.len(); let sources_count = sources.len();
ui.heading("Sources"); ui.heading("Sources");
ui.separator(); ui.separator();
for (i, source) in sources.iter().enumerate() { for (i, source) in sources.iter().enumerate() {
ui.horizontal(|ui| { ui.horizontal(|ui| {
if app.edit { // show buttons to move sources up and down ui.vertical(|ui| {
ui.vertical(|ui| { ui.add_space(10.0);
ui.add_space(10.0); if ui.small_button("+").clicked() {
if ui.small_button("+").clicked() { if i > 0 {
if i > 0 { to_swap = Some(i); // TODO kinda jank but is there a better way?
to_swap = Some(i); // TODO kinda jank but is there a better way?
}
} }
if ui.small_button("").clicked() { }
if i < sources_count - 1 { if ui.small_button("").clicked() {
to_swap = Some(i + 1); // TODO kinda jank but is there a better way? if i < sources_count - 1 {
} to_swap = Some(i + 1); // TODO kinda jank but is there a better way?
} }
}); }
} });
ui.vertical(|ui| { // actual sources list container ui.vertical(|ui| { // actual sources list container
let remaining_width = ui.available_width(); let remaining_width = ui.available_width();
if app.edit { ui.group(|ui| {
ui.group(|ui| { ui.horizontal(|ui| {
ui.horizontal(|ui| { source_edit_ui(ui, source, remaining_width - 34.0);
source_edit_ui(ui, source, remaining_width - 34.0); if ui.small_button("×").clicked() {
if ui.small_button("×").clicked() { // app.deleting_metric = None;
// app.deleting_metric = None; // app.deleting_source = Some(i);
// app.deleting_source = Some(i);
}
});
let metrics = app
.metrics
.borrow();
for (_j, metric) in metrics.iter().enumerate() {
if metric.source_id == source.id {
ui.horizontal(|ui| {
metric_edit_ui(
ui,
metric,
Some(&panels),
remaining_width - 53.0,
);
if ui.small_button("s").clicked() {
let path = FileDialog::new()
.add_filter("csv", &["csv"])
.set_file_name(format!("{}-{}.csv", source.name, metric.name).as_str())
.save_file();
if let Some(_path) = path {
// serialize_values(
// &*metric
// .data
// .read()
// .expect("Values RwLock poisoned"),
// metric,
// path,
// )
// .expect("Could not serialize data");
}
}
if ui.small_button("×").clicked() {
// app.deleting_source = None;
// app.deleting_metric = Some(j);
}
});
}
} }
ui.horizontal(|ui| { });
metric_edit_ui( let metrics = app
ui, .view
&mut app.buffer_metric, .metrics
None, .borrow();
remaining_width - 53.0, for (_j, metric) in metrics.iter().enumerate() {
); if metric.source_id == source.id {
ui.add_space(2.0); ui.horizontal(|ui| {
if ui.small_button(" + ").clicked() { metric_edit_ui(
source_to_put_metric_on = Some(source.id); ui,
} metric,
ui.add_space(1.0); // DAMN! Some(&panels),
if ui.small_button("o").clicked() { remaining_width - 53.0,
let path = FileDialog::new() );
.add_filter("csv", &["csv"]) if ui.small_button("s").clicked() {
.pick_file(); let path = FileDialog::new()
if let Some(path) = path { .add_filter("csv", &["csv"])
match deserialize_values(path) { .set_file_name(format!("{}-{}.csv", source.name, metric.name).as_str())
Ok((_name, _query_x, _query_y, _data)) => { .save_file();
// let mut store = app if let Some(_path) = path {
// .data // serialize_values(
// .storage // &*metric
// .lock() // .data
// .expect("Storage Mutex poisoned"); // .read()
// match store.new_metric( // .expect("Values RwLock poisoned"),
// name.as_str(), // metric,
// source.id, // path,
// query_x.as_str(), // )
// query_y.as_str(), // .expect("Could not serialize data");
// -1, }
// Color32::TRANSPARENT, }
// metrics.len() as i32, if ui.small_button("×").clicked() {
// ) { // app.deleting_source = None;
// Ok(verified_metric) => { // app.deleting_metric = Some(j);
// store.put_values(verified_metric.id, &data).unwrap(); }
// *verified_metric.data.write().expect("Values RwLock poisoned") = data; });
// to_insert.push(verified_metric); }
// } }
// Err(e) => { ui.horizontal(|ui| {
// error!(target: "ui", "could not save metric into archive : {:?}", e); metric_edit_ui(
// } ui,
// } &mut app.buffer_metric,
} None,
Err(e) => { remaining_width - 53.0,
error!(target: "ui", "Could not deserialize metric from file : {:?}", e); );
} ui.add_space(2.0);
if ui.small_button(" + ").clicked() {
source_to_put_metric_on = Some(source.id);
}
ui.add_space(1.0); // DAMN!
if ui.small_button("o").clicked() {
let path = FileDialog::new()
.add_filter("csv", &["csv"])
.pick_file();
if let Some(path) = path {
match deserialize_values(path) {
Ok((_name, _query_x, _query_y, _data)) => {
// let mut store = app
// .data
// .storage
// .lock()
// .expect("Storage Mutex poisoned");
// match store.new_metric(
// name.as_str(),
// source.id,
// query_x.as_str(),
// query_y.as_str(),
// -1,
// Color32::TRANSPARENT,
// metrics.len() as i32,
// ) {
// Ok(verified_metric) => {
// store.put_values(verified_metric.id, &data).unwrap();
// *verified_metric.data.write().expect("Values RwLock poisoned") = data;
// to_insert.push(verified_metric);
// }
// Err(e) => {
// error!(target: "ui", "could not save metric into archive : {:?}", e);
// }
// }
}
Err(e) => {
error!(target: "ui", "Could not deserialize metric from file : {:?}", e);
} }
} }
} }
if ui.small_button("×").clicked() {
app.buffer_metric = entities::metrics::Model::default();
}
})
});
} else {
let metrics = app.metrics.borrow();
source_display_ui(ui, source, remaining_width);
for metric in metrics.iter() {
if metric.source_id == source.id {
metric_display_ui(ui, metric, ui.available_width());
} }
} if ui.small_button("×").clicked() {
ui.separator(); app.buffer_metric = entities::metrics::Model::default();
} }
})
});
}); });
}); });
} }
@ -200,7 +188,7 @@ pub fn source_panel(app: &mut App, ui: &mut Ui) {
// } // }
// } // }
if let Some(s) = source_to_put_metric_on { if let Some(s) = source_to_put_metric_on {
for source in app.sources.borrow().iter() { for source in app.view.sources.borrow().iter() {
if source.id == s { if source.id == s {
// if let Err(e) = // if let Err(e) =
// app.data.add_metric(&app.input_metric, &source) // app.data.add_metric(&app.input_metric, &source)
@ -212,29 +200,31 @@ pub fn source_panel(app: &mut App, ui: &mut Ui) {
} }
} }
pub fn source_display_ui(_ui: &mut Ui, _source: &entities::sources::Model, _width: f32) { pub fn _source_display_ui(ui: &mut Ui, source: &entities::sources::Model, _width: f32) {
// ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.heading(&source.name).on_hover_text(&source.url);
// ui.add_enabled(false, Checkbox::new(&mut source.enabled, "")); // ui.add_enabled(false, Checkbox::new(&mut source.enabled, ""));
// ui.add_enabled( });
// false,
// DragValue::new(&mut source.interval).clamp_range(1..=120),
// );
// ui.heading(&source.name).on_hover_text(&source.url);
// });
} }
pub fn source_edit_ui(_ui: &mut Ui, _source: &entities::sources::Model, _width: f32) { pub fn source_edit_ui(ui: &mut Ui, source: &entities::sources::Model, width: f32) {
// ui.horizontal(|ui| { let mut interval = source.interval.clone();
// let text_width = width - 100.0; let mut name = source.name.clone();
// ui.checkbox(&mut source.enabled, ""); let mut url = source.url.clone();
// ui.add(DragValue::new(&mut source.interval).clamp_range(1..=3600)); let mut enabled = source.enabled.clone();
// TextEdit::singleline(&mut source.name) ui.horizontal(|ui| {
// .desired_width(text_width / 4.0) let text_width = width - 100.0;
// .hint_text("name") ui.add_enabled(false, Checkbox::new(&mut enabled, ""));
// .show(ui); ui.add_enabled(false, DragValue::new(&mut interval).clamp_range(1..=3600));
// TextEdit::singleline(&mut source.url) TextEdit::singleline(&mut name)
// .desired_width(text_width * 3.0 / 4.0) .interactive(false)
// .hint_text("url") .desired_width(text_width / 4.0)
// .show(ui); .hint_text("name")
// }); .show(ui);
TextEdit::singleline(&mut url)
.interactive(false)
.desired_width(text_width * 3.0 / 4.0)
.hint_text("url")
.show(ui);
});
} }

View file

@ -5,14 +5,17 @@ mod worker;
use tracing::metadata::LevelFilter; use tracing::metadata::LevelFilter;
use tracing_subscriber::prelude::*; use tracing_subscriber::prelude::*;
use tracing::info; use tracing::{info, error};
use tracing_subscriber::filter::filter_fn; use tracing_subscriber::filter::filter_fn;
use eframe::egui::Context;
use clap::Parser; use clap::Parser;
use tokio::sync::watch; use tokio::sync::watch;
use sea_orm::Database; use sea_orm::Database;
use worker::{surveyor_loop, visualizer_loop}; use worker::visualizer::AppState;
use worker::surveyor_loop;
use util::InternalLogger;
use gui::{ use gui::{
// util::InternalLogger, // util::InternalLogger,
App App
@ -34,12 +37,16 @@ struct CliArgs {
gui: bool, gui: bool,
/// Check interval for background worker /// Check interval for background worker
#[arg(short, long, default_value_t = 5)] #[arg(short, long, default_value_t = 10)]
interval: u64, interval: u64,
/// How often sources and metrics are refreshed /// How often sources and metrics are refreshed
#[arg(short, long, default_value_t = 300)] #[arg(short, long, default_value_t = 300)]
cache_time: u64, cache_time: u64,
/// How many log lines to keep in memory
#[arg(short, long, default_value_t = 1000)]
log_size: u64,
} }
// When compiling natively: // When compiling natively:
@ -47,25 +54,31 @@ struct CliArgs {
fn main() { fn main() {
let args = CliArgs::parse(); let args = CliArgs::parse();
// TODO is there an alternative to this ugly botch?
let (ctx_tx, ctx_rx) = watch::channel::<Option<Context>>(None);
let (width_tx, width_rx) = watch::channel(0);
let (run_tx, run_rx) = watch::channel(true);
let logger = InternalLogger::new(args.log_size as usize);
let logger_view = logger.view();
tracing_subscriber::registry() tracing_subscriber::registry()
.with(LevelFilter::INFO) .with(LevelFilter::INFO)
.with(filter_fn(|x| x.target() != "sqlx::query")) .with(filter_fn(|x| x.target() != "sqlx::query"))
.with(tracing_subscriber::fmt::Layer::new()) .with(tracing_subscriber::fmt::Layer::new())
// .with(InternalLogger::new(store.clone())) .with(logger.layer())
.init(); .init();
// // Set default file location let state = AppState::new(
// let mut store_path = dirs::data_dir().unwrap_or(PathBuf::from(".")); // TODO get cwd more consistently? width_rx,
// store_path.push("dashboard.db"); args.interval as i64,
// let store = args.cache_time as i64,
// Arc::new(ApplicationState::new(store_path).expect("Failed creating application state")); ).unwrap();
let (panel_tx, panel_rx) = watch::channel(vec![]); let view = state.view();
let (source_tx, source_rx) = watch::channel(vec![]); let run_rx_clone = run_rx.clone();
let (metric_tx, metric_rx) = watch::channel(vec![]); let db_uri = args.db.clone();
let (point_tx, point_rx) = watch::channel(vec![]);
let (view_tx, view_rx) = watch::channel(1440);
let worker = std::thread::spawn(move || { let worker = std::thread::spawn(move || {
tokio::runtime::Builder::new_current_thread() tokio::runtime::Builder::new_current_thread()
@ -73,11 +86,28 @@ fn main() {
.build() .build()
.unwrap() .unwrap()
.block_on(async { .block_on(async {
let db = Database::connect(args.db.clone()).await.unwrap(); let db = Database::connect(db_uri.clone()).await.unwrap();
info!(target: "launcher", "Connected to '{}'", args.db); info!(target: "launcher", "Connected to '{}'", db_uri);
let mut jobs = vec![]; let mut jobs = vec![];
let run_rx_clone_clone = run_rx_clone.clone();
jobs.push(
tokio::spawn(async move {
while *run_rx_clone_clone.borrow() {
if let Some(ctx) = &*ctx_rx.borrow() {
ctx.request_repaint();
}
tokio::time::sleep(std::time::Duration::from_secs(args.interval)).await;
}
})
);
jobs.push(
tokio::spawn(logger.worker(run_rx_clone.clone()))
);
if args.worker { if args.worker {
jobs.push( jobs.push(
tokio::spawn( tokio::spawn(
@ -85,6 +115,7 @@ fn main() {
db.clone(), db.clone(),
args.interval as i64, args.interval as i64,
args.cache_time as i64, args.cache_time as i64,
run_rx_clone.clone(),
) )
) )
); );
@ -93,16 +124,7 @@ fn main() {
if args.gui { if args.gui {
jobs.push( jobs.push(
tokio::spawn( tokio::spawn(
visualizer_loop( state.worker(db, run_rx_clone.clone())
db.clone(),
args.interval,
args.cache_time as i64,
panel_tx,
source_tx,
metric_tx,
point_tx,
view_rx,
)
) )
); );
} }
@ -116,23 +138,34 @@ fn main() {
if args.gui { if args.gui {
let native_options = eframe::NativeOptions::default(); let native_options = eframe::NativeOptions::default();
info!(target: "launcher", "Starting native GUI");
eframe::run_native( eframe::run_native(
// TODO replace this with a loop that ends so we can cleanly exit the background worker // TODO replace this with a loop that ends so we can cleanly exit the background worker
"dashboard", "dashboard",
native_options, native_options,
Box::new( Box::new(
move |cc| Box::new( move |cc| {
App::new( ctx_tx.send(Some(cc.egui_ctx.clone())).unwrap_or_else(|_| {
cc, error!(target: "launcher", "Could not share reference to egui context (won't be able to periodically refresh window)");
panel_rx, });
source_rx, Box::new(
metric_rx, App::new(
point_rx, cc,
view_tx, args.db,
args.interval as i64,
view,
width_tx,
logger_view,
)
) )
) }
), ),
); );
info!(target: "launcher", "Stopping native GUI");
run_tx.send(false).unwrap();
} }
worker.join().unwrap(); worker.join().unwrap();

View file

@ -1,9 +1,10 @@
use chrono::{DateTime, Local, NaiveDateTime, Utc}; use chrono::{DateTime, Local, NaiveDateTime, Utc};
use eframe::egui::{Color32, plot::PlotPoint}; use eframe::egui::{Color32, plot::PlotPoint};
use std::{sync::Arc, error::Error, path::PathBuf}; use tokio::sync::{watch, mpsc};
use std::{error::Error, path::PathBuf, collections::VecDeque};
use tracing_subscriber::Layer; use tracing_subscriber::Layer;
use super::data::{ApplicationState, entities}; use super::data::entities;
// if you're handling more than terabytes of data, it's the future and you ought to update this code! // 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"];
@ -51,7 +52,8 @@ pub fn deserialize_values(path: PathBuf) -> Result<(String, String, String, Vec<
)) ))
} }
pub fn _human_size(size: u64) -> String { #[allow(dead_code)]
pub fn human_size(size: u64) -> String {
let mut buf: f64 = size as f64; let mut buf: f64 = size as f64;
let mut prefix: usize = 0; let mut prefix: usize = 0;
while buf > 1024.0 && prefix < _PREFIXES.len() - 1 { while buf > 1024.0 && prefix < _PREFIXES.len() - 1 {
@ -101,16 +103,62 @@ pub fn repack_color(c: Color32) -> u32 {
} }
pub struct InternalLogger { pub struct InternalLogger {
_state: Arc<ApplicationState>, size: usize,
view_tx: watch::Sender<Vec<String>>,
view_rx: watch::Receiver<Vec<String>>,
msg_tx : mpsc::UnboundedSender<String>,
msg_rx : mpsc::UnboundedReceiver<String>,
} }
impl InternalLogger { impl InternalLogger {
pub fn _new(state: Arc<ApplicationState>) -> Self { pub fn new(size: usize) -> Self {
InternalLogger { _state: state } let (view_tx, view_rx) = watch::channel(vec![]);
let (msg_tx, msg_rx) = mpsc::unbounded_channel();
InternalLogger { size, view_tx, view_rx, msg_tx, msg_rx }
}
pub fn view(&self) -> watch::Receiver<Vec<String>> {
self.view_rx.clone()
}
pub fn layer(&self) -> InternalLoggerLayer {
InternalLoggerLayer::new(self.msg_tx.clone())
}
pub async fn worker(mut self, run: watch::Receiver<bool>) {
let mut messages = VecDeque::new();
while *run.borrow() {
tokio::select!{
msg = self.msg_rx.recv() => {
match msg {
Some(msg) => {
messages.push_back(msg);
while messages.len() > self.size {
messages.pop_front();
}
self.view_tx.send(messages.clone().into()).unwrap();
},
None => break,
}
},
_ = tokio::time::sleep(std::time::Duration::from_secs(1)) => {},
// unblock so it checks again run and exits cleanly
}
}
} }
} }
impl<S> Layer<S> for InternalLogger pub struct InternalLoggerLayer {
msg_tx: mpsc::UnboundedSender<String>,
}
impl InternalLoggerLayer {
pub fn new(msg_tx: mpsc::UnboundedSender<String>) -> Self {
InternalLoggerLayer { msg_tx }
}
}
impl<S> Layer<S> for InternalLoggerLayer
where where
S: tracing::Subscriber, S: tracing::Subscriber,
{ {
@ -123,18 +171,15 @@ where
msg: "".to_string(), msg: "".to_string(),
}; };
event.record(&mut msg_visitor); event.record(&mut msg_visitor);
let _out = format!( let out = format!(
"{} [{}] {}: {}", "{} [{}] {}: {}",
Local::now().format("%H:%M:%S"), Local::now().format("%H:%M:%S"),
event.metadata().level(), event.metadata().level(),
event.metadata().target(), event.metadata().target(),
msg_visitor.msg msg_visitor.msg
); );
// self.state
// .diagnostics self.msg_tx.send(out).unwrap_or_default();
// .write()
// .expect("Diagnostics RwLock poisoned")
// .push(out);
} }
} }

View file

@ -1,159 +0,0 @@
use chrono::Utc;
use sea_orm::{DatabaseConnection, ActiveValue::NotSet, Set, EntityTrait, Condition, ColumnTrait, QueryFilter};
use tokio::sync::watch;
use tracing::{error, info};
use std::collections::VecDeque;
use super::data::{entities, FetchError};
async fn fetch(url: &str) -> Result<serde_json::Value, FetchError> {
Ok(reqwest::get(url).await?.json().await?)
}
pub async fn surveyor_loop(
db: DatabaseConnection,
interval:i64,
cache_time:i64,
) {
let mut last_check = Utc::now().timestamp();
let mut last_fetch = 0;
let mut sources = vec![];
let mut metrics = vec![];
loop {
// sleep until next activation
let delta_time = (interval as i64) - (Utc::now().timestamp() - last_check);
if delta_time > 0 {
std::thread::sleep(std::time::Duration::from_secs(delta_time as u64));
}
last_check = Utc::now().timestamp();
if Utc::now().timestamp() - last_fetch > cache_time {
// TODO do both concurrently
let res = tokio::join!(
entities::sources::Entity::find().all(&db),
entities::metrics::Entity::find().all(&db)
);
sources = res.0.unwrap();
metrics = res.1.unwrap();
last_fetch = Utc::now().timestamp();
}
for source in sources.iter_mut() {
if !source.enabled || !source.ready() {
continue;
}
// source.last_fetch = Utc::now(); // TODO! do it in background threads again!
// tokio::spawn(async move {
match fetch(&source.url).await {
Ok(res) => {
let now = Utc::now().timestamp();
entities::sources::Entity::update(
entities::sources::ActiveModel{id: Set(source.id), last_update: Set(now), ..Default::default()}
).exec(&db).await.unwrap();
source.last_update = now;
for metric in metrics.iter().filter(|x| source.id == x.source_id) {
match metric.extract(&res) {
Ok(v) => {
entities::points::Entity::insert(
entities::points::ActiveModel {
id: NotSet, metric_id: Set(metric.id), x: Set(v.x), y: Set(v.y),
}).exec(&db).await.unwrap();
},
Err(e) => error!(target: "worker", "Failed extracting '{}' from {}: {:?}", metric.name, source.name, e),
}
}
},
Err(e) => error!(target: "worker", "Failed fetching {}: {:?}", source.name, e),
}
// source.last_fetch = Utc::now(); // TODO!
// });
}
// if let Ok(meta) = std::fs::metadata(state.file_path.clone()) {
// let mut fsize = state.file_size.write().expect("File Size RwLock poisoned");
// *fsize = meta.len();
// } // ignore errors
// ctx.request_repaint();
}
}
pub async fn visualizer_loop(
db: DatabaseConnection,
interval: u64,
cache_time: i64,
panels_tx: watch::Sender<Vec<entities::panels::Model>>,
sources_tx: watch::Sender<Vec<entities::sources::Model>>,
metrics_tx: watch::Sender<Vec<entities::metrics::Model>>,
points_tx: watch::Sender<Vec<entities::points::Model>>,
view_rx: watch::Receiver<i64>,
) {
let mut points : VecDeque<entities::points::Model> = VecDeque::new();
let mut last_fetch = 0;
let mut width = *view_rx.borrow() * 60; // TODO it's in minutes somewhere...
let mut lower = Utc::now().timestamp() - width;
let mut changes;
loop {
if Utc::now().timestamp() - last_fetch >= cache_time {
panels_tx.send(entities::panels::Entity::find().all(&db).await.unwrap()).unwrap();
sources_tx.send(entities::sources::Entity::find().all(&db).await.unwrap()).unwrap();
metrics_tx.send(entities::metrics::Entity::find().all(&db).await.unwrap()).unwrap();
last_fetch = Utc::now().timestamp();
info!(target: "worker", "Updated panels, sources and metrics");
}
changes = false;
let now = Utc::now().timestamp();
let new_width = *view_rx.borrow() * 60; // TODO it's in minutes somewhere...
if new_width != width {
let mut lower_points = entities::points::Entity::find()
.filter(
Condition::all()
.add(entities::points::Column::X.gte(now - new_width))
.add(entities::points::Column::X.lte(now - width))
)
.all(&db)
.await.unwrap();
lower_points.reverse(); // TODO wasteful!
for p in lower_points {
points.push_front(p);
changes = true;
}
}
width = new_width;
let new_points = entities::points::Entity::find()
.filter(
Condition::all()
.add(entities::points::Column::X.gte(lower as f64))
)
.all(&db)
.await.unwrap();
lower = Utc::now().timestamp();
while let Some(p) = points.get(0) {
if (p.x as i64) >= lower - (*view_rx.borrow() * 60) {
break;
}
points.pop_front();
changes = true;
}
for p in new_points {
points.push_back(p);
changes = true;
}
if changes {
points_tx.send(points.clone().into()).unwrap();
}
tokio::time::sleep(std::time::Duration::from_secs(interval)).await;
}
}

5
src/worker/mod.rs Normal file
View file

@ -0,0 +1,5 @@
pub mod surveyor;
pub mod visualizer;
pub use surveyor::surveyor_loop;
pub use visualizer::{AppState, AppStateView, BackgroundAction};

81
src/worker/surveyor.rs Normal file
View file

@ -0,0 +1,81 @@
use chrono::Utc;
use sea_orm::{DatabaseConnection, ActiveValue::NotSet, Set, EntityTrait};
use tokio::sync::watch;
use tracing::error;
use crate::data::{entities, FetchError};
async fn fetch(url: &str) -> Result<serde_json::Value, FetchError> {
Ok(reqwest::get(url).await?.json().await?)
}
pub async fn surveyor_loop(
db: DatabaseConnection,
interval:i64,
cache_time:i64,
run: watch::Receiver<bool>,
) {
let mut last_check = Utc::now().timestamp();
let mut last_fetch = 0;
let mut sources = vec![];
let mut metrics = vec![];
while *run.borrow() {
// sleep until next activation
let delta_time = (interval as i64) - (Utc::now().timestamp() - last_check);
if delta_time > 0 {
tokio::time::sleep(std::time::Duration::from_secs(delta_time as u64)).await;
}
last_check = Utc::now().timestamp();
if Utc::now().timestamp() - last_fetch > cache_time {
// TODO do both concurrently
let res = tokio::join!(
entities::sources::Entity::find().all(&db),
entities::metrics::Entity::find().all(&db)
);
sources = res.0.unwrap();
metrics = res.1.unwrap();
last_fetch = Utc::now().timestamp();
}
for source in sources.iter_mut() {
if !source.enabled || !source.ready() {
continue;
}
// source.last_fetch = Utc::now(); // TODO! do it in background threads again!
// tokio::spawn(async move {
match fetch(&source.url).await {
Ok(res) => {
let now = Utc::now().timestamp();
entities::sources::Entity::update(
entities::sources::ActiveModel{id: Set(source.id), last_update: Set(now), ..Default::default()}
).exec(&db).await.unwrap();
source.last_update = now;
for metric in metrics.iter().filter(|x| source.id == x.source_id) {
match metric.extract(&res) {
Ok(v) => {
entities::points::Entity::insert(
entities::points::ActiveModel {
id: NotSet, metric_id: Set(metric.id), x: Set(v.x), y: Set(v.y),
}).exec(&db).await.unwrap();
},
Err(e) => error!(target: "worker", "Failed extracting '{}' from {}: {:?}", metric.name, source.name, e),
}
}
},
Err(e) => error!(target: "worker", "Failed fetching {}: {:?}", source.name, e),
}
// source.last_fetch = Utc::now(); // TODO!
// });
}
// if let Ok(meta) = std::fs::metadata(state.file_path.clone()) {
// let mut fsize = state.file_size.write().expect("File Size RwLock poisoned");
// *fsize = meta.len();
// } // ignore errors
// ctx.request_repaint();
}
}

251
src/worker/visualizer.rs Normal file
View file

@ -0,0 +1,251 @@
use chrono::Utc;
use sea_orm::{DatabaseConnection, EntityTrait, Condition, ColumnTrait, QueryFilter, Set};
use tokio::sync::{watch, mpsc};
use tracing::info;
use std::collections::VecDeque;
use crate::data::{entities, FetchError};
#[derive(Clone)]
pub struct AppStateView {
pub panels: watch::Receiver<Vec<entities::panels::Model>>,
pub sources: watch::Receiver<Vec<entities::sources::Model>>,
pub metrics: watch::Receiver<Vec<entities::metrics::Model>>,
pub points: watch::Receiver<Vec<entities::points::Model>>,
pub flush: mpsc::Sender<()>,
pub op: mpsc::Sender<BackgroundAction>,
}
impl AppStateView {
pub async fn _request_flush(&self) -> bool {
match self.flush.send(()).await {
Ok(_) => true,
Err(_) => false,
}
}
}
struct AppStateTransmitters {
panels: watch::Sender<Vec<entities::panels::Model>>,
sources: watch::Sender<Vec<entities::sources::Model>>,
metrics: watch::Sender<Vec<entities::metrics::Model>>,
points: watch::Sender<Vec<entities::points::Model>>,
}
pub struct AppState {
tx: AppStateTransmitters,
panels: Vec<entities::panels::Model>,
sources: Vec<entities::sources::Model>,
metrics: Vec<entities::metrics::Model>,
last_refresh: i64,
points: VecDeque<entities::points::Model>,
last_check: i64,
flush: mpsc::Receiver<()>,
op: mpsc::Receiver<BackgroundAction>,
interval: i64,
cache_age: i64,
width: watch::Receiver<i64>,
view: AppStateView,
}
async fn sleep(t:i64) {
if t > 0 {
tokio::time::sleep(std::time::Duration::from_secs(t as u64)).await
}
}
impl AppState {
pub fn new(
width: watch::Receiver<i64>,
interval: i64,
cache_age: i64,
) -> Result<AppState, FetchError> {
let (panel_tx, panel_rx) = watch::channel(vec![]);
let (source_tx, source_rx) = watch::channel(vec![]);
let (metric_tx, metric_rx) = watch::channel(vec![]);
let (point_tx, point_rx) = watch::channel(vec![]);
// let (view_tx, view_rx) = watch::channel(0);
let (flush_tx, flush_rx) = mpsc::channel(10);
let (op_tx, op_rx) = mpsc::channel(100);
Ok(AppState {
panels: vec![],
sources: vec![],
metrics: vec![],
last_refresh: 0,
points: VecDeque::new(),
last_check: 0,
flush: flush_rx,
op: op_rx,
view: AppStateView {
panels: panel_rx,
sources: source_rx,
metrics: metric_rx,
points: point_rx,
flush: flush_tx,
op: op_tx,
},
tx: AppStateTransmitters {
panels: panel_tx,
sources: source_tx,
metrics: metric_tx,
points: point_tx,
},
width,
interval,
cache_age,
})
}
pub fn view(&self) -> AppStateView {
self.view.clone()
}
pub async fn fetch(&mut self, db: &DatabaseConnection) -> Result<(), sea_orm::DbErr> {
// TODO parallelize all this stuff
self.panels = entities::panels::Entity::find().all(db).await?;
self.tx.panels.send(self.panels.clone()).unwrap();
self.sources = entities::sources::Entity::find().all(db).await?;
self.tx.sources.send(self.sources.clone()).unwrap();
self.metrics = entities::metrics::Entity::find().all(db).await?;
self.tx.metrics.send(self.metrics.clone()).unwrap();
info!(target: "worker", "Updated panels, sources and metrics");
self.last_refresh = chrono::Utc::now().timestamp();
Ok(())
}
pub fn _cache_age(&self) -> i64 {
chrono::Utc::now().timestamp() - self.last_refresh
}
pub async fn worker(mut self, db: DatabaseConnection, run:watch::Receiver<bool>) {
let mut width = *self.width.borrow() * 60; // TODO it's in minutes somewhere...
let mut last = Utc::now().timestamp() - width;
while *run.borrow() {
let now = Utc::now().timestamp();
tokio::select!{
op = self.op.recv() => {
match op {
Some(op) => {
match op {
BackgroundAction::UpdateAllPanels { panels } => {
// TODO this is kinda rough, can it be done better?
entities::panels::Entity::delete_many().exec(&db).await.unwrap();
entities::panels::Entity::insert_many(
panels.iter().map(|v| entities::panels::ActiveModel{
id: Set(v.id),
name: Set(v.name.clone()),
view_scroll: Set(v.view_scroll),
view_size: Set(v.view_size),
timeserie: Set(v.timeserie),
height: Set(v.height),
limit_view: Set(v.limit_view),
position: Set(v.position),
reduce_view: Set(v.reduce_view),
view_chunks: Set(v.view_chunks),
shift_view: Set(v.shift_view),
view_offset: Set(v.view_offset),
average_view: Set(v.average_view),
}).collect::<Vec<entities::panels::ActiveModel>>()
).exec(&db).await.unwrap();
self.tx.panels.send(panels.clone()).unwrap();
self.panels = panels;
},
// _ => todo!(),
}
},
None => {},
}
},
_ = self.flush.recv() => {
let now = Utc::now().timestamp();
self.fetch(&db).await.unwrap();
let new_width = *self.width.borrow() * 60; // TODO it's in minutes somewhere...
self.points = entities::points::Entity::find()
.filter(
Condition::all()
.add(entities::points::Column::X.gte((now - new_width) as f64))
.add(entities::points::Column::X.lte(now as f64))
)
.all(&db)
.await.unwrap().into();
self.tx.points.send(self.points.clone().into()).unwrap();
last = Utc::now().timestamp();
info!(target: "worker", "Reloaded points");
},
_ = sleep(self.cache_age - (now - self.last_refresh)) => self.fetch(&db).await.unwrap(),
_ = sleep(self.interval - (now - self.last_check)) => {
let mut changes = false;
let now = Utc::now().timestamp();
let new_width = *self.width.borrow() * 60; // TODO it's in minutes somewhere...
// fetch previous points
if new_width != width {
let mut previous_points = entities::points::Entity::find()
.filter(
Condition::all()
.add(entities::points::Column::X.gte(now - new_width))
.add(entities::points::Column::X.lte(now - width))
)
.all(&db)
.await.unwrap();
info!(target: "worker", "Fetched {} previous points", previous_points.len());
previous_points.reverse(); // TODO wasteful!
for p in previous_points {
self.points.push_front(p);
changes = true;
}
}
// fetch new points
let new_points = entities::points::Entity::find()
.filter(
Condition::all()
.add(entities::points::Column::X.gte(last as f64))
.add(entities::points::Column::X.lte(now as f64))
)
.all(&db)
.await.unwrap();
info!(target: "worker", "Fetched {} new points", new_points.len());
for p in new_points {
self.points.push_back(p);
changes = true;
}
// remove old points
while let Some(p) = self.points.get(0) {
if (p.x as i64) >= now - (*self.width.borrow() * 60) {
break;
}
self.points.pop_front();
changes = true;
}
// update
last = now;
width = new_width;
self.last_check = now;
if changes {
self.tx.points.send(self.points.clone().into()).unwrap();
}
},
};
}
}
}
#[derive(Debug)]
pub enum BackgroundAction {
UpdateAllPanels { panels: Vec<entities::panels::Model> },
// UpdatePanel { panel : entities::panels::ActiveModel },
// UpdateSource { source: entities::sources::ActiveModel },
// UpdateMetric { metric: entities::metrics::ActiveModel },
}