mirror of
https://github.com/netbirdio/netbird.git
synced 2026-05-14 12:49:57 +00:00
add update context
This commit is contained in:
@@ -1,19 +1,20 @@
|
|||||||
import { Outlet } from "react-router-dom";
|
import { Outlet } from "react-router-dom";
|
||||||
import { Header } from "@/layouts/Header.tsx";
|
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 { DebugBundleProvider } from "@/modules/debug-bundle/DebugBundleContext.tsx";
|
||||||
import { ProfileProvider } from "@/modules/profile/ProfileContext.tsx";
|
import { ProfileProvider } from "@/modules/profile/ProfileContext.tsx";
|
||||||
|
|
||||||
export const AppLayout = () => {
|
export const AppLayout = () => {
|
||||||
return (
|
return (
|
||||||
<ProfileProvider>
|
<div className={"relative flex h-full flex-col"}>
|
||||||
<DebugBundleProvider>
|
<ProfileProvider>
|
||||||
<div className={"relative flex h-full flex-col"}>
|
<DebugBundleProvider>
|
||||||
<Header />
|
<ClientVersionProvider>
|
||||||
<Outlet />
|
<Header />
|
||||||
<UpdateAvailableBanner />
|
<Outlet />
|
||||||
</div>
|
</ClientVersionProvider>
|
||||||
</DebugBundleProvider>
|
</DebugBundleProvider>
|
||||||
</ProfileProvider>
|
</ProfileProvider>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,19 +1,14 @@
|
|||||||
import { useLocation, useNavigate } from "react-router-dom";
|
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 { ProfileSelector } from "@/components/ProfileSelector.tsx";
|
||||||
import { IconButton } from "@/components/IconButton.tsx";
|
import { IconButton } from "@/components/IconButton.tsx";
|
||||||
import { Tooltip } from "@/components/Tooltip.tsx";
|
import { UpdateHeaderTrigger } from "@/modules/auto-update/UpdateHeaderTrigger.tsx";
|
||||||
import { useStatus } from "@/hooks/useStatus";
|
|
||||||
import { cn } from "@/lib/cn";
|
import { cn } from "@/lib/cn";
|
||||||
|
|
||||||
export const Header = () => {
|
export const Header = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const isSettingsPage = location.pathname.startsWith("/settings");
|
const isSettingsPage = location.pathname.startsWith("/settings");
|
||||||
const { status } = useStatus();
|
|
||||||
const updateAvailable = (status?.events ?? []).some((e) =>
|
|
||||||
Boolean(e.metadata?.["new_version_available"]),
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -24,25 +19,7 @@ export const Header = () => {
|
|||||||
<div className={"ml-20"}>
|
<div className={"ml-20"}>
|
||||||
<ProfileSelector email={"eduard@netbird.io"} />
|
<ProfileSelector email={"eduard@netbird.io"} />
|
||||||
</div>
|
</div>
|
||||||
{updateAvailable && (
|
<UpdateHeaderTrigger />
|
||||||
<Tooltip content={"Update Available"}>
|
|
||||||
<div className={"relative h-11 w-11 flex items-center justify-center"}>
|
|
||||||
<span
|
|
||||||
className={
|
|
||||||
"animate-ping absolute inline-flex h-[15px] w-[15px] rounded-full bg-netbird opacity-20 pointer-events-none"
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<IconButton
|
|
||||||
icon={ArrowUpCircleIcon}
|
|
||||||
iconClassName={"text-netbird"}
|
|
||||||
onClick={() =>
|
|
||||||
navigate("/settings", { state: { tab: "about" } })
|
|
||||||
}
|
|
||||||
className={"absolute inset-0"}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={SettingsIcon}
|
icon={SettingsIcon}
|
||||||
onClick={() => navigate(isSettingsPage ? "/" : "/settings")}
|
onClick={() => navigate(isSettingsPage ? "/" : "/settings")}
|
||||||
|
|||||||
@@ -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<ClientVersionContextValue | null>(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<ClientVersionContextValue>(() => {
|
||||||
|
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 (
|
||||||
|
<ClientVersionContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
<UpdateAvailableBanner />
|
||||||
|
</ClientVersionContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,26 +1,16 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Button } from "@/components/Button";
|
import { Button } from "@/components/Button";
|
||||||
import { useStatus } from "@/hooks/useStatus";
|
import { useClientVersion } from "@/modules/auto-update/ClientVersionContext";
|
||||||
import { cn } from "@/lib/cn";
|
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
|
// TODO: Shown only when management has auto updates enabled + there are updates available + force updates is disabled
|
||||||
export const UpdateAvailableBanner = () => {
|
export const UpdateAvailableBanner = () => {
|
||||||
const { status } = useStatus();
|
const { updateVersion, triggerUpdate } = useClientVersion();
|
||||||
const [dismissed, setDismissed] = useState(false);
|
const [dismissed, setDismissed] = useState(false);
|
||||||
|
|
||||||
if (import.meta.env.DEV) return null;
|
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;
|
if (!updateVersion || dismissed) return null;
|
||||||
|
|
||||||
const triggerUpdate = () => {
|
|
||||||
UpdateSvc.Trigger().catch(() => {});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|||||||
20
client/ui/frontend/src/modules/auto-update/UpdateBadge.tsx
Normal file
20
client/ui/frontend/src/modules/auto-update/UpdateBadge.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { ArrowUpCircleIcon } from "lucide-react";
|
||||||
|
import { cn } from "@/lib/cn";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
size?: number;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const UpdateBadge = ({ size = 15, className }: Props) => {
|
||||||
|
return (
|
||||||
|
<div className={cn("relative flex items-center justify-center", className)}>
|
||||||
|
<span
|
||||||
|
className={
|
||||||
|
"animate-ping absolute inline-flex h-[15px] w-[15px] rounded-full bg-netbird opacity-20 pointer-events-none"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<ArrowUpCircleIcon size={size} className={"text-netbird"} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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 (
|
||||||
|
<Tooltip content={"Update Available"}>
|
||||||
|
<div className={"relative h-11 w-11 flex items-center justify-center"}>
|
||||||
|
<span
|
||||||
|
className={
|
||||||
|
"animate-ping absolute inline-flex h-[15px] w-[15px] rounded-full bg-netbird opacity-20 pointer-events-none"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
icon={ArrowUpCircleIcon}
|
||||||
|
iconClassName={"text-netbird"}
|
||||||
|
onClick={() =>
|
||||||
|
navigate("/settings", { state: { tab: "about" } })
|
||||||
|
}
|
||||||
|
className={"absolute inset-0"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
import { Browser } from "@wailsio/runtime";
|
import { Browser } from "@wailsio/runtime";
|
||||||
import { Update as UpdateSvc } from "@bindings/services";
|
|
||||||
import { Button } from "@/components/Button";
|
import { Button } from "@/components/Button";
|
||||||
import { useStatus } from "@/hooks/useStatus";
|
import { useClientVersion } from "@/modules/auto-update/ClientVersionContext";
|
||||||
import { cn } from "@/lib/cn";
|
import { cn } from "@/lib/cn";
|
||||||
|
|
||||||
function openUrl(url: string) {
|
function openUrl(url: string) {
|
||||||
@@ -18,15 +17,8 @@ function formatLastChecked(date: Date) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function triggerUpdate() {
|
export function UpdateVersionCard() {
|
||||||
UpdateSvc.Trigger().catch(() => {});
|
const { updateVersion, triggerUpdate } = useClientVersion();
|
||||||
}
|
|
||||||
|
|
||||||
export function NetBirdVersionCard() {
|
|
||||||
const { status } = useStatus();
|
|
||||||
const updateVersion = (status?.events ?? [])
|
|
||||||
.map((e) => e.metadata?.["new_version_available"])
|
|
||||||
.find((v): v is string => Boolean(v));
|
|
||||||
|
|
||||||
if (updateVersion) {
|
if (updateVersion) {
|
||||||
return (
|
return (
|
||||||
@@ -2,7 +2,7 @@ import { Browser } from "@wailsio/runtime";
|
|||||||
import netbirdFull from "@/assets/logos/netbird-full.svg";
|
import netbirdFull from "@/assets/logos/netbird-full.svg";
|
||||||
import pkg from "../../../package.json";
|
import pkg from "../../../package.json";
|
||||||
import { useStatus } from "@/hooks/useStatus";
|
import { useStatus } from "@/hooks/useStatus";
|
||||||
import { NetBirdVersionCard } from "@/components/NetBirdVersionCard";
|
import { UpdateVersionCard } from "@/modules/auto-update/UpdateVersionCard";
|
||||||
import { useAccentTrigger } from "@/modules/settings/SettingsAccent";
|
import { useAccentTrigger } from "@/modules/settings/SettingsAccent";
|
||||||
|
|
||||||
const LEGAL_LINKS: { label: string; url: string }[] = [
|
const LEGAL_LINKS: { label: string; url: string }[] = [
|
||||||
@@ -40,7 +40,7 @@ export function SettingsAbout() {
|
|||||||
<p className={"text-sm text-nb-gray-300"}>GUI v{guiVersion}</p>
|
<p className={"text-sm text-nb-gray-300"}>GUI v{guiVersion}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<NetBirdVersionCard />
|
<UpdateVersionCard />
|
||||||
|
|
||||||
<p className={"text-sm text-nb-gray-300 text-center"}>
|
<p className={"text-sm text-nb-gray-300 text-center"}>
|
||||||
© {new Date().getFullYear()} NetBird. All Rights Reserved.
|
© {new Date().getFullYear()} NetBird. All Rights Reserved.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Tooltip } from "@/components/Tooltip.tsx";
|
import { Tooltip } from "@/components/Tooltip.tsx";
|
||||||
import { VerticalTabs } from "@/components/VerticalTabs.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 {
|
import {
|
||||||
ArrowUpCircleIcon,
|
|
||||||
BoltIcon,
|
BoltIcon,
|
||||||
InfoIcon,
|
InfoIcon,
|
||||||
LifeBuoyIcon,
|
LifeBuoyIcon,
|
||||||
@@ -13,21 +13,11 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
export const SettingsNavigationTriggers = () => {
|
export const SettingsNavigationTriggers = () => {
|
||||||
const { status } = useStatus();
|
const { updateAvailable } = useClientVersion();
|
||||||
const updateAvailable = (status?.events ?? []).some((e) =>
|
|
||||||
Boolean(e.metadata?.["new_version_available"]),
|
|
||||||
);
|
|
||||||
|
|
||||||
const aboutAdornment = updateAvailable ? (
|
const aboutAdornment = updateAvailable ? (
|
||||||
<Tooltip content={"Update Available"} side={"right"}>
|
<Tooltip content={"Update Available"} side={"right"}>
|
||||||
<div className={"relative flex items-center justify-center"}>
|
<UpdateBadge />
|
||||||
<span
|
|
||||||
className={
|
|
||||||
"animate-ping absolute inline-flex h-[15px] w-[15px] rounded-full bg-netbird opacity-20 pointer-events-none"
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<ArrowUpCircleIcon size={15} className={"text-netbird"} />
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
) : undefined;
|
) : undefined;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user