mirror of
https://github.com/henrygd/beszel.git
synced 2026-03-22 05:36:15 +01:00
feat: Added tooltips for navbar buttons to clear meaning of each one (#1636)
* feat: Added tooltips for navbar buttons to clear meaning of each one. * update tooltips and fix linter errors --------- Co-authored-by: henrygd <hank@henrygd.me>
This commit is contained in:
@@ -1,21 +1,29 @@
|
|||||||
import { useLingui } from "@lingui/react/macro"
|
import { Trans, useLingui } from "@lingui/react/macro"
|
||||||
import { LanguagesIcon } from "lucide-react"
|
import { LanguagesIcon } from "lucide-react"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
|
||||||
import { dynamicActivate } from "@/lib/i18n"
|
import { dynamicActivate } from "@/lib/i18n"
|
||||||
import languages from "@/lib/languages"
|
import languages from "@/lib/languages"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip"
|
||||||
|
|
||||||
export function LangToggle() {
|
export function LangToggle() {
|
||||||
const { i18n } = useLingui()
|
const { i18n } = useLingui()
|
||||||
|
|
||||||
|
const LangTrans = <Trans>Language</Trans>
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger>
|
||||||
<Button variant={"ghost"} size="icon" className="hidden sm:flex">
|
<Tooltip>
|
||||||
<LanguagesIcon className="absolute h-[1.2rem] w-[1.2rem] light:opacity-85" />
|
<TooltipTrigger asChild>
|
||||||
<span className="sr-only">Language</span>
|
<Button variant={"ghost"} size="icon" className="hidden sm:flex">
|
||||||
</Button>
|
<LanguagesIcon className="absolute h-[1.2rem] w-[1.2rem] light:opacity-85" />
|
||||||
|
<span className="sr-only">{LangTrans}</span>
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>{LangTrans}</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent className="grid grid-cols-3">
|
<DropdownMenuContent className="grid grid-cols-3">
|
||||||
{languages.map(([lang, label, e]) => (
|
{languages.map(([lang, label, e]) => (
|
||||||
|
|||||||
@@ -2,19 +2,28 @@ import { t } from "@lingui/core/macro"
|
|||||||
import { MoonStarIcon, SunIcon } from "lucide-react"
|
import { MoonStarIcon, SunIcon } from "lucide-react"
|
||||||
import { useTheme } from "@/components/theme-provider"
|
import { useTheme } from "@/components/theme-provider"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip"
|
||||||
|
import { Trans } from "@lingui/react/macro"
|
||||||
|
|
||||||
export function ModeToggle() {
|
export function ModeToggle() {
|
||||||
const { theme, setTheme } = useTheme()
|
const { theme, setTheme } = useTheme()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Tooltip>
|
||||||
variant={"ghost"}
|
<TooltipTrigger>
|
||||||
size="icon"
|
<Button
|
||||||
aria-label={t`Toggle theme`}
|
variant={"ghost"}
|
||||||
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
|
size="icon"
|
||||||
>
|
aria-label={t`Toggle theme`}
|
||||||
<SunIcon className="h-[1.2rem] w-[1.2rem] transition-all -rotate-90 dark:opacity-0 dark:rotate-0" />
|
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
|
||||||
<MoonStarIcon className="absolute h-[1.2rem] w-[1.2rem] transition-all opacity-0 -rotate-90 dark:opacity-100 dark:rotate-0" />
|
>
|
||||||
</Button>
|
<SunIcon className="h-[1.2rem] w-[1.2rem] transition-all -rotate-90 dark:opacity-0 dark:rotate-0" />
|
||||||
|
<MoonStarIcon className="absolute h-[1.2rem] w-[1.2rem] transition-all opacity-0 -rotate-90 dark:opacity-100 dark:rotate-0" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<Trans>Toggle theme</Trans>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import { LangToggle } from "./lang-toggle"
|
|||||||
import { Logo } from "./logo"
|
import { Logo } from "./logo"
|
||||||
import { ModeToggle } from "./mode-toggle"
|
import { ModeToggle } from "./mode-toggle"
|
||||||
import { $router, basePath, Link, prependBasePath } from "./router"
|
import { $router, basePath, Link, prependBasePath } from "./router"
|
||||||
import { t } from "@lingui/core/macro"
|
import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip"
|
||||||
|
|
||||||
const CommandPalette = lazy(() => import("./command-palette"))
|
const CommandPalette = lazy(() => import("./command-palette"))
|
||||||
|
|
||||||
@@ -49,30 +49,52 @@ export default function Navbar() {
|
|||||||
</Link>
|
</Link>
|
||||||
<SearchButton />
|
<SearchButton />
|
||||||
|
|
||||||
|
{/** biome-ignore lint/a11y/noStaticElementInteractions: ignore */}
|
||||||
<div className="flex items-center ms-auto" onMouseEnter={() => import("@/components/routes/settings/general")}>
|
<div className="flex items-center ms-auto" onMouseEnter={() => import("@/components/routes/settings/general")}>
|
||||||
<Link
|
<Tooltip>
|
||||||
href={getPagePath($router, "containers")}
|
<TooltipTrigger asChild>
|
||||||
className={cn(buttonVariants({ variant: "ghost", size: "icon" }))}
|
<Link
|
||||||
aria-label="Containers"
|
href={getPagePath($router, "containers")}
|
||||||
>
|
className={cn(buttonVariants({ variant: "ghost", size: "icon" }))}
|
||||||
<ContainerIcon className="h-[1.2rem] w-[1.2rem]" strokeWidth={1.5} />
|
aria-label="Containers"
|
||||||
</Link>
|
>
|
||||||
<Link
|
<ContainerIcon className="h-[1.2rem] w-[1.2rem]" strokeWidth={1.5} />
|
||||||
href={getPagePath($router, "smart")}
|
</Link>
|
||||||
className={cn("hidden md:grid", buttonVariants({ variant: "ghost", size: "icon" }))}
|
</TooltipTrigger>
|
||||||
aria-label="S.M.A.R.T."
|
<TooltipContent>
|
||||||
>
|
<Trans>All Containers</Trans>
|
||||||
<HardDriveIcon className="h-[1.2rem] w-[1.2rem]" strokeWidth={1.5} />
|
</TooltipContent>
|
||||||
</Link>
|
</Tooltip>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Link
|
||||||
|
href={getPagePath($router, "smart")}
|
||||||
|
className={cn("hidden md:grid", buttonVariants({ variant: "ghost", size: "icon" }))}
|
||||||
|
aria-label="S.M.A.R.T."
|
||||||
|
>
|
||||||
|
<HardDriveIcon className="h-[1.2rem] w-[1.2rem]" strokeWidth={1.5} />
|
||||||
|
</Link>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<Trans>S.M.A.R.T.</Trans>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
<LangToggle />
|
<LangToggle />
|
||||||
<ModeToggle />
|
<ModeToggle />
|
||||||
<Link
|
<Tooltip>
|
||||||
href={getPagePath($router, "settings", { name: "general" })}
|
<TooltipTrigger asChild>
|
||||||
aria-label="Settings"
|
<Link
|
||||||
className={cn(buttonVariants({ variant: "ghost", size: "icon" }))}
|
href={getPagePath($router, "settings", { name: "general" })}
|
||||||
>
|
aria-label="Settings"
|
||||||
<SettingsIcon className="h-[1.2rem] w-[1.2rem]" />
|
className={cn(buttonVariants({ variant: "ghost", size: "icon" }))}
|
||||||
</Link>
|
>
|
||||||
|
<SettingsIcon className="h-[1.2rem] w-[1.2rem]" />
|
||||||
|
</Link>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<Trans>Settings</Trans>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<button aria-label="User Actions" className={cn(buttonVariants({ variant: "ghost", size: "icon" }))}>
|
<button aria-label="User Actions" className={cn(buttonVariants({ variant: "ghost", size: "icon" }))}>
|
||||||
@@ -135,15 +157,15 @@ export default function Navbar() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const Kbd = ({ children }: { children: React.ReactNode }) => (
|
||||||
|
<kbd className="pointer-events-none inline-flex h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground opacity-100">
|
||||||
|
{children}
|
||||||
|
</kbd>
|
||||||
|
)
|
||||||
|
|
||||||
function SearchButton() {
|
function SearchButton() {
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
|
|
||||||
const Kbd = ({ children }: { children: React.ReactNode }) => (
|
|
||||||
<kbd className="pointer-events-none inline-flex h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground opacity-100">
|
|
||||||
{children}
|
|
||||||
</kbd>
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import { Button } from "@/components/ui/button"
|
|||||||
import { Card } from "@/components/ui/card"
|
import { Card } from "@/components/ui/card"
|
||||||
import { FreeBsdIcon, TuxIcon, WebSocketIcon, WindowsIcon } from "@/components/ui/icons"
|
import { FreeBsdIcon, TuxIcon, WebSocketIcon, WindowsIcon } from "@/components/ui/icons"
|
||||||
import { Separator } from "@/components/ui/separator"
|
import { Separator } from "@/components/ui/separator"
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
|
||||||
import { ConnectionType, connectionTypeLabels, Os, SystemStatus } from "@/lib/enums"
|
import { ConnectionType, connectionTypeLabels, Os, SystemStatus } from "@/lib/enums"
|
||||||
import { cn, formatBytes, getHostDisplayValue, secondsToString, toFixedFloat } from "@/lib/utils"
|
import { cn, formatBytes, getHostDisplayValue, secondsToString, toFixedFloat } from "@/lib/utils"
|
||||||
import type { ChartData, SystemDetailsRecord, SystemRecord } from "@/types"
|
import type { ChartData, SystemDetailsRecord, SystemRecord } from "@/types"
|
||||||
@@ -135,43 +135,41 @@ export default function InfoBar({
|
|||||||
<div>
|
<div>
|
||||||
<h1 className="text-[1.6rem] font-semibold mb-1.5">{system.name}</h1>
|
<h1 className="text-[1.6rem] font-semibold mb-1.5">{system.name}</h1>
|
||||||
<div className="flex flex-wrap items-center gap-3 gap-y-2 text-sm opacity-90">
|
<div className="flex flex-wrap items-center gap-3 gap-y-2 text-sm opacity-90">
|
||||||
<TooltipProvider>
|
<Tooltip>
|
||||||
<Tooltip>
|
<TooltipTrigger asChild>
|
||||||
<TooltipTrigger asChild>
|
<div className="capitalize flex gap-2 items-center">
|
||||||
<div className="capitalize flex gap-2 items-center">
|
<span className={cn("relative flex h-3 w-3")}>
|
||||||
<span className={cn("relative flex h-3 w-3")}>
|
{system.status === SystemStatus.Up && (
|
||||||
{system.status === SystemStatus.Up && (
|
|
||||||
<span
|
|
||||||
className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"
|
|
||||||
style={{ animationDuration: "1.5s" }}
|
|
||||||
></span>
|
|
||||||
)}
|
|
||||||
<span
|
<span
|
||||||
className={cn("relative inline-flex rounded-full h-3 w-3", {
|
className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"
|
||||||
"bg-green-500": system.status === SystemStatus.Up,
|
style={{ animationDuration: "1.5s" }}
|
||||||
"bg-red-500": system.status === SystemStatus.Down,
|
|
||||||
"bg-primary/40": system.status === SystemStatus.Paused,
|
|
||||||
"bg-yellow-500": system.status === SystemStatus.Pending,
|
|
||||||
})}
|
|
||||||
></span>
|
></span>
|
||||||
</span>
|
)}
|
||||||
{translatedStatus}
|
<span
|
||||||
|
className={cn("relative inline-flex rounded-full h-3 w-3", {
|
||||||
|
"bg-green-500": system.status === SystemStatus.Up,
|
||||||
|
"bg-red-500": system.status === SystemStatus.Down,
|
||||||
|
"bg-primary/40": system.status === SystemStatus.Paused,
|
||||||
|
"bg-yellow-500": system.status === SystemStatus.Pending,
|
||||||
|
})}
|
||||||
|
></span>
|
||||||
|
</span>
|
||||||
|
{translatedStatus}
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
{system.info.ct && (
|
||||||
|
<TooltipContent>
|
||||||
|
<div className="flex gap-1 items-center">
|
||||||
|
{system.info.ct === ConnectionType.WebSocket ? (
|
||||||
|
<WebSocketIcon className="size-4" />
|
||||||
|
) : (
|
||||||
|
<ChevronRightSquareIcon className="size-4" strokeWidth={2} />
|
||||||
|
)}
|
||||||
|
{connectionTypeLabels[system.info.ct as ConnectionType]}
|
||||||
</div>
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipContent>
|
||||||
{system.info.ct && (
|
)}
|
||||||
<TooltipContent>
|
</Tooltip>
|
||||||
<div className="flex gap-1 items-center">
|
|
||||||
{system.info.ct === ConnectionType.WebSocket ? (
|
|
||||||
<WebSocketIcon className="size-4" />
|
|
||||||
) : (
|
|
||||||
<ChevronRightSquareIcon className="size-4" strokeWidth={2} />
|
|
||||||
)}
|
|
||||||
{connectionTypeLabels[system.info.ct as ConnectionType]}
|
|
||||||
</div>
|
|
||||||
</TooltipContent>
|
|
||||||
)}
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
|
|
||||||
{systemInfo.map(({ value, label, Icon, hide }) => {
|
{systemInfo.map(({ value, label, Icon, hide }) => {
|
||||||
if (hide || !value) {
|
if (hide || !value) {
|
||||||
@@ -186,12 +184,10 @@ export default function InfoBar({
|
|||||||
<div key={value} className="contents">
|
<div key={value} className="contents">
|
||||||
<Separator orientation="vertical" className="h-4 bg-primary/30" />
|
<Separator orientation="vertical" className="h-4 bg-primary/30" />
|
||||||
{label ? (
|
{label ? (
|
||||||
<TooltipProvider>
|
<Tooltip delayDuration={100}>
|
||||||
<Tooltip delayDuration={150}>
|
<TooltipTrigger asChild>{content}</TooltipTrigger>
|
||||||
<TooltipTrigger asChild>{content}</TooltipTrigger>
|
<TooltipContent>{label}</TooltipContent>
|
||||||
<TooltipContent>{label}</TooltipContent>
|
</Tooltip>
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
) : (
|
) : (
|
||||||
content
|
content
|
||||||
)}
|
)}
|
||||||
@@ -202,26 +198,24 @@ export default function InfoBar({
|
|||||||
</div>
|
</div>
|
||||||
<div className="xl:ms-auto flex items-center gap-2 max-sm:-mb-1">
|
<div className="xl:ms-auto flex items-center gap-2 max-sm:-mb-1">
|
||||||
<ChartTimeSelect className="w-full xl:w-40" agentVersion={chartData.agentVersion} />
|
<ChartTimeSelect className="w-full xl:w-40" agentVersion={chartData.agentVersion} />
|
||||||
<TooltipProvider delayDuration={100}>
|
<Tooltip>
|
||||||
<Tooltip>
|
<TooltipTrigger asChild>
|
||||||
<TooltipTrigger asChild>
|
<Button
|
||||||
<Button
|
aria-label={t`Toggle grid`}
|
||||||
aria-label={t`Toggle grid`}
|
variant="outline"
|
||||||
variant="outline"
|
size="icon"
|
||||||
size="icon"
|
className="hidden xl:flex p-0 text-primary"
|
||||||
className="hidden xl:flex p-0 text-primary"
|
onClick={() => setGrid(!grid)}
|
||||||
onClick={() => setGrid(!grid)}
|
>
|
||||||
>
|
{grid ? (
|
||||||
{grid ? (
|
<LayoutGridIcon className="h-[1.2rem] w-[1.2rem] opacity-75" />
|
||||||
<LayoutGridIcon className="h-[1.2rem] w-[1.2rem] opacity-75" />
|
) : (
|
||||||
) : (
|
<Rows className="h-[1.3rem] w-[1.3rem] opacity-75" />
|
||||||
<Rows className="h-[1.3rem] w-[1.3rem] opacity-75" />
|
)}
|
||||||
)}
|
</Button>
|
||||||
</Button>
|
</TooltipTrigger>
|
||||||
</TooltipTrigger>
|
<TooltipContent>{t`Toggle grid`}</TooltipContent>
|
||||||
<TooltipContent>{t`Toggle grid`}</TooltipContent>
|
</Tooltip>
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { CopyIcon } from "lucide-react"
|
|||||||
import { copyToClipboard } from "@/lib/utils"
|
import { copyToClipboard } from "@/lib/utils"
|
||||||
import { Button } from "./button"
|
import { Button } from "./button"
|
||||||
import { Input } from "./input"
|
import { Input } from "./input"
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./tooltip"
|
import { Tooltip, TooltipContent, TooltipTrigger } from "./tooltip"
|
||||||
|
|
||||||
export function InputCopy({ value, id, name }: { value: string; id: string; name: string }) {
|
export function InputCopy({ value, id, name }: { value: string; id: string; name: string }) {
|
||||||
return (
|
return (
|
||||||
@@ -14,25 +14,23 @@ export function InputCopy({ value, id, name }: { value: string; id: string; name
|
|||||||
"h-6 w-24 bg-linear-to-r rtl:bg-linear-to-l from-transparent to-background to-65% absolute top-2 end-1 pointer-events-none"
|
"h-6 w-24 bg-linear-to-r rtl:bg-linear-to-l from-transparent to-background to-65% absolute top-2 end-1 pointer-events-none"
|
||||||
}
|
}
|
||||||
></div>
|
></div>
|
||||||
<TooltipProvider delayDuration={100} disableHoverableContent>
|
<Tooltip disableHoverableContent={true}>
|
||||||
<Tooltip disableHoverableContent={true}>
|
<TooltipTrigger asChild>
|
||||||
<TooltipTrigger asChild>
|
<Button
|
||||||
<Button
|
type="button"
|
||||||
type="button"
|
variant={"link"}
|
||||||
variant={"link"}
|
className="absolute end-0 top-0"
|
||||||
className="absolute end-0 top-0"
|
onClick={() => copyToClipboard(value)}
|
||||||
onClick={() => copyToClipboard(value)}
|
>
|
||||||
>
|
<CopyIcon className="size-4" />
|
||||||
<CopyIcon className="size-4" />
|
</Button>
|
||||||
</Button>
|
</TooltipTrigger>
|
||||||
</TooltipTrigger>
|
<TooltipContent>
|
||||||
<TooltipContent>
|
<p>
|
||||||
<p>
|
<Trans>Click to copy</Trans>
|
||||||
<Trans>Click to copy</Trans>
|
</p>
|
||||||
</p>
|
</TooltipContent>
|
||||||
</TooltipContent>
|
</Tooltip>
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type * as React from "react"
|
|||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
function TooltipProvider({ delayDuration = 0, ...props }: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
|
function TooltipProvider({ delayDuration = 50, ...props }: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
|
||||||
return <TooltipPrimitive.Provider data-slot="tooltip-provider" delayDuration={delayDuration} {...props} />
|
return <TooltipPrimitive.Provider data-slot="tooltip-provider" delayDuration={delayDuration} {...props} />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user