BucketsPicker.tsx165 lines · main
1import { useDebounce } from '@uidotdev/usehooks'
2import { useParams } from 'common'
3import { ArrowDownNarrowWide, Search } from 'lucide-react'
4import { useCallback, useMemo, useState } from 'react'
5import {
6 Button,
7 Card,
8 DropdownMenu,
9 DropdownMenuContent,
10 DropdownMenuRadioGroup,
11 DropdownMenuRadioItem,
12 DropdownMenuTrigger,
13} from 'ui'
14import { Admonition } from 'ui-patterns/admonition'
15import { Input } from 'ui-patterns/DataInputs/Input'
16import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
17
18import { CreateBucketModal } from '../CreateBucketModal'
19import { EmptyBucketState } from '../EmptyBucketState'
20import { CreateBucketButton } from '../NewBucketButton'
21import { STORAGE_BUCKET_SORT } from '../Storage.constants'
22import { useStoragePreference } from '../StorageExplorer/useStoragePreference'
23import { BucketsTable } from './BucketsTable'
24import type { AllowedBucketType } from './types'
25import AlertError from '@/components/ui/AlertError'
26import { InlineLink } from '@/components/ui/InlineLink'
27import { useProjectStorageConfigQuery } from '@/data/config/project-storage-config-query'
28import { usePaginatedBucketsQuery, type Bucket } from '@/data/storage/buckets-query'
29import { IS_PLATFORM } from '@/lib/constants'
30import { formatBytes } from '@/lib/helpers'
31
32export const BucketsPicker = ({
33 onSelectBucket,
34 allowedBucketType = 'all',
35}: {
36 onSelectBucket: (bucket: Bucket) => void
37 allowedBucketType?: AllowedBucketType
38}) => {
39 const { ref: projectRef } = useParams()
40 const [createBucketShown, showCreateBucket] = useState(false)
41 const { sortBucket, setSortBucket } = useStoragePreference(projectRef!)
42
43 const [filterString, setFilterString] = useState('')
44 const debouncedFilterString = useDebounce(filterString, 250)
45 const normalizedSearch = debouncedFilterString.trim()
46
47 const sortColumn = sortBucket === STORAGE_BUCKET_SORT.ALPHABETICAL ? 'name' : 'created_at'
48 const sortOrder = sortBucket === STORAGE_BUCKET_SORT.ALPHABETICAL ? 'asc' : 'desc'
49
50 const { data } = useProjectStorageConfigQuery({ projectRef }, { enabled: IS_PLATFORM })
51 const {
52 data: bucketsData,
53 error: bucketsError,
54 isError: isErrorBuckets,
55 isPending: isLoadingBuckets,
56 isSuccess: isSuccessBuckets,
57 isFetching: isFetchingBuckets,
58 fetchNextPage,
59 hasNextPage,
60 } = usePaginatedBucketsQuery({
61 projectRef,
62 search: normalizedSearch.length > 0 ? normalizedSearch : undefined,
63 sortColumn,
64 sortOrder,
65 })
66 const buckets = useMemo(() => bucketsData?.pages.flatMap((page) => page) ?? [], [bucketsData])
67 const fileBuckets = buckets.filter((bucket) => !('type' in bucket) || bucket.type === 'STANDARD')
68 const hasNoBuckets = fileBuckets.length === 0 && normalizedSearch.length === 0
69
70 const formattedGlobalUploadLimit = formatBytes(data?.fileSizeLimit ?? 0)
71
72 const hasNoApiKeys =
73 isErrorBuckets && bucketsError.message.includes('Project has no active API keys')
74
75 const handleLoadMoreBuckets = useCallback(() => {
76 if (hasNextPage && !isFetchingBuckets) {
77 fetchNextPage()
78 }
79 }, [hasNextPage, isFetchingBuckets, fetchNextPage])
80
81 return (
82 <div className="flex h-full min-h-0 w-full flex-1 flex-col gap-3">
83 {isLoadingBuckets && <GenericSkeletonLoader />}
84 {isErrorBuckets && (
85 <>
86 {hasNoApiKeys ? (
87 <Admonition type="warning" title="Project has no active API keys enabled">
88 <p className="leading-normal! text-sm">
89 The Dashboard relies on having active API keys on the project to function. If you'd
90 like to use Storage through the Dashboard, create a set of API keys{' '}
91 <InlineLink href={`/project/${projectRef}/settings/api-keys/new`}>here</InlineLink>.
92 </p>
93 </Admonition>
94 ) : (
95 <AlertError error={bucketsError} subject="Failed to retrieve buckets" />
96 )}
97 </>
98 )}
99 {isSuccessBuckets && (
100 <>
101 {hasNoBuckets ? (
102 <EmptyBucketState
103 bucketType="files"
104 onCreateBucket={() => showCreateBucket(true)}
105 className="h-full justify-center"
106 />
107 ) : (
108 <>
109 <div className="flex items-center justify-between">
110 <div className="flex items-center gap-x-2">
111 <Input
112 size="tiny"
113 className="grow lg:grow-0 w-52"
114 placeholder="Search for a bucket"
115 value={filterString}
116 onChange={(e) => setFilterString(e.target.value)}
117 icon={<Search />}
118 />
119 <DropdownMenu>
120 <DropdownMenuTrigger asChild>
121 <Button type="default" icon={<ArrowDownNarrowWide />}>
122 Sorted by {sortBucket === 'alphabetical' ? 'name' : 'created at'}
123 </Button>
124 </DropdownMenuTrigger>
125 <DropdownMenuContent align="start" className="w-40">
126 <DropdownMenuRadioGroup
127 value={sortBucket}
128 onValueChange={(value) => setSortBucket(value as STORAGE_BUCKET_SORT)}
129 >
130 <DropdownMenuRadioItem value="alphabetical">
131 Sort by name
132 </DropdownMenuRadioItem>
133 <DropdownMenuRadioItem value="created_at">
134 Sort by created at
135 </DropdownMenuRadioItem>
136 </DropdownMenuRadioGroup>
137 </DropdownMenuContent>
138 </DropdownMenu>
139 </div>
140 <CreateBucketButton onClick={() => showCreateBucket(true)} />
141 </div>
142
143 <Card className="min-h-0 flex-1 overflow-hidden">
144 <BucketsTable
145 buckets={fileBuckets}
146 projectRef={projectRef ?? '_'}
147 filterString={filterString}
148 formattedGlobalUploadLimit={formattedGlobalUploadLimit}
149 onSelectBucket={onSelectBucket}
150 allowedBucketType={allowedBucketType}
151 pagination={{
152 hasMore: hasNextPage,
153 isLoadingMore: isFetchingBuckets,
154 onLoadMore: handleLoadMoreBuckets,
155 }}
156 />
157 </Card>
158 </>
159 )}
160 </>
161 )}
162 <CreateBucketModal open={createBucketShown} onOpenChange={showCreateBucket} />
163 </div>
164 )
165}