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 (
|
return (
|
||||||
<Dialog {...props}>
|
<Dialog {...props}>
|
||||||
<DialogContent className="overflow-hidden p-0">
|
<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}
|
{children}
|
||||||
</Command>
|
</Command>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
@ -87,7 +87,7 @@ const CommandGroup = React.forwardRef<
|
|||||||
<CommandPrimitive.Group
|
<CommandPrimitive.Group
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
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
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
@ -23,6 +23,7 @@ import { trpc } from '@/api/trpc';
|
|||||||
import { useUserStore } from '@/store/user';
|
import { useUserStore } from '@/store/user';
|
||||||
import { LayoutProps } from './types';
|
import { LayoutProps } from './types';
|
||||||
import { useTranslation } from '@i18next-toolkit/react';
|
import { useTranslation } from '@i18next-toolkit/react';
|
||||||
|
import { CommandPanel } from '@/components/CommandPanel';
|
||||||
|
|
||||||
const defaultLayout: [number, number, number] = [265, 440, 655];
|
const defaultLayout: [number, number, number] = [265, 440, 655];
|
||||||
|
|
||||||
@ -59,6 +60,10 @@ export const DesktopLayout: React.FC<LayoutProps> = React.memo((props) => {
|
|||||||
<WorkspaceSwitcher isCollapsed={isCollapsed} />
|
<WorkspaceSwitcher isCollapsed={isCollapsed} />
|
||||||
</div>
|
</div>
|
||||||
<Separator />
|
<Separator />
|
||||||
|
<div className="p-2">
|
||||||
|
<CommandPanel />
|
||||||
|
</div>
|
||||||
|
<Separator />
|
||||||
<Nav
|
<Nav
|
||||||
isCollapsed={isCollapsed}
|
isCollapsed={isCollapsed}
|
||||||
links={[
|
links={[
|
||||||
|
Loading…
Reference in New Issue
Block a user