feat(v2): add command panel

This commit is contained in:
moonrailgun 2024-04-05 00:08:58 +08:00
parent 07cb0b066a
commit af4c6f6bd1
3 changed files with 243 additions and 2 deletions

View File

@ -0,0 +1,236 @@
import React, { useEffect, useState } from 'react';
import {
Command,
CommandDialog,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
} from '@/components/ui/command';
import {
LuAreaChart,
LuBellDot,
LuFilePieChart,
LuMonitorDot,
LuSearch,
LuServer,
LuUserCircle2,
LuWifi,
} from 'react-icons/lu';
import { NavigateOptions, useNavigate } from '@tanstack/react-router';
import { useEvent } from '@/hooks/useEvent';
import { useCommandState } from 'cmdk';
import { useTranslation } from '@i18next-toolkit/react';
import { trpc } from '@/api/trpc';
import { useCurrentWorkspaceId } from '@/store/user';
import { Button } from './ui/button';
export const CommandPanel: React.FC = React.memo(() => {
const [open, setOpen] = useState(false);
const navigate = useNavigate();
const { t } = useTranslation();
const handleJump = useEvent((options: NavigateOptions) => {
return () => {
setOpen(false);
navigate(options);
};
});
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 (
<>
<Button
className="w-full !justify-between"
variant="secondary"
size="sm"
Icon={LuSearch}
onClick={() => setOpen(true)}
>
<span className="rounded bg-black/10 px-1 py-0.5">ctrl + k</span>
</Button>
<CommandDialog open={open} onOpenChange={setOpen}>
<Command loop={true}>
<CommandInput placeholder={t('Type a command or search...')} />
<CommandList>
<CommandEmpty>{t('No results found.')}</CommandEmpty>
<CommandPanelSearchGroup handleJump={handleJump} />
<CommandGroup heading={t('Suggestions')}>
<CommandItem
onSelect={handleJump({
to: '/website',
})}
>
<LuAreaChart className="mr-2 h-4 w-4" />
{t('Website')}
</CommandItem>
<CommandItem
onSelect={handleJump({
to: '/monitor',
})}
>
<LuMonitorDot className="mr-2 h-4 w-4" />
{t('Monitor')}
</CommandItem>
<CommandItem
onSelect={handleJump({
to: '/server',
})}
>
<LuServer className="mr-2 h-4 w-4" />
{t('Servers')}
</CommandItem>
<CommandItem
onSelect={handleJump({
to: '/telemetry',
})}
>
<LuWifi className="mr-2 h-4 w-4" />
{t('Telemetry')}
</CommandItem>
<CommandItem
onSelect={handleJump({
to: '/page',
})}
>
<LuFilePieChart className="mr-2 h-4 w-4" />
{t('Pages')}
</CommandItem>
</CommandGroup>
<CommandSeparator />
<CommandGroup heading={t('Settings')}>
<CommandItem
onSelect={handleJump({
to: '/settings/profile',
})}
>
<LuUserCircle2 className="mr-2 h-4 w-4" />
<span>{t('Profile')}</span>
</CommandItem>
<CommandItem
onSelect={handleJump({
to: '/settings/notifications',
})}
>
<LuBellDot className="mr-2 h-4 w-4" />
<span>{t('Notifications')}</span>
</CommandItem>
</CommandGroup>
</CommandList>
</Command>
</CommandDialog>
</>
);
});
CommandPanel.displayName = 'CommandPanel';
interface CommandPanelSearchGroupProps {
handleJump: (options: NavigateOptions) => () => void;
}
export const CommandPanelSearchGroup: React.FC<CommandPanelSearchGroupProps> =
React.memo((props) => {
const handleJump = props.handleJump;
const search = useCommandState((state) => state.search);
const workspaceId = useCurrentWorkspaceId();
const { t } = useTranslation();
const { data: websites = [] } = trpc.website.all.useQuery({
workspaceId,
});
const { data: monitors = [] } = trpc.monitor.all.useQuery({
workspaceId,
});
const { data: telemetryList = [] } = trpc.telemetry.all.useQuery({
workspaceId,
});
const { data: pages = [] } = trpc.monitor.getAllPages.useQuery({
workspaceId,
});
if (!search) {
return null;
}
return (
<CommandGroup heading={t('Search')}>
{websites.map((w) => (
<CommandItem
key={w.id}
value={w.id}
keywords={[w.name, w.id]}
onSelect={handleJump({
to: '/website/$websiteId',
params: {
websiteId: w.id,
},
})}
>
<LuFilePieChart className="mr-2 h-4 w-4" />
{w.name}
</CommandItem>
))}
{monitors.map((m) => (
<CommandItem
key={m.id}
value={m.id}
keywords={[m.name, m.id]}
onSelect={handleJump({
to: '/monitor/$monitorId',
params: {
monitorId: m.id,
},
})}
>
<LuMonitorDot className="mr-2 h-4 w-4" />
{m.name}
</CommandItem>
))}
{telemetryList.map((t) => (
<CommandItem
key={t.id}
value={t.id}
keywords={[t.name, t.id]}
onSelect={handleJump({
to: '/telemetry/$telemetryId',
params: {
telemetryId: t.id,
},
})}
>
<LuWifi className="mr-2 h-4 w-4" />
{t.name}
</CommandItem>
))}
{pages.map((p) => (
<CommandItem
key={p.id}
value={p.id}
keywords={[p.title, p.id]}
onSelect={handleJump({
to: '/page/$slug',
params: {
slug: p.slug,
},
})}
>
<LuFilePieChart className="mr-2 h-4 w-4" />
{p.title}
</CommandItem>
))}
</CommandGroup>
);
});
CommandPanelSearchGroup.displayName = 'CommandPanelSearchGroup';

View File

@ -27,7 +27,7 @@ const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
return (
<Dialog {...props}>
<DialogContent className="overflow-hidden p-0">
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-zinc-500 [&_[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 dark:[&_[cmdk-group-heading]]:text-zinc-400">
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-zinc-500 dark:[&_[cmdk-group-heading]]:text-zinc-400 [&_[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>
@ -87,7 +87,7 @@ const CommandGroup = React.forwardRef<
<CommandPrimitive.Group
ref={ref}
className={cn(
'overflow-hidden p-1 text-zinc-950 [&_[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-zinc-500 dark:text-zinc-50 dark:[&_[cmdk-group-heading]]:text-zinc-400',
'overflow-hidden p-1 text-zinc-950 dark:text-zinc-50 [&_[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-zinc-500 dark:[&_[cmdk-group-heading]]:text-zinc-400',
className
)}
{...props}

View File

@ -23,6 +23,7 @@ import { trpc } from '@/api/trpc';
import { useUserStore } from '@/store/user';
import { LayoutProps } from './types';
import { useTranslation } from '@i18next-toolkit/react';
import { CommandPanel } from '@/components/CommandPanel';
const defaultLayout: [number, number, number] = [265, 440, 655];
@ -59,6 +60,10 @@ export const DesktopLayout: React.FC<LayoutProps> = React.memo((props) => {
<WorkspaceSwitcher isCollapsed={isCollapsed} />
</div>
<Separator />
<div className="p-2">
<CommandPanel />
</div>
<Separator />
<Nav
isCollapsed={isCollapsed}
links={[