diff --git a/client/ui/frontend/src/layouts/AppLayout.tsx b/client/ui/frontend/src/layouts/AppLayout.tsx index 012506844..95a504523 100644 --- a/client/ui/frontend/src/layouts/AppLayout.tsx +++ b/client/ui/frontend/src/layouts/AppLayout.tsx @@ -1,19 +1,20 @@ import { Outlet } from "react-router-dom"; import { Header } from "@/layouts/Header.tsx"; -import { UpdateAvailableBanner } from "@/modules/auto-update/UpdateAvailableBanner.tsx"; +import { ClientVersionProvider } from "@/modules/auto-update/ClientVersionContext.tsx"; import { DebugBundleProvider } from "@/modules/debug-bundle/DebugBundleContext.tsx"; import { ProfileProvider } from "@/modules/profile/ProfileContext.tsx"; export const AppLayout = () => { return ( - - -
-
- - -
-
-
+
+ + + +
+ + + + +
); }; diff --git a/client/ui/frontend/src/layouts/Header.tsx b/client/ui/frontend/src/layouts/Header.tsx index 1bdfd8924..197cc82dc 100644 --- a/client/ui/frontend/src/layouts/Header.tsx +++ b/client/ui/frontend/src/layouts/Header.tsx @@ -1,19 +1,14 @@ import { useLocation, useNavigate } from "react-router-dom"; -import { ArrowUpCircleIcon, SettingsIcon } from "lucide-react"; +import { SettingsIcon } from "lucide-react"; import { ProfileSelector } from "@/components/ProfileSelector.tsx"; import { IconButton } from "@/components/IconButton.tsx"; -import { Tooltip } from "@/components/Tooltip.tsx"; -import { useStatus } from "@/hooks/useStatus"; +import { UpdateHeaderTrigger } from "@/modules/auto-update/UpdateHeaderTrigger.tsx"; import { cn } from "@/lib/cn"; export const Header = () => { const navigate = useNavigate(); const location = useLocation(); const isSettingsPage = location.pathname.startsWith("/settings"); - const { status } = useStatus(); - const updateAvailable = (status?.events ?? []).some((e) => - Boolean(e.metadata?.["new_version_available"]), - ); return (
{
- {updateAvailable && ( - -
- - - navigate("/settings", { state: { tab: "about" } }) - } - className={"absolute inset-0"} - /> -
-
- )} + navigate(isSettingsPage ? "/" : "/settings")} diff --git a/client/ui/frontend/src/modules/auto-update/ClientVersionContext.tsx b/client/ui/frontend/src/modules/auto-update/ClientVersionContext.tsx new file mode 100644 index 000000000..d83892f42 --- /dev/null +++ b/client/ui/frontend/src/modules/auto-update/ClientVersionContext.tsx @@ -0,0 +1,45 @@ +import { createContext, useContext, useMemo, type ReactNode } from "react"; +import { Update as UpdateSvc } from "@bindings/services"; +import { useStatus } from "@/hooks/useStatus"; +import { UpdateAvailableBanner } from "@/modules/auto-update/UpdateAvailableBanner"; + +type ClientVersionContextValue = { + updateAvailable: boolean; + updateVersion: string | null; + triggerUpdate: () => void; +}; + +const ClientVersionContext = createContext(null); + +export const useClientVersion = () => { + const ctx = useContext(ClientVersionContext); + if (!ctx) { + throw new Error("useClientVersion must be used inside ClientVersionProvider"); + } + return ctx; +}; + +export const ClientVersionProvider = ({ children }: { children: ReactNode }) => { + const { status } = useStatus(); + + const value = useMemo(() => { + const version = (status?.events ?? []) + .map((e) => e.metadata?.["new_version_available"]) + .find((v): v is string => Boolean(v)); + + return { + updateAvailable: Boolean(version), + updateVersion: version ?? null, + triggerUpdate: () => { + UpdateSvc.Trigger().catch(() => {}); + }, + }; + }, [status]); + + return ( + + {children} + + + ); +}; diff --git a/client/ui/frontend/src/modules/auto-update/UpdateAvailableBanner.tsx b/client/ui/frontend/src/modules/auto-update/UpdateAvailableBanner.tsx index 91de44c56..e7fd327cd 100644 --- a/client/ui/frontend/src/modules/auto-update/UpdateAvailableBanner.tsx +++ b/client/ui/frontend/src/modules/auto-update/UpdateAvailableBanner.tsx @@ -1,26 +1,16 @@ import { useState } from "react"; import { Button } from "@/components/Button"; -import { useStatus } from "@/hooks/useStatus"; +import { useClientVersion } from "@/modules/auto-update/ClientVersionContext"; import { cn } from "@/lib/cn"; -import { Update as UpdateSvc } from "@bindings/services"; // TODO: Shown only when management has auto updates enabled + there are updates available + force updates is disabled export const UpdateAvailableBanner = () => { - const { status } = useStatus(); + const { updateVersion, triggerUpdate } = useClientVersion(); const [dismissed, setDismissed] = useState(false); if (import.meta.env.DEV) return null; - - const updateVersion = (status?.events ?? []) - .map((e) => e.metadata?.["new_version_available"]) - .find((v): v is string => Boolean(v)); - if (!updateVersion || dismissed) return null; - const triggerUpdate = () => { - UpdateSvc.Trigger().catch(() => {}); - }; - return (
{ + return ( +
+ + +
+ ); +}; diff --git a/client/ui/frontend/src/modules/auto-update/UpdateHeaderTrigger.tsx b/client/ui/frontend/src/modules/auto-update/UpdateHeaderTrigger.tsx new file mode 100644 index 000000000..93128f81a --- /dev/null +++ b/client/ui/frontend/src/modules/auto-update/UpdateHeaderTrigger.tsx @@ -0,0 +1,32 @@ +import { useNavigate } from "react-router-dom"; +import { ArrowUpCircleIcon } from "lucide-react"; +import { IconButton } from "@/components/IconButton.tsx"; +import { Tooltip } from "@/components/Tooltip.tsx"; +import { useClientVersion } from "@/modules/auto-update/ClientVersionContext"; + +export const UpdateHeaderTrigger = () => { + const navigate = useNavigate(); + const { updateAvailable } = useClientVersion(); + + if (!updateAvailable) return null; + + return ( + +
+ + + navigate("/settings", { state: { tab: "about" } }) + } + className={"absolute inset-0"} + /> +
+
+ ); +}; diff --git a/client/ui/frontend/src/components/NetBirdVersionCard.tsx b/client/ui/frontend/src/modules/auto-update/UpdateVersionCard.tsx similarity index 85% rename from client/ui/frontend/src/components/NetBirdVersionCard.tsx rename to client/ui/frontend/src/modules/auto-update/UpdateVersionCard.tsx index 8c98b084b..ac5edc96f 100644 --- a/client/ui/frontend/src/components/NetBirdVersionCard.tsx +++ b/client/ui/frontend/src/modules/auto-update/UpdateVersionCard.tsx @@ -1,8 +1,7 @@ import { ReactNode } from "react"; import { Browser } from "@wailsio/runtime"; -import { Update as UpdateSvc } from "@bindings/services"; import { Button } from "@/components/Button"; -import { useStatus } from "@/hooks/useStatus"; +import { useClientVersion } from "@/modules/auto-update/ClientVersionContext"; import { cn } from "@/lib/cn"; function openUrl(url: string) { @@ -18,15 +17,8 @@ function formatLastChecked(date: Date) { }); } -function triggerUpdate() { - UpdateSvc.Trigger().catch(() => {}); -} - -export function NetBirdVersionCard() { - const { status } = useStatus(); - const updateVersion = (status?.events ?? []) - .map((e) => e.metadata?.["new_version_available"]) - .find((v): v is string => Boolean(v)); +export function UpdateVersionCard() { + const { updateVersion, triggerUpdate } = useClientVersion(); if (updateVersion) { return ( diff --git a/client/ui/frontend/src/modules/settings/SettingsAbout.tsx b/client/ui/frontend/src/modules/settings/SettingsAbout.tsx index 0e28531d0..4b81e173d 100644 --- a/client/ui/frontend/src/modules/settings/SettingsAbout.tsx +++ b/client/ui/frontend/src/modules/settings/SettingsAbout.tsx @@ -2,7 +2,7 @@ import { Browser } from "@wailsio/runtime"; import netbirdFull from "@/assets/logos/netbird-full.svg"; import pkg from "../../../package.json"; import { useStatus } from "@/hooks/useStatus"; -import { NetBirdVersionCard } from "@/components/NetBirdVersionCard"; +import { UpdateVersionCard } from "@/modules/auto-update/UpdateVersionCard"; import { useAccentTrigger } from "@/modules/settings/SettingsAccent"; const LEGAL_LINKS: { label: string; url: string }[] = [ @@ -40,7 +40,7 @@ export function SettingsAbout() {

GUI v{guiVersion}

- +

© {new Date().getFullYear()} NetBird. All Rights Reserved. diff --git a/client/ui/frontend/src/modules/settings/SettingsNavigationTriggers.tsx b/client/ui/frontend/src/modules/settings/SettingsNavigationTriggers.tsx index 8e6253569..3a751beb1 100644 --- a/client/ui/frontend/src/modules/settings/SettingsNavigationTriggers.tsx +++ b/client/ui/frontend/src/modules/settings/SettingsNavigationTriggers.tsx @@ -1,8 +1,8 @@ import { Tooltip } from "@/components/Tooltip.tsx"; import { VerticalTabs } from "@/components/VerticalTabs.tsx"; -import { useStatus } from "@/hooks/useStatus"; +import { UpdateBadge } from "@/modules/auto-update/UpdateBadge.tsx"; +import { useClientVersion } from "@/modules/auto-update/ClientVersionContext.tsx"; import { - ArrowUpCircleIcon, BoltIcon, InfoIcon, LifeBuoyIcon, @@ -13,21 +13,11 @@ import { } from "lucide-react"; export const SettingsNavigationTriggers = () => { - const { status } = useStatus(); - const updateAvailable = (status?.events ?? []).some((e) => - Boolean(e.metadata?.["new_version_available"]), - ); + const { updateAvailable } = useClientVersion(); const aboutAdornment = updateAvailable ? ( -

- - -
+ ) : undefined;