mirror of
https://github.com/Astatin3/unshell.git
synced 2026-06-09 06:47:59 -06:00
Move files into old directory
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
use egui::Frame;
|
||||
use egui_tiles::Tree;
|
||||
|
||||
use crate::{
|
||||
FORCE_REDRAW_DELAY,
|
||||
app::{AppState, windows::WindowWrapper},
|
||||
};
|
||||
|
||||
/// We derive Deserialize/Serialize so we can persist app state on shutdown.
|
||||
#[derive(serde::Deserialize, serde::Serialize)]
|
||||
#[serde(default)] // if we add new fields, give them default values when deserializing old state
|
||||
pub struct TemplateApp {
|
||||
// tab: Tab,
|
||||
state: AppState,
|
||||
tree: Tree<WindowWrapper>,
|
||||
}
|
||||
|
||||
impl Default for TemplateApp {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
state: AppState::default(),
|
||||
tree: egui_tiles::Tree::new_horizontal("tree_root", Vec::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TemplateApp {
|
||||
/// Called once before the first frame.
|
||||
pub fn new(cc: &eframe::CreationContext<'_>) -> Self {
|
||||
// This is also where you can customize the look and feel of egui using
|
||||
// `cc.egui_ctx.set_visuals` and `cc.egui_ctx.set_fonts`.
|
||||
|
||||
// Load previous app state (if any).
|
||||
// Note that you must enable the `persistence` feature for this to work.
|
||||
if let Some(storage) = cc.storage {
|
||||
eframe::get_value(storage, eframe::APP_KEY).unwrap_or_default()
|
||||
} else {
|
||||
Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl eframe::App for TemplateApp {
|
||||
/// Called by the framework to save state before shutdown.
|
||||
fn save(&mut self, storage: &mut dyn eframe::Storage) {
|
||||
eframe::set_value(storage, eframe::APP_KEY, self);
|
||||
}
|
||||
|
||||
/// Called each time the UI needs repainting, which may be many times per second.
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
ctx.plugin_or_default::<egui_async::EguiAsyncPlugin>();
|
||||
|
||||
if !self.state.auth.logged_in() {
|
||||
egui::CentralPanel::default()
|
||||
.frame(Frame::central_panel(&ctx.style()).inner_margin(0))
|
||||
.show(ctx, |ui| self.state.auth.update(ui));
|
||||
} else {
|
||||
egui::TopBottomPanel::top("tab_panel").show(ctx, |ui| {
|
||||
// The top panel is often a good place for a menu bar:
|
||||
|
||||
egui::MenuBar::new().ui(ui, |ui| {
|
||||
ui.menu_button("File", |ui| {
|
||||
ui.label("File");
|
||||
});
|
||||
|
||||
ui.menu_button("View", |ui| {
|
||||
ui.label("View");
|
||||
|
||||
self.state.labels(&mut self.tree, ui);
|
||||
});
|
||||
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
egui::widgets::global_theme_preference_switch(ui);
|
||||
});
|
||||
|
||||
// ui.style
|
||||
});
|
||||
});
|
||||
|
||||
egui::TopBottomPanel::bottom("tab_panel").show(ctx, |ui| {
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
ui.label(format!("UnShell UI {}", env!("CARGO_PKG_VERSION")));
|
||||
egui::warn_if_debug_build(ui);
|
||||
});
|
||||
});
|
||||
|
||||
egui::CentralPanel::default()
|
||||
.frame(Frame::central_panel(&ctx.style()).inner_margin(0))
|
||||
.show(ctx, |ui| self.tree.ui(&mut self.state, ui));
|
||||
}
|
||||
|
||||
ctx.request_repaint_after(FORCE_REDRAW_DELAY);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
mod app;
|
||||
mod windows;
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::{
|
||||
app::windows::WindowWrapper, auth::Auth, config::Config, flowchart::FlowChart,
|
||||
interface::InterfaceWindow, log_viewer::LogViewer, payload_config::PayloadConfig,
|
||||
};
|
||||
pub use app::TemplateApp;
|
||||
use egui_tiles::{TileId, Tree};
|
||||
|
||||
#[derive(Default, serde::Deserialize, serde::Serialize)]
|
||||
pub struct AppState {
|
||||
pub auth: Auth,
|
||||
|
||||
pub open_windows: HashSet<AppWindow>,
|
||||
|
||||
pub flowchart: FlowChart,
|
||||
pub config: Config,
|
||||
pub payload_config: PayloadConfig,
|
||||
pub log_viewer: LogViewer,
|
||||
pub interface: InterfaceWindow,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
pub fn labels(&mut self, tree: &mut Tree<WindowWrapper>, ui: &mut egui::Ui) {
|
||||
for (_, (key, name)) in (vec![
|
||||
(AppWindow::Flowchart, "Flowchart"),
|
||||
(AppWindow::PayloadConfig, "Payload Config"),
|
||||
(AppWindow::Config, "Config"),
|
||||
(AppWindow::LogViewer, "Log Viewer"),
|
||||
(AppWindow::InterfaceWindow, "Tree Test"),
|
||||
])
|
||||
.iter()
|
||||
.enumerate()
|
||||
{
|
||||
let enabled = self.open_windows.contains(&key);
|
||||
|
||||
if ui.selectable_label(enabled, *name).clicked() {
|
||||
if enabled {
|
||||
self.close_window(tree, key);
|
||||
} else {
|
||||
self.open_window(tree, key, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn close_window(&mut self, tree: &mut Tree<WindowWrapper>, key: &AppWindow) {
|
||||
match Self::find_pane_id(*key, tree) {
|
||||
Some(tid) => {
|
||||
let tid = tid.clone();
|
||||
tree.remove_recursively(tid);
|
||||
tree.tiles.remove(tid);
|
||||
self.open_windows.remove(&key);
|
||||
}
|
||||
None => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn open_window(&mut self, tree: &mut Tree<WindowWrapper>, key: &AppWindow, name: &str) {
|
||||
let tid = tree.tiles.insert_pane(WindowWrapper {
|
||||
name: name.to_string(),
|
||||
window: *key,
|
||||
});
|
||||
|
||||
match self.open_windows.len() {
|
||||
0 => {
|
||||
tree.root = Some(tid);
|
||||
}
|
||||
1 => {
|
||||
let old_root = tree.root.unwrap();
|
||||
let tab_id = tree.tiles.insert_tab_tile(vec![old_root, tid]);
|
||||
tree.root = Some(tab_id);
|
||||
tree.make_active(|t, _| t == tid);
|
||||
}
|
||||
_ => {
|
||||
let root = tree.root().unwrap();
|
||||
let n = tree.tiles.get_container(root).unwrap().num_children();
|
||||
tree.move_tile_to_container(tid, tree.root.unwrap().clone(), n, true);
|
||||
}
|
||||
}
|
||||
self.open_windows.insert(key.clone());
|
||||
}
|
||||
|
||||
fn find_pane_id(window_type: AppWindow, tree: &Tree<WindowWrapper>) -> Option<&TileId> {
|
||||
for (tid, window) in tree.tiles.iter() {
|
||||
match window {
|
||||
egui_tiles::Tile::Pane(pane) => {
|
||||
if pane.window == window_type {
|
||||
return Some(tid);
|
||||
}
|
||||
}
|
||||
egui_tiles::Tile::Container(_) => {}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, serde::Deserialize, serde::Serialize, PartialEq, Eq, Hash)]
|
||||
pub enum AppWindow {
|
||||
Flowchart,
|
||||
Config,
|
||||
PayloadConfig,
|
||||
LogViewer,
|
||||
InterfaceWindow,
|
||||
}
|
||||
|
||||
impl AppWindow {
|
||||
fn update(&self, state: &mut AppState, ui: &mut egui::Ui) {
|
||||
match self {
|
||||
AppWindow::Flowchart => state.flowchart.paint(ui),
|
||||
AppWindow::Config => state.config.update(&mut state.auth, ui),
|
||||
AppWindow::PayloadConfig => state.payload_config.update(ui),
|
||||
AppWindow::LogViewer => state.log_viewer.update(&mut state.auth, ui),
|
||||
AppWindow::InterfaceWindow => state.interface.update(&mut state.auth, ui),
|
||||
}
|
||||
}
|
||||
|
||||
fn render_title_buttons(&self, state: &mut AppState, ui: &mut egui::Ui) {
|
||||
match self {
|
||||
AppWindow::Flowchart => {
|
||||
state.flowchart.titlebar_buttons(ui);
|
||||
}
|
||||
AppWindow::Config => {
|
||||
state.config.titlebar_buttons(ui);
|
||||
}
|
||||
_ => {
|
||||
ui.label("");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
use egui::Rect;
|
||||
|
||||
use crate::app::{AppState, AppWindow};
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize)]
|
||||
pub struct WindowWrapper {
|
||||
pub name: String,
|
||||
pub window: AppWindow,
|
||||
}
|
||||
|
||||
impl egui_tiles::Behavior<WindowWrapper> for AppState {
|
||||
fn tab_title_for_pane(&mut self, pane: &WindowWrapper) -> egui::WidgetText {
|
||||
format!("{}", pane.name).into()
|
||||
}
|
||||
|
||||
fn pane_ui(
|
||||
&mut self,
|
||||
ui: &mut egui::Ui,
|
||||
_tile_id: egui_tiles::TileId,
|
||||
pane: &mut WindowWrapper,
|
||||
) -> egui_tiles::UiResponse {
|
||||
let mut ret = egui_tiles::UiResponse::None;
|
||||
|
||||
let mut rect = Rect::NOTHING;
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
rect = ui.max_rect();
|
||||
|
||||
let bg_color = ui.style().visuals.extreme_bg_color;
|
||||
|
||||
ui.painter().rect_filled(rect, 0.0, bg_color);
|
||||
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.label(&pane.name);
|
||||
});
|
||||
});
|
||||
|
||||
let mut open_space = Rect::NOTHING;
|
||||
|
||||
#[allow(deprecated)]
|
||||
ui.allocate_ui_at_rect(rect, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
pane.window.render_title_buttons(self, ui);
|
||||
open_space = ui.available_rect_before_wrap();
|
||||
})
|
||||
});
|
||||
|
||||
let drag_sense = ui.interact(
|
||||
open_space,
|
||||
ui.id().with(&format!("Pane_{}_sense", pane.name)),
|
||||
egui::Sense::drag(),
|
||||
);
|
||||
|
||||
if drag_sense.drag_started() {
|
||||
ret = egui_tiles::UiResponse::DragStarted;
|
||||
}
|
||||
if drag_sense.hovered() {
|
||||
ui.ctx().set_cursor_icon(egui::CursorIcon::Grab);
|
||||
}
|
||||
|
||||
pane.window.update(self, ui);
|
||||
|
||||
ret
|
||||
}
|
||||
|
||||
fn is_tab_closable(
|
||||
&self,
|
||||
tiles: &egui_tiles::Tiles<WindowWrapper>,
|
||||
tile_id: egui_tiles::TileId,
|
||||
) -> bool {
|
||||
match tiles.get(tile_id).unwrap() {
|
||||
egui_tiles::Tile::Pane(_) => true,
|
||||
egui_tiles::Tile::Container(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_tab_close(
|
||||
&mut self,
|
||||
tiles: &mut egui_tiles::Tiles<WindowWrapper>,
|
||||
tile_id: egui_tiles::TileId,
|
||||
) -> bool {
|
||||
match tiles.get(tile_id).unwrap() {
|
||||
egui_tiles::Tile::Pane(pane) => self.open_windows.remove(&pane.window),
|
||||
egui_tiles::Tile::Container(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn tab_bar_color(&self, visuals: &egui::Visuals) -> egui::Color32 {
|
||||
visuals.panel_fill // same as the tab contents
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
use serde::de::DeserializeOwned;
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen::prelude::*;
|
||||
use web_sys::js_sys::Promise;
|
||||
|
||||
pub struct PromiseWrapper<T> {
|
||||
state: Rc<RefCell<PromiseState<T>>>,
|
||||
}
|
||||
|
||||
enum PromiseState<T> {
|
||||
Pending,
|
||||
Resolved(T),
|
||||
Rejected(String),
|
||||
}
|
||||
|
||||
impl<T: 'static> PromiseWrapper<T>
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
/// Create a new PromiseWrapper from a JavaScript Promise
|
||||
/// The promise should resolve to a Response object (from fetch)
|
||||
pub fn new(promise: Promise) -> Self {
|
||||
let state = Rc::new(RefCell::new(PromiseState::Pending));
|
||||
let state_clone = state.clone();
|
||||
let state_clone2 = state.clone();
|
||||
|
||||
// Success callback
|
||||
let success = Closure::once(move |value: JsValue| {
|
||||
if let Ok(response) = value.dyn_into::<web_sys::Response>() {
|
||||
if let Ok(json_promise) = response.json() {
|
||||
let state_inner = state_clone.clone();
|
||||
|
||||
let json_success = Closure::once(move |json_value: JsValue| {
|
||||
*state_inner.borrow_mut() = if let Some(body) = json_value.as_string() {
|
||||
match serde_json::from_str(&body) {
|
||||
Ok(data) => PromiseState::Resolved(data),
|
||||
Err(e) => PromiseState::Rejected(format!("{:?}", e)),
|
||||
}
|
||||
} else {
|
||||
PromiseState::Rejected(format!("Response was not a string"))
|
||||
};
|
||||
|
||||
match serde_wasm_bindgen::from_value::<T>(json_value) {
|
||||
Ok(data) => *state_inner.borrow_mut() = PromiseState::Resolved(data),
|
||||
Err(e) => {
|
||||
*state_inner.borrow_mut() =
|
||||
PromiseState::Rejected(format!("{:?}", e))
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let _ = json_promise.then(&json_success);
|
||||
json_success.forget();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Error callback
|
||||
let error = Closure::once(move |err: JsValue| {
|
||||
*state_clone2.borrow_mut() = PromiseState::Rejected(
|
||||
err.as_string()
|
||||
.unwrap_or_else(|| "Unknown error".to_string()),
|
||||
);
|
||||
});
|
||||
|
||||
let _ = promise.then2(&success, &error);
|
||||
success.forget();
|
||||
error.forget();
|
||||
|
||||
Self { state }
|
||||
}
|
||||
|
||||
/// Poll the promise to check if it has completed
|
||||
/// Returns Some(T) if resolved successfully, None if still pending or rejected
|
||||
/// This is a lightweight check that's safe to call every frame
|
||||
#[inline]
|
||||
pub fn poll(&self) -> Option<T>
|
||||
where
|
||||
T: Clone,
|
||||
{
|
||||
match &*self.state.borrow() {
|
||||
PromiseState::Resolved(value) => Some(value.clone()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the promise is still pending (lightweight)
|
||||
#[inline]
|
||||
pub fn is_pending(&self) -> bool {
|
||||
matches!(&*self.state.borrow(), PromiseState::Pending)
|
||||
}
|
||||
|
||||
/// Check if the promise was rejected (lightweight)
|
||||
#[inline]
|
||||
pub fn is_rejected(&self) -> bool {
|
||||
matches!(&*self.state.borrow(), PromiseState::Rejected(_))
|
||||
}
|
||||
|
||||
/// Get the error message if rejected
|
||||
pub fn error(&self) -> Option<String> {
|
||||
match &*self.state.borrow() {
|
||||
PromiseState::Rejected(err) => Some(err.clone()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
use egui::{Align2, Area, Color32, Frame, Order, Sense, UiKind, Vec2, mutex::Mutex};
|
||||
use serde::{Serialize, de::DeserializeOwned};
|
||||
use serde_json::json;
|
||||
use std::sync::Arc;
|
||||
use wasm_bindgen::prelude::Closure;
|
||||
|
||||
use unshell::Result;
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize)]
|
||||
pub struct Auth {
|
||||
// Auth Stuff
|
||||
token: Option<Token>,
|
||||
#[serde(skip)]
|
||||
auth_state: Arc<Mutex<AuthState>>,
|
||||
|
||||
// UI Stuff
|
||||
username: String,
|
||||
#[serde(skip)]
|
||||
password: String,
|
||||
show_password: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, PartialEq, Eq)]
|
||||
struct Token {
|
||||
expiration: u128,
|
||||
token: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
enum AuthState {
|
||||
Unset,
|
||||
NotLoggedIn,
|
||||
RequestSent,
|
||||
Authorised(Token),
|
||||
Error(String),
|
||||
}
|
||||
|
||||
impl Default for AuthState {
|
||||
fn default() -> Self {
|
||||
Self::Unset
|
||||
}
|
||||
}
|
||||
|
||||
impl Auth {
|
||||
/// Refresh the authentication state
|
||||
pub fn logged_in(&mut self) -> bool {
|
||||
match (self.token.is_some(), &*self.auth_state.lock()) {
|
||||
// The client is actually authorized
|
||||
(true, AuthState::Authorised(_)) => true,
|
||||
|
||||
// If the user has just reloaded the session,
|
||||
// the AuthState is not automatically set by any other process
|
||||
(true, AuthState::Unset) => true,
|
||||
|
||||
// If the authentication state has been updated to unauthorized, delete the token
|
||||
(true, _) => {
|
||||
self.token = None;
|
||||
false
|
||||
}
|
||||
|
||||
// If the authentication state has been updated to authorized, set the token
|
||||
(false, AuthState::Authorised(token)) => {
|
||||
self.token = Some(token.clone());
|
||||
|
||||
// Also clear the password because it is bad to store it
|
||||
self.password.clear();
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
// The client is actually unauthorized
|
||||
(false, _) => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, ui: &mut egui::Ui) {
|
||||
Area::new("Auth".into())
|
||||
.kind(UiKind::Modal)
|
||||
.sense(Sense::hover())
|
||||
.anchor(Align2::CENTER_CENTER, Vec2::ZERO)
|
||||
.order(Order::Foreground)
|
||||
.interactable(true)
|
||||
.show(ui.ctx(), |ui| {
|
||||
Frame::popup(ui.style()).show(ui, |ui| {
|
||||
ui.heading("UnShell Login");
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Username");
|
||||
ui.text_edit_singleline(&mut self.username);
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Password");
|
||||
let _ = ui.add(
|
||||
// [ui.available_width(), 24.],
|
||||
egui::TextEdit::singleline(&mut self.password)
|
||||
.password(!self.show_password),
|
||||
);
|
||||
|
||||
self.show_password = ui.button("Show").is_pointer_button_down_on();
|
||||
|
||||
// ui.toggle_value(&mut self.show_password, "Show");
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
let (show_spinner, err_text) = match *self.auth_state.lock() {
|
||||
AuthState::Error(ref e) => {
|
||||
// self.login_button(ui);
|
||||
(false, e.clone())
|
||||
}
|
||||
AuthState::RequestSent => (true, "".into()),
|
||||
_ => (false, "".into()),
|
||||
};
|
||||
|
||||
if !show_spinner {
|
||||
if ui.button("Login").clicked() {
|
||||
let state = self.auth_state.clone();
|
||||
|
||||
crate::httpPost(
|
||||
"/api/auth",
|
||||
&json!({
|
||||
"username": self.username.clone(),
|
||||
"password": self.password.clone()
|
||||
})
|
||||
.to_string(),
|
||||
Closure::once_into_js(move |ok: bool, response: String| {
|
||||
*(state.lock()) = if ok {
|
||||
if let Ok(token) =
|
||||
serde_json::from_str::<Token>(&response)
|
||||
{
|
||||
AuthState::Authorised(token)
|
||||
} else {
|
||||
AuthState::Error("Malformed Response".into())
|
||||
}
|
||||
} else {
|
||||
AuthState::Error(response)
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
*(self.auth_state.lock()) = AuthState::RequestSent;
|
||||
}
|
||||
} else {
|
||||
ui.spinner();
|
||||
}
|
||||
|
||||
ui.colored_label(Color32::RED, err_text);
|
||||
});
|
||||
});
|
||||
});
|
||||
// });
|
||||
// });
|
||||
}
|
||||
|
||||
pub fn get<R, F>(&self, path: &str, ret: F) -> Result<()>
|
||||
where
|
||||
F: FnOnce(R) + 'static,
|
||||
R: DeserializeOwned,
|
||||
{
|
||||
if let Some(ref token) = self.token {
|
||||
let state = self.auth_state.clone();
|
||||
crate::httpGetAuth(
|
||||
path,
|
||||
format!("Bearer {}", token.token),
|
||||
Closure::once_into_js(move |ok: bool, response: String| {
|
||||
if ok {
|
||||
if let Ok(value) = serde_json::from_str::<R>(&response) {
|
||||
ret(value)
|
||||
} else {
|
||||
*(state.lock()) = AuthState::Error("Malformed Response".into());
|
||||
}
|
||||
} else {
|
||||
*(state.lock()) = AuthState::Error(response);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn post<R, T, F>(&self, path: &str, data: &T, ret: F) -> Result<()>
|
||||
where
|
||||
R: DeserializeOwned,
|
||||
T: Serialize,
|
||||
F: FnOnce(R) + 'static,
|
||||
{
|
||||
if let Some(ref token) = self.token {
|
||||
let state = self.auth_state.clone();
|
||||
crate::httpPostAuth(
|
||||
path,
|
||||
format!("Bearer {}", token.token),
|
||||
&serde_json::to_string(data)?,
|
||||
Closure::once_into_js(move |ok: bool, response: String| {
|
||||
if ok {
|
||||
if let Ok(value) = serde_json::from_str::<R>(&response) {
|
||||
ret(value)
|
||||
} else {
|
||||
*(state.lock()) = AuthState::Error("Malformed Response".into());
|
||||
}
|
||||
} else {
|
||||
*(state.lock()) = AuthState::Error(response);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Auth {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
token: Default::default(),
|
||||
auth_state: Arc::new(Mutex::new(AuthState::NotLoggedIn)),
|
||||
username: Default::default(),
|
||||
password: Default::default(),
|
||||
show_password: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
impl Auth {
|
||||
// pub fn get_test(url: String) -> Promise {
|
||||
// let fut = Self::get_async(&url);
|
||||
|
||||
// // wasm_bindgen_futures::(fut)
|
||||
// }
|
||||
|
||||
// pub fn get_async<R>(&self, url: &str) -> PromiseWrapper<R>
|
||||
// where
|
||||
// // F: FnOnce(R) + 'static,
|
||||
// R: DeserializeOwned + 'static,
|
||||
// {
|
||||
// let token = self.token.as_ref().unwrap();
|
||||
|
||||
// let opts = RequestInit::new();
|
||||
// opts.set_method("GET");
|
||||
|
||||
// let request = Request::new_with_str_and_init(url, &opts).unwrap();
|
||||
|
||||
// let headers = request.headers();
|
||||
|
||||
// headers.set("content-type", "application/json").unwrap();
|
||||
|
||||
// headers
|
||||
// .set("Authorization", &format!("Bearer {}", token.token))
|
||||
// .unwrap();
|
||||
|
||||
// let window = web_sys::window().unwrap();
|
||||
// let promise = window.fetch_with_request(&request);
|
||||
|
||||
// // wasm_bindgen_futures::spawn_local(async move {
|
||||
// // let resp_value = JsFuture::from(window.fetch_with_request(&request))
|
||||
// // .await
|
||||
// // .unwrap();
|
||||
|
||||
// // // `resp_value` is a `Response` object.
|
||||
// // assert!(resp_value.is_instance_of::<Response>());
|
||||
// // let resp: Response = resp_value.dyn_into().unwrap();
|
||||
|
||||
// // // Convert this other `Promise` into a rust `Future`.
|
||||
// // if let Ok(json) = resp.json() {
|
||||
// // if let Ok(json) = JsFuture::from(json).await {
|
||||
// // crate::log(&format!("{json:?}"));
|
||||
// // // let json = ;
|
||||
// // }
|
||||
// // }
|
||||
|
||||
// // // crate::log(text);
|
||||
// // // Any follow-up work here
|
||||
// // });
|
||||
|
||||
// PromiseWrapper::new(promise)
|
||||
|
||||
// // Ok(())
|
||||
// }
|
||||
|
||||
// pub fn get_async_callback<R, F>(&self, url: &str, callback: F) -> Result<()>
|
||||
// where
|
||||
// F: FnOnce(R) + 'static,
|
||||
// R: DeserializeOwned + 'static,
|
||||
// {
|
||||
// if self.token.is_none() {
|
||||
// return Err(ModuleError::Error("Not authenticated".into()));
|
||||
// }
|
||||
|
||||
// let token = self.token.clone().unwrap();
|
||||
|
||||
// let url = url.to_string();
|
||||
|
||||
// let state_clone = self.auth_state.clone();
|
||||
|
||||
// wasm_bindgen_futures::future_to_promise(async move {
|
||||
// let result = Self::get_async(&url, &token).await;
|
||||
|
||||
// match result {
|
||||
// Ok(result) => callback(result),
|
||||
// Err(err) => (*state_clone.lock()) = AuthState::Error(err.into()),
|
||||
// }
|
||||
|
||||
// Ok(JsValue::NULL)
|
||||
// });
|
||||
|
||||
// Ok(())
|
||||
// }
|
||||
|
||||
// async fn get_async<R>(url: &str, token: &Token) -> Result<R>
|
||||
// where
|
||||
// R: DeserializeOwned + 'static,
|
||||
// {
|
||||
// let res = reqwest::Client::new()
|
||||
// .get(format!("http://localhost:8080{url}"))
|
||||
// .bearer_auth(&token.token)
|
||||
// .send()
|
||||
// .await
|
||||
// .map_err(|e| ModuleError::Error(e.to_string()))?;
|
||||
|
||||
// match res.error_for_status() {
|
||||
// Ok(res) => res
|
||||
// .json()
|
||||
// .await
|
||||
// .map_err(|e| ModuleError::Error(e.to_string())),
|
||||
// Err(err) => Err(ModuleError::Error(format!(
|
||||
// "Server returned error: {err:?}"
|
||||
// ))),
|
||||
// }
|
||||
|
||||
// // .json::<R>()
|
||||
// // .await
|
||||
// // .map_err(|e| ModuleError::Error(e.to_string()))?;
|
||||
|
||||
// // serde_json::from_str(&res).map_err(|e| ModuleError::SerdeJsonError(e.to_string()))
|
||||
// }
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use egui_extras::{Column, TableBuilder};
|
||||
|
||||
use crate::auth::Auth;
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize)]
|
||||
pub struct Config {
|
||||
tree_option: String,
|
||||
#[serde(skip)]
|
||||
state: Arc<Mutex<ConfigState>>,
|
||||
// trees: Arc<Mutex<Option<Vec<String>>>>,
|
||||
// #[serde(skip)]
|
||||
// is_requesting: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ConfigState {
|
||||
trees: Option<Vec<String>>,
|
||||
tree_keys: Option<HashMap<String, String>>,
|
||||
is_requesting: bool,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
tree_option: "".into(),
|
||||
state: Arc::new(Mutex::new(ConfigState {
|
||||
trees: None,
|
||||
tree_keys: None,
|
||||
is_requesting: false,
|
||||
})), // trees: Arc::new(Mutex::new(None)),
|
||||
// is_requesting: Arc::new(AtomicBool::new(false)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn update(&mut self, auth: &mut Auth, ui: &mut egui::Ui) {
|
||||
let state_lock = self.state.lock().unwrap();
|
||||
let tree_list_none = state_lock.trees.is_none();
|
||||
let key_list_none = state_lock.tree_keys.is_none();
|
||||
let is_requesting = state_lock.is_requesting;
|
||||
|
||||
if !tree_list_none
|
||||
&& !state_lock
|
||||
.trees
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.contains(&self.tree_option)
|
||||
{
|
||||
self.tree_option.clear();
|
||||
}
|
||||
|
||||
drop(state_lock);
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
if tree_list_none && !is_requesting {
|
||||
self.state.lock().unwrap().is_requesting = true;
|
||||
let state_clone = self.state.clone();
|
||||
|
||||
auth.get("/api/trees", move |response: Vec<String>| {
|
||||
let mut state_lock = state_clone.lock().unwrap();
|
||||
state_lock.trees = Some(response);
|
||||
state_lock.is_requesting = false;
|
||||
drop(state_lock);
|
||||
})
|
||||
.unwrap();
|
||||
} else if tree_list_none && is_requesting {
|
||||
ui.spinner();
|
||||
}
|
||||
|
||||
// let state_lock = self.state.lock().unwrap();
|
||||
// // This might have changed since the above api call
|
||||
// tree_list_none = state_lock.trees.is_none();
|
||||
|
||||
if !self.tree_option.is_empty() && !tree_list_none {
|
||||
// ui.horizontal(|ui| {
|
||||
// ui.label(&format!("Tree: {}", self.tree_option));
|
||||
// });
|
||||
|
||||
if key_list_none && !is_requesting {
|
||||
self.state.lock().unwrap().is_requesting = true;
|
||||
let state_clone = self.state.clone();
|
||||
|
||||
auth.get(
|
||||
&format!("/api/values/{}", self.tree_option),
|
||||
move |response: Result<HashMap<String, String>, String>| {
|
||||
let mut state_lock = state_clone.lock().unwrap();
|
||||
state_lock.tree_keys = Some(response.unwrap());
|
||||
state_lock.is_requesting = false;
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
} else if key_list_none && is_requesting {
|
||||
ui.spinner();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if !key_list_none {
|
||||
// let body_text_size = TextStyle::Body.resolve(ui.style()).size;
|
||||
// use egui_extras::{Size, StripBuilder};
|
||||
// StripBuilder::new(ui)
|
||||
// .size(Size::remainder().at_least(100.0)) // for the table
|
||||
// .size(Size::exact(body_text_size))
|
||||
// .vertical(|mut strip| {
|
||||
// strip.cell(|ui| {
|
||||
|
||||
egui::ScrollArea::both().show(ui, |ui| {
|
||||
let table = TableBuilder::new(ui)
|
||||
.striped(true)
|
||||
.resizable(true)
|
||||
.cell_layout(egui::Layout::left_to_right(egui::Align::Center))
|
||||
.column(Column::auto())
|
||||
.column(Column::auto())
|
||||
.min_scrolled_height(0.0)
|
||||
.sense(egui::Sense::click());
|
||||
|
||||
table
|
||||
.header(20., |mut header| {
|
||||
header.col(|ui| {
|
||||
ui.strong("key");
|
||||
});
|
||||
header.col(|ui| {
|
||||
ui.strong("value");
|
||||
});
|
||||
})
|
||||
.body(|mut body| {
|
||||
let state_lock = self.state.lock().unwrap();
|
||||
let map = state_lock.tree_keys.as_ref().unwrap();
|
||||
|
||||
for (key, value) in (map).iter() {
|
||||
// // let runtime = self.current_runtimes
|
||||
|
||||
body.row(18., |mut row| {
|
||||
row.col(|ui| {
|
||||
ui.label(key);
|
||||
});
|
||||
row.col(|ui| {
|
||||
ui.label(value);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
// });
|
||||
// });
|
||||
}
|
||||
}
|
||||
|
||||
pub fn titlebar_buttons(&mut self, ui: &mut egui::Ui) {
|
||||
let state_lock = self.state.lock().unwrap();
|
||||
let mut tree_list_none = state_lock.trees.is_none();
|
||||
|
||||
drop(state_lock);
|
||||
|
||||
if ui.button("Refresh").clicked() {
|
||||
let mut state_lock = self.state.lock().unwrap();
|
||||
|
||||
(*state_lock).trees = None;
|
||||
(*state_lock).tree_keys = None;
|
||||
drop(state_lock);
|
||||
|
||||
tree_list_none = true;
|
||||
}
|
||||
|
||||
if !tree_list_none {
|
||||
let state_lock = self.state.lock().unwrap();
|
||||
let trees = state_lock.trees.as_ref().unwrap().clone();
|
||||
drop(state_lock);
|
||||
|
||||
let before = &self.tree_option.clone();
|
||||
egui::ComboBox::from_id_salt("Select Tree")
|
||||
.selected_text(&self.tree_option)
|
||||
.show_ui(ui, |ui| {
|
||||
for tree in trees {
|
||||
ui.selectable_value(&mut self.tree_option, tree.clone(), tree);
|
||||
}
|
||||
});
|
||||
|
||||
if before.ne(&self.tree_option) {
|
||||
(*self.state.lock().unwrap()).tree_keys = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
use egui::{TextStyle, Ui};
|
||||
use egui_extras::{Column, TableBuilder};
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize)]
|
||||
pub struct Config {
|
||||
state: ConfigState,
|
||||
current_payloads: Vec<PayloadConfig>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize)]
|
||||
enum ConfigState {
|
||||
Base,
|
||||
NewConfig(PayloadConfig),
|
||||
EditConfig(usize, PayloadConfig),
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
state: ConfigState::Base,
|
||||
current_payloads: vec![PayloadConfig {
|
||||
name: "Test".to_string(),
|
||||
components: vec!["server".to_string()],
|
||||
// runtimes: Vec::new(),
|
||||
}],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
// pub fn title(&self) -> &str {
|
||||
// match self.state {
|
||||
// ConfigState::Base => "Config",
|
||||
// ConfigState::NewConfig(..) => "Config/New",
|
||||
// ConfigState::EditConfig(..) => "Config/Edit",
|
||||
// }
|
||||
// }
|
||||
|
||||
pub fn update(&mut self, ui: &mut Ui) {
|
||||
match &mut self.state {
|
||||
ConfigState::Base => self.table_ui(ui),
|
||||
ConfigState::EditConfig(_, config) => {
|
||||
ui.heading("Edit Payload");
|
||||
match Self::edit_ui(ui, config) {
|
||||
Some(true) => {
|
||||
if let ConfigState::EditConfig(n, config) =
|
||||
std::mem::replace(&mut self.state, ConfigState::Base)
|
||||
{
|
||||
self.current_payloads[n] = config;
|
||||
}
|
||||
}
|
||||
Some(false) => {
|
||||
self.state = ConfigState::Base;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
ConfigState::NewConfig(config) => {
|
||||
ui.heading("Edit Payload");
|
||||
match Self::edit_ui(ui, config) {
|
||||
Some(true) => {
|
||||
if let ConfigState::NewConfig(config) =
|
||||
std::mem::replace(&mut self.state, ConfigState::Base)
|
||||
{
|
||||
self.current_payloads.push(config);
|
||||
}
|
||||
}
|
||||
Some(false) => {
|
||||
self.state = ConfigState::Base;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn table_ui(&mut self, ui: &mut Ui) {
|
||||
ui.horizontal(|ui| {
|
||||
ui.heading("Payloads");
|
||||
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
if ui.button("New").clicked() {
|
||||
self.state = ConfigState::NewConfig(PayloadConfig::new());
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
let body_text_size = TextStyle::Body.resolve(ui.style()).size;
|
||||
use egui_extras::{Size, StripBuilder};
|
||||
StripBuilder::new(ui)
|
||||
.size(Size::remainder().at_least(100.0)) // for the table
|
||||
.size(Size::exact(body_text_size)) // for the source code link
|
||||
.vertical(|mut strip| {
|
||||
strip.cell(|ui| {
|
||||
egui::ScrollArea::horizontal().show(ui, |ui| {
|
||||
let table = TableBuilder::new(ui)
|
||||
.striped(true)
|
||||
.resizable(true)
|
||||
.cell_layout(egui::Layout::left_to_right(egui::Align::Center))
|
||||
.column(Column::auto().resizable(false))
|
||||
.column(Column::remainder())
|
||||
.column(Column::remainder())
|
||||
.column(Column::remainder())
|
||||
.column(Column::auto().resizable(false))
|
||||
.min_scrolled_height(0.0)
|
||||
.sense(egui::Sense::click());
|
||||
|
||||
table
|
||||
.header(20., |mut header| {
|
||||
header.col(|ui| {
|
||||
ui.strong("#");
|
||||
});
|
||||
header.col(|ui| {
|
||||
ui.strong("Name");
|
||||
});
|
||||
header.col(|ui| {
|
||||
ui.strong("Components");
|
||||
});
|
||||
header.col(|ui| {
|
||||
ui.strong("Runtimes");
|
||||
});
|
||||
header.col(|_| {});
|
||||
})
|
||||
.body(|mut body| {
|
||||
for i in 0..self.current_payloads.len() {
|
||||
// let runtime = self.current_runtimes
|
||||
|
||||
body.row(18., |mut row| {
|
||||
row.col(|ui| {
|
||||
ui.label(i.to_string());
|
||||
});
|
||||
row.col(|ui| {
|
||||
ui.label(self.current_payloads[i].name.clone());
|
||||
});
|
||||
row.col(|ui| {
|
||||
ui.label(format!(
|
||||
"{:?}",
|
||||
self.current_payloads[i].components.clone()
|
||||
));
|
||||
});
|
||||
row.col(|ui| {
|
||||
ui.label("A");
|
||||
});
|
||||
row.col(|ui| {
|
||||
if ui.button("Edit").clicked() {
|
||||
self.state = ConfigState::EditConfig(
|
||||
i,
|
||||
self.current_payloads[i].clone(),
|
||||
);
|
||||
}
|
||||
// if ui.button("Delete").clicked() {
|
||||
// self.state = ConfigState::EditConfig(
|
||||
// i,
|
||||
// self.current_payloads[i].clone(),
|
||||
// );
|
||||
// }
|
||||
});
|
||||
|
||||
if row.response().clicked() {
|
||||
self.state = ConfigState::EditConfig(
|
||||
i,
|
||||
self.current_payloads[i].clone(),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn edit_ui(ui: &mut Ui, config: &mut PayloadConfig) -> Option<bool> {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Name");
|
||||
ui.text_edit_singleline(&mut config.name);
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Components: ");
|
||||
|
||||
for component in vec!["client", "server"] {
|
||||
let enabled = config
|
||||
.components
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|s| s.1.eq(component));
|
||||
if ui.selectable_label(enabled.is_some(), component).clicked() {
|
||||
if let Some((i, _)) = enabled {
|
||||
let _ = config.components.remove(i);
|
||||
} else {
|
||||
config.components.push(component.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let mut ret = None;
|
||||
ui.horizontal(|ui| {
|
||||
ret = if ui.button("Back").clicked() {
|
||||
Some(false)
|
||||
} else if ui.button("Save").clicked() {
|
||||
Some(true)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
});
|
||||
|
||||
ret
|
||||
|
||||
// return None;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, serde::Deserialize, serde::Serialize)]
|
||||
struct PayloadConfig {
|
||||
name: String,
|
||||
components: Vec<String>,
|
||||
// runtimes: Vec<RuntimeConfig>,
|
||||
}
|
||||
|
||||
impl PayloadConfig {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
name: "New Payload".to_string(),
|
||||
components: Vec::new(),
|
||||
// runtimes: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
use egui::{Pos2, Rect, UiBuilder, Vec2};
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize)]
|
||||
pub struct DraggableContainer {
|
||||
pub pos: egui::Vec2, // Offset from center of clip_rect
|
||||
pub size: egui::Vec2,
|
||||
|
||||
is_dragging: bool,
|
||||
drag_offset: egui::Vec2,
|
||||
drag_id: String,
|
||||
|
||||
pub vel: egui::Vec2,
|
||||
}
|
||||
|
||||
impl DraggableContainer {
|
||||
// pub fn new(center_offset: egui::Vec2, size: egui::Vec2, id: usize) -> Self {
|
||||
// Self {
|
||||
// pos: center_offset,
|
||||
// size,
|
||||
// is_dragging: false,
|
||||
// drag_offset: egui::Vec2::ZERO,
|
||||
// drag_id: format!("flowchart_drag_area{}", id),
|
||||
// vel: Vec2::ZERO,
|
||||
// }
|
||||
// }
|
||||
|
||||
pub fn new_zero(id: usize) -> Self {
|
||||
Self {
|
||||
pos: Vec2::ZERO,
|
||||
size: Vec2 { x: 100., y: 100. },
|
||||
is_dragging: false,
|
||||
drag_offset: egui::Vec2::ZERO,
|
||||
drag_id: format!("flowchart_drag_area{}", id),
|
||||
vel: Vec2::ZERO,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show<R>(
|
||||
&mut self,
|
||||
ui: &mut egui::Ui,
|
||||
add_contents: impl FnOnce(&mut egui::Ui, &Rect) -> R,
|
||||
) -> R {
|
||||
// Calculate center of the clip rect
|
||||
let clip_center = Pos2::ZERO;
|
||||
|
||||
// Calculate actual position from center offset
|
||||
let center_pos = clip_center + self.pos;
|
||||
let pos = center_pos - self.size / 2.0; // Top-left corner from center
|
||||
let rect = egui::Rect::from_min_size(pos, self.size);
|
||||
|
||||
// Handle dragging logic
|
||||
let response = ui.interact(rect, ui.id().with(&self.drag_id), egui::Sense::drag());
|
||||
|
||||
// if response.secondary_clicked() {
|
||||
|
||||
// }
|
||||
|
||||
if response.drag_started() {
|
||||
self.is_dragging = true;
|
||||
if let Some(pointer_pos) = ui.input(|i| i.pointer.latest_pos()) {
|
||||
let pointer_pos = ui
|
||||
.ctx()
|
||||
.layer_transform_from_global(ui.painter().layer_id())
|
||||
.unwrap_or_default()
|
||||
* pointer_pos;
|
||||
self.drag_offset = center_pos - pointer_pos;
|
||||
}
|
||||
}
|
||||
|
||||
if response.dragged() && self.is_dragging {
|
||||
// Pointer code from https://github.com/emilk/egui/pull/7149
|
||||
if let Some(pointer_pos) = ui.input(|i| i.pointer.latest_pos()) {
|
||||
let pointer_pos = ui
|
||||
.ctx()
|
||||
.layer_transform_from_global(ui.painter().layer_id())
|
||||
.unwrap_or_default()
|
||||
* pointer_pos;
|
||||
let new_center = pointer_pos + self.drag_offset;
|
||||
self.pos = new_center - clip_center;
|
||||
}
|
||||
}
|
||||
|
||||
if response.drag_stopped() {
|
||||
self.is_dragging = false;
|
||||
}
|
||||
|
||||
// Create a child UI at the specified position
|
||||
let mut child_ui = ui.new_child(UiBuilder::new().max_rect(rect));
|
||||
|
||||
// Add contents
|
||||
add_contents(&mut child_ui, &rect)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
use egui::{Color32, Painter, Pos2, Rect, Scene, Shape, Ui};
|
||||
|
||||
use crate::flowchart::CONNECTION_STROKE;
|
||||
use crate::flowchart::GROUP_BORDER_MARGIN;
|
||||
use crate::flowchart::ITERATIONS;
|
||||
use crate::flowchart::RESOLUTION;
|
||||
use crate::flowchart::container::DraggableContainer;
|
||||
use crate::flowchart::group::convex_hull;
|
||||
use crate::flowchart::{BG_STROKE, TARGET_LINE_GAP};
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize)]
|
||||
|
||||
pub struct FlowChart {
|
||||
scene_rect: Rect,
|
||||
pub containers: Vec<DraggableContainer>,
|
||||
pub connections: Vec<(usize, usize)>,
|
||||
pub groups: Vec<Vec<usize>>,
|
||||
}
|
||||
|
||||
impl Default for FlowChart {
|
||||
fn default() -> Self {
|
||||
let mut this = Self {
|
||||
scene_rect: Rect::ZERO,
|
||||
containers: vec![
|
||||
DraggableContainer::new_zero(0),
|
||||
DraggableContainer::new_zero(1),
|
||||
DraggableContainer::new_zero(2),
|
||||
DraggableContainer::new_zero(3),
|
||||
DraggableContainer::new_zero(4),
|
||||
DraggableContainer::new_zero(5),
|
||||
DraggableContainer::new_zero(6),
|
||||
DraggableContainer::new_zero(7),
|
||||
],
|
||||
connections: vec![(0, 1), (1, 2), (1, 3), (1, 4), (3, 5), (3, 6), (3, 7)],
|
||||
groups: vec![vec![1, 3, 5, 7]],
|
||||
};
|
||||
|
||||
this.arrange_circle();
|
||||
this.arrange();
|
||||
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
impl FlowChart {
|
||||
pub fn arrange(&mut self) {
|
||||
for _ in 0..ITERATIONS {
|
||||
self.force(RESOLUTION);
|
||||
}
|
||||
}
|
||||
|
||||
fn paint_bg(rect: &Rect, painter: &Painter) {
|
||||
let h_start = (rect.min.x / TARGET_LINE_GAP).round() as i32;
|
||||
let h_end = ((rect.min.x + rect.width()) / TARGET_LINE_GAP).round() as i32 + 1;
|
||||
for n in h_start..h_end {
|
||||
painter.vline(n as f32 * TARGET_LINE_GAP, rect.y_range(), BG_STROKE);
|
||||
}
|
||||
|
||||
let v_start = (rect.min.y / TARGET_LINE_GAP).round() as i32;
|
||||
let v_end = ((rect.min.y + rect.height()) / TARGET_LINE_GAP).round() as i32 + 1;
|
||||
for n in v_start..v_end {
|
||||
painter.hline(rect.x_range(), n as f32 * TARGET_LINE_GAP, BG_STROKE);
|
||||
}
|
||||
}
|
||||
|
||||
fn paint_groups(groups: &Vec<Vec<usize>>, containers: &Vec<DraggableContainer>, ui: &mut Ui) {
|
||||
for group in groups {
|
||||
let mut points = Vec::new();
|
||||
|
||||
for n in group {
|
||||
let container = &containers[*n];
|
||||
let pos = container.pos.to_pos2();
|
||||
let size = container.size;
|
||||
points.append(&mut vec![
|
||||
Pos2 {
|
||||
x: pos.x - size.x / 2. - GROUP_BORDER_MARGIN,
|
||||
y: pos.y - size.x / 2. - GROUP_BORDER_MARGIN,
|
||||
},
|
||||
Pos2 {
|
||||
x: pos.x + size.x / 2. + GROUP_BORDER_MARGIN,
|
||||
y: pos.y - size.y / 2. - GROUP_BORDER_MARGIN,
|
||||
},
|
||||
Pos2 {
|
||||
x: pos.x - size.x / 2. - GROUP_BORDER_MARGIN,
|
||||
y: pos.y + size.y / 2. + GROUP_BORDER_MARGIN,
|
||||
},
|
||||
Pos2 {
|
||||
x: pos.x + size.x / 2. + GROUP_BORDER_MARGIN,
|
||||
y: pos.y + size.y / 2. + GROUP_BORDER_MARGIN,
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
let points = convex_hull(&points);
|
||||
|
||||
ui.painter().add(Shape::convex_polygon(
|
||||
points,
|
||||
Color32::DEBUG_COLOR,
|
||||
BG_STROKE,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn paint(&mut self, ui: &mut Ui) {
|
||||
let scene = Scene::new()
|
||||
// .max_inner_size([350.0, 1000.0])
|
||||
.zoom_range(0.1..=2.0);
|
||||
|
||||
let containers = &mut self.containers;
|
||||
let groups = &self.groups;
|
||||
|
||||
let mut inner_rect = Rect::NAN;
|
||||
let rect = &self.scene_rect.clone();
|
||||
|
||||
let response = scene
|
||||
.show(ui, &mut self.scene_rect, |mut ui| {
|
||||
Self::paint_bg(rect, ui.painter());
|
||||
Self::paint_groups(groups, containers, &mut ui);
|
||||
|
||||
for (a, b) in &self.connections {
|
||||
ui.painter().line_segment(
|
||||
[containers[*a].pos.to_pos2(), containers[*b].pos.to_pos2()],
|
||||
CONNECTION_STROKE,
|
||||
);
|
||||
}
|
||||
|
||||
for container in containers {
|
||||
container.show(&mut ui, |ui, rect| {
|
||||
ui.painter().rect(
|
||||
// ui.top
|
||||
*rect,
|
||||
0.,
|
||||
Color32::PURPLE,
|
||||
BG_STROKE,
|
||||
egui::StrokeKind::Outside,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
inner_rect = ui.min_rect();
|
||||
})
|
||||
.response;
|
||||
|
||||
if response.double_clicked() {
|
||||
self.scene_rect = inner_rect;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn titlebar_buttons(&mut self, ui: &mut egui::Ui) {
|
||||
if ui.button("Arrange").clicked() {
|
||||
self.arrange();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
use std::f32::consts::TAU;
|
||||
|
||||
use egui::Vec2;
|
||||
|
||||
use crate::flowchart::{
|
||||
ATTRACTION_STRENGTH, CENTER_ATTRACTION_STRENGTH, DAMPING, FlowChart, GROUP_ATTRACTION_STRENGTH,
|
||||
REPULSION_STRENGTH, REST_LENGTH,
|
||||
};
|
||||
|
||||
pub fn normalize(v: &Vec2) -> Vec2 {
|
||||
let len = v.length();
|
||||
if len > 0.0 {
|
||||
Vec2 {
|
||||
x: v.x / len,
|
||||
y: v.y / len,
|
||||
}
|
||||
} else {
|
||||
Vec2 { x: 0.0, y: 0.0 }
|
||||
}
|
||||
}
|
||||
|
||||
impl FlowChart {
|
||||
pub fn force(&mut self, delta_time: f32) {
|
||||
let num_nodes = self.containers.len();
|
||||
let mut forces = vec![Vec2::new(0.0, 0.0); num_nodes];
|
||||
|
||||
// Calculate repulsive forces between all nodes
|
||||
for i in 0..num_nodes {
|
||||
for j in (i + 1)..num_nodes {
|
||||
let diff = self.containers[i].pos - self.containers[j].pos;
|
||||
let dist = diff.length().max(0.1); // Prevent division by zero
|
||||
let force = normalize(&diff) * (REPULSION_STRENGTH / (dist * dist));
|
||||
|
||||
forces[i] = forces[i] + force;
|
||||
forces[j] = forces[j] + (force * -1.0);
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate attractive forces along connections
|
||||
for &(i, j) in &self.connections {
|
||||
let diff = self.containers[j].pos - self.containers[i].pos;
|
||||
let dist = diff.length();
|
||||
let displacement = dist - REST_LENGTH;
|
||||
let force = normalize(&diff) * (displacement * ATTRACTION_STRENGTH);
|
||||
|
||||
forces[i] = forces[i] + force;
|
||||
forces[j] = forces[j] + (force * -1.0);
|
||||
}
|
||||
|
||||
// Apply force to center
|
||||
for i in 0..num_nodes {
|
||||
let diff = self.containers[i].pos;
|
||||
let dist = diff.length();
|
||||
let displacement = dist - REST_LENGTH;
|
||||
let force = normalize(&diff) * (displacement * CENTER_ATTRACTION_STRENGTH);
|
||||
|
||||
forces[i] = forces[i] + force * -1.;
|
||||
}
|
||||
|
||||
let group_avg = &self
|
||||
.groups
|
||||
.iter()
|
||||
.map(|group| {
|
||||
let mut sum = Vec2::ZERO;
|
||||
for n in group {
|
||||
sum += self.containers[*n].pos;
|
||||
}
|
||||
sum / group.len() as f32
|
||||
})
|
||||
.collect::<Vec<Vec2>>();
|
||||
|
||||
for (group, group_avg) in self.groups.iter().zip(group_avg) {
|
||||
for i in 0..num_nodes {
|
||||
let diff = self.containers[i].pos - *group_avg;
|
||||
let dist = diff.length();
|
||||
let displacement = dist - REST_LENGTH;
|
||||
let force = normalize(&diff) * (displacement * GROUP_ATTRACTION_STRENGTH);
|
||||
|
||||
if group.contains(&i) {
|
||||
forces[i] = forces[i] + force * -1.;
|
||||
} else {
|
||||
forces[i] = forces[i] + force;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update velocities and positions
|
||||
for i in 0..num_nodes {
|
||||
let c = &mut self.containers[i];
|
||||
c.vel = (c.vel + forces[i] * delta_time) * DAMPING;
|
||||
c.pos += c.vel * delta_time;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn arrange_circle(&mut self) {
|
||||
let node_count = self.containers.len() as f32;
|
||||
for (i, m) in self.containers.iter_mut().enumerate() {
|
||||
let ang = -(i as f32 / node_count) * TAU;
|
||||
m.pos = Vec2 {
|
||||
x: 300. * ang.sin(),
|
||||
y: 300. * ang.cos(),
|
||||
};
|
||||
m.vel = Vec2::ZERO;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
use egui::Pos2;
|
||||
|
||||
/// Calculate the convex hull of a set of points using Graham scan
|
||||
pub fn convex_hull(points: &[Pos2]) -> Vec<Pos2> {
|
||||
if points.len() < 3 {
|
||||
return points.to_vec();
|
||||
}
|
||||
|
||||
let mut pts = points.to_vec();
|
||||
|
||||
// Find the point with lowest y-coordinate (and leftmost if tie)
|
||||
let start_idx = pts
|
||||
.iter()
|
||||
.enumerate()
|
||||
.min_by(|(_, a), (_, b)| {
|
||||
a.y.partial_cmp(&b.y)
|
||||
.unwrap()
|
||||
.then(a.x.partial_cmp(&b.x).unwrap())
|
||||
})
|
||||
.unwrap()
|
||||
.0;
|
||||
|
||||
pts.swap(0, start_idx);
|
||||
let start = pts[0];
|
||||
|
||||
// Sort points by polar angle with respect to start point
|
||||
pts[1..].sort_by(|a, b| {
|
||||
let angle_a = polar_angle_to(&start, a);
|
||||
let angle_b = polar_angle_to(&start, b);
|
||||
angle_a.partial_cmp(&angle_b).unwrap()
|
||||
});
|
||||
|
||||
// Build convex hull
|
||||
let mut hull = Vec::new();
|
||||
hull.push(pts[0]);
|
||||
hull.push(pts[1]);
|
||||
|
||||
for i in 2..pts.len() {
|
||||
while hull.len() > 1
|
||||
&& cross_product(&hull[hull.len() - 2], &hull[hull.len() - 1], &pts[i]) <= 0.0
|
||||
{
|
||||
hull.pop();
|
||||
}
|
||||
hull.push(pts[i]);
|
||||
}
|
||||
|
||||
hull
|
||||
}
|
||||
|
||||
/// Calculate cross product of vectors (self->p2) and (self->p3)
|
||||
/// Positive if counter-clockwise, negative if clockwise, zero if collinear
|
||||
fn cross_product(p1: &Pos2, p2: &Pos2, p3: &Pos2) -> f32 {
|
||||
(p2.x - p1.x) * (p3.y - p1.y) - (p2.y - p1.y) * (p3.x - p1.x)
|
||||
}
|
||||
|
||||
/// Calculate polar angle from self to other point
|
||||
fn polar_angle_to(a: &Pos2, b: &Pos2) -> f32 {
|
||||
(b.y - a.y).atan2(b.x - a.x)
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
mod container;
|
||||
mod flowchart;
|
||||
mod force;
|
||||
mod group;
|
||||
|
||||
use egui::{Color32, Stroke};
|
||||
pub use flowchart::FlowChart;
|
||||
|
||||
const ITERATIONS: usize = 1_000;
|
||||
const RESOLUTION: f32 = 0.6;
|
||||
|
||||
const TARGET_LINE_GAP: f32 = 80.;
|
||||
|
||||
const BG_STROKE: Stroke = Stroke {
|
||||
width: 0.3,
|
||||
color: Color32::GRAY,
|
||||
};
|
||||
|
||||
const CONNECTION_STROKE: Stroke = Stroke {
|
||||
width: 3.,
|
||||
color: Color32::WHITE,
|
||||
};
|
||||
|
||||
const GROUP_BORDER_MARGIN: f32 = 20.;
|
||||
|
||||
static REPULSION_STRENGTH: f32 = 100000.0; // repulsion_strength
|
||||
static ATTRACTION_STRENGTH: f32 = 0.01; // attraction_strength
|
||||
static CENTER_ATTRACTION_STRENGTH: f32 = 0.01; // attraction_strength
|
||||
static GROUP_ATTRACTION_STRENGTH: f32 = 0.01; // attraction_strength
|
||||
static REST_LENGTH: f32 = 50.0; // rest_length
|
||||
static DAMPING: f32 = 0.9; // damping
|
||||
@@ -0,0 +1,82 @@
|
||||
use egui::{Color32, TextEdit};
|
||||
use unshell::config::{ConfigStructField, InterfaceData, InterfaceStruct, config_struct};
|
||||
|
||||
use crate::config::Config;
|
||||
|
||||
struct ConfigInterface(Config);
|
||||
|
||||
pub fn render(
|
||||
ui: &mut egui::Ui,
|
||||
interface_struct: &InterfaceStruct,
|
||||
interface_data: &mut InterfaceData,
|
||||
) {
|
||||
match (interface_struct, interface_data) {
|
||||
(InterfaceStruct::ConfigStruct(interface), InterfaceData::ConfigStruct(data)) => {
|
||||
render_config_struct(ui, interface, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_config_struct(
|
||||
ui: &mut egui::Ui,
|
||||
interface: &config_struct::ConfigStructKeys,
|
||||
data: &mut config_struct::ConfigStructValues,
|
||||
) {
|
||||
for (interface, data) in interface.iter().zip(data) {
|
||||
match (interface, data) {
|
||||
(ConfigStructField::Header(text), serde_json::Value::Null) => {
|
||||
ui.heading(text);
|
||||
}
|
||||
|
||||
(ConfigStructField::Text(text), serde_json::Value::Null) => {
|
||||
ui.label(text);
|
||||
}
|
||||
|
||||
(
|
||||
ConfigStructField::String {
|
||||
default: _,
|
||||
max_length,
|
||||
protected,
|
||||
},
|
||||
serde_json::Value::String(value),
|
||||
) => {
|
||||
ui.horizontal(|ui| {
|
||||
let mut widget = TextEdit::singleline(value);
|
||||
|
||||
if let Some(limit) = &max_length {
|
||||
widget = widget.char_limit(*limit);
|
||||
}
|
||||
|
||||
let password = *protected && !ui.button("👁").is_pointer_button_down_on();
|
||||
|
||||
widget = widget.password(password);
|
||||
|
||||
// if protected
|
||||
// ui.selectable_label(show_plaintext, "👁")
|
||||
// .on_hover_text("Show/hide password")
|
||||
// .clicked();
|
||||
// {
|
||||
// widget = widget.password(true);
|
||||
// }
|
||||
|
||||
ui.add(widget);
|
||||
|
||||
if let Some(limit) = max_length {
|
||||
ui.label(format!("{}/{}", value.len(), limit));
|
||||
}
|
||||
});
|
||||
}
|
||||
(
|
||||
ConfigStructField::Integer { default, min, max },
|
||||
serde_json::Value::Number(number),
|
||||
) => todo!(),
|
||||
|
||||
(interface, data) => {
|
||||
ui.colored_label(
|
||||
Color32::RED,
|
||||
&format!("Incorrect type and value! {interface:?} and {data:?}"),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
mod interface;
|
||||
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use unshell::{Result, config::TreeMessage};
|
||||
|
||||
use crate::auth::Auth;
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize)]
|
||||
pub struct InterfaceWindow {
|
||||
path: PathBuf,
|
||||
// #[serde(skip)]
|
||||
// data_bind: Bind<String, ModuleError>,
|
||||
#[serde(skip)]
|
||||
state: Arc<Mutex<InterfaceWindowState>>,
|
||||
// #[serde(skip)]
|
||||
// promise: Option<PromiseWrapper<Result<TreeMessage>>>,
|
||||
}
|
||||
|
||||
pub struct InterfaceWindowState {
|
||||
is_request: bool,
|
||||
is_error: bool,
|
||||
branch: Option<TreeMessage>,
|
||||
}
|
||||
|
||||
impl InterfaceWindow {
|
||||
pub fn update(&mut self, auth: &mut Auth, ui: &mut egui::Ui) {
|
||||
// let data_bind = Bind::<InterfaceWindowState, ModuleError>::new(false);
|
||||
|
||||
ui.heading("Interface");
|
||||
ui.label(self.path.to_string_lossy());
|
||||
|
||||
{
|
||||
if !self.path.eq("/") && ui.button("Go up").clicked() {
|
||||
self.go_up();
|
||||
self.state.lock().unwrap().branch = None;
|
||||
}
|
||||
|
||||
let state_lock = self.state.lock().unwrap();
|
||||
|
||||
let (is_request, is_error, is_some) = (
|
||||
state_lock.is_request,
|
||||
state_lock.is_error,
|
||||
state_lock.branch.is_some(),
|
||||
);
|
||||
|
||||
drop(state_lock);
|
||||
|
||||
if is_request {
|
||||
ui.spinner();
|
||||
} else if is_error {
|
||||
self.reset_path();
|
||||
let mut state_lock = self.state.lock().unwrap();
|
||||
|
||||
state_lock.is_error = false;
|
||||
state_lock.is_request = false;
|
||||
state_lock.branch = None;
|
||||
|
||||
drop(state_lock)
|
||||
} else if !is_some {
|
||||
{
|
||||
let mut state_lock = self.state.lock().unwrap();
|
||||
|
||||
state_lock.is_request = true;
|
||||
}
|
||||
|
||||
let state_clone = self.state.clone();
|
||||
auth.get(
|
||||
&format!("/api/interface{}", self.path.display()),
|
||||
move |response: Result<TreeMessage>| {
|
||||
let mut state_lock = state_clone.lock().unwrap();
|
||||
|
||||
match response {
|
||||
Ok(item) => {
|
||||
state_lock.branch = Some(item);
|
||||
}
|
||||
Err(err) => {
|
||||
crate::log(&format!("Got error {err:?}"));
|
||||
state_lock.is_error = true;
|
||||
}
|
||||
}
|
||||
|
||||
state_lock.is_request = false;
|
||||
|
||||
drop(state_lock);
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
let state_clone = self.state.clone();
|
||||
|
||||
let mut state_lock = state_clone.lock().unwrap();
|
||||
|
||||
let branch = (state_lock.branch).as_mut().unwrap();
|
||||
|
||||
let clear = match branch {
|
||||
TreeMessage::InterfaceAndValue(interface_struct, interface_data) => {
|
||||
interface::render(ui, interface_struct, interface_data);
|
||||
|
||||
if ui.button("Save").clicked() {
|
||||
auth.post(
|
||||
&format!("/api/interface{}", self.path.display()),
|
||||
&TreeMessage::State(interface_data.clone()),
|
||||
move |response: Result<TreeMessage>| {
|
||||
// debug!("{response:?}");
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
TreeMessage::Folder(items) => {
|
||||
let mut clear = false;
|
||||
for item in items {
|
||||
if ui.button(&format!("Item {}", item)).clicked() {
|
||||
self.go_down(item.clone());
|
||||
clear = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
clear
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
|
||||
if clear {
|
||||
state_lock.branch = None;
|
||||
}
|
||||
|
||||
drop(state_lock)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn reset_path(&mut self) {
|
||||
self.path = PathBuf::from("/");
|
||||
}
|
||||
|
||||
fn go_up(&mut self) {
|
||||
if let Some(parent) = self.path.parent() {
|
||||
self.path = PathBuf::from(parent);
|
||||
}
|
||||
}
|
||||
|
||||
fn go_down(&mut self, path: String) {
|
||||
self.path = self.path.join(path);
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for InterfaceWindow {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
path: "/".into(),
|
||||
// promise: None,
|
||||
state: Arc::new(Mutex::new(InterfaceWindowState::default())),
|
||||
// data_bind: Bind::new(false),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for InterfaceWindowState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
is_request: false,
|
||||
branch: None,
|
||||
is_error: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
#![warn(clippy::all, rust_2018_idioms)]
|
||||
#![macro_use]
|
||||
// #[allow(unused_extern_crates)]
|
||||
// extern crate log;
|
||||
|
||||
pub mod app;
|
||||
mod auth;
|
||||
mod blobs;
|
||||
mod config;
|
||||
mod flowchart;
|
||||
mod interface;
|
||||
mod log_viewer;
|
||||
mod payload_config;
|
||||
|
||||
use std::time::Duration;
|
||||
const FORCE_REDRAW_DELAY: Duration = Duration::from_millis(300);
|
||||
|
||||
// mod JsFunc {
|
||||
// use wasm_bindgen::JsValue;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
#[wasm_bindgen]
|
||||
extern "C" {
|
||||
#[wasm_bindgen(js_namespace = console)]
|
||||
pub fn log(text: &str);
|
||||
|
||||
}
|
||||
|
||||
#[wasm_bindgen(module = "/assets/sw.js")]
|
||||
extern "C" {
|
||||
pub fn httpGet(url: &str, ok_callback: JsValue);
|
||||
pub fn httpPost(url: &str, data: &str, ok_callback: JsValue);
|
||||
pub fn httpGetAuth(url: &str, auth: String, ok_callback: JsValue);
|
||||
pub fn httpPostAuth(url: &str, auth: String, data: &str, ok_callback: JsValue);
|
||||
}
|
||||
|
||||
// When compiling to web using trunk:
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[wasm_bindgen(start)]
|
||||
pub async fn run() {
|
||||
use wasm_bindgen::JsCast as _;
|
||||
|
||||
use app::TemplateApp;
|
||||
|
||||
// Redirect `log` message to `console.log` and friends:
|
||||
eframe::WebLogger::init(log::LevelFilter::Debug).ok();
|
||||
|
||||
let web_options = eframe::WebOptions::default();
|
||||
|
||||
let document = web_sys::window()
|
||||
.expect("No window")
|
||||
.document()
|
||||
.expect("No document");
|
||||
|
||||
let canvas = document
|
||||
.get_element_by_id("the_canvas_id")
|
||||
.expect("Failed to find the_canvas_id")
|
||||
.dyn_into::<web_sys::HtmlCanvasElement>()
|
||||
.expect("the_canvas_id was not a HtmlCanvasElement");
|
||||
|
||||
let start_result = eframe::WebRunner::new()
|
||||
.start(
|
||||
canvas,
|
||||
web_options,
|
||||
Box::new(|cc| Ok(Box::new(TemplateApp::new(cc)))),
|
||||
)
|
||||
.await;
|
||||
|
||||
// Remove the loading text and spinner:
|
||||
if let Some(loading_text) = document.get_element_by_id("loading_text") {
|
||||
match start_result {
|
||||
Ok(_) => {
|
||||
loading_text.remove();
|
||||
}
|
||||
Err(e) => {
|
||||
loading_text.set_inner_html(
|
||||
"<p> The app has crashed. See the developer console for details. </p>",
|
||||
);
|
||||
panic!("Failed to start eframe: {e:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// }
|
||||
// #[cfg(not(target_arch = "wasm32"))]
|
||||
// mod JsFunc {
|
||||
|
||||
// pub fn httpGet(url: &str, callback: fn() -> {}) {}
|
||||
// }
|
||||
@@ -0,0 +1,207 @@
|
||||
use chrono::DateTime;
|
||||
use chrono::Utc;
|
||||
use egui::Color32;
|
||||
use egui::TextStyle;
|
||||
use egui_extras::Column;
|
||||
use egui_extras::TableBuilder;
|
||||
|
||||
use crate::auth::Auth;
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
use std::time::SystemTime;
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize)]
|
||||
pub struct LogViewer {
|
||||
enable_location: bool,
|
||||
#[serde(skip)]
|
||||
state: Arc<Mutex<LogState>>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct LogState {
|
||||
logs: Vec<Record>,
|
||||
// trees: Option<Vec<String>>,
|
||||
// tree_keys: Option<HashMap<String, String>>,
|
||||
is_requesting: bool,
|
||||
requested_data: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub enum LogLevel {
|
||||
Debug,
|
||||
Info,
|
||||
Warn,
|
||||
Error,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Record {
|
||||
log_level: LogLevel,
|
||||
location: Option<String>,
|
||||
time: SystemTime,
|
||||
message: String,
|
||||
}
|
||||
|
||||
impl Record {
|
||||
pub fn display_level(&self, ui: &mut egui::Ui) {
|
||||
match self.log_level {
|
||||
LogLevel::Debug => ui.colored_label(Color32::LIGHT_BLUE, "DBUG"),
|
||||
LogLevel::Info => ui.colored_label(Color32::DARK_GREEN, "INFO"),
|
||||
LogLevel::Warn => ui.colored_label(Color32::YELLOW, "WARN"),
|
||||
LogLevel::Error => ui.colored_label(Color32::RED, "ERR!"),
|
||||
};
|
||||
}
|
||||
pub fn display_location(&self, ui: &mut egui::Ui) {
|
||||
if let Some(ref location) = self.location {
|
||||
ui.label(location);
|
||||
}
|
||||
}
|
||||
pub fn display_time(&self, ui: &mut egui::Ui) {
|
||||
let date: DateTime<Utc> = self.time.into();
|
||||
let date = date.to_rfc2822().to_string();
|
||||
ui.label(date);
|
||||
}
|
||||
pub fn display_message(&self, ui: &mut egui::Ui) {
|
||||
ui.strong(&self.message);
|
||||
}
|
||||
}
|
||||
|
||||
impl LogViewer {
|
||||
pub fn update(&mut self, auth: &mut Auth, ui: &mut egui::Ui) {
|
||||
ui.heading("Log Viewer");
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("Refresh").clicked() {
|
||||
self.refresh_logs(auth);
|
||||
}
|
||||
|
||||
ui.checkbox(&mut self.enable_location, "Enable Location");
|
||||
});
|
||||
|
||||
// let logs = ;
|
||||
|
||||
let body_text_size = TextStyle::Body.resolve(ui.style()).size;
|
||||
use egui_extras::{Size, StripBuilder};
|
||||
StripBuilder::new(ui)
|
||||
.size(Size::remainder().at_least(100.0)) // for the table
|
||||
.size(Size::exact(body_text_size))
|
||||
.vertical(|mut strip| {
|
||||
strip.cell(|ui| {
|
||||
egui::ScrollArea::both()
|
||||
.stick_to_bottom(true)
|
||||
.show(ui, |ui| {
|
||||
let table = TableBuilder::new(ui)
|
||||
.striped(true)
|
||||
.resizable(true)
|
||||
.stick_to_bottom(true)
|
||||
.cell_layout(egui::Layout::left_to_right(egui::Align::Center))
|
||||
.column(Column::auto())
|
||||
.column(Column::auto())
|
||||
.column(Column::auto())
|
||||
.min_scrolled_height(0.0)
|
||||
.sense(egui::Sense::click());
|
||||
|
||||
let table = if self.enable_location {
|
||||
table.column(Column::auto())
|
||||
} else {
|
||||
table
|
||||
};
|
||||
|
||||
table
|
||||
.header(20., |mut header| {
|
||||
header.col(|ui| {
|
||||
ui.strong("Time");
|
||||
});
|
||||
header.col(|ui| {
|
||||
ui.strong("Level");
|
||||
});
|
||||
header.col(|ui| {
|
||||
ui.strong("Message");
|
||||
});
|
||||
if self.enable_location {
|
||||
header.col(|ui| {
|
||||
ui.strong("Location");
|
||||
});
|
||||
}
|
||||
})
|
||||
.body(|mut body| {
|
||||
let state_lock = self.state.lock().unwrap();
|
||||
// let logs = state_lock.logs.as_ref();
|
||||
|
||||
for log in state_lock.logs.iter() {
|
||||
// // let runtime = self.current_runtimes
|
||||
|
||||
body.row(18., |mut row| {
|
||||
row.col(|ui| log.display_time(ui));
|
||||
row.col(|ui| log.display_level(ui));
|
||||
row.col(|ui| log.display_message(ui));
|
||||
if self.enable_location {
|
||||
row.col(|ui| log.display_location(ui));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
{
|
||||
let state_lock = self.state.lock().unwrap();
|
||||
|
||||
match (
|
||||
state_lock.is_requesting,
|
||||
state_lock.logs.len() == 0,
|
||||
state_lock.requested_data,
|
||||
) {
|
||||
(true, _, _) => {
|
||||
drop(state_lock);
|
||||
ui.spinner();
|
||||
}
|
||||
(false, true, true) => {
|
||||
drop(state_lock);
|
||||
ui.label("There are no logs");
|
||||
}
|
||||
(false, true, false) => {
|
||||
drop(state_lock);
|
||||
self.refresh_logs(auth);
|
||||
}
|
||||
_ => {
|
||||
drop(state_lock);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn refresh_logs(&self, auth: &mut Auth) {
|
||||
let state_clone = self.state.clone();
|
||||
{
|
||||
let mut state_lock = self.state.lock().unwrap();
|
||||
state_lock.logs.clear();
|
||||
state_lock.is_requesting = true;
|
||||
}
|
||||
auth.get(&format!("/api/log/{}", 0), move |e: Vec<String>| {
|
||||
let mut state_lock = state_clone.lock().unwrap();
|
||||
state_lock.logs.append(
|
||||
&mut e
|
||||
.iter()
|
||||
.map(|log| serde_json::from_str(log).unwrap())
|
||||
.collect(),
|
||||
);
|
||||
state_lock.is_requesting = false;
|
||||
state_lock.requested_data = true;
|
||||
|
||||
// crate::log(&format!("{e:?}"));
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for LogViewer {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enable_location: false,
|
||||
state: Arc::new(Mutex::new(LogState::default())),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
#![warn(clippy::all, rust_2018_idioms)]
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
|
||||
|
||||
// When compiling natively:
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn main() -> eframe::Result {
|
||||
// pretty_env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`).
|
||||
|
||||
// let native_options = eframe::NativeOptions {
|
||||
// viewport: egui::ViewportBuilder::default()
|
||||
// .with_inner_size([400.0, 300.0])
|
||||
// .with_min_inner_size([300.0, 220.0]),
|
||||
// ..Default::default()
|
||||
// };
|
||||
// eframe::run_native(
|
||||
// "eframe template",
|
||||
// native_options,
|
||||
// Box::new(|cc| Ok(Box::new(TemplateApp::new(cc)))),
|
||||
// )
|
||||
todo!()
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// use crate::payload_config::structs::ConfigStructField;
|
||||
|
||||
mod structs;
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize)]
|
||||
pub struct PayloadConfig {
|
||||
config_struct: structs::Config,
|
||||
}
|
||||
|
||||
// struct ServerConfigState {
|
||||
// // config: Vec<PayloadConfig>
|
||||
// }
|
||||
|
||||
impl PayloadConfig {
|
||||
pub fn update(&mut self, ui: &mut egui::Ui) {
|
||||
if ui.button("export").clicked() {
|
||||
crate::log(&self.config_struct.export());
|
||||
}
|
||||
// ui.heading("Test");
|
||||
self.config_struct.update(ui);
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PayloadConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
config_struct: structs::default_configurable(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use egui::TextEdit;
|
||||
use serde_json::Value;
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize)]
|
||||
enum ConfigStructField {
|
||||
Header(String),
|
||||
Text(String),
|
||||
String {
|
||||
default: Option<String>,
|
||||
max_length: Option<usize>,
|
||||
protected: bool,
|
||||
},
|
||||
Integer {
|
||||
default: i32,
|
||||
min: Option<i32>,
|
||||
max: Option<i32>,
|
||||
},
|
||||
// Checkbox
|
||||
// Dropdown
|
||||
// Collapsing header
|
||||
// Slider
|
||||
// ...
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize)]
|
||||
pub struct Config {
|
||||
config: Vec<(String, ConfigStructField)>,
|
||||
state: HashMap<String, Value>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
fn new(config: Vec<(String, ConfigStructField)>) -> Self {
|
||||
Self {
|
||||
config,
|
||||
state: HashMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, ui: &mut egui::Ui) {
|
||||
for (id, field) in &self.config {
|
||||
match field {
|
||||
ConfigStructField::Header(text) => {
|
||||
ui.heading(text);
|
||||
}
|
||||
ConfigStructField::Text(text) => {
|
||||
ui.label(text);
|
||||
}
|
||||
ConfigStructField::String {
|
||||
default,
|
||||
max_length,
|
||||
protected,
|
||||
} => {
|
||||
let value = if let Some(Value::String(value)) = self.state.get_mut(id) {
|
||||
value
|
||||
} else {
|
||||
self.state.insert(
|
||||
id.clone(),
|
||||
Value::String(default.clone().unwrap_or(String::new())),
|
||||
);
|
||||
if let Some(Value::String(value)) = self.state.get_mut(id) {
|
||||
value
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
};
|
||||
|
||||
let mut widget = TextEdit::singleline(value).password(*protected);
|
||||
|
||||
if let Some(limit) = &max_length {
|
||||
widget = widget.char_limit(*limit);
|
||||
}
|
||||
|
||||
ui.add(widget);
|
||||
}
|
||||
_ => {} // ConfigStructField::Integer { default, min, max } => todo!(),
|
||||
}
|
||||
}
|
||||
|
||||
// match &self.field {
|
||||
// ConfigStructField::Header(text) => {
|
||||
// ui.heading(text);
|
||||
// }
|
||||
// ConfigStructField::Text(text) => {
|
||||
// ui.label(text);
|
||||
// }
|
||||
// ConfigStructField::String {
|
||||
// default,
|
||||
// max_length,
|
||||
// protected,
|
||||
// } => ui.text_edit_singleline(),
|
||||
// ConfigStructField::Integer { default, min, max } => todo!(),
|
||||
// }
|
||||
}
|
||||
|
||||
pub fn export(&self) -> String {
|
||||
serde_json::to_string(&self.config).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default_configurable() -> Config {
|
||||
Config::new(vec![
|
||||
(
|
||||
"Header".into(),
|
||||
ConfigStructField::Header("Test Header!".into()),
|
||||
),
|
||||
("text".into(), ConfigStructField::Text("Test Text!".into())),
|
||||
(
|
||||
"Config".into(),
|
||||
ConfigStructField::String {
|
||||
default: Some("Test String".into()),
|
||||
max_length: Some(30),
|
||||
protected: false,
|
||||
},
|
||||
),
|
||||
(
|
||||
"Protected".into(),
|
||||
ConfigStructField::String {
|
||||
default: Some("Protected String".into()),
|
||||
max_length: None,
|
||||
protected: true,
|
||||
},
|
||||
),
|
||||
])
|
||||
}
|
||||
Reference in New Issue
Block a user