import { keepPreviousData, useInfiniteQuery, useQuery } from '@tanstack/react-query' import { useDebounce, useIntersectionObserver } from '@uidotdev/usehooks' import { Check, ChevronsUpDown, CircleAlert, Info } from 'lucide-react' import { useEffect, useMemo, useRef, useState } from 'react' import { Button, cn, Command, CommandGroup, CommandInput, CommandItem, CommandList, Popover, PopoverContent, PopoverTrigger, ScrollArea, Tooltip, TooltipContent, TooltipTrigger, } from 'ui' import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' import { exposedTableCountsQueryOptions } from '@/data/privileges/exposed-table-counts-query' import { exposedTablesInfiniteQueryOptions } from '@/data/privileges/exposed-tables-infinite-query' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' import { pluralize } from '@/lib/helpers' interface ExposedTableSelectorProps { disabled?: boolean selectedSchemas: string[] pendingAddTableIds: number[] pendingRemoveTableIds: number[] onTogglePendingAdd: (tableId: number) => void onTogglePendingRemove: (tableId: number) => void } export const ExposedTableSelector = ({ disabled = false, selectedSchemas, pendingAddTableIds, pendingRemoveTableIds, onTogglePendingAdd, onTogglePendingRemove, }: ExposedTableSelectorProps) => { const [open, setOpen] = useState(false) const [search, setSearch] = useState('') const debouncedSearch = useDebounce(search, 300) const { data: project } = useSelectedProjectQuery() const scrollRootRef = useRef(null) const [sentinelRef, entry] = useIntersectionObserver({ root: scrollRootRef.current, threshold: 0, rootMargin: '0px', }) const { data: countsData, isPending: isCountsPending } = useQuery({ ...exposedTableCountsQueryOptions({ projectRef: project?.ref, connectionString: project?.connectionString, selectedSchemas, }), placeholderData: keepPreviousData, }) const pendingCount = pendingAddTableIds.length + pendingRemoveTableIds.length const totalCount = countsData?.total_count ?? 0 const grantsCount = countsData?.grants_count ?? 0 const { data, isPending, isError, isFetching, isFetchingNextPage, hasNextPage, fetchNextPage } = useInfiniteQuery({ ...exposedTablesInfiniteQueryOptions({ projectRef: project?.ref, connectionString: project?.connectionString, search: search.length === 0 ? undefined : debouncedSearch || undefined, }), placeholderData: search.length > 0 ? keepPreviousData : undefined, }) const tables = useMemo(() => data?.pages.flatMap((page) => page.tables) ?? [], [data?.pages]) const pendingAddSet = useMemo(() => new Set(pendingAddTableIds), [pendingAddTableIds]) const pendingRemoveSet = useMemo(() => new Set(pendingRemoveTableIds), [pendingRemoveTableIds]) useEffect(() => { if (!isPending && !isFetching && entry?.isIntersecting && hasNextPage && !isFetchingNextPage) { fetchNextPage() } }, [entry?.isIntersecting, hasNextPage, isFetching, isFetchingNextPage, isPending, fetchNextPage]) return ( {isPending ? ( <>
) : isError ? (

Failed to retrieve tables

) : ( <> {tables.length === 0 && (

{search.length > 0 ? 'No tables found' : 'No tables available'}

)} 7 ? 'h-[210px]' : ''}> {tables.map((table) => { const isSchemaExposed = selectedSchemas.includes(table.schema) const hasPendingAdd = pendingAddSet.has(table.id) const hasPendingRemove = pendingRemoveSet.has(table.id) const isCustomTable = table.status === 'custom' const isGranted = table.status === 'granted' const isCustomNeutral = isCustomTable && !hasPendingAdd && !hasPendingRemove const isExposed = isSchemaExposed && (isCustomTable ? hasPendingAdd : isGranted ? !hasPendingRemove : hasPendingAdd) const customGrantsTooltip = getCustomGrantsTooltip({ hasPendingAdd, hasPendingRemove, }) return ( { if (!isSchemaExposed) return if (isCustomTable) { if (hasPendingAdd) { onTogglePendingAdd(table.id) onTogglePendingRemove(table.id) } else if (hasPendingRemove) { onTogglePendingRemove(table.id) onTogglePendingAdd(table.id) } else { onTogglePendingAdd(table.id) } return } if (isGranted) { onTogglePendingRemove(table.id) } else { onTogglePendingAdd(table.id) } }} >
{isExposed && } {!isSchemaExposed && ( The schema "{table.schema}" must be exposed before enabling this table. )}
{`${table.schema}.${table.name}`}
{isCustomTable && (
{customGrantsTooltip}
)}
) })}
{hasNextPage && (
)} )} ) } const getCustomGrantsTooltip = ({ hasPendingAdd, hasPendingRemove, }: { hasPendingAdd: boolean hasPendingRemove: boolean }) => { if (hasPendingAdd) { return 'This table has custom grants. Saving will override them with standard Data API grants for anon, authenticated, and service_role. Select again to revoke all grants instead.' } if (hasPendingRemove) { return 'This table has custom grants. Saving will revoke all grants for anon, authenticated, and service_role. Select again to override with standard Data API grants instead.' } return 'This table has custom grants. Select it to override with standard Data API grants for anon, authenticated, and service_role.' }