Files
supabase/apps/studio/components/interfaces/Database/Extensions/Extensions.tsx
T
Ali Waseem 618c902e1a feat(studio): add keyboard shortcuts to Database listing pages (#45467)
## Summary

Adds the `O→S` / `Shift+F` / `Shift+N` / `F→C` shortcut set across every
Database listing page, on top of the schema-visualizer pattern from
#45386. Fixes [FE-3131](https://linear.app/supabase/issue/FE-3131).

A shared `list-page.*` registry replaces what would have been a per-page
registry file for each route, and `useShortcut`/`Shortcut` gain a
`label` override so a single ID renders contextually in Cmd+K and hover
tooltips.

## Pages and shortcuts

| Page | `O→S` | `Shift+F` | `Shift+N` | `F→C` |
| --- | --- | --- | --- | --- |
| Tables | Open schema selector | Search tables | Create new table |
Clears search + entity-type filter |
| Functions | Open schema selector | Search functions | Create new
function | Clears search + Return Type + Security |
| Triggers — data | Open schema selector | Search triggers | Create new
trigger | Clears search + Table filter |
| Triggers — event | — | Search event triggers | Create new event
trigger | Clears search + Owner filter |
| Enumerated Types | Open schema selector | Search enumerated types |
Create new enumerated type | Clears search |
| Indexes | Open schema selector | Search indexes | Create new index |
Clears search |
| Roles | — | Search roles | Add new role | Clears search + filter type
→ 'all' |
| Publications | — | Search publications | — | Clears search |
| Extensions | — | Search extensions | — | Clears search |

`Shift+N` only fires when the page-specific gate allows it (permission +
unlocked schema + any other prerequisite like `hasTables` for triggers).

## Test plan

- [ ] On each of the 9 pages, all four shortcuts behave as listed in the
table above
- [ ] Hover the wrapped controls — tooltip shows the page-specific label
and the right keybind badge
- [ ] Open the schema selector via `O→S` — the hover tooltip is
suppressed while the popover is open
- [ ] `Shift+N` is a no-op on locked schemas / when permission is
missing / when a prereq fails (e.g. no tables on Triggers data)
- [ ] `Shift+F` focuses the search input; while focused, Escape clears
(with text) → Escape blurs (when empty)
- [ ] `Cmd+K` shows the page-specific shortcut name while on each page;
the entry goes away on navigation
- [ ] `Mod+/` reference sheet shows one "List pages" group with 4
generic entries
- [ ] No regression to existing schema-visualizer / table-editor /
SQL-editor shortcut behavior

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added a dedicated list-page shortcut group in the shortcuts reference.

* **Improvements**
* Keyboard shortcuts across database list pages: focus & select search,
reset filters, and create-new-item shortcuts.
* Escape now clears/searches or blurs inputs to avoid accidental popover
closes.
* Create/new buttons respond to shortcuts when allowed; disabled actions
keep permission tooltips for clarity.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-05-04 07:08:35 -06:00

151 lines
5.3 KiB
TypeScript

import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useParams } from 'common'
import { isNull, partition } from 'lodash'
import { AlertCircle, Search } from 'lucide-react'
import { useEffect, useRef, useState } from 'react'
import {
Card,
Input,
ShadowScrollArea,
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from 'ui'
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
import { ExtensionRow } from './ExtensionRow'
import { HIDDEN_EXTENSIONS, SEARCH_TERMS } from './Extensions.constants'
import InformationBox from '@/components/ui/InformationBox'
import { NoSearchResults } from '@/components/ui/NoSearchResults'
import { useDatabaseExtensionsQuery } from '@/data/database-extensions/database-extensions-query'
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { onSearchInputEscape } from '@/lib/keyboard'
import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
import { useShortcut } from '@/state/shortcuts/useShortcut'
export const Extensions = () => {
const { filter } = useParams()
const { data: project } = useSelectedProjectQuery()
const [filterString, setFilterString] = useState<string>('')
const searchInputRef = useRef<HTMLInputElement>(null)
useShortcut(
SHORTCUT_IDS.LIST_PAGE_FOCUS_SEARCH,
() => {
searchInputRef.current?.focus()
searchInputRef.current?.select()
},
{ label: 'Search extensions' }
)
useShortcut(SHORTCUT_IDS.LIST_PAGE_RESET_FILTERS, () => {
setFilterString('')
})
const { data = [], isPending: isLoading } = useDatabaseExtensionsQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const visibleExtensions = data.filter((ext) => !HIDDEN_EXTENSIONS.includes(ext.name))
const extensions =
filterString.length === 0
? visibleExtensions
: visibleExtensions.filter((ext) => {
const nameMatchesSearch = ext.name.toLowerCase().includes(filterString.toLowerCase())
const searchTermsMatchesSearch = (SEARCH_TERMS[ext.name] || []).some((x) =>
x.includes(filterString.toLowerCase())
)
return nameMatchesSearch || searchTermsMatchesSearch
})
const [enabledExtensions, disabledExtensions] = partition(
extensions,
(ext) => !isNull(ext.installed_version)
)
const { can: canUpdateExtensions, isSuccess: isPermissionsLoaded } = useAsyncCheckPermissions(
PermissionAction.TENANT_SQL_ADMIN_WRITE,
'extensions'
)
useEffect(() => {
if (filter !== undefined) setFilterString(filter as string)
}, [filter])
return (
<>
<div className="mb-4">
<Input
inputRef={searchInputRef}
size="tiny"
placeholder="Search for an extension"
value={filterString}
onChange={(e) => setFilterString(e.target.value)}
onKeyDown={onSearchInputEscape(filterString, setFilterString)}
className="w-52"
icon={<Search />}
/>
</div>
{isPermissionsLoaded && !canUpdateExtensions && (
<InformationBox
icon={<AlertCircle className="text-foreground-light" size={18} strokeWidth={2} />}
title="You need additional permissions to update database extensions"
/>
)}
{isLoading ? (
<GenericSkeletonLoader />
) : (
<Card>
<ShadowScrollArea stickyLastColumn>
<Table>
<TableHeader>
<TableRow>
<TableHead key="name">Name</TableHead>
<TableHead key="version">Version</TableHead>
<TableHead key="schema">Schema</TableHead>
<TableHead key="description">Description</TableHead>
<TableHead key="used-by">Used by</TableHead>
<TableHead key="links">Links</TableHead>
{/*
[Joshen] All these classes are just to make the last column sticky
I reckon we can pull these out into the Table component where we can declare
sticky columns via props, but we can do that if we start to have more tables
in the dashboard with sticky columns
*/}
<TableHead key="enabled" className="px-0">
<div className="bg-200! px-4 w-full h-full flex items-center border-l">
Enabled
</div>
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{[...enabledExtensions, ...disabledExtensions].map((extension) => (
<ExtensionRow key={extension.name} extension={extension} />
))}
{extensions.length === 0 && (
<TableRow>
<TableCell colSpan={7}>
<NoSearchResults
className="border-none p-0! bg-transparent"
searchString={filterString}
onResetFilter={() => setFilterString('')}
/>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</ShadowScrollArea>
</Card>
)}
</>
)
}