diff --git a/client/ui-wails/frontend/Inter Font License.txt b/client/ui-wails/frontend/Inter Font License.txt deleted file mode 100644 index b525cbf3a..000000000 --- a/client/ui-wails/frontend/Inter Font License.txt +++ /dev/null @@ -1,93 +0,0 @@ -Copyright 2020 The Inter Project Authors (https://github.com/rsms/inter) - -This Font Software is licensed under the SIL Open Font License, Version 1.1. -This license is copied below, and is also available with a FAQ at: -http://scripts.sil.org/OFL - - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/client/ui-wails/frontend/index.html b/client/ui-wails/frontend/index.html index 95d7870be..f540ff29c 100644 --- a/client/ui-wails/frontend/index.html +++ b/client/ui-wails/frontend/index.html @@ -7,6 +7,6 @@
- + diff --git a/client/ui-wails/frontend/public/Inter-Medium.ttf b/client/ui-wails/frontend/public/Inter-Medium.ttf deleted file mode 100644 index a01f3777a..000000000 Binary files a/client/ui-wails/frontend/public/Inter-Medium.ttf and /dev/null differ diff --git a/client/ui-wails/frontend/public/react.svg b/client/ui-wails/frontend/public/react.svg deleted file mode 100644 index 6c87de9bb..000000000 --- a/client/ui-wails/frontend/public/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/client/ui-wails/frontend/public/style.css b/client/ui-wails/frontend/public/style.css deleted file mode 100644 index 0ba9cf5cc..000000000 --- a/client/ui-wails/frontend/public/style.css +++ /dev/null @@ -1,157 +0,0 @@ -:root { - font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", - "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", - sans-serif; - font-size: 16px; - line-height: 24px; - font-weight: 400; - color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: rgba(27, 38, 54, 1); - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - -webkit-text-size-adjust: 100%; - user-select: none; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; -} - -@font-face { - font-family: "Inter"; - font-style: normal; - font-weight: 400; - src: local(""), - url("./Inter-Medium.ttf") format("truetype"); -} - -h3 { - font-size: 3em; - line-height: 1.1; -} - -a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; -} - -a:hover { - color: #535bf2; -} - -button { - width: 60px; - height: 30px; - line-height: 30px; - border-radius: 3px; - border: none; - margin: 0 0 0 20px; - padding: 0 8px; - cursor: pointer; -} - -.result { - height: 20px; - line-height: 20px; -} - -body { - margin: 0; - display: flex; - place-items: center; - place-content: center; - min-width: 320px; - min-height: 100vh; -} - -.container { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; -} - -h1 { - font-size: 3.2em; - line-height: 1.1; -} - -#app { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; -} - -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; -} - -.logo:hover { - filter: drop-shadow(0 0 2em #e80000aa); -} - -.logo.react:hover { - filter: drop-shadow(0 0 2em #61dafbaa); -} - -.result { - height: 20px; - line-height: 20px; - margin: 1.5rem auto; - text-align: center; -} - -.footer { - margin-top: 1rem; - align-content: center; - text-align: center; -} - -@media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; - } - - a:hover { - color: #747bff; - } - - button { - background-color: #f9f9f9; - } -} - - -.input-box .btn:hover { - background-image: linear-gradient(to top, #cfd9df 0%, #e2ebf0 100%); - color: #333333; -} - -.input-box .input { - border: none; - border-radius: 3px; - outline: none; - height: 30px; - line-height: 30px; - padding: 0 10px; - color: black; - background-color: rgba(240, 240, 240, 1); - -webkit-font-smoothing: antialiased; -} - -.input-box .input:hover { - border: none; - background-color: rgba(255, 255, 255, 1); -} - -.input-box .input:focus { - border: none; - background-color: rgba(255, 255, 255, 1); -} diff --git a/client/ui-wails/frontend/public/wails.png b/client/ui-wails/frontend/public/wails.png deleted file mode 100644 index 8bdf42483..000000000 Binary files a/client/ui-wails/frontend/public/wails.png and /dev/null differ diff --git a/client/ui-wails/frontend/src/App.tsx b/client/ui-wails/frontend/src/App.tsx index 6d8303ca5..332b23cb9 100644 --- a/client/ui-wails/frontend/src/App.tsx +++ b/client/ui-wails/frontend/src/App.tsx @@ -1,32 +1,35 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import "./globals.css"; import { HashRouter, Navigate, Route, Routes } from "react-router-dom"; -import Layout from "./Layout"; -import Status from "./pages/Status"; -import Settings from "./pages/Settings"; -import Networks from "./pages/Networks"; -import Peers from "./pages/Peers"; -import Profiles from "./pages/Profiles"; -import Debug from "./pages/Debug"; -import Update from "./pages/Update"; -import QuickActions from "./pages/QuickActions"; -import LoginUrl from "./pages/LoginUrl"; +import QuickActions from "@/screens/QuickActions.tsx"; +import LoginUrl from "@/screens/LoginUrl.tsx"; +import Update from "@/screens/Update.tsx"; +import Layout from "@/layout.tsx"; +import Peers from "@/screens/Peers.tsx"; +import Networks from "@/screens/Networks.tsx"; +import Profiles from "@/screens/Profiles.tsx"; +import Settings from "@/screens/Settings.tsx"; +import Debug from "@/screens/Debug.tsx"; +import {Main} from "@/screens/Main.tsx"; -export default function App() { - return ( - - - } /> - } /> - } /> - }> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - - - ); -} +ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( + + + + } /> + } /> + } /> + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + + , +); diff --git a/client/ui-wails/frontend/src/Layout.tsx b/client/ui-wails/frontend/src/Layout.tsx index 46349571b..c0f5a67d1 100644 --- a/client/ui-wails/frontend/src/Layout.tsx +++ b/client/ui-wails/frontend/src/Layout.tsx @@ -1,45 +1,15 @@ -import { NavLink, Outlet } from "react-router-dom"; -import { Activity, Bug, Network, Settings as SettingsIcon, Share2, Users } from "lucide-react"; -import { cn } from "./lib/cn"; - -const nav = [ - { to: "/", label: "Status", icon: Activity, end: true }, - { to: "/peers", label: "Peers", icon: Share2 }, - { to: "/networks", label: "Networks", icon: Network }, - { to: "/profiles", label: "Profiles", icon: Users }, - { to: "/settings", label: "Settings", icon: SettingsIcon }, - { to: "/debug", label: "Debug", icon: Bug }, -]; +import { Outlet } from "react-router-dom"; +import PlaceholderHeader from "@/components/PlaceholderHeader"; export default function Layout() { return ( -
- -
- -
+
+ +
+
+ +
+
); } diff --git a/client/ui-wails/frontend/src/assets/netbird-full.svg b/client/ui-wails/frontend/src/assets/netbird-full.svg deleted file mode 100644 index f925d5761..000000000 --- a/client/ui-wails/frontend/src/assets/netbird-full.svg +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/client/ui-wails/frontend/src/assets/netbird.svg b/client/ui-wails/frontend/src/assets/netbird.svg deleted file mode 100644 index 6254931c6..000000000 --- a/client/ui-wails/frontend/src/assets/netbird.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/client/ui-wails/frontend/src/components/NetBirdConnectToggle.tsx b/client/ui-wails/frontend/src/components/NetBirdConnectToggle.tsx index 9a1f2ace5..d5bc83b0b 100644 --- a/client/ui-wails/frontend/src/components/NetBirdConnectToggle.tsx +++ b/client/ui-wails/frontend/src/components/NetBirdConnectToggle.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from "react"; import { motion } from "framer-motion"; import { cn } from "@/lib/cn"; -import netbirdLogo from "@/assets/netbird.svg"; +import netbirdLogo from "@/assets/logos/netbird.svg"; export enum ConnectionState { Disconnected = "disconnected", @@ -16,13 +16,13 @@ type StateProps = { type NetBirdConnectToggleProps = { state: ConnectionState; + size?: number; onClick?: () => void; }; -export const NetBirdConnectToggle = ({ state, onClick }: NetBirdConnectToggleProps) => { +export const NetBirdConnectToggle = ({ state, size = 140, onClick }: NetBirdConnectToggleProps) => { const [visualState, setVisualState] = useState(state); - // Sync with external state when it reaches a settled value useEffect(() => { setVisualState(state); }, [state]); @@ -36,20 +36,30 @@ export const NetBirdConnectToggle = ({ state, onClick }: NetBirdConnectTogglePro onClick?.(); }; + const padding = size * 0.075; + const borderGap = 2; + const borderInset = padding - borderGap; + const innerSize = size * 0.7; + const logoSize = size * 0.26; + const pingInset = size * 0.075; + return ( - - - - - - - - +
+ + + + + + + + +
); }; @@ -67,7 +77,7 @@ const OuterRing = ({ state }: StateProps) => { ); }; -const BorderInnerRing = ({ state }: StateProps) => ( +const BorderInnerRing = ({ state, inset }: StateProps & { inset: number }) => (
( state === ConnectionState.Disconnecting && "bg-conic-netbird animate-spin-slow", state !== ConnectionState.Connected && state !== ConnectionState.Disconnecting && "bg-neutral-500", )} - style={{ inset: "12px" }} + style={{ inset }} /> ); -const InnerRing = ({ children }: { children: React.ReactNode }) => ( +const InnerRing = ({ children, size }: { children: React.ReactNode; size: number }) => (
{children}
); -const NetBirdLogo = ({ state }: StateProps) => { +const NetBirdLogo = ({ state, logoSize }: StateProps & { logoSize: number }) => { const isConnecting = state === ConnectionState.Connecting; return ( @@ -98,7 +109,7 @@ const NetBirdLogo = ({ state }: StateProps) => { NetBird { ); }; -const PingRing = ({ state }: StateProps) => ( +const PingRing = ({ state, inset }: StateProps & { inset: number }) => ( ); diff --git a/client/ui-wails/frontend/src/index.css b/client/ui-wails/frontend/src/index.css deleted file mode 100644 index d6c7d11bb..000000000 --- a/client/ui-wails/frontend/src/index.css +++ /dev/null @@ -1,17 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - -html, -body, -#root { - height: 100%; -} - -body { - @apply bg-white text-nb-gray-900 antialiased; -} - -.dark body { - @apply bg-nb-gray-950 text-nb-gray-50; -} diff --git a/client/ui-wails/frontend/src/main.tsx b/client/ui-wails/frontend/src/main.tsx deleted file mode 100644 index 8b1ddb971..000000000 --- a/client/ui-wails/frontend/src/main.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import React from "react"; -import ReactDOM from "react-dom/client"; -import App from "./App"; -import "./index.css"; - -ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( - - - , -); diff --git a/client/ui-wails/frontend/src/pages/Debug.tsx b/client/ui-wails/frontend/src/pages/Debug.tsx deleted file mode 100644 index 929e4325f..000000000 --- a/client/ui-wails/frontend/src/pages/Debug.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import { useState } from "react"; -import { Debug as DebugSvc } from "../../bindings/github.com/netbirdio/netbird/client/ui-wails/services"; -import type { DebugBundleResult } from "../../bindings/github.com/netbirdio/netbird/client/ui-wails/services/models.js"; -import { Button } from "../components/Button"; -import { Input } from "../components/Input"; -import { Switch } from "../components/Switch"; -import { Card } from "../components/Card"; - -export default function Debug() { - const [anonymize, setAnonymize] = useState(true); - const [systemInfo, setSystemInfo] = useState(true); - const [upload, setUpload] = useState(false); - const [uploadUrl, setUploadUrl] = useState(""); - const [logFiles, setLogFiles] = useState(0); - - const [running, setRunning] = useState(false); - const [result, setResult] = useState(null); - const [error, setError] = useState(null); - - const run = async () => { - setRunning(true); - setResult(null); - setError(null); - try { - const r = await DebugSvc.Bundle({ - anonymize, - systemInfo, - uploadUrl: upload ? uploadUrl : "", - logFileCount: logFiles, - }); - setResult(r); - } catch (e) { - setError(String(e)); - } finally { - setRunning(false); - } - }; - - return ( -
-

Debug bundle

- - - - - - {upload && ( - setUploadUrl(e.target.value)} - /> - )} - setLogFiles(Number(e.target.value))} - /> -
- -
-
- - {error &&

{error}

} - - {result && ( - - {result.path && ( -

- Path:{" "} - {result.path} -

- )} - {result.uploadedKey && ( -

- Uploaded key:{" "} - {result.uploadedKey} -

- )} - {result.uploadFailureReason && ( -

- Upload failed: {result.uploadFailureReason} -

- )} -
- )} -
- ); -} diff --git a/client/ui-wails/frontend/src/pages/LoginUrl.tsx b/client/ui-wails/frontend/src/pages/LoginUrl.tsx deleted file mode 100644 index 71c8e88a1..000000000 --- a/client/ui-wails/frontend/src/pages/LoginUrl.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { useEffect, useState } from "react"; -import { ExternalLink } from "lucide-react"; -import { Button } from "../components/Button"; - -export default function LoginUrl() { - const [url, setUrl] = useState(""); - - useEffect(() => { - const params = new URLSearchParams(window.location.hash.split("?")[1] ?? ""); - setUrl(params.get("url") ?? ""); - }, []); - - if (!url) { - return ( -
- No login URL provided. -
- ); - } - - return ( -
-

Continue in your browser

-

- Open the following URL to finish signing in. -

- -

{url}

-
- ); -} diff --git a/client/ui-wails/frontend/src/pages/Networks.tsx b/client/ui-wails/frontend/src/pages/Networks.tsx deleted file mode 100644 index ea3bd055e..000000000 --- a/client/ui-wails/frontend/src/pages/Networks.tsx +++ /dev/null @@ -1,159 +0,0 @@ -import { useCallback, useEffect, useMemo, useState } from "react"; -import { RefreshCw } from "lucide-react"; -import { Networks as NetworksSvc } from "../../bindings/github.com/netbirdio/netbird/client/ui-wails/services"; -import type { Network } from "../../bindings/github.com/netbirdio/netbird/client/ui-wails/services/models.js"; -import { Button } from "../components/Button"; -import { Tabs } from "../components/Tabs"; - -export default function Networks() { - const [routes, setRoutes] = useState([]); - const [error, setError] = useState(null); - const [loading, setLoading] = useState(false); - - const refresh = useCallback(async () => { - setLoading(true); - try { - const list = await NetworksSvc.List(); - setRoutes(list); - setError(null); - } catch (e) { - setError(String(e)); - } finally { - setLoading(false); - } - }, []); - - useEffect(() => { - refresh(); - }, [refresh]); - - const toggle = async (id: string, selected: boolean) => { - try { - if (selected) { - await NetworksSvc.Deselect({ networkIds: [id], append: false, all: false }); - } else { - await NetworksSvc.Select({ networkIds: [id], append: true, all: false }); - } - await refresh(); - } catch (e) { - setError(String(e)); - } - }; - - const setAll = async (ids: string[], on: boolean) => { - try { - if (on) { - await NetworksSvc.Select({ networkIds: ids, append: false, all: true }); - } else { - await NetworksSvc.Deselect({ networkIds: ids, append: false, all: true }); - } - await refresh(); - } catch (e) { - setError(String(e)); - } - }; - - const overlapping = useMemo(() => filterOverlapping(routes), [routes]); - const exitNodes = useMemo(() => routes.filter((r) => r.range === "0.0.0.0/0"), [routes]); - - return ( -
-
-

Networks

- -
- - {error && ( -

{error}

- )} - -
- , - }, - { - value: "overlap", - label: `Overlapping (${overlapping.length})`, - content: , - }, - { - value: "exit", - label: `Exit-node (${exitNodes.length})`, - content: , - }, - ]} - /> -
-
- ); -} - -function NetworkList({ - routes, - onToggle, - onSetAll, -}: { - routes: Network[]; - onToggle: (id: string, selected: boolean) => void; - onSetAll: (ids: string[], on: boolean) => void; -}) { - if (routes.length === 0) { - return

No networks.

; - } - const ids = routes.map((r) => r.id); - return ( -
-
- - -
-
    - {routes.map((r) => ( -
  • - onToggle(r.id, r.selected)} - className="mt-1 h-4 w-4 accent-netbird" - /> -
    -

    {r.id}

    -

    {r.range}

    - {r.domains.length > 0 && ( -

    - {r.domains.join(", ")} -

    - )} -
    -
  • - ))} -
-
- ); -} - -function filterOverlapping(routes: Network[]): Network[] { - const byRange = new Map(); - for (const r of routes) { - if (r.domains.length > 0) continue; - const arr = byRange.get(r.range) ?? []; - arr.push(r); - byRange.set(r.range, arr); - } - const out: Network[] = []; - for (const arr of byRange.values()) { - if (arr.length > 1) out.push(...arr); - } - return out; -} diff --git a/client/ui-wails/frontend/src/pages/Peers.tsx b/client/ui-wails/frontend/src/pages/Peers.tsx deleted file mode 100644 index f1522ca87..000000000 --- a/client/ui-wails/frontend/src/pages/Peers.tsx +++ /dev/null @@ -1,211 +0,0 @@ -import { useMemo, useState } from "react"; -import { ChevronDown, ChevronRight, Network, ShieldCheck, Zap } from "lucide-react"; -import { useStatus } from "../hooks/useStatus"; -import type { PeerStatus } from "../../bindings/github.com/netbirdio/netbird/client/ui-wails/services/models.js"; -import { Card } from "../components/Card"; -import { Input } from "../components/Input"; -import { cn } from "../lib/cn"; - -export default function Peers() { - const { status } = useStatus(); - const [filter, setFilter] = useState(""); - const [expanded, setExpanded] = useState(null); - - const peers = useMemo(() => { - const all = status?.peers ?? []; - if (!filter.trim()) return all; - const q = filter.trim().toLowerCase(); - return all.filter( - (p) => - p.fqdn.toLowerCase().includes(q) || - p.ip.toLowerCase().includes(q) || - p.networks.some((n) => n.toLowerCase().includes(q)), - ); - }, [status?.peers, filter]); - - return ( -
-
-

- Peers - - {status?.peers?.length ?? 0} - -

-
- setFilter(e.target.value)} - /> -
-
- - {peers.length === 0 ? ( - - {status?.peers?.length === 0 - ? "No peers visible from this client." - : "No peers match the filter."} - - ) : ( -
    - {peers.map((p) => ( - setExpanded(expanded === p.pubKey ? null : p.pubKey)} - /> - ))} -
- )} -
- ); -} - -function PeerRow({ - peer, - expanded, - onToggle, -}: { - peer: PeerStatus; - expanded: boolean; - onToggle: () => void; -}) { - return ( -
  • - - - {expanded && } -
  • - ); -} - -function PeerDetails({ peer }: { peer: PeerStatus }) { - return ( -
    - - - - - - - {peer.relayed && ( - - )} - {peer.networks.length > 0 && ( - - )} -
    - ); -} - -function Detail({ label, value, mono }: { label: string; value: string; mono?: boolean }) { - return ( -
    - {label} - - {value} - -
    - ); -} - -function ChevronIcon({ expanded }: { expanded: boolean }) { - const Icon = expanded ? ChevronDown : ChevronRight; - return ; -} - -function StateBadge({ state }: { state: string }) { - const cls = "h-2 w-2 rounded-full shrink-0"; - switch (state) { - case "Connected": - return ; - case "Connecting": - return ; - case "Idle": - return ; - default: - return ; - } -} - -function RouteIcon({ relayed, connected }: { relayed: boolean; connected: boolean }) { - if (!connected) { - return ; - } - if (relayed) { - return ( - - Relayed - - ); - } - return ( - - P2P - - ); -} - -function fmtBytes(n: number): string { - if (n < 1024) return `${n} B`; - if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`; - if (n < 1024 * 1024 * 1024) return `${(n / 1024 / 1024).toFixed(1)} MB`; - return `${(n / 1024 / 1024 / 1024).toFixed(2)} GB`; -} - -function fmtRelative(unixSec: number): string { - if (!unixSec) return "—"; - const ageSec = Math.max(0, Math.floor(Date.now() / 1000) - unixSec); - if (ageSec < 60) return `${ageSec}s ago`; - if (ageSec < 3600) return `${Math.floor(ageSec / 60)}m ago`; - if (ageSec < 86400) return `${Math.floor(ageSec / 3600)}h ago`; - return `${Math.floor(ageSec / 86400)}d ago`; -} diff --git a/client/ui-wails/frontend/src/pages/Profiles.tsx b/client/ui-wails/frontend/src/pages/Profiles.tsx deleted file mode 100644 index 3a1035afa..000000000 --- a/client/ui-wails/frontend/src/pages/Profiles.tsx +++ /dev/null @@ -1,173 +0,0 @@ -import { FormEvent, useCallback, useEffect, useState } from "react"; -import { Plus, RefreshCw } from "lucide-react"; -import { - Profiles as ProfilesSvc, - Connection, -} from "../../bindings/github.com/netbirdio/netbird/client/ui-wails/services"; -import type { Profile } from "../../bindings/github.com/netbirdio/netbird/client/ui-wails/services/models.js"; -import { Button } from "../components/Button"; -import { Input } from "../components/Input"; -import { Card } from "../components/Card"; - -export default function Profiles() { - const [username, setUsername] = useState(""); - const [profiles, setProfiles] = useState([]); - const [error, setError] = useState(null); - const [adding, setAdding] = useState(false); - - const refresh = useCallback(async () => { - try { - const u = username || (await ProfilesSvc.Username()); - if (!username) setUsername(u); - const list = await ProfilesSvc.List(u); - setProfiles(list); - setError(null); - } catch (e) { - setError(String(e)); - } - }, [username]); - - useEffect(() => { - refresh(); - }, [refresh]); - - const select = async (name: string) => { - try { - await ProfilesSvc.Switch({ profileName: name, username }); - await Connection.Up({ profileName: name, username }); - await refresh(); - } catch (e) { - setError(String(e)); - } - }; - - const deregister = async (name: string) => { - try { - await Connection.Logout({ profileName: name, username }); - await refresh(); - } catch (e) { - setError(String(e)); - } - }; - - const remove = async (name: string) => { - if (name === "default") return; - try { - await ProfilesSvc.Remove({ profileName: name, username }); - await refresh(); - } catch (e) { - setError(String(e)); - } - }; - - return ( -
    -
    -

    Profiles

    -
    - - -
    -
    - - {error &&

    {error}

    } - -
    - {profiles.map((p) => ( - - select(p.name)} - className="h-4 w-4 accent-netbird" - /> -
    -

    {p.name}

    - {p.isActive &&

    Active

    } -
    - - -
    - ))} - {profiles.length === 0 && ( -

    No profiles.

    - )} -
    - - {adding && ( - setAdding(false)} - onAdded={async () => { - setAdding(false); - await refresh(); - }} - /> - )} -
    - ); -} - -function AddDialog({ - username, - onClose, - onAdded, -}: { - username: string; - onClose: () => void; - onAdded: () => void; -}) { - const [name, setName] = useState(""); - const [err, setErr] = useState(null); - - const submit = async (e: FormEvent) => { - e.preventDefault(); - if (!name.trim()) return; - try { - await ProfilesSvc.Add({ profileName: name.trim(), username }); - onAdded(); - } catch (e) { - setErr(String(e)); - } - }; - - return ( -
    -
    -

    New profile

    - setName(e.target.value)} - /> - {err &&

    {err}

    } -
    - - -
    -
    -
    - ); -} diff --git a/client/ui-wails/frontend/src/pages/QuickActions.tsx b/client/ui-wails/frontend/src/pages/QuickActions.tsx deleted file mode 100644 index 749b4bf8a..000000000 --- a/client/ui-wails/frontend/src/pages/QuickActions.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { CheckCircle2, Circle, Loader2, Power } from "lucide-react"; -import { useStatus } from "../hooks/useStatus"; -import { Connection } from "../../bindings/github.com/netbirdio/netbird/client/ui-wails/services"; -import { Button } from "../components/Button"; -import { cn } from "../lib/cn"; - -export default function QuickActions() { - const { status } = useStatus(); - const state = status?.status ?? "Disconnected"; - const connected = state === "Connected"; - const connecting = state === "Connecting"; - - return ( -
    - -

    {state}

    - {connected ? ( - - ) : ( - - )} -
    - ); -} - -function Icon({ state }: { state: string }) { - const cls = "h-12 w-12"; - switch (state) { - case "Connected": - return ; - case "Connecting": - return ; - default: - return ; - } -} diff --git a/client/ui-wails/frontend/src/pages/Settings.tsx b/client/ui-wails/frontend/src/pages/Settings.tsx deleted file mode 100644 index 3781611b6..000000000 --- a/client/ui-wails/frontend/src/pages/Settings.tsx +++ /dev/null @@ -1,240 +0,0 @@ -import { useCallback, useEffect, useState } from "react"; -import { - Settings as SettingsSvc, - Profiles as ProfilesSvc, -} from "../../bindings/github.com/netbirdio/netbird/client/ui-wails/services"; -import type { Config } from "../../bindings/github.com/netbirdio/netbird/client/ui-wails/services/models.js"; -import { Button } from "../components/Button"; -import { Input } from "../components/Input"; -import { Switch } from "../components/Switch"; -import { Tabs } from "../components/Tabs"; - -interface Ctx { - cfg: Config; - setField: (k: K, v: Config[K]) => void; -} - -export default function Settings() { - const [username, setUsername] = useState(""); - const [profile, setProfile] = useState(""); - const [cfg, setCfg] = useState(null); - const [error, setError] = useState(null); - const [saving, setSaving] = useState(false); - - const load = useCallback(async () => { - try { - const u = await ProfilesSvc.Username(); - const active = await ProfilesSvc.GetActive(); - const profileName = active.profileName || "default"; - setUsername(u); - setProfile(profileName); - const c = await SettingsSvc.GetConfig({ profileName, username: u }); - setCfg(c); - setError(null); - } catch (e) { - setError(String(e)); - } - }, []); - - useEffect(() => { - load(); - }, [load]); - - const setField: Ctx["setField"] = (k, v) => { - setCfg((c) => (c ? { ...c, [k]: v } : c)); - }; - - const save = async () => { - if (!cfg) return; - setSaving(true); - try { - await SettingsSvc.SetConfig({ - profileName: profile, - username, - managementUrl: cfg.managementUrl, - adminUrl: cfg.adminUrl, - interfaceName: cfg.interfaceName, - wireguardPort: cfg.wireguardPort, - mtu: cfg.mtu, - preSharedKey: cfg.preSharedKey, - disableAutoConnect: cfg.disableAutoConnect, - serverSshAllowed: cfg.serverSshAllowed, - rosenpassEnabled: cfg.rosenpassEnabled, - rosenpassPermissive: cfg.rosenpassPermissive, - disableNotifications: cfg.disableNotifications, - lazyConnectionEnabled: cfg.lazyConnectionEnabled, - blockInbound: cfg.blockInbound, - networkMonitor: cfg.networkMonitor, - disableClientRoutes: cfg.disableClientRoutes, - disableServerRoutes: cfg.disableServerRoutes, - disableDns: cfg.disableDns, - blockLanAccess: cfg.blockLanAccess, - enableSshRoot: cfg.enableSshRoot, - enableSshSftp: cfg.enableSshSftp, - enableSshLocalPortForwarding: cfg.enableSshLocalPortForwarding, - enableSshRemotePortForwarding: cfg.enableSshRemotePortForwarding, - disableSshAuth: cfg.disableSshAuth, - sshJwtCacheTtl: cfg.sshJwtCacheTtl, - }); - setError(null); - } catch (e) { - setError(String(e)); - } finally { - setSaving(false); - } - }; - - if (!cfg) { - return
    Loading…
    ; - } - - const ctx: Ctx = { cfg, setField }; - - return ( -
    -
    -

    Settings

    - -
    - {error &&

    {error}

    } -
    - }, - { value: "net", label: "Network", content: }, - { value: "ssh", label: "SSH", content: }, - ]} - /> -
    -
    - ); -} - -function ConnectionTab({ cfg, setField }: Ctx) { - return ( -
    - setField("managementUrl", e.target.value)} - /> - setField("preSharedKey", e.target.value)} - /> - setField("interfaceName", e.target.value)} - /> -
    - setField("wireguardPort", Number(e.target.value))} - /> - setField("mtu", Number(e.target.value))} - /> -
    - setField("rosenpassEnabled", v)} - label="Rosenpass (post-quantum)" - /> - setField("rosenpassPermissive", v)} - label="Rosenpass permissive mode" - /> -
    - ); -} - -function NetworkTab({ cfg, setField }: Ctx) { - return ( -
    - setField("networkMonitor", v)} - label="Network monitor" - /> - setField("disableDns", v)} - label="Disable DNS" - /> - setField("disableClientRoutes", v)} - label="Disable client routes" - /> - setField("disableServerRoutes", v)} - label="Disable server routes" - /> - setField("blockLanAccess", v)} - label="Block LAN access" - /> - setField("blockInbound", v)} - label="Block inbound connections" - /> -
    - ); -} - -function SSHTab({ cfg, setField }: Ctx) { - return ( -
    - setField("serverSshAllowed", v)} - label="Server SSH allowed" - /> - setField("enableSshRoot", v)} - label="SSH root login" - /> - setField("enableSshSftp", v)} - label="SFTP" - /> - setField("enableSshLocalPortForwarding", v)} - label="Local port forwarding" - /> - setField("enableSshRemotePortForwarding", v)} - label="Remote port forwarding" - /> - setField("disableSshAuth", v)} - label="Disable SSH auth" - /> - setField("sshJwtCacheTtl", Number(e.target.value))} - /> -
    - ); -} diff --git a/client/ui-wails/frontend/src/pages/Status.tsx b/client/ui-wails/frontend/src/pages/Status.tsx deleted file mode 100644 index a666f755c..000000000 --- a/client/ui-wails/frontend/src/pages/Status.tsx +++ /dev/null @@ -1,172 +0,0 @@ -import { CheckCircle2, Circle, Loader2, AlertTriangle, Power } from "lucide-react"; -import { useStatus } from "../hooks/useStatus"; -import { Connection } from "../../bindings/github.com/netbirdio/netbird/client/ui-wails/services"; -import type { SystemEvent } from "../../bindings/github.com/netbirdio/netbird/client/ui-wails/services/models.js"; -import { Button } from "../components/Button"; -import { Card } from "../components/Card"; -import { cn } from "../lib/cn"; -import { NetBirdConnectToggle, ConnectionState } from "../components/NetBirdConnectToggle"; - -export default function Status() { - const { status, error } = useStatus(); - - const connState = status?.status ?? "Disconnected"; - const connected = connState === "Connected"; - const connecting = connState === "Connecting"; - - const toggleState: ConnectionState = - connected ? ConnectionState.Connected - : connecting ? ConnectionState.Connecting - : ConnectionState.Disconnected; - - const connect = () => Connection.Up({ profileName: "", username: "" }).catch(console.error); - const disconnect = () => Connection.Down().catch(console.error); - const toggleConnection = () => (connected ? disconnect() : connect()); - - return ( -
    -
    -
    - -
    -

    {connState}

    -

    - {status?.local.fqdn || "—"} -

    -
    -
    -
    - - -
    -
    - - {error && ( -
    - - {error} -
    - )} - -
    - - - - -
    - - -

    - Recent events -

    - {(() => { - const events = dedupEvents(status?.events ?? []).slice(0, 8); - if (events.length === 0) { - return

    No recent events.

    ; - } - return ( -
      - {events.map((e, i) => ( -
    • - - {e.severity} - - - {e.userMessage || e.message} - -
    • - ))} -
    - ); - })()} -
    - -
    - -
    -
    - ); -} - -function StateIcon({ state }: { state: string }) { - const cls = "h-7 w-7"; - switch (state) { - case "Connected": - return ; - case "Connecting": - return ; - case "Error": - return ; - default: - return ; - } -} - -function InfoCard({ label, value }: { label: string; value: string }) { - return ( - -

    {label}

    -

    {value}

    -
    - ); -} - -// dedupEvents collapses repeated daemon events that carry the same logical -// content. The daemon emits one "new_version_available" event per check tick, -// so its 10-event ring buffer fills with duplicates after a quiet hour. Same -// goes for periodic "DNS unreachable" or "auth retry" events. We key by -// message + a small set of identity-bearing metadata fields and keep the -// newest occurrence (the events array is already in publish order). -function dedupEvents(events: SystemEvent[]): SystemEvent[] { - const seen = new Set(); - const out: SystemEvent[] = []; - for (let i = events.length - 1; i >= 0; i--) { - const e = events[i]; - const md = e.metadata ?? {}; - const key = [ - e.severity, - e.category, - e.userMessage || e.message, - md["new_version_available"] ?? "", - md["enforced"] ?? "", - ].join("|"); - // eslint-disable-next-line no-console - console.log("[dedup]", { key, event: e }); - if (seen.has(key)) continue; - seen.add(key); - out.unshift(e); - } - return out; -} - -function LinkCard({ - label, - link, -}: { - label: string; - link?: { url: string; connected: boolean; error?: string }; -}) { - return ( - -
    -

    {label}

    - -
    -

    - {link?.url || "—"} -

    - {link?.error && ( -

    {link.error}

    - )} -
    - ); -} diff --git a/client/ui-wails/frontend/src/pages/Update.tsx b/client/ui-wails/frontend/src/pages/Update.tsx deleted file mode 100644 index 04d9eb245..000000000 --- a/client/ui-wails/frontend/src/pages/Update.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { useEffect, useState } from "react"; -import { Loader2 } from "lucide-react"; -import { Update as UpdateSvc } from "../../bindings/github.com/netbirdio/netbird/client/ui-wails/services"; - -const TIMEOUT_MS = 15 * 60 * 1000; - -export default function Update() { - const [done, setDone] = useState(false); - const [error, setError] = useState(null); - - useEffect(() => { - let cancelled = false; - UpdateSvc.Trigger().catch((e) => !cancelled && setError(String(e))); - - const start = Date.now(); - const timer = setInterval(async () => { - if (Date.now() - start > TIMEOUT_MS) { - setError("Update timed out."); - clearInterval(timer); - return; - } - try { - const r = await UpdateSvc.GetInstallerResult(); - if (r.success) { - setDone(true); - clearInterval(timer); - } else if (r.errorMsg) { - setError(r.errorMsg); - clearInterval(timer); - } - } catch { - // installer not finished yet - } - }, 2000); - - return () => { - cancelled = true; - clearInterval(timer); - }; - }, []); - - return ( -
    -
    - {done ? ( -

    Update complete

    - ) : error ? ( -

    {error}

    - ) : ( - <> - -

    Updating…

    -

    - Please don't close this window. -

    - - )} -
    -
    - ); -} diff --git a/client/ui-wails/frontend/src/screens/Main.tsx b/client/ui-wails/frontend/src/screens/Main.tsx new file mode 100644 index 000000000..87c6e9b25 --- /dev/null +++ b/client/ui-wails/frontend/src/screens/Main.tsx @@ -0,0 +1,78 @@ +import {ConnectionState, NetBirdConnectToggle} from "@/components/NetBirdConnectToggle.tsx"; +import { + BoltIcon, + ChevronDown, + CircleUserRound, + Layers3Icon, + MonitorSmartphoneIcon, SettingsIcon, + SquareArrowUpRight +} from "lucide-react"; + +type Props = { + +}; +export const Main = ({}: Props) => { + return ( +
    +
    +
    +
    +
    +
    + D +
    +
    + Default + eduard@netbird.io +
    + + +
    +
    +
    +
    + +
    +
    +
    + +

    Connected

    +

    peer-hostname.netbird.cloud

    +

    192.168.0.1

    + +
    +
    + +
    +
    + + ); +}; diff --git a/client/ui-wails/frontend/tailwind.config.ts b/client/ui-wails/frontend/tailwind.config.ts index 568432205..78dcc3a07 100644 --- a/client/ui-wails/frontend/tailwind.config.ts +++ b/client/ui-wails/frontend/tailwind.config.ts @@ -4,6 +4,10 @@ const config: Config = { content: ["./index.html", "./src/**/*.{ts,tsx}"], darkMode: "class", theme: { + fontFamily: { + sans: ['"Inter Variable"', 'ui-sans-serif', 'system-ui', 'sans-serif'], + mono: ['"JetBrains Mono Variable"', 'ui-monospace', 'monospace'], + }, extend: { colors: { "nb-gray": {