feat(v2): add command panel
This commit is contained in:
parent
07cb0b066a
commit
af4c6f6bd1
236
src/client/components/CommandPanel.tsx
Normal file
236
src/client/components/CommandPanel.tsx
Normal 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';
|
@ -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}
|
||||
|
@ -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={[
|
||||
|
Loading…
Reference in New Issue
Block a user