mirror of
https://github.com/supabase/supabase.git
synced 2026-05-06 22:18:00 +08:00
## Summary Adds a contextual `D + <letter>` chord pattern for jumping between Database sub-pages, mounted only while `DatabaseLayout` is active. Establishes the pattern we can repeat for other sections (Auth, Storage, Functions, etc.). Linear: [FE-3140](https://linear.app/supabase/issue/FE-3140/define-subnavigation-pattern-for-database-management-page) ## Pattern - Chords are 2-key sequences (`D`, `<letter>`) — no global leader, no `G` prefix. - Registration is contextual: `<DatabaseNavShortcuts />` lives inside `DatabaseLayout`, so the leading `D` is only "owned" while the user is under `/project/<ref>/database/*`. Doesn't burn a global key. - Hover tooltips on each sub-menu item show the chord, anchored to the label text (Linear-style). Powered by `<ShortcutTooltip>` already used in the main nav. - Items hidden by feature flags (Roles, Column Privileges, Replication) auto-disable the chord — no muscle-memory navigating to a 404. ## Shortcuts added | Sub-page | Chord | Notes | |---|---|---| | Tables | `D T` | | | Functions | `D F` | | | Triggers | `D R` | t**R**iggers — `T` taken by Tables | | Indexes | `D I` | | | Extensions | `D X` | e**X**tensions | | Schema Visualizer | `D V` | | | Enumerated Types | `D E` | | | Publications | `D U` | p**U**blications — avoids collision with Schema Visualizer's `D P` (Download as PNG) | | Column Privileges | `D C` | flag-gated | | Settings | `D ,` | mirrors global `G ,` for project settings — avoids collision with Schema Visualizer's `D S` (Download as SVG) | | Replication | `D L` | rep**L**ication — flag-gated | | Roles | `D O` | r**O**les — flag-gated | | Backups | `D B` | platform-only | | Migrations | `D M` | | External-link sub-menu items (Policies, Wrappers, Webhooks, Security Advisor, Performance Advisor, Query Performance) are intentionally not chorded — they route out of `/database/*` and don't belong to the section's namespace. ## Collision audit Other shortcuts active on database pages (table-list, schema-visualizer) were checked against the new chords: - **Schema Visualizer** (`/database/schemas`): `D P` (Download PNG), `D S` (Download SVG), `O A`, `O S`. Publications and Settings were remapped to `D U` and `D ,` to avoid the `D P` / `D S` clashes. - **List pages** (`/database/tables`, etc.): `Shift+F`, `Shift+N`, `O S`, `F C` — no overlap with `D + <letter>`. ## Files - `state/shortcuts/registry/database-nav.ts` — new registry module with the 14 chord definitions. - `state/shortcuts/registry.ts` — spreads the new IDs/definitions into the canonical registry. - `components/interfaces/DatabaseNavShortcuts.tsx` — null-rendering hook component that wires `useShortcut` for each chord, keyed off `useGenerateDatabaseMenu` so URLs and feature gating stay in sync with the sidebar. - `components/layouts/DatabaseLayout/DatabaseLayout.tsx` — mounts the component. - `components/layouts/DatabaseLayout/DatabaseMenu.utils.tsx` — tags each menu item with its `shortcutId`. - `components/ui/ProductMenu/ProductMenu.types.ts` — adds optional `shortcutId?: ShortcutId` field. - `components/ui/ProductMenu/ProductMenuItem.tsx` — renders the hover tooltip when an item has a `shortcutId`, anchored to the label span. ## Test plan - [ ] On `/project/<ref>/database/tables`, press `D F` — navigates to `/database/functions`. - [ ] On `/project/<ref>/database/schemas`, press `D P` — downloads the PNG (Schema Visualizer wins, no nav conflict). - [ ] On `/project/<ref>/database/schemas`, press `D U` — navigates to `/database/publications`. - [ ] On `/project/<ref>/database/tables`, press `D ,` — navigates to `/database/settings`. - [ ] Hover any sub-menu item with a chord — pill appears next to the label after ~1s. - [ ] On a project with the Replication flag off — `D L` does nothing. - [ ] Navigate to `/auth` — pressing `D F` does nothing (chord unmounts with the layout). - [ ] Type `D` then `F` slowly inside an input — does not navigate (input-focus guard).
187 lines
6.0 KiB
TypeScript
187 lines
6.0 KiB
TypeScript
import { useParams } from 'common'
|
|
import { ArrowUpRight } from 'lucide-react'
|
|
|
|
import { useIsColumnLevelPrivilegesEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext'
|
|
import { useIsETLPrivateAlpha } from '@/components/interfaces/Database/Replication/useIsETLPrivateAlpha'
|
|
import type {
|
|
ProductMenuGroup,
|
|
ProductMenuGroupItem,
|
|
} from '@/components/ui/ProductMenu/ProductMenu.types'
|
|
import { useDatabaseExtensionsQuery } from '@/data/database-extensions/database-extensions-query'
|
|
import { useProjectAddonsQuery } from '@/data/subscriptions/project-addons-query'
|
|
import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
|
|
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
|
|
import { IS_PLATFORM } from '@/lib/constants'
|
|
import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
|
|
|
|
const ExternalLinkIcon = <ArrowUpRight strokeWidth={1} className="h-4 w-4" />
|
|
|
|
export const useGenerateDatabaseMenu = (): ProductMenuGroup[] => {
|
|
const { ref } = useParams()
|
|
const { data: project } = useSelectedProjectQuery()
|
|
|
|
const {
|
|
databaseReplication: showPgReplicate,
|
|
databaseRoles: showRoles,
|
|
integrationsWrappers: showWrappers,
|
|
} = useIsFeatureEnabled(['database:replication', 'database:roles', 'integrations:wrappers'])
|
|
|
|
const { data } = useDatabaseExtensionsQuery({
|
|
projectRef: project?.ref,
|
|
connectionString: project?.connectionString,
|
|
})
|
|
const { data: addons } = useProjectAddonsQuery({ projectRef: project?.ref })
|
|
|
|
const pgNetExtensionExists = (data ?? []).some((ext) => ext.name === 'pg_net')
|
|
const pitrEnabled = addons?.selected_addons.some((addon) => addon.type === 'pitr') ?? false
|
|
const columnLevelPrivileges = useIsColumnLevelPrivilegesEnabled()
|
|
const enablePgReplicate = useIsETLPrivateAlpha()
|
|
|
|
const getDatabaseURL = (path: string) => `/project/${ref}/database/${path}`
|
|
|
|
return [
|
|
{
|
|
title: 'Database Management',
|
|
items: [
|
|
{
|
|
name: 'Schema Visualizer',
|
|
key: 'schemas',
|
|
url: getDatabaseURL('schemas'),
|
|
shortcutId: SHORTCUT_IDS.NAV_DATABASE_SCHEMA_VISUALIZER,
|
|
},
|
|
{
|
|
name: 'Tables',
|
|
key: 'tables',
|
|
url: getDatabaseURL('tables'),
|
|
shortcutId: SHORTCUT_IDS.NAV_DATABASE_TABLES,
|
|
},
|
|
{
|
|
name: 'Functions',
|
|
key: 'functions',
|
|
url: getDatabaseURL('functions'),
|
|
shortcutId: SHORTCUT_IDS.NAV_DATABASE_FUNCTIONS,
|
|
},
|
|
{
|
|
name: 'Triggers',
|
|
key: 'triggers',
|
|
url: getDatabaseURL('triggers/data'),
|
|
shortcutId: SHORTCUT_IDS.NAV_DATABASE_TRIGGERS,
|
|
},
|
|
{
|
|
name: 'Enumerated Types',
|
|
key: 'types',
|
|
url: getDatabaseURL('types'),
|
|
shortcutId: SHORTCUT_IDS.NAV_DATABASE_TYPES,
|
|
},
|
|
{
|
|
name: 'Extensions',
|
|
key: 'extensions',
|
|
url: getDatabaseURL('extensions'),
|
|
shortcutId: SHORTCUT_IDS.NAV_DATABASE_EXTENSIONS,
|
|
},
|
|
{
|
|
name: 'Indexes',
|
|
key: 'indexes',
|
|
url: getDatabaseURL('indexes'),
|
|
shortcutId: SHORTCUT_IDS.NAV_DATABASE_INDEXES,
|
|
},
|
|
{
|
|
name: 'Publications',
|
|
key: 'publications',
|
|
url: getDatabaseURL('publications'),
|
|
shortcutId: SHORTCUT_IDS.NAV_DATABASE_PUBLICATIONS,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
title: 'Configuration',
|
|
items: [
|
|
showRoles && {
|
|
name: 'Roles',
|
|
key: 'roles',
|
|
url: getDatabaseURL('roles'),
|
|
shortcutId: SHORTCUT_IDS.NAV_DATABASE_ROLES,
|
|
},
|
|
columnLevelPrivileges && {
|
|
name: 'Column Privileges',
|
|
key: 'column-privileges',
|
|
url: getDatabaseURL('column-privileges'),
|
|
shortcutId: SHORTCUT_IDS.NAV_DATABASE_COLUMN_PRIVILEGES,
|
|
},
|
|
{
|
|
name: 'Policies',
|
|
key: 'policies',
|
|
url: `/project/${ref}/auth/policies`,
|
|
rightIcon: ExternalLinkIcon,
|
|
},
|
|
{
|
|
name: 'Settings',
|
|
key: 'settings',
|
|
url: getDatabaseURL('settings'),
|
|
shortcutId: SHORTCUT_IDS.NAV_DATABASE_SETTINGS,
|
|
},
|
|
].filter(Boolean) as ProductMenuGroupItem[],
|
|
},
|
|
{
|
|
title: 'Platform',
|
|
items: [
|
|
IS_PLATFORM &&
|
|
showPgReplicate && {
|
|
name: 'Replication',
|
|
key: 'replication',
|
|
url: getDatabaseURL('replication'),
|
|
label: enablePgReplicate ? 'New' : undefined,
|
|
shortcutId: SHORTCUT_IDS.NAV_DATABASE_REPLICATION,
|
|
},
|
|
IS_PLATFORM && {
|
|
name: 'Backups',
|
|
key: 'backups',
|
|
url: pitrEnabled ? getDatabaseURL('backups/pitr') : getDatabaseURL('backups/scheduled'),
|
|
shortcutId: SHORTCUT_IDS.NAV_DATABASE_BACKUPS,
|
|
},
|
|
{
|
|
name: 'Migrations',
|
|
key: 'migrations',
|
|
url: getDatabaseURL('migrations'),
|
|
shortcutId: SHORTCUT_IDS.NAV_DATABASE_MIGRATIONS,
|
|
},
|
|
showWrappers && {
|
|
name: 'Wrappers',
|
|
key: 'wrappers',
|
|
url: `/project/${ref}/integrations?category=wrapper`,
|
|
rightIcon: ExternalLinkIcon,
|
|
},
|
|
pgNetExtensionExists && {
|
|
name: 'Database Webhooks',
|
|
key: 'hooks',
|
|
url: `/project/${ref}/integrations/webhooks/overview`,
|
|
rightIcon: ExternalLinkIcon,
|
|
},
|
|
].filter(Boolean) as ProductMenuGroupItem[],
|
|
},
|
|
{
|
|
title: 'Tools',
|
|
items: [
|
|
{
|
|
name: 'Security Advisor',
|
|
key: 'security-advisor',
|
|
url: `/project/${ref}/advisors/security`,
|
|
rightIcon: ExternalLinkIcon,
|
|
},
|
|
{
|
|
name: 'Performance Advisor',
|
|
key: 'performance-advisor',
|
|
url: `/project/${ref}/advisors/performance`,
|
|
rightIcon: ExternalLinkIcon,
|
|
},
|
|
{
|
|
name: 'Query Performance',
|
|
key: 'query-performance',
|
|
url: `/project/${ref}/observability/query-performance`,
|
|
rightIcon: ExternalLinkIcon,
|
|
},
|
|
],
|
|
},
|
|
]
|
|
}
|