From 7117e0d5f1f6e054ddcf3911fa48b71377ac7bcd Mon Sep 17 00:00:00 2001 From: Herbert Wolverson Date: Wed, 26 Jun 2024 12:04:48 -0500 Subject: [PATCH] Loading/saving layouts to/from the server is functioning. --- .gitignore | 1 + src/rust/lqosd/src/node_manager.rs | 1 + .../js_build/src/dashlets/dashboard.js | 113 +++++++++++++++++- src/rust/lqosd/src/node_manager/localApi.rs | 12 ++ .../node_manager/localApi/dashboard_themes.rs | 84 +++++++++++++ src/rust/lqosd/src/node_manager/run.rs | 4 +- 6 files changed, 213 insertions(+), 2 deletions(-) create mode 100644 src/rust/lqosd/src/node_manager/localApi.rs create mode 100644 src/rust/lqosd/src/node_manager/localApi/dashboard_themes.rs diff --git a/.gitignore b/.gitignore index 94f61426..8f4fab36 100644 --- a/.gitignore +++ b/.gitignore @@ -122,3 +122,4 @@ src/ShapedDevices.csv.good .gitignore src/rust/lqosd/lts_keys.bin src/rust/lqosd/src/node_manager/js_build/out +src/bin/dashboards diff --git a/src/rust/lqosd/src/node_manager.rs b/src/rust/lqosd/src/node_manager.rs index 71c0c05e..c8d9b11e 100644 --- a/src/rust/lqosd/src/node_manager.rs +++ b/src/rust/lqosd/src/node_manager.rs @@ -2,5 +2,6 @@ mod run; mod static_pages; mod template; mod ws; +mod localApi; pub use run::spawn_webserver; \ No newline at end of file diff --git a/src/rust/lqosd/src/node_manager/js_build/src/dashlets/dashboard.js b/src/rust/lqosd/src/node_manager/js_build/src/dashlets/dashboard.js index 9923677f..97db3c7c 100644 --- a/src/rust/lqosd/src/node_manager/js_build/src/dashlets/dashboard.js +++ b/src/rust/lqosd/src/node_manager/js_build/src/dashlets/dashboard.js @@ -157,9 +157,98 @@ export class Dashboard { col2.classList.add("col-6"); col2.appendChild(this.#buildMenu()); + // Themes from the server + col2.appendChild(document.createElement("hr")); + col2.appendChild(heading5Icon("cloud", "Saved Dashboard Layouts")) + let remoteList = document.createElement("select"); + remoteList.id = "remoteThemes"; + let remoteBtn = document.createElement("button"); + remoteBtn.classList.add("btn", "btn-success"); + remoteBtn.style.marginLeft = "5px"; + remoteBtn.innerHTML = " Load Theme From Server"; + remoteBtn.id = "remoteLoadBtn"; + remoteBtn.onclick = () => { + let layoutName = { + theme: $('#remoteThemes').find(":selected").val().toString() + } + $.ajax({ + type: "POST", + url: "/local-api/dashletGet", + data: JSON.stringify(layoutName), + contentType: 'application/json', + success: (data) => { + this.dashletIdentities = data; + this.#replaceDashletList(); + } + }); + } + + let delBtn = document.createElement("button"); + delBtn.classList.add("btn", "btn-danger"); + delBtn.innerHTML = " Delete Remote Theme"; + delBtn.style.marginLeft = "4px"; + delBtn.onclick = () => { + let layoutName = $('#remoteThemes').find(":selected").val(); + if (confirm("Are you sure you wish to delete " + layoutName)) { + let layoutNameObj = { + theme: layoutName.toString() + } + $.ajax({ + type: "POST", + url: "/local-api/dashletDelete", + data: JSON.stringify(layoutNameObj), + contentType: 'application/json', + success: (data) => { + $.get("/local-api/dashletThemes", (data) => { + this.fillServerLayoutList(data); + }); + } + }); + } + } + col2.appendChild(remoteList); + col2.appendChild(remoteBtn); + col2.appendChild(delBtn); + + $.get("/local-api/dashletThemes", (data) => { + this.fillServerLayoutList(data); + }); + + // Save theme to the server col2.appendChild(document.createElement("hr")); col2.appendChild(heading5Icon("save", "Save")); - col2.appendChild(document.createElement("hr")); + let lbl = document.createElement("label"); + lbl.htmlFor = "saveDashName"; + let saveDashName = document.createElement("input"); + saveDashName.id = "saveDashName"; + saveDashName.type = "text"; + let saveBtn = document.createElement("button"); + saveBtn.type = "button"; + saveBtn.classList.add("btn", "btn-success"); + saveBtn.innerHTML = " Save to Server"; + saveBtn.style.marginLeft = "4px"; + saveBtn.onclick = () => { + let name = $("#saveDashName").val(); + if (name.length < 1) return; + let request = { + name: name, + entries: this.dashletIdentities + } + $.ajax({ + type: "POST", + url: "/local-api/dashletSave", + data: JSON.stringify(request), + contentType : 'application/json', + success: (data) => { + $.get("/local-api/dashletThemes", (data) => { + this.fillServerLayoutList(data); + }); + } + }) + } + col2.appendChild(lbl); + col2.appendChild(saveDashName); + col2.appendChild(saveBtn); row.appendChild(col1); row.appendChild(col2); @@ -169,6 +258,28 @@ export class Dashboard { document.body.appendChild(darken); } + fillServerLayoutList(data) { + let parent = document.getElementById("remoteThemes"); + while (parent.children.length > 0) { + parent.removeChild(parent.lastChild); + } + for (let i=0; i 1) { this.parentDiv.removeChild(this.parentDiv.lastChild); diff --git a/src/rust/lqosd/src/node_manager/localApi.rs b/src/rust/lqosd/src/node_manager/localApi.rs new file mode 100644 index 00000000..91bede02 --- /dev/null +++ b/src/rust/lqosd/src/node_manager/localApi.rs @@ -0,0 +1,12 @@ +mod dashboard_themes; + +use axum::Router; +use axum::routing::{get, post}; + +pub fn local_api() -> Router { + Router::new() + .route("/dashletThemes", get(dashboard_themes::list_themes)) + .route("/dashletSave", post(dashboard_themes::save_theme)) + .route("/dashletDelete", post(dashboard_themes::delete_theme)) + .route("/dashletGet", post(dashboard_themes::get_theme)) +} \ No newline at end of file diff --git a/src/rust/lqosd/src/node_manager/localApi/dashboard_themes.rs b/src/rust/lqosd/src/node_manager/localApi/dashboard_themes.rs new file mode 100644 index 00000000..1615b6a4 --- /dev/null +++ b/src/rust/lqosd/src/node_manager/localApi/dashboard_themes.rs @@ -0,0 +1,84 @@ +use axum::http::StatusCode; +use axum::Json; +use serde::{Deserialize, Serialize}; + +use lqos_config::load_config; + +pub async fn list_themes() -> Json> { + if let Ok(config) = load_config() { + let base_path = std::path::Path::new(&config.lqos_directory).join("bin").join("dashboards"); + if !base_path.exists() { + std::fs::create_dir(&base_path).unwrap(); + } + + let mut result = Vec::new(); + for f in std::fs::read_dir(&base_path).unwrap() { + if let Ok(f) = f { + let fs = f.file_name().to_string_lossy().to_string(); + if fs.ends_with("json") { + result.push(fs.to_string()); + } + } + } + return Json(result); + } + Json(Vec::new()) +} + +#[derive(Serialize, Deserialize)] +pub struct DashletSave { + name: String, + entries: Vec, +} + +#[derive(Serialize, Deserialize)] +pub struct DashletIdentity { + name: String, + tag: String, + size: i32, +} + +pub async fn save_theme(Json(data): Json) -> StatusCode { + if let Ok(config) = load_config() { + let base_path = std::path::Path::new(&config.lqos_directory).join("bin").join("dashboards"); + if !base_path.exists() { + std::fs::create_dir(&base_path).unwrap(); + } + + let name = data.name.replace('/', "_"); + let name = format!("{}.json", name); + let file_path = base_path.join(name); + let serialized = serde_json::to_string(&data).unwrap(); + std::fs::write(&file_path, serialized.as_bytes()).unwrap(); + } + + StatusCode::OK +} + +#[derive(Deserialize)] +pub struct ThemeSelector { + theme: String, +} + +pub async fn delete_theme(Json(f): Json) -> StatusCode { + if let Ok(config) = load_config() { + let base_path = std::path::Path::new(&config.lqos_directory).join("bin").join("dashboards").join(&f.theme); + if base_path.exists() { + std::fs::remove_file(base_path).unwrap(); + } + } + + StatusCode::OK +} + +pub async fn get_theme(Json(f): Json) -> Json> { + if let Ok(config) = load_config() { + let base_path = std::path::Path::new(&config.lqos_directory).join("bin").join("dashboards").join(&f.theme); + if base_path.exists() { + let raw = std::fs::read_to_string(&base_path).unwrap(); + let result: DashletSave = serde_json::from_str(&raw).unwrap(); + return Json(result.entries); + } + } + Json(Vec::new()) +} \ No newline at end of file diff --git a/src/rust/lqosd/src/node_manager/run.rs b/src/rust/lqosd/src/node_manager/run.rs index 8767b47a..8bdf51dc 100644 --- a/src/rust/lqosd/src/node_manager/run.rs +++ b/src/rust/lqosd/src/node_manager/run.rs @@ -3,6 +3,7 @@ use log::info; use tokio::net::TcpListener; use anyhow::Result; use crate::node_manager::{static_pages::{static_routes, vendor_route}, ws::websocket_router}; +use crate::node_manager::localApi::local_api; /// Launches the Axum webserver to take over node manager duties. /// This is designed to be run as an independent Tokio future, @@ -15,7 +16,8 @@ pub async fn spawn_webserver() -> Result<()> { let router = Router::new() .nest("/", websocket_router()) .nest("/vendor", vendor_route()?) // Serve /vendor as purely static - .nest("/", static_routes()?); + .nest("/", static_routes()?) + .nest("/local-api", local_api()); info!("Webserver listening on :: port 9223"); axum::serve(listener, router).await?;