From a1c4d6c6d05c1122744bc06066caa4c9f6d23ce5 Mon Sep 17 00:00:00 2001 From: Herbert Wolverson Date: Wed, 10 May 2023 16:28:54 +0000 Subject: [PATCH] Add initial site view. --- .../lts_node/src/web/wss/mod.rs | 41 ++++- .../lts_node/src/web/wss/queries/mod.rs | 8 +- .../lts_node/src/web/wss/queries/rtt/mod.rs | 96 ++++++++++- .../src/web/wss/queries/rtt/rtt_row.rs | 21 +++ .../lts_node/src/web/wss/queries/site_info.rs | 25 +++ .../src/web/wss/queries/throughput/mod.rs | 101 ++++++++++- .../wss/queries/throughput/throughput_row.rs | 23 +++ src/rust/long_term_stats/pgdb/src/tree.rs | 15 +- .../long_term_stats/site_build/src/bus.ts | 29 ++++ .../src/components/rtt_histo_site.ts | 62 +++++++ .../site_build/src/components/rtt_site.ts | 113 ++++++++++++ .../site_build/src/components/site_info.ts | 38 ++++ .../src/components/throughput_site.ts | 162 ++++++++++++++++++ .../long_term_stats/site_build/src/helpers.ts | 11 +- .../long_term_stats/site_build/src/router.ts | 6 + .../site_build/src/site/site.ts | 52 ++++++ .../site_build/src/site/template.html | 51 ++++++ .../site_build/src/site_tree/site_tree.ts | 15 +- 18 files changed, 857 insertions(+), 12 deletions(-) create mode 100644 src/rust/long_term_stats/lts_node/src/web/wss/queries/site_info.rs create mode 100644 src/rust/long_term_stats/site_build/src/components/rtt_histo_site.ts create mode 100644 src/rust/long_term_stats/site_build/src/components/rtt_site.ts create mode 100644 src/rust/long_term_stats/site_build/src/components/site_info.ts create mode 100644 src/rust/long_term_stats/site_build/src/components/throughput_site.ts create mode 100644 src/rust/long_term_stats/site_build/src/site/site.ts create mode 100644 src/rust/long_term_stats/site_build/src/site/template.html diff --git a/src/rust/long_term_stats/lts_node/src/web/wss/mod.rs b/src/rust/long_term_stats/lts_node/src/web/wss/mod.rs index 89ef41d4..7e320d4c 100644 --- a/src/rust/long_term_stats/lts_node/src/web/wss/mod.rs +++ b/src/rust/long_term_stats/lts_node/src/web/wss/mod.rs @@ -1,7 +1,7 @@ use crate::web::wss::queries::{ 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_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::{ extract::{ @@ -113,6 +113,20 @@ async fn handle_socket(mut socket: WebSocket, cnn: Pool) { 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" => { if let Some(credentials) = &credentials { let _ = send_throughput_for_node( @@ -141,6 +155,20 @@ async fn handle_socket(mut socket: WebSocket, cnn: Pool) { 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" => { if let Some(credentials) = &credentials { let _ = send_rtt_for_node( @@ -206,6 +234,17 @@ async fn handle_socket(mut socket: WebSocket, cnn: Pool) { .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}"); } diff --git a/src/rust/long_term_stats/lts_node/src/web/wss/queries/mod.rs b/src/rust/long_term_stats/lts_node/src/web/wss/queries/mod.rs index 3fa93248..eef72a06 100644 --- a/src/rust/long_term_stats/lts_node/src/web/wss/queries/mod.rs +++ b/src/rust/long_term_stats/lts_node/src/web/wss/queries/mod.rs @@ -7,11 +7,13 @@ mod rtt; mod node_perf; mod search; mod site_heat_map; +mod site_info; pub mod site_tree; pub mod time_period; 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 rtt::{ send_rtt_for_all_nodes, send_rtt_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, send_rtt_for_all_nodes_site }; pub use node_perf::send_perf_for_node; pub use search::omnisearch; -pub use site_heat_map::root_heat_map; \ No newline at end of file +pub use site_heat_map::root_heat_map; +pub use site_info::send_site_info; \ No newline at end of file diff --git a/src/rust/long_term_stats/lts_node/src/web/wss/queries/rtt/mod.rs b/src/rust/long_term_stats/lts_node/src/web/wss/queries/rtt/mod.rs index d36dc339..11aa64d8 100644 --- a/src/rust/long_term_stats/lts_node/src/web/wss/queries/rtt/mod.rs +++ b/src/rust/long_term_stats/lts_node/src/web/wss/queries/rtt/mod.rs @@ -3,7 +3,7 @@ use futures::future::join_all; use influxdb2::{Client, models::Query}; use pgdb::sqlx::{Pool, Postgres}; 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; mod rtt_row; @@ -26,6 +26,23 @@ pub async fn send_rtt_for_all_nodes(cnn: Pool, socket: &mut WebSocket, Ok(()) } +pub async fn send_rtt_for_all_nodes_site(cnn: Pool, 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, 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 nodes = vec![node]; @@ -61,6 +78,24 @@ pub async fn get_rtt_for_all_nodes(cnn: Pool, key: &str, period: Influ all_nodes } +pub async fn get_rtt_for_all_nodes_site(cnn: Pool, key: &str, site_id: &str, period: InfluxTimePeriod) -> anyhow::Result> { + 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> = join_all(futures).await + .into_iter().collect(); + all_nodes +} + pub async fn get_rtt_for_node( cnn: Pool, key: &str, @@ -116,3 +151,62 @@ pub async fn get_rtt_for_node( } Err(anyhow::Error::msg("Unable to query influx")) } + +pub async fn get_rtt_for_node_site( + cnn: Pool, + key: &str, + node_id: String, + node_name: String, + site_id: String, + period: InfluxTimePeriod, +) -> anyhow::Result { + 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::(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")) +} diff --git a/src/rust/long_term_stats/lts_node/src/web/wss/queries/rtt/rtt_row.rs b/src/rust/long_term_stats/lts_node/src/web/wss/queries/rtt/rtt_row.rs index 6f3de88b..aebb06dd 100644 --- a/src/rust/long_term_stats/lts_node/src/web/wss/queries/rtt/rtt_row.rs +++ b/src/rust/long_term_stats/lts_node/src/web/wss/queries/rtt/rtt_row.rs @@ -20,4 +20,25 @@ impl Default for RttRow { time: DateTime::::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, +} + +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::::MIN_UTC.into(), + } + } } \ No newline at end of file diff --git a/src/rust/long_term_stats/lts_node/src/web/wss/queries/site_info.rs b/src/rust/long_term_stats/lts_node/src/web/wss/queries/site_info.rs new file mode 100644 index 00000000..72bc1160 --- /dev/null +++ b/src/rust/long_term_stats/lts_node/src/web/wss/queries/site_info.rs @@ -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, 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); + } + } +} \ No newline at end of file diff --git a/src/rust/long_term_stats/lts_node/src/web/wss/queries/throughput/mod.rs b/src/rust/long_term_stats/lts_node/src/web/wss/queries/throughput/mod.rs index fcddb684..b9c0a180 100644 --- a/src/rust/long_term_stats/lts_node/src/web/wss/queries/throughput/mod.rs +++ b/src/rust/long_term_stats/lts_node/src/web/wss/queries/throughput/mod.rs @@ -3,7 +3,7 @@ use futures::future::join_all; use influxdb2::{Client, models::Query}; use pgdb::sqlx::{Pool, Postgres}; 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; mod throughput_host; @@ -18,6 +18,15 @@ pub async fn send_throughput_for_all_nodes(cnn: Pool, socket: &mut Web Ok(()) } +pub async fn send_throughput_for_all_nodes_by_site(cnn: Pool, 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, 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?; @@ -44,6 +53,24 @@ pub async fn get_throughput_for_all_nodes(cnn: Pool, key: &str, period all_nodes } +pub async fn get_throughput_for_all_nodes_by_site(cnn: Pool, key: &str, period: InfluxTimePeriod, site_name: &str) -> anyhow::Result> { + 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> = join_all(futures).await + .into_iter().collect(); + all_nodes +} + pub async fn get_throughput_for_node( cnn: Pool, key: &str, @@ -111,3 +138,75 @@ pub async fn get_throughput_for_node( } Err(anyhow::Error::msg("Unable to query influx")) } + +pub async fn get_throughput_for_node_by_site( + cnn: Pool, + key: &str, + node_id: String, + node_name: String, + site_name: String, + period: InfluxTimePeriod, +) -> anyhow::Result { + 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::(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")) +} diff --git a/src/rust/long_term_stats/lts_node/src/web/wss/queries/throughput/throughput_row.rs b/src/rust/long_term_stats/lts_node/src/web/wss/queries/throughput/throughput_row.rs index e60d9bca..f60ed563 100644 --- a/src/rust/long_term_stats/lts_node/src/web/wss/queries/throughput/throughput_row.rs +++ b/src/rust/long_term_stats/lts_node/src/web/wss/queries/throughput/throughput_row.rs @@ -22,4 +22,27 @@ impl Default for ThroughputRow { time: DateTime::::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, +} + +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::::MIN_UTC.into(), + } + } } \ No newline at end of file diff --git a/src/rust/long_term_stats/pgdb/src/tree.rs b/src/rust/long_term_stats/pgdb/src/tree.rs index 77186937..0f83ba50 100644 --- a/src/rust/long_term_stats/pgdb/src/tree.rs +++ b/src/rust/long_term_stats/pgdb/src/tree.rs @@ -25,4 +25,17 @@ pub async fn get_site_tree( .fetch_all(&cnn) .await .map_err(|e| StatsHostError::DatabaseError(e.to_string())) -} \ No newline at end of file +} + +pub async fn get_site_info( + cnn: Pool, + key: &str, + site_name: &str, +) -> Result { + 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())) +} diff --git a/src/rust/long_term_stats/site_build/src/bus.ts b/src/rust/long_term_stats/site_build/src/bus.ts index 9584f37d..a18b8124 100644 --- a/src/rust/long_term_stats/site_build/src/bus.ts +++ b/src/rust/long_term_stats/site_build/src/bus.ts @@ -89,6 +89,16 @@ export class Bus { 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() { this.ws.send("{ \"msg\": \"rttChart\", \"period\": \"" + window.graphPeriod + "\" }"); } @@ -104,6 +114,16 @@ export class Bus { 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) { let request = { msg: "nodePerf", @@ -131,6 +151,15 @@ export class Bus { requestTree(parent: string) { 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) { diff --git a/src/rust/long_term_stats/site_build/src/components/rtt_histo_site.ts b/src/rust/long_term_stats/site_build/src/components/rtt_histo_site.ts new file mode 100644 index 00000000..80e81104 --- /dev/null +++ b/src/rust/long_term_stats/site_build/src/components/rtt_histo_site.ts @@ -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( + (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; + } + } + } +} \ No newline at end of file diff --git a/src/rust/long_term_stats/site_build/src/components/rtt_site.ts b/src/rust/long_term_stats/site_build/src/components/rtt_site.ts new file mode 100644 index 00000000..1c5e6dfe --- /dev/null +++ b/src/rust/long_term_stats/site_build/src/components/rtt_site.ts @@ -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( + (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; + } + } + } +} \ No newline at end of file diff --git a/src/rust/long_term_stats/site_build/src/components/site_info.ts b/src/rust/long_term_stats/site_build/src/components/site_info.ts new file mode 100644 index 00000000..2e222699 --- /dev/null +++ b/src/rust/long_term_stats/site_build/src/components/site_info.ts @@ -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 += ""; + html += ""; + html += ""; + html += ""; + html += "
Max:" + scaleNumber(event.data.max_down * mbps_to_bps) + " / " + scaleNumber(event.data.max_up * mbps_to_bps) + "
Current:" + scaleNumber(event.data.current_down) + " / " + scaleNumber(event.data.current_up) + "
Current RTT:" + event.data.current_rtt / 100.0 + "
"; + div.innerHTML = html; + } + } +} \ No newline at end of file diff --git a/src/rust/long_term_stats/site_build/src/components/throughput_site.ts b/src/rust/long_term_stats/site_build/src/components/throughput_site.ts new file mode 100644 index 00000000..11b0a79d --- /dev/null +++ b/src/rust/long_term_stats/site_build/src/components/throughput_site.ts @@ -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( + (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; + } + } + } +} \ No newline at end of file diff --git a/src/rust/long_term_stats/site_build/src/helpers.ts b/src/rust/long_term_stats/site_build/src/helpers.ts index ffe645ea..1e880f8d 100644 --- a/src/rust/long_term_stats/site_build/src/helpers.ts +++ b/src/rust/long_term_stats/site_build/src/helpers.ts @@ -37,7 +37,7 @@ export function usageColor(percent: number): string { return "#aaffaa"; } -export function rttColor(n) { +export function rttColor(n: number): string { if (n <= 100) { return "#aaffaa"; } else if (n <= 150) { @@ -45,4 +45,13 @@ export function rttColor(n) { } else { 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; + } } \ No newline at end of file diff --git a/src/rust/long_term_stats/site_build/src/router.ts b/src/rust/long_term_stats/site_build/src/router.ts index 20f12787..c1f1a008 100644 --- a/src/rust/long_term_stats/site_build/src/router.ts +++ b/src/rust/long_term_stats/site_build/src/router.ts @@ -4,6 +4,7 @@ import { LoginPage } from './login/login'; import { Page } from './page'; import { ShaperNodePage } from './shapernode/shapernode'; import { ShaperNodesPage } from './shapernodes/shapernodes'; +import { SitePage } from './site/site'; import { SiteTreePage } from './site_tree/site_tree'; export class SiteRouter { @@ -70,6 +71,11 @@ export class SiteRouter { this.curentPage = new SiteTreePage(); break; } + case "site": { + this.currentAnchor = "site:" + split[1]; + this.curentPage = new SitePage(split[1]); + break; + } default: { alert("I don't know how to go to: " + split[0].toLowerCase()); this.goto("dashboard"); diff --git a/src/rust/long_term_stats/site_build/src/site/site.ts b/src/rust/long_term_stats/site_build/src/site/site.ts new file mode 100644 index 00000000..924b0539 --- /dev/null +++ b/src/rust/long_term_stats/site_build/src/site/site.ts @@ -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); + }); + } + } +} diff --git a/src/rust/long_term_stats/site_build/src/site/template.html b/src/rust/long_term_stats/site_build/src/site/template.html new file mode 100644 index 00000000..84450e4c --- /dev/null +++ b/src/rust/long_term_stats/site_build/src/site/template.html @@ -0,0 +1,51 @@ +
+
+
+

Site Name

+
+
+
+
+
+
+ Details +
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
diff --git a/src/rust/long_term_stats/site_build/src/site_tree/site_tree.ts b/src/rust/long_term_stats/site_build/src/site_tree/site_tree.ts index d983dba6..d3fee155 100644 --- a/src/rust/long_term_stats/site_build/src/site_tree/site_tree.ts +++ b/src/rust/long_term_stats/site_build/src/site_tree/site_tree.ts @@ -3,12 +3,13 @@ import { Page } from '../page' import { MenuPage } from '../menu/menu'; import { Component } from '../components/component'; import mermaid from 'mermaid'; -import { rttColor, scaleNumber, siteIcon, usageColor } from '../helpers'; +import { makeUrl, rttColor, scaleNumber, siteIcon, usageColor } from '../helpers'; export class SiteTreePage implements Page { menu: MenuPage; components: Component[]; selectedNode: string; + count: number = 0; constructor() { this.menu = new MenuPage("sitetreeDash"); @@ -33,6 +34,10 @@ export class SiteTreePage implements Page { this.components.forEach(component => { component.ontick(); }); + if (this.count % 10 == 0 && this.selectedNode != "") { + fetchTree(this.selectedNode); + } + this.count++; } onmessage(event: any) { @@ -90,7 +95,7 @@ class TreeItem { current_rtt: number; } -const mbps_to_bps = 1000000; +export const mbps_to_bps = 1000000; function buildTree(data: TreeItem[]) { data.sort((a,b) => { @@ -110,7 +115,8 @@ function buildTree(data: TreeItem[]) { let usageBg = usageColor(peak); let rttBg = rttColor(data[i].current_rtt / 100); 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) + " " + data[i].site_name + ""; html += "" + scaleNumber(data[i].max_down * mbps_to_bps) + " / " + scaleNumber(data[i].max_up * mbps_to_bps) + ""; html += "" + scaleNumber(data[i].current_down) + " / " + scaleNumber(data[i].current_up) + ""; html += "" + up.toFixed(1) + "% / " + down.toFixed(1) + "%"; @@ -149,7 +155,8 @@ function treeChildren(data: TreeItem[], parent: number, depth: number) : string for (let j=0; j" + data[i].site_name + ""; html += "" + scaleNumber(data[i].max_down * mbps_to_bps) + " / " + scaleNumber(data[i].max_up * mbps_to_bps) + ""; html += "" + scaleNumber(data[i].current_down) + " / " + scaleNumber(data[i].current_up) + ""; html += "" + up.toFixed(1) + "% / " + down.toFixed(1) + "%";