mirror of
https://github.com/netbirdio/netbird.git
synced 2026-05-15 21:29:56 +00:00
wip
This commit is contained in:
@@ -5,35 +5,30 @@ import { HashRouter, Navigate, Route, Routes } from "react-router-dom";
|
||||
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";
|
||||
import { AppLayout } from "@/layouts/AppLayout.tsx";
|
||||
import { Main } from "@/layouts/Main.tsx";
|
||||
import { Settings } from "@/modules/settings/Settings.tsx";
|
||||
import { SkeletonTheme } from "react-loading-skeleton";
|
||||
import "react-loading-skeleton/dist/skeleton.css";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||
<React.StrictMode>
|
||||
<SkeletonTheme baseColor={"#25282d"} highlightColor={"#33373e"}>
|
||||
<HashRouter>
|
||||
<Routes>
|
||||
<Route path="/quick" element={<QuickActions />} />
|
||||
<Route path="/login" element={<LoginUrl />} />
|
||||
<Route path="/update" element={<Update />} />
|
||||
<Route element={<Layout />}>
|
||||
<Route index element={<Main />} />
|
||||
<Route path="peers" element={<Peers />} />
|
||||
<Route path="networks" element={<Networks />} />
|
||||
<Route path="profiles" element={<Profiles />} />
|
||||
<Route path="settings" element={<Settings />} />
|
||||
<Route path="debug" element={<Debug />} />
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</HashRouter>
|
||||
</SkeletonTheme>
|
||||
</React.StrictMode>,
|
||||
<React.StrictMode>
|
||||
<SkeletonTheme baseColor={"#25282d"} highlightColor={"#33373e"}>
|
||||
<HashRouter>
|
||||
<Routes>
|
||||
<Route path="/quick" element={<QuickActions />} />
|
||||
<Route path="/login" element={<LoginUrl />} />
|
||||
<Route path="/update" element={<Update />} />
|
||||
<Route element={<AppLayout />}>
|
||||
<Route index element={<Main />} />
|
||||
<Route path="settings" element={<Settings />} />
|
||||
<Route
|
||||
path="*"
|
||||
element={<Navigate to={"/"} replace />}
|
||||
/>
|
||||
</Route>
|
||||
</Routes>
|
||||
</HashRouter>
|
||||
</SkeletonTheme>
|
||||
</React.StrictMode>,
|
||||
);
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import React from "react";
|
||||
import { HelpText } from "@/components/HelpText";
|
||||
import { Label } from "@/components/Label";
|
||||
import { ToggleSwitch } from "@/components/ToggleSwitch";
|
||||
import { cn } from "@/lib/cn";
|
||||
|
||||
interface Props {
|
||||
value: boolean;
|
||||
onChange: (value: boolean) => void;
|
||||
helpText?: React.ReactNode;
|
||||
label?: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
disabled?: boolean;
|
||||
dataCy?: string;
|
||||
className?: string;
|
||||
labelClassName?: string;
|
||||
textWrapperClassName?: string;
|
||||
}
|
||||
|
||||
export default function FancyToggleSwitch({
|
||||
value,
|
||||
onChange,
|
||||
helpText,
|
||||
label,
|
||||
children,
|
||||
disabled = false,
|
||||
dataCy,
|
||||
className,
|
||||
labelClassName,
|
||||
textWrapperClassName = "max-w-sm",
|
||||
}: Readonly<Props>) {
|
||||
const handleToggle = () => {
|
||||
if (disabled) return;
|
||||
onChange(!value);
|
||||
};
|
||||
|
||||
const handleKeyDown = (event: React.KeyboardEvent) => {
|
||||
if (disabled) return;
|
||||
if (event.key === "Enter" || event.key === " ") {
|
||||
event.preventDefault();
|
||||
handleToggle();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={handleToggle}
|
||||
onKeyDown={handleKeyDown}
|
||||
tabIndex={-1}
|
||||
role={"switch"}
|
||||
aria-checked={value}
|
||||
className={cn(
|
||||
"cursor-pointer transition-all duration-300 relative z-[1]",
|
||||
"inline-block text-left w-full",
|
||||
disabled && "opacity-50 pointer-events-none",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className={"flex justify-between gap-10"}>
|
||||
<div className={cn(textWrapperClassName)}>
|
||||
<Label className={labelClassName}>{label}</Label>
|
||||
<HelpText margin={false}>{helpText}</HelpText>
|
||||
</div>
|
||||
<div className={"mt-2 pr-1"}>
|
||||
<ToggleSwitch
|
||||
checked={value}
|
||||
onCheckedChange={onChange}
|
||||
dataCy={dataCy}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{children && value ? (
|
||||
<div className="mt-4" onClick={(e) => e.stopPropagation()}>
|
||||
{children}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -9,6 +9,7 @@ type Props = HTMLMotionProps<"button"> & {
|
||||
description?: string;
|
||||
active?: boolean;
|
||||
iconSize?: number;
|
||||
iconBackground?: boolean;
|
||||
};
|
||||
|
||||
export const NavItem = forwardRef<HTMLButtonElement, Props>(
|
||||
@@ -19,6 +20,7 @@ export const NavItem = forwardRef<HTMLButtonElement, Props>(
|
||||
description,
|
||||
active = false,
|
||||
iconSize = 15,
|
||||
iconBackground = true,
|
||||
className,
|
||||
type = "button",
|
||||
...props
|
||||
@@ -40,21 +42,33 @@ export const NavItem = forwardRef<HTMLButtonElement, Props>(
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"h-9 w-9 rounded-md flex items-center justify-center shrink-0",
|
||||
"transition-colors duration-150",
|
||||
active ? "bg-nb-gray-800" : "bg-nb-gray-920",
|
||||
)}
|
||||
>
|
||||
{iconBackground ? (
|
||||
<div
|
||||
className={cn(
|
||||
"h-9 w-9 rounded-md flex items-center justify-center shrink-0",
|
||||
"transition-colors duration-150",
|
||||
active ? "bg-nb-gray-800" : "bg-nb-gray-920",
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
size={iconSize}
|
||||
className={cn(
|
||||
"transition-colors duration-150",
|
||||
active
|
||||
? "text-nb-gray-200"
|
||||
: "text-nb-gray-400",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<Icon
|
||||
size={iconSize}
|
||||
className={cn(
|
||||
"transition-colors duration-150",
|
||||
"shrink-0 ml-2 transition-colors duration-150",
|
||||
active ? "text-nb-gray-200" : "text-nb-gray-400",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className={"min-w-0"}>
|
||||
<h2
|
||||
className={cn(
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
export default function PlaceholderHeader() {
|
||||
return (
|
||||
<div
|
||||
className="h-[36px] shrink-0 cursor-default wails-draggable"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -124,9 +124,10 @@ export const ProfileSelector = ({ email = "" }: Props) => {
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
"h-7 w-7 flex items-center justify-center bg-nb-gray-900 rounded-md text-xs font-semibold"
|
||||
}
|
||||
className={cn(
|
||||
"flex items-center justify-center bg-nb-gray-900 rounded-md text-xs font-semibold",
|
||||
email ? "h-7 w-7" : "h-6 w-6",
|
||||
)}
|
||||
style={{ color: initialColor }}
|
||||
>
|
||||
{initial}
|
||||
|
||||
71
client/ui-wails/frontend/src/components/ToggleSwitch.tsx
Normal file
71
client/ui-wails/frontend/src/components/ToggleSwitch.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
"use client";
|
||||
|
||||
import * as SwitchPrimitives from "@radix-ui/react-switch";
|
||||
import { cva, VariantProps } from "class-variance-authority";
|
||||
import * as React from "react";
|
||||
import { cn } from "@/lib/cn";
|
||||
|
||||
type SwitchVariants = VariantProps<typeof switchVariants>;
|
||||
|
||||
const switchVariants = cva("", {
|
||||
variants: {
|
||||
size: {
|
||||
default: "h-[24px] w-[44px]",
|
||||
small: "h-[18px] w-[36px]",
|
||||
},
|
||||
variant: {
|
||||
default: [
|
||||
"dark:data-[state=checked]:bg-netbird dark:data-[state=unchecked]:bg-nb-gray-700",
|
||||
"data-[state=checked]:bg-neutral-900 data-[state=unchecked]:bg-neutral-200",
|
||||
],
|
||||
"red-green": [
|
||||
"dark:data-[state=checked]:bg-red-600 dark:data-[state=unchecked]:bg-nb-gray-700",
|
||||
"data-[state=checked]:bg-red-500 data-[state=unchecked]:bg-red-200",
|
||||
],
|
||||
red: [
|
||||
"dark:data-[state=checked]:bg-red-600 dark:data-[state=unchecked]:bg-nb-gray-700",
|
||||
"data-[state=checked]:bg-red-500 data-[state=unchecked]:bg-red-200",
|
||||
],
|
||||
},
|
||||
"thumb-size": {
|
||||
default: "h-5 w-5 data-[state=checked]:translate-x-5",
|
||||
small: "h-[14px] w-[14px] data-[state=checked]:translate-x-[17px]",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const ToggleSwitch = React.forwardRef<
|
||||
React.ElementRef<typeof SwitchPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root> &
|
||||
SwitchVariants & { dataCy?: string }
|
||||
>(
|
||||
(
|
||||
{ className, size = "default", variant = "default", dataCy, ...props },
|
||||
ref,
|
||||
) => (
|
||||
<SwitchPrimitives.Root
|
||||
className={cn(
|
||||
"peer inline-flex shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-950 focus-visible:ring-offset-2 focus-visible:ring-offset-white disabled:cursor-not-allowed disabled:opacity-50 dark:focus-visible:ring-neutral-300 dark:focus-visible:ring-offset-neutral-950",
|
||||
className,
|
||||
switchVariants({ size, variant }),
|
||||
)}
|
||||
{...props}
|
||||
data-cy={dataCy}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
props.onClick?.(e);
|
||||
}}
|
||||
ref={ref}
|
||||
>
|
||||
<SwitchPrimitives.Thumb
|
||||
className={cn(
|
||||
switchVariants({ "thumb-size": size }),
|
||||
"pointer-events-none block rounded-full bg-white shadow-lg ring-0 transition-transform data-[state=unchecked]:translate-x-0 dark:bg-white",
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitives.Root>
|
||||
),
|
||||
);
|
||||
ToggleSwitch.displayName = SwitchPrimitives.Root.displayName;
|
||||
|
||||
export { ToggleSwitch };
|
||||
@@ -1,15 +0,0 @@
|
||||
import { Outlet } from "react-router-dom";
|
||||
import PlaceholderHeader from "@/components/PlaceholderHeader";
|
||||
|
||||
export default function Layout() {
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
<PlaceholderHeader />
|
||||
<div className="flex min-h-0 flex-1">
|
||||
<main className="flex-1 overflow-hidden">
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
11
client/ui-wails/frontend/src/layouts/AppLayout.tsx
Normal file
11
client/ui-wails/frontend/src/layouts/AppLayout.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Outlet } from "react-router-dom";
|
||||
import { Header } from "@/layouts/Header.tsx";
|
||||
|
||||
export const AppLayout = () => {
|
||||
return (
|
||||
<div className={"flex h-full flex-col"}>
|
||||
<Header />
|
||||
<Outlet />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
|
||||
export const ConnectionStatus = () => {
|
||||
return (
|
||||
<div className={"flex flex-col items-center"}>
|
||||
<div className={"flex flex-col h-full items-center justify-center"}>
|
||||
<NetBirdConnectToggle state={ConnectionState.Connected} />
|
||||
<h1
|
||||
className={
|
||||
|
||||
@@ -1,20 +1,26 @@
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import { SettingsIcon } from "lucide-react";
|
||||
import { ProfileSelector } from "@/components/ProfileSelector.tsx";
|
||||
import { IconButton } from "@/components/IconButton.tsx";
|
||||
import { SettingsIcon } from "lucide-react";
|
||||
import { cn } from "@/lib/cn";
|
||||
|
||||
type Props = {
|
||||
settingsActive?: boolean;
|
||||
onSettingsClick?: () => void;
|
||||
};
|
||||
export const Header = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const settingsActive = location.pathname.startsWith("/settings");
|
||||
|
||||
export const Header = ({ settingsActive = false, onSettingsClick }: Props) => {
|
||||
return (
|
||||
<div className={"w-full justify-between flex mb-12"}>
|
||||
<ProfileSelector email={"eduard@netbird.io"} />
|
||||
<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"
|
||||
}
|
||||
>
|
||||
<div className={"ml-20"}>
|
||||
<ProfileSelector email={"eduard@netbird.io"} />
|
||||
</div>
|
||||
<IconButton
|
||||
icon={SettingsIcon}
|
||||
onClick={onSettingsClick}
|
||||
onClick={() => navigate(settingsActive ? "/" : "/settings")}
|
||||
className={cn(
|
||||
settingsActive &&
|
||||
"bg-nb-gray-930 text-nb-gray-200 hover:text-nb-gray-200",
|
||||
|
||||
22
client/ui-wails/frontend/src/layouts/Main.tsx
Normal file
22
client/ui-wails/frontend/src/layouts/Main.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { ConnectionStatus } from "@/layouts/ConnectionStatus.tsx";
|
||||
import { MainRightSide } from "@/layouts/MainRightSide.tsx";
|
||||
import { Navigation } from "@/layouts/Navigation.tsx";
|
||||
import { Peers } from "@/modules/peers/Peers.tsx";
|
||||
|
||||
export const Main = () => {
|
||||
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"
|
||||
}
|
||||
>
|
||||
<ConnectionStatus />
|
||||
<Navigation peersActive />
|
||||
</div>
|
||||
<MainRightSide>
|
||||
<Peers />
|
||||
</MainRightSide>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,32 +0,0 @@
|
||||
import { ConnectionStatus } from "@/layouts/ConnectionStatus.tsx";
|
||||
import { Header } from "@/layouts/Header.tsx";
|
||||
import { Navigation } from "@/layouts/Navigation.tsx";
|
||||
|
||||
export type MainModule = "peers" | "settings";
|
||||
|
||||
type Props = {
|
||||
active: MainModule;
|
||||
onChange: (module: MainModule) => void;
|
||||
};
|
||||
|
||||
export const MainLeftSide = ({ active, onChange }: Props) => {
|
||||
return (
|
||||
<div
|
||||
className={"flex flex-col max-w-xs w-full shrink-0 items-center"}
|
||||
>
|
||||
<Header
|
||||
settingsActive={active === "settings"}
|
||||
onSettingsClick={() =>
|
||||
onChange(active === "settings" ? "peers" : "settings")
|
||||
}
|
||||
/>
|
||||
<ConnectionStatus />
|
||||
<Navigation
|
||||
peersActive={active === "peers"}
|
||||
onPeersClick={() => {
|
||||
if (active !== "peers") onChange("peers");
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -12,7 +12,7 @@ type Props = {
|
||||
|
||||
export const Navigation = ({ peersActive = false, onPeersClick }: Props) => {
|
||||
return (
|
||||
<nav className={"w-full flex flex-col gap-1 mt-8"}>
|
||||
<nav className={"w-full flex flex-col gap-1 mt-auto"}>
|
||||
<NavItem
|
||||
icon={MonitorSmartphoneIcon}
|
||||
title={"Peers"}
|
||||
|
||||
27
client/ui-wails/frontend/src/lib/MainModuleContext.tsx
Normal file
27
client/ui-wails/frontend/src/lib/MainModuleContext.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { createContext, ReactNode, useContext, useState } from "react";
|
||||
|
||||
export type MainModule = "peers" | "settings";
|
||||
|
||||
type Ctx = {
|
||||
active: MainModule;
|
||||
setActive: (m: MainModule) => void;
|
||||
};
|
||||
|
||||
const MainModuleContext = createContext<Ctx | null>(null);
|
||||
|
||||
export const MainModuleProvider = ({ children }: { children: ReactNode }) => {
|
||||
const [active, setActive] = useState<MainModule>("peers");
|
||||
return (
|
||||
<MainModuleContext.Provider value={{ active, setActive }}>
|
||||
{children}
|
||||
</MainModuleContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useMainModule = () => {
|
||||
const ctx = useContext(MainModuleContext);
|
||||
if (!ctx) {
|
||||
throw new Error("useMainModule must be used within MainModuleProvider");
|
||||
}
|
||||
return ctx;
|
||||
};
|
||||
@@ -1,7 +1,19 @@
|
||||
import { useState } from "react";
|
||||
import { MainRightSide } from "@/layouts/MainRightSide.tsx";
|
||||
import {
|
||||
SettingsNavigation,
|
||||
SettingsSection,
|
||||
} from "@/modules/settings/SettingsNavigation.tsx";
|
||||
|
||||
export const Settings = () => {
|
||||
const [active, setActive] = useState<SettingsSection>("general");
|
||||
|
||||
return (
|
||||
<div className={"flex flex-col w-full h-full min-h-0 pt-4 px-4"}>
|
||||
<h2 className={"text-sm font-medium text-nb-gray-200"}>Settings</h2>
|
||||
<div className={"wails-draggable flex flex-1 min-h-0 p-4 gap-4"}>
|
||||
<div className={"flex flex-col w-52 shrink-0 items-center"}>
|
||||
<SettingsNavigation active={active} onChange={setActive} />
|
||||
</div>
|
||||
<MainRightSide>{null}</MainRightSide>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { NavItem } from "@/components/NavItem.tsx";
|
||||
import {
|
||||
InfoIcon,
|
||||
LifeBuoyIcon,
|
||||
NetworkIcon,
|
||||
SlidersHorizontalIcon,
|
||||
TerminalIcon,
|
||||
} from "lucide-react";
|
||||
|
||||
export type SettingsSection =
|
||||
| "general"
|
||||
| "network"
|
||||
| "ssh"
|
||||
| "troubleshooting"
|
||||
| "about";
|
||||
|
||||
type Props = {
|
||||
active: SettingsSection;
|
||||
onChange: (section: SettingsSection) => void;
|
||||
};
|
||||
|
||||
const ITEMS: {
|
||||
id: SettingsSection;
|
||||
icon: typeof SlidersHorizontalIcon;
|
||||
title: string;
|
||||
}[] = [
|
||||
{ id: "general", icon: SlidersHorizontalIcon, title: "General" },
|
||||
{ id: "network", icon: NetworkIcon, title: "Network" },
|
||||
{ id: "ssh", icon: TerminalIcon, title: "SSH" },
|
||||
{ id: "troubleshooting", icon: LifeBuoyIcon, title: "Troubleshooting" },
|
||||
{ id: "about", icon: InfoIcon, title: "About" },
|
||||
];
|
||||
|
||||
export const SettingsNavigation = ({ active, onChange }: Props) => {
|
||||
return (
|
||||
<nav className={"w-full flex flex-col gap-1"}>
|
||||
{ITEMS.map(({ id, icon, title }) => (
|
||||
<NavItem
|
||||
key={id}
|
||||
icon={icon}
|
||||
title={title}
|
||||
iconSize={14}
|
||||
iconBackground={false}
|
||||
className={"py-2.5"}
|
||||
active={active === id}
|
||||
onClick={() => {
|
||||
if (active !== id) onChange(id);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
@@ -1,23 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { MainLeftSide, MainModule } from "@/layouts/MainLeftSide.tsx";
|
||||
import { MainRightSide } from "@/layouts/MainRightSide.tsx";
|
||||
import { Peers } from "@/modules/peers/Peers.tsx";
|
||||
import { Settings } from "@/modules/settings/Settings.tsx";
|
||||
|
||||
type Props = {
|
||||
|
||||
};
|
||||
export const Main = ({}: Props) => {
|
||||
const [active, setActive] = useState<MainModule>("peers");
|
||||
|
||||
return (
|
||||
<div className={"wails-draggable flex h-full p-4 gap-4 min-h-0"}>
|
||||
<MainLeftSide active={active} onChange={setActive} />
|
||||
|
||||
<MainRightSide>
|
||||
{active === "peers" ? <Peers /> : <Settings />}
|
||||
</MainRightSide>
|
||||
</div>
|
||||
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user