mirror of
https://github.com/hansjone/netx.git
synced 2026-10-08 22:20:58 +08:00
Unify dark UI surfaces and add adjustable topology canvas background.
Lazy-load module routes, restyle remaining light cards/modals/sidebars, and let canvas background be tuned from the display menu. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
254c50a2c0
commit
3e7bfdb8e9
6 changed files with 722 additions and 344 deletions
|
|
@ -97,10 +97,11 @@ def _windows_cpu_utility_pdh(sample_sec: float = 0.25) -> float | None:
|
|||
_PDH_COUNTER = h_counter
|
||||
_PDH_PRIMED = False
|
||||
|
||||
# After prime, CollectQueryData alone is enough (matches Task Manager Utility).
|
||||
if pdh.PdhCollectQueryData(_PDH_QUERY) != 0:
|
||||
return _PDH_LAST
|
||||
if not _PDH_PRIMED:
|
||||
time.sleep(max(0.05, float(sample_sec)))
|
||||
time.sleep(max(0.05, min(0.15, float(sample_sec))))
|
||||
if pdh.PdhCollectQueryData(_PDH_QUERY) != 0:
|
||||
return _PDH_LAST
|
||||
_PDH_PRIMED = True
|
||||
|
|
|
|||
148
web/src/App.tsx
148
web/src/App.tsx
|
|
@ -1,33 +1,63 @@
|
|||
import { lazy, Suspense } from "react";
|
||||
import { Navigate, Route, Routes, useLocation } from "react-router-dom";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { AppLayout } from "./layout/AppLayout";
|
||||
import { queryKeys } from "./constants/queryKeys";
|
||||
import { WorkbenchPage } from "./pages/WorkbenchPage";
|
||||
import { CollectPage } from "./pages/CollectPage";
|
||||
import { ConfigSyncPage } from "./pages/ConfigSyncPage";
|
||||
import { NePage } from "./pages/NePage";
|
||||
import { UmePage } from "./pages/UmePage";
|
||||
import { WebcrtPage } from "./pages/WebcrtPage";
|
||||
import { TopologyPage } from "./pages/TopologyPage";
|
||||
import { NetworkLayout } from "./pages/network/NetworkLayout";
|
||||
import { NetworkDevicesPage } from "./pages/network/NetworkDevicesPage";
|
||||
import { NetworkAlarmsPage } from "./pages/network/NetworkAlarmsPage";
|
||||
import { NetworkConfigsPage } from "./pages/network/NetworkConfigsPage";
|
||||
import { LldpLinksPage } from "./pages/network/LldpLinksPage";
|
||||
import { TopologyClassifyPage } from "./pages/network/TopologyClassifyPage";
|
||||
import { PortTrafficPage } from "./pages/network/PortTrafficPage";
|
||||
import { PortTrafficBoardListPage } from "./pages/network/PortTrafficBoardListPage";
|
||||
import { PortTrafficWallPage } from "./pages/network/PortTrafficWallPage";
|
||||
import { LoginPage } from "./pages/LoginPage";
|
||||
import { UsersPage } from "./pages/UsersPage";
|
||||
import { AuditLayout } from "./pages/audit/AuditLayout";
|
||||
import { TaskOverviewPage } from "./pages/audit/TaskOverviewPage";
|
||||
import { AuditPage } from "./pages/AuditPage";
|
||||
import { ApiTokensPage } from "./pages/ApiTokensPage";
|
||||
import { ForceChangePasswordPage } from "./pages/ForceChangePasswordPage";
|
||||
import { fetchIntegrationStatus } from "./services/api";
|
||||
import { useAuth } from "./auth/AuthContext";
|
||||
|
||||
const CollectPage = lazy(() => import("./pages/CollectPage").then((m) => ({ default: m.CollectPage })));
|
||||
const ConfigSyncPage = lazy(() =>
|
||||
import("./pages/ConfigSyncPage").then((m) => ({ default: m.ConfigSyncPage })),
|
||||
);
|
||||
const NePage = lazy(() => import("./pages/NePage").then((m) => ({ default: m.NePage })));
|
||||
const UmePage = lazy(() => import("./pages/UmePage").then((m) => ({ default: m.UmePage })));
|
||||
const WebcrtPage = lazy(() => import("./pages/WebcrtPage").then((m) => ({ default: m.WebcrtPage })));
|
||||
const TopologyPage = lazy(() => import("./pages/TopologyPage").then((m) => ({ default: m.TopologyPage })));
|
||||
const NetworkLayout = lazy(() =>
|
||||
import("./pages/network/NetworkLayout").then((m) => ({ default: m.NetworkLayout })),
|
||||
);
|
||||
const NetworkDevicesPage = lazy(() =>
|
||||
import("./pages/network/NetworkDevicesPage").then((m) => ({ default: m.NetworkDevicesPage })),
|
||||
);
|
||||
const NetworkAlarmsPage = lazy(() =>
|
||||
import("./pages/network/NetworkAlarmsPage").then((m) => ({ default: m.NetworkAlarmsPage })),
|
||||
);
|
||||
const NetworkConfigsPage = lazy(() =>
|
||||
import("./pages/network/NetworkConfigsPage").then((m) => ({ default: m.NetworkConfigsPage })),
|
||||
);
|
||||
const LldpLinksPage = lazy(() =>
|
||||
import("./pages/network/LldpLinksPage").then((m) => ({ default: m.LldpLinksPage })),
|
||||
);
|
||||
const TopologyClassifyPage = lazy(() =>
|
||||
import("./pages/network/TopologyClassifyPage").then((m) => ({ default: m.TopologyClassifyPage })),
|
||||
);
|
||||
const PortTrafficPage = lazy(() =>
|
||||
import("./pages/network/PortTrafficPage").then((m) => ({ default: m.PortTrafficPage })),
|
||||
);
|
||||
const PortTrafficBoardListPage = lazy(() =>
|
||||
import("./pages/network/PortTrafficBoardListPage").then((m) => ({
|
||||
default: m.PortTrafficBoardListPage,
|
||||
})),
|
||||
);
|
||||
const PortTrafficWallPage = lazy(() =>
|
||||
import("./pages/network/PortTrafficWallPage").then((m) => ({ default: m.PortTrafficWallPage })),
|
||||
);
|
||||
const UsersPage = lazy(() => import("./pages/UsersPage").then((m) => ({ default: m.UsersPage })));
|
||||
const AuditLayout = lazy(() =>
|
||||
import("./pages/audit/AuditLayout").then((m) => ({ default: m.AuditLayout })),
|
||||
);
|
||||
const TaskOverviewPage = lazy(() =>
|
||||
import("./pages/audit/TaskOverviewPage").then((m) => ({ default: m.TaskOverviewPage })),
|
||||
);
|
||||
const AuditPage = lazy(() => import("./pages/AuditPage").then((m) => ({ default: m.AuditPage })));
|
||||
const ApiTokensPage = lazy(() =>
|
||||
import("./pages/ApiTokensPage").then((m) => ({ default: m.ApiTokensPage })),
|
||||
);
|
||||
|
||||
/** Preserve query when redirecting legacy /network/webcrt → /webcrt. */
|
||||
function NetworkWebcrtRedirect() {
|
||||
const { search } = useLocation();
|
||||
|
|
@ -44,13 +74,21 @@ function LegacyPortTrafficWallRedirect() {
|
|||
return <PortTrafficBoardListPage />;
|
||||
}
|
||||
|
||||
function PageFallback() {
|
||||
return (
|
||||
<div className="page-loading" role="status" aria-live="polite">
|
||||
Loading…
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ProtectedApp() {
|
||||
const { ready, user } = useAuth();
|
||||
const integrationsQuery = useQuery({
|
||||
queryKey: queryKeys.integrationsStatus,
|
||||
queryFn: fetchIntegrationStatus,
|
||||
refetchInterval: 5000,
|
||||
staleTime: 2000,
|
||||
refetchInterval: 10_000,
|
||||
staleTime: 5_000,
|
||||
enabled: ready && Boolean(user) && !user?.must_change_password,
|
||||
});
|
||||
|
||||
|
|
@ -95,38 +133,40 @@ function ProtectedApp() {
|
|||
: undefined,
|
||||
}}
|
||||
>
|
||||
<Routes>
|
||||
<Route path="/" element={<WorkbenchPage />} />
|
||||
<Route path="/workbench" element={<Navigate to="/" replace />} />
|
||||
<Route path="/ume" element={<UmePage />} />
|
||||
<Route path="/ne" element={<NePage />} />
|
||||
<Route path="/topology" element={<TopologyPage />} />
|
||||
<Route path="/webcrt" element={<WebcrtPage />} />
|
||||
<Route path="/port-traffic/wall/:boardId" element={<PortTrafficWallPage />} />
|
||||
<Route path="/network" element={<NetworkLayout />}>
|
||||
<Route index element={<Navigate to="devices" replace />} />
|
||||
<Route path="devices" element={<NetworkDevicesPage />} />
|
||||
<Route path="alarms" element={<NetworkAlarmsPage />} />
|
||||
<Route path="configs" element={<NetworkConfigsPage />} />
|
||||
<Route path="topology/lldp" element={<LldpLinksPage />} />
|
||||
<Route path="topology/classify" element={<TopologyClassifyPage />} />
|
||||
<Route path="topology" element={<Navigate to="/network/topology/lldp" replace />} />
|
||||
<Route path="webcrt" element={<NetworkWebcrtRedirect />} />
|
||||
<Route path="tasks/collect" element={<CollectPage />} />
|
||||
<Route path="tasks/config-sync" element={<ConfigSyncPage />} />
|
||||
<Route path="tasks/port-traffic/wall" element={<LegacyPortTrafficWallRedirect />} />
|
||||
<Route path="tasks/port-traffic" element={<PortTrafficPage />} />
|
||||
</Route>
|
||||
<Route path="/collect" element={<Navigate to="/network/tasks/collect" replace />} />
|
||||
<Route path="/users" element={<UsersPage />} />
|
||||
<Route path="/audit" element={<AuditLayout />}>
|
||||
<Route index element={<Navigate to="tasks" replace />} />
|
||||
<Route path="tasks" element={<TaskOverviewPage />} />
|
||||
<Route path="logs" element={<AuditPage />} />
|
||||
</Route>
|
||||
<Route path="/api-keys" element={<ApiTokensPage />} />
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Routes>
|
||||
<Suspense fallback={<PageFallback />}>
|
||||
<Routes>
|
||||
<Route path="/" element={<WorkbenchPage />} />
|
||||
<Route path="/workbench" element={<Navigate to="/" replace />} />
|
||||
<Route path="/ume" element={<UmePage />} />
|
||||
<Route path="/ne" element={<NePage />} />
|
||||
<Route path="/topology" element={<TopologyPage />} />
|
||||
<Route path="/webcrt" element={<WebcrtPage />} />
|
||||
<Route path="/port-traffic/wall/:boardId" element={<PortTrafficWallPage />} />
|
||||
<Route path="/network" element={<NetworkLayout />}>
|
||||
<Route index element={<Navigate to="devices" replace />} />
|
||||
<Route path="devices" element={<NetworkDevicesPage />} />
|
||||
<Route path="alarms" element={<NetworkAlarmsPage />} />
|
||||
<Route path="configs" element={<NetworkConfigsPage />} />
|
||||
<Route path="topology/lldp" element={<LldpLinksPage />} />
|
||||
<Route path="topology/classify" element={<TopologyClassifyPage />} />
|
||||
<Route path="topology" element={<Navigate to="/network/topology/lldp" replace />} />
|
||||
<Route path="webcrt" element={<NetworkWebcrtRedirect />} />
|
||||
<Route path="tasks/collect" element={<CollectPage />} />
|
||||
<Route path="tasks/config-sync" element={<ConfigSyncPage />} />
|
||||
<Route path="tasks/port-traffic/wall" element={<LegacyPortTrafficWallRedirect />} />
|
||||
<Route path="tasks/port-traffic" element={<PortTrafficPage />} />
|
||||
</Route>
|
||||
<Route path="/collect" element={<Navigate to="/network/tasks/collect" replace />} />
|
||||
<Route path="/users" element={<UsersPage />} />
|
||||
<Route path="/audit" element={<AuditLayout />}>
|
||||
<Route index element={<Navigate to="tasks" replace />} />
|
||||
<Route path="tasks" element={<TaskOverviewPage />} />
|
||||
<Route path="logs" element={<AuditPage />} />
|
||||
</Route>
|
||||
<Route path="/api-keys" element={<ApiTokensPage />} />
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Routes>
|
||||
</Suspense>
|
||||
</AppLayout>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1464,6 +1464,9 @@ const en = {
|
|||
fullscreen: "Fullscreen",
|
||||
exitFullscreen: "Exit fullscreen",
|
||||
display: "Display",
|
||||
canvasBg: "Canvas background",
|
||||
canvasBgColor: "Background color",
|
||||
canvasBgReset: "Reset light blue",
|
||||
hideIp: "Hide IP",
|
||||
hideVendor: "Hide vendor",
|
||||
hidePorts: "Hide ports",
|
||||
|
|
|
|||
|
|
@ -1458,6 +1458,9 @@ const zh = {
|
|||
fullscreen: "全屏显示",
|
||||
exitFullscreen: "退出全屏",
|
||||
display: "显示",
|
||||
canvasBg: "画布背景",
|
||||
canvasBgColor: "背景色",
|
||||
canvasBgReset: "恢复浅蓝",
|
||||
hideIp: "隐藏 IP",
|
||||
hideVendor: "隐藏厂商",
|
||||
hidePorts: "隐藏端口",
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -451,6 +451,36 @@ const AUTO_LAYOUT_DISCOVER_KEY = "netx.topology.autoLayoutAfterDiscover";
|
|||
const DISCOVER_AUTO_ADD_KEY = "netx.topology.discoverAutoAddUnmatched";
|
||||
const DISCOVER_PROJECT_NEIGHBORS_KEY = "netx.topology.discoverProjectNeighbors";
|
||||
const SCALE_BUNDLE_WIDTH_KEY = "netx.topology.scaleBundleWidth";
|
||||
const CANVAS_BG_KEY = "netx.topology.canvasBg";
|
||||
const DEFAULT_CANVAS_BG = "#dbeafe";
|
||||
|
||||
function loadCanvasBg(): string {
|
||||
try {
|
||||
const raw = String(localStorage.getItem(CANVAS_BG_KEY) || "").trim();
|
||||
if (/^#[0-9a-fA-F]{6}$/.test(raw)) return raw.toLowerCase();
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
return DEFAULT_CANVAS_BG;
|
||||
}
|
||||
|
||||
function persistCanvasBg(value: string) {
|
||||
try {
|
||||
localStorage.setItem(CANVAS_BG_KEY, value);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
function canvasDotColor(bg: string): string {
|
||||
const hex = String(bg || "").replace("#", "");
|
||||
if (hex.length !== 6) return "#93c5fd";
|
||||
const r = parseInt(hex.slice(0, 2), 16);
|
||||
const g = parseInt(hex.slice(2, 4), 16);
|
||||
const b = parseInt(hex.slice(4, 6), 16);
|
||||
const lum = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
|
||||
return lum > 0.55 ? "#93c5fd" : "#64748b";
|
||||
}
|
||||
|
||||
function loadBoolFlag(key: string, defaultValue: boolean): boolean {
|
||||
try {
|
||||
|
|
@ -703,6 +733,7 @@ export function TopologyPage() {
|
|||
);
|
||||
const [edgeFlow, setEdgeFlow] = useState(false);
|
||||
const [edgeDefaults, setEdgeDefaults] = useState<EdgeDefaults>(() => loadEdgeDefaults());
|
||||
const [canvasBg, setCanvasBg] = useState(loadCanvasBg);
|
||||
const [toolMode, setToolMode] = useState<ToolMode>("select");
|
||||
const [snapToGrid, setSnapToGrid] = useState(true);
|
||||
const [autoLayoutAfterDiscover, setAutoLayoutAfterDiscover] = useState(loadAutoLayoutAfterDiscover);
|
||||
|
|
@ -2827,6 +2858,51 @@ export function TopologyPage() {
|
|||
/>
|
||||
{t("topology.discoverProjectNeighbors")}
|
||||
</label>
|
||||
<div className="topo-display-defaults topo-display-defaults--canvas-bg">
|
||||
<div className="topo-display-defaults__head">
|
||||
<strong>{t("topology.canvasBg")}</strong>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn--sm btn--ghost"
|
||||
onClick={() => {
|
||||
setCanvasBg(DEFAULT_CANVAS_BG);
|
||||
persistCanvasBg(DEFAULT_CANVAS_BG);
|
||||
}}
|
||||
>
|
||||
{t("topology.canvasBgReset")}
|
||||
</button>
|
||||
</div>
|
||||
<div className="topo-display-defaults__row">
|
||||
<span className="topo-display-defaults__name">{t("topology.canvasBgColor")}</span>
|
||||
<input
|
||||
type="color"
|
||||
value={canvasBg}
|
||||
title={t("topology.canvasBgColor")}
|
||||
onChange={(e) => {
|
||||
const next = e.target.value;
|
||||
setCanvasBg(next);
|
||||
persistCanvasBg(next);
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
className="topo-toolbar__select topo-canvas-bg-hex"
|
||||
value={canvasBg}
|
||||
spellCheck={false}
|
||||
aria-label={t("topology.canvasBgColor")}
|
||||
onChange={(e) => {
|
||||
const raw = e.target.value.trim();
|
||||
if (!/^#[0-9a-fA-F]{0,6}$/.test(raw)) return;
|
||||
setCanvasBg(raw);
|
||||
if (/^#[0-9a-fA-F]{6}$/.test(raw)) persistCanvasBg(raw.toLowerCase());
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (!/^#[0-9a-fA-F]{6}$/.test(canvasBg)) {
|
||||
setCanvasBg(loadCanvasBg());
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="topo-display-defaults">
|
||||
<div className="topo-display-defaults__head">
|
||||
<strong>{t("topology.edgeDefaults")}</strong>
|
||||
|
|
@ -3244,6 +3320,7 @@ export function TopologyPage() {
|
|||
<div
|
||||
className={`topo-canvas${fullscreen ? " is-fullscreen" : ""}${toolMode === "pan" ? " is-pan-mode" : ""}`}
|
||||
ref={canvasRef}
|
||||
style={{ ["--topo-canvas-bg" as string]: canvasBg }}
|
||||
onDragOver={onCanvasDragOver}
|
||||
onDrop={onCanvasDrop}
|
||||
>
|
||||
|
|
@ -3383,7 +3460,13 @@ export function TopologyPage() {
|
|||
deleteKeyCode={null}
|
||||
edgesFocusable
|
||||
>
|
||||
<Background variant={BackgroundVariant.Dots} gap={16} size={1} color="#d8dee8" />
|
||||
<Background
|
||||
variant={BackgroundVariant.Dots}
|
||||
gap={16}
|
||||
size={1}
|
||||
color={canvasDotColor(canvasBg)}
|
||||
bgColor={canvasBg}
|
||||
/>
|
||||
{toolMode === "connect" ? (
|
||||
<div className="topo-mode-hint" role="status">
|
||||
{t("topology.connectHint")}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue