mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-28 17:49:08 +02:00
wip
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { Outlet } from "react-router-dom";
|
||||
import { Header } from "@/layouts/Header.tsx";
|
||||
import { AppearanceProvider } from "@/modules/appearance/AppearanceContext.tsx";
|
||||
import { ClientVersionProvider } from "@/modules/auto-update/ClientVersionContext.tsx";
|
||||
import { DebugBundleProvider } from "@/modules/debug-bundle/DebugBundleContext.tsx";
|
||||
import { ProfileProvider } from "@/modules/profile/ProfileContext.tsx";
|
||||
@@ -7,14 +8,16 @@ import { ProfileProvider } from "@/modules/profile/ProfileContext.tsx";
|
||||
export const AppLayout = () => {
|
||||
return (
|
||||
<div className={"relative flex h-full flex-col"}>
|
||||
<ProfileProvider>
|
||||
<DebugBundleProvider>
|
||||
<ClientVersionProvider>
|
||||
<Header />
|
||||
<Outlet />
|
||||
</ClientVersionProvider>
|
||||
</DebugBundleProvider>
|
||||
</ProfileProvider>
|
||||
<AppearanceProvider>
|
||||
<ProfileProvider>
|
||||
<DebugBundleProvider>
|
||||
<ClientVersionProvider>
|
||||
<Header />
|
||||
<Outlet />
|
||||
</ClientVersionProvider>
|
||||
</DebugBundleProvider>
|
||||
</ProfileProvider>
|
||||
</AppearanceProvider>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,25 +1,117 @@
|
||||
import {
|
||||
ConnectionState,
|
||||
NetBirdConnectToggle,
|
||||
} from "@/components/NetBirdConnectToggle.tsx";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { ConnectionState, NetBirdConnectToggle } from "@/components/NetBirdConnectToggle.tsx";
|
||||
import Button from "@/components/Button.tsx";
|
||||
import { cn } from "@/lib/cn.ts";
|
||||
|
||||
const CONNECT_DURATION_MS = 1500;
|
||||
const DISCONNECT_DURATION_MS = 800;
|
||||
|
||||
const STATUS_LABEL: Record<ConnectionState, string> = {
|
||||
[ConnectionState.Disconnected]: "Disconnected",
|
||||
[ConnectionState.Connecting]: "Connecting...",
|
||||
[ConnectionState.Connected]: "Connected",
|
||||
[ConnectionState.Disconnecting]: "Disconnecting...",
|
||||
};
|
||||
|
||||
export const ConnectionStatus = () => {
|
||||
const [state, setState] = useState<ConnectionState>(ConnectionState.Disconnected);
|
||||
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
if (timerRef.current) clearTimeout(timerRef.current);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const transition = (next: ConnectionState, after: ConnectionState, delay: number) => {
|
||||
if (timerRef.current) clearTimeout(timerRef.current);
|
||||
setState(next);
|
||||
timerRef.current = setTimeout(() => {
|
||||
setState(after);
|
||||
timerRef.current = null;
|
||||
}, delay);
|
||||
};
|
||||
|
||||
const connect = () =>
|
||||
transition(ConnectionState.Connecting, ConnectionState.Connected, CONNECT_DURATION_MS);
|
||||
const disconnect = () =>
|
||||
transition(
|
||||
ConnectionState.Disconnecting,
|
||||
ConnectionState.Disconnected,
|
||||
DISCONNECT_DURATION_MS,
|
||||
);
|
||||
|
||||
const handleToggleClick = () => {
|
||||
if (state === ConnectionState.Disconnected) connect();
|
||||
else if (state === ConnectionState.Connected) disconnect();
|
||||
};
|
||||
|
||||
const handleButtonClick = () => {
|
||||
if (state === ConnectionState.Disconnected) {
|
||||
connect();
|
||||
return;
|
||||
}
|
||||
if (state === ConnectionState.Connected) {
|
||||
if (timerRef.current) {
|
||||
clearTimeout(timerRef.current);
|
||||
timerRef.current = null;
|
||||
}
|
||||
setState(ConnectionState.Disconnected);
|
||||
}
|
||||
};
|
||||
|
||||
const isTransitioning =
|
||||
state === ConnectionState.Connecting || state === ConnectionState.Disconnecting;
|
||||
const isConnectedSide =
|
||||
state === ConnectionState.Connected || state === ConnectionState.Disconnecting;
|
||||
|
||||
const buttonLabel = isConnectedSide ? "Disconnect" : "Connect";
|
||||
const buttonVariant = isConnectedSide ? "secondary" : "primary";
|
||||
|
||||
return (
|
||||
<div className={"flex flex-col h-full items-center justify-center"}>
|
||||
<NetBirdConnectToggle state={ConnectionState.Connected} />
|
||||
<h1
|
||||
className={
|
||||
"text-base font-medium mt-8 text-nb-gray-200 tracking-wide"
|
||||
}
|
||||
>
|
||||
Connected
|
||||
</h1>
|
||||
<p className={"font-mono text-xs text-nb-gray-300 mt-1"}>
|
||||
peer-hostname.netbird.cloud
|
||||
</p>
|
||||
<p className={"font-mono text-xs text-nb-gray-300 mt-0.5"}>
|
||||
192.168.0.1
|
||||
</p>
|
||||
<div className={cn("flex flex-col h-full w-full items-center justify-between", "-mt-4")}>
|
||||
<div className={"w-full h-full flex flex-col items-center justify-center"}>
|
||||
<div className={"flex flex-col items-center justify-center"}>
|
||||
<p
|
||||
className={
|
||||
"font-mono text-xs text-nb-gray-300 transition-opacity duration-300 " +
|
||||
(state === ConnectionState.Connected ? "opacity-100" : "opacity-0")
|
||||
}
|
||||
>
|
||||
peer-hostname.netbird.cloud
|
||||
</p>
|
||||
<p
|
||||
className={
|
||||
"font-mono text-xs text-nb-gray-300 mt-0.5 mb-6 transition-opacity duration-300 " +
|
||||
(state === ConnectionState.Connected ? "opacity-100" : "opacity-0")
|
||||
}
|
||||
>
|
||||
192.168.0.1
|
||||
</p>
|
||||
</div>
|
||||
<NetBirdConnectToggle state={state} onClick={handleToggleClick} />
|
||||
<div
|
||||
className={
|
||||
"flex flex-col w-full items-center justify-center gap-3 p-4 rounded-2xl mt-2"
|
||||
}
|
||||
>
|
||||
<h1 className={"text-sm font-medium text-nb-gray-200 tracking-wide"}>
|
||||
{STATUS_LABEL[state]}
|
||||
</h1>
|
||||
<div className={"w-full"}>
|
||||
<Button
|
||||
variant={buttonVariant}
|
||||
size={"xs"}
|
||||
className={"w-full"}
|
||||
disabled={isTransitioning}
|
||||
onClick={handleButtonClick}
|
||||
>
|
||||
{buttonLabel}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { ConnectionState } from "@/components/NetBirdConnectToggle.tsx";
|
||||
import { ToggleSwitch } from "@/components/ToggleSwitch.tsx";
|
||||
import { cn } from "@/lib/cn.ts";
|
||||
import netbirdFullLogo from "@/assets/logos/netbird-full.svg";
|
||||
|
||||
const CONNECT_DURATION_MS = 1500;
|
||||
const DISCONNECT_DURATION_MS = 800;
|
||||
|
||||
const STATUS_LABEL: Record<ConnectionState, string> = {
|
||||
[ConnectionState.Disconnected]: "Disconnected",
|
||||
[ConnectionState.Connecting]: "Connecting...",
|
||||
[ConnectionState.Connected]: "Connected",
|
||||
[ConnectionState.Disconnecting]: "Disconnecting...",
|
||||
};
|
||||
|
||||
export const ConnectionStatusSwitch = () => {
|
||||
const [state, setState] = useState<ConnectionState>(ConnectionState.Disconnected);
|
||||
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
if (timerRef.current) clearTimeout(timerRef.current);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const transition = (next: ConnectionState, after: ConnectionState, delay: number) => {
|
||||
if (timerRef.current) clearTimeout(timerRef.current);
|
||||
setState(next);
|
||||
timerRef.current = setTimeout(() => {
|
||||
setState(after);
|
||||
timerRef.current = null;
|
||||
}, delay);
|
||||
};
|
||||
|
||||
const connect = () =>
|
||||
transition(ConnectionState.Connecting, ConnectionState.Connected, CONNECT_DURATION_MS);
|
||||
const disconnect = () =>
|
||||
transition(
|
||||
ConnectionState.Disconnecting,
|
||||
ConnectionState.Disconnected,
|
||||
DISCONNECT_DURATION_MS,
|
||||
);
|
||||
|
||||
const handleSwitch = (next: boolean) => {
|
||||
if (next) {
|
||||
if (state === ConnectionState.Disconnected) connect();
|
||||
} else if (state === ConnectionState.Connected) {
|
||||
disconnect();
|
||||
}
|
||||
};
|
||||
|
||||
const isTransitioning =
|
||||
state === ConnectionState.Connecting || state === ConnectionState.Disconnecting;
|
||||
const isOn = state === ConnectionState.Connected || state === ConnectionState.Connecting;
|
||||
|
||||
return (
|
||||
<div className={cn("flex flex-col h-full w-full items-center justify-center gap-4 -mt-4")}>
|
||||
<img
|
||||
src={netbirdFullLogo}
|
||||
alt={"NetBird"}
|
||||
className={"h-7 w-auto select-none mb-4"}
|
||||
draggable={false}
|
||||
/>
|
||||
|
||||
<ToggleSwitch
|
||||
size={"large"}
|
||||
checked={isOn}
|
||||
onCheckedChange={handleSwitch}
|
||||
disabled={isTransitioning}
|
||||
className={cn(isTransitioning && "opacity-80")}
|
||||
/>
|
||||
|
||||
<div className={"flex flex-col items-center"}>
|
||||
<h1
|
||||
className={
|
||||
"text-sm font-medium text-nb-gray-200 tracking-wide transition-colors duration-300"
|
||||
}
|
||||
>
|
||||
{STATUS_LABEL[state]}
|
||||
</h1>
|
||||
<p
|
||||
className={
|
||||
"font-mono text-xs text-nb-gray-300 mt-2 transition-opacity duration-300 " +
|
||||
(state === ConnectionState.Connected ? "opacity-100" : "opacity-0")
|
||||
}
|
||||
>
|
||||
peer-hostname.netbird.cloud
|
||||
</p>
|
||||
<p
|
||||
className={
|
||||
"font-mono text-xs text-nb-gray-300 mt-0.5 transition-opacity duration-300 " +
|
||||
(state === ConnectionState.Connected ? "opacity-100" : "opacity-0")
|
||||
}
|
||||
>
|
||||
192.168.0.1
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,33 +1,76 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import { SettingsIcon } from "lucide-react";
|
||||
import { PanelRightCloseIcon, PanelRightOpenIcon, SettingsIcon } from "lucide-react";
|
||||
import { Window } from "@wailsio/runtime";
|
||||
import { ProfileSelector } from "@/components/ProfileSelector.tsx";
|
||||
import { IconButton } from "@/components/IconButton.tsx";
|
||||
import { UpdateHeaderTrigger } from "@/modules/auto-update/UpdateHeaderTrigger.tsx";
|
||||
import { useAppearance } from "@/modules/appearance/AppearanceContext.tsx";
|
||||
import { cn } from "@/lib/cn";
|
||||
|
||||
const WINDOW_SMALL_WIDTH = 380;
|
||||
const WINDOW_BIG_WIDTH = 925;
|
||||
const WINDOW_HEIGHT = 615;
|
||||
const EXPANDED_THRESHOLD = 500;
|
||||
|
||||
export const Header = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const isSettingsPage = location.pathname.startsWith("/settings");
|
||||
const { showProfileSelector, showSettingsButton, expanded, setField } = useAppearance();
|
||||
const showSettings = showSettingsButton || isSettingsPage;
|
||||
const didInitialResize = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (didInitialResize.current) return;
|
||||
didInitialResize.current = true;
|
||||
const w = expanded ? WINDOW_BIG_WIDTH : WINDOW_SMALL_WIDTH;
|
||||
void Window.SetSize(w, WINDOW_HEIGHT).catch(() => {});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const onResize = () => {
|
||||
const isWide = window.innerWidth >= EXPANDED_THRESHOLD;
|
||||
if (isWide !== expanded) setField("expanded", isWide);
|
||||
};
|
||||
window.addEventListener("resize", onResize);
|
||||
return () => window.removeEventListener("resize", onResize);
|
||||
}, [expanded, setField]);
|
||||
|
||||
const togglePanel = () => {
|
||||
const next = !expanded;
|
||||
setField("expanded", next);
|
||||
const w = next ? WINDOW_BIG_WIDTH : WINDOW_SMALL_WIDTH;
|
||||
void Window.SetSize(w, WINDOW_HEIGHT).catch(() => {});
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
"pt-4 shrink-0 cursor-default wails-draggable flex items-center justify-end px-4 gap-3 bg-gradient-to-b from-nb-gray-800/15"
|
||||
}
|
||||
className={cn(
|
||||
"shrink-0 cursor-default wails-draggable flex items-center justify-end px-4 gap-3 bg-gradient-to-b from-nb-gray-800/15",
|
||||
"pt-4",
|
||||
)}
|
||||
>
|
||||
<div className={"ml-20"}>
|
||||
<ProfileSelector email={"eduard@netbird.io"} />
|
||||
</div>
|
||||
<UpdateHeaderTrigger />
|
||||
{showProfileSelector && (
|
||||
<div className={"ml-20"}>
|
||||
<ProfileSelector email={"eduard@netbird.io"} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<IconButton
|
||||
icon={SettingsIcon}
|
||||
onClick={() => navigate(isSettingsPage ? "/" : "/settings")}
|
||||
className={cn(
|
||||
isSettingsPage &&
|
||||
"bg-nb-gray-910 hover:bg-nb-gray-910 text-nb-gray-200 hover:text-nb-gray-200",
|
||||
)}
|
||||
icon={expanded ? PanelRightOpenIcon : PanelRightCloseIcon}
|
||||
onClick={togglePanel}
|
||||
/>
|
||||
{showSettings && (
|
||||
<IconButton
|
||||
icon={SettingsIcon}
|
||||
onClick={() => navigate(isSettingsPage ? "/" : "/settings")}
|
||||
className={cn(
|
||||
isSettingsPage &&
|
||||
"bg-nb-gray-910 hover:bg-nb-gray-910 text-nb-gray-200 hover:text-nb-gray-200",
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,22 +1,33 @@
|
||||
import { ConnectionStatus } from "@/layouts/ConnectionStatus.tsx";
|
||||
import { ConnectionStatusSwitch } from "@/layouts/ConnectionStatusSwitch.tsx";
|
||||
import { MainRightSide } from "@/layouts/MainRightSide.tsx";
|
||||
import { Navigation } from "@/layouts/Navigation.tsx";
|
||||
import { Peers } from "@/modules/peers/Peers.tsx";
|
||||
import { useAppearance } from "@/modules/appearance/AppearanceContext.tsx";
|
||||
import { cn } from "@/lib/cn";
|
||||
|
||||
export const Main = () => {
|
||||
const { connectionLayout, expanded } = useAppearance();
|
||||
return (
|
||||
<div className={"wails-draggable flex flex-1 min-h-0 p-4 gap-4"}>
|
||||
<div
|
||||
className={
|
||||
"flex flex-col max-w-xs w-full shrink-0 items-center"
|
||||
}
|
||||
className={cn(
|
||||
"flex flex-col w-full shrink-0 items-center",
|
||||
expanded && "max-w-xs",
|
||||
)}
|
||||
>
|
||||
<ConnectionStatus />
|
||||
{connectionLayout === "switch" ? (
|
||||
<ConnectionStatusSwitch />
|
||||
) : (
|
||||
<ConnectionStatus />
|
||||
)}
|
||||
<Navigation peersActive />
|
||||
</div>
|
||||
<MainRightSide>
|
||||
<Peers />
|
||||
</MainRightSide>
|
||||
{expanded && (
|
||||
<MainRightSide>
|
||||
<Peers />
|
||||
</MainRightSide>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { CardNavItem } from "@/components/CardNavItem.tsx";
|
||||
import { Layers3Icon, MonitorSmartphoneIcon } from "lucide-react";
|
||||
import deFlag from "@/assets/flags/1x1/de.svg";
|
||||
import { useAppearance } from "@/modules/appearance/AppearanceContext.tsx";
|
||||
|
||||
type Props = {
|
||||
peersActive?: boolean;
|
||||
@@ -8,32 +9,42 @@ type Props = {
|
||||
};
|
||||
|
||||
export const Navigation = ({ peersActive = false, onPeersClick }: Props) => {
|
||||
const { showPeersNav, showResourcesNav, showExitNodeNav } = useAppearance();
|
||||
|
||||
return (
|
||||
<nav className={"w-full flex flex-col gap-1 mt-auto"}>
|
||||
<CardNavItem
|
||||
icon={MonitorSmartphoneIcon}
|
||||
title={"Peers"}
|
||||
description={"17 of 25 Online"}
|
||||
active={peersActive}
|
||||
onClick={onPeersClick}
|
||||
/>
|
||||
<CardNavItem
|
||||
icon={Layers3Icon}
|
||||
title={"Resources"}
|
||||
description={"13 of 16 Active"}
|
||||
iconSize={14}
|
||||
/>
|
||||
<CardNavItem
|
||||
iconNode={
|
||||
<img
|
||||
src={deFlag}
|
||||
alt={"Germany"}
|
||||
className={"h-6 w-6 rounded-full border-[3px] border-nb-gray-850 shrink-0"}
|
||||
/>
|
||||
}
|
||||
title={"Exit Node Berlin"}
|
||||
description={"100.92.14.37"}
|
||||
/>
|
||||
{showPeersNav && (
|
||||
<CardNavItem
|
||||
icon={MonitorSmartphoneIcon}
|
||||
title={"Peers"}
|
||||
description={"17 of 25 Online"}
|
||||
active={peersActive}
|
||||
onClick={onPeersClick}
|
||||
/>
|
||||
)}
|
||||
{showResourcesNav && (
|
||||
<CardNavItem
|
||||
icon={Layers3Icon}
|
||||
title={"Resources"}
|
||||
description={"13 of 16 Active"}
|
||||
iconSize={14}
|
||||
/>
|
||||
)}
|
||||
{showExitNodeNav && (
|
||||
<CardNavItem
|
||||
iconNode={
|
||||
<img
|
||||
src={deFlag}
|
||||
alt={"Germany"}
|
||||
className={
|
||||
"h-6 w-6 rounded-full border-[3px] border-nb-gray-850 shrink-0"
|
||||
}
|
||||
/>
|
||||
}
|
||||
title={"Exit Node Berlin"}
|
||||
description={"100.92.14.37"}
|
||||
/>
|
||||
)}
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user