diff --git a/migration/src/lib.rs b/migration/src/lib.rs index 66f2e28..606065e 100644 --- a/migration/src/lib.rs +++ b/migration/src/lib.rs @@ -2,6 +2,8 @@ pub use sea_orm_migration::prelude::*; mod m20220101_000001_create_table; mod m20221030_192706_add_last_update; +mod m20221102_232244_add_join_table; +mod m20221102_232858_remove_unused_columns; pub struct Migrator; @@ -9,8 +11,10 @@ pub struct Migrator; impl MigratorTrait for Migrator { fn migrations() -> Vec> { vec![ - Box::new(m20220101_000001_create_table::Migration), - Box::new(m20221030_192706_add_last_update::Migration), - ] + Box::new(m20220101_000001_create_table::Migration), + Box::new(m20221030_192706_add_last_update::Migration), + Box::new(m20221102_232244_add_join_table::Migration), + Box::new(m20221102_232858_remove_unused_columns::Migration), + ] } } diff --git a/migration/src/m20221102_232244_add_join_table.rs b/migration/src/m20221102_232244_add_join_table.rs new file mode 100644 index 0000000..10b86f7 --- /dev/null +++ b/migration/src/m20221102_232244_add_join_table.rs @@ -0,0 +1,40 @@ +use sea_orm_migration::prelude::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .table(PanelMetric::Table) + .if_not_exists() + .col( + ColumnDef::new(PanelMetric::Id) + .big_integer() + .not_null() + .auto_increment() + .primary_key(), + ) + .col(ColumnDef::new(PanelMetric::PanelId).big_integer().not_null()) + .col(ColumnDef::new(PanelMetric::MetricId).big_integer().not_null()) + .to_owned(), + ).await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(PanelMetric::Table).to_owned()) + .await + } +} + +#[derive(Iden)] +enum PanelMetric { + Table, + Id, + PanelId, + MetricId, +} diff --git a/migration/src/m20221102_232858_remove_unused_columns.rs b/migration/src/m20221102_232858_remove_unused_columns.rs new file mode 100644 index 0000000..f637149 --- /dev/null +++ b/migration/src/m20221102_232858_remove_unused_columns.rs @@ -0,0 +1,85 @@ +use sea_orm_migration::prelude::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager. + alter_table( + Table::alter() + .table(Panels::Table) + .drop_column(Panels::Width) + .drop_column(Panels::LimitView) + .drop_column(Panels::ShiftView) + .to_owned() + ) + .await?; + manager. + alter_table( + Table::alter() + .table(Metrics::Table) + .drop_column(Metrics::PanelId) + .to_owned() + ) + .await?; + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .alter_table( + Table::alter() + .table(Panels::Table) + .add_column( + ColumnDef::new(Panels::Width) + .integer() + .not_null() + .default(100) + ) + .add_column( + ColumnDef::new(Panels::LimitView) + .boolean() + .not_null() + .default(true) + ) + .add_column( + ColumnDef::new(Panels::ShiftView) + .boolean() + .not_null() + .default(false) + ) + .to_owned() + ) + .await?; + manager + .alter_table( + Table::alter() + .table(Metrics::Table) + .add_column( + ColumnDef::new(Metrics::PanelId) + .big_integer() + .not_null() + .default(0) + ) + .to_owned() + ) + .await?; + Ok(()) + } +} + +#[derive(Iden)] +enum Panels { + Table, + Width, + LimitView, + ShiftView, +} + +#[derive(Iden)] +enum Metrics { + Table, + PanelId, +} diff --git a/src/data/entities/metrics.rs b/src/data/entities/metrics.rs index 16d9ccb..9925972 100644 --- a/src/data/entities/metrics.rs +++ b/src/data/entities/metrics.rs @@ -15,17 +15,43 @@ pub struct Model { pub source_id: i64, pub query_x: String, pub query_y: String, - pub panel_id: i64, pub color: i32, pub position: i32, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} +pub enum Relation { + #[sea_orm( + belongs_to = "super::sources::Entity", + from = "Column::SourceId", + to = "super::sources::Column::Id" + )] + Source, + + #[sea_orm(has_many = "super::points::Entity")] + Point, +} + +impl Related for Entity { + fn to() -> RelationDef { Relation::Source.def() } +} + +impl Related for Entity { + fn to() -> RelationDef { Relation::Point.def() } +} + +impl Related for Entity { + fn to() -> RelationDef { + super::panel_metric::Relation::Panel.def() + } + + fn via() -> Option { + Some(super::panel_metric::Relation::Metric.def().rev()) + } +} impl ActiveModelBehavior for ActiveModel {} - impl Model { pub fn extract(&self, value: &serde_json::Value) -> Result { let x: f64; @@ -51,7 +77,6 @@ impl Default for Model { source_id: 0, query_x: "".into(), query_y: "".into(), - panel_id: 0, color: 0, position: 0, } diff --git a/src/data/entities/mod.rs b/src/data/entities/mod.rs index ebcfbac..ce9f081 100644 --- a/src/data/entities/mod.rs +++ b/src/data/entities/mod.rs @@ -2,7 +2,8 @@ pub mod prelude; -pub mod metrics; pub mod panels; +pub mod panel_metric; +pub mod metrics; pub mod points; pub mod sources; diff --git a/src/data/entities/panel_metric.rs b/src/data/entities/panel_metric.rs new file mode 100644 index 0000000..5cc8562 --- /dev/null +++ b/src/data/entities/panel_metric.rs @@ -0,0 +1,29 @@ +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm(table_name = "panel_metric")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = true)] + pub id: i64, + pub panel_id: i64, + pub metric_id: i64, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::panels::Entity", + from = "Column::PanelId", + to = "super::panels::Column::Id" + )] + Panel, + + #[sea_orm( + belongs_to = "super::metrics::Entity", + from = "Column::MetricId", + to = "super::metrics::Column::Id" + )] + Metric, +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/src/data/entities/panels.rs b/src/data/entities/panels.rs index 17c2f6f..dc9bd68 100644 --- a/src/data/entities/panels.rs +++ b/src/data/entities/panels.rs @@ -12,11 +12,9 @@ pub struct Model { pub view_size: i32, pub timeserie: bool, pub height: i32, - pub limit_view: bool, pub position: i32, pub reduce_view: bool, pub view_chunks: i32, - pub shift_view: bool, pub view_offset: i32, pub average_view: bool, } @@ -24,6 +22,16 @@ pub struct Model { #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] pub enum Relation {} +impl Related for Entity { + fn to() -> RelationDef { + super::panel_metric::Relation::Metric.def() + } + + fn via() -> Option { + Some(super::panel_metric::Relation::Panel.def().rev()) + } +} + impl ActiveModelBehavior for ActiveModel {} impl Default for Model { @@ -35,11 +43,9 @@ impl Default for Model { view_size: 1000, timeserie: true, height: 100, - limit_view: true, position: 0, reduce_view: false, view_chunks: 10, - shift_view: false, view_offset: 0, average_view: true, } diff --git a/src/data/entities/points.rs b/src/data/entities/points.rs index 66b972b..a96746f 100644 --- a/src/data/entities/points.rs +++ b/src/data/entities/points.rs @@ -13,7 +13,18 @@ pub struct Model { } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} +pub enum Relation { + #[sea_orm( + belongs_to = "super::metrics::Entity", + from = "Column::MetricId", + to = "super::metrics::Column::Id" + )] + Metric, +} + +impl Related for Entity { + fn to() -> RelationDef { Relation::Metric.def() } +} impl ActiveModelBehavior for ActiveModel {} diff --git a/src/data/entities/prelude.rs b/src/data/entities/prelude.rs index 295401c..f716e24 100644 --- a/src/data/entities/prelude.rs +++ b/src/data/entities/prelude.rs @@ -4,3 +4,4 @@ pub use super::metrics::Entity as Metrics; pub use super::panels::Entity as Panels; pub use super::points::Entity as Points; pub use super::sources::Entity as Sources; +pub use super::panel_metric::Entity as PanelMetric; diff --git a/src/data/entities/sources.rs b/src/data/entities/sources.rs index 6698571..4ac72d3 100644 --- a/src/data/entities/sources.rs +++ b/src/data/entities/sources.rs @@ -15,7 +15,16 @@ pub struct Model { } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} +pub enum Relation { + #[sea_orm(has_many = "super::metrics::Entity")] + Metric, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Metric.def() + } +} impl ActiveModelBehavior for ActiveModel {} diff --git a/src/gui/metric.rs b/src/gui/metric.rs index 92054ec..1cdb642 100644 --- a/src/gui/metric.rs +++ b/src/gui/metric.rs @@ -2,7 +2,7 @@ use eframe::{egui::{Ui, Layout, Sense, color_picker::show_color_at, ComboBox, Te 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 (rect, response) = ui.allocate_exact_size(size, Sense::click()); if ui.is_rect_visible(rect) { @@ -19,11 +19,11 @@ fn _color_square(ui: &mut Ui, color:Color32) { pub fn _metric_display_ui(ui: &mut Ui, metric: &entities::metrics::Model, _width: f32) { ui.horizontal(|ui| { - _color_square(ui, unpack_color(metric.color)); + color_square(ui, unpack_color(metric.color)); ui.label(&metric.name); ui.with_layout(Layout::top_down(Align::RIGHT), |ui| { ui.horizontal(|ui| { - ui.label(format!("panel: {}", metric.panel_id)); + ui.label("panel: ???"); ui.label(format!("y: {}", metric.query_y)); // if let Some(query_x) = metric.query_x { // ui.label(format!("x: {}", query_x)); @@ -40,7 +40,8 @@ pub fn metric_edit_ui(ui: &mut Ui, metric: &entities::metrics::Model, panels: Op let mut query_y = metric.query_y.clone(); let mut panel_id = 0; ui.horizontal(|ui| { - ui.color_edit_button_srgba(&mut unpack_color(metric.color)); + // ui.color_edit_button_srgba(&mut unpack_color(metric.color)); + color_square(ui, unpack_color(metric.color)); TextEdit::singleline(&mut name) .interactive(false) .desired_width(text_width / 2.0) @@ -62,7 +63,7 @@ pub fn metric_edit_ui(ui: &mut Ui, metric: &entities::metrics::Model, panels: Op if let Some(panels) = panels { ComboBox::from_id_source(format!("panel-selector-{}", metric.id)) .width(60.0) - .selected_text(format!("panel: {:02}", metric.panel_id)) + .selected_text("panel: ???") .show_ui(ui, |ui| { ui.selectable_value(&mut panel_id, -1, "None"); for p in panels { diff --git a/src/gui/mod.rs b/src/gui/mod.rs index 472db7c..34d1c72 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -17,7 +17,7 @@ use scaffold::{ }; use source::source_panel; -use self::scaffold::{footer, popup_edit_ui, EditingModel}; +use self::scaffold::{footer, EditingModel, popup_edit_ui}; pub struct App { view: AppStateView, @@ -94,7 +94,9 @@ impl eframe::App for App { }); for m in self.editing.iter_mut() { - Window::new(m.id_repr()).show(ctx, |ui| popup_edit_ui(ui, m)); + Window::new(m.id_repr()) + .default_width(150.0) + .show(ctx, |ui| popup_edit_ui(ui, m, &self.view.sources.borrow(), &self.view.metrics.borrow())); } if self.sidebar { @@ -121,7 +123,7 @@ impl eframe::App for App { for m in self.editing.iter() { if m.should_fetch() { - self.op(m.to_msg()); + self.op(m.to_msg(self.view.clone())); // TODO cloning is super wasteful } } diff --git a/src/gui/panel.rs b/src/gui/panel.rs index 9037728..36c08ea 100644 --- a/src/gui/panel.rs +++ b/src/gui/panel.rs @@ -8,6 +8,8 @@ use crate::util::{timestamp_to_str, unpack_color}; use crate::gui::App; use crate::data::entities; +use super::scaffold::EditingModel; + pub fn main_content(app: &mut App, ctx: &Context, ui: &mut Ui) { let mut _to_swap: Option = None; let mut _to_delete: Option = None; @@ -39,9 +41,9 @@ pub fn main_content(app: &mut App, ctx: &Context, ui: &mut Ui) { // to_delete = Some(index); // TODO kinda jank but is there a better way? // } // ui.separator(); - panel_title_ui(ui, panel, app.edit); + panel_title_ui(ui, panel, &mut app.editing, &app.view.metrics.borrow(), &app.view.panel_metric.borrow()); }) - .body(|ui| panel_body_ui(ui, panel, &metrics, &app.view.points.borrow())); + .body(|ui| panel_body_ui(ui, panel, &metrics, &app.view.points.borrow(), &app.view.panel_metric.borrow())); } }); } @@ -53,10 +55,23 @@ pub fn _panel_edit_inline_ui(_ui: &mut Ui, _panel: &entities::panels::Model) { // .show(ui); } -pub fn panel_title_ui(ui: &mut Ui, panel: &mut entities::panels::Model, _edit: bool) { // TODO make edit UI in separate func +pub fn panel_title_ui( + ui: &mut Ui, + panel: &mut entities::panels::Model, + editing: &mut Vec, + metrics: &Vec, + panel_metric: &Vec, +) { // TODO make edit UI in separate func ui.horizontal(|ui| { ui.heading(panel.name.as_str()); ui.separator(); + if ui.small_button("#").clicked() { + // TODO don't add duplicates + editing.push( + EditingModel::make_edit_panel(panel.clone(), metrics, panel_metric) + ); + } + ui.separator(); ui.add(Slider::new(&mut panel.height, 0..=500).text("height")); //ui.separator(); //ui.checkbox(&mut panel.timeserie, "timeserie"); @@ -93,26 +108,29 @@ pub fn panel_title_ui(ui: &mut Ui, panel: &mut entities::panels::Model, _edit: b }); } -pub fn panel_body_ui(ui: &mut Ui, panel: &entities::panels::Model, metrics: &Vec, points: &Vec) { +pub fn panel_body_ui( + ui: &mut Ui, + panel: &entities::panels::Model, + metrics: &Vec, + points: &Vec, + panel_metric: &Vec, +) { let mut p = Plot::new(format!("plot-{}", panel.name)) .height(panel.height as f32) .allow_scroll(false) .legend(Legend::default().position(Corner::LeftTop)); - if panel.limit_view { + if panel.view_scroll { p = p.set_margin_fraction(Vec2 { x: 0.0, y: 0.1 }); } if panel.timeserie { if panel.view_scroll { - let _now = (Utc::now().timestamp() as f64) - (60.0 * panel.view_offset as f64); - p = p.include_x(_now); - if panel.limit_view { - p = p - .include_x(_now + (panel.view_size as f64 * 3.0)) - .include_x(_now - (panel.view_size as f64 * 60.0)); // ??? TODO - } + let now = (Utc::now().timestamp() as f64) - (60.0 * panel.view_offset as f64); + p = p.include_x(now) + .include_x(now + (panel.view_size as f64 * 3.0)) + .include_x(now - (panel.view_size as f64 * 60.0)); // ??? TODO } p = p .x_axis_formatter(|x, _range| timestamp_to_str(x as i64, true, false)) @@ -165,8 +183,9 @@ pub fn panel_body_ui(ui: &mut Ui, panel: &entities::panels::Model, metrics: &Vec let min_x = now - size - off; let max_x = now - off; let chunk_size = if panel.reduce_view { Some(panel.view_chunks) } else { None }; + let metric_ids : Vec = panel_metric.iter().filter(|x| x.panel_id == panel.id).map(|x| x.metric_id).collect(); for metric in metrics { - if metric.panel_id == panel.id { + if metric_ids.contains(&metric.id) { // let values = metric.values(min_x, max_x, chunk_size, panel.average_view); let mut values : Vec<[f64;2]> = points .iter() diff --git a/src/gui/scaffold.rs b/src/gui/scaffold.rs index 336c618..edc17c4 100644 --- a/src/gui/scaffold.rs +++ b/src/gui/scaffold.rs @@ -1,8 +1,8 @@ -use eframe::{Frame, egui::{collapsing_header::CollapsingState, Context, Ui, Layout, ScrollArea, global_dark_light_mode_switch, TextEdit, Checkbox, Slider}, emath::Align}; +use eframe::{Frame, egui::{collapsing_header::CollapsingState, Context, Ui, Layout, ScrollArea, global_dark_light_mode_switch, TextEdit, Checkbox, Slider, ComboBox}, emath::Align}; use sea_orm::{Set, Unchanged, ActiveValue::NotSet}; use tokio::sync::watch; -use crate::{gui::App, data::entities, util::unpack_color, worker::BackgroundAction}; +use crate::{gui::App, data::entities, util::{unpack_color, repack_color}, worker::{BackgroundAction, AppStateView}}; // TODO make this not super specific! pub fn _confirmation_popup_delete_metric(_app: &mut App, ui: &mut Ui, _metric_index: usize) { @@ -68,7 +68,7 @@ pub struct EditingModel { impl EditingModel { pub fn id_repr(&self) -> String { let prefix = match self.m { - EditingModelType::EditingPanel { panel: _ } => "panel", + EditingModelType::EditingPanel { panel: _, opts: _ } => "panel", EditingModelType::EditingSource { source: _ } => "source", EditingModelType::EditingMetric { metric: _ } => "metric", }; @@ -83,9 +83,30 @@ impl EditingModel { return !self.ready; } - pub fn to_msg(&self) -> BackgroundAction { + pub fn make_edit_panel( + panel: entities::panels::Model, + metrics: &Vec, + panel_metric: &Vec + ) -> EditingModel { + let metric_ids : Vec = panel_metric.iter().filter(|x| x.panel_id == panel.id).map(|x| x.metric_id).collect(); + let mut opts = vec![false; metrics.len()]; + for i in 0..metrics.len() { + if metric_ids.contains(&metrics[i].id) { + opts[i] = true; + } + } + EditingModel { + id: panel.id, + new: if panel.id > 0 { false } else { true }, + m: EditingModelType::EditingPanel { panel, opts }, + valid: false, + ready: false, + } + } + + pub fn to_msg(&self, view:AppStateView) -> BackgroundAction { match &self.m { - EditingModelType::EditingPanel { panel } => + EditingModelType::EditingPanel { panel, opts: metrics } => BackgroundAction::UpdatePanel { panel: entities::panels::ActiveModel { id: if self.new { NotSet } else { Unchanged(panel.id) }, @@ -94,14 +115,21 @@ impl EditingModel { view_size: Set(panel.view_size), timeserie: Set(panel.timeserie), height: Set(panel.height), - limit_view: Set(panel.limit_view), position: Set(panel.position), reduce_view: Set(panel.reduce_view), view_chunks: Set(panel.view_chunks), - shift_view: Set(panel.shift_view), view_offset: Set(panel.view_offset), average_view: Set(panel.average_view), - } + }, + metrics: view.metrics.borrow().iter() + .enumerate() + .filter(|(i,x)| *metrics.get(*i).unwrap_or(&false)) + .map(|(i,m)| entities::panel_metric::ActiveModel { + id: NotSet, + panel_id: Set(panel.id), + metric_id: Set(m.id), + }) + .collect(), }, EditingModelType::EditingSource { source } => BackgroundAction::UpdateSource { @@ -122,7 +150,6 @@ impl EditingModel { name: Set(metric.name.clone()), source_id: Set(metric.source_id), color: Set(metric.color), - panel_id: Set(metric.panel_id), query_x: Set(metric.query_x.clone()), query_y: Set(metric.query_y.clone()), position: Set(metric.position), @@ -154,24 +181,35 @@ impl From for EditingModel { fn from(p: entities::panels::Model) -> Self { EditingModel { new: if p.id == 0 { true } else { false }, - id: p.id, m: EditingModelType::EditingPanel { panel: p }, valid: false, ready: false, + id: p.id, m: EditingModelType::EditingPanel { panel: p , opts: vec![] }, valid: false, ready: false, } } } pub enum EditingModelType { - EditingPanel { panel : entities::panels::Model }, + EditingPanel { panel : entities::panels::Model, opts: Vec }, EditingSource { source: entities::sources::Model }, EditingMetric { metric: entities::metrics::Model }, } -pub fn popup_edit_ui(ui: &mut Ui, model: &mut EditingModel) { +pub fn popup_edit_ui( + ui: &mut Ui, + model: &mut EditingModel, + sources: &Vec, + metrics: &Vec +) { match &mut model.m { - EditingModelType::EditingPanel { panel } => { + EditingModelType::EditingPanel { panel, opts } => { ui.heading(format!("Edit panel #{}", panel.id)); TextEdit::singleline(&mut panel.name) .hint_text("name") .show(ui); + for (i, metric) in metrics.iter().enumerate() { + if i >= opts.len() { // TODO safe but jank: always starts with all off + opts.push(false); + } + ui.checkbox(&mut opts[i], &metric.name); + } }, EditingModelType::EditingSource { source } => { ui.heading(format!("Edit source #{}", source.id)); @@ -189,11 +227,21 @@ pub fn popup_edit_ui(ui: &mut Ui, model: &mut EditingModel) { EditingModelType::EditingMetric { metric } => { ui.heading(format!("Edit metric #{}", metric.id)); ui.horizontal(|ui| { - ui.color_edit_button_srgba(&mut unpack_color(metric.color)); + let mut color_buf = unpack_color(metric.color); + ui.color_edit_button_srgba(&mut color_buf); + metric.color = repack_color(color_buf); TextEdit::singleline(&mut metric.name) .hint_text("name") .show(ui); }); + ComboBox::from_id_source(format!("source-selector-{}", metric.id)) + .selected_text(format!("source: {:02}", metric.source_id)) + .show_ui(ui, |ui| { + ui.selectable_value(&mut metric.source_id, -1, "None"); + for s in sources.iter() { + ui.selectable_value(&mut metric.source_id, s.id, s.name.as_str()); + } + }); TextEdit::singleline(&mut metric.query_x) .hint_text("x") .show(ui); diff --git a/src/gui/source.rs b/src/gui/source.rs index 1745062..d45702e 100644 --- a/src/gui/source.rs +++ b/src/gui/source.rs @@ -16,6 +16,7 @@ pub fn source_panel(app: &mut App, ui: &mut Ui) { // let mut to_delete: Option = None; let panels = &app.panels; let panel_width = ui.available_width(); + let mut orphaned_metrics = app.view.metrics.borrow().clone(); ScrollArea::vertical() .max_width(panel_width) .show(ui, |ui| { @@ -29,16 +30,8 @@ pub fn source_panel(app: &mut App, ui: &mut Ui) { ui.horizontal(|ui| { ui.vertical(|ui| { ui.add_space(10.0); - if ui.small_button("+").clicked() { - if i > 0 { - to_swap = Some(i); // TODO kinda jank but is there a better way? - } - } - if ui.small_button("−").clicked() { - if i < sources_count - 1 { - to_swap = Some(i + 1); // TODO kinda jank but is there a better way? - } - } + if ui.small_button("+").clicked() { } + if ui.small_button("−").clicked() { } }); ui.vertical(|ui| { // actual sources list container let remaining_width = ui.available_width(); @@ -56,6 +49,7 @@ pub fn source_panel(app: &mut App, ui: &mut Ui) { .borrow(); for (_j, metric) in metrics.iter().enumerate() { if metric.source_id == source.id { + orphaned_metrics.retain(|m| m.id != metric.id); ui.horizontal(|ui| { metric_edit_ui( ui, @@ -91,6 +85,55 @@ pub fn source_panel(app: &mut App, ui: &mut Ui) { }); }); } + ui.horizontal(|ui| { // 1 more for uncategorized sources + ui.vertical(|ui| { + ui.add_space(10.0); + if ui.small_button("+").clicked() { } + if ui.small_button("−").clicked() { } + }); + ui.vertical(|ui| { // actual sources list container + let remaining_width = ui.available_width(); + ui.group(|ui| { + ui.horizontal(|ui| { + source_edit_ui(ui, &app.buffer_source, remaining_width - 34.0); + if ui.small_button("×").clicked() { + app.buffer_source = entities::sources::Model::default(); + } + }); + for metric in orphaned_metrics.iter() { + 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() { + // TODO don't add duplicates + app.editing.push(metric.clone().into()); + } + }); + } + }); + }); + }); } if app.edit { ui.separator(); diff --git a/src/worker/visualizer.rs b/src/worker/visualizer.rs index f2315a8..669ffe0 100644 --- a/src/worker/visualizer.rs +++ b/src/worker/visualizer.rs @@ -1,5 +1,5 @@ use chrono::Utc; -use sea_orm::{TransactionTrait, DatabaseConnection, EntityTrait, Condition, ColumnTrait, QueryFilter, Set, QueryOrder, Order, ActiveModelTrait, ActiveValue::NotSet}; +use sea_orm::{TransactionTrait, DatabaseConnection, EntityTrait, Condition, ColumnTrait, QueryFilter, Set, QueryOrder, Order, ActiveModelTrait, ActiveValue::{NotSet, self}}; use tokio::sync::{watch, mpsc}; use tracing::{info, error}; use std::collections::VecDeque; @@ -8,12 +8,13 @@ use crate::data::{entities, FetchError}; #[derive(Clone)] pub struct AppStateView { - pub panels: watch::Receiver>, - pub sources: watch::Receiver>, - pub metrics: watch::Receiver>, - pub points: watch::Receiver>, - pub flush: mpsc::Sender<()>, - pub op: mpsc::Sender, + pub panels: watch::Receiver>, + pub sources: watch::Receiver>, + pub metrics: watch::Receiver>, + pub panel_metric: watch::Receiver>, + pub points: watch::Receiver>, + pub flush: mpsc::Sender<()>, + pub op: mpsc::Sender, } impl AppStateView { @@ -26,18 +27,20 @@ impl AppStateView { } struct AppStateTransmitters { - panels: watch::Sender>, - sources: watch::Sender>, - metrics: watch::Sender>, - points: watch::Sender>, + panels: watch::Sender>, + sources: watch::Sender>, + metrics: watch::Sender>, + points: watch::Sender>, + panel_metric: watch::Sender>, } pub struct AppState { tx: AppStateTransmitters, - panels: Vec, - sources: Vec, - metrics: Vec, + panels: Vec, + sources: Vec, + metrics: Vec, + panel_metric: Vec, last_refresh: i64, points: VecDeque, @@ -70,6 +73,7 @@ impl AppState { 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 (panel_metric_tx, panel_metric_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); @@ -78,6 +82,7 @@ impl AppState { panels: vec![], sources: vec![], metrics: vec![], + panel_metric: vec![], last_refresh: 0, points: VecDeque::new(), last_check: 0, @@ -88,6 +93,7 @@ impl AppState { sources: source_rx, metrics: metric_rx, points: point_rx, + panel_metric: panel_metric_rx, flush: flush_tx, op: op_tx, }, @@ -96,6 +102,7 @@ impl AppState { sources: source_tx, metrics: metric_tx, points: point_tx, + panel_metric: panel_metric_tx, }, width, interval, @@ -130,6 +137,12 @@ impl AppState { error!(target: "worker", "Could not send metrics update: {:?}", e); } + self.panel_metric = entities::panel_metric::Entity::find() + .all(db).await?; + if let Err(e) = self.tx.panel_metric.send(self.panel_metric.clone()) { + error!(target: "worker", "Could not send panel-metric update: {:?}", e); + } + info!(target: "worker", "Updated panels, sources and metrics"); self.last_refresh = chrono::Utc::now().timestamp(); Ok(()) @@ -164,11 +177,9 @@ impl AppState { 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::>() @@ -184,12 +195,34 @@ impl AppState { self.panels = panels; } }, - BackgroundAction::UpdatePanel { panel } => { + BackgroundAction::UpdatePanel { panel, metrics } => { + let panel_id = match panel.id { + ActiveValue::Unchanged(pid) => Some(pid), + _ => None, + }; let op = if panel.id == NotSet { panel.insert(&db) } else { panel.update(&db) }; + // TODO chained if is trashy if let Err(e) = op.await { error!(target: "worker", "Could not update panel: {:?}", e); } else { - self.view.request_flush().await; + if let Some(panel_id) = panel_id { + if let Err(e) = db.transaction::<_, (), sea_orm::DbErr>(|txn| { + Box::pin(async move { + entities::panel_metric::Entity::delete_many() + .filter( + Condition::all() + .add(entities::panel_metric::Column::PanelId.eq(panel_id)) + ) + .exec(txn).await?; + entities::panel_metric::Entity::insert_many(metrics).exec(txn).await?; + Ok(()) + }) + }).await { + error!(target: "worker", "Could not update panels on database: {:?}", e); + } + } else { + self.view.request_flush().await; + } } }, BackgroundAction::UpdateSource { source } => { @@ -330,7 +363,7 @@ impl AppState { #[derive(Debug)] pub enum BackgroundAction { UpdateAllPanels { panels: Vec }, - UpdatePanel { panel : entities::panels::ActiveModel }, + UpdatePanel { panel : entities::panels::ActiveModel, metrics: Vec }, UpdateSource { source: entities::sources::ActiveModel }, UpdateMetric { metric: entities::metrics::ActiveModel }, // InsertPanel { panel : entities::panels::ActiveModel },