diff --git a/mycelium-ui/Cargo.lock b/mycelium-ui/Cargo.lock index 6c7994b..3a29b68 100644 --- a/mycelium-ui/Cargo.lock +++ b/mycelium-ui/Cargo.lock @@ -1146,6 +1146,15 @@ dependencies = [ "wry", ] +[[package]] +name = "dioxus-free-icons" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfbba8b2089b185e4cebc394fb36353c7269a3230b542d97d3d192ccd864d48b" +dependencies = [ + "dioxus 0.5.1", +] + [[package]] name = "dioxus-fullstack" version = "0.5.2" @@ -3030,6 +3039,7 @@ name = "mycelium-ui" version = "0.1.0" dependencies = [ "dioxus 0.5.1", + "dioxus-free-icons", "dioxus-logger", "dioxus-sortable", "manganis", diff --git a/mycelium-ui/Cargo.toml b/mycelium-ui/Cargo.toml index 6155d1c..3f262d0 100644 --- a/mycelium-ui/Cargo.toml +++ b/mycelium-ui/Cargo.toml @@ -19,3 +19,8 @@ reqwest = { version = "0.12.5", features = ["json"] } serde_json = "1.0.120" dioxus-sortable = "0.1.2" manganis = "0.2.2" +dioxus-free-icons = { version = "0.8.6", features = [ + "font-awesome-solid", + "font-awesome-brands", + "font-awesome-regular", +] } diff --git a/mycelium-ui/Dioxus.toml b/mycelium-ui/Dioxus.toml index 0eb1d3d..cf5849e 100644 --- a/mycelium-ui/Dioxus.toml +++ b/mycelium-ui/Dioxus.toml @@ -34,7 +34,10 @@ index_on_404 = true # CSS style file -style = ["./assets/styles.css"] +style = [ + "./assets/styles.css", + "https://fonts.googleapis.com/css2?family=Lato:wght@300;400;700&display=swap", +] # Javascript code file script = [] diff --git a/mycelium-ui/assets/styles.css b/mycelium-ui/assets/styles.css index c42fe64..57947d1 100644 --- a/mycelium-ui/assets/styles.css +++ b/mycelium-ui/assets/styles.css @@ -1,70 +1,187 @@ +/* V2 */ + +/* @import url('https://fonts.googleapis.com/css2?family=Lato:wght@300;400;700&display=swap'); */ +/**/ +/* :root { */ +/* --primary-color: #3498db; */ +/* --secondary-color: #2c3e50; */ +/* --background-color: #ecf0f1; */ +/* --text-color: #34495e; */ +/* --border-color: #bdc3c7; */ +/* } */ +/**/ +/* * { */ +/* box-sizing: border-box; */ +/* margin: 0; */ +/* padding: 0; */ +/* } */ +/**/ +/* body { */ +/* font-family: 'Lato', sans-serif; */ +/* background-color: var(--background-color); */ +/* color: var(--text-color); */ +/* } */ +/**/ /* .app-container { */ -/* display: flex; */ -/* flex-direction: column; */ -/* height: 100vh; */ +/* display: flex; */ +/* flex-direction: column; */ +/* height: 100vh; */ /* } */ /**/ /* header { */ -/* background-color: #f0f0f0; */ -/* padding: 1rem; */ +/* background-color: var(--primary-color); */ +/* color: white; */ +/* padding: 1rem; */ +/* display: flex; */ +/* justify-content: space-between; */ +/* align-items: center; */ +/* position: fixed; */ +/* width: 100%; */ +/* z-index: 1000; */ +/* } */ +/**/ +/* header h1 { */ +/* font-size: 1.5rem; */ +/* font-weight: 700; */ +/* } */ +/**/ +/* .node-info { */ +/* font-size: 0.9rem; */ /* } */ /**/ /* .content-container { */ -/* display: flex; */ -/* flex: 1; */ -/* overflow: hidden; */ +/* display: flex; */ +/* padding-top: 60px; */ +/* height: calc(100vh - 60px); */ /* } */ /**/ /* .sidebar { */ -/* width: 200px; */ -/* background-color: #e0e0e0; */ -/* padding: 1rem; */ -/* overflow-y: auto; */ +/* background-color: var(--secondary-color); */ +/* color: white; */ +/* width: 250px; */ +/* padding: 1rem; */ +/* transition: transform 0.3s ease-in-out; */ +/* } */ +/**/ +/* .sidebar.collapsed { */ +/* transform: translateX(-250px); */ +/* } */ +/**/ +/* .sidebar ul { */ +/* list-style-type: none; */ +/* } */ +/**/ +/* .sidebar li { */ +/* margin-bottom: 1rem; */ +/* } */ +/**/ +/* .sidebar a { */ +/* color: white; */ +/* text-decoration: none; */ +/* font-size: 1.1rem; */ +/* transition: color 0.3s ease; */ +/* } */ +/**/ +/* .sidebar a:hover { */ +/* color: var(--primary-color); */ /* } */ /**/ /* .main-content { */ -/* flex: 1; */ -/* padding: 1rem; */ -/* overflow-y: auto; */ +/* flex: 1; */ +/* padding: 2rem; */ +/* overflow-y: auto; */ +/* } */ +/**/ +/* .toggle-sidebar { */ +/* background-color: var(--secondary-color); */ +/* color: white; */ +/* border: none; */ +/* padding: 0.5rem 1rem; */ +/* cursor: pointer; */ +/* font-size: 1rem; */ +/* transition: background-color 0.3s ease; */ +/* } */ +/**/ +/* .toggle-sidebar:hover { */ +/* background-color: var(--primary-color); */ /* } */ /**/ /* table { */ -/* width: 100%; */ -/* border-collapse: collapse; */ +/* width: 100%; */ +/* border-collapse: collapse; */ +/* margin-top: 1rem; */ +/* background-color: white; */ +/* box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); */ /* } */ /**/ /* th, td { */ -/* border: 1px solid #ddd; */ -/* padding: 8px; */ -/* text-align: left; */ +/* padding: 1rem; */ +/* text-align: left; */ +/* border-bottom: 1px solid var(--border-color); */ /* } */ /**/ /* th { */ -/* background-color: #f2f2f2; */ -/* cursor: pointer; */ +/* background-color: var(--primary-color); */ +/* color: white; */ +/* font-weight: 700; */ +/* cursor: pointer; */ +/* transition: background-color 0.3s ease; */ /* } */ /**/ /* th:hover { */ -/* background-color: #e2e2e2; */ +/* background-color: #2980b9; */ /* } */ /**/ /* .pagination { */ -/* margin-top: 1rem; */ -/* display: flex; */ -/* justify-content: center; */ -/* align-items: center; */ +/* display: flex; */ +/* justify-content: center; */ +/* align-items: center; */ +/* margin-top: 1rem; */ /* } */ /**/ /* .pagination button { */ -/* margin: 0 0.5rem; */ +/* background-color: var(--primary-color); */ +/* color: white; */ +/* border: none; */ +/* padding: 0.5rem 1rem; */ +/* margin: 0 0.5rem; */ +/* cursor: pointer; */ +/* transition: background-color 0.3s ease; */ +/* } */ +/**/ +/* .pagination button:hover:not(:disabled) { */ +/* background-color: #2980b9; */ +/* } */ +/**/ +/* .pagination button:disabled { */ +/* background-color: var(--border-color); */ +/* cursor: not-allowed; */ /* } */ /**/ /* .pagination span { */ -/* margin: 0 0.5rem; */ +/* margin: 0 0.5rem; */ +/* } */ +/**/ +/* .selected-routes, .fallback-routes { */ +/* margin-top: 2rem; */ +/* } */ +/**/ +/* h2 { */ +/* color: var(--secondary-color); */ +/* margin-bottom: 1rem; */ +/* } */ +/**/ +/* .node-info h3 { */ +/* margin-bottom: 0.5rem; */ +/* font-size: 1.1rem; */ +/* font-weight: 400; */ /* } */ -@import url('https://fonts.googleapis.com/css2?family=Lato:wght@300;400;700&display=swap'); + + +/* @import url('https://fonts.googleapis.com/css2?family=Lato:wght@300;400;700&display=swap'); */ +/* @import url('https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.1/css/all.min.css'); */ :root { --primary-color: #3498db; @@ -89,7 +206,7 @@ body { .app-container { display: flex; flex-direction: column; - height: 100vh; + min-height: 100vh; } header { @@ -111,12 +228,22 @@ header h1 { .node-info { font-size: 0.9rem; + display: flex; + align-items: center; +} + +.node-info span { + margin-right: 1rem; +} + +.node-info .separator { + margin: 0 1rem; } .content-container { display: flex; padding-top: 60px; - height: calc(100vh - 60px); + flex: 1; } .sidebar { @@ -154,6 +281,11 @@ header h1 { flex: 1; padding: 2rem; overflow-y: auto; + transition: margin-left 0.3s ease-in-out; +} + +.main-content.expanded { + margin-left: -250px; } .toggle-sidebar { @@ -163,25 +295,51 @@ header h1 { padding: 0.5rem 1rem; cursor: pointer; font-size: 1rem; - transition: background-color 0.3s ease; + /* transition: background-color 0.3s ease; */ + transition: left 0.3s ease-in-out, transform 0.3 ease-in-out; + position: fixed; + left: 250px; + top: 70px; + z-index: 999; + + display: flex; + align-items: center; + justify-content: center; +} + +.toggle-sidebar.collapsed { + left: 0; + transform: translateX(-250px) rotate(180deg); +} + +.main-content.expanded .toggle-sidebar { + left: 0; } .toggle-sidebar:hover { background-color: var(--primary-color); } +.table-container { + overflow-x: auto; +} + table { width: 100%; border-collapse: collapse; margin-top: 1rem; background-color: white; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + table-layout: fixed; } th, td { padding: 1rem; text-align: left; border-bottom: 1px solid var(--border-color); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } th { @@ -196,6 +354,19 @@ th:hover { background-color: #2980b9; } +/* Routes table column widths */ +.subnet-column { width: 25%; } +.next-hop-column { width: 35%; } +.metric-column { width: 20%; } +.seqno-column { width: 20%; } + +/* Peers table column widths */ +.endpoint-column { width: 30%; } +.type-column { width: 15%; } +.connection-state-column { width: 20%; } +.tx-bytes-column { width: 17.5%; } +.rx-bytes-column { width: 17.5%; } + .pagination { display: flex; justify-content: center; @@ -226,6 +397,10 @@ th:hover { margin: 0 0.5rem; } +.peers-table { + margin-top: 2rem; +} + .selected-routes, .fallback-routes { margin-top: 2rem; } diff --git a/mycelium-ui/src/main.rs b/mycelium-ui/src/main.rs index 2415b7a..143ed1c 100644 --- a/mycelium-ui/src/main.rs +++ b/mycelium-ui/src/main.rs @@ -3,6 +3,8 @@ mod api; use dioxus::prelude::*; +use dioxus_free_icons::icons::fa_solid_icons::{FaChevronLeft, FaChevronRight}; +use dioxus_free_icons::Icon; use mycelium::peer_manager::PeerType; use std::cmp::Ordering; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; @@ -43,14 +45,30 @@ fn Layout() -> Element { Header {} div { class: "content-container", Sidebar { collapsed: sidebar_collapsed } - main { class: "main-content", + main { class: if *sidebar_collapsed.read() { "main-content expanded" } else { "main-content" }, button { - class: "toggle-sidebar", + class: if *sidebar_collapsed.read() { "toggle-sidebar collapsed" } else { "toggle-sidebar" }, onclick: { let sb_collapsed = *sidebar_collapsed.read(); move |_| sidebar_collapsed.set(!sb_collapsed) }, - if *sidebar_collapsed.read() { "Show sidebar" } else { "Hide sidebar" } + // i { + // if *sidebar_collapsed.read() { + // Icon { + // fill: "white", + // icon: FaChevronRight, + // } + // } else { + // Icon { + // fill: "white", + // icon: FaChevronLeft, + // } + // } + // } + Icon { + fill: "white", + icon: FaChevronLeft, + } } Outlet:: {} } @@ -75,7 +93,8 @@ fn Header() -> Element { div { class: "node-info", { match &*fetched_node_info.read_unchecked() { Some(Ok(info)) => rsx! { - span { "Subnet: {info.node_subnet} | " } + span { "Subnet: {info.node_subnet}" } + span { class: "separator", "|" } span { "Public Key: {info.node_pubkey}" } }, Some(Err(_)) => rsx! { span { "Error loading node info" } }, @@ -238,39 +257,41 @@ fn PeersTable(peers: Vec) -> Element { 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 { - onclick: move |_| sort_peers_signal("Connection State".to_string()), - "Connection State {get_sort_indicator(sort_column, sort_direction, \"Connection State\".to_string())}" - } - th { - onclick: move |_| sort_peers_signal("Tx bytes".to_string()), - "Tx bytes {get_sort_indicator(sort_column, sort_direction, \"Tx bytes\".to_string())}" - } - th { - onclick: move |_| sort_peers_signal("Rx bytes".to_string()), - "Rx bytes {get_sort_indicator(sort_column, sort_direction, \"Rx bytes\".to_string())}" + div { class: "table-container", + table { + thead { + tr { + th { class: "endpoint-column", + onclick: move |_| sort_peers_signal("Endpoint".to_string()), + "Endpoint {get_sort_indicator(sort_column, sort_direction, \"Endpoint\".to_string())}" + } + th { class: "type-column", + onclick: move |_| sort_peers_signal("Type".to_string()), + "Type {get_sort_indicator(sort_column, sort_direction, \"Type\".to_string())}" + } + th { class: "connection-state-column", + onclick: move |_| sort_peers_signal("Connection State".to_string()), + "Connection State {get_sort_indicator(sort_column, sort_direction, \"Connection State\".to_string())}" + } + th { class: "tx-bytes-column", + onclick: move |_| sort_peers_signal("Tx bytes".to_string()), + "Tx bytes {get_sort_indicator(sort_column, sort_direction, \"Tx bytes\".to_string())}" + } + th { class: "rx-bytes-column", + onclick: move |_| sort_peers_signal("Rx bytes".to_string()), + "Rx bytes {get_sort_indicator(sort_column, sort_direction, \"Rx bytes\".to_string())}" + } } } - } - 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}" } + tbody { + for peer in current_peers { + tr { + td { class: "endpoint-column", "{peer.endpoint}" } + td { class: "type-column", "{peer.pt}" } + td { class: "connection-state-column", "{peer.connection_state}" } + td { class: "tx-bytes-column", "{peer.tx_bytes}" } + td { class: "rx-bytes-column", "{peer.rx_bytes}" } + } } } } @@ -351,7 +372,7 @@ fn sort_routes(routes: &mut [mycelium_api::Route], column: &str, direction: &Sor fn RoutesTable(routes: Vec, table_name: String) -> Element { let mut current_page = use_signal(|| 0); - let items_per_page = 20; + let items_per_page = 10; let mut sort_column = use_signal(|| "Subnet".to_string()); let mut sort_direction = use_signal(|| SortDirection::Descending); let routes_len = routes.len(); @@ -391,34 +412,36 @@ fn RoutesTable(routes: Vec, table_name: String) -> Element rsx! { div { class: "{table_name.to_lowercase()}-routes", h2 { "{table_name} Routes" } - table { - thead { - tr { - th { - onclick: move |_| sort_routes_signal("Subnet".to_string()), - "Subnet {get_sort_indicator(sort_column, sort_direction, \"Subnet\".to_string())}" - } - th { - onclick: move |_| sort_routes_signal("Next-hop".to_string()), - "Next-hop {get_sort_indicator(sort_column, sort_direction, \"Next-hop\".to_string())}" - } - th { - onclick: move |_| sort_routes_signal("Metric".to_string()), - "Metric {get_sort_indicator(sort_column, sort_direction, \"Metric\".to_string())}" - } - th { - onclick: move |_| sort_routes_signal("Seqno".to_string()), - "Seqno {get_sort_indicator(sort_column, sort_direction, \"Seqno\".to_string())}" + div { class: "table-container", + table { + thead { + tr { + th { class: "subnet-column", + onclick: move |_| sort_routes_signal("Subnet".to_string()), + "Subnet {get_sort_indicator(sort_column, sort_direction, \"Subnet\".to_string())}" + } + th { class: "next-hop-column", + onclick: move |_| sort_routes_signal("Next-hop".to_string()), + "Next-hop {get_sort_indicator(sort_column, sort_direction, \"Next-hop\".to_string())}" + } + th { class: "metric-column", + onclick: move |_| sort_routes_signal("Metric".to_string()), + "Metric {get_sort_indicator(sort_column, sort_direction, \"Metric\".to_string())}" + } + th { class: "seqno_column", + onclick: move |_| sort_routes_signal("Seqno".to_string()), + "Seqno {get_sort_indicator(sort_column, sort_direction, \"Seqno\".to_string())}" + } } } - } - tbody { - for route in current_routes { - tr { - td { "{route.subnet}" } - td { "{route.next_hop}" } - td { "{route.metric}" } - td { "{route.seqno}" } + tbody { + for route in current_routes { + tr { + td { class: "subnet-column", "{route.subnet}" } + td { class: "next-hop-column", "{route.next_hop}" } + td { class: "metric-column", "{route.metric}" } + td { class: "seqno-column", "{route.seqno}" } + } } } }