feat: added store/load capabilities, cleared code

This commit is contained in:
əlemi 2022-06-05 01:06:17 +02:00
parent 8e29b59c35
commit 0fed3fff95
Signed by: alemi
GPG key ID: A4895B84D311642C
2 changed files with 177 additions and 68 deletions

View file

@ -1,30 +1,78 @@
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use rand::Rng; use rand::Rng;
use std::io::{Write, Read};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize, de::{DeserializeOwned}};
use eframe::egui::plot::Value; use eframe::egui::{plot::Value, Context};
struct DataSource { pub fn native_save(name: &str, data:String) -> std::io::Result<()> {
let mut file = std::fs::File::create(name)?;
file.write_all(data.as_bytes())?;
return Ok(());
}
pub struct DataSource {
data : Arc<Mutex<Vec<Value>>>, data : Arc<Mutex<Vec<Value>>>,
} }
#[derive(Serialize, Deserialize)]
struct SerializableValue {
x : f64,
y : f64,
}
impl DataSource { impl DataSource {
fn new() -> Self { pub fn new() -> Self {
Self{ data: Arc::new(Mutex::new(Vec::new())) } Self{ data: Arc::new(Mutex::new(Vec::new())) }
} }
fn view(&self) -> Vec<Value> { // TODO handle errors pub fn view(&self) -> Vec<Value> { // TODO handle errors
return self.data.lock().unwrap().clone(); return self.data.lock().unwrap().clone();
} }
pub fn serialize(&self) -> String {
let mut out : Vec<SerializableValue> = Vec::new();
for value in self.view() {
out.push(SerializableValue { x: value.x, y: value.y });
}
return serde_json::to_string(&out).unwrap();
}
}
pub trait PlotValue {
fn as_value(&self) -> Value;
} }
pub trait Data { pub trait Data {
fn load(&mut self, url:&str); fn load_remote(&mut self, url:&str, ctx:Context);
fn view(&self) -> Vec<Value>; fn load_local(&mut self, file:&str, ctx:Context);
fn read(&mut self, file:&str, storage:Arc<Mutex<Vec<Value>>>, ctx:Context) -> std::io::Result<()> {
let mut file = std::fs::File::open(file)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
let data : Vec<SerializableValue> = serde_json::from_str(contents.as_str())?;
for v in data {
storage.lock().unwrap().push(Value { x: v.x, y: v.y });
}
ctx.request_repaint();
Ok(())
}
fn fetch<T>(&mut self, base:&str, endpoint:&str, storage:Arc<Mutex<Vec<Value>>>, ctx:Context)
where T : DeserializeOwned + PlotValue {
let request = ehttp::Request::get(format!("{}/{}", base, endpoint));
ehttp::fetch(request, move |result: ehttp::Result<ehttp::Response>| {
let data : T = serde_json::from_slice(result.unwrap().bytes.as_slice()).unwrap();
storage.lock().unwrap().push(data.as_value());
ctx.request_repaint();
});
}
} }
pub struct TpsData { pub struct TpsData {
ds: DataSource, pub ds: DataSource,
load_interval : i64, load_interval : i64,
last_load : DateTime<Utc>, last_load : DateTime<Utc>,
} }
@ -34,6 +82,12 @@ struct TpsResponseData {
tps: f64 tps: f64
} }
impl PlotValue for TpsResponseData {
fn as_value(&self) -> Value {
Value { x: Utc::now().timestamp() as f64, y: self.tps }
}
}
impl TpsData { impl TpsData {
pub fn new(load_interval:i64) -> Self { pub fn new(load_interval:i64) -> Self {
Self { ds: DataSource::new() , last_load: Utc::now(), load_interval } Self { ds: DataSource::new() , last_load: Utc::now(), load_interval }
@ -41,22 +95,19 @@ impl TpsData {
} }
impl Data for TpsData{ impl Data for TpsData{
fn load(&mut self, url:&str) { fn load_remote(&mut self, url:&str, ctx:Context) {
if (Utc::now() - self.last_load).num_seconds() < self.load_interval { return; } if (Utc::now() - self.last_load).num_seconds() < self.load_interval { return; }
self.last_load = Utc::now(); self.last_load = Utc::now();
let ds_data = self.ds.data.clone(); self.fetch::<TpsResponseData>(url, "tps", self.ds.data.clone(), ctx);
let request = ehttp::Request::get(format!("{}/tps", url));
ehttp::fetch(request, move |result: ehttp::Result<ehttp::Response>| {
let data : TpsResponseData = serde_json::from_slice(result.unwrap().bytes.as_slice()).unwrap();
ds_data.lock().unwrap().push(Value {x:Utc::now().timestamp() as f64, y:data.tps});
});
} }
fn view(&self) -> Vec<Value> { self.ds.view() } fn load_local(&mut self, file:&str, ctx:Context) {
self.read(file, self.ds.data.clone(), ctx).unwrap_or_else(|_err| println!("Could not load {}", file));
}
} }
pub struct ChatData { pub struct ChatData {
ds : DataSource, pub ds : DataSource,
load_interval : i64, load_interval : i64,
last_load : DateTime<Utc>, last_load : DateTime<Utc>,
} }
@ -66,6 +117,12 @@ struct ChatResponseData {
volume: f64 volume: f64
} }
impl PlotValue for ChatResponseData {
fn as_value(&self) -> Value {
Value { x:Utc::now().timestamp() as f64, y: self.volume }
}
}
impl ChatData { impl ChatData {
pub fn new(load_interval:i64) -> Self { pub fn new(load_interval:i64) -> Self {
Self { ds: DataSource::new() , last_load: Utc::now(), load_interval } Self { ds: DataSource::new() , last_load: Utc::now(), load_interval }
@ -73,22 +130,19 @@ impl ChatData {
} }
impl Data for ChatData{ impl Data for ChatData{
fn load(&mut self, url:&str) { fn load_remote(&mut self, url:&str, ctx:Context) {
if (Utc::now() - self.last_load).num_seconds() < self.load_interval { return; } if (Utc::now() - self.last_load).num_seconds() < self.load_interval { return; }
self.last_load = Utc::now(); self.last_load = Utc::now();
let ds_data = self.ds.data.clone(); self.fetch::<ChatResponseData>(url, "chat_activity", self.ds.data.clone(), ctx);
let request = ehttp::Request::get(format!("{}/chat_activity", url));
ehttp::fetch(request, move |result: ehttp::Result<ehttp::Response>| {
let data : ChatResponseData = serde_json::from_slice(result.unwrap().bytes.as_slice()).unwrap();
ds_data.lock().unwrap().push(Value {x:Utc::now().timestamp() as f64, y:data.volume});
});
} }
fn view(&self) -> Vec<Value> { self.ds.view() } fn load_local(&mut self, file:&str, ctx:Context) {
self.read(file, self.ds.data.clone(), ctx).unwrap_or_else(|_err| println!("Could not load {}", file));
}
} }
pub struct PlayerCountData { pub struct PlayerCountData {
ds : DataSource, pub ds : DataSource,
load_interval : i64, load_interval : i64,
last_load : DateTime<Utc>, last_load : DateTime<Utc>,
} }
@ -98,6 +152,12 @@ struct PlayerCountResponseData {
count: i32 count: i32
} }
impl PlotValue for PlayerCountResponseData {
fn as_value(&self) -> Value {
Value { x:Utc::now().timestamp() as f64, y: self.count as f64 }
}
}
impl PlayerCountData { impl PlayerCountData {
pub fn new(load_interval:i64) -> Self { pub fn new(load_interval:i64) -> Self {
Self { ds: DataSource::new() , last_load: Utc::now(), load_interval } Self { ds: DataSource::new() , last_load: Utc::now(), load_interval }
@ -105,39 +165,38 @@ impl PlayerCountData {
} }
impl Data for PlayerCountData{ impl Data for PlayerCountData{
fn load(&mut self, url:&str) { fn load_remote(&mut self, url:&str, ctx:Context) {
if (Utc::now() - self.last_load).num_seconds() < self.load_interval { return; } if (Utc::now() - self.last_load).num_seconds() < self.load_interval { return; }
self.last_load = Utc::now(); self.last_load = Utc::now();
let ds_data = self.ds.data.clone(); self.fetch::<PlayerCountResponseData>(url, "player_count", self.ds.data.clone(), ctx);
let request = ehttp::Request::get(format!("{}/chat_activity", url));
ehttp::fetch(request, move |result: ehttp::Result<ehttp::Response>| {
let data : PlayerCountResponseData = serde_json::from_slice(result.unwrap().bytes.as_slice()).unwrap();
ds_data.lock().unwrap().push(Value {x:Utc::now().timestamp() as f64, y:data.count as f64});
});
} }
fn view(&self) -> Vec<Value> { self.ds.view() } fn load_local(&mut self, file:&str, ctx:Context) {
self.read(file, self.ds.data.clone(), ctx).unwrap_or_else(|_err| println!("Could not load {}", file));
}
} }
pub struct RandomData { pub struct RandomData {
ds : DataSource, pub ds : DataSource,
load_interval : i64, load_interval : i64,
last_load : DateTime<Utc>, last_load : DateTime<Utc>,
rng: rand::rngs::ThreadRng, rng: rand::rngs::ThreadRng,
} }
impl RandomData { impl RandomData {
#[allow(dead_code)]
pub fn new(load_interval:i64) -> Self { pub fn new(load_interval:i64) -> Self {
Self { ds: DataSource::new() , last_load: Utc::now(), load_interval, rng : rand::thread_rng() } Self { ds: DataSource::new() , last_load: Utc::now(), load_interval, rng : rand::thread_rng() }
} }
} }
impl Data for RandomData{ impl Data for RandomData{
fn load(&mut self, _url:&str) { fn load_remote(&mut self, _url:&str, ctx:Context) {
if (Utc::now() - self.last_load).num_seconds() < self.load_interval { return; } if (Utc::now() - self.last_load).num_seconds() < self.load_interval { return; }
self.last_load = Utc::now(); self.last_load = Utc::now();
self.ds.data.lock().unwrap().push(Value {x:Utc::now().timestamp() as f64, y:self.rng.gen()}); self.ds.data.lock().unwrap().push(Value {x:Utc::now().timestamp() as f64, y:self.rng.gen()});
ctx.request_repaint();
} }
fn view(&self) -> Vec<Value> { self.ds.view() } fn load_local(&mut self, _file:&str, _ctx:Context) {}
} }

View file

@ -1,39 +1,102 @@
mod datasource; mod datasource;
use chrono::{DateTime, NaiveDateTime, Utc}; use chrono::{DateTime, NaiveDateTime, Utc};
use datasource::{ChatData, PlayerCountData, TpsData, Data, RandomData}; use datasource::{ChatData, PlayerCountData, TpsData, Data, native_save};
use eframe::egui; use eframe::egui;
use eframe::egui::plot::{Line, Plot, Value, Values}; use eframe::egui::plot::{Line, Plot, Values};
pub struct App { pub struct App {
servers : Vec<ServerOptions>,
}
struct ServerOptions {
title: String,
url: String,
player_count: PlayerCountData, player_count: PlayerCountData,
tps: TpsData, tps: TpsData,
chat: ChatData, chat: ChatData,
rand: RandomData,
sync_time:bool, sync_time:bool,
} }
impl App { impl App {
pub fn new(_cc: &eframe::CreationContext) -> Self { pub fn new(_cc: &eframe::CreationContext) -> Self {
let mut servers = Vec::new();
servers.push(ServerOptions::new("9b9t", "https://alemi.dev/mcbots/9b"));
servers.push(ServerOptions::new("const", "https://alemi.dev/mcbots/const"));
servers.push(ServerOptions::new("of", "https://alemi.dev/mcbots/of"));
Self { servers }
}
}
impl ServerOptions {
fn new(title:&str, url:&str) -> Self {
Self { Self {
title: title.to_string(),
url: url.to_string(),
player_count: PlayerCountData::new(60), player_count: PlayerCountData::new(60),
tps: TpsData::new(30), tps: TpsData::new(15),
chat: ChatData::new(15), chat: ChatData::new(30),
rand: RandomData::new(1),
sync_time: false, sync_time: false,
} }
} }
fn display(&mut self, ui:&mut eframe::egui::Ui) {
ui.vertical(|ui| {
ui.horizontal(|ui| {
ui.heading(self.title.as_str());
ui.checkbox(&mut self.sync_time, "Lock X to now");
});
let mut p = Plot::new(format!("plot-{}", self.title)).x_axis_formatter(|x, _range| {
format!(
"{}",
DateTime::<Utc>::from_utc(NaiveDateTime::from_timestamp(x as i64, 0), Utc)
.format("%Y/%m/%d %H:%M:%S")
)
}).center_x_axis(false).height(260.0); // TODO make it fucking reactive! It fills the whole screen with 1 plot no matter what I do...
if self.sync_time {
p = p.include_x(Utc::now().timestamp() as f64);
}
p.show(ui, |plot_ui| {
plot_ui.line(
Line::new(Values::from_values(self.player_count.ds.view())).name("Player Count"),
);
plot_ui.line(Line::new(Values::from_values(self.tps.ds.view())).name("TPS over 15s"));
plot_ui.line(
Line::new(Values::from_values(self.chat.ds.view()))
.name("Chat messages per minute"),
);
});
});
}
} }
impl eframe::App for App { impl eframe::App for App {
fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) { fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
self.rand.load(""); for server in &mut self.servers {
server.tps.load_remote(server.url.as_str(), ctx.clone());
server.player_count.load_remote(server.url.as_str(), ctx.clone());
server.chat.load_remote(server.url.as_str(), ctx.clone());
}
egui::TopBottomPanel::top("??? wtf").show(ctx, |ui| { egui::TopBottomPanel::top("??? wtf").show(ctx, |ui| {
ui.horizontal(|ui| { ui.horizontal(|ui| {
egui::widgets::global_dark_light_mode_switch(ui); egui::widgets::global_dark_light_mode_switch(ui);
ui.heading("nnbot dashboard"); ui.heading("nnbot dashboard");
ui.checkbox(&mut self.sync_time, "Lock X to now"); if ui.button("save").clicked() {
for server in &self.servers {
native_save(format!("{}-tps.json", server.title).as_str(), server.tps.ds.serialize()).unwrap();
native_save(format!("{}-chat.json", server.title).as_str(), server.chat.ds.serialize()).unwrap();
native_save(format!("{}-players.json", server.title).as_str(), server.player_count.ds.serialize()).unwrap();
}
}
if ui.button("load").clicked() {
for server in &mut self.servers {
server.tps.load_local(format!("{}-tps.json", server.title).as_str(), ctx.clone());
server.chat.load_local(format!("{}-chat.json", server.title).as_str(), ctx.clone());
server.player_count.load_local(format!("{}-players.json", server.title).as_str(), ctx.clone());
}
}
ui.with_layout(egui::Layout::top_down(egui::Align::RIGHT), |ui| { ui.with_layout(egui::Layout::top_down(egui::Align::RIGHT), |ui| {
if ui.button("x").clicked() { if ui.button("x").clicked() {
frame.quit(); frame.quit();
@ -42,29 +105,16 @@ impl eframe::App for App {
}); });
}); });
egui::CentralPanel::default().show(ctx, |ui| { egui::CentralPanel::default().show(ctx, |ui| {
let mut p = Plot::new("test").x_axis_formatter(|x, _range| { ui.group(|v_ui| {
format!( self.servers[0].display(v_ui);
"{}", });
DateTime::<Utc>::from_utc(NaiveDateTime::from_timestamp(x as i64, 0), Utc) ui.group(|v_ui| {
.format("%Y/%m/%d %H:%M:%S") self.servers[1].display(v_ui);
) });
}).center_x_axis(false); ui.group(|v_ui| {
self.servers[2].display(v_ui);
if self.sync_time {
p = p.include_x(Utc::now().timestamp() as f64);
}
p.show(ui, |plot_ui| {
plot_ui.line(
Line::new(Values::from_values(self.player_count.view())).name("Player Count"),
);
plot_ui.line(Line::new(Values::from_values(self.tps.view())).name("TPS over 15s"));
plot_ui.line(Line::new(Values::from_values(self.rand.view())).name("Random Data"));
plot_ui.line(
Line::new(Values::from_values(self.chat.view()))
.name("Chat messages per minute"),
);
}); });
}); });
ctx.request_repaint(); // TODO super jank way to sorta keep drawing
} }
} }