'use client' import { useDebounce } from '@uidotdev/usehooks' import { useParams } from 'common' import { AnalyticsBucket as AnalyticsBucketIcon, FilesBucket, Storage, VectorBucket } from 'icons' import { Loader2 } from 'lucide-react' import { useCallback, useMemo } from 'react' import { EmptyState, ResultsList, SkeletonResults, type SearchResult, } from './ContextSearchResults.shared' import { useIsAnalyticsBucketsEnabled, useIsVectorBucketsEnabled, } from '@/data/config/project-storage-config-query' import { useAnalyticsBucketsQuery, type AnalyticsBucket, } from '@/data/storage/analytics-buckets-query' import { useBucketNumberEstimateQuery, usePaginatedBucketsQuery, type Bucket, } from '@/data/storage/buckets-query' import { useVectorBucketsQuery } from '@/data/storage/vector-buckets-query' interface StorageSearchResultsProps { query: string } type ExtendedSearchResult = SearchResult & { bucketType?: 'file' | 'analytics' | 'vector' bucket?: unknown } function filterBuckets( buckets: T[] | null | undefined, query: string, filterFn: (bucket: T, searchLower: string) => boolean, mapFn: (bucket: T) => ExtendedSearchResult ): ExtendedSearchResult[] { if (!buckets) return [] const trimmedQuery = query.trim() const filtered = trimmedQuery ? buckets.filter((bucket) => filterFn(bucket, trimmedQuery.toLowerCase())) : buckets return filtered.slice(0, 10).map(mapFn) } export function StorageSearchResults({ query }: StorageSearchResultsProps) { const { ref: projectRef } = useParams() const isAnalyticsBucketsEnabled = useIsAnalyticsBucketsEnabled({ projectRef }) const isVectorBucketsEnabled = useIsVectorBucketsEnabled({ projectRef }) // Debounce the search query to avoid excessive API calls const debouncedQuery = useDebounce(query.trim(), 300) const { data: fileBucketsData, isLoading: isLoadingFileBuckets, isError: isErrorFileBuckets, } = usePaginatedBucketsQuery( { projectRef: projectRef ?? undefined, limit: 10, search: debouncedQuery.length > 0 ? debouncedQuery : undefined, }, { enabled: !!projectRef, } ) const fileBuckets = useMemo( () => fileBucketsData?.pages.flatMap((page) => page) ?? [], [fileBucketsData] ) const { data: analyticsBuckets, isLoading: isLoadingAnalyticsBuckets, isError: isErrorAnalyticsBuckets, } = useAnalyticsBucketsQuery( { projectRef: projectRef ?? undefined, }, { enabled: !!projectRef && isAnalyticsBucketsEnabled, } ) const { data: vectorBucketsData, isLoading: isLoadingVectorBuckets, isError: isErrorVectorBuckets, } = useVectorBucketsQuery( { projectRef: projectRef ?? undefined, }, { enabled: !!projectRef && isVectorBucketsEnabled, } ) const vectorBuckets = useMemo(() => vectorBucketsData?.vectorBuckets ?? [], [vectorBucketsData]) const { data: fileBucketsEstimate } = useBucketNumberEstimateQuery({ projectRef, }) const isLoading = isLoadingFileBuckets || (isAnalyticsBucketsEnabled && isLoadingAnalyticsBuckets) || (isVectorBucketsEnabled && isLoadingVectorBuckets) const isError = isErrorFileBuckets || (isAnalyticsBucketsEnabled && isErrorAnalyticsBuckets) || (isVectorBucketsEnabled && isErrorVectorBuckets) const fileBucketResults: ExtendedSearchResult[] = useMemo(() => { // Server-side search is already applied, no need for client-side filtering if (!fileBuckets) return [] return fileBuckets.map((bucket) => { const displayName = bucket.name || bucket.id || 'Untitled Bucket' const visibility = bucket.public ? 'Public' : 'Private' const description = `File bucket • ${visibility}` return { id: `file-bucket-${bucket.id || bucket.name}`, name: displayName, description, bucketType: 'file' as const, bucket, } }) }, [fileBuckets]) const analyticsBucketResults: ExtendedSearchResult[] = useMemo(() => { return filterBuckets( analyticsBuckets, debouncedQuery, // Use debounced query for consistency (bucket, searchLower) => { const bucketName = bucket.name?.toLowerCase() || '' return bucketName.includes(searchLower) }, (bucket) => { const displayName = bucket.name || 'Untitled Bucket' const description = 'Analytics bucket' return { id: `analytics-bucket-${bucket.name}`, name: displayName, description, bucketType: 'analytics' as const, bucket, } } ) }, [analyticsBuckets, debouncedQuery]) const vectorBucketResults: ExtendedSearchResult[] = useMemo(() => { return filterBuckets( vectorBuckets, debouncedQuery, // Use debounced query for consistency (bucket, searchLower) => { const bucketName = bucket.vectorBucketName?.toLowerCase() || '' return bucketName.includes(searchLower) }, (bucket) => { const displayName = bucket.vectorBucketName || 'Untitled Bucket' const description = 'Vector bucket' return { id: `vector-bucket-${bucket.vectorBucketName}`, name: displayName, description, bucketType: 'vector' as const, bucket, } } ) }, [vectorBuckets, debouncedQuery]) const allResults: ExtendedSearchResult[] = useMemo(() => { const results = [fileBucketResults] if (isAnalyticsBucketsEnabled) { results.push(analyticsBucketResults) } if (isVectorBucketsEnabled) { results.push(vectorBucketResults) } return results.flat().slice(0, 20) }, [ fileBucketResults, analyticsBucketResults, vectorBucketResults, isAnalyticsBucketsEnabled, isVectorBucketsEnabled, ]) const getRoute = useCallback( (result: SearchResult) => { if (!projectRef) return '/storage/files' as `/${string}` const extendedResult = result as ExtendedSearchResult if (extendedResult.bucketType && extendedResult.bucket) { const bucketType = extendedResult.bucketType if (bucketType === 'file') { const fileBucket = extendedResult.bucket as Bucket return `/project/${projectRef}/storage/files/buckets/${encodeURIComponent(fileBucket.name)}` as `/${string}` } if (bucketType === 'analytics') { const analyticsBucket = extendedResult.bucket as AnalyticsBucket return `/project/${projectRef}/storage/analytics/buckets/${encodeURIComponent(analyticsBucket.name)}` as `/${string}` } if (bucketType === 'vector') { const vectorBucket = extendedResult.bucket as { vectorBucketName: string } return `/project/${projectRef}/storage/vectors/buckets/${encodeURIComponent(vectorBucket.vectorBucketName)}` as `/${string}` } } return `/project/${projectRef}/storage/files` as `/${string}` }, [projectRef] ) const totalBucketsEstimate = useMemo(() => { const fileBucketCount = fileBucketsEstimate ?? 0 const analyticsBucketCount = isAnalyticsBucketsEnabled ? (analyticsBuckets?.length ?? 0) : 0 const vectorBucketCount = isVectorBucketsEnabled ? (vectorBuckets?.length ?? 0) : 0 return fileBucketCount + analyticsBucketCount + vectorBucketCount }, [ fileBucketsEstimate, analyticsBuckets?.length, vectorBuckets?.length, isAnalyticsBucketsEnabled, isVectorBucketsEnabled, ]) const getIcon = useCallback((result: SearchResult) => { const extendedResult = result as ExtendedSearchResult if (extendedResult.bucketType === 'file') return FilesBucket if (extendedResult.bucketType === 'analytics') return AnalyticsBucketIcon if (extendedResult.bucketType === 'vector') return VectorBucket return Storage }, []) const renderFooter = () => (
{isLoading ? ( Loading... ) : ( Total: {totalBucketsEstimate.toLocaleString()} bucket {totalBucketsEstimate !== 1 ? 's' : ''} (estimate) )}
) if (isLoading) { return (
{renderFooter()}
) } if (isError) { return (

Failed to load storage buckets

{renderFooter()}
) } if (allResults.length === 0) { return (
{renderFooter()}
) } return (
{renderFooter()}
) }