Joshen/fe 3613 database tables query should have schema filter wherever appropriate (#46935)

## 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 -->
This commit is contained in:
Joshen Lim authored and GitHub committed 2026-06-16 15:45:39 +08:00
1 parent 7d4ec098ea
commit aba4e147eb
18 files changed
+267 -195

No files matched your search

@@ -1,9 +1,10 @@
'use client'
import { useDebounce } from '@uidotdev/usehooks'
import { IS_PLATFORM } from 'common'
import { Auth, EdgeFunctions, Storage } from 'icons'
import { Database } from 'lucide-react'
import { useMemo } from 'react'
import { useMemo, useState } from 'react'
import type { ICommand } from 'ui-patterns/CommandMenu'
import {
CommandHeader,
@@ -36,7 +37,7 @@ const SEARCH_CONTEXT_OPTIONS: SearchContextOption[] = [
value: 'database-tables',
label: 'Database Tables',
pageName: 'Search Database Tables',
placeholder: 'Search database tables...',
placeholder: 'Search database schema or tables...',
icon: Database,
},
{
@@ -71,12 +72,23 @@ function ContextSearchPage({
}) {
const query = useQuery()
const [filterString, setFilterString] = useState('')
const debouncedFilterString = useDebounce(filterString, 300)
return (
<CommandWrapper>
<CommandWrapper shouldFilter={context !== 'database-tables'}>
<CommandHeader>
<CommandMenuInput placeholder={placeholder} />
<CommandMenuInput
placeholder={placeholder}
value={filterString}
onValueChange={setFilterString}
/>
</CommandHeader>
<ContextSearchResults context={context} query={query} />
<ContextSearchResults
context={context}
query={query}
debouncedFilterString={debouncedFilterString}
/>
</CommandWrapper>
)
}
@@ -1,5 +1,3 @@
'use client'
import { cn, CommandList } from 'ui'
import { ShimmeringLoader } from 'ui-patterns'
import { TextHighlighter } from 'ui-patterns/CommandMenu'
@@ -53,6 +51,7 @@ interface ResultsListProps {
onResultClick?: (result: SearchResult) => void
getRoute?: (result: SearchResult) => `/${string}` | `http${string}`
className?: string
infiniteLoadingObserverRef?: (node: Element | null) => void
}
export function ResultsList({
@@ -62,6 +61,7 @@ export function ResultsList({
onResultClick,
getRoute,
className,
infiniteLoadingObserverRef,
}: ResultsListProps) {
const commands = results.map((result): IRouteCommand | IActionCommand => {
const ResultIcon = getIcon ? getIcon(result) : Icon
@@ -92,7 +92,7 @@ export function ResultsList({
className
)}
>
<CommandMenuGroup>
<CommandMenuGroup className="py-2">
{commands.map((command) => (
<CommandMenuItem key={command.id} command={command}>
<div className="flex flex-col min-w-0 text-foreground-light">
@@ -106,6 +106,7 @@ export function ResultsList({
</CommandMenuItem>
))}
</CommandMenuGroup>
{infiniteLoadingObserverRef && <div ref={infiniteLoadingObserverRef} />}
</CommandList>
)
}
@@ -45,6 +45,7 @@ const StorageSearchResults = dynamic(
interface ContextSearchResultsProps {
context: SearchContextValue
query: string
debouncedFilterString: string
}
const CONTEXT_CONFIG: Record<
@@ -77,13 +78,17 @@ const CONTEXT_CONFIG: Record<
},
}
export function ContextSearchResults({ context, query }: ContextSearchResultsProps) {
export function ContextSearchResults({
context,
query,
debouncedFilterString,
}: ContextSearchResultsProps) {
const config = CONTEXT_CONFIG[context]
if (context === 'database-tables') {
return (
<div className="flex-1 min-h-0 flex flex-col">
<TableSearchResults query={query} />
<TableSearchResults debouncedFilterString={debouncedFilterString} />
</div>
)
}
@@ -1,8 +1,7 @@
'use client'
import { useIntersectionObserver } from '@uidotdev/usehooks'
import { useParams } from 'common'
import { Database, Loader2 } from 'lucide-react'
import { useMemo } from 'react'
import { useEffect, useMemo } from 'react'
import {
EmptyState,
@@ -10,55 +9,44 @@ import {
SkeletonResults,
type SearchResult,
} from './ContextSearchResults.shared'
import { useTablesQuery } from '@/data/tables/tables-query'
import { useInfiniteTablesQuery } from '@/data/tables/tables-query'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
interface TableSearchResultsProps {
query: string
debouncedFilterString: string
}
export function TableSearchResults({ query }: TableSearchResultsProps) {
export function TableSearchResults({ debouncedFilterString }: TableSearchResultsProps) {
const { ref: projectRef } = useParams()
const { data: project } = useSelectedProjectQuery()
const trimmedQuery = query.trim()
const [sentinelRef, sentinelEntry] = useIntersectionObserver({
threshold: 0,
rootMargin: '200px 0px 200px 0px',
})
const {
data: tables,
isLoading: isLoadingTables,
data: tablesData,
isSuccess,
isError: isErrorTables,
} = useTablesQuery(
{
projectRef: project?.ref,
connectionString: project?.connectionString,
includeColumns: false,
sortByProperty: 'name',
},
{
enabled: !!project?.ref,
}
)
isPending: isLoadingTables,
hasNextPage: hasNextTablesPage,
isFetchingNextPage: isFetchingNextTablesPage,
fetchNextPage: fetchNextTablesPage,
} = useInfiniteTablesQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
includeColumns: false,
pageSize: 50,
nameFilter: debouncedFilterString,
})
const tables = useMemo(() => tablesData?.pages.flat() ?? [], [tablesData])
const tableResults: SearchResult[] = useMemo(() => {
if (!tables) return []
const filtered = trimmedQuery
? tables.filter((table) => {
const searchLower = trimmedQuery.toLowerCase()
const tableName = table.name?.toLowerCase() || ''
const schemaName = table.schema?.toLowerCase() || ''
const fullName = `${schemaName}.${tableName}`
return (
tableName.includes(searchLower) ||
schemaName.includes(searchLower) ||
fullName.includes(searchLower)
)
})
: tables
// Limit results for performance
return filtered.slice(0, 20).map((table) => {
return tables.map((table) => {
const displayName =
table.schema && table.schema !== 'public'
? `${table.schema}.${table.name}`
@@ -76,79 +64,71 @@ export function TableSearchResults({ query }: TableSearchResultsProps) {
description,
}
})
}, [tables, trimmedQuery])
}, [tables])
const totalTables = tables?.length ?? 0
const renderFooter = () => (
<div className="absolute bottom-0 left-0 right-0 flex items-center justify-between min-h-9 h-9 px-4 border-t bg-surface-200 text-xs text-foreground-light z-10">
<div className="flex items-center gap-x-2">
{isLoadingTables ? (
<span className="flex items-center gap-2">
<Loader2 size={14} className="animate-spin" /> Loading...
</span>
) : (
<span>
Total: {totalTables.toLocaleString()} table{totalTables !== 1 ? 's' : ''}
</span>
)}
</div>
</div>
)
if (isLoadingTables) {
return (
<div className="relative h-full flex flex-col">
<div className="flex-1 min-h-0 overflow-hidden">
<SkeletonResults />
</div>
{renderFooter()}
</div>
)
}
if (isErrorTables) {
return (
<div className="relative h-full flex flex-col">
<div className="flex-1 min-h-0 overflow-hidden">
<div className="h-full flex flex-col items-center justify-center py-12 px-4 gap-4 text-center text-foreground-lighter">
<Database className="h-6 w-6" strokeWidth={1.5} />
<p className="text-sm">Failed to load tables</p>
</div>
</div>
{renderFooter()}
</div>
)
}
if (tableResults.length === 0) {
return (
<div className="relative h-full flex flex-col">
<div className="flex-1 min-h-0 overflow-hidden">
<EmptyState icon={Database} label="Database Tables" query={query} />
</div>
{renderFooter()}
</div>
)
}
useEffect(() => {
if (
sentinelEntry?.isIntersecting &&
hasNextTablesPage &&
!isFetchingNextTablesPage &&
isSuccess
) {
fetchNextTablesPage()
}
}, [
isSuccess,
sentinelEntry?.isIntersecting,
hasNextTablesPage,
isFetchingNextTablesPage,
fetchNextTablesPage,
])
return (
<div className="relative h-full flex flex-col">
<div className="flex-1 min-h-0 overflow-hidden">
<ResultsList
results={tableResults}
icon={Database}
getRoute={(result) => {
const table = tables?.find((t) => String(t.id) === result.id)
if (!table || !projectRef) return `/project/${projectRef}/editor` as `/${string}`
{isLoadingTables ? (
<SkeletonResults />
) : isErrorTables ? (
<div className="h-full flex flex-col items-center justify-center py-12 px-4 gap-4 text-center text-foreground-lighter">
<Database className="h-6 w-6" strokeWidth={1.5} />
<p className="text-sm">Failed to load tables</p>
</div>
) : tableResults.length === 0 ? (
<EmptyState icon={Database} label="Database Tables" query={debouncedFilterString} />
) : (
<>
<ResultsList
results={tableResults}
icon={Database}
getRoute={(result) => {
const table = tables?.find((t) => String(t.id) === result.id)
if (!table || !projectRef) return `/project/${projectRef}/editor` as `/${string}`
const schemaParam = table.schema ? `?schema=${table.schema}` : ''
return `/project/${projectRef}/editor/${table.id}${schemaParam}` as `/${string}`
}}
className="pb-9"
/>
const schemaParam = table.schema ? `?schema=${table.schema}` : ''
return `/project/${projectRef}/editor/${table.id}${schemaParam}` as `/${string}`
}}
infiniteLoadingObserverRef={sentinelRef}
className="pb-9"
/>
<div className="absolute bottom-0 left-0 right-0 flex items-center justify-between min-h-9 h-9 px-4 border-t bg-surface-200 text-xs text-foreground-light z-10">
<div className="flex items-center gap-x-2">
{isLoadingTables ? (
<span className="flex items-center gap-2">
<Loader2 size={14} className="animate-spin" /> Loading...
</span>
) : (
<span>
Total: {totalTables.toLocaleString()} table{totalTables !== 1 ? 's' : ''}
{hasNextTablesPage ? ' loaded' : ''}
</span>
)}
</div>
</div>
</>
)}
</div>
{renderFooter()}
</div>
)
}
@@ -75,7 +75,6 @@ export const PublicationsTableItem = ({
return (
<TableRow key={table.id}>
<TableCell className="py-3 whitespace-nowrap">{table.name}</TableCell>
<TableCell className="py-3 whitespace-nowrap text-foreground-light">{table.schema}</TableCell>
<TableCell className="py-3 whitespace-nowrap hidden lg:table-cell max-w-sm truncate text-foreground-light">
{table.comment}
</TableCell>
@@ -10,9 +10,12 @@ import { PublicationTablesSkeleton } from './PublicationSkeleton'
import { PublicationsTableItem } from './PublicationsTableItem'
import { AlertError } from '@/components/ui/AlertError'
import { NoSearchResults } from '@/components/ui/NoSearchResults'
import SchemaSelector from '@/components/ui/SchemaSelector'
import { Shortcut } from '@/components/ui/Shortcut'
import { useDatabasePublicationsQuery } from '@/data/database-publications/database-publications-query'
import { useTablesQuery } from '@/data/tables/tables-query'
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
import { useQuerySchemaState } from '@/hooks/misc/useSchemaQueryState'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { onSearchInputEscape } from '@/lib/keyboard'
import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
@@ -21,7 +24,10 @@ import { useShortcut } from '@/state/shortcuts/useShortcut'
export const PublicationsTables = () => {
const { id } = useParams()
const { data: project } = useSelectedProjectQuery()
const { selectedSchema, setSelectedSchema } = useQuerySchemaState()
const [filterString, setFilterString] = useState<string>('')
const [schemaSelectorOpen, setSchemaSelectorOpen] = useState(false)
const searchInputRef = useRef<HTMLInputElement>(null)
const { can: canUpdatePublications, isLoading: isLoadingPermissions } = useAsyncCheckPermissions(
@@ -53,6 +59,7 @@ export const PublicationsTables = () => {
} = useTablesQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
schema: selectedSchema,
})
const tables = useMemo(() => {
@@ -63,7 +70,23 @@ export const PublicationsTables = () => {
return (
<>
<div className="flex items-center justify-between mb-4">
<div className="flex flex-col lg:flex-row lg:items-center gap-2 flex-wrap mb-4">
<Shortcut
id={SHORTCUT_IDS.LIST_PAGE_FOCUS_SCHEMA}
onTrigger={() => setSchemaSelectorOpen(true)}
side="bottom"
tooltipOpen={schemaSelectorOpen ? false : undefined}
>
<SchemaSelector
className="w-full lg:w-[180px]"
size="tiny"
showError={false}
selectedSchemaName={selectedSchema}
onSelectSchema={setSelectedSchema}
open={schemaSelectorOpen}
onOpenChange={setSchemaSelectorOpen}
/>
</Shortcut>
<Input
size="tiny"
ref={searchInputRef}
@@ -89,7 +112,6 @@ export const PublicationsTables = () => {
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Schema</TableHead>
<TableHead className="hidden lg:table-cell">Description</TableHead>
{/*
We've disabled All tables toggle for publications.
@@ -106,7 +128,7 @@ export const PublicationsTables = () => {
{isError && (
<TableRow>
<TableCell colSpan={4}>
<TableCell colSpan={3}>
<AlertError error={error} subject="Failed to retrieve tables" />
</TableCell>
</TableRow>
@@ -114,7 +136,7 @@ export const PublicationsTables = () => {
{!isLoading && !isLoadingPermissions && tables.length === 0 && (
<TableRow>
<TableCell colSpan={4}>
<TableCell colSpan={3}>
<NoSearchResults
className="border-none !p-0"
searchString={filterString}
@@ -135,7 +157,7 @@ export const PublicationsTables = () => {
))
) : (
<TableRow>
<TableCell colSpan={4}>
<TableCell colSpan={3}>
<p>The selected publication with ID {id} cannot be found</p>
<p className="text-foreground-light">
Head back to the list of publications to select one from there
@@ -39,6 +39,7 @@ import FormBoxEmpty from '@/components/ui/FormBoxEmpty'
import { useDatabaseTriggerCreateMutation } from '@/data/database-triggers/database-trigger-create-mutation'
import { useDatabaseTriggerUpdateMutation } from '@/data/database-triggers/database-trigger-update-mutation'
import { useTablesQuery } from '@/data/tables/tables-query'
import { useQuerySchemaState } from '@/hooks/misc/useSchemaQueryState'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { useConfirmOnClose } from '@/hooks/ui/useConfirmOnClose'
import { useProtectedSchemas } from '@/hooks/useProtectedSchemas'
@@ -89,6 +90,7 @@ export const TriggerSheet = ({
onClose,
}: TriggerSheetProps) => {
const { data: project } = useSelectedProjectQuery()
const { selectedSchema } = useQuerySchemaState()
const [showFunctionSelector, setShowFunctionSelector] = useState(false)
@@ -118,6 +120,7 @@ export const TriggerSheet = ({
const { data = [], isSuccess: isSuccessTables } = useTablesQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
schema: selectedSchema,
})
const { data: protectedSchemas, isSuccess: isSuccessProtectedSchemas } = useProtectedSchemas()
const isSuccess = isSuccessTables && isSuccessProtectedSchemas
@@ -76,6 +76,7 @@ export const TriggersList = () => {
const { data = [] } = useTablesQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
schema: selectedSchema,
})
const hasTables =
data.filter((a) => !protectedSchemas.find((s) => s.name === a.schema)).length > 0
@@ -437,7 +437,7 @@ export const SidePanelEditor = ({
}),
queryClient.invalidateQueries({ queryKey: entityTypeKeys.list(project?.ref) }),
queryClient.invalidateQueries({
queryKey: tableKeys.list(project?.ref, selectedTable?.schema, includeColumns),
queryKey: tableKeys.list(project?.ref, selectedTable?.schema, { includeColumns }),
}),
queryClient.invalidateQueries({
queryKey: tableKeys.infiniteListPrefix(project?.ref, selectedTable?.schema),
@@ -508,7 +508,7 @@ export const SidePanelEditor = ({
// Switch it to individual tables via an array of strings
// Refer to PublicationStore for more information about this
const publicTables = await queryClient.fetchQuery({
queryKey: tableKeys.list(project.ref, 'public', includeColumns),
queryKey: tableKeys.list(project.ref, 'public', { includeColumns }),
queryFn: ({ signal }) =>
getTables(
{
@@ -646,10 +646,10 @@ export const SidePanelEditor = ({
.map(([, data]) => data?.pages?.[0]?.data?.count)
.find((count) => typeof count === 'number') ??
queryClient.getQueryData<unknown[]>(
tableKeys.list(project?.ref, payload.schema, true)
tableKeys.list(project?.ref, payload.schema, { includeColumns: true })
)?.length ??
queryClient.getQueryData<unknown[]>(
tableKeys.list(project?.ref, payload.schema, false)
tableKeys.list(project?.ref, payload.schema, { includeColumns: false })
)?.length
createTableSpan.setAttributes({
@@ -713,7 +713,7 @@ export const SidePanelEditor = ({
async () => {
await Promise.all([
queryClient.invalidateQueries({
queryKey: tableKeys.list(project?.ref, table.schema, includeColumns),
queryKey: tableKeys.list(project?.ref, table.schema, { includeColumns }),
}),
queryClient.invalidateQueries({
queryKey: tableKeys.infiniteListPrefix(project?.ref, table.schema),
@@ -783,7 +783,7 @@ export const SidePanelEditor = ({
await Promise.all([
queryClient.invalidateQueries({
queryKey: tableKeys.list(project?.ref, table.schema, includeColumns),
queryKey: tableKeys.list(project?.ref, table.schema, { includeColumns }),
}),
queryClient.invalidateQueries({
queryKey: tableKeys.infiniteListPrefix(project?.ref, table.schema),
@@ -908,7 +908,9 @@ export const updateTable = async ({
}),
queryClient.invalidateQueries({ queryKey: databaseKeys.tableDefinition(projectRef, table.id) }),
queryClient.invalidateQueries({ queryKey: entityTypeKeys.list(projectRef) }),
queryClient.invalidateQueries({ queryKey: tableKeys.list(projectRef, table.schema, true) }),
queryClient.invalidateQueries({
queryKey: tableKeys.list(projectRef, table.schema, { includeColumns: true }),
}),
queryClient.invalidateQueries({ queryKey: lintKeys.lint(projectRef) }),
])
@@ -478,7 +478,7 @@ export const TableEditor = ({
>
<DocsButton
abbrev={false}
className="mt-2"
className="mt-2 w-min"
href={`${DOCS_URL}${docsRowLevelSecurityGuidePath}`}
/>
</Admonition>
@@ -496,7 +496,7 @@ export const TableEditor = ({
>
<DocsButton
abbrev={false}
className="mt-2"
className="mt-2 w-min"
href={`${DOCS_URL}${docsRowLevelSecurityGuidePath}`}
/>
</Admonition>
@@ -1,10 +1,11 @@
import type { PGColumn } from '@supabase/pg-meta'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useDebounce, useIntersectionObserver } from '@uidotdev/usehooks'
import { useParams } from 'common'
import { AlertTriangle, Code, Loader2, Table2 } from 'lucide-react'
import { useRouter } from 'next/navigation'
import { useEffect, useMemo, useRef } from 'react'
import { cn, CommandEmpty, CommandGroup, CommandItem, CommandList } from 'ui'
import { useEffect, useRef, useState } from 'react'
import { cn, CommandGroup, CommandItem, CommandList } from 'ui'
import { CodeBlock } from 'ui-patterns/CodeBlock'
import type { CommandOptions } from 'ui-patterns/CommandMenu'
import {
@@ -26,10 +27,13 @@ import {
import { COMMAND_MENU_SECTIONS } from '@/components/interfaces/App/CommandMenu/CommandMenu.utils'
import { orderCommandSectionsByPriority } from '@/components/interfaces/App/CommandMenu/ordering'
import { useSqlSnippetsQuery, type SqlSnippet } from '@/data/content/sql-snippets-query'
import { usePrefetchTables, useTablesQuery, type TablesData } from '@/data/tables/tables-query'
import {
useInfiniteTablesQuery,
usePrefetchTables,
type TablesData,
} from '@/data/tables/tables-query'
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { useProtectedSchemas } from '@/hooks/useProtectedSchemas'
import { useProfile } from '@/lib/profile'
export function useSqlEditorGotoCommands(options?: CommandOptions) {
@@ -294,34 +298,66 @@ export function useQueryTableCommands(options?: CommandOptions) {
function TableSelector() {
const router = useRouter()
const { data: project } = useSelectedProjectQuery()
const { data: protectedSchemas } = useProtectedSchemas()
const [filterString, setFilterString] = useState('')
const debouncedFilterString = useDebounce(filterString, 300)
const [sentinelRef, sentinelEntry] = useIntersectionObserver({
threshold: 0,
rootMargin: '200px 0px 200px 0px',
})
const {
data: tablesData,
isPending: isLoading,
isError,
isSuccess,
} = useTablesQuery({
isPending: isLoading,
hasNextPage: hasNextTablesPage,
isFetchingNextPage: isFetchingNextTablesPage,
fetchNextPage: fetchNextTablesPage,
} = useInfiniteTablesQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
includeColumns: true,
pageSize: 50,
nameFilter: debouncedFilterString,
})
const tables = useMemo(() => {
return tablesData?.filter((table) => !protectedSchemas.find((s) => s.name === table.schema))
}, [tablesData, protectedSchemas])
const tables = tablesData?.pages.flat() ?? []
useEffect(() => {
if (
sentinelEntry?.isIntersecting &&
hasNextTablesPage &&
!isFetchingNextTablesPage &&
isSuccess
) {
fetchNextTablesPage()
}
}, [
isSuccess,
sentinelEntry?.isIntersecting,
hasNextTablesPage,
isFetchingNextTablesPage,
fetchNextTablesPage,
])
return (
<CommandWrapper>
<CommandWrapper shouldFilter={false}>
<CommandHeader>
<Breadcrumb />
<CommandMenuInput autoFocus />
<CommandMenuInput
autoFocus
placeholder="Search for schema or a table to query"
value={filterString}
onValueChange={setFilterString}
/>
</CommandHeader>
<CommandList>
<CommandList className="pb-9">
{isLoading && <LoadingState />}
{isError && <ErrorState />}
{isSuccess && (
<>
<CommandEmpty />
<CommandGroup>
<CommandGroup className="py-2">
{tables?.map((table) => (
<CommandItem
key={table.id}
@@ -337,6 +373,30 @@ function TableSelector() {
</CommandItem>
))}
</CommandGroup>
{tables.length === 0 && debouncedFilterString && (
<p className="text-xs text-center text-foreground-lighter py-3">
No tables found based on your search
</p>
)}
{tables.length > 0 && (
<>
<div ref={sentinelRef} />
<div className="absolute bottom-0 left-0 right-0 flex items-center justify-between min-h-9 h-9 px-4 border-t bg-surface-200 text-xs text-foreground-light z-10">
<div className="flex items-center gap-x-2">
{isFetchingNextTablesPage ? (
<span className="flex items-center gap-2">
<Loader2 size={14} className="animate-spin" /> Loading...
</span>
) : (
<span>
Total: {tables.length} {tables.length === 1 ? 'table' : 'tables'}
{hasNextTablesPage ? ' loaded' : ''}
</span>
)}
</div>
</div>
</>
)}
</>
)}
</CommandList>
@@ -129,7 +129,10 @@ export const getContextualInvalidationKeys = ({
'auth/users': [authKeys.usersInfinite(ref)],
'auth/policies': [databasePoliciesKeys.list(ref)],
'database/functions': [databaseKeys.databaseFunctions(ref)],
'database/tables': [tableKeys.list(ref, schema, true), tableKeys.list(ref, schema, false)],
'database/tables': [
tableKeys.list(ref, schema, { includeColumns: true }),
tableKeys.list(ref, schema, { includeColumns: false }),
],
'database/triggers': [databaseTriggerKeys.list(ref)],
'database/types': [enumeratedTypesKeys.list(ref)],
'database/extensions': [databaseExtensionsKeys.list(ref)],
+5 -2
View File
@@ -1,7 +1,10 @@
export const tableKeys = {
names: (projectRef: string | undefined) => ['projects', projectRef, 'table-names'] as const,
list: (projectRef: string | undefined, schema?: string, includeColumns?: boolean) =>
['projects', projectRef, 'tables', schema, includeColumns].filter(Boolean),
list: (
projectRef: string | undefined,
schema: string | undefined,
options?: { includeColumns?: boolean }
) => ['projects', projectRef, 'tables', schema, options].filter(Boolean),
infiniteListPrefix: (projectRef: string | undefined, schema?: string) =>
['projects', projectRef, 'tables', 'infinite', schema].filter(
(part) => part !== undefined && part !== null && part !== ''
@@ -52,10 +52,10 @@ export const useTableCreateMutation = ({
await Promise.all([
queryClient.invalidateQueries({
queryKey: tableKeys.list(projectRef, payload.schema, true),
queryKey: tableKeys.list(projectRef, payload.schema, { includeColumns: true }),
}),
queryClient.invalidateQueries({
queryKey: tableKeys.list(projectRef, payload.schema, false),
queryKey: tableKeys.list(projectRef, payload.schema, { includeColumns: false }),
}),
queryClient.invalidateQueries({
queryKey: privilegeKeys.tablePrivilegesList(projectRef),
+23 -47
View File
@@ -1,11 +1,10 @@
import { getTablesPaginatedSql, type PGTable } from '@supabase/pg-meta'
import { DEFAULT_PLATFORM_APPLICATION_NAME } from '@supabase/pg-meta/src/constants'
import pgMeta, { getTablesPaginatedSql, type PGTable } from '@supabase/pg-meta'
import { InfiniteData, useInfiniteQuery, useQuery, useQueryClient } from '@tanstack/react-query'
import { sortBy } from 'lodash'
import { useCallback } from 'react'
import { z } from 'zod'
import { tableKeys } from './keys'
import { get, handleError } from '@/data/fetchers'
import { executeSql } from '@/data/sql/execute-sql-mutation'
import type { SafePostgresTable } from '@/lib/postgres-types'
import type { ResponseError, UseCustomInfiniteQueryOptions, UseCustomQueryOptions } from '@/types'
@@ -14,13 +13,14 @@ export type TablesVariables = {
projectRef?: string
connectionString?: string | null
schema?: string
/**
* Defaults to false
*/
/** Defaults to false */
includeColumns?: boolean
sortByProperty?: keyof PGTable
}
const pgMetaTablesList = pgMeta.tables.list()
export type TablesData = z.infer<typeof pgMetaTablesList.zod>
export async function getTables(
{
projectRef,
@@ -31,55 +31,31 @@ export async function getTables(
}: TablesVariables,
signal?: AbortSignal
) {
if (!projectRef) {
throw new Error('projectRef is required')
}
let headers = new Headers()
if (connectionString) headers.set('x-connection-encrypted', connectionString)
let queryParams: Record<string, string> = {
//include_columns is a string, even though it's true or false
include_columns: `${includeColumns}`,
}
if (schema) {
queryParams.included_schemas = schema
}
const { data, error } = await get('/platform/pg-meta/{ref}/tables', {
params: {
header: {
'x-connection-encrypted': connectionString!,
'x-pg-application-name': DEFAULT_PLATFORM_APPLICATION_NAME,
},
path: { ref: projectRef },
query: queryParams as any,
},
headers,
signal,
})
if (!Array.isArray(data) && error) handleError(error)
if (!projectRef) throw new Error('projectRef is required')
const sql = pgMeta.tables.list({
includeColumns,
includedSchemas: schema ? [schema] : undefined,
}).sql
const queryKey = ['tables', schema]
const { result } = await executeSql({ projectRef, connectionString, sql, queryKey }, signal)
// Sort the data if the sortByName option is true
if (Array.isArray(data) && sortByProperty) {
return sortBy(data, (t) => t[sortByProperty]) as SafePostgresTable[]
if (Array.isArray(result) && sortByProperty) {
return sortBy(result, (t) => t[sortByProperty]) as SafePostgresTable[]
}
return data as SafePostgresTable[]
return result as SafePostgresTable[]
}
export type TablesData = Awaited<ReturnType<typeof getTables>>
export type TablesError = ResponseError
export const useTablesQuery = <TData = TablesData>(
{ projectRef, connectionString, schema, includeColumns }: TablesVariables,
vars: TablesVariables,
{ enabled = true, ...options }: UseCustomQueryOptions<TablesData, TablesError, TData> = {}
) => {
const { projectRef, schema, includeColumns } = vars
return useQuery<TablesData, TablesError, TData>({
queryKey: tableKeys.list(projectRef, schema, includeColumns),
queryFn: ({ signal }) =>
getTables({ projectRef, connectionString, schema, includeColumns }, signal),
queryKey: tableKeys.list(projectRef, schema, { includeColumns }),
queryFn: ({ signal }) => getTables(vars, signal),
enabled: enabled && typeof projectRef !== 'undefined',
...options,
})
@@ -98,7 +74,7 @@ export function useGetTables({
return useCallback(
(schema?: TablesVariables['schema'], includeColumns?: TablesVariables['includeColumns']) => {
return queryClient.fetchQuery({
queryKey: tableKeys.list(projectRef, schema, includeColumns),
queryKey: tableKeys.list(projectRef, schema, { includeColumns }),
queryFn: ({ signal }) =>
getTables({ projectRef, connectionString, schema, includeColumns }, signal),
})
@@ -116,7 +92,7 @@ export function usePrefetchTables({
return useCallback(
(schema?: TablesVariables['schema'], includeColumns?: TablesVariables['includeColumns']) => {
return queryClient.prefetchQuery({
queryKey: tableKeys.list(projectRef, schema, includeColumns),
queryKey: tableKeys.list(projectRef, schema, { includeColumns }),
queryFn: ({ signal }) =>
getTables({ projectRef, connectionString, schema, includeColumns }, signal),
})
@@ -176,7 +152,7 @@ export async function getTablesPage(
return result as SafePostgresTable[]
}
export const useInfiniteTablesQuery = <TData = InfiniteData<TablesData>>(
export const useInfiniteTablesQuery = <TData = InfiniteData<SafePostgresTable[]>>(
{
projectRef,
connectionString,
@@ -37,7 +37,12 @@ export const getTablesPaginatedSql = ({
const schemaFilter = filter ? safeSql`and nc.nspname ${filter}` : safeSql``
const nameFilterClause =
nameFilter && nameFilter.length > 0
? safeSql`and c.relname ilike ${literal(`%${escapeIlikeLiteral(nameFilter)}%`)}`
? schema
? safeSql`and c.relname ilike ${literal(`%${escapeIlikeLiteral(nameFilter)}%`)}`
: safeSql`and (
c.relname ilike ${literal(`%${escapeIlikeLiteral(nameFilter)}%`)}
or nc.nspname ilike ${literal(`%${escapeIlikeLiteral(nameFilter)}%`)}
)`
: safeSql``
const columnsCte = includeColumns
@@ -90,7 +90,7 @@ const CommandList = React.forwardRef<
>(({ className, ...props }, ref) => (
<CommandPrimitive.List
ref={ref}
className={cn('max-h-[300px] overflow-y-auto overflow-x-hidden', className)}
className={cn('max-h-full overflow-y-auto overflow-x-hidden', className)}
{...props}
/>
))