StoragePoliciesBucketsSection.tsx 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. import { useVirtualizer } from '@tanstack/react-virtual'
  2. import { ChevronUp, Search, X } from 'lucide-react'
  3. import { forwardRef, useEffect, useState, type HTMLAttributes, type ReactNode } from 'react'
  4. import { Button, cn, Collapsible, CollapsibleContent, CollapsibleTrigger } from 'ui'
  5. import { ShimmeringLoader } from 'ui-patterns'
  6. import { Input } from 'ui-patterns/DataInputs/Input'
  7. import {
  8. PageSection,
  9. PageSectionContent,
  10. PageSectionDescription,
  11. PageSectionMeta,
  12. PageSectionSummary,
  13. PageSectionTitle,
  14. } from 'ui-patterns/PageSection'
  15. import { StoragePoliciesBucketRow } from './StoragePoliciesBucketRow'
  16. import StoragePoliciesPlaceholder from './StoragePoliciesPlaceholder'
  17. import type { Policy } from '@/components/interfaces/Auth/Policies/PolicyTableRow/PolicyTableRow.utils'
  18. import { useMainScrollContainer } from '@/components/layouts/MainScrollContainerContext'
  19. import { NoSearchResults } from '@/components/ui/NoSearchResults'
  20. import { type Bucket } from '@/data/storage/buckets-query'
  21. import { useStaticEffectEvent } from '@/hooks/useStaticEffectEvent'
  22. export type SelectBucketPolicyForAction = {
  23. addPolicy: (bucketName?: string, table?: string) => void
  24. editPolicy: (policy: Policy, bucketName?: string, table?: string) => void
  25. deletePolicy: (policy: Policy) => void
  26. }
  27. type BucketsPoliciesProps = {
  28. buckets: { bucket: Bucket; policies: Policy[] }[]
  29. search?: string
  30. debouncedSearch?: string
  31. setSearch: (search: string) => void
  32. actions: SelectBucketPolicyForAction
  33. pagination: {
  34. hasNextPage: boolean
  35. isFetchingNextPage: boolean
  36. fetchNextPage: () => void
  37. }
  38. }
  39. export const BucketsPolicies = ({
  40. buckets,
  41. search,
  42. debouncedSearch,
  43. setSearch,
  44. actions,
  45. pagination,
  46. }: BucketsPoliciesProps): ReactNode => {
  47. const [expanded, setExpanded] = useState(true)
  48. const showEmptyState = buckets.length === 0 && (!debouncedSearch || debouncedSearch.length === 0)
  49. return (
  50. <PageSection>
  51. <Collapsible open={expanded} onOpenChange={setExpanded}>
  52. <PageSectionMeta>
  53. <PageSectionSummary>
  54. <PageSectionTitle>Buckets</PageSectionTitle>
  55. <PageSectionDescription>
  56. Write policies for each bucket to control access to the bucket and its contents
  57. </PageSectionDescription>
  58. </PageSectionSummary>
  59. <CollapsibleTrigger asChild>
  60. <button>
  61. <span className="sr-only">Toggle bucket list</span>
  62. <ChevronUp
  63. size={14}
  64. className={cn(
  65. !expanded && 'rotate-180',
  66. 'transition',
  67. 'text-foreground-light hover:text-foreground'
  68. )}
  69. />
  70. </button>
  71. </CollapsibleTrigger>
  72. </PageSectionMeta>
  73. <CollapsibleContent>
  74. <PageSectionContent className="mt-6">
  75. {showEmptyState && <StoragePoliciesPlaceholder />}
  76. {buckets.length > 0 && (
  77. <div className="mb-4">
  78. <Input
  79. size="tiny"
  80. placeholder="Filter buckets"
  81. className="block"
  82. containerClassName="w-full lg:w-52"
  83. value={search || ''}
  84. onChange={(e) => {
  85. const str = e.target.value
  86. setSearch(str)
  87. }}
  88. icon={<Search />}
  89. actions={
  90. search ? (
  91. <Button
  92. size="tiny"
  93. type="text"
  94. className="p-0 h-5 w-5"
  95. icon={<X />}
  96. onClick={() => setSearch('')}
  97. />
  98. ) : null
  99. }
  100. />
  101. </div>
  102. )}
  103. {!!search && search.length > 0 && buckets.length === 0 && (
  104. <NoSearchResults searchString={search} onResetFilter={() => setSearch('')} />
  105. )}
  106. <BucketsPoliciesVirtualizedList
  107. items={buckets}
  108. actions={actions}
  109. pagination={pagination}
  110. />
  111. </PageSectionContent>
  112. </CollapsibleContent>
  113. </Collapsible>
  114. </PageSection>
  115. )
  116. }
  117. type BucketsPoliciesVirtualizedListProps = {
  118. items: { bucket: Bucket; policies: Policy[] }[]
  119. actions: SelectBucketPolicyForAction
  120. pagination: BucketsPoliciesProps['pagination']
  121. }
  122. const BucketsPoliciesVirtualizedList = ({
  123. items,
  124. actions,
  125. pagination,
  126. }: BucketsPoliciesVirtualizedListProps) => {
  127. const { hasNextPage, isFetchingNextPage, fetchNextPage } = pagination
  128. const itemCount = hasNextPage ? items.length + 1 : items.length
  129. const scrollElement = useMainScrollContainer()
  130. const virtualizer = useVirtualizer({
  131. count: itemCount,
  132. estimateSize: () => 129,
  133. overscan: 5,
  134. getItemKey: (index) => items[index]?.bucket.name ?? `bucket-${index}`,
  135. getScrollElement: () => scrollElement,
  136. })
  137. const virtualItems = virtualizer.getVirtualItems()
  138. const lastItem = virtualItems[virtualItems.length - 1]
  139. const fetchNext = useStaticEffectEvent(() => {
  140. if (lastItem && lastItem.index >= items.length - 1 && hasNextPage && !isFetchingNextPage) {
  141. fetchNextPage()
  142. }
  143. })
  144. useEffect(fetchNext, [lastItem, fetchNext])
  145. return (
  146. <div
  147. style={{
  148. height: `${virtualizer.getTotalSize()}px`,
  149. width: '100%',
  150. position: 'relative',
  151. }}
  152. >
  153. {virtualItems.map((virtualRow) => {
  154. const isLoaderRow = virtualRow.index > items.length - 1
  155. const commonStyle = {
  156. position: 'absolute' as const,
  157. top: 0,
  158. left: 0,
  159. width: '100%',
  160. transform: `translateY(${virtualRow.start}px)`,
  161. }
  162. if (isLoaderRow) {
  163. return (
  164. <BucketsPoliciesLoader
  165. key={`loader-${virtualRow.index}`}
  166. data-index={virtualRow.index}
  167. ref={virtualizer.measureElement}
  168. className="pb-4"
  169. style={commonStyle}
  170. />
  171. )
  172. }
  173. const item = items[virtualRow.index]
  174. if (!item) return null
  175. return (
  176. <div
  177. key={virtualRow.key}
  178. data-index={virtualRow.index}
  179. ref={virtualizer.measureElement}
  180. className="pb-4"
  181. style={commonStyle}
  182. >
  183. <StoragePoliciesBucketRow
  184. table="objects"
  185. label={item.bucket.name}
  186. bucket={item.bucket}
  187. policies={item.policies}
  188. onSelectPolicyAdd={actions.addPolicy}
  189. onSelectPolicyEdit={actions.editPolicy}
  190. onSelectPolicyDelete={actions.deletePolicy}
  191. />
  192. </div>
  193. )
  194. })}
  195. </div>
  196. )
  197. }
  198. type BucketsPoliciesLoaderProps = HTMLAttributes<HTMLDivElement>
  199. const BucketsPoliciesLoader = forwardRef<HTMLDivElement, BucketsPoliciesLoaderProps>(
  200. (props: BucketsPoliciesLoaderProps, ref) => (
  201. <div ref={ref} {...props}>
  202. <p className="sr-only">Loading more...</p>
  203. <ShimmeringLoader />
  204. </div>
  205. )
  206. )
  207. BucketsPoliciesLoader.displayName = 'BucketsPoliciesLoader'