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