BucketsPicker.tsx 6.5 KB

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