diff --git a/client/ui/frontend/src/layouts/AppLayout.tsx b/client/ui/frontend/src/layouts/AppLayout.tsx index df8c571c0..5668b1d4a 100644 --- a/client/ui/frontend/src/layouts/AppLayout.tsx +++ b/client/ui/frontend/src/layouts/AppLayout.tsx @@ -1,5 +1,6 @@ import { Outlet } from "react-router-dom"; import { Header } from "@/layouts/Header.tsx"; +import { ViewModeProvider } from "@/lib/viewMode"; import { ClientVersionProvider } from "@/modules/auto-update/ClientVersionContext.tsx"; import { StatusProvider } from "@/modules/daemon-status/StatusContext.tsx"; import { DebugBundleProvider } from "@/modules/debug-bundle/DebugBundleContext.tsx"; @@ -12,8 +13,10 @@ export const AppLayout = () => { -
- + +
+ + diff --git a/client/ui/frontend/src/layouts/Header.tsx b/client/ui/frontend/src/layouts/Header.tsx index d34716e14..3d6b692f9 100644 --- a/client/ui/frontend/src/layouts/Header.tsx +++ b/client/ui/frontend/src/layouts/Header.tsx @@ -9,7 +9,6 @@ import { Settings, type LucideIcon, } from "lucide-react"; -import { Window } from "@wailsio/runtime"; import { WindowManager } from "@bindings/services"; import { DropdownMenu, @@ -22,21 +21,12 @@ import { IconButton } from "@/components/IconButton"; import { ProfileDropdown } from "@/components/ProfileDropdown"; import { useClientVersion } from "@/modules/auto-update/ClientVersionContext"; import { cn } from "@/lib/cn"; - -type ViewMode = "default" | "advanced"; - -// Window dimensions per view. Height matches the Settings window (640) so the -// chrome height is identical across surfaces; width grows from the compact -// 380 default to 900 in advanced. -const VIEW_SIZE: Record = { - default: { width: 380, height: 640 }, - advanced: { width: 900, height: 640 }, -}; +import { useViewMode, type ViewMode } from "@/lib/viewMode"; export const Header = () => { const { t } = useTranslation(); const [menuOpen, setMenuOpen] = useState(false); - const [viewMode, setViewMode] = useState("default"); + const { viewMode, setViewMode } = useViewMode(); const { updateAvailable } = useClientVersion(); const openSettings = () => { @@ -55,10 +45,7 @@ export const Header = () => { const selectMode = (mode: ViewMode) => { setMenuOpen(false); - if (mode === viewMode) return; setViewMode(mode); - const { width, height } = VIEW_SIZE[mode]; - void Window.SetSize(width, height).catch(() => {}); }; return ( diff --git a/client/ui/frontend/src/layouts/Main.tsx b/client/ui/frontend/src/layouts/Main.tsx index b6f31829c..3c507770e 100644 --- a/client/ui/frontend/src/layouts/Main.tsx +++ b/client/ui/frontend/src/layouts/Main.tsx @@ -1,11 +1,28 @@ import { ConnectionStatusSwitch } from "@/layouts/ConnectionStatusSwitch.tsx"; +import { MainRightSide } from "@/layouts/MainRightSide.tsx"; +import { useViewMode } from "@/lib/viewMode"; +import { Peers } from "@/modules/peers/Peers"; export const Main = () => { + const { viewMode } = useViewMode(); + const isAdvanced = viewMode === "advanced"; + return (
-
+ {/* Fixed-width column for the connection switch — same in both + default and advanced view so the NetBird logo doesn't shift + horizontally when the window grows. In default view the + inner row is 348px so the column fills it; in advanced view + the column sits on the left with Peers in the remaining + space. */} +
+ {isAdvanced && ( + + + + )}
); }; diff --git a/client/ui/frontend/src/lib/viewMode.tsx b/client/ui/frontend/src/lib/viewMode.tsx new file mode 100644 index 000000000..988f8f116 --- /dev/null +++ b/client/ui/frontend/src/lib/viewMode.tsx @@ -0,0 +1,45 @@ +import { createContext, useCallback, useContext, useState, type ReactNode } from "react"; +import { Window } from "@wailsio/runtime"; + +export type ViewMode = "default" | "advanced"; + +// Window dimensions per view. Height matches the Settings window (640) so +// the chrome height is identical across surfaces; width grows from the +// compact 380 default to 900 in advanced. +export const VIEW_SIZE: Record = { + default: { width: 380, height: 640 }, + advanced: { width: 900, height: 640 }, +}; + +type ViewModeContextValue = { + viewMode: ViewMode; + setViewMode: (mode: ViewMode) => void; +}; + +const ViewModeContext = createContext(null); + +export const ViewModeProvider = ({ children }: { children: ReactNode }) => { + const [viewMode, setMode] = useState("default"); + const setViewMode = useCallback( + (mode: ViewMode) => { + setMode((prev) => { + if (prev === mode) return prev; + const { width, height } = VIEW_SIZE[mode]; + void Window.SetSize(width, height).catch(() => {}); + return mode; + }); + }, + [], + ); + return ( + + {children} + + ); +}; + +export const useViewMode = () => { + const ctx = useContext(ViewModeContext); + if (!ctx) throw new Error("useViewMode must be used inside ViewModeProvider"); + return ctx; +}; diff --git a/client/ui/frontend/src/modules/peers/Peers.tsx b/client/ui/frontend/src/modules/peers/Peers.tsx index 9f2dd98b3..f71d7ac3b 100644 --- a/client/ui/frontend/src/modules/peers/Peers.tsx +++ b/client/ui/frontend/src/modules/peers/Peers.tsx @@ -1,44 +1,56 @@ -import { useMemo, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import * as ScrollArea from "@radix-ui/react-scroll-area"; import { cn } from "@/lib/cn"; import { SearchInput } from "@/components/SearchInput"; -import { mockPeers } from "./mockPeers"; +import { useStatus } from "@/modules/daemon-status/StatusContext"; import { PeerFilters, StatusFilter } from "./PeerFilters"; import { PeersList } from "./PeersList"; -const isOnline = (status: string) => status === "connected"; +const isOnline = (connStatus: string) => connStatus === "Connected"; export const Peers = () => { const { t } = useTranslation(); + const { status } = useStatus(); const [search, setSearch] = useState(""); const [statusFilter, setStatusFilter] = useState("all"); + const searchRef = useRef(null); + + // Peers is only mounted in advanced view (see layouts/Main.tsx), so a + // mount-time focus is equivalent to "focus when the user toggles into + // advanced view". + useEffect(() => { + searchRef.current?.focus(); + }, []); + + const peers = status?.peers ?? []; const counts = useMemo>(() => { - const online = mockPeers.filter((p) => isOnline(p.status)).length; + const online = peers.filter((p) => isOnline(p.connStatus)).length; return { - all: mockPeers.length, + all: peers.length, online, - offline: mockPeers.length - online, + offline: peers.length - online, }; - }, []); + }, [peers]); const filtered = useMemo(() => { const q = search.trim().toLowerCase(); - return mockPeers.filter((p) => { - if (statusFilter === "online" && !isOnline(p.status)) return false; - if (statusFilter === "offline" && isOnline(p.status)) return false; + return peers.filter((p) => { + if (statusFilter === "online" && !isOnline(p.connStatus)) return false; + if (statusFilter === "offline" && isOnline(p.connStatus)) return false; if (q && !p.fqdn.toLowerCase().includes(q) && !p.ip.includes(q)) { return false; } return true; }); - }, [search, statusFilter]); + }, [peers, search, statusFilter]); return (
setSearch(e.target.value)} diff --git a/client/ui/frontend/src/modules/peers/PeersList.tsx b/client/ui/frontend/src/modules/peers/PeersList.tsx index 7ea6d7cd3..cf5dc306d 100644 --- a/client/ui/frontend/src/modules/peers/PeersList.tsx +++ b/client/ui/frontend/src/modules/peers/PeersList.tsx @@ -1,14 +1,19 @@ import { useTranslation } from "react-i18next"; +import type { PeerStatus } from "@bindings/services/models.js"; import { cn } from "@/lib/cn"; -import { Peer, PeerStatus } from "./types"; -const DOT: Record = { - connected: "bg-green-400", - connecting: "bg-yellow-300 animate-pulse-slow", - disconnected: "bg-nb-gray-500", +const dotClass = (connStatus: string): string => { + switch (connStatus) { + case "Connected": + return "bg-green-400"; + case "Connecting": + return "bg-yellow-300 animate-pulse-slow"; + default: + return "bg-nb-gray-500"; + } }; -export const PeersList = ({ data }: { data: Peer[] }) => { +export const PeersList = ({ data }: { data: PeerStatus[] }) => { const { t } = useTranslation(); if (data.length === 0) { return ( @@ -22,15 +27,15 @@ export const PeersList = ({ data }: { data: Peer[] }) => {
    {data.map((peer) => (
  • new Date(Date.now() - m * 60 * 1000); - -export const mockPeers: Peer[] = [ - { - id: "p-001", - fqdn: "alice-laptop.netbird.cloud", - ip: "100.64.0.12", - status: "connected", - lastHandshake: minutesAgo(1), - latencyMs: 18, - relayed: false, - iceLocalCandidate: "srflx", - iceRemoteCandidate: "host", - bytesRx: 1024 * 1024 * 84, - bytesTx: 1024 * 1024 * 12, - endpointLocal: "192.168.1.24:51820", - endpointRemote: "203.0.113.45:51820", - }, - { - id: "p-002", - fqdn: "bob-desktop.netbird.cloud", - ip: "100.64.0.21", - status: "connected", - lastHandshake: minutesAgo(3), - latencyMs: 42, - relayed: true, - relayAddress: "rel.eu-central.netbird.io:443", - iceLocalCandidate: "relay", - iceRemoteCandidate: "relay", - bytesRx: 1024 * 380, - bytesTx: 1024 * 940, - endpointLocal: "10.0.0.8:51820", - endpointRemote: "198.51.100.7:51820", - }, - { - id: "p-003", - fqdn: "build-runner-01.netbird.cloud", - ip: "100.64.0.34", - status: "connecting", - lastHandshake: minutesAgo(15), - latencyMs: 0, - relayed: false, - iceLocalCandidate: "host", - iceRemoteCandidate: "host", - bytesRx: 0, - bytesTx: 0, - endpointLocal: "192.168.1.45:51820", - endpointRemote: "—", - }, - { - id: "p-004", - fqdn: "carol-phone.netbird.cloud", - ip: "100.64.0.55", - status: "disconnected", - lastHandshake: minutesAgo(620), - latencyMs: 0, - relayed: false, - iceLocalCandidate: "srflx", - iceRemoteCandidate: "srflx", - bytesRx: 1024 * 1024 * 5, - bytesTx: 1024 * 1024 * 2, - endpointLocal: "—", - endpointRemote: "—", - }, - { - id: "p-005", - fqdn: "exit-berlin.netbird.cloud", - ip: "100.64.0.2", - status: "connected", - lastHandshake: minutesAgo(0.2), - latencyMs: 9, - relayed: false, - iceLocalCandidate: "host", - iceRemoteCandidate: "srflx", - bytesRx: 1024 * 1024 * 1024 * 2, - bytesTx: 1024 * 1024 * 512, - endpointLocal: "10.10.0.4:51820", - endpointRemote: "203.0.113.99:51820", - }, - { - id: "p-006", - fqdn: "db-replica-eu.netbird.cloud", - ip: "100.64.0.78", - status: "connected", - lastHandshake: minutesAgo(7), - latencyMs: 64, - relayed: true, - relayAddress: "rel.us-east.netbird.io:443", - iceLocalCandidate: "relay", - iceRemoteCandidate: "host", - bytesRx: 1024 * 1024 * 240, - bytesTx: 1024 * 1024 * 90, - endpointLocal: "172.16.0.10:51820", - endpointRemote: "198.51.100.42:51820", - }, - { - id: "p-007", - fqdn: "dev-vm-mac.netbird.cloud", - ip: "100.64.0.91", - status: "disconnected", - lastHandshake: minutesAgo(2880), - latencyMs: 0, - relayed: false, - iceLocalCandidate: "host", - iceRemoteCandidate: "host", - bytesRx: 0, - bytesTx: 0, - endpointLocal: "—", - endpointRemote: "—", - }, - { - id: "p-008", - fqdn: "ci-worker-03.netbird.cloud", - ip: "100.64.0.103", - status: "connected", - lastHandshake: minutesAgo(0.5), - latencyMs: 27, - relayed: false, - iceLocalCandidate: "prflx", - iceRemoteCandidate: "srflx", - bytesRx: 1024 * 1024 * 14, - bytesTx: 1024 * 1024 * 3, - endpointLocal: "192.168.50.7:51820", - endpointRemote: "203.0.113.61:51820", - }, - { - id: "p-009", - fqdn: "k8s-control-plane.netbird.cloud", - ip: "100.64.0.110", - status: "connected", - lastHandshake: minutesAgo(2), - latencyMs: 12, - relayed: false, - iceLocalCandidate: "host", - iceRemoteCandidate: "host", - bytesRx: 1024 * 1024 * 410, - bytesTx: 1024 * 1024 * 380, - endpointLocal: "10.0.1.10:51820", - endpointRemote: "10.0.1.11:51820", - }, - { - id: "p-010", - fqdn: "k8s-worker-01.netbird.cloud", - ip: "100.64.0.111", - status: "connected", - lastHandshake: minutesAgo(2), - latencyMs: 14, - relayed: false, - iceLocalCandidate: "host", - iceRemoteCandidate: "host", - bytesRx: 1024 * 1024 * 220, - bytesTx: 1024 * 1024 * 190, - endpointLocal: "10.0.1.20:51820", - endpointRemote: "10.0.1.21:51820", - }, - { - id: "p-011", - fqdn: "k8s-worker-02.netbird.cloud", - ip: "100.64.0.112", - status: "connecting", - lastHandshake: minutesAgo(8), - latencyMs: 0, - relayed: false, - iceLocalCandidate: "srflx", - iceRemoteCandidate: "srflx", - bytesRx: 0, - bytesTx: 0, - endpointLocal: "10.0.1.22:51820", - endpointRemote: "—", - }, - { - id: "p-012", - fqdn: "monitoring-prom.netbird.cloud", - ip: "100.64.0.130", - status: "connected", - lastHandshake: minutesAgo(0.3), - latencyMs: 22, - relayed: false, - iceLocalCandidate: "host", - iceRemoteCandidate: "srflx", - bytesRx: 1024 * 1024 * 56, - bytesTx: 1024 * 1024 * 18, - endpointLocal: "10.20.0.5:51820", - endpointRemote: "203.0.113.122:51820", - }, - { - id: "p-013", - fqdn: "grafana.netbird.cloud", - ip: "100.64.0.131", - status: "connected", - lastHandshake: minutesAgo(0.4), - latencyMs: 19, - relayed: false, - iceLocalCandidate: "host", - iceRemoteCandidate: "srflx", - bytesRx: 1024 * 1024 * 32, - bytesTx: 1024 * 1024 * 8, - endpointLocal: "10.20.0.6:51820", - endpointRemote: "203.0.113.123:51820", - }, - { - id: "p-014", - fqdn: "loki-log-aggregator.netbird.cloud", - ip: "100.64.0.132", - status: "disconnected", - lastHandshake: minutesAgo(45), - latencyMs: 0, - relayed: false, - iceLocalCandidate: "host", - iceRemoteCandidate: "host", - bytesRx: 1024 * 1024 * 12, - bytesTx: 1024 * 1024 * 4, - endpointLocal: "—", - endpointRemote: "—", - }, - { - id: "p-015", - fqdn: "dave-laptop.netbird.cloud", - ip: "100.64.0.140", - status: "connected", - lastHandshake: minutesAgo(1), - latencyMs: 38, - relayed: true, - relayAddress: "rel.eu-west.netbird.io:443", - iceLocalCandidate: "relay", - iceRemoteCandidate: "relay", - bytesRx: 1024 * 720, - bytesTx: 1024 * 410, - endpointLocal: "192.168.43.21:51820", - endpointRemote: "198.51.100.88:51820", - }, - { - id: "p-016", - fqdn: "eve-iphone.netbird.cloud", - ip: "100.64.0.150", - status: "connecting", - lastHandshake: minutesAgo(20), - latencyMs: 0, - relayed: false, - iceLocalCandidate: "srflx", - iceRemoteCandidate: "host", - bytesRx: 0, - bytesTx: 0, - endpointLocal: "—", - endpointRemote: "—", - }, - { - id: "p-017", - fqdn: "frank-windows.netbird.cloud", - ip: "100.64.0.155", - status: "connected", - lastHandshake: minutesAgo(4), - latencyMs: 76, - relayed: false, - iceLocalCandidate: "srflx", - iceRemoteCandidate: "srflx", - bytesRx: 1024 * 1024 * 6, - bytesTx: 1024 * 1024 * 2, - endpointLocal: "192.168.1.55:51820", - endpointRemote: "203.0.113.200:51820", - }, - { - id: "p-018", - fqdn: "exit-frankfurt.netbird.cloud", - ip: "100.64.0.3", - status: "connected", - lastHandshake: minutesAgo(0.1), - latencyMs: 6, - relayed: false, - iceLocalCandidate: "host", - iceRemoteCandidate: "host", - bytesRx: 1024 * 1024 * 1024 * 5, - bytesTx: 1024 * 1024 * 1024 * 1, - endpointLocal: "10.10.0.5:51820", - endpointRemote: "203.0.113.150:51820", - }, - { - id: "p-019", - fqdn: "exit-singapore.netbird.cloud", - ip: "100.64.0.4", - status: "disconnected", - lastHandshake: minutesAgo(180), - latencyMs: 0, - relayed: false, - iceLocalCandidate: "host", - iceRemoteCandidate: "host", - bytesRx: 1024 * 1024 * 880, - bytesTx: 1024 * 1024 * 220, - endpointLocal: "—", - endpointRemote: "—", - }, - { - id: "p-020", - fqdn: "nas-home.netbird.cloud", - ip: "100.64.0.180", - status: "connected", - lastHandshake: minutesAgo(0.7), - latencyMs: 31, - relayed: false, - iceLocalCandidate: "srflx", - iceRemoteCandidate: "host", - bytesRx: 1024 * 1024 * 1024 * 3, - bytesTx: 1024 * 1024 * 480, - endpointLocal: "192.168.0.50:51820", - endpointRemote: "203.0.113.45:51820", - }, - { - id: "p-021", - fqdn: "raspberrypi-iot.netbird.cloud", - ip: "100.64.0.181", - status: "connected", - lastHandshake: minutesAgo(5), - latencyMs: 54, - relayed: true, - relayAddress: "rel.eu-central.netbird.io:443", - iceLocalCandidate: "relay", - iceRemoteCandidate: "host", - bytesRx: 1024 * 240, - bytesTx: 1024 * 110, - endpointLocal: "192.168.0.121:51820", - endpointRemote: "198.51.100.42:51820", - }, - { - id: "p-022", - fqdn: "staging-api.netbird.cloud", - ip: "100.64.0.200", - status: "connected", - lastHandshake: minutesAgo(0.2), - latencyMs: 16, - relayed: false, - iceLocalCandidate: "host", - iceRemoteCandidate: "host", - bytesRx: 1024 * 1024 * 92, - bytesTx: 1024 * 1024 * 140, - endpointLocal: "10.30.0.10:51820", - endpointRemote: "10.30.0.11:51820", - }, - { - id: "p-023", - fqdn: "prod-api-eu.netbird.cloud", - ip: "100.64.0.201", - status: "connected", - lastHandshake: minutesAgo(0.1), - latencyMs: 8, - relayed: false, - iceLocalCandidate: "host", - iceRemoteCandidate: "host", - bytesRx: 1024 * 1024 * 1024 * 12, - bytesTx: 1024 * 1024 * 1024 * 3, - endpointLocal: "10.40.0.10:51820", - endpointRemote: "10.40.0.11:51820", - }, - { - id: "p-024", - fqdn: "prod-api-us.netbird.cloud", - ip: "100.64.0.202", - status: "connected", - lastHandshake: minutesAgo(0.1), - latencyMs: 92, - relayed: false, - iceLocalCandidate: "srflx", - iceRemoteCandidate: "srflx", - bytesRx: 1024 * 1024 * 1024 * 8, - bytesTx: 1024 * 1024 * 1024 * 2, - endpointLocal: "10.50.0.10:51820", - endpointRemote: "203.0.113.210:51820", - }, - { - id: "p-025", - fqdn: "old-jenkins.netbird.cloud", - ip: "100.64.0.220", - status: "disconnected", - lastHandshake: minutesAgo(8640), - latencyMs: 0, - relayed: false, - iceLocalCandidate: "host", - iceRemoteCandidate: "host", - bytesRx: 0, - bytesTx: 0, - endpointLocal: "—", - endpointRemote: "—", - }, -]; diff --git a/client/ui/frontend/src/modules/peers/types.ts b/client/ui/frontend/src/modules/peers/types.ts deleted file mode 100644 index 226b77536..000000000 --- a/client/ui/frontend/src/modules/peers/types.ts +++ /dev/null @@ -1,20 +0,0 @@ -export type PeerStatus = "connected" | "connecting" | "disconnected"; - -export type IceCandidateType = "host" | "srflx" | "relay" | "prflx"; - -export type Peer = { - id: string; - fqdn: string; - ip: string; - status: PeerStatus; - lastHandshake: Date; - latencyMs: number; - relayed: boolean; - relayAddress?: string; - iceLocalCandidate: IceCandidateType; - iceRemoteCandidate: IceCandidateType; - bytesRx: number; - bytesTx: number; - endpointLocal: string; - endpointRemote: string; -};