import type { PostgresTable } from '@supabase/postgres-meta' import { PermissionAction } from '@supabase/shared-types/out/constants' import { noop } from 'lodash' import { Check, Columns, 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 { useState } from 'react' import { useParams } from 'common' import { buildTableEditorUrl } from 'components/grid/SupabaseGrid.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 { 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 { Button, Card, Checkbox_Shadcn_, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, Label_Shadcn_, PopoverContent_Shadcn_, PopoverTrigger_Shadcn_, Popover_Shadcn_, Table, TableBody, TableCell, 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' interface TableListProps { onAddTable: () => void onEditTable: (table: PostgresTable) => void onDeleteTable: (table: PostgresTable) => void onDuplicateTable: (table: PostgresTable) => 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 { 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 === 'supabase_realtime' ) const entities = formatAllEntities({ tables, views, materializedViews, foreignTables }).filter( (x) => visibleTypes.includes(x.type) ) const { isSchemaLocked } = useIsProtectedSchema({ schema: selectedSchema }) 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 (
))}
setFilterString(e.target.value)} icon={} /> {!isSchemaLocked && ( } disabled={!canUpdateTables} onClick={() => onAddTable()} tooltip={{ content: { side: 'bottom', text: !canUpdateTables ? 'You need additional permissions to create tables' : undefined, }, }} > New table )}
{isSchemaLocked && } {isLoading && } {isError && } {isSuccess && (
Name Description Rows (Estimated) Size (Estimated) Realtime Enabled <> {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) => ( {/* [Alaister]: EntityTypeIcon supports PARTITIONED_TABLE, but formatAllEntities doesn't distinguish between tables and partitioned tables yet. Once the endpoint/formatAllEntities is updated to include partitioned tables, EntityTypeIcon will automatically style them correctly. */} {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} ) : (

No description

)}
{x.rows !== undefined ? x.rows.toLocaleString() : '-'} {x.size !== undefined ? ( {x.size} ) : ( '-' )} {(realtimePublication?.tables ?? []).find( (table) => table.id === x.id ) ? (
) : (
)}
{!isSchemaLocked && (
))}
)} ) }