mirror of
https://github.com/henrygd/beszel.git
synced 2026-03-27 07:56:19 +01:00
feat: use dropdown menu as navigation on mobile devices (#1840)
Co-authored-by: henrygd <hank@henrygd.me>
This commit is contained in:
@@ -1,8 +1,8 @@
|
|||||||
import { msg, t } from "@lingui/core/macro"
|
import { t } from "@lingui/core/macro"
|
||||||
import { Trans } from "@lingui/react/macro"
|
import { Trans } from "@lingui/react/macro"
|
||||||
import { useStore } from "@nanostores/react"
|
import { useStore } from "@nanostores/react"
|
||||||
import { getPagePath } from "@nanostores/router"
|
import { getPagePath } from "@nanostores/router"
|
||||||
import { ChevronDownIcon, ExternalLinkIcon, PlusIcon } from "lucide-react"
|
import { ChevronDownIcon, ExternalLinkIcon } from "lucide-react"
|
||||||
import { memo, useEffect, useRef, useState } from "react"
|
import { memo, useEffect, useRef, useState } from "react"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import {
|
import {
|
||||||
@@ -12,7 +12,6 @@ import {
|
|||||||
DialogFooter,
|
DialogFooter,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
|
||||||
} from "@/components/ui/dialog"
|
} from "@/components/ui/dialog"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import { Label } from "@/components/ui/label"
|
import { Label } from "@/components/ui/label"
|
||||||
@@ -35,28 +34,19 @@ import { DropdownMenu, DropdownMenuTrigger } from "./ui/dropdown-menu"
|
|||||||
import { AppleIcon, DockerIcon, FreeBsdIcon, TuxIcon, WindowsIcon } from "./ui/icons"
|
import { AppleIcon, DockerIcon, FreeBsdIcon, TuxIcon, WindowsIcon } from "./ui/icons"
|
||||||
import { InputCopy } from "./ui/input-copy"
|
import { InputCopy } from "./ui/input-copy"
|
||||||
|
|
||||||
export function AddSystemButton({ className }: { className?: string }) {
|
// To avoid a refactor of the dialog, we will just keep this function as a "skeleton" for the actual dialog
|
||||||
if (isReadOnlyUser()) {
|
export function AddSystemDialog({ open, setOpen }: { open: boolean; setOpen: (open: boolean) => void }) {
|
||||||
return null
|
|
||||||
}
|
|
||||||
const [open, setOpen] = useState(false)
|
|
||||||
const opened = useRef(false)
|
const opened = useRef(false)
|
||||||
if (open) {
|
if (open) {
|
||||||
opened.current = true
|
opened.current = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isReadOnlyUser()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
<DialogTrigger asChild>
|
|
||||||
<Button variant="outline" className={cn("flex gap-1 max-xs:h-[2.4rem]", className)}>
|
|
||||||
<PlusIcon className="h-4 w-4 450:-ms-1" />
|
|
||||||
<span className="hidden 450:inline">
|
|
||||||
<Trans>
|
|
||||||
Add <span className="hidden sm:inline">System</span>
|
|
||||||
</Trans>
|
|
||||||
</span>
|
|
||||||
</Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
{opened.current && <SystemDialog setOpen={setOpen} />}
|
{opened.current && <SystemDialog setOpen={setOpen} />}
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import {
|
|||||||
HardDriveIcon,
|
HardDriveIcon,
|
||||||
LogOutIcon,
|
LogOutIcon,
|
||||||
LogsIcon,
|
LogsIcon,
|
||||||
|
MenuIcon,
|
||||||
|
PlusIcon,
|
||||||
SearchIcon,
|
SearchIcon,
|
||||||
ServerIcon,
|
ServerIcon,
|
||||||
SettingsIcon,
|
SettingsIcon,
|
||||||
@@ -21,15 +23,18 @@ import {
|
|||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuLabel,
|
DropdownMenuLabel,
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuSub,
|
||||||
|
DropdownMenuSubContent,
|
||||||
|
DropdownMenuSubTrigger,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu"
|
} from "@/components/ui/dropdown-menu"
|
||||||
import { isAdmin, isReadOnlyUser, logOut, pb } from "@/lib/api"
|
import { isAdmin, isReadOnlyUser, logOut, pb } from "@/lib/api"
|
||||||
import { cn, runOnce } from "@/lib/utils"
|
import { cn, runOnce } from "@/lib/utils"
|
||||||
import { AddSystemButton } from "./add-system"
|
import { AddSystemDialog } from "./add-system"
|
||||||
import { LangToggle } from "./lang-toggle"
|
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, navigate, prependBasePath } from "./router"
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip"
|
import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip"
|
||||||
|
|
||||||
const CommandPalette = lazy(() => import("./command-palette"))
|
const CommandPalette = lazy(() => import("./command-palette"))
|
||||||
@@ -37,8 +42,18 @@ const CommandPalette = lazy(() => import("./command-palette"))
|
|||||||
const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0
|
const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0
|
||||||
|
|
||||||
export default function Navbar() {
|
export default function Navbar() {
|
||||||
|
const [addSystemDialogOpen, setAddSystemDialogOpen] = useState(false)
|
||||||
|
const [commandPaletteOpen, setCommandPaletteOpen] = useState(false)
|
||||||
|
|
||||||
|
const AdminLinks = AdminDropdownGroup()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center h-14 md:h-16 bg-card px-4 pe-3 sm:px-6 border border-border/60 bt-0 rounded-md my-4">
|
<div className="flex items-center h-14 md:h-16 bg-card px-4 pe-3 sm:px-6 border border-border/60 bt-0 rounded-md my-4">
|
||||||
|
<Suspense>
|
||||||
|
<CommandPalette open={commandPaletteOpen} setOpen={setCommandPaletteOpen} />
|
||||||
|
</Suspense>
|
||||||
|
<AddSystemDialog open={addSystemDialogOpen} setOpen={setAddSystemDialogOpen} />
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
href={basePath}
|
href={basePath}
|
||||||
aria-label="Home"
|
aria-label="Home"
|
||||||
@@ -47,10 +62,90 @@ export default function Navbar() {
|
|||||||
>
|
>
|
||||||
<Logo className="h-[1.1rem] md:h-5 fill-foreground" />
|
<Logo className="h-[1.1rem] md:h-5 fill-foreground" />
|
||||||
</Link>
|
</Link>
|
||||||
<SearchButton />
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="hidden md:block text-sm text-muted-foreground px-4"
|
||||||
|
onClick={() => setCommandPaletteOpen(true)}
|
||||||
|
>
|
||||||
|
<span className="flex items-center">
|
||||||
|
<SearchIcon className="me-1.5 h-4 w-4" />
|
||||||
|
<Trans>Search</Trans>
|
||||||
|
<span className="flex items-center ms-3.5">
|
||||||
|
<Kbd>{isMac ? "⌘" : "Ctrl"}</Kbd>
|
||||||
|
<Kbd>K</Kbd>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* mobile menu */}
|
||||||
|
<div className="ms-auto flex items-center text-xl md:hidden">
|
||||||
|
<ModeToggle />
|
||||||
|
<Button variant="ghost" size="icon" onClick={() => setCommandPaletteOpen(true)}>
|
||||||
|
<SearchIcon className="h-[1.2rem] w-[1.2rem]" />
|
||||||
|
</Button>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger
|
||||||
|
onMouseEnter={() => import("@/components/routes/settings/general")}
|
||||||
|
className="ms-3"
|
||||||
|
aria-label="Open Menu"
|
||||||
|
>
|
||||||
|
<MenuIcon />
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuLabel className="max-w-40 truncate">{pb.authStore.record?.email}</DropdownMenuLabel>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuGroup>
|
||||||
|
<DropdownMenuItem onClick={() => navigate(getPagePath($router, "home"))} className="flex items-center">
|
||||||
|
<ContainerIcon className="h-4 w-4 me-2.5" strokeWidth={1.5} />
|
||||||
|
<Trans>All Containers</Trans>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={() => navigate(getPagePath($router, "smart"))} className="flex items-center">
|
||||||
|
<HardDriveIcon className="h-4 w-4 me-2.5" strokeWidth={1.5} />
|
||||||
|
<span>S.M.A.R.T.</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => navigate(getPagePath($router, "settings", { name: "general" }))}
|
||||||
|
className="flex items-center"
|
||||||
|
>
|
||||||
|
<SettingsIcon className="h-4 w-4 me-2.5" />
|
||||||
|
<Trans>Settings</Trans>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
{isAdmin() && (
|
||||||
|
<DropdownMenuSub>
|
||||||
|
<DropdownMenuSubTrigger>
|
||||||
|
<UserIcon className="h-4 w-4 me-2.5" />
|
||||||
|
<Trans>Admin</Trans>
|
||||||
|
</DropdownMenuSubTrigger>
|
||||||
|
<DropdownMenuSubContent>{AdminLinks}</DropdownMenuSubContent>
|
||||||
|
</DropdownMenuSub>
|
||||||
|
)}
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="flex items-center"
|
||||||
|
onSelect={() => {
|
||||||
|
setAddSystemDialogOpen(true)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PlusIcon className="h-4 w-4 me-2.5" />
|
||||||
|
<Trans>Add System</Trans>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuGroup>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuGroup>
|
||||||
|
<DropdownMenuItem onSelect={logOut} className="flex items-center">
|
||||||
|
<LogOutIcon className="h-4 w-4 me-2.5" />
|
||||||
|
<Trans>Log Out</Trans>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuGroup>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* desktop nav */}
|
||||||
{/** biome-ignore lint/a11y/noStaticElementInteractions: ignore */}
|
{/** biome-ignore lint/a11y/noStaticElementInteractions: ignore */}
|
||||||
<div className="flex items-center ms-auto" onMouseEnter={() => import("@/components/routes/settings/general")}>
|
<div
|
||||||
|
className="hidden md:flex items-center ms-auto"
|
||||||
|
onMouseEnter={() => import("@/components/routes/settings/general")}
|
||||||
|
>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Link
|
<Link
|
||||||
@@ -102,45 +197,12 @@ export default function Navbar() {
|
|||||||
<DropdownMenuContent align={isReadOnlyUser() ? "end" : "center"} className="min-w-44">
|
<DropdownMenuContent align={isReadOnlyUser() ? "end" : "center"} className="min-w-44">
|
||||||
<DropdownMenuLabel>{pb.authStore.record?.email}</DropdownMenuLabel>
|
<DropdownMenuLabel>{pb.authStore.record?.email}</DropdownMenuLabel>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuGroup>
|
{isAdmin() && (
|
||||||
{isAdmin() && (
|
<>
|
||||||
<>
|
{AdminLinks}
|
||||||
<DropdownMenuItem asChild>
|
<DropdownMenuSeparator />
|
||||||
<a href={prependBasePath("/_/")} target="_blank">
|
</>
|
||||||
<UsersIcon className="me-2.5 h-4 w-4" />
|
)}
|
||||||
<span>
|
|
||||||
<Trans>Users</Trans>
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem asChild>
|
|
||||||
<a href={prependBasePath("/_/#/collections?collection=systems")} target="_blank">
|
|
||||||
<ServerIcon className="me-2.5 h-4 w-4" />
|
|
||||||
<span>
|
|
||||||
<Trans>Systems</Trans>
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem asChild>
|
|
||||||
<a href={prependBasePath("/_/#/logs")} target="_blank">
|
|
||||||
<LogsIcon className="me-2.5 h-4 w-4" />
|
|
||||||
<span>
|
|
||||||
<Trans>Logs</Trans>
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem asChild>
|
|
||||||
<a href={prependBasePath("/_/#/settings/backups")} target="_blank">
|
|
||||||
<DatabaseBackupIcon className="me-2.5 h-4 w-4" />
|
|
||||||
<span>
|
|
||||||
<Trans>Backups</Trans>
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</DropdownMenuGroup>
|
|
||||||
<DropdownMenuItem onSelect={logOut}>
|
<DropdownMenuItem onSelect={logOut}>
|
||||||
<LogOutIcon className="me-2.5 h-4 w-4" />
|
<LogOutIcon className="me-2.5 h-4 w-4" />
|
||||||
<span>
|
<span>
|
||||||
@@ -149,7 +211,10 @@ export default function Navbar() {
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
<AddSystemButton className="ms-2" />
|
<Button variant="outline" className="flex gap-1 ms-2" onClick={() => setAddSystemDialogOpen(true)}>
|
||||||
|
<PlusIcon className="h-4 w-4 -ms-1" />
|
||||||
|
<Trans>Add System</Trans>
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -161,28 +226,41 @@ const Kbd = ({ children }: { children: React.ReactNode }) => (
|
|||||||
</kbd>
|
</kbd>
|
||||||
)
|
)
|
||||||
|
|
||||||
function SearchButton() {
|
function AdminDropdownGroup() {
|
||||||
const [open, setOpen] = useState(false)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<DropdownMenuGroup>
|
||||||
<Button
|
<DropdownMenuItem asChild>
|
||||||
variant="outline"
|
<a href={prependBasePath("/_/")} target="_blank">
|
||||||
className="hidden md:block text-sm text-muted-foreground px-4"
|
<UsersIcon className="me-2.5 h-4 w-4" />
|
||||||
onClick={() => setOpen(true)}
|
<span>
|
||||||
>
|
<Trans>Users</Trans>
|
||||||
<span className="flex items-center">
|
|
||||||
<SearchIcon className="me-1.5 h-4 w-4" />
|
|
||||||
<Trans>Search</Trans>
|
|
||||||
<span className="flex items-center ms-3.5">
|
|
||||||
<Kbd>{isMac ? "⌘" : "Ctrl"}</Kbd>
|
|
||||||
<Kbd>K</Kbd>
|
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</a>
|
||||||
</Button>
|
</DropdownMenuItem>
|
||||||
<Suspense>
|
<DropdownMenuItem asChild>
|
||||||
<CommandPalette open={open} setOpen={setOpen} />
|
<a href={prependBasePath("/_/#/collections?collection=systems")} target="_blank">
|
||||||
</Suspense>
|
<ServerIcon className="me-2.5 h-4 w-4" />
|
||||||
</>
|
<span>
|
||||||
|
<Trans>Systems</Trans>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem asChild>
|
||||||
|
<a href={prependBasePath("/_/#/logs")} target="_blank">
|
||||||
|
<LogsIcon className="me-2.5 h-4 w-4" />
|
||||||
|
<span>
|
||||||
|
<Trans>Logs</Trans>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem asChild>
|
||||||
|
<a href={prependBasePath("/_/#/settings/backups")} target="_blank">
|
||||||
|
<DatabaseBackupIcon className="me-2.5 h-4 w-4" />
|
||||||
|
<span>
|
||||||
|
<Trans>Backups</Trans>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuGroup>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user