mirror of
https://github.com/netbirdio/netbird.git
synced 2026-05-19 07:09:56 +00:00
add update available icon
This commit is contained in:
@@ -21,6 +21,7 @@
|
|||||||
"@radix-ui/react-scroll-area": "^1.2.10",
|
"@radix-ui/react-scroll-area": "^1.2.10",
|
||||||
"@radix-ui/react-switch": "^1.2.6",
|
"@radix-ui/react-switch": "^1.2.6",
|
||||||
"@radix-ui/react-tabs": "^1.1.13",
|
"@radix-ui/react-tabs": "^1.1.13",
|
||||||
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
"@radix-ui/react-visually-hidden": "^1.2.4",
|
"@radix-ui/react-visually-hidden": "^1.2.4",
|
||||||
"@wailsio/runtime": "latest",
|
"@wailsio/runtime": "latest",
|
||||||
"chroma-js": "^3.2.0",
|
"chroma-js": "^3.2.0",
|
||||||
|
|||||||
54
client/ui/frontend/pnpm-lock.yaml
generated
54
client/ui/frontend/pnpm-lock.yaml
generated
@@ -29,6 +29,9 @@ dependencies:
|
|||||||
'@radix-ui/react-tabs':
|
'@radix-ui/react-tabs':
|
||||||
specifier: ^1.1.13
|
specifier: ^1.1.13
|
||||||
version: 1.1.13(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1)
|
version: 1.1.13(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1)
|
||||||
|
'@radix-ui/react-tooltip':
|
||||||
|
specifier: ^1.2.8
|
||||||
|
version: 1.2.8(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1)
|
||||||
'@radix-ui/react-visually-hidden':
|
'@radix-ui/react-visually-hidden':
|
||||||
specifier: ^1.2.4
|
specifier: ^1.2.4
|
||||||
version: 1.2.4(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1)
|
version: 1.2.4(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1)
|
||||||
@@ -1196,6 +1199,37 @@ packages:
|
|||||||
react-dom: 18.3.1(react@18.3.1)
|
react-dom: 18.3.1(react@18.3.1)
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@radix-ui/react-tooltip@1.2.8(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1):
|
||||||
|
resolution: {integrity: sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
'@types/react-dom': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
'@types/react-dom':
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@radix-ui/primitive': 1.1.3
|
||||||
|
'@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.28)(react@18.3.1)
|
||||||
|
'@radix-ui/react-context': 1.1.2(@types/react@18.3.28)(react@18.3.1)
|
||||||
|
'@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1)
|
||||||
|
'@radix-ui/react-id': 1.1.1(@types/react@18.3.28)(react@18.3.1)
|
||||||
|
'@radix-ui/react-popper': 1.2.8(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1)
|
||||||
|
'@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1)
|
||||||
|
'@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1)
|
||||||
|
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1)
|
||||||
|
'@radix-ui/react-slot': 1.2.3(@types/react@18.3.28)(react@18.3.1)
|
||||||
|
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.28)(react@18.3.1)
|
||||||
|
'@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1)
|
||||||
|
'@types/react': 18.3.28
|
||||||
|
'@types/react-dom': 18.3.7(@types/react@18.3.28)
|
||||||
|
react: 18.3.1
|
||||||
|
react-dom: 18.3.1(react@18.3.1)
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@radix-ui/react-use-callback-ref@1.1.1(@types/react@18.3.28)(react@18.3.1):
|
/@radix-ui/react-use-callback-ref@1.1.1(@types/react@18.3.28)(react@18.3.1):
|
||||||
resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==}
|
resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -1306,6 +1340,26 @@ packages:
|
|||||||
react: 18.3.1
|
react: 18.3.1
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1):
|
||||||
|
resolution: {integrity: sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
'@types/react-dom': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
'@types/react-dom':
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1)
|
||||||
|
'@types/react': 18.3.28
|
||||||
|
'@types/react-dom': 18.3.7(@types/react@18.3.28)
|
||||||
|
react: 18.3.1
|
||||||
|
react-dom: 18.3.1(react@18.3.1)
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@radix-ui/react-visually-hidden@1.2.4(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1):
|
/@radix-ui/react-visually-hidden@1.2.4(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1):
|
||||||
resolution: {integrity: sha512-kaeiyGCe844dkb9AVF+rb4yTyb1LiLN/e3es3nLiRyN4dC8AduBYPMnnNlDjX2VDOcvDEiPnRNMJeWCfsX0txg==}
|
resolution: {integrity: sha512-kaeiyGCe844dkb9AVF+rb4yTyb1LiLN/e3es3nLiRyN4dC8AduBYPMnnNlDjX2VDOcvDEiPnRNMJeWCfsX0txg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
|||||||
74
client/ui/frontend/src/components/Tooltip.tsx
Normal file
74
client/ui/frontend/src/components/Tooltip.tsx
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import { ReactNode, useRef, useState } from "react";
|
||||||
|
import * as RTooltip from "@radix-ui/react-tooltip";
|
||||||
|
import { cn } from "@/lib/cn";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
content: ReactNode;
|
||||||
|
children: ReactNode;
|
||||||
|
side?: RTooltip.TooltipContentProps["side"];
|
||||||
|
align?: RTooltip.TooltipContentProps["align"];
|
||||||
|
delayDuration?: number;
|
||||||
|
sideOffset?: number;
|
||||||
|
interactive?: boolean;
|
||||||
|
keepOpenOnClick?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Tooltip = ({
|
||||||
|
content,
|
||||||
|
children,
|
||||||
|
side = "bottom",
|
||||||
|
align = "center",
|
||||||
|
delayDuration = 200,
|
||||||
|
sideOffset = 6,
|
||||||
|
interactive = false,
|
||||||
|
keepOpenOnClick = true,
|
||||||
|
}: Props) => {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const hoveringRef = useRef(false);
|
||||||
|
|
||||||
|
const handleOpenChange = (next: boolean) => {
|
||||||
|
if (!next && keepOpenOnClick && hoveringRef.current) return;
|
||||||
|
setOpen(next);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RTooltip.Provider
|
||||||
|
delayDuration={delayDuration}
|
||||||
|
disableHoverableContent={!interactive}
|
||||||
|
>
|
||||||
|
<RTooltip.Root open={open} onOpenChange={handleOpenChange}>
|
||||||
|
<RTooltip.Trigger
|
||||||
|
asChild
|
||||||
|
onPointerEnter={() => {
|
||||||
|
hoveringRef.current = true;
|
||||||
|
}}
|
||||||
|
onPointerLeave={() => {
|
||||||
|
hoveringRef.current = false;
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</RTooltip.Trigger>
|
||||||
|
<RTooltip.Portal>
|
||||||
|
<RTooltip.Content
|
||||||
|
side={side}
|
||||||
|
align={align}
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
onPointerDownOutside={
|
||||||
|
interactive ? undefined : (e) => e.preventDefault()
|
||||||
|
}
|
||||||
|
className={cn(
|
||||||
|
"z-50 select-none rounded-md border border-nb-gray-850 bg-nb-gray-900 px-2 py-1",
|
||||||
|
"text-xs text-nb-gray-100 shadow-lg",
|
||||||
|
"data-[state=delayed-open]:animate-in data-[state=closed]:animate-out",
|
||||||
|
"data-[state=closed]:fade-out-0 data-[state=delayed-open]:fade-in-0",
|
||||||
|
!interactive && "pointer-events-none",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</RTooltip.Content>
|
||||||
|
</RTooltip.Portal>
|
||||||
|
</RTooltip.Root>
|
||||||
|
</RTooltip.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ComponentType, forwardRef } from "react";
|
import { ComponentType, ReactNode, forwardRef } from "react";
|
||||||
import * as Tabs from "@radix-ui/react-tabs";
|
import * as Tabs from "@radix-ui/react-tabs";
|
||||||
import { LucideProps } from "lucide-react";
|
import { LucideProps } from "lucide-react";
|
||||||
import { cn } from "@/lib/cn";
|
import { cn } from "@/lib/cn";
|
||||||
@@ -33,11 +33,12 @@ type TriggerProps = Tabs.TabsTriggerProps & {
|
|||||||
icon: ComponentType<LucideProps>;
|
icon: ComponentType<LucideProps>;
|
||||||
title: string;
|
title: string;
|
||||||
iconSize?: number;
|
iconSize?: number;
|
||||||
|
adornment?: ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Trigger = forwardRef<HTMLButtonElement, TriggerProps>(
|
const Trigger = forwardRef<HTMLButtonElement, TriggerProps>(
|
||||||
function VerticalTabsTrigger(
|
function VerticalTabsTrigger(
|
||||||
{ icon: Icon, title, iconSize = 16, className, ...props },
|
{ icon: Icon, title, iconSize = 16, adornment, className, ...props },
|
||||||
ref,
|
ref,
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
@@ -67,6 +68,7 @@ const Trigger = forwardRef<HTMLButtonElement, TriggerProps>(
|
|||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
</h2>
|
</h2>
|
||||||
|
{adornment && <div className={"ml-auto mr-2 shrink-0"}>{adornment}</div>}
|
||||||
</Tabs.Trigger>
|
</Tabs.Trigger>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
import { useLocation, useNavigate } from "react-router-dom";
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
import { SettingsIcon } from "lucide-react";
|
import { ArrowUpCircleIcon, 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 { 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
|
||||||
@@ -18,6 +24,25 @@ export const Header = () => {
|
|||||||
<div className={"ml-20"}>
|
<div className={"ml-20"}>
|
||||||
<ProfileSelector email={"eduard@netbird.io"} />
|
<ProfileSelector email={"eduard@netbird.io"} />
|
||||||
</div>
|
</div>
|
||||||
|
{updateAvailable && (
|
||||||
|
<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")}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import { useLocation } from "react-router-dom";
|
||||||
import * as ScrollArea from "@radix-ui/react-scroll-area";
|
import * as ScrollArea from "@radix-ui/react-scroll-area";
|
||||||
import { cn } from "@/lib/cn";
|
import { cn } from "@/lib/cn";
|
||||||
import { MainRightSide } from "@/layouts/MainRightSide.tsx";
|
import { MainRightSide } from "@/layouts/MainRightSide.tsx";
|
||||||
@@ -14,7 +15,13 @@ import { SettingsTroubleshooting } from "@/modules/settings/SettingsTroubleshoot
|
|||||||
import { SettingsAbout } from "@/modules/settings/SettingsAbout.tsx";
|
import { SettingsAbout } from "@/modules/settings/SettingsAbout.tsx";
|
||||||
|
|
||||||
export const Settings = () => {
|
export const Settings = () => {
|
||||||
const [active, setActive] = useState("general");
|
const location = useLocation();
|
||||||
|
const navState = location.state as { tab?: string } | null;
|
||||||
|
const [active, setActive] = useState(navState?.tab ?? "general");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (navState?.tab) setActive(navState.tab);
|
||||||
|
}, [navState?.tab, location.key]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VerticalTabs value={active} onValueChange={setActive} className={"p-4"}>
|
<VerticalTabs value={active} onValueChange={setActive} className={"p-4"}>
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
|
import { Tooltip } from "@/components/Tooltip.tsx";
|
||||||
import { VerticalTabs } from "@/components/VerticalTabs.tsx";
|
import { VerticalTabs } from "@/components/VerticalTabs.tsx";
|
||||||
|
import { useStatus } from "@/hooks/useStatus";
|
||||||
import {
|
import {
|
||||||
|
ArrowUpCircleIcon,
|
||||||
BoltIcon,
|
BoltIcon,
|
||||||
InfoIcon,
|
InfoIcon,
|
||||||
LifeBuoyIcon,
|
LifeBuoyIcon,
|
||||||
@@ -10,6 +13,24 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
export const SettingsNavigationTriggers = () => {
|
export const SettingsNavigationTriggers = () => {
|
||||||
|
const { status } = useStatus();
|
||||||
|
const updateAvailable = (status?.events ?? []).some((e) =>
|
||||||
|
Boolean(e.metadata?.["new_version_available"]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const aboutAdornment = updateAvailable ? (
|
||||||
|
<Tooltip content={"Update Available"} side={"right"}>
|
||||||
|
<div className={"relative 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"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<ArrowUpCircleIcon size={15} className={"text-netbird"} />
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
) : undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={"flex flex-col w-52 shrink-0 items-center select-none"}>
|
<div className={"flex flex-col w-52 shrink-0 items-center select-none"}>
|
||||||
<VerticalTabs.List>
|
<VerticalTabs.List>
|
||||||
@@ -47,6 +68,7 @@ export const SettingsNavigationTriggers = () => {
|
|||||||
value={"about"}
|
value={"about"}
|
||||||
icon={InfoIcon}
|
icon={InfoIcon}
|
||||||
title={"About"}
|
title={"About"}
|
||||||
|
adornment={aboutAdornment}
|
||||||
/>
|
/>
|
||||||
</VerticalTabs.List>
|
</VerticalTabs.List>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user