mirror of
https://github.com/supabase/supabase.git
synced 2026-10-09 19:35:06 +03:00
## Context There's certain areas in the dashboard where we're calling `useTablesQuery` without a schema filter, in which case the dashboard then fires a query against the project's database to fetch _all_ tables across _all_ schemas - this could easily be a heavy query if there's a large number of relations in the project's database. Am hence opting to either add a schema filter if appropriate, or otherwise opt to use the infinite loading behaviour ## Changes involved - Add schema filter to `useTablesQuery` in database triggers and publications - Use infinite loading for tables in Cmd K for "Run query on table" and "Search database tables" ## To test - [x] Verify that database triggers + publications still function as expected - [x] Verify that CMD K "Run query on table" and "Search database tables" still function as expected (including search) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Implemented debounced infinite-scrolling table search in the command menu and SQL editor command flow. * Added a schema selector dropdown to publications management for easier navigation. * **Improvements** * Removed the “Schema” column from the publications tables UI. * Updated search guidance and table-picker status (counts/loading) during infinite browsing. * Trigger table listings now follow the selected schema context. * Refined command menu list height and improved the database-tables placeholder text. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
113 lines
3.5 KiB
TypeScript
113 lines
3.5 KiB
TypeScript
import { cn, CommandList } from 'ui'
|
|
import { ShimmeringLoader } from 'ui-patterns'
|
|
import { TextHighlighter } from 'ui-patterns/CommandMenu'
|
|
import { CommandMenuGroup } from 'ui-patterns/CommandMenu/internal/CommandMenuGroup'
|
|
import { CommandMenuItem } from 'ui-patterns/CommandMenu/internal/CommandMenuItem'
|
|
import type { IActionCommand, IRouteCommand } from 'ui-patterns/CommandMenu/internal/types'
|
|
|
|
export interface SearchResult {
|
|
id: string
|
|
name: string
|
|
description?: string
|
|
}
|
|
|
|
export function SkeletonResults() {
|
|
return (
|
|
<div className="p-2 space-y-1">
|
|
{[0, 1, 2, 3].map((i) => (
|
|
<div key={i} className="flex items-center gap-3 px-2 py-2">
|
|
<ShimmeringLoader className="w-4! h-4! py-0! rounded-sm" delayIndex={i} />
|
|
<div className="flex-1 space-y-1">
|
|
<ShimmeringLoader className="w-32! py-1.5!" delayIndex={i} />
|
|
<ShimmeringLoader className="w-48! py-1!" delayIndex={i + 1} />
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
interface EmptyStateProps {
|
|
icon: React.ComponentType<React.SVGProps<SVGSVGElement>>
|
|
label: string
|
|
query: string
|
|
}
|
|
|
|
export function EmptyState({ icon: Icon, label, query }: EmptyStateProps) {
|
|
return (
|
|
<div className="h-full flex flex-col items-center justify-center py-12 px-4 gap-4 text-center text-foreground-lighter">
|
|
<Icon className="h-6 w-6" strokeWidth={1.5} />
|
|
<p className="text-sm">
|
|
{query ? `No results found for "${query}"` : `Type to search in ${label}`}
|
|
</p>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
interface ResultsListProps {
|
|
results: SearchResult[]
|
|
icon: React.ComponentType<React.SVGProps<SVGSVGElement>>
|
|
getIcon?: (result: SearchResult) => React.ComponentType<React.SVGProps<SVGSVGElement>>
|
|
onResultClick?: (result: SearchResult) => void
|
|
getRoute?: (result: SearchResult) => `/${string}` | `http${string}`
|
|
className?: string
|
|
infiniteLoadingObserverRef?: (node: Element | null) => void
|
|
}
|
|
|
|
export function ResultsList({
|
|
results,
|
|
icon: Icon,
|
|
getIcon,
|
|
onResultClick,
|
|
getRoute,
|
|
className,
|
|
infiniteLoadingObserverRef,
|
|
}: ResultsListProps) {
|
|
const commands = results.map((result): IRouteCommand | IActionCommand => {
|
|
const ResultIcon = getIcon ? getIcon(result) : Icon
|
|
const baseCommand = {
|
|
id: result.id,
|
|
name: result.name,
|
|
value: result.description ? `${result.name} ${result.description}` : result.name,
|
|
icon: () => <ResultIcon className="h-4 w-4" strokeWidth={1.5} />,
|
|
}
|
|
|
|
if (getRoute) {
|
|
return {
|
|
...baseCommand,
|
|
route: getRoute(result),
|
|
} as IRouteCommand
|
|
}
|
|
|
|
return {
|
|
...baseCommand,
|
|
action: () => onResultClick?.(result),
|
|
} as IActionCommand
|
|
})
|
|
|
|
return (
|
|
<CommandList
|
|
className={cn(
|
|
'max-h-full! flex-1 min-h-0 overflow-y-auto overflow-x-hidden bg-transparent',
|
|
className
|
|
)}
|
|
>
|
|
<CommandMenuGroup className="py-2">
|
|
{commands.map((command) => (
|
|
<CommandMenuItem key={command.id} command={command}>
|
|
<div className="flex flex-col min-w-0 text-foreground-light">
|
|
<TextHighlighter>{command.name}</TextHighlighter>
|
|
{command.value && command.value !== command.name && (
|
|
<p className="text-xs text-foreground-lighter/70 truncate mt-0.5">
|
|
{command.value.replace(command.name, '').trim()}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</CommandMenuItem>
|
|
))}
|
|
</CommandMenuGroup>
|
|
{infiniteLoadingObserverRef && <div ref={infiniteLoadingObserverRef} />}
|
|
</CommandList>
|
|
)
|
|
}
|