UI changes

This commit is contained in:
Maxime Van Hees
2024-07-23 14:36:22 +02:00
committed by Lee Smet
parent 83e5c6a55b
commit 2345b73e19
5 changed files with 312 additions and 96 deletions

10
mycelium-ui/Cargo.lock generated
View File

@@ -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",

View File

@@ -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",
] }

View File

@@ -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 = []

View File

@@ -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;
}

View File

@@ -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::<Route> {}
}
@@ -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<mycelium::peer_manager::PeerStats>) -> 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<mycelium_api::Route>, 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<mycelium_api::Route>, 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}" }
}
}
}
}