import { PermissionAction } from '@supabase/shared-types/out/constants' import { useParams } from 'common' import { noop } from 'lodash' import { Check, Copy, Edit, Eye, Filter, MoreVertical, Plus, Search, Trash, X } from 'lucide-react' import Link from 'next/link' import { useRouter } from 'next/router' import { parseAsString, useQueryState } from 'nuqs' import { useRef, useState } from 'react' import { Button, Card, Checkbox, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, Label, Popover, PopoverContent, PopoverTrigger, Table, TableBody, TableCell, TableFooter, TableHead, TableHeader, TableRow, Tooltip, TooltipContent, TooltipTrigger, } from 'ui' import { Input } from 'ui-patterns/DataInputs/Input' import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' import { ProtectedSchemaWarning } from '../ProtectedSchemaWarning' import { formatAllEntities } from './Tables.utils' import { buildTableEditorUrl } from '@/components/grid/BrivenGrid.utils' import AlertError from '@/components/ui/AlertError' import { ButtonTooltip } from '@/components/ui/ButtonTooltip' import { DropdownMenuItemTooltip } from '@/components/ui/DropdownMenuItemTooltip' import { EntityTypeIcon } from '@/components/ui/EntityTypeIcon' import SchemaSelector from '@/components/ui/SchemaSelector' import { Shortcut } from '@/components/ui/Shortcut' import { useDatabasePublicationsQuery } from '@/data/database-publications/database-publications-query' import { ENTITY_TYPE } from '@/data/entity-types/entity-type-constants' import { useForeignTablesQuery } from '@/data/foreign-tables/foreign-tables-query' import { useMaterializedViewsQuery } from '@/data/materialized-views/materialized-views-query' import { usePrefetchEditorTablePage } from '@/data/prefetchers/project.$ref.editor.$id' import { useTablesQuery } from '@/data/tables/tables-query' import { useViewsQuery } from '@/data/views/views-query' import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' import { useQuerySchemaState } from '@/hooks/misc/useSchemaQueryState' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' import { useIsProtectedSchema } from '@/hooks/useProtectedSchemas' import { onSearchInputEscape } from '@/lib/keyboard' import type { SafePostgresTable } from '@/lib/postgres-types' import { SHORTCUT_IDS } from '@/state/shortcuts/registry' import { useShortcut } from '@/state/shortcuts/useShortcut' interface TableListProps { onAddTable: () => void onEditTable: (table: SafePostgresTable) => void onDeleteTable: (table: SafePostgresTable) => void onDuplicateTable: (table: SafePostgresTable) => void } export const TableList = ({ onDuplicateTable, onAddTable = noop, onEditTable = noop, onDeleteTable = noop, }: TableListProps) => { const router = useRouter() const { ref } = useParams() const { data: project } = useSelectedProjectQuery() const prefetchEditorTablePage = usePrefetchEditorTablePage() const { selectedSchema, setSelectedSchema } = useQuerySchemaState() const [filterString, setFilterString] = useQueryState('search', parseAsString.withDefault('')) const [visibleTypes, setVisibleTypes] = useState(Object.values(ENTITY_TYPE)) const [schemaSelectorOpen, setSchemaSelectorOpen] = useState(false) const searchInputRef = useRef(null) const { can: canUpdateTables } = useAsyncCheckPermissions( PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables' ) const { data: tables, error: tablesError, isError: isErrorTables, isPending: isLoadingTables, isSuccess: isSuccessTables, } = useTablesQuery( { projectRef: project?.ref, connectionString: project?.connectionString, schema: selectedSchema, sortByProperty: 'name', includeColumns: true, }, { select(tables) { return filterString.length === 0 ? tables : tables.filter((table) => table.name.toLowerCase().includes(filterString.toLowerCase())) }, } ) const { data: views, error: viewsError, isError: isErrorViews, isPending: isLoadingViews, isSuccess: isSuccessViews, } = useViewsQuery( { projectRef: project?.ref, connectionString: project?.connectionString, schema: selectedSchema, }, { select(views) { return filterString.length === 0 ? views : views.filter((view) => view.name.toLowerCase().includes(filterString.toLowerCase())) }, } ) const { data: materializedViews, error: materializedViewsError, isError: isErrorMaterializedViews, isPending: isLoadingMaterializedViews, isSuccess: isSuccessMaterializedViews, } = useMaterializedViewsQuery( { projectRef: project?.ref, connectionString: project?.connectionString, schema: selectedSchema, }, { select(materializedViews) { return filterString.length === 0 ? materializedViews : materializedViews.filter((view) => view.name.toLowerCase().includes(filterString.toLowerCase()) ) }, } ) const { data: foreignTables, error: foreignTablesError, isError: isErrorForeignTables, isPending: isLoadingForeignTables, isSuccess: isSuccessForeignTables, } = useForeignTablesQuery( { projectRef: project?.ref, connectionString: project?.connectionString, schema: selectedSchema, }, { select(foreignTables) { return filterString.length === 0 ? foreignTables : foreignTables.filter((table) => table.name.toLowerCase().includes(filterString.toLowerCase()) ) }, } ) const { data: publications } = useDatabasePublicationsQuery({ projectRef: project?.ref, connectionString: project?.connectionString, }) const realtimePublication = (publications ?? []).find( (publication) => publication.name === 'briven_realtime' ) const entities = formatAllEntities({ tables, views, materializedViews, foreignTables }).filter( (x) => visibleTypes.includes(x.type) ) const { isSchemaLocked } = useIsProtectedSchema({ schema: selectedSchema }) const canAddTables = canUpdateTables && !isSchemaLocked useShortcut( SHORTCUT_IDS.LIST_PAGE_FOCUS_SEARCH, () => { searchInputRef.current?.focus() searchInputRef.current?.select() }, { label: 'Search tables' } ) useShortcut(SHORTCUT_IDS.LIST_PAGE_RESET_FILTERS, () => { setVisibleTypes(Object.values(ENTITY_TYPE)) setFilterString('') }) const error = tablesError || viewsError || materializedViewsError || foreignTablesError const isError = isErrorTables || isErrorViews || isErrorMaterializedViews || isErrorForeignTables const isLoading = isLoadingTables || isLoadingViews || isLoadingMaterializedViews || isLoadingForeignTables const isSuccess = isSuccessTables && isSuccessViews && isSuccessMaterializedViews && isSuccessForeignTables const formatTooltipText = (entityType: string) => { const text = Object.entries(ENTITY_TYPE) .find(([, value]) => value === entityType)?.[0] ?.toLowerCase() ?.split('_') ?.join(' ') || '' // Return sentence case (capitalize first letter only) return text.charAt(0).toUpperCase() + text.slice(1) } return (
setSchemaSelectorOpen(true)} side="bottom" tooltipOpen={schemaSelectorOpen ? false : undefined} >
))}
setFilterString(e.target.value)} onKeyDown={onSearchInputEscape(filterString, setFilterString)} icon={} /> {!isSchemaLocked && (canAddTables ? ( onAddTable()} side="bottom" > ) : ( } disabled tooltip={{ content: { side: 'bottom', text: 'You need additional permissions to create tables', }, }} > New table ))}
{isSchemaLocked && } {isLoading && } {isError && } {isSuccess && (
Name Columns Rows (Estimated) Size (Estimated) Realtime <> {entities.length === 0 && filterString.length === 0 && ( {visibleTypes.length === 0 ? ( <>

Please select at least one entity type to filter with

There are currently no results based on the filter that you have applied

) : ( <>

No tables created yet

There are no{' '} {visibleTypes.length === 5 ? 'tables' : visibleTypes.length === 1 ? `${formatTooltipText(visibleTypes[0])}s` : `${visibleTypes .slice(0, -1) .map((x) => `${formatTooltipText(x)}s`) .join( ', ' )}, and ${formatTooltipText(visibleTypes[visibleTypes.length - 1])}s`}{' '} found in the schema "{selectedSchema}"

)}
)} {entities.length === 0 && filterString.length > 0 && (

No results found

Your search for "{filterString}" did not return any results

)} {entities.length > 0 && entities.map((x) => (
{formatTooltipText(x.type)}
{/* only show tooltips if required, to reduce noise */} {x.name.length > 20 ? (

{x.name}

{x.name}
) : (

{x.name}

)} {x.comment !== null ? ( {x.comment} ) : null}

{x.columns.length.toLocaleString()}

{x.rows !== undefined ? (

{x.rows.toLocaleString()}

) : (

)}
{x.size !== undefined ? (

{x.size}

) : (

)}
{(realtimePublication?.tables ?? []).find( (table) => table.id === x.id ) ? (

Enabled

) : (

Disabled

)}
{!isSchemaLocked && (
))}
{entities.length} {entities.length === 1 ? 'table' : 'tables'}
)} ) }