mirror of
https://github.com/MeshCore-Beacon/beacon-web.git
synced 2026-09-01 16:48:19 +00:00
Integrate the stats feature onto main: live mesh KPIs, observations time-series, payload/node-type donuts, top-nodes/observers leaderboards, radio presets, scope stats, and per-observer telemetry (battery, airtime, noise, queue, receive errors) with 1h/6h/24h bucketing. - adapt to the multi-IATA region model (useRegion -> single iata for /stats/*) - rename stats getScopes -> getStatsScopes (avoids /scopes name-list collision) - donut legend kept right but scrollable + truncating so it never clips - balanced mesh grid; observer empty-state for telemetry-less observers - normalize telemetry t to ms (raw path emits seconds, bucketed emits ms) - add echarts 5.5.1; lazy-load StatsOverview chunk
62 lines
2.5 KiB
TypeScript
62 lines
2.5 KiB
TypeScript
import { useCallback } from "react";
|
|
import { useSearchParams } from "react-router-dom";
|
|
import type { WsManager } from "../../api/ws-manager";
|
|
import { StatsSubHeader } from "./StatsSubHeader";
|
|
import { MeshTab } from "./MeshTab";
|
|
import { ObserverTab } from "./ObserverTab";
|
|
import type { StatsRange, StatsTab } from "./types";
|
|
|
|
const TABS: StatsTab[] = ["mesh", "observer"];
|
|
const RANGES: StatsRange[] = ["24h", "7d", "30d"];
|
|
|
|
const asTab = (v: string | null): StatsTab => (TABS.includes(v as StatsTab) ? (v as StatsTab) : "mesh");
|
|
const asRange = (v: string | null): StatsRange => (RANGES.includes(v as StatsRange) ? (v as StatsRange) : "7d");
|
|
|
|
interface StatsOverviewProps {
|
|
wsManager: WsManager;
|
|
}
|
|
|
|
// Stats page shell: a sub-header bar (Mesh / Observer pills + range + live dot) over the active
|
|
// sub-tab. Sub-tab, range, and selected observer live in the URL (?statsTab/?range/?observerId) so the
|
|
// view is shareable; replace:true keeps it out of history. Queries are cached, so switching is instant.
|
|
export function StatsOverview({ wsManager }: StatsOverviewProps) {
|
|
const [params, setParams] = useSearchParams();
|
|
const tab = asTab(params.get("statsTab"));
|
|
const range = asRange(params.get("range"));
|
|
const observerId = params.get("observerId");
|
|
|
|
const patch = useCallback(
|
|
(updates: Record<string, string | null>) => {
|
|
setParams(
|
|
(prev) => {
|
|
const next = new URLSearchParams(prev);
|
|
for (const [k, v] of Object.entries(updates)) {
|
|
if (v == null) next.delete(k);
|
|
else next.set(k, v);
|
|
}
|
|
return next;
|
|
},
|
|
{ replace: true },
|
|
);
|
|
},
|
|
[setParams],
|
|
);
|
|
|
|
const handleTab = useCallback((t: StatsTab) => patch({ statsTab: t }), [patch]);
|
|
const handleRange = useCallback((r: StatsRange) => patch({ range: r }), [patch]);
|
|
const handleSelectObserver = useCallback((id: string) => patch({ statsTab: "observer", observerId: id }), [patch]);
|
|
|
|
return (
|
|
<div className="flex min-h-0 flex-1 flex-col">
|
|
<StatsSubHeader tab={tab} onTabChange={handleTab} range={range} onRangeChange={handleRange} wsManager={wsManager} />
|
|
<div className="min-h-0 flex-1 overflow-y-auto">
|
|
{tab === "mesh" ? (
|
|
<MeshTab range={range} onSelectObserver={handleSelectObserver} wsManager={wsManager} />
|
|
) : (
|
|
<ObserverTab range={range} selectedObserverId={observerId} onSelectObserver={handleSelectObserver} wsManager={wsManager} />
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|