diff --git a/mycelium-ui/src/api.rs b/mycelium-ui/src/api.rs new file mode 100644 index 0000000..8652587 --- /dev/null +++ b/mycelium-ui/src/api.rs @@ -0,0 +1,51 @@ +use std::net::SocketAddr; + +pub async fn get_peers( + server_addr: SocketAddr, +) -> Result, reqwest::Error> { + let request_url = format!("http://{server_addr}/api/v1/admin/peers"); + match reqwest::get(&request_url).await { + Err(e) => Err(e), + Ok(resp) => match resp.json::>().await { + Err(e) => Err(e), + Ok(peers) => Ok(peers), + }, + } +} + +pub async fn get_selected_routes( + server_addr: SocketAddr, +) -> Result, reqwest::Error> { + let request_url = format!("http://{server_addr}/api/v1/admin/routes/selected"); + match reqwest::get(&request_url).await { + Err(e) => Err(e), + Ok(resp) => match resp.json::>().await { + Err(e) => Err(e), + Ok(selected_routes) => Ok(selected_routes), + }, + } +} + +pub async fn get_fallback_routes( + server_addr: SocketAddr, +) -> Result, reqwest::Error> { + let request_url = format!("http://{server_addr}/api/v1/admin/routes/fallback"); + match reqwest::get(&request_url).await { + Err(e) => Err(e), + Ok(resp) => match resp.json::>().await { + Err(e) => Err(e), + Ok(selected_routes) => Ok(selected_routes), + }, + } +} + +pub async fn get_node_info(server_addr: SocketAddr) -> Result { + let request_url = format!("http://{server_addr}/api/v1/admin"); + match reqwest::get(&request_url).await { + Err(e) => Err(e), + Ok(resp) => match resp.json::().await { + Err(e) => Err(e), + Ok(node_info) => Ok(node_info), + }, + } +} diff --git a/mycelium-ui/src/main.rs b/mycelium-ui/src/main.rs index 7601851..00515ad 100644 --- a/mycelium-ui/src/main.rs +++ b/mycelium-ui/src/main.rs @@ -1,5 +1,7 @@ #![allow(non_snake_case)] +mod api; + use dioxus::prelude::*; use mycelium::peer_manager::PeerType; use std::cmp::Ordering; @@ -24,56 +26,6 @@ enum Route { PageNotFound { route: Vec }, } -pub async fn get_peers( - server_addr: SocketAddr, -) -> Result, reqwest::Error> { - let request_url = format!("http://{server_addr}/api/v1/admin/peers"); - match reqwest::get(&request_url).await { - Err(e) => Err(e), - Ok(resp) => match resp.json::>().await { - Err(e) => Err(e), - Ok(peers) => Ok(peers), - }, - } -} - -pub async fn get_selected_routes( - server_addr: SocketAddr, -) -> Result, reqwest::Error> { - let request_url = format!("http://{server_addr}/api/v1/admin/routes/selected"); - match reqwest::get(&request_url).await { - Err(e) => Err(e), - Ok(resp) => match resp.json::>().await { - Err(e) => Err(e), - Ok(selected_routes) => Ok(selected_routes), - }, - } -} - -pub async fn get_fallback_routes( - server_addr: SocketAddr, -) -> Result, reqwest::Error> { - let request_url = format!("http://{server_addr}/api/v1/admin/routes/fallback"); - match reqwest::get(&request_url).await { - Err(e) => Err(e), - Ok(resp) => match resp.json::>().await { - Err(e) => Err(e), - Ok(selected_routes) => Ok(selected_routes), - }, - } -} - -pub async fn get_node_info(server_addr: SocketAddr) -> Result { - let request_url = format!("http://{server_addr}/api/v1/admin"); - match reqwest::get(&request_url).await { - Err(e) => Err(e), - Ok(resp) => match resp.json::().await { - Err(e) => Err(e), - Ok(node_info) => Ok(node_info), - }, - } -} - fn main() { // Init logger dioxus_logger::init(Level::INFO).expect("failed to init logger"); @@ -115,7 +67,7 @@ fn Sidebar() -> Element { #[component] fn Home() -> Element { - let fetched_node_info = use_resource(move || get_node_info(SERVER_ADDR)); + let fetched_node_info = use_resource(move || api::get_node_info(SERVER_ADDR)); match &*fetched_node_info.read_unchecked() { Some(Ok(info)) => rsx! { div { class: "node-info", @@ -131,12 +83,11 @@ fn Home() -> Element { #[component] fn Peers() -> Element { - rsx! { - div { class: "peers", - h2 { "Peers" } - { PeersTable() } - } - Outlet:: {} + let fetched_peers = use_resource(move || api::get_peers(SERVER_ADDR)); + match &*fetched_peers.read_unchecked() { + Some(Ok(peers)) => rsx! { {PeersTable(peers.clone()) } }, + Some(Err(e)) => rsx! { div { "An error has occurred while fetching the peers: {e}" } }, + None => rsx! { div { "Loading peers..." } }, } } @@ -144,11 +95,14 @@ fn Peers() -> Element { fn Routes() -> Element { rsx! { SelectedRoutesTable {} - div { class: "fallback-routes", - h2 { "Fallback Routes" } - { FallbackRoutesTable() } - } - // FallbackRoutesTable {} + FallbackRoutesTable {} + } +} + +#[component] +fn PageNotFound(route: Vec) -> Element { + rsx! { + p { "Page not found"} } } @@ -187,21 +141,41 @@ enum SortDirection { Descending, } -#[component] -fn PeersTable() -> Element { - let fetched_peers = use_resource(move || get_peers(SERVER_ADDR)); +fn sort_peers( + peers: &mut [mycelium::peer_manager::PeerStats], + column: &str, + direction: &SortDirection, +) { + peers.sort_by(|a, b| { + let cmp = match column { + //"Endpoint" => a.endpoint.cmp(&b.endpoint), + "Type" => PeerTypeWrapper(a.pt.clone()).cmp(&PeerTypeWrapper(b.pt.clone())), + _ => Ordering::Equal, + }; + match direction { + SortDirection::Ascending => cmp, + SortDirection::Descending => cmp.reverse(), + } + }); +} + +fn PeersTable(peers: Vec) -> Element { let mut current_page = use_signal(|| 0); let items_per_page = 20; - - let mut change_page = move |delta: i32| { - current_page.set(current_page + delta as usize); - }; - - // Sorting let mut sort_column = use_signal(|| "Type".to_string()); let mut sort_direction = use_signal(|| SortDirection::Ascending); + let peers_len = peers.len(); - let sort_peers = move |column: String| { + let mut change_page = move |delta: i32| { + let cur_page = *current_page.read() as i32; + current_page.set( + (cur_page + delta) + .max(0) + .min((peers_len - 1) as i32 / items_per_page as i32) as usize, + ); + }; + + let mut sort_peers_signal = move |column: String| { if column == *sort_column.read() { let new_sort_direction = match *sort_direction.read() { SortDirection::Ascending => SortDirection::Descending, @@ -210,55 +184,71 @@ fn PeersTable() -> Element { sort_direction.set(new_sort_direction); } else { sort_column.set(column); - sort_direction.set(SortDirection::Ascending); + sort_direction.set(SortDirection::Descending); } }; - match &*fetched_peers.read_unchecked() { - Some(Ok(peers)) => { - // Sort peers based on the current sort column and sort direction - let mut sorted_peers = peers.clone(); - sorted_peers.sort_by(|a, b| { - let cmp = match sort_column.read().as_str() { - // "Endpoint" => a.endpoint.cmp(&b.endpoint), - "Type" => PeerTypeWrapper(a.pt.clone()).cmp(&PeerTypeWrapper(b.pt.clone())), - _ => std::cmp::Ordering::Equal, - }; - match *sort_direction.read() { - SortDirection::Descending => cmp.reverse(), - SortDirection::Ascending => cmp, - } - }); + let sorted_peers = use_memo(move || { + let mut sorted = peers.clone(); + sort_peers(&mut sorted, &sort_column.read(), &sort_direction.read()); + sorted + }); - let start = current_page * items_per_page; - let end = (start + items_per_page).min(peers.len()); - let current_peers = &sorted_peers[start..end]; - rsx! { - {create_peer_table(current_peers, sort_column, sort_direction, sort_peers)} - div { class: "pagination", - button { - disabled: *current_page.read() == 0, - onclick: move |_| change_page(-1), - "Previous" - } - span { "Page {current_page + 1}"} - button { - disabled: (current_page + 1) * items_per_page >= peers.len(), - onclick: move |_| change_page(1), - "Next" + let start = current_page * items_per_page; + let end = (start + items_per_page).min(peers_len); + let current_peers = &sorted_peers.read()[start..end]; + + rsx! { + div { class: "peers-table", + h2 { "Peers" } + table { + thead { + tr { + th { + onclick: move |_| sort_peers_signal("Endpoint".to_string()), + "Endpoint {get_sort_indicator(sort_column, sort_direction, \"Endpoint\".to_string())}" + } + th { + onclick: move |_| sort_peers_signal("Type".to_string()), + "Type {get_sort_indicator(sort_column, sort_direction, \"Type\".to_string())}" + } + th { "Connection State" } + th { "Tx bytes" } + th { "Rx bytes" } } } - + tbody { + for peer in current_peers { + tr { + td { "{peer.endpoint}" } + td { "{peer.pt}" } + td { "{peer.connection_state}" } + td { "{peer.tx_bytes}" } + td { "{peer.rx_bytes}" } + } + } + } + } + div { class: "pagination", + button { + disabled: *current_page.read() == 0, + onclick: move |_| change_page(-1), + "Previous" + } + span { "Page {current_page + 1}" } + button { + disabled: (current_page + 1) * items_per_page >= peers_len, + onclick: move |_| change_page(1), + "Next" + } } } - Some(Err(e)) => rsx! { div { "An error has occurred while fetching peers: {e}" }}, - None => rsx! { div { "Loading peers..." }}, } } #[component] fn SelectedRoutesTable() -> Element { - let fetched_selected_routes = use_resource(move || get_selected_routes(SERVER_ADDR)); + let fetched_selected_routes = use_resource(move || api::get_selected_routes(SERVER_ADDR)); match &*fetched_selected_routes.read_unchecked() { Some(Ok(routes)) => { @@ -271,7 +261,7 @@ fn SelectedRoutesTable() -> Element { #[component] fn FallbackRoutesTable() -> Element { - let fetched_fallback_routes = use_resource(move || get_selected_routes(SERVER_ADDR)); + let fetched_fallback_routes = use_resource(move || api::get_fallback_routes(SERVER_ADDR)); match &*fetched_fallback_routes.read_unchecked() { Some(Ok(routes)) => { @@ -282,13 +272,6 @@ fn FallbackRoutesTable() -> Element { } } -#[component] -fn PageNotFound(route: Vec) -> Element { - rsx! { - p { "Page not found"} - } -} - fn get_sort_indicator( sort_column: Signal, sort_direction: Signal, @@ -304,53 +287,6 @@ fn get_sort_indicator( } } -fn create_peer_table( - peers: &[mycelium::peer_manager::PeerStats], - sort_column: Signal, - sort_direction: Signal, - mut sort_peers: impl FnMut(String) + Clone + 'static, -) -> Element { - rsx! { - table { - thead { - tr { - // th { "Endpoint" } - th { - onclick: { - let mut sort_peers = sort_peers.clone(); - move |_| { - sort_peers("Endpoint".to_string()); - } - }, - "Endpoint {get_sort_indicator(sort_column, sort_direction, \"Endpoint\".to_string())}" - } - th { - onclick: move |_| { - sort_peers("Type".to_string()); - }, - "Type {get_sort_indicator(sort_column, sort_direction, \"Type\".to_string())}" - } - // th { "Type" } - th { "Connection State" } - th { "Tx bytes" } - th { "Rx bytes" } - } - } - tbody { - for peer in peers { - tr { - td { "{peer.endpoint}" } - td { "{peer.pt}" } - td { "{peer.connection_state}" } - td { "{peer.tx_bytes}" } - td { "{peer.rx_bytes}" } - } - } - } - } - } -} - fn sort_routes(routes: &mut [mycelium_api::Route], column: &str, direction: &SortDirection) { routes.sort_by(|a, b| { let cmp = match column { diff --git a/mycelium/src/peer_manager.rs b/mycelium/src/peer_manager.rs index 898e64d..bc1ee71 100644 --- a/mycelium/src/peer_manager.rs +++ b/mycelium/src/peer_manager.rs @@ -112,7 +112,7 @@ pub enum ConnectionState { } /// Identification and information/statistics for a specific [`Peer`] -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] #[serde(rename_all = "camelCase")] pub struct PeerStats { /// The endpoint of the [`Peer`].