generated from TBD54566975/tbd-project-template
-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add global search to console (#2625)
Fixes #2623 ![Screenshot 2024-09-05 at 11 27 59 AM](https://github.com/user-attachments/assets/89ed962c-3055-4a0c-b879-4b8300dca02f) ![Screenshot 2024-09-05 at 11 28 13 AM](https://github.com/user-attachments/assets/3772c23e-b078-4910-bba1-26a9acd70a2c) ![Screenshot 2024-09-05 at 11 28 24 AM](https://github.com/user-attachments/assets/6913b31a-4a76-41d1-80a8-620f54bbbeae) https://github.com/user-attachments/assets/0c1fa35c-7e95-4780-ab9c-b4f5764fdcf9
- Loading branch information
1 parent
775b2de
commit 4f4ca4e
Showing
8 changed files
with
243 additions
and
41 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import { expect, ftlTest } from './ftl-test' | ||
|
||
ftlTest('shows command palette results', async ({ page }) => { | ||
await page.goto('http://localhost:8892') | ||
|
||
await page.click('#command-palette-search') | ||
await page.fill('#search-input', 'echo') | ||
|
||
// Command palette should be showing the echo parts | ||
await expect(page.getByText('echo.EchoRequest')).toBeVisible() | ||
await expect(page.getByText('echo.EchoReponse')).toBeVisible() | ||
await expect(page.getByText('echo.echo')).toBeVisible() | ||
}) |
98 changes: 98 additions & 0 deletions
98
frontend/console/src/features/command-pallete/CommandPalette.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
import { Combobox, ComboboxInput, ComboboxOption, ComboboxOptions, Dialog, DialogBackdrop, DialogPanel } from '@headlessui/react' | ||
import { ArrowRight01Icon, CellsIcon } from 'hugeicons-react' | ||
import { useEffect, useState } from 'react' | ||
import { useNavigate } from 'react-router-dom' | ||
import { useSchema } from '../../api/schema/use-schema' | ||
import { type PaletteItem, paletteItems } from './command-palette.utils' | ||
|
||
type CommandPaletteProps = { | ||
isOpen: boolean | ||
onClose: () => void | ||
} | ||
|
||
export const CommandPalette: React.FC<CommandPaletteProps> = ({ isOpen, onClose }) => { | ||
const navigate = useNavigate() | ||
const { data: schemaData } = useSchema() | ||
const [query, setQuery] = useState('') | ||
const [items, setItems] = useState<PaletteItem[]>([]) | ||
|
||
useEffect(() => { | ||
if (schemaData) { | ||
const newItems = paletteItems(schemaData) | ||
setItems(newItems) | ||
} | ||
}, [schemaData]) | ||
|
||
const filteredItems = query === '' ? [] : items.filter((item) => item.title.toLowerCase().includes(query.toLowerCase())) | ||
|
||
const handleClose = () => { | ||
onClose() | ||
setQuery('') | ||
} | ||
|
||
if (!isOpen) return | ||
|
||
return ( | ||
<Dialog className='relative z-10' open={isOpen} onClose={handleClose}> | ||
<DialogBackdrop | ||
transition | ||
className='fixed inset-0 bg-gray-900 dark:bg-gray-900 bg-opacity-40 dark:bg-opacity-60 transition-opacity data-[closed]:opacity-0 data-[enter]:duration-300 data-[leave]:duration-200 data-[enter]:ease-out data-[leave]:ease-in' | ||
/> | ||
|
||
<div className='fixed inset-0 z-10 w-screen overflow-y-auto p-4 sm:p-6 md:p-20'> | ||
<DialogPanel | ||
transition | ||
className='mx-auto max-w-xl transform rounded-xl bg-white dark:bg-gray-800 p-2 shadow-2xl ring-1 ring-black dark:ring-gray-700 ring-opacity-5 transition-all data-[closed]:scale-95 data-[closed]:opacity-0 data-[enter]:duration-300 data-[leave]:duration-200 data-[enter]:ease-out data-[leave]:ease-in' | ||
> | ||
<Combobox | ||
onChange={(item: PaletteItem) => { | ||
if (item) { | ||
navigate(item.url) | ||
handleClose() | ||
} | ||
}} | ||
> | ||
<ComboboxInput | ||
id='command-palette-search-input' | ||
autoFocus | ||
className='w-full rounded-md border-0 bg-gray-100 dark:bg-gray-900 px-4 py-2.5 text-gray-900 dark:text-gray-200 placeholder-gray-500 dark:placeholder-gray-300 focus:ring-0 sm:text-sm' | ||
placeholder='Search...' | ||
onChange={(event) => setQuery(event.target.value)} | ||
onBlur={handleClose} | ||
/> | ||
|
||
{filteredItems.length > 0 && ( | ||
<ComboboxOptions static className='-mb-2 max-h-72 scroll-py-2 overflow-y-auto py-2 text-sm text-gray-800 dark:text-gray-300'> | ||
{filteredItems.map((item) => ( | ||
<ComboboxOption | ||
key={item.id} | ||
value={item} | ||
className='group flex cursor-default select-none rounded-md px-2 py-2 data-[focus]:bg-indigo-600 data-[focus]:text-white dark:data-[focus]:bg-indigo-500' | ||
> | ||
<div className='flex size-10 flex-none items-center justify-center rounded-lg'> | ||
<item.icon className='size-5 text-gray-500 dark:text-gray-400 group-data-[focus]:text-white' aria-hidden='true' /> | ||
</div> | ||
<div className='ml-2 flex-auto'> | ||
<p className='text-sm font-medium text-gray-700 dark:text-gray-200 group-data-[focus]:text-white'>{item.title}</p> | ||
<p className='mt-0.5 text-xs font-roboto-mono text-gray-500 dark:text-gray-400 group-data-[focus]:text-gray-300'>{item.subtitle}</p> | ||
</div> | ||
<div className='mr-2 flex items-center justify-end'> | ||
<ArrowRight01Icon className='size-5 text-gray-200 dark:text-gray-400 hidden group-data-[focus]:block' aria-hidden='true' /> | ||
</div> | ||
</ComboboxOption> | ||
))} | ||
</ComboboxOptions> | ||
)} | ||
|
||
{query !== '' && filteredItems.length === 0 && ( | ||
<div className='px-4 py-14 text-center sm:px-14'> | ||
<CellsIcon className='mx-auto h-6 w-6 text-gray-400 dark:text-gray-500' aria-hidden='true' /> | ||
<p className='mt-4 text-sm text-gray-900 dark:text-gray-200'>No items found using that search term.</p> | ||
</div> | ||
)} | ||
</Combobox> | ||
</DialogPanel> | ||
</div> | ||
</Dialog> | ||
) | ||
} |
41 changes: 41 additions & 0 deletions
41
frontend/console/src/features/command-pallete/command-palette.utils.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
import { CellsIcon, type HugeiconsProps } from 'hugeicons-react' | ||
import type { PullSchemaResponse } from '../../protos/xyz/block/ftl/v1/ftl_pb' | ||
import { declIcons, declUrl } from '../modules/module.utils' | ||
|
||
export interface PaletteItem { | ||
id: string | ||
icon: React.FC<Omit<HugeiconsProps, 'ref'> & React.RefAttributes<SVGSVGElement>> | ||
title: string | ||
subtitle?: string | ||
url: string | ||
} | ||
|
||
export const paletteItems = (schema: PullSchemaResponse[]): PaletteItem[] => { | ||
const items: PaletteItem[] = [] | ||
|
||
for (const module of schema) { | ||
items.push({ | ||
id: `${module.moduleName}-module`, | ||
icon: CellsIcon, | ||
title: module.moduleName, | ||
subtitle: module.moduleName, | ||
url: `/modules/${module.moduleName}`, | ||
}) | ||
|
||
for (const decl of module.schema?.decls ?? []) { | ||
if (!decl.value || !decl.value.case || !decl.value.value) { | ||
return [] | ||
} | ||
|
||
items.push({ | ||
id: `${module.moduleName}-${decl.value.value.name}`, | ||
icon: declIcons[decl.value.case], | ||
title: decl.value.value.name, | ||
subtitle: `${module.moduleName}.${decl.value.value.name}`, | ||
url: declUrl(module.moduleName, decl), | ||
}) | ||
} | ||
} | ||
|
||
return items | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
import { Search01Icon } from 'hugeicons-react' | ||
import type React from 'react' | ||
import { useEffect } from 'react' | ||
|
||
type SearchInputProps = { | ||
onFocus: () => void | ||
} | ||
|
||
export const SearchInput: React.FC<SearchInputProps> = ({ onFocus }) => { | ||
const shortcutText = window.navigator.userAgent.includes('Mac') ? '⌘ + K' : 'Ctrl + K' | ||
|
||
useEffect(() => { | ||
const handleKeydown = (event: KeyboardEvent) => { | ||
if ((event.metaKey || event.ctrlKey) && event.key === 'k') { | ||
event.preventDefault() | ||
onFocus() | ||
} | ||
} | ||
|
||
window.addEventListener('keydown', handleKeydown) | ||
|
||
return () => { | ||
window.removeEventListener('keydown', handleKeydown) | ||
} | ||
}, [onFocus]) | ||
|
||
return ( | ||
<div className='flex flex-1 items-center justify-center px-2 lg:ml-6 lg:justify-end'> | ||
<div className='w-full max-w-lg lg:max-w-xs'> | ||
<label htmlFor='search' className='sr-only'> | ||
Search | ||
</label> | ||
<div | ||
id='command-palette-search' | ||
className='relative block w-full cursor-pointer rounded-md border border-indigo-700 bg-indigo-700/50 py-1.5 pl-10 pr-3 text-indigo-200 placeholder:text-indigo-300 sm:text-sm sm:leading-6 hover:border-indigo-500 hover:ring-indigo-500 focus-within:border-indigo-400 focus-within:ring-indigo-400' | ||
onClick={onFocus} | ||
> | ||
<div className='pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3'> | ||
<Search01Icon aria-hidden='true' className='h-5 w-5 text-indigo-300' /> | ||
</div> | ||
<span className='text-indigo-300'>Search</span> | ||
<div className='absolute inset-y-0 right-0 flex items-center pr-3'> | ||
<span className='text-indigo-200 text-xs bg-indigo-600 px-2 py-1 rounded-md'>{shortcutText}</span> | ||
</div> | ||
</div> | ||
</div> | ||
</div> | ||
) | ||
} |