Add breadcrumbs to AP and Site pages.

This commit is contained in:
Herbert Wolverson 2023-05-10 18:48:27 +00:00
parent 8585cec8e6
commit 60224ba67b
12 changed files with 208 additions and 10 deletions

View File

@ -2,4 +2,4 @@
pushd ../site_build pushd ../site_build
./esbuild.mjs ./esbuild.mjs
popd popd
RUST_LOG=info RUST_BACKTRACE=1 cargo run RUST_LOG=warn RUST_BACKTRACE=1 cargo run

View File

@ -1,7 +1,8 @@
use crate::web::wss::queries::{ use crate::web::wss::queries::{
omnisearch, root_heat_map, send_packets_for_all_nodes, send_packets_for_node, omnisearch, root_heat_map, send_packets_for_all_nodes, send_packets_for_node,
send_perf_for_node, send_rtt_for_all_nodes, send_rtt_for_node, send_throughput_for_all_nodes, send_perf_for_node, send_rtt_for_all_nodes, send_rtt_for_all_nodes_site, send_rtt_for_node,
send_throughput_for_node, site_tree::send_site_tree, send_throughput_for_all_nodes_by_site, send_site_info, send_rtt_for_all_nodes_site, send_site_info, send_site_parents, send_throughput_for_all_nodes,
send_throughput_for_all_nodes_by_site, send_throughput_for_node, site_tree::send_site_tree,
}; };
use axum::{ use axum::{
extract::{ extract::{
@ -245,6 +246,17 @@ async fn handle_socket(mut socket: WebSocket, cnn: Pool<Postgres>) {
.await; .await;
} }
} }
"siteParents" => {
if let Some(credentials) = &credentials {
send_site_parents(
cnn.clone(),
&mut socket,
&credentials.license_key,
json.get("site_id").unwrap().as_str().unwrap(),
)
.await;
}
}
_ => { _ => {
log::warn!("Unknown message type: {msg_type}"); log::warn!("Unknown message type: {msg_type}");
} }

View File

@ -8,6 +8,7 @@ mod node_perf;
mod search; mod search;
mod site_heat_map; mod site_heat_map;
mod site_info; mod site_info;
mod site_parents;
pub mod site_tree; pub mod site_tree;
pub mod time_period; pub mod time_period;
pub use packet_counts::{ send_packets_for_all_nodes, send_packets_for_node }; pub use packet_counts::{ send_packets_for_all_nodes, send_packets_for_node };
@ -17,3 +18,4 @@ pub use node_perf::send_perf_for_node;
pub use search::omnisearch; pub use search::omnisearch;
pub use site_heat_map::root_heat_map; pub use site_heat_map::root_heat_map;
pub use site_info::send_site_info; pub use site_info::send_site_info;
pub use site_parents::send_site_parents;

View File

@ -0,0 +1,47 @@
use axum::extract::ws::{WebSocket, Message};
use pgdb::sqlx::{Pool, Postgres};
use serde::Serialize;
pub async fn send_site_parents(
cnn: Pool<Postgres>,
socket: &mut WebSocket,
key: &str,
site_name: &str,
) {
if let Ok(parents) = pgdb::get_parent_list(cnn.clone(), key, site_name).await {
let msg = TreeMessage {
msg: "site_parents".to_string(),
data: parents,
};
let json = serde_json::to_string(&msg).unwrap();
if let Err(e) = socket.send(Message::Text(json)).await {
tracing::error!("Error sending message: {}", e);
}
}
let child_result = pgdb::get_child_list(cnn, key, site_name).await;
if let Ok(children) = child_result {
let msg = TreeChildMessage {
msg: "site_children".to_string(),
data: children,
};
let json = serde_json::to_string(&msg).unwrap();
if let Err(e) = socket.send(Message::Text(json)).await {
tracing::error!("Error sending message: {}", e);
}
} else {
log::error!("Error getting children: {:?}", child_result);
}
}
#[derive(Serialize)]
struct TreeMessage {
msg: String,
data: Vec<(String, String)>,
}
#[derive(Serialize)]
struct TreeChildMessage {
msg: String,
data: Vec<(String, String, String)>,
}

View File

@ -1,4 +1,4 @@
use sqlx::{FromRow, Pool, Postgres}; use sqlx::{FromRow, Pool, Postgres, Row};
use crate::license::StatsHostError; use crate::license::StatsHostError;
#[derive(Debug, FromRow)] #[derive(Debug, FromRow)]
@ -39,3 +39,85 @@ pub async fn get_site_info(
.await .await
.map_err(|e| StatsHostError::DatabaseError(e.to_string())) .map_err(|e| StatsHostError::DatabaseError(e.to_string()))
} }
pub async fn get_parent_list(
cnn: Pool<Postgres>,
key: &str,
site_name: &str,
) -> Result<Vec<(String, String)>, StatsHostError> {
let mut result = Vec::new();
// Get the site index
let site_id_db = sqlx::query("SELECT index FROM site_tree WHERE key = $1 AND site_name=$2")
.bind(key)
.bind(site_name)
.fetch_one(&cnn)
.await
.map_err(|e| StatsHostError::DatabaseError(e.to_string()))?;
let mut site_id: i32 = site_id_db.try_get("index").map_err(|e| StatsHostError::DatabaseError(e.to_string()))?;
// Get the parent list
while site_id != 0 {
let parent_db = sqlx::query("SELECT site_name, parent, site_type FROM site_tree WHERE key = $1 AND index=$2")
.bind(key)
.bind(site_id)
.fetch_one(&cnn)
.await
.map_err(|e| StatsHostError::DatabaseError(e.to_string()))?;
let parent: String = parent_db.try_get("site_name").map_err(|e| StatsHostError::DatabaseError(e.to_string()))?;
let site_type: String = parent_db.try_get("site_type").map_err(|e| StatsHostError::DatabaseError(e.to_string()))?;
site_id = parent_db.try_get("parent").map_err(|e| StatsHostError::DatabaseError(e.to_string()))?;
result.push((site_type, parent));
}
Ok(result)
}
pub async fn get_child_list(
cnn: Pool<Postgres>,
key: &str,
site_name: &str,
) -> Result<Vec<(String, String, String)>, StatsHostError> {
let mut result = Vec::new();
// Get the site index
let site_id_db = sqlx::query("SELECT index FROM site_tree WHERE key = $1 AND site_name=$2")
.bind(key)
.bind(site_name)
.fetch_one(&cnn)
.await
.map_err(|e| StatsHostError::DatabaseError(e.to_string()))?;
let site_id: i32 = site_id_db.try_get("index").map_err(|e| StatsHostError::DatabaseError(e.to_string()))?;
// Add child sites
let child_sites = sqlx::query("SELECT site_name, parent, site_type FROM site_tree WHERE key=$1 AND parent=$2")
.bind(key)
.bind(site_id)
.fetch_all(&cnn)
.await
.map_err(|e| StatsHostError::DatabaseError(e.to_string()))?;
for child in child_sites {
let child_name: String = child.try_get("site_name").map_err(|e| StatsHostError::DatabaseError(e.to_string()))?;
let child_type: String = child.try_get("site_type").map_err(|e| StatsHostError::DatabaseError(e.to_string()))?;
result.push((child_type, child_name.clone(), child_name));
}
// Add child shaper nodes
let child_circuits = sqlx::query("SELECT circuit_id, circuit_name FROM shaped_devices WHERE key=$1 AND parent_node=$2")
.bind(key)
.bind(site_name)
.fetch_all(&cnn)
.await
.map_err(|e| StatsHostError::DatabaseError(e.to_string()))?;
for child in child_circuits {
let child_name: String = child.try_get("circuit_name").map_err(|e| StatsHostError::DatabaseError(e.to_string()))?;
let child_id: String = child.try_get("circuit_id").map_err(|e| StatsHostError::DatabaseError(e.to_string()))?;
result.push(("circuit".to_string(), child_id, child_name));
}
result.sort_by(|a, b| a.2.cmp(&b.2));
Ok(result)
}

