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_Shadcn_, CommandGroup_Shadcn_, CommandInput_Shadcn_, CommandItem_Shadcn_, CommandList_Shadcn_, Popover_Shadcn_, PopoverContent_Shadcn_, PopoverTrigger_Shadcn_, ScrollArea, Tooltip, TooltipContent, TooltipTrigger, } from 'ui' import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' import { exposedFunctionCountsQueryOptions } from '@/data/privileges/exposed-function-counts-query' import { exposedFunctionsInfiniteQueryOptions } from '@/data/privileges/exposed-functions-infinite-query' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' import { pluralize } from '@/lib/helpers' interface ExposedFunctionSelectorProps { disabled?: boolean selectedSchemas: string[] pendingAddFunctionNames: string[] pendingRemoveFunctionNames: string[] onTogglePendingAdd: (functionName: string) => void onTogglePendingRemove: (functionName: string) => void } export const ExposedFunctionSelector = ({ disabled = false, selectedSchemas, pendingAddFunctionNames, pendingRemoveFunctionNames, onTogglePendingAdd, onTogglePendingRemove, }: ExposedFunctionSelectorProps) => { 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({ ...exposedFunctionCountsQueryOptions({ projectRef: project?.ref, connectionString: project?.connectionString, selectedSchemas, }), placeholderData: keepPreviousData, }) const pendingCount = pendingAddFunctionNames.length + pendingRemoveFunctionNames.length const totalCount = countsData?.total_count ?? 0 const grantsCount = countsData?.grants_count ?? 0 const { data, isPending, isError, isFetching, isFetchingNextPage, hasNextPage, fetchNextPage } = useInfiniteQuery({ ...exposedFunctionsInfiniteQueryOptions({ projectRef: project?.ref, connectionString: project?.connectionString, search: search.length === 0 ? undefined : debouncedSearch || undefined, }), placeholderData: search.length > 0 ? keepPreviousData : undefined, }) const functions = useMemo( () => data?.pages.flatMap((page) => page.functions) ?? [], [data?.pages] ) const pendingAddSet = useMemo(() => new Set(pendingAddFunctionNames), [pendingAddFunctionNames]) const pendingRemoveSet = useMemo( () => new Set(pendingRemoveFunctionNames), [pendingRemoveFunctionNames] ) useEffect(() => { if (!isPending && !isFetching && entry?.isIntersecting && hasNextPage && !isFetchingNextPage) { fetchNextPage() } }, [entry?.isIntersecting, hasNextPage, isFetching, isFetchingNextPage, isPending, fetchNextPage]) return ( {isPending ? ( <>
) : isError ? (

Failed to retrieve functions

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

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

)} 7 ? 'h-[210px]' : ''} > {functions.map((fn) => { const key = `${fn.schema}.${fn.name}` const isSchemaExposed = selectedSchemas.includes(fn.schema) const hasPendingAdd = pendingAddSet.has(key) const hasPendingRemove = pendingRemoveSet.has(key) const isCustom = fn.status === 'custom' const isGranted = fn.status === 'granted' const isCustomNeutral = isCustom && !hasPendingAdd && !hasPendingRemove const isExposed = isSchemaExposed && (isCustom ? hasPendingAdd : isGranted ? !hasPendingRemove : hasPendingAdd) const customGrantsTooltip = getCustomGrantsTooltip({ hasPendingAdd, hasPendingRemove, }) return ( { if (!isSchemaExposed) return if (isCustom) { if (hasPendingAdd) { onTogglePendingAdd(key) onTogglePendingRemove(key) } else if (hasPendingRemove) { onTogglePendingRemove(key) onTogglePendingAdd(key) } else { onTogglePendingAdd(key) } return } if (isGranted) { onTogglePendingRemove(key) } else { onTogglePendingAdd(key) } }} >
{isExposed && } {!isSchemaExposed && ( The schema "{fn.schema}" must be exposed before enabling this function. )}
{key}
{isCustom && (
{customGrantsTooltip}
)}
) })}
{hasNextPage && (
)} )} ) } const getCustomGrantsTooltip = ({ hasPendingAdd, hasPendingRemove, }: { hasPendingAdd: boolean hasPendingRemove: boolean }) => { if (hasPendingAdd) { return 'This function 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 function 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 function has custom grants. Select it to override with standard Data API grants for anon, authenticated, and service_role.' }