mirror of
https://git.alemi.dev/dashboard.git
synced 2024-11-14 03:49:19 +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
2ce61c03a3
commit
8e29b59c35
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