Add tree thing

This commit is contained in:
Michael Mikovsky
2025-12-17 16:40:34 -07:00
parent 6e73cb8a8e
commit 2dc736b02e
12 changed files with 385 additions and 3 deletions
+5 -1
View File
@@ -5,7 +5,7 @@ use std::collections::HashSet;
use crate::{ use crate::{
app::windows::WindowWrapper, auth::Auth, config::Config, flowchart::FlowChart, app::windows::WindowWrapper, auth::Auth, config::Config, flowchart::FlowChart,
log_viewer::LogViewer, payload_config::PayloadConfig, interface::InterfaceWindow, log_viewer::LogViewer, payload_config::PayloadConfig,
}; };
pub use app::TemplateApp; pub use app::TemplateApp;
use egui_tiles::{TileId, Tree}; use egui_tiles::{TileId, Tree};
@@ -20,6 +20,7 @@ pub struct AppState {
pub config: Config, pub config: Config,
pub payload_config: PayloadConfig, pub payload_config: PayloadConfig,
pub log_viewer: LogViewer, pub log_viewer: LogViewer,
pub interface: InterfaceWindow,
} }
impl AppState { impl AppState {
@@ -29,6 +30,7 @@ impl AppState {
(AppWindow::PayloadConfig, "Payload Config"), (AppWindow::PayloadConfig, "Payload Config"),
(AppWindow::Config, "Config"), (AppWindow::Config, "Config"),
(AppWindow::LogViewer, "Log Viewer"), (AppWindow::LogViewer, "Log Viewer"),
(AppWindow::InterfaceWindow, "Tree Test"),
]) ])
.iter() .iter()
.enumerate() .enumerate()
@@ -103,6 +105,7 @@ pub enum AppWindow {
Config, Config,
PayloadConfig, PayloadConfig,
LogViewer, LogViewer,
InterfaceWindow,
} }
impl AppWindow { impl AppWindow {
@@ -112,6 +115,7 @@ impl AppWindow {
AppWindow::Config => state.config.update(&mut state.auth, ui), AppWindow::Config => state.config.update(&mut state.auth, ui),
AppWindow::PayloadConfig => state.payload_config.update(ui), AppWindow::PayloadConfig => state.payload_config.update(ui),
AppWindow::LogViewer => state.log_viewer.update(&mut state.auth, ui), AppWindow::LogViewer => state.log_viewer.update(&mut state.auth, ui),
AppWindow::InterfaceWindow => state.interface.update(&mut state.auth, ui),
} }
} }
+42
View File
@@ -0,0 +1,42 @@
use std::collections::HashMap;
use serde_json::Value;
#[derive(Clone, serde::Deserialize, serde::Serialize)]
pub enum Tree2Repr {
File(String),
Folder(Vec<String>),
}
// #[derive(Clone, serde::Deserialize, serde::Serialize)]
// pub struct InterfaceWrapper {
// pub name: String,
// pub interface: Interface,
// }
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
pub enum ConfigStructField {
Header(String),
Text(String),
String {
// Default value of string edit in struct
#[serde(default)]
default: String,
max_length: Option<usize>,
// Display string edit as password
#[serde(default)]
protected: Option<bool>,
},
Integer {
// Default value of integer in struct
#[serde(default)]
default: i32,
min: Option<i32>,
max: Option<i32>,
},
// Checkbox
// Dropdown
// Collapsing header
// Slider
// ...
}
+150
View File
@@ -0,0 +1,150 @@
mod config;
use std::{
path::PathBuf,
sync::{Arc, Mutex},
};
use crate::{auth::Auth, interface::config::Tree2Repr};
#[derive(serde::Deserialize, serde::Serialize)]
pub struct InterfaceWindow {
path: PathBuf,
#[serde(skip)]
state: Arc<Mutex<InterfaceWindowState>>,
}
pub struct InterfaceWindowState {
is_request: bool,
is_error: bool,
branch: Option<Tree2Repr>,
}
impl InterfaceWindow {
pub fn update(&mut self, auth: &mut Auth, ui: &mut egui::Ui) {
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;
drop(state_lock)
}
let state_clone = self.state.clone();
auth.get(
&format!("/api/interface{}", self.path.display()),
move |response: Result<Tree2Repr, String>| {
let mut state_lock = state_clone.lock().unwrap();
match response {
Ok(item) => {
state_lock.branch = Some(item);
}
Err(_) => {
state_lock.is_error = true;
}
}
state_lock.is_request = false;
drop(state_lock);
},
);
} else {
let state_clone = self.state.clone();
let mut state_lock = state_clone.lock().unwrap();
let branch = (state_lock.branch).as_ref().unwrap();
let clear = match branch {
Tree2Repr::File(file) => {
ui.label(&format!("File {}", file));
false
}
Tree2Repr::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) {
self.path = PathBuf::from(self.path.parent().unwrap());
}
fn go_down(&mut self, path: String) {
self.path = self.path.join(path);
}
}
impl Default for InterfaceWindow {
fn default() -> Self {
Self {
path: "/".into(),
state: Arc::new(Mutex::new(InterfaceWindowState::default())),
}
}
}
impl Default for InterfaceWindowState {
fn default() -> Self {
Self {
is_request: false,
branch: None,
is_error: false,
}
}
}
+2
View File
@@ -5,8 +5,10 @@ extern crate log;
pub mod app; pub mod app;
mod auth; mod auth;
mod blobs;
mod config; mod config;
mod flowchart; mod flowchart;
mod interface;
mod log_viewer; mod log_viewer;
mod payload_config; mod payload_config;
+3
View File
@@ -0,0 +1,3 @@
GET /api/interface/*/some_config -> Returns the interface and struct at that location
GET /api/interface/*/some_folder -> Returns the list of sub-folders and interfaces at that location
+3
View File
@@ -53,6 +53,9 @@ pub async fn start_api(address: &str, server: Server) {
router = route_get!(router, "/api/keys/{*path}", Server::all_tree_keys_api); router = route_get!(router, "/api/keys/{*path}", Server::all_tree_keys_api);
router = route_get!(router, "/api/values/{*path}", Server::all_tree_values_api); router = route_get!(router, "/api/values/{*path}", Server::all_tree_values_api);
router = route_get!(router, "/api/interface/", Server::get_tree2_root);
router = route_get!(router, "/api/interface/{*path}", Server::get_tree2);
// router = route_get_log(router); // router = route_get_log(router);
axum::serve(listener, router.with_state(server)) axum::serve(listener, router.with_state(server))
+49
View File
@@ -0,0 +1,49 @@
use std::collections::HashMap;
use serde_json::Value;
use crate::config::ConfigStructField;
#[derive(Clone, serde::Deserialize, serde::Serialize)]
pub enum Interface {
Sub(HashMap<String, InterfaceWrapper>),
Struct {
fields: HashMap<String, ConfigStructField>,
value: HashMap<String, Value>,
},
}
#[derive(Clone, serde::Deserialize, serde::Serialize)]
pub struct InterfaceWrapper {
pub name: String,
pub interface: Interface,
}
impl InterfaceWrapper {
pub fn get_path(&self, elements: &Vec<&str>, depth: usize) -> Result<InterfaceWrapper, String> {
if depth == elements.len() {
return Ok(self.clone());
}
let element = elements[depth];
match &self.interface {
Interface::Sub(interface_wrappers) => {
if let Some(interface) = interface_wrappers.get(element) {
interface.get_path(elements, depth)
} else {
Err("Invalid Path".into())
}
}
_ => Err("Invalid Path".into()),
}
}
}
pub fn get_test_interface() -> InterfaceWrapper {
InterfaceWrapper {
name: "Root Interface".into(),
interface: Interface::Sub(HashMap::new()),
}
}
+3 -1
View File
@@ -1,6 +1,8 @@
mod blob; mod blob;
// pub mod interface;
pub use blob::Blob; pub use blob::Blob;
// pub use interface::InterfaceWrapper;
use std::{ use std::{
collections::HashMap, collections::HashMap,
@@ -37,7 +39,7 @@ struct BuildConfig {
} }
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
enum ConfigStructField { pub enum ConfigStructField {
Header(String), Header(String),
Text(String), Text(String),
String { String {
+1
View File
@@ -5,6 +5,7 @@ mod config;
pub mod logger; pub mod logger;
mod modules; mod modules;
mod server; mod server;
pub use api::app::start_api; pub use api::app::start_api;
pub use server::Server; pub use server::Server;
+7 -1
View File
@@ -1,14 +1,18 @@
use std::{error::Error, path::PathBuf}; use std::{error::Error, path::PathBuf};
use crate::server::tree2::Tree2;
mod blobs; mod blobs;
mod database; mod database;
mod manager; mod tree2;
#[derive(Clone)] #[derive(Clone)]
pub struct Server { pub struct Server {
pub component_configs: Vec<crate::config::ComponentState>, pub component_configs: Vec<crate::config::ComponentState>,
// pub interface: InterfaceWrapper,
// pub manager: Arc<Mutex<Manager>>, // pub manager: Arc<Mutex<Manager>>,
pub db: sled::Db, pub db: sled::Db,
pub tree: Tree2,
} }
impl Server { impl Server {
@@ -23,6 +27,8 @@ impl Server {
component_configs, component_configs,
// manager: Manager::start(&SERVER_CONFIG, Vec::new()), // manager: Manager::start(&SERVER_CONFIG, Vec::new()),
db: sled::open(database)?, db: sled::open(database)?,
tree: Tree2::default(),
// interface: get_test_interface(),
}) })
} }
} }
+120
View File
@@ -0,0 +1,120 @@
use std::collections::HashMap;
use axum::{
Extension, Json,
extract::{Path, State},
};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use unshell_lib::debug;
use crate::{Server, api::CurrentUser};
#[derive(Clone)]
enum Tree2Branch {
File(String),
Folder(String, HashMap<String, Tree2Branch>),
}
#[derive(Clone, Serialize, Deserialize)]
enum Tree2Repr {
File(String),
Folder(Vec<String>),
}
#[derive(Clone)]
pub struct Tree2 {
root: Tree2Branch,
}
impl Tree2Branch {
pub fn get_path(&self, elements: &Vec<&str>, depth: usize) -> Result<&Tree2Branch, String> {
if depth == elements.len() {
return Ok(self);
}
let element = elements[depth];
if let Tree2Branch::Folder(_, hash_map) = self {
if let Some(branch) = hash_map.get(element) {
branch.get_path(elements, depth + 1)
} else {
Err("Invalid Path".into())
}
} else {
return Err("This is a folder, not a file".into());
}
}
pub fn to_repr(&self) -> Tree2Repr {
match self {
Tree2Branch::File(name) => Tree2Repr::File(name.clone()),
Tree2Branch::Folder(_, hash_map) => {
Tree2Repr::Folder(hash_map.keys().cloned().collect::<Vec<String>>())
}
}
}
}
impl Tree2 {
fn get(&self, path: &str) -> Result<Tree2Repr, String> {
let elements = if path.is_empty() {
Vec::new()
} else {
path.split("/").collect::<Vec<&str>>()
};
// let elements = path.split("/").collect::<Vec<&str>>();
let branch = self.root.get_path(&elements, 0)?;
Ok(branch.to_repr())
}
}
impl Default for Tree2 {
fn default() -> Self {
Self {
root: Tree2Branch::Folder(
"ROOT".into(),
HashMap::from([
("File A".into(), Tree2Branch::File("A".into())),
("File B".into(), Tree2Branch::File("B".into())),
(
"Folder C".into(),
Tree2Branch::Folder(
"A".into(),
HashMap::from([
("File A".into(), Tree2Branch::File("A".into())),
("File B".into(), Tree2Branch::File("B".into())),
]),
),
),
]),
),
}
}
}
impl Server {
pub async fn get_tree2(
State(server): State<Server>,
Path(path): Path<String>,
Extension(_): Extension<CurrentUser>,
) -> Json<Value> {
debug!("GET /api/interface/{}", path);
let result = (|| -> Result<_, String> {
let interface = server.tree.get(&path)?;
Ok(interface)
})();
Json(serde_json::to_value(result).unwrap())
}
pub async fn get_tree2_root(
State(server): State<Server>,
Extension(extension): Extension<CurrentUser>,
) -> Json<Value> {
Self::get_tree2(State(server), Path("".into()), Extension(extension)).await
}
}