mirror of
https://github.com/RetroShare/RSNewWebUI.git
synced 2026-09-12 19:50:04 +05:00
Improved Stats page look & feel
Added Bandwidth page
This commit is contained in:
parent
add4a3ab80
commit
f99f3c28c9
@ -1,40 +1,706 @@
|
||||
.statistics-page { max-width: 1500px; margin: 0 auto; padding: 1.5rem; color: #1e293b; }
|
||||
.statistics-header { display: flex; align-items: center; justify-content: space-between; gap: 1rem; margin-bottom: 1.25rem; }
|
||||
.statistics-header h1 { display: flex; align-items: center; gap: .65rem; margin: 0 0 .25rem; font-size: 1.7rem; }
|
||||
.statistics-header h1 i { color: #0788cb; }
|
||||
.statistics-header p, .traffic-panel__heading p { margin: 0; color: #64748b; }
|
||||
.statistics-header button { display: inline-flex; align-items: center; gap: .45rem; white-space: nowrap; }
|
||||
.statistics-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1.25rem; align-items: start; }
|
||||
.traffic-panel { min-width: 0; padding: 1.25rem; border: 1px solid #dbe4ec; border-radius: .75rem; background: #fff; box-shadow: 0 3px 14px rgba(15, 23, 42, .06); }
|
||||
.traffic-panel__heading h2 { margin: 0 0 .2rem; font-size: 1.2rem; }
|
||||
.traffic-pie { display: grid; grid-template-columns: minmax(13rem, 45%) 1fr; align-items: center; gap: 1.25rem; margin: 1.25rem 0; }
|
||||
.traffic-pie svg { width: 100%; max-height: 18rem; transform: rotate(-90deg); }
|
||||
.traffic-pie__track, .traffic-pie__segment { fill: none; stroke-width: 18; }
|
||||
.traffic-pie__track { stroke: #e8eef3; }
|
||||
.traffic-pie__segment { transition: stroke-dasharray .25s ease; }
|
||||
.traffic-pie__value, .traffic-pie__caption { transform: rotate(90deg); transform-origin: 60px 60px; fill: #1e293b; }
|
||||
.traffic-pie__value { font-size: 9px; font-weight: 700; }
|
||||
.traffic-pie__caption { font-size: 5px; fill: #64748b; }
|
||||
.traffic-legend { min-width: 0; max-height: 16rem; overflow-y: auto; }
|
||||
.traffic-legend__item { display: grid; grid-template-columns: .7rem minmax(0, 1fr) auto; align-items: center; gap: .5rem; padding: .35rem 0; font-size: .84rem; }
|
||||
.traffic-legend__swatch { width: .7rem; height: .7rem; border-radius: 50%; }
|
||||
.traffic-legend__name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.traffic-table-wrap { overflow-x: auto; }
|
||||
.traffic-table { width: 100%; font-size: .85rem; }
|
||||
.traffic-table th, .traffic-table td { padding: .55rem .45rem; text-align: right; border-bottom: 1px solid #e8eef3; white-space: nowrap; }
|
||||
.traffic-table th:first-child, .traffic-table td:first-child { text-align: left; white-space: normal; }
|
||||
.traffic-empty { display: grid; place-items: center; gap: .5rem; min-height: 18rem; color: #94a3b8; text-align: center; }
|
||||
.traffic-empty i { font-size: 3rem; }
|
||||
.statistics-error { display: flex; gap: .5rem; align-items: center; margin-bottom: 1rem; padding: .8rem 1rem; color: #991b1b; background: #fef2f2; border: 1px solid #fecaca; border-radius: .5rem; }
|
||||
.statistics-note { margin: 1rem 0 0; color: #64748b; font-size: .8rem; text-align: right; }
|
||||
@media (max-width: 1050px) { .statistics-grid { grid-template-columns: 1fr; } }
|
||||
@media (max-width: 600px) {
|
||||
.statistics-page { padding: .75rem; }
|
||||
.statistics-header { align-items: flex-start; }
|
||||
.statistics-header h1 { font-size: 1.35rem; }
|
||||
.statistics-header button { padding: .5rem; font-size: 0; }
|
||||
.statistics-header button i { font-size: 1rem; }
|
||||
.traffic-panel { padding: .8rem; }
|
||||
.traffic-pie { grid-template-columns: 1fr; }
|
||||
.traffic-pie svg { max-height: 14rem; }
|
||||
// ---------------------------------------------------------------------------
|
||||
// Statistics page — two-pane master-detail layout
|
||||
// Mirrors the pattern used in Chat, Network, and People pages.
|
||||
// ---------------------------------------------------------------------------
|
||||
@use '../abstracts' as *;
|
||||
|
||||
// ── Container: full-height flex row ──
|
||||
.statistics-container {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
background-color: #f1f5f9;
|
||||
}
|
||||
|
||||
// ── Left pane: sidebar with header card + navigation ──
|
||||
.statistics-left-pane {
|
||||
width: 280px;
|
||||
min-width: 260px;
|
||||
max-width: 320px;
|
||||
border-right: 1px solid #cbd5e1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #ffffff;
|
||||
box-shadow: 2px 0 5px rgba(0, 0, 0, 0.05);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
// ── Header card (top of left pane) ──
|
||||
.statistics-header-card {
|
||||
padding: 1rem 1.25rem;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
|
||||
|
||||
&__title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
|
||||
> i {
|
||||
font-size: 1.4rem;
|
||||
color: #0788cb;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.15rem;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: 0.8rem;
|
||||
color: #64748b;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Navigation list ──
|
||||
.statistics-nav {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0.5rem 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.statistics-nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.7rem 1.25rem;
|
||||
cursor: pointer;
|
||||
color: #334155;
|
||||
border-left: 3px solid transparent;
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
border-radius: 0;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
transition: background-color 0.15s ease, color 0.15s ease;
|
||||
|
||||
&:active {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
i {
|
||||
width: 1.1rem;
|
||||
text-align: center;
|
||||
color: #64748b;
|
||||
font-size: 0.95rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&:hover:not(.active) {
|
||||
background-color: #f1f5f9;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: #e0f2fe;
|
||||
color: #0369a1;
|
||||
font-weight: 600;
|
||||
border-left-color: #0284c7;
|
||||
|
||||
i {
|
||||
color: #0284c7;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Mobile tab bar (hidden on desktop) ──
|
||||
.statistics-mobile-tabs {
|
||||
display: none;
|
||||
}
|
||||
|
||||
// ── Right pane: content area ──
|
||||
.statistics-right-pane {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
padding: 1.5rem;
|
||||
background-color: #f8fafc;
|
||||
}
|
||||
|
||||
// ── Content header ──
|
||||
.statistics-content-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.25rem;
|
||||
|
||||
&__title {
|
||||
h2 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 800;
|
||||
color: #1e293b;
|
||||
margin: 0 0 0.25rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
color: #64748b;
|
||||
}
|
||||
}
|
||||
|
||||
.statistics-refresh-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
// Desktop mode: text only (no icon) for a more compact appearance
|
||||
i {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Traffic panel cards ──
|
||||
.statistics-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 1.25rem;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.traffic-panel {
|
||||
min-width: 0;
|
||||
padding: 1.25rem;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 0.5rem;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.traffic-panel__heading {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.6rem;
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 0.75rem;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
|
||||
> i {
|
||||
color: #0788cb;
|
||||
font-size: 1.05rem;
|
||||
margin-top: 0.15rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 0 0 0.2rem;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: 0.82rem;
|
||||
color: #64748b;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Pie/Donut chart ──
|
||||
.traffic-pie {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(10rem, 42%) 1fr;
|
||||
align-items: center;
|
||||
gap: 1.25rem;
|
||||
margin: 1rem 0;
|
||||
|
||||
svg {
|
||||
width: 100%;
|
||||
max-height: 16rem;
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
}
|
||||
|
||||
.traffic-pie__track,
|
||||
.traffic-pie__segment {
|
||||
fill: none;
|
||||
stroke-width: 18;
|
||||
}
|
||||
|
||||
.traffic-pie__track {
|
||||
stroke: #e8eef3;
|
||||
}
|
||||
|
||||
.traffic-pie__segment {
|
||||
transition: stroke-dasharray 0.25s ease;
|
||||
}
|
||||
|
||||
.traffic-pie__value,
|
||||
.traffic-pie__caption {
|
||||
transform: rotate(90deg);
|
||||
transform-origin: 60px 60px;
|
||||
fill: #1e293b;
|
||||
}
|
||||
|
||||
.traffic-pie__value {
|
||||
font-size: 9px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.traffic-pie__caption {
|
||||
font-size: 5px;
|
||||
fill: #64748b;
|
||||
}
|
||||
|
||||
// ── Legend ──
|
||||
.traffic-legend {
|
||||
min-width: 0;
|
||||
max-height: 16rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.traffic-legend__item {
|
||||
display: grid;
|
||||
grid-template-columns: 0.7rem minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.35rem 0;
|
||||
font-size: 0.84rem;
|
||||
}
|
||||
|
||||
.traffic-legend__swatch {
|
||||
width: 0.7rem;
|
||||
height: 0.7rem;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.traffic-legend__name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
// ── Data table ──
|
||||
.traffic-table-wrap {
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.traffic-table {
|
||||
width: 100%;
|
||||
font-size: 0.85rem;
|
||||
border-collapse: collapse;
|
||||
|
||||
th, td {
|
||||
padding: 0.55rem 0.45rem;
|
||||
text-align: right;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
th {
|
||||
font-weight: 600;
|
||||
color: #475569;
|
||||
border-bottom: 2px solid #e2e8f0;
|
||||
}
|
||||
|
||||
th:first-child, td:first-child {
|
||||
text-align: left;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
tbody tr:hover {
|
||||
background-color: #f8fafc;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Empty state ──
|
||||
.traffic-empty {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
gap: 0.5rem;
|
||||
min-height: 14rem;
|
||||
color: #94a3b8;
|
||||
text-align: center;
|
||||
|
||||
i {
|
||||
font-size: 3rem;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Error banner ──
|
||||
.statistics-error {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
padding: 0.8rem 1rem;
|
||||
color: #991b1b;
|
||||
background: #fef2f2;
|
||||
border: 1px solid #fecaca;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
// ── Footer note ──
|
||||
.statistics-note {
|
||||
margin: 1rem 0 0;
|
||||
color: #64748b;
|
||||
font-size: 0.8rem;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
// ── Placeholder section (Coming soon) ──
|
||||
.statistics-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.75rem;
|
||||
min-height: 50vh;
|
||||
text-align: center;
|
||||
color: #94a3b8;
|
||||
|
||||
i {
|
||||
font-size: 4rem;
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: #64748b;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 0.95rem;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Bandwidth section ──
|
||||
.bandwidth-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.bandwidth-summary-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.bandwidth-stat-card {
|
||||
background: #ffffff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem 1.25rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02);
|
||||
|
||||
&__icon {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.2rem;
|
||||
flex-shrink: 0;
|
||||
|
||||
&--in {
|
||||
background: #e0f2fe;
|
||||
color: #0284c7;
|
||||
}
|
||||
|
||||
&--out {
|
||||
background: #dcfce7;
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
&--queue {
|
||||
background: #fef3c7;
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
&--session {
|
||||
background: #f3e8ff;
|
||||
color: #7e22ce;
|
||||
}
|
||||
}
|
||||
|
||||
&__body {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__value {
|
||||
font-size: 1.15rem;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-size: 0.8rem;
|
||||
color: #64748b;
|
||||
margin-top: 0.15rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.bandwidth-panel {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.bandwidth-table {
|
||||
font-size: 0.82rem;
|
||||
min-width: 1050px;
|
||||
width: 100%;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
|
||||
th, td {
|
||||
padding: 0.6rem 0.65rem;
|
||||
white-space: nowrap !important;
|
||||
}
|
||||
|
||||
th:first-child, td:first-child {
|
||||
text-align: left;
|
||||
white-space: nowrap !important;
|
||||
position: sticky;
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
background: #ffffff;
|
||||
box-shadow: 2px 0 5px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
th:first-child {
|
||||
background: #ffffff;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
tbody tr:hover td:first-child {
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.bandwidth-peer-name {
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
display: inline-block;
|
||||
max-width: 150px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.bandwidth-peerid-cell {
|
||||
font-family: monospace;
|
||||
font-size: 0.78rem;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.bandwidth-totals-row {
|
||||
background-color: #f1f5f9;
|
||||
font-weight: 600;
|
||||
|
||||
td {
|
||||
border-bottom: 2px solid #cbd5e1;
|
||||
}
|
||||
|
||||
td:first-child {
|
||||
background: #f1f5f9;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #e8eef3;
|
||||
|
||||
td:first-child {
|
||||
background: #e8eef3;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bandwidth-drain-badge {
|
||||
display: inline-block;
|
||||
padding: 0.15rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
background: #f1f5f9;
|
||||
color: #475569;
|
||||
|
||||
&--warning {
|
||||
background: #fef3c7;
|
||||
color: #b45309;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
&--critical {
|
||||
background: #fee2e2;
|
||||
color: #dc2626;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Responsive: medium screens (stack traffic cards & 2-column stats) ──
|
||||
@media (max-width: 1200px) {
|
||||
.bandwidth-summary-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.statistics-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Responsive: phone mode ──
|
||||
@media (max-width: $bp-mobile), (max-width: $bp-narrow) and (max-height: $bp-short) {
|
||||
// Hide left sidebar on mobile
|
||||
.statistics-left-pane {
|
||||
display: none;
|
||||
}
|
||||
|
||||
// Show mobile tab bar
|
||||
.statistics-mobile-tabs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: #ffffff;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.statistics-mobile-tabs__list {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
overflow-x: auto;
|
||||
flex: 1;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.statistics-mobile-tab {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
padding: 0.4rem 0.75rem;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
color: #64748b;
|
||||
background: none;
|
||||
border: 1px solid transparent;
|
||||
box-shadow: none;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
|
||||
&:active {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: #e0f2fe;
|
||||
color: #0369a1;
|
||||
font-weight: 600;
|
||||
border-color: #bae6fd;
|
||||
}
|
||||
|
||||
&:hover:not(.active) {
|
||||
background-color: #f1f5f9;
|
||||
}
|
||||
}
|
||||
|
||||
.statistics-mobile-refresh {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
// Container becomes column for mobile
|
||||
.statistics-container {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
// Right pane takes full width
|
||||
.statistics-right-pane {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.statistics-content-header {
|
||||
margin-bottom: 0.75rem;
|
||||
|
||||
&__title h2 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
// Hide desktop refresh button on mobile (the mobile tab bar has its own icon button)
|
||||
.statistics-refresh-btn {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Stack cards
|
||||
.statistics-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.traffic-panel {
|
||||
padding: 0.8rem;
|
||||
}
|
||||
|
||||
// Stack chart and legend vertically on phone
|
||||
.traffic-pie {
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
svg {
|
||||
max-height: 14rem;
|
||||
}
|
||||
}
|
||||
|
||||
// Bandwidth mobile
|
||||
.bandwidth-summary-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 0.65rem;
|
||||
}
|
||||
|
||||
.bandwidth-stat-card {
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
|
||||
.bandwidth-panel {
|
||||
padding: 0.8rem 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
360
webui-src/app/statistics/bandwidth.js
Normal file
360
webui-src/app/statistics/bandwidth.js
Normal file
@ -0,0 +1,360 @@
|
||||
const m = require('mithril');
|
||||
const rs = require('rswebui');
|
||||
const NetworkData = require('network/network_data');
|
||||
|
||||
function idString(value) {
|
||||
if (!value) return '';
|
||||
if (typeof value === 'string') return value;
|
||||
return rs.idToHex(value);
|
||||
}
|
||||
|
||||
const formatBytes = rs.formatBytes;
|
||||
|
||||
function friendNamesFromCache() {
|
||||
const names = {};
|
||||
Object.values(NetworkData.gpgDetails || {}).forEach((profile) => {
|
||||
(profile.locations || []).forEach((location) => {
|
||||
const id = idString(location.id);
|
||||
if (id) names[id] = profile.name || location.name || id;
|
||||
});
|
||||
});
|
||||
return names;
|
||||
}
|
||||
|
||||
function number64(value) {
|
||||
if (!value) return 0;
|
||||
if (typeof value === 'object') return Number(value.xstr64 || value.xint64) || 0;
|
||||
return Number(value) || 0;
|
||||
}
|
||||
|
||||
function parseRates(raw) {
|
||||
if (!raw) return {};
|
||||
return {
|
||||
rateIn: Number(raw.mRateIn !== undefined ? raw.mRateIn : raw.rateIn) || 0,
|
||||
rateMaxIn: Number(raw.mRateMaxIn !== undefined ? raw.mRateMaxIn : raw.rateMaxIn) || 0,
|
||||
allocIn: Number(raw.mAllocIn !== undefined ? raw.mAllocIn : raw.allocIn) || 0,
|
||||
rateOut: Number(raw.mRateOut !== undefined ? raw.mRateOut : raw.rateOut) || 0,
|
||||
rateMaxOut: Number(raw.mRateMaxOut !== undefined ? raw.mRateMaxOut : raw.rateMaxOut) || 0,
|
||||
allowedOut: Number(raw.mAllowedOut !== undefined ? raw.mAllowedOut : raw.allowedOut) || 0,
|
||||
queueIn: Number(raw.mQueueIn !== undefined ? raw.mQueueIn : raw.queueIn) || 0,
|
||||
queueOut: Number(raw.mQueueOut !== undefined ? raw.mQueueOut : raw.queueOut) || 0,
|
||||
queueOutBytes: number64(raw.mQueueOutBytes !== undefined ? raw.mQueueOutBytes : raw.queueOutBytes),
|
||||
totalIn: number64(raw.mTotalIn !== undefined ? raw.mTotalIn : raw.totalIn),
|
||||
totalOut: number64(raw.mTotalOut !== undefined ? raw.mTotalOut : raw.totalOut),
|
||||
};
|
||||
}
|
||||
|
||||
function computeDrain(queueOutBytes, rateOut) {
|
||||
const effectiveSpeed = Math.max(rateOut, 1.0);
|
||||
return queueOutBytes / (effectiveSpeed * 1024.0);
|
||||
}
|
||||
|
||||
function formatRate(rate) {
|
||||
if (!rate || rate <= 0) return '0.0';
|
||||
return rate.toFixed(1);
|
||||
}
|
||||
|
||||
const COLORS = ['#0788cb', '#10b981', '#f59e0b', '#8b5cf6', '#ef4444', '#06b6d4', '#ec4899', '#84cc16', '#64748b', '#f97316'];
|
||||
|
||||
function DonutChart() {
|
||||
return {
|
||||
view(vnode) {
|
||||
const rows = (vnode.attrs.rows || []).filter((r) => r.value > 0);
|
||||
const total = vnode.attrs.total !== undefined ? vnode.attrs.total : rows.reduce((s, r) => s + r.value, 0);
|
||||
const caption = vnode.attrs.caption || 'total';
|
||||
let offset = 0;
|
||||
|
||||
if (!rows.length && !total) {
|
||||
return m('.traffic-empty', [
|
||||
m('i.fas.fa-chart-pie'),
|
||||
m('p', 'No session data recorded yet.'),
|
||||
]);
|
||||
}
|
||||
|
||||
return m('.traffic-pie', [
|
||||
m('svg[viewBox="0 0 120 120"][role=img]', { 'aria-label': vnode.attrs.label }, [
|
||||
m('circle[cx=60][cy=60][r=44].traffic-pie__track'),
|
||||
m('g[transform="rotate(-90 60 60)"]', rows.map((row, index) => {
|
||||
const length = total ? (row.value / total) * 276.46 : 0;
|
||||
const segment = m('circle[cx=60][cy=60][r=44].traffic-pie__segment', {
|
||||
stroke: COLORS[index % COLORS.length],
|
||||
'stroke-dasharray': `${length} ${276.46 - length}`,
|
||||
'stroke-dashoffset': -offset,
|
||||
}, m('title', `${row.label}: ${formatBytes(row.value)}`));
|
||||
offset += length;
|
||||
return segment;
|
||||
})),
|
||||
m('text[x=60][y=57][text-anchor=middle].traffic-pie__value', formatBytes(total)),
|
||||
m('text[x=60][y=70][text-anchor=middle].traffic-pie__caption', caption),
|
||||
]),
|
||||
m('.traffic-legend', rows.map((row, index) =>
|
||||
m('.traffic-legend__item', [
|
||||
m('span.traffic-legend__swatch', { style: { backgroundColor: COLORS[index % COLORS.length] } }),
|
||||
m('span.traffic-legend__name', row.label),
|
||||
m('strong', `${total ? ((row.value / total) * 100).toFixed(1) : 0}%`),
|
||||
])
|
||||
)),
|
||||
]);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function DrainBadge(drainSec) {
|
||||
let badgeClass = 'bandwidth-drain-badge';
|
||||
if (drainSec >= 60) {
|
||||
badgeClass += ' bandwidth-drain-badge--critical';
|
||||
} else if (drainSec >= 30) {
|
||||
badgeClass += ' bandwidth-drain-badge--warning';
|
||||
}
|
||||
const display = Math.round(drainSec);
|
||||
return m('span', { class: badgeClass }, `${display} s`);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
totalRates: null,
|
||||
peerRates: [],
|
||||
loading: false,
|
||||
error: '',
|
||||
timer: null,
|
||||
|
||||
async load() {
|
||||
if (this.loading) return;
|
||||
this.loading = true;
|
||||
try {
|
||||
const [totalRes, allRes] = await Promise.all([
|
||||
rs.rsJsonApiRequest('/rsConfig/getTotalBandwidthRates'),
|
||||
rs.rsJsonApiRequest('/rsConfig/getAllBandwidthRates'),
|
||||
]);
|
||||
|
||||
const totalBody = totalRes.body || totalRes || {};
|
||||
const allBody = allRes.body || allRes || {};
|
||||
|
||||
if (totalRes.status === 200 && totalBody.retval) {
|
||||
const rawTotals = totalBody.rates || {};
|
||||
this.totalRates = parseRates(rawTotals);
|
||||
this.totalRates.drain = computeDrain(this.totalRates.queueOutBytes, this.totalRates.rateOut);
|
||||
this.error = '';
|
||||
} else {
|
||||
this.totalRates = null;
|
||||
}
|
||||
|
||||
if (allRes.status === 200 && allBody.retval) {
|
||||
const friendNames = friendNamesFromCache();
|
||||
const rawMap = allBody.ratemap || {};
|
||||
const entries = Array.isArray(rawMap)
|
||||
? rawMap
|
||||
: Object.entries(rawMap).map(([key, value]) => ({ key, value }));
|
||||
|
||||
this.peerRates = entries.map((entry) => {
|
||||
const id = idString(entry.key);
|
||||
const rates = parseRates(entry.value);
|
||||
const drain = computeDrain(rates.queueOutBytes, rates.rateOut);
|
||||
const name = friendNames[id] || (id ? `Peer ${id.slice(0, 8)}…` : 'Unknown peer');
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
...rates,
|
||||
drain,
|
||||
};
|
||||
}).sort((a, b) => b.rateIn + b.rateOut - (a.rateIn + a.rateOut));
|
||||
} else {
|
||||
this.peerRates = [];
|
||||
}
|
||||
} catch (err) {
|
||||
this.error = 'Failed to load bandwidth statistics from RetroShare Core.';
|
||||
} finally {
|
||||
this.loading = false;
|
||||
m.redraw();
|
||||
}
|
||||
},
|
||||
|
||||
oninit() {
|
||||
this.totalRates = null;
|
||||
this.peerRates = [];
|
||||
this.error = '';
|
||||
this.load();
|
||||
this.timer = setInterval(() => this.load(), 5000);
|
||||
},
|
||||
|
||||
onremove() {
|
||||
if (this.timer) {
|
||||
clearInterval(this.timer);
|
||||
this.timer = null;
|
||||
}
|
||||
},
|
||||
|
||||
view() {
|
||||
const totals = this.totalRates;
|
||||
const peers = this.peerRates;
|
||||
|
||||
return m('.bandwidth-view', [
|
||||
this.error && m('.statistics-error', [m('i.fas.fa-exclamation-triangle'), this.error]),
|
||||
|
||||
// ── Top summary cards ──
|
||||
totals && m('.bandwidth-summary-grid', [
|
||||
m('.bandwidth-stat-card', [
|
||||
m('.bandwidth-stat-card__icon.bandwidth-stat-card__icon--in', m('i.fas.fa-arrow-down')),
|
||||
m('.bandwidth-stat-card__body', [
|
||||
m('.bandwidth-stat-card__value', `${formatRate(totals.rateIn)} kB/s`),
|
||||
m('.bandwidth-stat-card__label', 'Download speed'),
|
||||
]),
|
||||
]),
|
||||
m('.bandwidth-stat-card', [
|
||||
m('.bandwidth-stat-card__icon.bandwidth-stat-card__icon--out', m('i.fas.fa-arrow-up')),
|
||||
m('.bandwidth-stat-card__body', [
|
||||
m('.bandwidth-stat-card__value', `${formatRate(totals.rateOut)} kB/s`),
|
||||
m('.bandwidth-stat-card__label', 'Upload speed'),
|
||||
]),
|
||||
]),
|
||||
m('.bandwidth-stat-card', [
|
||||
m('.bandwidth-stat-card__icon.bandwidth-stat-card__icon--queue', m('i.fas.fa-layer-group')),
|
||||
m('.bandwidth-stat-card__body', [
|
||||
m('.bandwidth-stat-card__value', formatBytes(totals.queueOutBytes)),
|
||||
m('.bandwidth-stat-card__label', `Queue (${totals.queueOut} pkts / ${Math.round(totals.drain)}s drain)`),
|
||||
]),
|
||||
]),
|
||||
m('.bandwidth-stat-card', [
|
||||
m('.bandwidth-stat-card__icon.bandwidth-stat-card__icon--session', m('i.fas.fa-exchange-alt')),
|
||||
m('.bandwidth-stat-card__body', [
|
||||
m('.bandwidth-stat-card__value', `${formatBytes(totals.totalIn)} / ${formatBytes(totals.totalOut)}`),
|
||||
m('.bandwidth-stat-card__label', 'Session In / Out'),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
|
||||
// ── Session distribution charts by friends ──
|
||||
(() => {
|
||||
const inRows = peers
|
||||
.map((p) => ({ label: p.name, value: p.totalIn }))
|
||||
.filter((r) => r.value > 0)
|
||||
.sort((a, b) => b.value - a.value);
|
||||
|
||||
const peerInSum = inRows.reduce((sum, r) => sum + r.value, 0);
|
||||
const pastIn = totals ? Math.max(0, totals.totalIn - peerInSum) : 0;
|
||||
if (pastIn > 0) {
|
||||
inRows.push({ label: 'Disconnected peers', value: pastIn });
|
||||
}
|
||||
|
||||
const outRows = peers
|
||||
.map((p) => ({ label: p.name, value: p.totalOut }))
|
||||
.filter((r) => r.value > 0)
|
||||
.sort((a, b) => b.value - a.value);
|
||||
|
||||
const peerOutSum = outRows.reduce((sum, r) => sum + r.value, 0);
|
||||
const pastOut = totals ? Math.max(0, totals.totalOut - peerOutSum) : 0;
|
||||
if (pastOut > 0) {
|
||||
outRows.push({ label: 'Disconnected peers', value: pastOut });
|
||||
}
|
||||
|
||||
return m('.statistics-grid', [
|
||||
m('section.traffic-panel', [
|
||||
m('.traffic-panel__heading', [
|
||||
m('i.fas.fa-arrow-down'),
|
||||
m('div', [
|
||||
m('h3', 'Session Received by friend'),
|
||||
m('p', 'Incoming data transferred per friend during this session.'),
|
||||
]),
|
||||
]),
|
||||
m(DonutChart, {
|
||||
rows: inRows,
|
||||
total: totals ? totals.totalIn : peerInSum,
|
||||
label: 'Session received distribution',
|
||||
caption: 'session in',
|
||||
}),
|
||||
]),
|
||||
m('section.traffic-panel', [
|
||||
m('.traffic-panel__heading', [
|
||||
m('i.fas.fa-arrow-up'),
|
||||
m('div', [
|
||||
m('h3', 'Session Sent by friend'),
|
||||
m('p', 'Outgoing data transferred per friend during this session.'),
|
||||
]),
|
||||
]),
|
||||
m(DonutChart, {
|
||||
rows: outRows,
|
||||
total: totals ? totals.totalOut : peerOutSum,
|
||||
label: 'Session sent distribution',
|
||||
caption: 'session out',
|
||||
}),
|
||||
]),
|
||||
]);
|
||||
})(),
|
||||
|
||||
// ── Bandwidth detailed rates table ──
|
||||
m('section.traffic-panel.bandwidth-panel', [
|
||||
m('.traffic-panel__heading', [
|
||||
m('i.fas.fa-tachometer-alt'),
|
||||
m('div', [
|
||||
m('h3', 'Bandwidth Control Rates'),
|
||||
m('p', 'Real-time throughput, allocation limits, output queues, and estimated drain time per peer.'),
|
||||
]),
|
||||
]),
|
||||
|
||||
m('.traffic-table-wrap.bandwidth-table-wrap', [
|
||||
m('table.traffic-table.bandwidth-table', [
|
||||
m('thead', [
|
||||
m('tr', [
|
||||
m('th', 'Peer'),
|
||||
m('th', 'Peer ID'),
|
||||
m('th', { title: 'Current real-time download speed' }, 'In Rate (kB/s)'),
|
||||
m('th', { title: 'Total data received this session' }, 'Session In'),
|
||||
m('th', { title: 'Maximum download speed allocated to this peer' }, 'In Max (kB/s)'),
|
||||
m('th', { title: 'Incoming data packets waiting to be processed' }, 'In Queue'),
|
||||
m('th', { title: 'Current real-time upload speed' }, 'Out Rate (kB/s)'),
|
||||
m('th', { title: 'Total data sent this session' }, 'Session Out'),
|
||||
m('th', { title: 'Maximum upload speed allocated to this peer' }, 'Out Max (kB/s)'),
|
||||
m('th', { title: 'Upload limit requested by remote peer' }, 'Out Allowed (kB/s)'),
|
||||
m('th', { title: 'Outgoing data packets waiting to be sent' }, 'Out Queue'),
|
||||
m('th', { title: 'Total size buffered in output queue' }, 'Queue Size'),
|
||||
m('th', { title: 'Estimated time to empty output queue at current speed' }, 'Drain'),
|
||||
]),
|
||||
]),
|
||||
m('tbody', [
|
||||
// Pinned Totals row at top (same as Qt BwCtrlWindow)
|
||||
totals && m('tr.bandwidth-totals-row', [
|
||||
m('td', m('strong', 'Totals')),
|
||||
m('td.bandwidth-peerid-cell', '—'),
|
||||
m('td', m('strong', formatRate(totals.rateIn))),
|
||||
m('td', m('strong', formatBytes(totals.totalIn))),
|
||||
m('td', formatRate(totals.rateMaxIn)),
|
||||
m('td', totals.queueIn.toLocaleString()),
|
||||
m('td', m('strong', formatRate(totals.rateOut))),
|
||||
m('td', m('strong', formatBytes(totals.totalOut))),
|
||||
m('td', formatRate(totals.rateMaxOut)),
|
||||
m('td', '—'),
|
||||
m('td', totals.queueOut.toLocaleString()),
|
||||
m('td', formatBytes(totals.queueOutBytes)),
|
||||
m('td', DrainBadge(totals.drain)),
|
||||
]),
|
||||
|
||||
// Peer rows
|
||||
peers.length
|
||||
? peers.map((p) =>
|
||||
m('tr', [
|
||||
m('td', { title: p.id }, m('span.bandwidth-peer-name', p.name)),
|
||||
m('td.bandwidth-peerid-cell', { title: p.id }, `${p.id.slice(0, 8)}…`),
|
||||
m('td', formatRate(p.rateIn)),
|
||||
m('td', formatBytes(p.totalIn)),
|
||||
m('td', p.rateMaxIn > 0 ? formatRate(p.rateMaxIn) : '—'),
|
||||
m('td', p.queueIn.toLocaleString()),
|
||||
m('td', formatRate(p.rateOut)),
|
||||
m('td', formatBytes(p.totalOut)),
|
||||
m('td', p.rateMaxOut > 0 ? formatRate(p.rateMaxOut) : '—'),
|
||||
m('td', p.allowedOut > 0 ? formatRate(p.allowedOut) : '—'),
|
||||
m('td', p.queueOut.toLocaleString()),
|
||||
m('td', formatBytes(p.queueOutBytes)),
|
||||
m('td', DrainBadge(p.drain)),
|
||||
])
|
||||
)
|
||||
: (!totals
|
||||
? m('tr', [m('td[colspan=13]', m('.traffic-empty', [m('i.fas.fa-tachometer-alt'), m('p', 'No bandwidth data available.')]))])
|
||||
: null),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
|
||||
m('p.statistics-note', 'Bandwidth rates reflect current peer socket transfer states and refresh every 5 seconds.'),
|
||||
]);
|
||||
},
|
||||
};
|
||||
@ -1,6 +1,7 @@
|
||||
const m = require('mithril');
|
||||
const rs = require('rswebui');
|
||||
const NetworkData = require('network/network_data');
|
||||
const Bandwidth = require('statistics/bandwidth');
|
||||
|
||||
const COLORS = ['#0788cb', '#10b981', '#f59e0b', '#8b5cf6', '#ef4444', '#06b6d4', '#ec4899', '#84cc16', '#64748b', '#f97316'];
|
||||
const SERVICE_NAMES = {
|
||||
@ -13,6 +14,7 @@ const SERVICE_NAMES = {
|
||||
0x0200: 'Network exchange', 0x0211: 'Identities', 0x0215: 'Forums',
|
||||
0x0216: 'Boards', 0x0217: 'Channels', 0x0218: 'Circles', 0x0219: 'Reputation',
|
||||
0x0220: 'GXS recognition', 0x0230: 'GXS mail', 0x0240: 'JSON API',
|
||||
0x1011: 'RTT',
|
||||
};
|
||||
|
||||
function idString(value) {
|
||||
@ -130,7 +132,10 @@ function TrafficPanel() {
|
||||
view(vnode) {
|
||||
const rows = vnode.attrs.rows;
|
||||
return m('section.traffic-panel', [
|
||||
m('.traffic-panel__heading', [m('div', [m('h2', vnode.attrs.title), m('p', vnode.attrs.description)])]),
|
||||
m('.traffic-panel__heading', [
|
||||
m('i.fas.' + (vnode.attrs.icon || 'fa-chart-pie')),
|
||||
m('div', [m('h3', vnode.attrs.title), m('p', vnode.attrs.description)]),
|
||||
]),
|
||||
rows.length
|
||||
? [m(PieChart, { rows, label: `${vnode.attrs.title} traffic distribution` }),
|
||||
m('.traffic-table-wrap', m('table.traffic-table', [
|
||||
@ -146,8 +151,28 @@ function TrafficPanel() {
|
||||
};
|
||||
}
|
||||
|
||||
// Navigation sections — Traffic and Bandwidth are implemented
|
||||
const NAV_SECTIONS = [
|
||||
{ id: 'traffic', label: 'Traffic', icon: 'fa-chart-pie', description: 'Live traffic distribution reported by RetroShare Core.' },
|
||||
{ id: 'bandwidth', label: 'Bandwidth', icon: 'fa-tachometer-alt', description: 'Real-time bandwidth rates and peer throughput.' },
|
||||
];
|
||||
|
||||
function PlaceholderSection() {
|
||||
return {
|
||||
view(vnode) {
|
||||
const section = vnode.attrs.section;
|
||||
return m('.statistics-placeholder', [
|
||||
m('i.fas.' + section.icon),
|
||||
m('h3', section.label),
|
||||
m('p', 'Coming soon — this section is not yet implemented.'),
|
||||
]);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
oninit(vnode) {
|
||||
vnode.state.activeSection = 'traffic';
|
||||
vnode.state.incoming = [];
|
||||
vnode.state.outgoing = [];
|
||||
vnode.state.error = '';
|
||||
@ -201,10 +226,23 @@ module.exports = {
|
||||
vnode.state.friendsRefreshedAt = 0;
|
||||
vnode.state.loading = false;
|
||||
vnode.state.load();
|
||||
vnode.state.timer = setInterval(vnode.state.load, 5000);
|
||||
vnode.state.timer = setInterval(() => {
|
||||
if (vnode.state.activeSection === 'traffic') {
|
||||
vnode.state.load();
|
||||
}
|
||||
}, 5000);
|
||||
},
|
||||
onremove(vnode) { clearInterval(vnode.state.timer); },
|
||||
view(vnode) {
|
||||
const activeSection = NAV_SECTIONS.find((s) => s.id === vnode.state.activeSection) || NAV_SECTIONS[0];
|
||||
|
||||
const switchSection = (id) => {
|
||||
vnode.state.activeSection = id;
|
||||
if (id === 'traffic') {
|
||||
vnode.state.load();
|
||||
}
|
||||
};
|
||||
|
||||
const serviceLabel = (value) => {
|
||||
const id = Number(value) || 0;
|
||||
return SERVICE_NAMES[id] || `Service 0x${id.toString(16).padStart(4, '0')}`;
|
||||
@ -222,19 +260,88 @@ module.exports = {
|
||||
? cumulativeRows(vnode.state.cumulativePeers, friendLabel)
|
||||
: aggregate(vnode.state.incoming, vnode.state.outgoing,
|
||||
(clue) => idString(clue.peer_id) || 'unknown', (_clue, id) => friendLabel(id));
|
||||
return m('.statistics-page', [
|
||||
m('.statistics-header', [
|
||||
m('div', [m('h1', [m('i.fas.fa-chart-pie'), ' Traffic statistics']), m('p', 'Live traffic distribution reported by RetroShare Core.')]),
|
||||
m('button[type=button]', { disabled: vnode.state.loading, onclick: vnode.state.load }, [m('i.fas.fa-sync-alt'), ' Refresh']),
|
||||
|
||||
// Refresh handler based on active section
|
||||
const isBandwidth = activeSection.id === 'bandwidth';
|
||||
const isLoading = isBandwidth ? Bandwidth.loading : vnode.state.loading;
|
||||
const handleRefresh = () => {
|
||||
if (isBandwidth) {
|
||||
Bandwidth.load();
|
||||
} else {
|
||||
vnode.state.load();
|
||||
}
|
||||
};
|
||||
|
||||
// Build the content for the active section
|
||||
let sectionContent;
|
||||
if (activeSection.id === 'traffic') {
|
||||
sectionContent = [
|
||||
vnode.state.error && m('.statistics-error', [m('i.fas.fa-exclamation-triangle'), vnode.state.error]),
|
||||
m('.statistics-grid', [
|
||||
m(TrafficPanel, { title: 'By service', icon: 'fa-layer-group', description: 'Which RetroShare services use the most bandwidth.', column: 'Service', rows: serviceRows }),
|
||||
m(TrafficPanel, { title: 'By friend', icon: 'fa-user-friends', description: 'Traffic exchanged with each friend location.', column: 'Friend', rows: friendRows }),
|
||||
]),
|
||||
m('p.statistics-note', vnode.state.cumulativeServices
|
||||
? 'Cumulative values are retained by the Core and refresh every 5 seconds.'
|
||||
: 'Values cover the current traffic window and refresh every 5 seconds.'),
|
||||
];
|
||||
} else if (activeSection.id === 'bandwidth') {
|
||||
sectionContent = m(Bandwidth);
|
||||
} else {
|
||||
sectionContent = m(PlaceholderSection, { section: activeSection });
|
||||
}
|
||||
|
||||
return m('.statistics-container', [
|
||||
// ── Left pane: header card + navigation ──
|
||||
m('.statistics-left-pane', [
|
||||
m('.statistics-header-card', [
|
||||
m('.statistics-header-card__title', [
|
||||
m('i.fas.fa-chart-pie'),
|
||||
m('div', [m('h1', 'Statistics'), m('p', 'Traffic & routing stats')]),
|
||||
]),
|
||||
]),
|
||||
m('nav.statistics-nav', NAV_SECTIONS.map((section) =>
|
||||
m('button.statistics-nav-item[type=button]', {
|
||||
class: activeSection.id === section.id ? 'active' : '',
|
||||
onclick: () => switchSection(section.id),
|
||||
title: section.description,
|
||||
}, [m('i.fas.' + section.icon), m('span', section.label)])
|
||||
)),
|
||||
]),
|
||||
vnode.state.error && m('.statistics-error', [m('i.fas.fa-exclamation-triangle'), vnode.state.error]),
|
||||
m('.statistics-grid', [
|
||||
m(TrafficPanel, { title: 'By service', description: 'Which RetroShare services use the most bandwidth.', column: 'Service', rows: serviceRows }),
|
||||
m(TrafficPanel, { title: 'By friend', description: 'Traffic exchanged with each friend location.', column: 'Friend', rows: friendRows }),
|
||||
|
||||
// ── Mobile tab bar (visible only on small screens) ──
|
||||
m('.statistics-mobile-tabs', [
|
||||
m('.statistics-mobile-tabs__list', NAV_SECTIONS.map((section) =>
|
||||
m('button.statistics-mobile-tab[type=button]', {
|
||||
class: activeSection.id === section.id ? 'active' : '',
|
||||
onclick: () => switchSection(section.id),
|
||||
}, [m('i.fas.' + section.icon), m('span', section.label)])
|
||||
)),
|
||||
m('button.statistics-mobile-refresh[type=button]', {
|
||||
disabled: isLoading,
|
||||
onclick: handleRefresh,
|
||||
title: 'Refresh',
|
||||
}, m('i.fas.fa-sync-alt')),
|
||||
]),
|
||||
|
||||
// ── Right pane: section content ──
|
||||
m('.statistics-right-pane', [
|
||||
m('.statistics-content-header', [
|
||||
m('.statistics-content-header__title', [
|
||||
m('h2', activeSection.label),
|
||||
m('p', activeSection.description),
|
||||
]),
|
||||
m('button.statistics-refresh-btn[type=button]', {
|
||||
disabled: isLoading,
|
||||
onclick: handleRefresh,
|
||||
title: 'Refresh statistics',
|
||||
}, [
|
||||
m('i.fas.fa-sync-alt'),
|
||||
m('span.btn-text', 'Refresh'),
|
||||
]),
|
||||
]),
|
||||
sectionContent,
|
||||
]),
|
||||
m('p.statistics-note', vnode.state.cumulativeServices
|
||||
? 'Cumulative values are retained by the Core and refresh every 5 seconds.'
|
||||
: 'Values cover the current traffic window and refresh every 5 seconds.'),
|
||||
]);
|
||||
},
|
||||
};
|
||||
|
||||
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user