mirror of
https://git.alemi.dev/dashboard.git
synced 2024-11-22 23:44:55 +01:00
feat: committing current state
Committing current state, which is a simple full screen plot with some basic features. It compiles for both native and web. There's a basic generic datasource impl. It currently plots random data for demo.
This commit is contained in:
parent
c256fec42f
commit
6653024bad
17 changed files with 2046 additions and 0 deletions
25
Cargo.toml
Normal file
25
Cargo.toml
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
[package]
|
||||||
|
name = "plotter"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "plotter_bin"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib", "rlib"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
rand = "0.8"
|
||||||
|
chrono = { version = "0.4", features = ["wasmbind"] }
|
||||||
|
eframe = { version = "0.18", features = ["persistence"] }
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
|
ehttp = "0.2.0"
|
||||||
|
# web:
|
||||||
|
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||||
|
console_error_panic_hook = "0.1.6"
|
||||||
|
tracing-wasm = "0.2"
|
77
build_web.sh
Executable file
77
build_web.sh
Executable file
|
@ -0,0 +1,77 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -eu
|
||||||
|
script_path=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P )
|
||||||
|
cd "$script_path"
|
||||||
|
|
||||||
|
OPEN=false
|
||||||
|
FAST=false
|
||||||
|
|
||||||
|
while test $# -gt 0; do
|
||||||
|
case "$1" in
|
||||||
|
-h|--help)
|
||||||
|
echo "build_web.sh [--fast] [--open]"
|
||||||
|
echo " --fast: skip optimization step"
|
||||||
|
echo " --open: open the result in a browser"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
--fast)
|
||||||
|
shift
|
||||||
|
FAST=true
|
||||||
|
;;
|
||||||
|
--open)
|
||||||
|
shift
|
||||||
|
OPEN=true
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
break
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# ./setup_web.sh # <- call this first!
|
||||||
|
|
||||||
|
FOLDER_NAME=${PWD##*/}
|
||||||
|
CRATE_NAME=$FOLDER_NAME # assume crate name is the same as the folder name
|
||||||
|
CRATE_NAME_SNAKE_CASE="${CRATE_NAME//-/_}" # for those who name crates with-kebab-case
|
||||||
|
|
||||||
|
# This is required to enable the web_sys clipboard API which egui_web uses
|
||||||
|
# https://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.Clipboard.html
|
||||||
|
# https://rustwasm.github.io/docs/wasm-bindgen/web-sys/unstable-apis.html
|
||||||
|
export RUSTFLAGS=--cfg=web_sys_unstable_apis
|
||||||
|
|
||||||
|
# Clear output from old stuff:
|
||||||
|
rm -f "docs/${CRATE_NAME_SNAKE_CASE}_bg.wasm"
|
||||||
|
|
||||||
|
echo "Building rust…"
|
||||||
|
BUILD=release
|
||||||
|
cargo build -p "${CRATE_NAME}" --release --lib --target wasm32-unknown-unknown
|
||||||
|
|
||||||
|
# Get the output directory (in the workspace it is in another location)
|
||||||
|
TARGET=$(cargo metadata --format-version=1 | jq --raw-output .target_directory)
|
||||||
|
|
||||||
|
echo "Generating JS bindings for wasm…"
|
||||||
|
TARGET_NAME="${CRATE_NAME_SNAKE_CASE}.wasm"
|
||||||
|
WASM_PATH="${TARGET}/wasm32-unknown-unknown/${BUILD}/${TARGET_NAME}"
|
||||||
|
wasm-bindgen "${WASM_PATH}" --out-dir docs --no-modules --no-typescript
|
||||||
|
|
||||||
|
if [[ "${FAST}" == false ]]; then
|
||||||
|
echo "Optimizing wasm…"
|
||||||
|
# to get wasm-opt: apt/brew/dnf install binaryen
|
||||||
|
wasm-opt "docs/${CRATE_NAME}_bg.wasm" -O2 --fast-math -o "docs/${CRATE_NAME}_bg.wasm" # add -g to get debug symbols
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Finished: docs/${CRATE_NAME_SNAKE_CASE}.wasm"
|
||||||
|
|
||||||
|
if [[ "${OPEN}" == true ]]; then
|
||||||
|
if [[ "$OSTYPE" == "linux-gnu"* ]]; then
|
||||||
|
# Linux, ex: Fedora
|
||||||
|
xdg-open http://localhost:8080/index.html
|
||||||
|
elif [[ "$OSTYPE" == "msys" ]]; then
|
||||||
|
# Windows
|
||||||
|
start http://localhost:8080/index.html
|
||||||
|
else
|
||||||
|
# Darwin/MacOS, or something else
|
||||||
|
open http://localhost:8080/index.html
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
3
docs/README.md
Normal file
3
docs/README.md
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
This folder contains the files required for the web app.
|
||||||
|
|
||||||
|
The reason the folder is called "docs" is because that is the name that GitHub requires in order to host a web page from the `master` branch of a repository. You can test the `eframe_template` at <https://emilk.github.io/eframe_template/>.
|
BIN
docs/favicon.ico
Executable file
BIN
docs/favicon.ico
Executable file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
BIN
docs/icon-1024.png
Normal file
BIN
docs/icon-1024.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 314 KiB |
BIN
docs/icon-256.png
Normal file
BIN
docs/icon-256.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 47 KiB |
BIN
docs/icon_ios_touch_192.png
Normal file
BIN
docs/icon_ios_touch_192.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 21 KiB |
173
docs/index.html
Normal file
173
docs/index.html
Normal file
|
@ -0,0 +1,173 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||||
|
|
||||||
|
<!-- Disable zooming: -->
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>eframe template</title>
|
||||||
|
<link rel="manifest" href="/manifest.json">
|
||||||
|
<meta name="theme-color" media="(prefers-color-scheme: light)" content="white">
|
||||||
|
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#404040">
|
||||||
|
<link rel="apple-touch-icon" href="/icon_ios_touch_192.png">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
html {
|
||||||
|
/* Remove touch delay: */
|
||||||
|
touch-action: manipulation;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
/* Light mode background color for what is not covered by the egui canvas,
|
||||||
|
or where the egui canvas is translucent. */
|
||||||
|
background: #909090;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
body {
|
||||||
|
/* Dark mode background color for what is not covered by the egui canvas,
|
||||||
|
or where the egui canvas is translucent. */
|
||||||
|
background: #404040;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Allow canvas to fill entire web page: */
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Position canvas in center-top: */
|
||||||
|
canvas {
|
||||||
|
margin-right: auto;
|
||||||
|
margin-left: auto;
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: 0%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, 0%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.centered {
|
||||||
|
margin-right: auto;
|
||||||
|
margin-left: auto;
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
color: #f0f0f0;
|
||||||
|
font-size: 24px;
|
||||||
|
font-family: Ubuntu-Light, Helvetica, sans-serif;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------- */
|
||||||
|
/* Loading animation from https://loading.io/css/ */
|
||||||
|
.lds-dual-ring {
|
||||||
|
display: inline-block;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lds-dual-ring:after {
|
||||||
|
content: " ";
|
||||||
|
display: block;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
margin: 0px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 3px solid #fff;
|
||||||
|
border-color: #fff transparent #fff transparent;
|
||||||
|
animation: lds-dual-ring 1.2s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes lds-dual-ring {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<!-- The WASM code will resize the canvas dynamically -->
|
||||||
|
<canvas id="the_canvas_id"></canvas>
|
||||||
|
<div class="centered" id="center_text">
|
||||||
|
<p style="font-size:16px">
|
||||||
|
Loading…
|
||||||
|
</p>
|
||||||
|
<div class="lds-dual-ring"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// The `--no-modules`-generated JS from `wasm-bindgen` attempts to use
|
||||||
|
// `WebAssembly.instantiateStreaming` to instantiate the wasm module,
|
||||||
|
// but this doesn't work with `file://` urls. This example is frequently
|
||||||
|
// viewed by simply opening `index.html` in a browser (with a `file://`
|
||||||
|
// url), so it would fail if we were to call this function!
|
||||||
|
//
|
||||||
|
// Work around this for now by deleting the function to ensure that the
|
||||||
|
// `no_modules.js` script doesn't have access to it. You won't need this
|
||||||
|
// hack when deploying over HTTP.
|
||||||
|
delete WebAssembly.instantiateStreaming;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- this is the JS generated by the `wasm-bindgen` CLI tool -->
|
||||||
|
<script src="plotter.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// We'll defer our execution until the wasm is ready to go.
|
||||||
|
// Here we tell bindgen the path to the wasm file so it can start
|
||||||
|
// initialization and return to us a promise when it's done.
|
||||||
|
console.debug("loading wasm…");
|
||||||
|
wasm_bindgen("./plotter_bg.wasm")
|
||||||
|
.then(on_wasm_loaded)
|
||||||
|
.catch(on_wasm_error);
|
||||||
|
|
||||||
|
function on_wasm_loaded() {
|
||||||
|
console.debug("wasm loaded. starting app…");
|
||||||
|
|
||||||
|
// This call installs a bunch of callbacks and then returns:
|
||||||
|
wasm_bindgen.start("the_canvas_id");
|
||||||
|
|
||||||
|
console.debug("app started.");
|
||||||
|
document.getElementById("center_text").remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
function on_wasm_error(error) {
|
||||||
|
console.error("Failed to start: " + error);
|
||||||
|
document.getElementById("center_text").innerHTML = `
|
||||||
|
<p>
|
||||||
|
An error occurred during loading:
|
||||||
|
</p>
|
||||||
|
<p style="font-family:Courier New">
|
||||||
|
${error}
|
||||||
|
</p>
|
||||||
|
<p style="font-size:14px">
|
||||||
|
Make sure you use a modern browser with WebGL and WASM enabled.
|
||||||
|
</p>`;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!--Register Service Worker-->
|
||||||
|
<script>
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
window.addEventListener('load', function() {
|
||||||
|
navigator.serviceWorker.register('/sw.js');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
|
|
||||||
|
<!-- Powered by egui: https://github.com/emilk/egui/ -->
|
27
docs/manifest.json
Normal file
27
docs/manifest.json
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
"name": "2b2t queue stats",
|
||||||
|
"short_name": "2b-queue",
|
||||||
|
"icons": [{
|
||||||
|
"src": "./icon-256.png",
|
||||||
|
"sizes": "256x256",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "./maskable_icon_x512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "any maskable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "./icon-1024.png",
|
||||||
|
"sizes": "1024x1024",
|
||||||
|
"type": "image/png"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"lang": "en-US",
|
||||||
|
"id": "/index.html",
|
||||||
|
"start_url": "./index.html",
|
||||||
|
"display": "standalone",
|
||||||
|
"background_color": "black",
|
||||||
|
"theme_color": "black"
|
||||||
|
}
|
BIN
docs/maskable_icon_x512.png
Normal file
BIN
docs/maskable_icon_x512.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 128 KiB |
1468
docs/plotter.js
Normal file
1468
docs/plotter.js
Normal file
File diff suppressed because it is too large
Load diff
BIN
docs/plotter_bg.wasm
Normal file
BIN
docs/plotter_bg.wasm
Normal file
Binary file not shown.
25
docs/sw.js
Normal file
25
docs/sw.js
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
var cacheName = 'egui-template-pwa';
|
||||||
|
var filesToCache = [
|
||||||
|
'./',
|
||||||
|
'./index.html',
|
||||||
|
'./eframe_template.js',
|
||||||
|
'./eframe_template_bg.wasm',
|
||||||
|
];
|
||||||
|
|
||||||
|
/* Start the service worker and cache all of the app's content */
|
||||||
|
self.addEventListener('install', function (e) {
|
||||||
|
e.waitUntil(
|
||||||
|
caches.open(cacheName).then(function (cache) {
|
||||||
|
return cache.addAll(filesToCache);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Serve cached content when offline */
|
||||||
|
self.addEventListener('fetch', function (e) {
|
||||||
|
e.respondWith(
|
||||||
|
caches.match(e.request).then(function (response) {
|
||||||
|
return response || fetch(e.request);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
143
src/app/datasource.rs
Normal file
143
src/app/datasource.rs
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use rand::Rng;
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use eframe::egui::plot::Value;
|
||||||
|
|
||||||
|
struct DataSource {
|
||||||
|
data : Arc<Mutex<Vec<Value>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DataSource {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self{ data: Arc::new(Mutex::new(Vec::new())) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view(&self) -> Vec<Value> { // TODO handle errors
|
||||||
|
return self.data.lock().unwrap().clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Data {
|
||||||
|
fn load(&mut self, url:&str);
|
||||||
|
fn view(&self) -> Vec<Value>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TpsData {
|
||||||
|
ds: DataSource,
|
||||||
|
load_interval : i64,
|
||||||
|
last_load : DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
struct TpsResponseData {
|
||||||
|
tps: f64
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TpsData {
|
||||||
|
pub fn new(load_interval:i64) -> Self {
|
||||||
|
Self { ds: DataSource::new() , last_load: Utc::now(), load_interval }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Data for TpsData{
|
||||||
|
fn load(&mut self, url:&str) {
|
||||||
|
if (Utc::now() - self.last_load).num_seconds() < self.load_interval { return; }
|
||||||
|
self.last_load = Utc::now();
|
||||||
|
let ds_data = self.ds.data.clone();
|
||||||
|
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() }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ChatData {
|
||||||
|
ds : DataSource,
|
||||||
|
load_interval : i64,
|
||||||
|
last_load : DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
struct ChatResponseData {
|
||||||
|
volume: f64
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChatData {
|
||||||
|
pub fn new(load_interval:i64) -> Self {
|
||||||
|
Self { ds: DataSource::new() , last_load: Utc::now(), load_interval }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Data for ChatData{
|
||||||
|
fn load(&mut self, url:&str) {
|
||||||
|
if (Utc::now() - self.last_load).num_seconds() < self.load_interval { return; }
|
||||||
|
self.last_load = Utc::now();
|
||||||
|
let ds_data = self.ds.data.clone();
|
||||||
|
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() }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PlayerCountData {
|
||||||
|
ds : DataSource,
|
||||||
|
load_interval : i64,
|
||||||
|
last_load : DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
struct PlayerCountResponseData {
|
||||||
|
count: i32
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PlayerCountData {
|
||||||
|
pub fn new(load_interval:i64) -> Self {
|
||||||
|
Self { ds: DataSource::new() , last_load: Utc::now(), load_interval }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Data for PlayerCountData{
|
||||||
|
fn load(&mut self, url:&str) {
|
||||||
|
if (Utc::now() - self.last_load).num_seconds() < self.load_interval { return; }
|
||||||
|
self.last_load = Utc::now();
|
||||||
|
let ds_data = self.ds.data.clone();
|
||||||
|
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() }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct RandomData {
|
||||||
|
ds : DataSource,
|
||||||
|
load_interval : i64,
|
||||||
|
last_load : DateTime<Utc>,
|
||||||
|
rng: rand::rngs::ThreadRng,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RandomData {
|
||||||
|
pub fn new(load_interval:i64) -> Self {
|
||||||
|
Self { ds: DataSource::new() , last_load: Utc::now(), load_interval, rng : rand::thread_rng() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Data for RandomData{
|
||||||
|
fn load(&mut self, _url:&str) {
|
||||||
|
if (Utc::now() - self.last_load).num_seconds() < self.load_interval { return; }
|
||||||
|
self.last_load = Utc::now();
|
||||||
|
self.ds.data.lock().unwrap().push(Value {x:Utc::now().timestamp() as f64, y:self.rng.gen()});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view(&self) -> Vec<Value> { self.ds.view() }
|
||||||
|
}
|
70
src/app/mod.rs
Normal file
70
src/app/mod.rs
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
mod datasource;
|
||||||
|
|
||||||
|
use chrono::{DateTime, NaiveDateTime, Utc};
|
||||||
|
use datasource::{ChatData, PlayerCountData, TpsData, Data, RandomData};
|
||||||
|
use eframe::egui;
|
||||||
|
use eframe::egui::plot::{Line, Plot, Value, Values};
|
||||||
|
|
||||||
|
|
||||||
|
pub struct App {
|
||||||
|
player_count: PlayerCountData,
|
||||||
|
tps: TpsData,
|
||||||
|
chat: ChatData,
|
||||||
|
rand: RandomData,
|
||||||
|
sync_time:bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl App {
|
||||||
|
pub fn new(_cc: &eframe::CreationContext) -> Self {
|
||||||
|
Self {
|
||||||
|
player_count: PlayerCountData::new(60),
|
||||||
|
tps: TpsData::new(30),
|
||||||
|
chat: ChatData::new(15),
|
||||||
|
rand: RandomData::new(1),
|
||||||
|
sync_time: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl eframe::App for App {
|
||||||
|
fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
|
||||||
|
self.rand.load("");
|
||||||
|
egui::TopBottomPanel::top("??? wtf").show(ctx, |ui| {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
egui::widgets::global_dark_light_mode_switch(ui);
|
||||||
|
ui.heading("nnbot dashboard");
|
||||||
|
ui.checkbox(&mut self.sync_time, "Lock X to now");
|
||||||
|
ui.with_layout(egui::Layout::top_down(egui::Align::RIGHT), |ui| {
|
||||||
|
if ui.button("x").clicked() {
|
||||||
|
frame.quit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
egui::CentralPanel::default().show(ctx, |ui| {
|
||||||
|
let mut p = Plot::new("test").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);
|
||||||
|
|
||||||
|
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"),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
20
src/lib.rs
Normal file
20
src/lib.rs
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
mod app;
|
||||||
|
|
||||||
|
pub use app::App;
|
||||||
|
|
||||||
|
// When compiling for web:
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
use eframe::wasm_bindgen::{self, prelude::*};
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn start(canvas_id: &str) -> Result<(), eframe::wasm_bindgen::JsValue> {
|
||||||
|
// Make sure panics are logged using `console.error`.
|
||||||
|
console_error_panic_hook::set_once();
|
||||||
|
|
||||||
|
// Redirect tracing to console.log and friends:
|
||||||
|
tracing_wasm::set_as_global_default();
|
||||||
|
|
||||||
|
eframe::start_web(canvas_id, Box::new(|cc| Box::new(App::new(cc))))
|
||||||
|
}
|
15
src/main.rs
Normal file
15
src/main.rs
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
mod app;
|
||||||
|
|
||||||
|
use app::App;
|
||||||
|
|
||||||
|
// When compiling natively:
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
fn main() {
|
||||||
|
let native_options = eframe::NativeOptions::default();
|
||||||
|
|
||||||
|
eframe::run_native(
|
||||||
|
"2b2t queue stats",
|
||||||
|
native_options,
|
||||||
|
Box::new(|cc| Box::new(App::new(cc))),
|
||||||
|
);
|
||||||
|
}
|
Loading…
Reference in a new issue