feat: floating windows to edit stuff

allow editing models via floating windows. Also added weird buttons to
create new models. Still need to do relations!
This commit is contained in:
əlemi 2022-11-02 03:18:23 +01:00
parent 3fcf059095
commit 76772465a3
Signed by: alemi
GPG key ID: A4895B84D311642C
9 changed files with 265 additions and 121 deletions

View file

@ -13,7 +13,7 @@ pub struct Model {
pub id: i64,
pub name: String,
pub source_id: i64,
pub query_x: Option<String>,
pub query_x: String,
pub query_y: String,
pub panel_id: i64,
pub color: i32,
@ -29,14 +29,8 @@ impl ActiveModelBehavior for ActiveModel {}
impl Model {
pub fn extract(&self, value: &serde_json::Value) -> Result<PlotPoint, FetchError> {
let x: f64;
let fallback_query = "".into();
let q_x = self.query_x.as_ref().unwrap_or(&fallback_query);
// TODO because of bad design, empty queries are
// empty strings in my db. Rather than converting
// them right away, I'm putting this jank fix:
// checking len
if q_x.len() > 0 {
x = jql::walker(value, q_x.as_str())?
if self.query_x.len() > 0 {
x = jql::walker(value, self.query_x.as_str())?
.as_f64()
.ok_or(FetchError::JQLError("X query is null".to_string()))?; // TODO what if it's given to us as a string?
} else {
@ -55,7 +49,7 @@ impl Default for Model {
id: 0,
name: "".into(),
source_id: 0,
query_x: None,
query_x: "".into(),
query_y: "".into(),
panel_id: 0,
color: 0,

View file

@ -36,8 +36,7 @@ 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) {
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_x = metric.query_x.clone();
let mut query_y = metric.query_y.clone();
let mut panel_id = 0;
ui.horizontal(|ui| {

View file

@ -9,7 +9,7 @@ use eframe::egui::{CentralPanel, Context, SidePanel, TopBottomPanel, Window};
use tokio::sync::watch;
use tracing::error;
use crate::{data::entities, worker::visualizer::AppStateView};
use crate::{data::entities, worker::{visualizer::AppStateView, BackgroundAction}};
use panel::main_content;
use scaffold::{
// confirmation_popup_delete_metric, confirmation_popup_delete_source, footer,
@ -17,7 +17,7 @@ use scaffold::{
};
use source::source_panel;
use self::scaffold::footer;
use self::scaffold::{footer, popup_edit_ui, EditingModel};
pub struct App {
view: AppStateView,
@ -29,11 +29,12 @@ pub struct App {
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_metric: entities::metrics::Model,
// buffer_metric: entities::metrics::Model,
edit: bool,
editing: Vec<EditingModel>,
sidebar: bool,
padding: bool,
// windows: Vec<Window<'open>>,
@ -51,11 +52,10 @@ impl App {
let panels = view.panels.borrow().clone();
Self {
db_path, interval, panels, width_tx, view, logger_view,
buffer_panel: entities::panels::Model::default(),
buffer_source: entities::sources::Model::default(),
buffer_metric: entities::metrics::Model::default(),
last_redraw: 0,
edit: false,
editing: vec![],
sidebar: true,
padding: false,
// windows: vec![],
@ -75,6 +75,12 @@ impl App {
error!(target: "app", "Could not request flush: {:?}", e);
}
}
pub fn op(&self, op: BackgroundAction) {
if let Err(e) = self.view.op.blocking_send(op) {
error!(target: "app", "Could not send operation: {:?}", e);
}
}
}
impl eframe::App for App {
@ -87,20 +93,9 @@ impl eframe::App for App {
footer(ctx, ui, self.logger_view.clone(), self.db_path.clone(), self.view.points.borrow().len());
});
let _w = Window::new("a");
// if let Some(index) = self.deleting_metric {
// Window::new(format!("Delete Metric #{}?", index))
// .show(ctx, |ui| confirmation_popup_delete_metric(self, ui, index));
// }
// if let Some(index) = self.deleting_source {
// Window::new(format!("Delete Source #{}?", index))
// .show(ctx, |ui| confirmation_popup_delete_source(self, ui, index));
// }
// for window in self.windows {
// }
for m in self.editing.iter_mut() {
Window::new(m.id_repr()).show(ctx, |ui| popup_edit_ui(ui, m));
}
if self.sidebar {
SidePanel::left("sources-bar")
@ -113,7 +108,7 @@ impl eframe::App for App {
main_content(self, ctx, ui);
});
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 + p.view_offset).max() {
if let Err(e) = self.width_tx.send(viewsize as i64) {
error!(target: "app", "Could not update fetch size : {:?}", e);
}
@ -123,5 +118,13 @@ impl eframe::App for App {
ctx.request_repaint();
self.last_redraw = Utc::now().timestamp();
}
for m in self.editing.iter() {
if m.should_fetch() {
self.op(m.to_msg());
}
}
self.editing.retain(|v| v.modifying());
}
}

View file

@ -46,7 +46,7 @@ pub fn main_content(app: &mut App, ctx: &Context, ui: &mut Ui) {
});
}
pub fn panel_edit_inline_ui(_ui: &mut Ui, _panel: &entities::panels::Model) {
pub fn _panel_edit_inline_ui(_ui: &mut Ui, _panel: &entities::panels::Model) {
// TextEdit::singleline(&mut panel.name)
// .hint_text("name")
// .desired_width(100.0)

View file

@ -1,9 +1,8 @@
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, TextEdit, Checkbox, Slider}, emath::Align};
use sea_orm::{Set, Unchanged, ActiveValue::NotSet};
use tokio::sync::watch;
use crate::gui::App;
use super::panel::panel_edit_inline_ui;
use crate::{gui::App, data::entities, util::unpack_color, worker::BackgroundAction};
// TODO make this not super specific!
pub fn _confirmation_popup_delete_metric(_app: &mut App, ui: &mut Ui, _metric_index: usize) {
@ -58,6 +57,166 @@ pub fn _confirmation_popup_delete_source(_app: &mut App, ui: &mut Ui, _source_in
});
}
pub struct EditingModel {
pub id: i64,
m: EditingModelType,
new: bool,
valid: bool,
ready: bool,
}
impl EditingModel {
pub fn id_repr(&self) -> String {
let prefix = match self.m {
EditingModelType::EditingPanel { panel: _ } => "panel",
EditingModelType::EditingSource { source: _ } => "source",
EditingModelType::EditingMetric { metric: _ } => "metric",
};
format!("edit_{}_{}", prefix, self.id)
}
pub fn should_fetch(&self) -> bool {
return self.ready && self.valid;
}
pub fn modifying(&self) -> bool {
return !self.ready;
}
pub fn to_msg(&self) -> BackgroundAction {
match &self.m {
EditingModelType::EditingPanel { panel } =>
BackgroundAction::UpdatePanel {
panel: entities::panels::ActiveModel {
id: if self.new { NotSet } else { Unchanged(panel.id) },
name: Set(panel.name.clone()),
view_scroll: Set(panel.view_scroll),
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),
}
},
EditingModelType::EditingSource { source } =>
BackgroundAction::UpdateSource {
source: entities::sources::ActiveModel {
id: if self.new { NotSet } else { Unchanged(source.id) },
name: Set(source.name.clone()),
enabled: Set(source.enabled),
url: Set(source.url.clone()),
interval: Set(source.interval),
last_update: Set(source.last_update),
position: Set(source.position),
}
},
EditingModelType::EditingMetric { metric } =>
BackgroundAction::UpdateMetric {
metric: entities::metrics::ActiveModel {
id: if self.new { NotSet} else { Unchanged(metric.id) },
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),
}
},
}
}
}
impl From<entities::sources::Model> for EditingModel {
fn from(s: entities::sources::Model) -> Self {
EditingModel {
new: if s.id == 0 { true } else { false },
id: s.id, m: EditingModelType::EditingSource { source: s }, valid: false, ready: false,
}
}
}
impl From<entities::metrics::Model> for EditingModel {
fn from(m: entities::metrics::Model) -> Self {
EditingModel {
new: if m.id == 0 { true } else { false },
id: m.id, m: EditingModelType::EditingMetric { metric: m }, valid: false, ready: false,
}
}
}
impl From<entities::panels::Model> 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,
}
}
}
pub enum EditingModelType {
EditingPanel { panel : entities::panels::Model },
EditingSource { source: entities::sources::Model },
EditingMetric { metric: entities::metrics::Model },
}
pub fn popup_edit_ui(ui: &mut Ui, model: &mut EditingModel) {
match &mut model.m {
EditingModelType::EditingPanel { panel } => {
ui.heading(format!("Edit panel #{}", panel.id));
TextEdit::singleline(&mut panel.name)
.hint_text("name")
.show(ui);
},
EditingModelType::EditingSource { source } => {
ui.heading(format!("Edit source #{}", source.id));
ui.horizontal(|ui| {
ui.add(Checkbox::new(&mut source.enabled, ""));
TextEdit::singleline(&mut source.name)
.hint_text("name")
.show(ui);
});
TextEdit::singleline(&mut source.url)
.hint_text("url")
.show(ui);
ui.add(Slider::new(&mut source.interval, 1..=3600).text("interval"));
},
EditingModelType::EditingMetric { metric } => {
ui.heading(format!("Edit metric #{}", metric.id));
ui.horizontal(|ui| {
ui.color_edit_button_srgba(&mut unpack_color(metric.color));
TextEdit::singleline(&mut metric.name)
.hint_text("name")
.show(ui);
});
TextEdit::singleline(&mut metric.query_x)
.hint_text("x")
.show(ui);
TextEdit::singleline(&mut metric.query_y)
.hint_text("y")
.show(ui);
},
}
ui.separator();
ui.horizontal(|ui| {
if ui.button(" save ").clicked() {
model.valid = true;
model.ready = true;
}
ui.with_layout(Layout::top_down(Align::RIGHT), |ui| {
if ui.button(" close ").clicked() {
model.valid = false;
model.ready = true;
}
});
});
}
pub fn header(app: &mut App, ui: &mut Ui, frame: &mut Frame) {
ui.horizontal(|ui| {
global_dark_light_mode_switch(ui);
@ -77,17 +236,29 @@ pub fn header(app: &mut App, ui: &mut Ui, frame: &mut Frame) {
app.refresh_data();
}
ui.separator();
ui.checkbox(&mut app.edit, "edit");
if app.edit {
ui.label("+ panel");
panel_edit_inline_ui(ui, &mut app.buffer_panel);
if ui.button("add").clicked() {
// if let Err(e) = app.data.add_panel(&app.input_panel) {
// error!(target: "ui", "Failed to add panel: {:?}", e);
// };
if ui.button("new panel").clicked() {
app.editing.push(entities::panels::Model::default().into());
}
ui.separator();
if ui.button("new source").clicked() {
app.editing.push(entities::sources::Model::default().into());
}
ui.separator();
if ui.button("new metric").clicked() {
app.editing.push(entities::metrics::Model::default().into());
}
// ui.separator();
// ui.checkbox(&mut app.edit, "edit");
// if app.edit {
// ui.label("+ panel");
// panel_edit_inline_ui(ui, &mut app.buffer_panel);
// if ui.button("add").clicked() {
// // if let Err(e) = app.data.add_panel(&app.input_panel) {
// // error!(target: "ui", "Failed to add panel: {:?}", e);
// // };
// }
// ui.separator();
// }
ui.with_layout(Layout::top_down(Align::RIGHT), |ui| {
ui.horizontal(|ui| {
if ui.small_button("×").clicked() {

View file

@ -3,16 +3,14 @@ use eframe::{
emath::Align,
};
use rfd::FileDialog;
use tracing::error;
use crate::util::deserialize_values;
use crate::gui::App;
use crate::data::entities;
use super::metric::metric_edit_ui;
pub fn source_panel(app: &mut App, ui: &mut Ui) {
let mut source_to_put_metric_on : Option<i64> = None;
let source_to_put_metric_on : Option<i64> = None;
let mut to_swap: Option<usize> = None;
let _to_insert: Vec<entities::metrics::Model> = Vec::new();
// let mut to_delete: Option<usize> = None;
@ -48,8 +46,8 @@ pub fn source_panel(app: &mut App, ui: &mut Ui) {
ui.horizontal(|ui| {
source_edit_ui(ui, source, remaining_width - 34.0);
if ui.small_button("×").clicked() {
// app.deleting_metric = None;
// app.deleting_source = Some(i);
// TODO don't add duplicates
app.editing.push(source.clone().into());
}
});
let metrics = app
@ -83,65 +81,12 @@ pub fn source_panel(app: &mut App, ui: &mut Ui) {
}
}
if ui.small_button("×").clicked() {
// app.deleting_source = None;
// app.deleting_metric = Some(j);
// TODO don't add duplicates
app.editing.push(metric.clone().into());
}
});
}
}
ui.horizontal(|ui| {
metric_edit_ui(
ui,
&mut app.buffer_metric,
None,
remaining_width - 53.0,
);
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();
}
})
});
});
});

View file

@ -175,9 +175,9 @@ fn main() {
native_options,
Box::new(
move |cc| {
ctx_tx.send(Some(cc.egui_ctx.clone())).unwrap_or_else(|_| {
if let Err(_e) = ctx_tx.send(Some(cc.egui_ctx.clone())) {
error!(target: "launcher", "Could not share reference to egui context (won't be able to periodically refresh window)");
});
};
Box::new(
App::new(
cc,

View file

@ -13,9 +13,8 @@ const _PREFIXES: &'static [&'static str] = &["", "k", "M", "G", "T"];
pub fn _serialize_values(values: &Vec<PlotPoint>, metric: &entities::metrics::Model, path: PathBuf) -> Result<(), Box<dyn Error>> {
let mut wtr = csv::Writer::from_writer(std::fs::File::create(path)?);
// DAMN! VVVVV
let def_q_x = "".into();
let name = metric.name.as_str();
let q_x = metric.query_x.as_ref().unwrap_or(&def_q_x).as_str();
let q_x = metric.query_x.as_str();
let q_y = metric.query_y.as_str();
wtr.write_record(&[name, q_x, q_y])?;
// DAMN! AAAAA
@ -26,7 +25,7 @@ pub fn _serialize_values(values: &Vec<PlotPoint>, metric: &entities::metrics::Mo
Ok(())
}
pub fn deserialize_values(path: PathBuf) -> Result<(String, String, String, Vec<PlotPoint>), Box<dyn Error>> {
pub fn _deserialize_values(path: PathBuf) -> Result<(String, String, String, Vec<PlotPoint>), Box<dyn Error>> {
let mut values = Vec::new();
let mut rdr = csv::Reader::from_reader(std::fs::File::open(path)?);

View file

@ -1,5 +1,5 @@
use chrono::Utc;
use sea_orm::{TransactionTrait, DatabaseConnection, EntityTrait, Condition, ColumnTrait, QueryFilter, Set, QueryOrder, Order};
use sea_orm::{TransactionTrait, DatabaseConnection, EntityTrait, Condition, ColumnTrait, QueryFilter, Set, QueryOrder, Order, ActiveModelTrait, ActiveValue::NotSet};
use tokio::sync::{watch, mpsc};
use tracing::{info, error};
use std::collections::VecDeque;
@ -17,7 +17,7 @@ pub struct AppStateView {
}
impl AppStateView {
pub async fn _request_flush(&self) -> bool {
pub async fn request_flush(&self) -> bool {
match self.flush.send(()).await {
Ok(_) => true,
Err(_) => false,
@ -109,17 +109,23 @@ impl AppState {
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.panels = entities::panels::Entity::find()
.order_by(entities::panels::Column::Position, Order::Asc)
.all(db).await?;
if let Err(e) = self.tx.panels.send(self.panels.clone()) {
error!(target: "worker", "Could not send panels update: {:?}", e);
}
self.sources = entities::sources::Entity::find().all(db).await?;
self.sources = entities::sources::Entity::find()
.order_by(entities::sources::Column::Position, Order::Asc)
.all(db).await?;
if let Err(e) = self.tx.sources.send(self.sources.clone()) {
error!(target: "worker", "Could not send sources update: {:?}", e);
}
self.metrics = entities::metrics::Entity::find().all(db).await?;
self.metrics = entities::metrics::Entity::find()
.order_by(entities::metrics::Column::Position, Order::Asc)
.all(db).await?;
if let Err(e) = self.tx.metrics.send(self.metrics.clone()) {
error!(target: "worker", "Could not send metrics update: {:?}", e);
}
@ -178,6 +184,30 @@ impl AppState {
self.panels = panels;
}
},
BackgroundAction::UpdatePanel { panel } => {
let op = if panel.id == NotSet { panel.insert(&db) } else { panel.update(&db) };
if let Err(e) = op.await {
error!(target: "worker", "Could not update panel: {:?}", e);
} else {
self.view.request_flush().await;
}
},
BackgroundAction::UpdateSource { source } => {
let op = if source.id == NotSet { source.insert(&db) } else { source.update(&db) };
if let Err(e) = op.await {
error!(target: "worker", "Could not update source: {:?}", e);
} else {
self.view.request_flush().await;
}
},
BackgroundAction::UpdateMetric { metric } => {
let op = if metric.id == NotSet { metric.insert(&db) } else { metric.update(&db) };
if let Err(e) = op.await {
error!(target: "worker", "Could not update metric: {:?}", e);
} else {
self.view.request_flush().await;
}
},
// _ => todo!(),
}
},
@ -300,7 +330,10 @@ impl AppState {
#[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 },
UpdatePanel { panel : entities::panels::ActiveModel },
UpdateSource { source: entities::sources::ActiveModel },
UpdateMetric { metric: entities::metrics::ActiveModel },
// InsertPanel { panel : entities::panels::ActiveModel },
// InsertSource { source: entities::sources::ActiveModel },
// InsertMetric { metric: entities::metrics::ActiveModel },
}