mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-22 13:29:08 +02:00
* [proxy] Fix npm audit findings in the web UI npm audit reported 10 vulnerabilities (7 high), all in build tooling: Vite/launch-editor, PostCSS, Babel, browserslist, js-yaml, nanoid, brace-expansion and source-map-js. All are resolved by the lockfile update; Vite is pinned exactly, so it is bumped from 7.3.2 to 7.3.7. * [client] Migrate the desktop UI to Tailwind CSS 4 and fix npm audit findings pnpm audit reported 36 vulnerabilities in the desktop UI. All but two (braces and postcss-selector-parser, both pulled in by Tailwind 3 with no patched version in range) are fixed by updating within the existing ranges; react-router moves to 7.18.4, which also fixes an open redirect and an XSS in its error handler. The remaining two go away by moving to Tailwind CSS 4, which the proxy web UI already uses: - The config moves into an @theme block in globals.css; the official upgrade tool applied the v4 utility renames across the components. - @tailwindcss/vite replaces the PostCSS plugin and autoprefixer. - tw-animate-css replaces tailwindcss-animate with the same classes. - The Prettier plugin reads the theme from the stylesheet. A screenshot comparison of every screen against the Tailwind 3 build found three regressions caused by v4's changed utility and variant ordering, fixed here so the UI renders as before: - Label combined inline-block and flex; v4 let inline-block win, which made every settings row taller. The dead inline-block is removed. - The active settings tab lost its highlight in dark mode because dark: now outranks group-data-[state=active]:. The active color is set for dark mode explicitly. - v4 rebuilt its default palette in OKLCH, shifting colors the theme does not override such as sky-400. The Tailwind 3 values for the neutral, zinc, sky, orange and amber families are pinned in @theme. The only remaining visible difference is 2px less height per row in the profiles list, because v4's preflight resets the browser's default table-cell padding.
205 lines
8.0 KiB
TypeScript
205 lines
8.0 KiB
TypeScript
import { useCallback, useState } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import {
|
|
ArrowUpCircleIcon,
|
|
Check,
|
|
MoreVertical,
|
|
PanelsRightBottom,
|
|
RectangleVertical,
|
|
Settings,
|
|
type LucideIcon,
|
|
} from "lucide-react";
|
|
import { WindowManager } from "@bindings/services";
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuShortcut,
|
|
DropdownMenuTrigger,
|
|
} from "@/components/DropdownMenu";
|
|
import { IconButton } from "@/components/buttons/IconButton";
|
|
import { ProfileDropdown } from "@/modules/profiles/ProfileDropdown";
|
|
import { useClientVersion } from "@/contexts/ClientVersionContext";
|
|
import { cn } from "@/lib/cn";
|
|
import { formatShortcut, useKeyboardShortcut } from "@/hooks/useKeyboardShortcut";
|
|
import { useViewMode, type ViewMode } from "@/contexts/ViewModeContext";
|
|
import { useRestrictions } from "@/contexts/RestrictionsContext";
|
|
import { isMacOS, isWindows } from "@/lib/platform.ts";
|
|
import { useDirection } from "@/hooks/useDirection";
|
|
|
|
const SETTINGS_SHORTCUT = { key: ",", cmd: true } as const;
|
|
|
|
export const MainHeader = () => {
|
|
const { t } = useTranslation();
|
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
const { viewMode, setViewMode } = useViewMode();
|
|
const { updateAvailable } = useClientVersion();
|
|
const { mdm, features } = useRestrictions();
|
|
const dir = useDirection();
|
|
const pinSettingsRight = isMacOS();
|
|
|
|
const openSettings = useCallback(() => {
|
|
setMenuOpen(false);
|
|
WindowManager.OpenSettings("").catch((err: unknown) =>
|
|
console.error("open settings window failed", err),
|
|
);
|
|
}, []);
|
|
|
|
useKeyboardShortcut(SETTINGS_SHORTCUT, openSettings);
|
|
|
|
const openAbout = () => {
|
|
setMenuOpen(false);
|
|
WindowManager.OpenSettings("about").catch((err: unknown) =>
|
|
console.error("open settings (about) window failed", err),
|
|
);
|
|
};
|
|
|
|
const openManageProfiles = () => {
|
|
WindowManager.OpenSettings("profiles").catch((err: unknown) =>
|
|
console.error("open settings (profiles) window failed", err),
|
|
);
|
|
};
|
|
|
|
const selectMode = (mode: ViewMode) => {
|
|
setMenuOpen(false);
|
|
setViewMode(mode);
|
|
};
|
|
|
|
const profileSlot = features.disableProfiles ? null : (
|
|
<ProfileDropdown onManageProfiles={openManageProfiles} />
|
|
);
|
|
|
|
const settingsSlot = (
|
|
<div className={"relative"}>
|
|
<DropdownMenu modal={false} open={menuOpen} onOpenChange={setMenuOpen}>
|
|
<DropdownMenuTrigger asChild className={"wails-no-draggable"}>
|
|
<IconButton
|
|
icon={MoreVertical}
|
|
iconClassName={"text-nb-gray-200 wails-no-draggable"}
|
|
className={
|
|
"select-none hover:bg-nb-gray-800 data-[state=open]:bg-nb-gray-800 dark:hover:bg-nb-gray-900 dark:data-[state=open]:bg-nb-gray-900"
|
|
}
|
|
aria-label={t("header.menu.open")}
|
|
aria-haspopup={"menu"}
|
|
aria-expanded={menuOpen}
|
|
/>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent
|
|
align={pinSettingsRight && dir === "rtl" ? "start" : "end"}
|
|
sideOffset={8}
|
|
className={
|
|
"min-w-52 select-none data-[state=closed]:animate-none! data-[state=closed]:duration-0!"
|
|
}
|
|
>
|
|
{updateAvailable && (
|
|
<>
|
|
<DropdownMenuItem onClick={openAbout}>
|
|
<div className={"flex items-center gap-2"}>
|
|
<ArrowUpCircleIcon
|
|
size={14}
|
|
className={"text-netbird"}
|
|
aria-hidden={"true"}
|
|
/>
|
|
<span className={"text-netbird"}>
|
|
{t("header.menu.updateAvailable")}
|
|
</span>
|
|
</div>
|
|
</DropdownMenuItem>
|
|
<DropdownMenuSeparator />
|
|
</>
|
|
)}
|
|
<DropdownMenuItem onClick={openSettings}>
|
|
<div className={"flex w-full items-center gap-2"}>
|
|
<Settings size={14} aria-hidden={"true"} />
|
|
<span className={"flex-1"}>{t("header.menu.settings")}</span>
|
|
<DropdownMenuShortcut>
|
|
{formatShortcut(SETTINGS_SHORTCUT)}
|
|
</DropdownMenuShortcut>
|
|
</div>
|
|
</DropdownMenuItem>
|
|
{!mdm.disableAdvancedView && (
|
|
<>
|
|
<DropdownMenuSeparator />
|
|
<ViewModeItem
|
|
icon={RectangleVertical}
|
|
label={t("header.menu.defaultView")}
|
|
selected={viewMode === "default"}
|
|
onSelect={() => selectMode("default")}
|
|
/>
|
|
<ViewModeItem
|
|
icon={PanelsRightBottom}
|
|
label={t("header.menu.advancedView")}
|
|
selected={viewMode === "advanced"}
|
|
onSelect={() => selectMode("advanced")}
|
|
/>
|
|
</>
|
|
)}
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
{updateAvailable && (
|
|
<span
|
|
aria-hidden={"true"}
|
|
className={
|
|
"pointer-events-none absolute inset-e-1.5 top-1.5 flex h-2.5 w-2.5 items-center justify-center"
|
|
}
|
|
>
|
|
<span
|
|
className={
|
|
"absolute inset-0 animate-ping rounded-full bg-netbird opacity-60"
|
|
}
|
|
/>
|
|
<span className={"relative h-1.5 w-1.5 rounded-full bg-netbird"} />
|
|
</span>
|
|
)}
|
|
</div>
|
|
);
|
|
|
|
return (
|
|
<header
|
|
className={cn(
|
|
"wails-draggable relative z-10 shrink-0 cursor-default",
|
|
"top-3 flex h-12 items-center",
|
|
)}
|
|
>
|
|
{/* Windows narrower width compensates for the OS frame Wails counts differently than macOS.
|
|
See https://github.com/wailsapp/wails/issues/3260 */}
|
|
<div
|
|
className={cn(
|
|
"grid shrink-0 grid-cols-3 items-center",
|
|
isWindows() ? "w-[364px]" : "w-[380px]",
|
|
)}
|
|
>
|
|
<div />
|
|
<div className={"ml-4 flex justify-center rtl:ml-1"}>{profileSlot}</div>
|
|
<div />
|
|
</div>
|
|
<div
|
|
className={cn(
|
|
"absolute top-1/2 -translate-y-1/2",
|
|
pinSettingsRight ? "right-[1.3rem]" : "inset-e-[1.3rem]",
|
|
)}
|
|
>
|
|
{settingsSlot}
|
|
</div>
|
|
</header>
|
|
);
|
|
};
|
|
|
|
type ViewModeItemProps = {
|
|
icon: LucideIcon;
|
|
label: string;
|
|
selected: boolean;
|
|
onSelect: () => void;
|
|
};
|
|
|
|
const ViewModeItem = ({ icon: Icon, label, selected, onSelect }: ViewModeItemProps) => (
|
|
<DropdownMenuItem onClick={onSelect} role={"menuitemradio"} aria-checked={selected}>
|
|
<div className={"flex w-full items-center gap-2"}>
|
|
<Icon size={14} aria-hidden={"true"} />
|
|
<span className={"flex-1"}>{label}</span>
|
|
{selected && <Check size={14} className={"text-netbird"} aria-hidden={"true"} />}
|
|
</div>
|
|
</DropdownMenuItem>
|
|
);
|