Files
Joshen Lim aba4e147eb 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 -->
2026-06-16 15:45:39 +08:00

135 lines
4.2 KiB
TypeScript

import { useIntersectionObserver } from '@uidotdev/usehooks'
import { useParams } from 'common'
import { Database, Loader2 } from 'lucide-react'
import { useEffect, useMemo } from 'react'
import {
EmptyState,
ResultsList,
SkeletonResults,
type SearchResult,
} from './ContextSearchResults.shared'
import { useInfiniteTablesQuery } from '@/data/tables/tables-query'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
interface TableSearchResultsProps {
debouncedFilterString: string
}
export function TableSearchResults({ debouncedFilterString }: TableSearchResultsProps) {
const { ref: projectRef } = useParams()
const { data: project } = useSelectedProjectQuery()
const [sentinelRef, sentinelEntry] = useIntersectionObserver({
threshold: 0,
rootMargin: '200px 0px 200px 0px',
})
const {
data: tablesData,
isSuccess,
isError: isErrorTables,
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 []
return tables.map((table) => {
const displayName =
table.schema && table.schema !== 'public'
? `${table.schema}.${table.name}`
: table.name || 'Untitled Table'
const description = table.comment
? table.comment.length > 50
? `${table.comment.slice(0, 50)}...`
: table.comment
: undefined
return {
id: String(table.id),
name: displayName,
description,
}
})
}, [tables])
const totalTables = tables?.length ?? 0
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">
{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}`
}}
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>
</div>
)
}