Add app command/search (#622)

This commit is contained in:
Saeed Vaziry
2025-06-22 22:58:05 +02:00
committed by GitHub
parent 5689e751af
commit dc7fa6b55c
12 changed files with 287 additions and 87 deletions

View File

@ -3,9 +3,25 @@ import { useEffect, useState } from 'react';
import { Button } from '@/components/ui/button';
import { CommandIcon, SearchIcon } from 'lucide-react';
import CreateServer from '@/pages/servers/components/create-server';
import ProjectForm from '@/pages/projects/components/project-form';
import { useQuery } from '@tanstack/react-query';
import axios from 'axios';
import { Badge } from '@/components/ui/badge';
import { router } from '@inertiajs/react';
type SearchResult = {
id: number;
parent_id?: number;
label: string;
type: 'server' | 'project' | 'site';
};
export default function AppCommand() {
const [open, setOpen] = useState(false);
const [openServer, setOpenServer] = useState(false);
const [openProject, setOpenProject] = useState(false);
const [queryText, setQueryText] = useState('');
const [selected, setSelected] = useState<string>('create-server');
useEffect(() => {
const down = (e: KeyboardEvent) => {
@ -19,6 +35,44 @@ export default function AppCommand() {
return () => document.removeEventListener('keydown', down);
}, []);
const handleOpenChange = (open: boolean) => {
setOpen(open);
if (!open) {
setOpenServer(false);
setOpenProject(false);
}
};
const query = useQuery<SearchResult[]>({
queryKey: ['search'],
queryFn: async () => {
const response = await axios.get(route('search', { query: queryText }));
return response.data.data;
},
retry: false,
enabled: false,
refetchInterval: false,
refetchIntervalInBackground: false,
});
useEffect(() => {
if (query.data && query.data.length > 0) {
setSelected(`result-0`);
} else {
setSelected('create-server');
}
}, [query.data]);
useEffect(() => {
if (queryText !== '' && queryText.length >= 3) {
const timeoutId = setTimeout(() => {
query.refetch();
}, 300);
return () => clearTimeout(timeoutId);
}
}, [queryText]);
return (
<div>
<Button className="hidden px-1! lg:flex" variant="outline" size="sm" onClick={() => setOpen(true)}>
@ -32,15 +86,51 @@ export default function AppCommand() {
<Button className="lg:hidden" variant="outline" size="sm" onClick={() => setOpen(true)}>
<CommandIcon className="mr-1 size-3" /> K
</Button>
<CommandDialog open={open} onOpenChange={setOpen}>
<CommandInput placeholder="Type a command or search..." />
<CommandDialog open={open} onOpenChange={handleOpenChange} shouldFilter={false} value={selected}>
<CommandInput placeholder="Type a command or search..." onValueChange={setQueryText} />
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup heading="Suggestions">
<CreateServer>
<CommandItem>Create server</CommandItem>
{query.isFetching && <p className="text-muted-foreground p-4 text-sm">Searching...</p>}
{query.data && query.data?.length > 0 && (
<CommandGroup heading="Search Results">
{query.data.map((result, index) => (
<CommandItem
key={`search-result-${result.id}`}
className="flex items-center justify-between"
value={`result-${index}`}
onSelect={() => {
if (result.type === 'server') {
router.post(route('servers.switch', { server: result.id }));
} else if (result.type === 'project') {
router.patch(
route('projects.switch', {
project: result.id,
currentPath: window.location.pathname,
}),
);
} else if (result.type === 'site') {
router.post(route('sites.switch', { server: result.parent_id, site: result.id }));
}
setOpen(false);
}}
>
{result.label}
<Badge variant="outline">{result.type}</Badge>
</CommandItem>
))}
</CommandGroup>
)}
<CommandGroup heading="Commands">
<CreateServer defaultOpen={openServer} onOpenChange={setOpenServer}>
<CommandItem value="create-server" key="cmd-create-server" onSelect={() => setOpenServer(true)}>
Create server
</CommandItem>
</CreateServer>
<CommandItem>Create project</CommandItem>
<ProjectForm defaultOpen={openProject} onOpenChange={setOpenProject}>
<CommandItem value="create-project" key="cmd-create-project" onSelect={() => setOpenProject(true)}>
Create project
</CommandItem>
</ProjectForm>
</CommandGroup>
</CommandList>
</CommandDialog>

View File

@ -45,12 +45,12 @@ const mainNavItems: NavItem[] = [
const footerNavItems: NavItem[] = [
{
title: 'Workers',
title: 'Horizon Dashboard',
href: route('horizon.index'),
icon: ListEndIcon,
},
{
title: 'Logs',
title: 'Vito Logs',
href: route('log-viewer.index'),
icon: LogsIcon,
},

View File

@ -42,7 +42,6 @@ export default function Refresh() {
setPoll(undefined);
}
localStorage.setItem('refresh_interval', refreshInterval.toString());
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [refreshInterval]);
return (

View File

@ -30,6 +30,7 @@ export function SiteSwitch() {
if (storedSite && page.props.server_sites && !page.props.server_sites.find((site) => site.id === storedSite.id)) {
siteHelper.storeSite();
setSelectedSite(null);
}
const handleSiteChange = (site: Site) => {

View File

@ -18,11 +18,15 @@ function Command({ className, ...props }: React.ComponentProps<typeof CommandPri
function CommandDialog({
title = 'Command Palette',
description = 'Search for a command to run...',
shouldFilter = true,
value,
children,
...props
}: React.ComponentProps<typeof Dialog> & {
title?: string;
description?: string;
shouldFilter?: boolean;
value?: string;
}) {
return (
<Dialog {...props}>
@ -31,7 +35,11 @@ function CommandDialog({
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<DialogContent className="overflow-hidden p-0">
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[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">
<Command
shouldFilter={shouldFilter}
value={value}
className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[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>