View File

@ -6,6 +6,7 @@ import { ThroughputSiteChart } from '../components/throughput_site';
import { SiteInfo } from '../components/site_info'; import { SiteInfo } from '../components/site_info';
import { RttChartSite } from '../components/rtt_site'; import { RttChartSite } from '../components/rtt_site';
import { RttHistoSite } from '../components/rtt_histo_site'; import { RttHistoSite } from '../components/rtt_histo_site';
import { SiteBreadcrumbs } from '../components/site_breadcrumbs';
export class AccessPointPage implements Page { export class AccessPointPage implements Page {
menu: MenuPage; menu: MenuPage;
@ -24,6 +25,7 @@ export class AccessPointPage implements Page {
new ThroughputSiteChart(siteId), new ThroughputSiteChart(siteId),
new RttChartSite(siteId), new RttChartSite(siteId),
new RttHistoSite(), new RttHistoSite(),
new SiteBreadcrumbs(siteId),
]; ];
} }

View File

@ -1,7 +1,6 @@
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col-12" id="nodeStatus"> <div class="col-12" id="siteName">
<h1 id="siteName">Site Name</h1>
</div> </div>
</div> </div>
<div class="row"> <div class="row">

View File

@ -160,6 +160,15 @@ export class Bus {
let json = JSON.stringify(request); let json = JSON.stringify(request);
this.ws.send(json); this.ws.send(json);
} }
requestSiteParents(site_id: string) {
let request = {
msg: "siteParents",
site_id: decodeURI(site_id),
};
let json = JSON.stringify(request);
this.ws.send(json);
}
} }
function formatToken(token: string) { function formatToken(token: string) {

View File

@ -0,0 +1,45 @@
import { makeUrl } from "../helpers";
import { Component } from "./component";
export class SiteBreadcrumbs implements Component {
siteId: string;
constructor(siteId: string) {
this.siteId = siteId;
}
wireup(): void {
window.bus.requestSiteParents(this.siteId);
}
ontick(): void {
}
onmessage(event: any): void {
if (event.msg == "site_parents") {
//console.log(event.data);
let div = document.getElementById("siteName") as HTMLDivElement;
let html = "";
let crumbs = event.data.reverse();
for (let i = 0; i < crumbs.length-1; i++) {
let url = makeUrl(crumbs[i][0], crumbs[i][1]);
html += "<a href='#" + url + "' onclick='window.router.goto(\"" + url + "\")'>" + crumbs[i][1] + "</a> | ";
}
html += crumbs[crumbs.length-1][1] + " | ";
html += "<select id='siteChildren'></select>";
div.innerHTML = html;
} else if (event.msg == "site_children") {
//console.log(event.data);
let html = "<option value=''>-- Children --</option>";
for (let i=0; i<event.data.length; i++) {
html += "<option value='" + makeUrl(event.data[i][0], event.data[i][1]) + "'>" + event.data[i][2] + "</option>";
}
let select = document.getElementById("siteChildren") as HTMLSelectElement;
select.innerHTML = html;
select.onchange = () => {
let select = document.getElementById("siteChildren") as HTMLSelectElement;
window.router.goto(select.value);
};
}
}
}

View File

@ -24,7 +24,6 @@ export class SiteInfo implements Component {
onmessage(event: any): void { onmessage(event: any): void {
if (event.msg == "site_info") { if (event.msg == "site_info") {
//console.log(event.data); //console.log(event.data);
(document.getElementById("siteName") as HTMLElement).innerText = event.data.site_name;
let div = document.getElementById("siteInfo") as HTMLDivElement; let div = document.getElementById("siteInfo") as HTMLDivElement;
let html = ""; let html = "";
html += "<table class='table table-striped'>"; html += "<table class='table table-striped'>";

View File

@ -6,6 +6,7 @@ import { ThroughputSiteChart } from '../components/throughput_site';
import { SiteInfo } from '../components/site_info'; import { SiteInfo } from '../components/site_info';
import { RttChartSite } from '../components/rtt_site'; import { RttChartSite } from '../components/rtt_site';
import { RttHistoSite } from '../components/rtt_histo_site'; import { RttHistoSite } from '../components/rtt_histo_site';
import { SiteBreadcrumbs } from '../components/site_breadcrumbs';
export class SitePage implements Page { export class SitePage implements Page {
menu: MenuPage; menu: MenuPage;
@ -24,6 +25,7 @@ export class SitePage implements Page {
new ThroughputSiteChart(siteId), new ThroughputSiteChart(siteId),
new RttChartSite(siteId), new RttChartSite(siteId),
new RttHistoSite(), new RttHistoSite(),
new SiteBreadcrumbs(siteId),
]; ];
} }

View File

@ -1,7 +1,6 @@
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col-12" id="nodeStatus"> <div class="col-12" id="siteName">
<h1 id="siteName">Site Name</h1>
</div> </div>
</div> </div>
<div class="row"> <div class="row">