mirror of
https://github.com/LibreQoE/LibreQoS.git
synced 2025-02-25 18:55:32 -06:00
Add initial site view.
This commit is contained in:
parent
3fe8e3db50
commit
a1c4d6c6d0
@ -1,7 +1,7 @@
|
|||||||
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_node, send_throughput_for_all_nodes,
|
||||||
send_throughput_for_node, site_tree::send_site_tree,
|
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,
|
||||||
};
|
};
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{
|
extract::{
|
||||||
@ -113,6 +113,20 @@ async fn handle_socket(mut socket: WebSocket, cnn: Pool<Postgres>) {
|
|||||||
log::info!("Throughput requested but no credentials provided");
|
log::info!("Throughput requested but no credentials provided");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"throughputChartSite" => {
|
||||||
|
if let Some(credentials) = &credentials {
|
||||||
|
let _ = send_throughput_for_all_nodes_by_site(
|
||||||
|
cnn.clone(),
|
||||||
|
&mut socket,
|
||||||
|
&credentials.license_key,
|
||||||
|
json.get("site_id").unwrap().as_str().unwrap().to_string(),
|
||||||
|
period,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
} else {
|
||||||
|
log::info!("Throughput requested but no credentials provided");
|
||||||
|
}
|
||||||
|
}
|
||||||
"throughputChartSingle" => {
|
"throughputChartSingle" => {
|
||||||
if let Some(credentials) = &credentials {
|
if let Some(credentials) = &credentials {
|
||||||
let _ = send_throughput_for_node(
|
let _ = send_throughput_for_node(
|
||||||
@ -141,6 +155,20 @@ async fn handle_socket(mut socket: WebSocket, cnn: Pool<Postgres>) {
|
|||||||
log::info!("Throughput requested but no credentials provided");
|
log::info!("Throughput requested but no credentials provided");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"rttChartSite" => {
|
||||||
|
if let Some(credentials) = &credentials {
|
||||||
|
let _ = send_rtt_for_all_nodes_site(
|
||||||
|
cnn.clone(),
|
||||||
|
&mut socket,
|
||||||
|
&credentials.license_key,
|
||||||
|
json.get("site_id").unwrap().as_str().unwrap().to_string(),
|
||||||
|
period,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
} else {
|
||||||
|
log::info!("Throughput requested but no credentials provided");
|
||||||
|
}
|
||||||
|
}
|
||||||
"rttChartSingle" => {
|
"rttChartSingle" => {
|
||||||
if let Some(credentials) = &credentials {
|
if let Some(credentials) = &credentials {
|
||||||
let _ = send_rtt_for_node(
|
let _ = send_rtt_for_node(
|
||||||
@ -206,6 +234,17 @@ async fn handle_socket(mut socket: WebSocket, cnn: Pool<Postgres>) {
|
|||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"siteInfo" => {
|
||||||
|
if let Some(credentials) = &credentials {
|
||||||
|
send_site_info(
|
||||||
|
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}");
|
||||||
}
|
}
|
||||||
|
@ -7,11 +7,13 @@ mod rtt;
|
|||||||
mod node_perf;
|
mod node_perf;
|
||||||
mod search;
|
mod search;
|
||||||
mod site_heat_map;
|
mod site_heat_map;
|
||||||
|
mod site_info;
|
||||||
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 };
|
||||||
pub use throughput::{ send_throughput_for_all_nodes, send_throughput_for_node };
|
pub use throughput::{ send_throughput_for_all_nodes, send_throughput_for_node, send_throughput_for_all_nodes_by_site };
|
||||||
pub use rtt::{ send_rtt_for_all_nodes, send_rtt_for_node };
|
pub use rtt::{ send_rtt_for_all_nodes, send_rtt_for_node, send_rtt_for_all_nodes_site };
|
||||||
pub use node_perf::send_perf_for_node;
|
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;
|
@ -3,7 +3,7 @@ use futures::future::join_all;
|
|||||||
use influxdb2::{Client, models::Query};
|
use influxdb2::{Client, models::Query};
|
||||||
use pgdb::sqlx::{Pool, Postgres};
|
use pgdb::sqlx::{Pool, Postgres};
|
||||||
use crate::submissions::get_org_details;
|
use crate::submissions::get_org_details;
|
||||||
use self::{rtt_row::RttRow, rtt_host::{Rtt, RttHost, RttChart}};
|
use self::{rtt_row::{RttRow, RttSiteRow}, rtt_host::{Rtt, RttHost, RttChart}};
|
||||||
|
|
||||||
use super::time_period::InfluxTimePeriod;
|
use super::time_period::InfluxTimePeriod;
|
||||||
mod rtt_row;
|
mod rtt_row;
|
||||||
@ -26,6 +26,23 @@ pub async fn send_rtt_for_all_nodes(cnn: Pool<Postgres>, socket: &mut WebSocket,
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn send_rtt_for_all_nodes_site(cnn: Pool<Postgres>, socket: &mut WebSocket, key: &str, site_id: String, period: InfluxTimePeriod) -> anyhow::Result<()> {
|
||||||
|
let nodes = get_rtt_for_all_nodes_site(cnn, key, &site_id, period).await?;
|
||||||
|
|
||||||
|
let mut histogram = vec![0; 20];
|
||||||
|
for node in nodes.iter() {
|
||||||
|
for rtt in node.rtt.iter() {
|
||||||
|
let bucket = usize::min(19, (rtt.value / 200.0) as usize);
|
||||||
|
histogram[bucket] += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let chart = RttChart { msg: "rttChartSite".to_string(), nodes, histogram };
|
||||||
|
let json = serde_json::to_string(&chart).unwrap();
|
||||||
|
socket.send(Message::Text(json)).await.unwrap();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn send_rtt_for_node(cnn: Pool<Postgres>, socket: &mut WebSocket, key: &str, period: InfluxTimePeriod, node_id: String, node_name: String) -> anyhow::Result<()> {
|
pub async fn send_rtt_for_node(cnn: Pool<Postgres>, socket: &mut WebSocket, key: &str, period: InfluxTimePeriod, node_id: String, node_name: String) -> anyhow::Result<()> {
|
||||||
let node = get_rtt_for_node(cnn, key, node_id, node_name, period).await?;
|
let node = get_rtt_for_node(cnn, key, node_id, node_name, period).await?;
|
||||||
let nodes = vec![node];
|
let nodes = vec![node];
|
||||||
@ -61,6 +78,24 @@ pub async fn get_rtt_for_all_nodes(cnn: Pool<Postgres>, key: &str, period: Influ
|
|||||||
all_nodes
|
all_nodes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_rtt_for_all_nodes_site(cnn: Pool<Postgres>, key: &str, site_id: &str, period: InfluxTimePeriod) -> anyhow::Result<Vec<RttHost>> {
|
||||||
|
let node_status = pgdb::node_status(cnn.clone(), key).await?;
|
||||||
|
let mut futures = Vec::new();
|
||||||
|
for node in node_status {
|
||||||
|
futures.push(get_rtt_for_node_site(
|
||||||
|
cnn.clone(),
|
||||||
|
key,
|
||||||
|
node.node_id.to_string(),
|
||||||
|
node.node_name.to_string(),
|
||||||
|
site_id.to_string(),
|
||||||
|
period.clone(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let all_nodes: anyhow::Result<Vec<RttHost>> = join_all(futures).await
|
||||||
|
.into_iter().collect();
|
||||||
|
all_nodes
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn get_rtt_for_node(
|
pub async fn get_rtt_for_node(
|
||||||
cnn: Pool<Postgres>,
|
cnn: Pool<Postgres>,
|
||||||
key: &str,
|
key: &str,
|
||||||
@ -116,3 +151,62 @@ pub async fn get_rtt_for_node(
|
|||||||
}
|
}
|
||||||
Err(anyhow::Error::msg("Unable to query influx"))
|
Err(anyhow::Error::msg("Unable to query influx"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_rtt_for_node_site(
|
||||||
|
cnn: Pool<Postgres>,
|
||||||
|
key: &str,
|
||||||
|
node_id: String,
|
||||||
|
node_name: String,
|
||||||
|
site_id: String,
|
||||||
|
period: InfluxTimePeriod,
|
||||||
|
) -> anyhow::Result<RttHost> {
|
||||||
|
if let Some(org) = get_org_details(cnn, key).await {
|
||||||
|
let influx_url = format!("http://{}:8086", org.influx_host);
|
||||||
|
let client = Client::new(influx_url, &org.influx_org, &org.influx_token);
|
||||||
|
|
||||||
|
let qs = format!(
|
||||||
|
"from(bucket: \"{}\")
|
||||||
|
|> {}
|
||||||
|
|> filter(fn: (r) => r[\"_measurement\"] == \"tree\")
|
||||||
|
|> filter(fn: (r) => r[\"organization_id\"] == \"{}\")
|
||||||
|
|> filter(fn: (r) => r[\"host_id\"] == \"{}\")
|
||||||
|
|> filter(fn: (r) => r[\"node_name\"] == \"{}\")
|
||||||
|
|> filter(fn: (r) => r[\"_field\"] == \"rtt_avg\" or r[\"_field\"] == \"rtt_max\" or r[\"_field\"] == \"rtt_min\")
|
||||||
|
|> {}
|
||||||
|
|> yield(name: \"last\")",
|
||||||
|
org.influx_bucket, period.range(), org.key, node_id, site_id, period.aggregate_window()
|
||||||
|
);
|
||||||
|
|
||||||
|
let query = Query::new(qs);
|
||||||
|
let rows = client.query::<RttSiteRow>(Some(query)).await;
|
||||||
|
match rows {
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Error querying InfluxDB: {}", e);
|
||||||
|
return Err(anyhow::Error::msg("Unable to query influx"));
|
||||||
|
}
|
||||||
|
Ok(rows) => {
|
||||||
|
// Parse and send the data
|
||||||
|
//println!("{rows:?}");
|
||||||
|
|
||||||
|
let mut rtt = Vec::new();
|
||||||
|
|
||||||
|
// Fill download
|
||||||
|
for row in rows.iter() {
|
||||||
|
rtt.push(Rtt {
|
||||||
|
value: row.rtt_avg,
|
||||||
|
date: row.time.format("%Y-%m-%d %H:%M:%S").to_string(),
|
||||||
|
l: row.rtt_min,
|
||||||
|
u: row.rtt_max - row.rtt_min,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(RttHost{
|
||||||
|
node_id,
|
||||||
|
node_name,
|
||||||
|
rtt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(anyhow::Error::msg("Unable to query influx"))
|
||||||
|
}
|
||||||
|
@ -20,4 +20,25 @@ impl Default for RttRow {
|
|||||||
time: DateTime::<Utc>::MIN_UTC.into(),
|
time: DateTime::<Utc>::MIN_UTC.into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, FromDataPoint)]
|
||||||
|
pub struct RttSiteRow {
|
||||||
|
pub host_id: String,
|
||||||
|
pub rtt_min: f64,
|
||||||
|
pub rtt_max: f64,
|
||||||
|
pub rtt_avg: f64,
|
||||||
|
pub time: DateTime<FixedOffset>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for RttSiteRow {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
host_id: "".to_string(),
|
||||||
|
rtt_min: 0.0,
|
||||||
|
rtt_max: 0.0,
|
||||||
|
rtt_avg: 0.0,
|
||||||
|
time: DateTime::<Utc>::MIN_UTC.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
use axum::extract::ws::{WebSocket, Message};
|
||||||
|
use pgdb::sqlx::{Pool, Postgres};
|
||||||
|
use serde::Serialize;
|
||||||
|
use super::site_tree::SiteTree;
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct SiteInfoMessage {
|
||||||
|
msg: String,
|
||||||
|
data: SiteTree,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn send_site_info(cnn: Pool<Postgres>, socket: &mut WebSocket, key: &str, site_id: &str) {
|
||||||
|
if let Ok(host) = pgdb::get_site_info(cnn, key, site_id).await {
|
||||||
|
let host = SiteTree::from(host);
|
||||||
|
let msg = SiteInfoMessage {
|
||||||
|
msg: "site_info".to_string(),
|
||||||
|
data: host,
|
||||||
|
};
|
||||||
|
let json = serde_json::to_string(&msg).unwrap();
|
||||||
|
if let Err(e) = socket.send(Message::Text(json)).await {
|
||||||
|
tracing::error!("Error sending message: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -3,7 +3,7 @@ use futures::future::join_all;
|
|||||||
use influxdb2::{Client, models::Query};
|
use influxdb2::{Client, models::Query};
|
||||||
use pgdb::sqlx::{Pool, Postgres};
|
use pgdb::sqlx::{Pool, Postgres};
|
||||||
use crate::submissions::get_org_details;
|
use crate::submissions::get_org_details;
|
||||||
use self::{throughput_host::{ThroughputHost, Throughput, ThroughputChart}, throughput_row::ThroughputRow};
|
use self::{throughput_host::{ThroughputHost, Throughput, ThroughputChart}, throughput_row::{ThroughputRow, ThroughputRowBySite}};
|
||||||
|
|
||||||
use super::time_period::InfluxTimePeriod;
|
use super::time_period::InfluxTimePeriod;
|
||||||
mod throughput_host;
|
mod throughput_host;
|
||||||
@ -18,6 +18,15 @@ pub async fn send_throughput_for_all_nodes(cnn: Pool<Postgres>, socket: &mut Web
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn send_throughput_for_all_nodes_by_site(cnn: Pool<Postgres>, socket: &mut WebSocket, key: &str, site_name: String, period: InfluxTimePeriod) -> anyhow::Result<()> {
|
||||||
|
let nodes = get_throughput_for_all_nodes_by_site(cnn, key, period, &site_name).await?;
|
||||||
|
|
||||||
|
let chart = ThroughputChart { msg: "bitsChartSite".to_string(), nodes };
|
||||||
|
let json = serde_json::to_string(&chart).unwrap();
|
||||||
|
socket.send(Message::Text(json)).await.unwrap();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn send_throughput_for_node(cnn: Pool<Postgres>, socket: &mut WebSocket, key: &str, period: InfluxTimePeriod, node_id: String, node_name: String) -> anyhow::Result<()> {
|
pub async fn send_throughput_for_node(cnn: Pool<Postgres>, socket: &mut WebSocket, key: &str, period: InfluxTimePeriod, node_id: String, node_name: String) -> anyhow::Result<()> {
|
||||||
let node = get_throughput_for_node(cnn, key, node_id, node_name, period).await?;
|
let node = get_throughput_for_node(cnn, key, node_id, node_name, period).await?;
|
||||||
|
|
||||||
@ -44,6 +53,24 @@ pub async fn get_throughput_for_all_nodes(cnn: Pool<Postgres>, key: &str, period
|
|||||||
all_nodes
|
all_nodes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_throughput_for_all_nodes_by_site(cnn: Pool<Postgres>, key: &str, period: InfluxTimePeriod, site_name: &str) -> anyhow::Result<Vec<ThroughputHost>> {
|
||||||
|
let node_status = pgdb::node_status(cnn.clone(), key).await?;
|
||||||
|
let mut futures = Vec::new();
|
||||||
|
for node in node_status {
|
||||||
|
futures.push(get_throughput_for_node_by_site(
|
||||||
|
cnn.clone(),
|
||||||
|
key,
|
||||||
|
node.node_id.to_string(),
|
||||||
|
node.node_name.to_string(),
|
||||||
|
site_name.to_string(),
|
||||||
|
period.clone(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let all_nodes: anyhow::Result<Vec<ThroughputHost>> = join_all(futures).await
|
||||||
|
.into_iter().collect();
|
||||||
|
all_nodes
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn get_throughput_for_node(
|
pub async fn get_throughput_for_node(
|
||||||
cnn: Pool<Postgres>,
|
cnn: Pool<Postgres>,
|
||||||
key: &str,
|
key: &str,
|
||||||
@ -111,3 +138,75 @@ pub async fn get_throughput_for_node(
|
|||||||
}
|
}
|
||||||
Err(anyhow::Error::msg("Unable to query influx"))
|
Err(anyhow::Error::msg("Unable to query influx"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_throughput_for_node_by_site(
|
||||||
|
cnn: Pool<Postgres>,
|
||||||
|
key: &str,
|
||||||
|
node_id: String,
|
||||||
|
node_name: String,
|
||||||
|
site_name: String,
|
||||||
|
period: InfluxTimePeriod,
|
||||||
|
) -> anyhow::Result<ThroughputHost> {
|
||||||
|
if let Some(org) = get_org_details(cnn, key).await {
|
||||||
|
let influx_url = format!("http://{}:8086", org.influx_host);
|
||||||
|
let client = Client::new(influx_url, &org.influx_org, &org.influx_token);
|
||||||
|
|
||||||
|
let qs = format!(
|
||||||
|
"from(bucket: \"{}\")
|
||||||
|
|> {}
|
||||||
|
|> filter(fn: (r) => r[\"_measurement\"] == \"tree\")
|
||||||
|
|> filter(fn: (r) => r[\"organization_id\"] == \"{}\")
|
||||||
|
|> filter(fn: (r) => r[\"host_id\"] == \"{}\")
|
||||||
|
|> filter(fn: (r) => r[\"node_name\"] == \"{}\")
|
||||||
|
|> filter(fn: (r) => r[\"_field\"] == \"bits_avg\" or r[\"_field\"] == \"bits_max\" or r[\"_field\"] == \"bits_min\")
|
||||||
|
|> {}
|
||||||
|
|> yield(name: \"last\")",
|
||||||
|
org.influx_bucket, period.range(), org.key, node_id, site_name, period.aggregate_window()
|
||||||
|
);
|
||||||
|
|
||||||
|
let query = Query::new(qs);
|
||||||
|
//println!("{:?}", query);
|
||||||
|
let rows = client.query::<ThroughputRowBySite>(Some(query)).await;
|
||||||
|
match rows {
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Error querying InfluxDB: {}", e);
|
||||||
|
return Err(anyhow::Error::msg("Unable to query influx"));
|
||||||
|
}
|
||||||
|
Ok(rows) => {
|
||||||
|
// Parse and send the data
|
||||||
|
//println!("{rows:?}");
|
||||||
|
|
||||||
|
let mut down = Vec::new();
|
||||||
|
let mut up = Vec::new();
|
||||||
|
|
||||||
|
// Fill download
|
||||||
|
for row in rows.iter().filter(|r| r.direction == "down") {
|
||||||
|
down.push(Throughput {
|
||||||
|
value: row.bits_avg,
|
||||||
|
date: row.time.format("%Y-%m-%d %H:%M:%S").to_string(),
|
||||||
|
l: row.bits_min,
|
||||||
|
u: row.bits_max - row.bits_min,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill upload
|
||||||
|
for row in rows.iter().filter(|r| r.direction == "up") {
|
||||||
|
up.push(Throughput {
|
||||||
|
value: row.bits_avg,
|
||||||
|
date: row.time.format("%Y-%m-%d %H:%M:%S").to_string(),
|
||||||
|
l: row.bits_min,
|
||||||
|
u: row.bits_max - row.bits_min,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(ThroughputHost{
|
||||||
|
node_id,
|
||||||
|
node_name,
|
||||||
|
down,
|
||||||
|
up,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(anyhow::Error::msg("Unable to query influx"))
|
||||||
|
}
|
||||||
|
@ -22,4 +22,27 @@ impl Default for ThroughputRow {
|
|||||||
time: DateTime::<Utc>::MIN_UTC.into(),
|
time: DateTime::<Utc>::MIN_UTC.into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, FromDataPoint)]
|
||||||
|
pub struct ThroughputRowBySite {
|
||||||
|
pub direction: String,
|
||||||
|
pub host_id: String,
|
||||||
|
pub bits_min: f64,
|
||||||
|
pub bits_max: f64,
|
||||||
|
pub bits_avg: f64,
|
||||||
|
pub time: DateTime<FixedOffset>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ThroughputRowBySite {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
direction: "".to_string(),
|
||||||
|
host_id: "".to_string(),
|
||||||
|
bits_min: 0.0,
|
||||||
|
bits_max: 0.0,
|
||||||
|
bits_avg: 0.0,
|
||||||
|
time: DateTime::<Utc>::MIN_UTC.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -25,4 +25,17 @@ pub async fn get_site_tree(
|
|||||||
.fetch_all(&cnn)
|
.fetch_all(&cnn)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| StatsHostError::DatabaseError(e.to_string()))
|
.map_err(|e| StatsHostError::DatabaseError(e.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_site_info(
|
||||||
|
cnn: Pool<Postgres>,
|
||||||
|
key: &str,
|
||||||
|
site_name: &str,
|
||||||
|
) -> Result<TreeNode, StatsHostError> {
|
||||||
|
sqlx::query_as::<_, TreeNode>("SELECT site_name, index, parent, site_type, max_down, max_up, current_down, current_up, current_rtt 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()))
|
||||||
|
}
|
||||||
|
@ -89,6 +89,16 @@ export class Bus {
|
|||||||
this.ws.send(json);
|
this.ws.send(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
requestThroughputChartSite(site_id: string) {
|
||||||
|
let request = {
|
||||||
|
msg: "throughputChartSite",
|
||||||
|
period: window.graphPeriod,
|
||||||
|
site_id: decodeURI(site_id),
|
||||||
|
};
|
||||||
|
let json = JSON.stringify(request);
|
||||||
|
this.ws.send(json);
|
||||||
|
}
|
||||||
|
|
||||||
requestRttChart() {
|
requestRttChart() {
|
||||||
this.ws.send("{ \"msg\": \"rttChart\", \"period\": \"" + window.graphPeriod + "\" }");
|
this.ws.send("{ \"msg\": \"rttChart\", \"period\": \"" + window.graphPeriod + "\" }");
|
||||||
}
|
}
|
||||||
@ -104,6 +114,16 @@ export class Bus {
|
|||||||
this.ws.send(json);
|
this.ws.send(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
requestRttChartSite(site_id: string) {
|
||||||
|
let request = {
|
||||||
|
msg: "rttChartSite",
|
||||||
|
period: window.graphPeriod,
|
||||||
|
site_id: decodeURI(site_id),
|
||||||
|
};
|
||||||
|
let json = JSON.stringify(request);
|
||||||
|
this.ws.send(json);
|
||||||
|
}
|
||||||
|
|
||||||
requestNodePerfChart(node_id: string, node_name: string) {
|
requestNodePerfChart(node_id: string, node_name: string) {
|
||||||
let request = {
|
let request = {
|
||||||
msg: "nodePerf",
|
msg: "nodePerf",
|
||||||
@ -131,6 +151,15 @@ export class Bus {
|
|||||||
requestTree(parent: string) {
|
requestTree(parent: string) {
|
||||||
this.ws.send("{ \"msg\": \"siteTree\", \"parent\": \"" + parent + "\" }");
|
this.ws.send("{ \"msg\": \"siteTree\", \"parent\": \"" + parent + "\" }");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
requestSiteInfo(site_id: string) {
|
||||||
|
let request = {
|
||||||
|
msg: "siteInfo",
|
||||||
|
site_id: decodeURI(site_id),
|
||||||
|
};
|
||||||
|
let json = JSON.stringify(request);
|
||||||
|
this.ws.send(json);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatToken(token: string) {
|
function formatToken(token: string) {
|
||||||
|
@ -0,0 +1,62 @@
|
|||||||
|
import { scaleNumber } from "../helpers";
|
||||||
|
import { Component } from "./component";
|
||||||
|
import * as echarts from 'echarts';
|
||||||
|
|
||||||
|
export class RttHistoSite implements Component {
|
||||||
|
div: HTMLElement;
|
||||||
|
myChart: echarts.ECharts;
|
||||||
|
download: any;
|
||||||
|
x: any;
|
||||||
|
chartMade: boolean = false;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.div = document.getElementById("rttHisto") as HTMLElement;
|
||||||
|
this.myChart = echarts.init(this.div);
|
||||||
|
this.myChart.showLoading();
|
||||||
|
}
|
||||||
|
|
||||||
|
wireup(): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
ontick(): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
onmessage(event: any): void {
|
||||||
|
if (event.msg == "rttChartSite") {
|
||||||
|
//console.log(event);
|
||||||
|
this.download = [];
|
||||||
|
this.x = [];
|
||||||
|
for (let i = 0; i < event.histogram.length; i++) {
|
||||||
|
this.download.push(event.histogram[i]);
|
||||||
|
this.x.push(i * 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.chartMade) {
|
||||||
|
this.myChart.hideLoading();
|
||||||
|
var option: echarts.EChartsOption;
|
||||||
|
this.myChart.setOption<echarts.EChartsOption>(
|
||||||
|
(option = {
|
||||||
|
title: { text: "TCP Round-Trip Time Histogram" },
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: this.x,
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
name: 'ms',
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: "RTT",
|
||||||
|
type: "bar",
|
||||||
|
data: this.download,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
);
|
||||||
|
option && this.myChart.setOption(option);
|
||||||
|
// this.chartMade = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
113
src/rust/long_term_stats/site_build/src/components/rtt_site.ts
Normal file
113
src/rust/long_term_stats/site_build/src/components/rtt_site.ts
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
import { scaleNumber } from "../helpers";
|
||||||
|
import { Component } from "./component";
|
||||||
|
import * as echarts from 'echarts';
|
||||||
|
|
||||||
|
export class RttChartSite implements Component {
|
||||||
|
div: HTMLElement;
|
||||||
|
myChart: echarts.ECharts;
|
||||||
|
chartMade: boolean = false;
|
||||||
|
siteId: string;
|
||||||
|
|
||||||
|
constructor(siteId: string) {
|
||||||
|
this.siteId = siteId;
|
||||||
|
this.div = document.getElementById("rttChart") as HTMLElement;
|
||||||
|
this.myChart = echarts.init(this.div);
|
||||||
|
this.myChart.showLoading();
|
||||||
|
}
|
||||||
|
|
||||||
|
wireup(): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
ontick(): void {
|
||||||
|
window.bus.requestRttChartSite(this.siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
onmessage(event: any): void {
|
||||||
|
if (event.msg == "rttChartSite") {
|
||||||
|
let series: echarts.SeriesOption[] = [];
|
||||||
|
|
||||||
|
// Iterate all provides nodes and create a set of series for each,
|
||||||
|
// providing upload and download banding per node.
|
||||||
|
let x: any[] = [];
|
||||||
|
let first = true;
|
||||||
|
let legend: string[] = [];
|
||||||
|
for (let i=0; i<event.nodes.length; i++) {
|
||||||
|
let node = event.nodes[i];
|
||||||
|
legend.push(node.node_name);
|
||||||
|
//console.log(node);
|
||||||
|
|
||||||
|
let d: number[] = [];
|
||||||
|
let u: number[] = [];
|
||||||
|
let l: number[] = [];
|
||||||
|
for (let j=0; j<node.rtt.length; j++) {
|
||||||
|
if (first) x.push(node.rtt[j].date);
|
||||||
|
d.push(node.rtt[j].value);
|
||||||
|
u.push(node.rtt[j].u);
|
||||||
|
l.push(node.rtt[j].l);
|
||||||
|
}
|
||||||
|
if (first) first = false;
|
||||||
|
|
||||||
|
let min: echarts.SeriesOption = {
|
||||||
|
name: "L",
|
||||||
|
type: "line",
|
||||||
|
data: l,
|
||||||
|
symbol: 'none',
|
||||||
|
stack: 'confidence-band-' + node.node_id,
|
||||||
|
lineStyle: {
|
||||||
|
opacity: 0
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let max: echarts.SeriesOption = {
|
||||||
|
name: "U",
|
||||||
|
type: "line",
|
||||||
|
data: u,
|
||||||
|
symbol: 'none',
|
||||||
|
stack: 'confidence-band-' + node.node_id,
|
||||||
|
lineStyle: {
|
||||||
|
opacity: 0
|
||||||
|
},
|
||||||
|
areaStyle: {
|
||||||
|
color: '#ccc'
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let val: echarts.SeriesOption = {
|
||||||
|
name: node.node_name,
|
||||||
|
type: "line",
|
||||||
|
data: d,
|
||||||
|
symbol: 'none',
|
||||||
|
};
|
||||||
|
|
||||||
|
series.push(min);
|
||||||
|
series.push(max);
|
||||||
|
series.push(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.chartMade) {
|
||||||
|
this.myChart.hideLoading();
|
||||||
|
var option: echarts.EChartsOption;
|
||||||
|
this.myChart.setOption<echarts.EChartsOption>(
|
||||||
|
(option = {
|
||||||
|
title: { text: "TCP Round-Trip Time" },
|
||||||
|
legend: {
|
||||||
|
orient: "horizontal",
|
||||||
|
right: 10,
|
||||||
|
top: "bottom",
|
||||||
|
data: legend,
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: x,
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
name: 'ms',
|
||||||
|
},
|
||||||
|
series: series
|
||||||
|
})
|
||||||
|
);
|
||||||
|
option && this.myChart.setOption(option);
|
||||||
|
// this.chartMade = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,38 @@
|
|||||||
|
import { scaleNumber } from "../helpers";
|
||||||
|
import { mbps_to_bps } from "../site_tree/site_tree";
|
||||||
|
import { Component } from "./component";
|
||||||
|
|
||||||
|
export class SiteInfo implements Component {
|
||||||
|
siteId: string;
|
||||||
|
count: number = 0;
|
||||||
|
|
||||||
|
constructor(siteId: string) {
|
||||||
|
this.siteId = siteId;
|
||||||
|
}
|
||||||
|
|
||||||
|
wireup(): void {
|
||||||
|
window.bus.requestSiteInfo(this.siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
ontick(): void {
|
||||||
|
this.count++;
|
||||||
|
if (this.count % 10 == 0) {
|
||||||
|
window.bus.requestSiteInfo(this.siteId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onmessage(event: any): void {
|
||||||
|
if (event.msg == "site_info") {
|
||||||
|
//console.log(event.data);
|
||||||
|
(document.getElementById("siteName") as HTMLElement).innerText = event.data.site_name;
|
||||||
|
let div = document.getElementById("siteInfo") as HTMLDivElement;
|
||||||
|
let html = "";
|
||||||
|
html += "<table class='table table-striped'>";
|
||||||
|
html += "<tr><td>Max:</td><td>" + scaleNumber(event.data.max_down * mbps_to_bps) + " / " + scaleNumber(event.data.max_up * mbps_to_bps) + "</td></tr>";
|
||||||
|
html += "<tr><td>Current:</td><td>" + scaleNumber(event.data.current_down) + " / " + scaleNumber(event.data.current_up) + "</td></tr>";
|
||||||
|
html += "<tr><td>Current RTT:</td><td>" + event.data.current_rtt / 100.0 + "</td></tr>";
|
||||||
|
html += "</table>";
|
||||||
|
div.innerHTML = html;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,162 @@
|
|||||||
|
import { scaleNumber } from "../helpers";
|
||||||
|
import { Component } from "./component";
|
||||||
|
import * as echarts from 'echarts';
|
||||||
|
|
||||||
|
export class ThroughputSiteChart implements Component {
|
||||||
|
div: HTMLElement;
|
||||||
|
myChart: echarts.ECharts;
|
||||||
|
chartMade: boolean = false;
|
||||||
|
siteId: string;
|
||||||
|
|
||||||
|
constructor(siteId: string) {
|
||||||
|
this.siteId = siteId;
|
||||||
|
this.div = document.getElementById("throughputChart") as HTMLElement;
|
||||||
|
this.myChart = echarts.init(this.div);
|
||||||
|
this.myChart.showLoading();
|
||||||
|
}
|
||||||
|
|
||||||
|
wireup(): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
ontick(): void {
|
||||||
|
window.bus.requestThroughputChartSite(this.siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
onmessage(event: any): void {
|
||||||
|
if (event.msg == "bitsChartSite") {
|
||||||
|
let series: echarts.SeriesOption[] = [];
|
||||||
|
|
||||||
|
// Iterate all provides nodes and create a set of series for each,
|
||||||
|
// providing upload and download banding per node.
|
||||||
|
let x: any[] = [];
|
||||||
|
let first = true;
|
||||||
|
let legend: string[] = [];
|
||||||
|
for (let i=0; i<event.nodes.length; i++) {
|
||||||
|
let node = event.nodes[i];
|
||||||
|
legend.push(node.node_name + " DL");
|
||||||
|
legend.push(node.node_name + " UL");
|
||||||
|
//console.log(node);
|
||||||
|
|
||||||
|
let d: number[] = [];
|
||||||
|
let u: number[] = [];
|
||||||
|
let l: number[] = [];
|
||||||
|
for (let j=0; j<node.down.length; j++) {
|
||||||
|
if (first) x.push(node.down[j].date);
|
||||||
|
d.push(node.down[j].value);
|
||||||
|
u.push(node.down[j].u);
|
||||||
|
l.push(node.down[j].l);
|
||||||
|
}
|
||||||
|
if (first) first = false;
|
||||||
|
|
||||||
|
let min: echarts.SeriesOption = {
|
||||||
|
name: "L",
|
||||||
|
type: "line",
|
||||||
|
data: l,
|
||||||
|
symbol: 'none',
|
||||||
|
stack: 'confidence-band-' + node.node_id,
|
||||||
|
lineStyle: {
|
||||||
|
opacity: 0
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let max: echarts.SeriesOption = {
|
||||||
|
name: "U",
|
||||||
|
type: "line",
|
||||||
|
data: u,
|
||||||
|
symbol: 'none',
|
||||||
|
stack: 'confidence-band-' + node.node_id,
|
||||||
|
lineStyle: {
|
||||||
|
opacity: 0
|
||||||
|
},
|
||||||
|
areaStyle: {
|
||||||
|
color: '#ccc'
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let val: echarts.SeriesOption = {
|
||||||
|
name: node.node_name + " DL",
|
||||||
|
type: "line",
|
||||||
|
data: d,
|
||||||
|
symbol: 'none',
|
||||||
|
};
|
||||||
|
|
||||||
|
series.push(min);
|
||||||
|
series.push(max);
|
||||||
|
series.push(val);
|
||||||
|
|
||||||
|
// Do the same for upload
|
||||||
|
d = [];
|
||||||
|
u = [];
|
||||||
|
l = [];
|
||||||
|
for (let j=0; j<node.down.length; j++) {
|
||||||
|
d.push(0.0 - node.up[j].value);
|
||||||
|
u.push(0.0 - node.up[j].u);
|
||||||
|
l.push(0.0 - node.up[j].l);
|
||||||
|
}
|
||||||
|
|
||||||
|
min = {
|
||||||
|
name: "L",
|
||||||
|
type: "line",
|
||||||
|
data: l,
|
||||||
|
symbol: 'none',
|
||||||
|
stack: 'confidence-band-' + node.node_id,
|
||||||
|
lineStyle: {
|
||||||
|
opacity: 0
|
||||||
|
},
|
||||||
|
};
|
||||||
|
max = {
|
||||||
|
name: "U",
|
||||||
|
type: "line",
|
||||||
|
data: u,
|
||||||
|
symbol: 'none',
|
||||||
|
stack: 'confidence-band-' + node.node_id,
|
||||||
|
lineStyle: {
|
||||||
|
opacity: 0
|
||||||
|
},
|
||||||
|
areaStyle: {
|
||||||
|
color: '#ccc'
|
||||||
|
},
|
||||||
|
};
|
||||||
|
val = {
|
||||||
|
name: node.node_name + " UL",
|
||||||
|
type: "line",
|
||||||
|
data: d,
|
||||||
|
symbol: 'none',
|
||||||
|
};
|
||||||
|
|
||||||
|
series.push(min);
|
||||||
|
series.push(max);
|
||||||
|
series.push(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.chartMade) {
|
||||||
|
this.myChart.hideLoading();
|
||||||
|
var option: echarts.EChartsOption;
|
||||||
|
this.myChart.setOption<echarts.EChartsOption>(
|
||||||
|
(option = {
|
||||||
|
title: { text: "Bits" },
|
||||||
|
legend: {
|
||||||
|
orient: "horizontal",
|
||||||
|
right: 10,
|
||||||
|
top: "bottom",
|
||||||
|
data: legend,
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: x,
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
axisLabel: {
|
||||||
|
formatter: function (val: number) {
|
||||||
|
return scaleNumber(Math.abs(val));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
series: series
|
||||||
|
})
|
||||||
|
);
|
||||||
|
option && this.myChart.setOption(option);
|
||||||
|
// this.chartMade = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -37,7 +37,7 @@ export function usageColor(percent: number): string {
|
|||||||
return "#aaffaa";
|
return "#aaffaa";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function rttColor(n) {
|
export function rttColor(n: number): string {
|
||||||
if (n <= 100) {
|
if (n <= 100) {
|
||||||
return "#aaffaa";
|
return "#aaffaa";
|
||||||
} else if (n <= 150) {
|
} else if (n <= 150) {
|
||||||
@ -45,4 +45,13 @@ export function rttColor(n) {
|
|||||||
} else {
|
} else {
|
||||||
return "#ffaaaa";
|
return "#ffaaaa";
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeUrl(type: string, id: string): string {
|
||||||
|
switch (type) {
|
||||||
|
case "site": return "site:" + id;
|
||||||
|
case "ap": return "ap:" + id;
|
||||||
|
case "circuit": return "circuit:" + id;
|
||||||
|
default: return "site:" + id;
|
||||||
|
}
|
||||||
}
|
}
|
@ -4,6 +4,7 @@ import { LoginPage } from './login/login';
|
|||||||
import { Page } from './page';
|
import { Page } from './page';
|
||||||
import { ShaperNodePage } from './shapernode/shapernode';
|
import { ShaperNodePage } from './shapernode/shapernode';
|
||||||
import { ShaperNodesPage } from './shapernodes/shapernodes';
|
import { ShaperNodesPage } from './shapernodes/shapernodes';
|
||||||
|
import { SitePage } from './site/site';
|
||||||
import { SiteTreePage } from './site_tree/site_tree';
|
import { SiteTreePage } from './site_tree/site_tree';
|
||||||
|
|
||||||
export class SiteRouter {
|
export class SiteRouter {
|
||||||
@ -70,6 +71,11 @@ export class SiteRouter {
|
|||||||
this.curentPage = new SiteTreePage();
|
this.curentPage = new SiteTreePage();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "site": {
|
||||||
|
this.currentAnchor = "site:" + split[1];
|
||||||
|
this.curentPage = new SitePage(split[1]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
default: {
|
default: {
|
||||||
alert("I don't know how to go to: " + split[0].toLowerCase());
|
alert("I don't know how to go to: " + split[0].toLowerCase());
|
||||||
this.goto("dashboard");
|
this.goto("dashboard");
|
||||||
|
52
src/rust/long_term_stats/site_build/src/site/site.ts
Normal file
52
src/rust/long_term_stats/site_build/src/site/site.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import html from './template.html';
|
||||||
|
import { Page } from '../page'
|
||||||
|
import { MenuPage } from '../menu/menu';
|
||||||
|
import { Component } from '../components/component';
|
||||||
|
import { ThroughputSiteChart } from '../components/throughput_site';
|
||||||
|
import { SiteInfo } from '../components/site_info';
|
||||||
|
import { RttChartSite } from '../components/rtt_site';
|
||||||
|
import { RttHistoSite } from '../components/rtt_histo_site';
|
||||||
|
|
||||||
|
export class SitePage implements Page {
|
||||||
|
menu: MenuPage;
|
||||||
|
components: Component[];
|
||||||
|
siteId: string;
|
||||||
|
|
||||||
|
constructor(siteId: string) {
|
||||||
|
this.siteId = siteId;
|
||||||
|
this.menu = new MenuPage("sitetreeDash");
|
||||||
|
let container = document.getElementById('mainContent');
|
||||||
|
if (container) {
|
||||||
|
container.innerHTML = html;
|
||||||
|
}
|
||||||
|
this.components = [
|
||||||
|
new SiteInfo(siteId),
|
||||||
|
new ThroughputSiteChart(siteId),
|
||||||
|
new RttChartSite(siteId),
|
||||||
|
new RttHistoSite(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
wireup() {
|
||||||
|
this.components.forEach(component => {
|
||||||
|
component.wireup();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ontick(): void {
|
||||||
|
this.menu.ontick();
|
||||||
|
this.components.forEach(component => {
|
||||||
|
component.ontick();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onmessage(event: any) {
|
||||||
|
if (event.msg) {
|
||||||
|
this.menu.onmessage(event);
|
||||||
|
|
||||||
|
this.components.forEach(component => {
|
||||||
|
component.onmessage(event);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
51
src/rust/long_term_stats/site_build/src/site/template.html
Normal file
51
src/rust/long_term_stats/site_build/src/site/template.html
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12" id="nodeStatus">
|
||||||
|
<h1 id="siteName">Site Name</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body" id="siteInfo">
|
||||||
|
Details
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div id="throughputChart" style="height: 250px"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div id="rttChart" style="height: 250px"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div id="rttHisto" style="height: 250px"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div id="rootHeat" style="height: 900px"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -3,12 +3,13 @@ import { Page } from '../page'
|
|||||||
import { MenuPage } from '../menu/menu';
|
import { MenuPage } from '../menu/menu';
|
||||||
import { Component } from '../components/component';
|
import { Component } from '../components/component';
|
||||||
import mermaid from 'mermaid';
|
import mermaid from 'mermaid';
|
||||||
import { rttColor, scaleNumber, siteIcon, usageColor } from '../helpers';
|
import { makeUrl, rttColor, scaleNumber, siteIcon, usageColor } from '../helpers';
|
||||||
|
|
||||||
export class SiteTreePage implements Page {
|
export class SiteTreePage implements Page {
|
||||||
menu: MenuPage;
|
menu: MenuPage;
|
||||||
components: Component[];
|
components: Component[];
|
||||||
selectedNode: string;
|
selectedNode: string;
|
||||||
|
count: number = 0;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.menu = new MenuPage("sitetreeDash");
|
this.menu = new MenuPage("sitetreeDash");
|
||||||
@ -33,6 +34,10 @@ export class SiteTreePage implements Page {
|
|||||||
this.components.forEach(component => {
|
this.components.forEach(component => {
|
||||||
component.ontick();
|
component.ontick();
|
||||||
});
|
});
|
||||||
|
if (this.count % 10 == 0 && this.selectedNode != "") {
|
||||||
|
fetchTree(this.selectedNode);
|
||||||
|
}
|
||||||
|
this.count++;
|
||||||
}
|
}
|
||||||
|
|
||||||
onmessage(event: any) {
|
onmessage(event: any) {
|
||||||
@ -90,7 +95,7 @@ class TreeItem {
|
|||||||
current_rtt: number;
|
current_rtt: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mbps_to_bps = 1000000;
|
export const mbps_to_bps = 1000000;
|
||||||
|
|
||||||
function buildTree(data: TreeItem[]) {
|
function buildTree(data: TreeItem[]) {
|
||||||
data.sort((a,b) => {
|
data.sort((a,b) => {
|
||||||
@ -110,7 +115,8 @@ function buildTree(data: TreeItem[]) {
|
|||||||
let usageBg = usageColor(peak);
|
let usageBg = usageColor(peak);
|
||||||
let rttBg = rttColor(data[i].current_rtt / 100);
|
let rttBg = rttColor(data[i].current_rtt / 100);
|
||||||
html += "<tr>";
|
html += "<tr>";
|
||||||
html += "<td>" + siteIcon(data[i].site_type) + " " + data[i].site_name;
|
let url = makeUrl(data[i].site_type, data[i].site_name);
|
||||||
|
html += "<td>" + siteIcon(data[i].site_type) + " <a href='#" + url + "' onclick='window.router.goto(\"" + url + "\")'>" + data[i].site_name + "</a>";
|
||||||
html += "</td><td>" + scaleNumber(data[i].max_down * mbps_to_bps) + " / " + scaleNumber(data[i].max_up * mbps_to_bps) + "</td>";
|
html += "</td><td>" + scaleNumber(data[i].max_down * mbps_to_bps) + " / " + scaleNumber(data[i].max_up * mbps_to_bps) + "</td>";
|
||||||
html += "</td><td>" + scaleNumber(data[i].current_down) + " / " + scaleNumber(data[i].current_up) + "</td>";
|
html += "</td><td>" + scaleNumber(data[i].current_down) + " / " + scaleNumber(data[i].current_up) + "</td>";
|
||||||
html += "<td style='background-color: " + usageBg + "'>" + up.toFixed(1) + "% / " + down.toFixed(1) + "%</td>";
|
html += "<td style='background-color: " + usageBg + "'>" + up.toFixed(1) + "% / " + down.toFixed(1) + "%</td>";
|
||||||
@ -149,7 +155,8 @@ function treeChildren(data: TreeItem[], parent: number, depth: number) : string
|
|||||||
for (let j=0; j<depth; j++) {
|
for (let j=0; j<depth; j++) {
|
||||||
html += " ";
|
html += " ";
|
||||||
}
|
}
|
||||||
html += siteIcon(data[i].site_type) + " " + data[i].site_name;
|
let url = makeUrl(data[i].site_type, data[i].site_name);
|
||||||
|
html += siteIcon(data[i].site_type) + " <a href='#" + url + "' onclick='window.router.goto(\"" + url + "\")'>" + data[i].site_name + "</a>";
|
||||||
html += "</td><td>" + scaleNumber(data[i].max_down * mbps_to_bps) + " / " + scaleNumber(data[i].max_up * mbps_to_bps) + "</td>";
|
html += "</td><td>" + scaleNumber(data[i].max_down * mbps_to_bps) + " / " + scaleNumber(data[i].max_up * mbps_to_bps) + "</td>";
|
||||||
html += "</td><td>" + scaleNumber(data[i].current_down) + " / " + scaleNumber(data[i].current_up) + "</td>";
|
html += "</td><td>" + scaleNumber(data[i].current_down) + " / " + scaleNumber(data[i].current_up) + "</td>";
|
||||||
html += "<td style='background-color: " + usageBg + "'>" + up.toFixed(1) + "% / " + down.toFixed(1) + "%</td>";
|
html += "<td style='background-color: " + usageBg + "'>" + up.toFixed(1) + "% / " + down.toFixed(1) + "%</td>";
|
||||||
|
Loading…
Reference in New Issue
Block a user