gitscope
← marzneshin / Diff · 3d1a4b1 → 3d1a4b1
Changes 3d1a4b1..3d1a4b1
Patch commit
diff --git a/dashboard/package.json b/dashboard/package.json
index 8578441..1c7f1cd 100644
--- a/dashboard/package.json
+++ b/dashboard/package.json
@@ -64,6 +64,7 @@
"@vitest/coverage-istanbul": "^1.6.0",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
+ "cmdk": "^1.0.0",
"date-fns": "^3.6.0",
"dayjs": "^1.11.11",
"framer-motion": "^11.2.4",
diff --git a/dashboard/pnpm-lock.yaml b/dashboard/pnpm-lock.yaml
index 6722a89..96a6787 100644
--- a/dashboard/pnpm-lock.yaml
+++ b/dashboard/pnpm-lock.yaml
@@ -152,6 +152,9 @@ dependencies:
clsx:
specifier: ^2.1.1
version: 2.1.1
+ cmdk:
+ specifier: ^1.0.0
+ version: 1.0.0(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1)
date-fns:
specifier: ^3.6.0
version: 3.6.0
@@ -7271,6 +7274,21 @@ packages:
engines: {node: '>=6'}
dev: false
+ /cmdk@1.0.0(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1):
+ resolution: {integrity: sha512-gDzVf0a09TvoJ5jnuPvygTB77+XdOSwEmJ88L6XPFPlv7T3RxbP9jgenfylrAMD0+Le1aO0nVjQUzl2g+vjz5Q==}
+ peerDependencies:
+ react: ^18.0.0
+ react-dom: ^18.0.0
+ dependencies:
+ '@radix-ui/react-dialog': 1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1)
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ transitivePeerDependencies:
+ - '@types/react'
+ - '@types/react-dom'
+ dev: false
+
/codemirror@5.65.16:
resolution: {integrity: sha512-br21LjYmSlVL0vFCPWPfhzUCT34FM/pAdK7rRIZwa0rrtrIdotvP4Oh4GUHsu2E3IrQMCfRkL/fN3ytMNxVQvg==}
dev: false
diff --git a/dashboard/src/components/header/index.tsx b/dashboard/src/components/header/index.tsx
index 9e75aa0..5bf28ec 100644
--- a/dashboard/src/components/header/index.tsx
+++ b/dashboard/src/components/header/index.tsx
@@ -2,24 +2,26 @@ import { LanguageSwitch } from "@marzneshin/features/language-switch";
import { ThemeToggle } from "@marzneshin/features/theme-switch";
import { Logout } from "@marzneshin/features/auth";
import { Link } from "@tanstack/react-router";
-import { FC, PropsWithChildren } from "react";
+import { CommandBox } from "@marzneshin/features/search-command";
+import type { FC, PropsWithChildren } from "react";
export const Header: FC<PropsWithChildren> = ({ children }) => {
- return (
- <div className="flex flex-row justify-between items-center p-1 px-4 w-full h-full bg-primary text-primary-foreground dark:bg-primary-foreground">
- <div className="flex flex-row gap-4 justify-center items-center">
- <Link to="/">
- <div className="flex flex-col justify-center items-center p-2 h-10 text-2xl font-bold bg-gray-800 rounded-lg font-header text-secondary border-accent dark:text-secondary-foreground">
- M
- </div>
- </Link>
- {children}
- </div>
- <div className="flex flex-row gap-2 h-10 justify-reverse">
- <Logout />
- <LanguageSwitch />
- <ThemeToggle />
- </div>
- </div>
- );
+ return (
+ <div className="flex flex-row justify-between items-center p-1 px-4 w-full h-full bg-primary text-primary-foreground dark:bg-primary-foreground">
+ <div className="flex flex-row gap-4 justify-center items-center">
+ <Link to="/">
+ <div className="flex flex-col justify-center items-center p-2 h-10 text-2xl font-bold bg-gray-800 rounded-lg font-header text-secondary border-accent dark:text-secondary-foreground">
+ M
+ </div>
+ </Link>
+ {children}
+ </div>
+ <CommandBox />
+ <div className="flex flex-row gap-2 h-10 justify-reverse">
+ <Logout />
+ <LanguageSwitch />
+ <ThemeToggle />
+ </div>
+ </div>
+ );
};
diff --git a/dashboard/src/components/ui/command.tsx b/dashboard/src/components/ui/command.tsx
new file mode 100644
index 0000000..3152023
--- /dev/null
+++ b/dashboard/src/components/ui/command.tsx
@@ -0,0 +1,153 @@
+import * as React from "react"
+import { type DialogProps } from "@radix-ui/react-dialog"
+import { Command as CommandPrimitive } from "cmdk"
+import { Search } from "lucide-react"
+
+import { cn } from "@marzneshin/utils"
+import { Dialog, DialogContent } from "@marzneshin/components/ui/dialog"
+
+const Command = React.forwardRef<
+ React.ElementRef<typeof CommandPrimitive>,
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive>
+>(({ className, ...props }, ref) => (
+ <CommandPrimitive
+ ref={ref}
+ className={cn(
+ "flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
+ className
+ )}
+ {...props}
+ />
+))
+Command.displayName = CommandPrimitive.displayName
+
+interface CommandDialogProps extends DialogProps {}
+
+const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
+ return (
+ <Dialog {...props}>
+ <DialogContent className="overflow-hidden p-0 shadow-lg">
+ <Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
+ {children}
+ </Command>
+ </DialogContent>
+ </Dialog>
+ )
+}
+
+const CommandInput = React.forwardRef<
+ React.ElementRef<typeof CommandPrimitive.Input>,
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
+>(({ className, ...props }, ref) => (
+ <div className="flex items-center border-b px-3" cmdk-input-wrapper="">
+ <Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
+ <CommandPrimitive.Input
+ ref={ref}
+ className={cn(
+ "flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
+ className
+ )}
+ {...props}
+ />
+ </div>
+))
+
+CommandInput.displayName = CommandPrimitive.Input.displayName
+
+const CommandList = React.forwardRef<
+ React.ElementRef<typeof CommandPrimitive.List>,
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
+>(({ className, ...props }, ref) => (
+ <CommandPrimitive.List
+ ref={ref}
+ className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
+ {...props}
+ />
+))
+
+CommandList.displayName = CommandPrimitive.List.displayName
+
+const CommandEmpty = React.forwardRef<
+ React.ElementRef<typeof CommandPrimitive.Empty>,
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
+>((props, ref) => (
+ <CommandPrimitive.Empty
+ ref={ref}
+ className="py-6 text-center text-sm"
+ {...props}
+ />
+))
+
+CommandEmpty.displayName = CommandPrimitive.Empty.displayName
+
+const CommandGroup = React.forwardRef<
+ React.ElementRef<typeof CommandPrimitive.Group>,
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
+>(({ className, ...props }, ref) => (
+ <CommandPrimitive.Group
+ ref={ref}
+ className={cn(
+ "overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
+ className
+ )}
+ {...props}
+ />
+))
+
+CommandGroup.displayName = CommandPrimitive.Group.displayName
+
+const CommandSeparator = React.forwardRef<
+ React.ElementRef<typeof CommandPrimitive.Separator>,
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
+>(({ className, ...props }, ref) => (
+ <CommandPrimitive.Separator
+ ref={ref}
+ className={cn("-mx-1 h-px bg-border", className)}
+ {...props}
+ />
+))
+CommandSeparator.displayName = CommandPrimitive.Separator.displayName
+
+const CommandItem = React.forwardRef<
+ React.ElementRef<typeof CommandPrimitive.Item>,
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
+>(({ className, ...props }, ref) => (
+ <CommandPrimitive.Item
+ ref={ref}
+ className={cn(
+ "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50",
+ className
+ )}
+ {...props}
+ />
+))
+
+CommandItem.displayName = CommandPrimitive.Item.displayName
+
+const CommandShortcut = ({
+ className,
+ ...props
+}: React.HTMLAttributes<HTMLSpanElement>) => {
+ return (
+ <span
+ className={cn(
+ "ml-auto text-xs tracking-widest text-muted-foreground",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+CommandShortcut.displayName = "CommandShortcut"
+
+export {
+ Command,
+ CommandDialog,
+ CommandInput,
+ CommandList,
+ CommandEmpty,
+ CommandGroup,
+ CommandItem,
+ CommandShortcut,
+ CommandSeparator,
+}
diff --git a/dashboard/src/components/ui/index.ts b/dashboard/src/components/ui/index.ts
index e633c4e..4a47b94 100644
--- a/dashboard/src/components/ui/index.ts
+++ b/dashboard/src/components/ui/index.ts
@@ -32,3 +32,4 @@ export * from "./toggle-group";
export * from "./textarea";
export * from "./radio-group";
export * from "./accordion";
+export * from "./command";
diff --git a/dashboard/src/features/search-command/command-box.tsx b/dashboard/src/features/search-command/command-box.tsx
new file mode 100644
index 0000000..b6fc29d
--- /dev/null
+++ b/dashboard/src/features/search-command/command-box.tsx
@@ -0,0 +1,102 @@
+import {
+ UsersIcon,
+ ServerIcon,
+ BoxIcon,
+ ServerCogIcon,
+ SettingsIcon,
+} from "lucide-react";
+import { useState, useEffect } from "react";
+import { useTranslation } from "react-i18next";
+import {
+ CommandDialog,
+ CommandEmpty,
+ CommandGroup,
+ CommandInput,
+ CommandItem,
+ CommandList,
+ CommandSeparator,
+} from "@marzneshin/components";
+import { useNavigate } from "@tanstack/react-router";
+import { SearchBox } from "./search-box";
+
+export function CommandBox() {
+ const [open, setOpen] = useState(false);
+ const { t } = useTranslation();
+ const navigate = useNavigate();
+
+ useEffect(() => {
+ const down = (e: KeyboardEvent) => {
+ if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
+ e.preventDefault();
+ setOpen((open) => !open);
+ }
+ };
+ document.addEventListener("keydown", down);
+ return () => document.removeEventListener("keydown", down);
+ }, []);
+
+ return (
+ <>
+ <SearchBox onClick={() => setOpen(true)} />
+ <CommandDialog open={open} onOpenChange={setOpen}>
+ <CommandInput
+ placeholder="Type a command or search..."
+ className="focus:ring-0 ring-0 m-1 border-none"
+ />
+ <CommandList>
+ <CommandEmpty>No results found.</CommandEmpty>
+ <CommandGroup heading="Pages">
+ <CommandItem
+ onSelect={() => {
+ navigate({ to: "/users" });
+ setOpen(false);
+ }}
+ >
+ <UsersIcon className="mr-2 size-4" />
+ <span>{t("users")}</span>
+ </CommandItem>
+ <CommandItem
+ onSelect={() => {
+ navigate({ to: "/services" });
+ setOpen(false);
+ }}
+ >
+ <ServerIcon className="mr-2 size-4" />
+ <span>{t("services")}</span>
+ </CommandItem>
+ <CommandItem
+ onSelect={() => {
+ navigate({ to: "/nodes" });
+ setOpen(false);
+ }}
+ >
+ <BoxIcon className="mr-2 h-4 w-4" />
+ <span>{t("nodes")}</span>
+ </CommandItem>
+ <CommandItem
+ onSelect={() => {
+ navigate({ to: "/hosts" });
+ setOpen(false);
+ }}
+ >
+ <ServerCogIcon className="mr-2 h-4 w-4" />
+ <span>{t("hosts")}</span>
+ </CommandItem>
+ </CommandGroup>
+ <CommandSeparator />
+ <CommandGroup heading="Settings">
+ <CommandItem
+ onSelect={() => {
+ navigate({ to: "/settings" });
+ setOpen(false);
+ }}
+ >
+ <SettingsIcon className="mr-2 h-4 w-4" />
+ <span>{t("settings")}</span>
+ </CommandItem>
+ </CommandGroup>
+ </CommandList>
+ </CommandDialog>
+ </>
+ );
+}
diff --git a/dashboard/src/features/search-command/index.ts b/dashboard/src/features/search-command/index.ts
new file mode 100644
index 0000000..40b9c43
--- /dev/null
+++ b/dashboard/src/features/search-command/index.ts
@@ -0,0 +1,2 @@
+export * from "./command-box";
+export * from "./search-box";
diff --git a/dashboard/src/features/search-command/search-box.tsx b/dashboard/src/features/search-command/search-box.tsx
new file mode 100644
index 0000000..6c948b3
--- /dev/null
+++ b/dashboard/src/features/search-command/search-box.tsx
@@ -0,0 +1,19 @@
+import type { FC, HTMLAttributes } from "react";
+import { SearchIcon } from "lucide-react";
+
+export const SearchBox: FC<HTMLAttributes<HTMLDivElement>> = ({ ...props }) => {
+ return (
+ <div
+ className="bg-gray-800 p-2 md:w-60 flex flex-row items-center rounded-md justify-between"
+ {...props}
+ >
+ <div className="flex flex-row items-center gap-1">
+ <SearchIcon className="size-4 text-muted dark:text-muted-foreground" />
+ <p className="text-sm text-muted dark:text-muted-foreground ">Search</p>
+ </div>
+ <kbd className="pointer-events-none md:inline-flex h-5 select-none items-center gap-1 rounded bg-gray-600 px-1.5 font-mono text-[10px] font-medium text-muted dark:text-muted-foreground hidden">
+ <span className="text-xs">CTRL</span>K
+ </kbd>
+ </div>
+ );
+};
diff --git a/dashboard/src/routes/_dashboard.tsx b/dashboard/src/routes/_dashboard.tsx
index e5c206f..23e4169 100644
--- a/dashboard/src/routes/_dashboard.tsx
+++ b/dashboard/src/routes/_dashboard.tsx
@@ -1,88 +1,103 @@
import {
- Drawer,
- DrawerContent,
- Header,
- ResizableHandle,
- ResizablePanel,
- ResizablePanelGroup,
- Toaster,
-} from '@marzneshin/components'
-import { NavigationDirectory } from '@marzneshin/components/nav-dir';
-import { useAuth } from '@marzneshin/features/auth'
-import { DashboardSidebar, ToggleButton, sidebarItems } from '@marzneshin/features/sidebar';
-import { usePanelToggle } from '@marzneshin/features/sidebar/use-panel-toggle';
-import { useScreenBreakpoint } from '@marzneshin/hooks/use-screen-breakpoint';
-import { cn } from '@marzneshin/utils';
-import { Outlet, createFileRoute, redirect } from '@tanstack/react-router'
+ Drawer,
+ DrawerContent,
+ Header,
+ ResizableHandle,
+ ResizablePanel,
+ ResizablePanelGroup,
+ Toaster,
+} from "@marzneshin/components";
+import { useAuth } from "@marzneshin/features/auth";
+import {
+ DashboardSidebar,
+ ToggleButton,
+} from "@marzneshin/features/sidebar";
+import { usePanelToggle } from "@marzneshin/features/sidebar/use-panel-toggle";
+import { useScreenBreakpoint } from "@marzneshin/hooks/use-screen-breakpoint";
+import { cn } from "@marzneshin/utils";
+import { Outlet, createFileRoute, redirect } from "@tanstack/react-router";
export const DashboardLayout = () => {
- const isDesktop = useScreenBreakpoint("md");
- const { collapsed, panelRef, open, setCollapsed, setOpen, toggleCollapse, toggleOpen } = usePanelToggle(isDesktop);
+ const isDesktop = useScreenBreakpoint("md");
+ const {
+ collapsed,
+ panelRef,
+ open,
+ setCollapsed,
+ setOpen,
+ toggleCollapse,
+ toggleOpen,
+ } = usePanelToggle(isDesktop);
- return (
- <div className="flex flex-col w-screen h-screen">
- <header className="h-[3.5rem]">
- <Header>
- <ToggleButton
- isDesktop={isDesktop}
- collapsed={collapsed}
- open={open}
- onToggle={isDesktop ? toggleCollapse : toggleOpen}
- />
- <NavigationDirectory sidebar={sidebarItems} />
- </Header>
- </header>
- {isDesktop ?
- (
- <ResizablePanelGroup direction="horizontal" className="block sm:hidden">
- <ResizablePanel
- collapsible
- collapsedSize={2}
- onCollapse={() => setCollapsed(true)}
- onExpand={() => setCollapsed(false)}
- minSize={15}
- className={cn("w-[120px] min-w-[70px]")}
- defaultSize={20}
- ref={panelRef}
- maxSize={30}>
- <DashboardSidebar collapsed={collapsed} setCollapsed={setCollapsed} />
- </ResizablePanel>
- <ResizableHandle withHandle className="w-[2px]" />
- <ResizablePanel>
- <main>
- <Outlet />
- <Toaster position="top-center" />
- </main>
- </ResizablePanel>
- </ResizablePanelGroup>
- ) : (
- <div>
- <aside>
- <Drawer open={open} onOpenChange={setOpen}>
- <DrawerContent>
- <DashboardSidebar collapsed={collapsed} setCollapsed={setCollapsed} />
- </DrawerContent>
- </Drawer>
- </aside>
- <main className="sm:block">
- <Outlet />
- <Toaster position="top-center" />
- </main>
- </div>
- )}
- </div>
- )
-}
+ return (
+ <div className="flex flex-col w-screen h-screen">
+ <header className="h-[3.5rem]">
+ <Header>
+ <ToggleButton
+ isDesktop={isDesktop}
+ collapsed={collapsed}
+ open={open}
+ onToggle={isDesktop ? toggleCollapse : toggleOpen}
+ />
+ </Header>
+ </header>
+ {isDesktop ? (
+ <ResizablePanelGroup direction="horizontal" className="block sm:hidden">
+ <ResizablePanel
+ collapsible
+ collapsedSize={2}
+ onCollapse={() => setCollapsed(true)}
+ onExpand={() => setCollapsed(false)}
+ minSize={15}
+ className={cn("w-[120px] min-w-[70px]")}
+ defaultSize={20}
+ ref={panelRef}
+ maxSize={30}
+ >
+ <DashboardSidebar
+ collapsed={collapsed}
+ setCollapsed={setCollapsed}
+ />
+ </ResizablePanel>
+ <ResizableHandle withHandle className="w-[2px]" />
+ <ResizablePanel>
+ <main>
+ <Outlet />
+ <Toaster position="top-center" />
+ </main>
+ </ResizablePanel>
+ </ResizablePanelGroup>
+ ) : (
+ <div>
+ <aside>
+ <Drawer open={open} onOpenChange={setOpen}>
+ <DrawerContent>
+ <DashboardSidebar
+ collapsed={collapsed}
+ setCollapsed={setCollapsed}
+ />
+ </DrawerContent>
+ </Drawer>
+ </aside>
+ <main className="sm:block">
+ <Outlet />
+ <Toaster position="top-center" />
+ </main>
+ </div>
+ )}
+ </div>
+ );
+};
-export const Route = createFileRoute('/_dashboard')({
- component: () => <DashboardLayout />,
+export const Route = createFileRoute("/_dashboard")({
+ component: () => <DashboardLayout />,
- beforeLoad: async () => {
- const loggedIn = await useAuth.getState().isLoggedIn()
- if (!loggedIn) {
- throw redirect({
- to: '/login',
- })
- }
- }
-})
+ beforeLoad: async () => {
+ const loggedIn = await useAuth.getState().isLoggedIn();
+ if (!loggedIn) {
+ throw redirect({
+ to: "/login",
+ });
+ }
+ },
+});