| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228 |
- import { useVirtualizer } from '@tanstack/react-virtual'
- import { ChevronUp, Search, X } from 'lucide-react'
- import { forwardRef, useEffect, useState, type HTMLAttributes, type ReactNode } from 'react'
- import { Button, cn, Collapsible, CollapsibleContent, CollapsibleTrigger } from 'ui'
- import { ShimmeringLoader } from 'ui-patterns'
- import { Input } from 'ui-patterns/DataInputs/Input'
- import {
- PageSection,
- PageSectionContent,
- PageSectionDescription,
- PageSectionMeta,
- PageSectionSummary,
- PageSectionTitle,
- } from 'ui-patterns/PageSection'
- import { StoragePoliciesBucketRow } from './StoragePoliciesBucketRow'
- import StoragePoliciesPlaceholder from './StoragePoliciesPlaceholder'
- import type { Policy } from '@/components/interfaces/Auth/Policies/PolicyTableRow/PolicyTableRow.utils'
- import { useMainScrollContainer } from '@/components/layouts/MainScrollContainerContext'
- import { NoSearchResults } from '@/components/ui/NoSearchResults'
- import { type Bucket } from '@/data/storage/buckets-query'
- import { useStaticEffectEvent } from '@/hooks/useStaticEffectEvent'
- export type SelectBucketPolicyForAction = {
- addPolicy: (bucketName?: string, table?: string) => void
- editPolicy: (policy: Policy, bucketName?: string, table?: string) => void
- deletePolicy: (policy: Policy) => void
- }
- type BucketsPoliciesProps = {
- buckets: { bucket: Bucket; policies: Policy[] }[]
- search?: string
- debouncedSearch?: string
- setSearch: (search: string) => void
- actions: SelectBucketPolicyForAction
- pagination: {
- hasNextPage: boolean
- isFetchingNextPage: boolean
- fetchNextPage: () => void
- }
- }
- export const BucketsPolicies = ({
- buckets,
- search,
- debouncedSearch,
- setSearch,
- actions,
- pagination,
- }: BucketsPoliciesProps): ReactNode => {
- const [expanded, setExpanded] = useState(true)
- const showEmptyState = buckets.length === 0 && (!debouncedSearch || debouncedSearch.length === 0)
- return (
- <PageSection>
- <Collapsible open={expanded} onOpenChange={setExpanded}>
- <PageSectionMeta>
- <PageSectionSummary>
- <PageSectionTitle>Buckets</PageSectionTitle>
- <PageSectionDescription>
- Write policies for each bucket to control access to the bucket and its contents
- </PageSectionDescription>
- </PageSectionSummary>
- <CollapsibleTrigger asChild>
- <button>
- <span className="sr-only">Toggle bucket list</span>
- <ChevronUp
- size={14}
- className={cn(
- !expanded && 'rotate-180',
- 'transition',
- 'text-foreground-light hover:text-foreground'
- )}
- />
- </button>
- </CollapsibleTrigger>
- </PageSectionMeta>
- <CollapsibleContent>
- <PageSectionContent className="mt-6">
- {showEmptyState && <StoragePoliciesPlaceholder />}
- {buckets.length > 0 && (
- <div className="mb-4">
- <Input
- size="tiny"
- placeholder="Filter buckets"
- className="block"
- containerClassName="w-full lg:w-52"
- value={search || ''}
- onChange={(e) => {
- const str = e.target.value
- setSearch(str)
- }}
- icon={<Search />}
- actions={
- search ? (
- <Button
- size="tiny"
- type="text"
- className="p-0 h-5 w-5"
- icon={<X />}
- onClick={() => setSearch('')}
- />
- ) : null
- }
- />
- </div>
- )}
- {!!search && search.length > 0 && buckets.length === 0 && (
- <NoSearchResults searchString={search} onResetFilter={() => setSearch('')} />
- )}
- <BucketsPoliciesVirtualizedList
- items={buckets}
- actions={actions}
- pagination={pagination}
- />
- </PageSectionContent>
- </CollapsibleContent>
- </Collapsible>
- </PageSection>
- )
- }
- type BucketsPoliciesVirtualizedListProps = {
- items: { bucket: Bucket; policies: Policy[] }[]
- actions: SelectBucketPolicyForAction
- pagination: BucketsPoliciesProps['pagination']
- }
- const BucketsPoliciesVirtualizedList = ({
- items,
- actions,
- pagination,
- }: BucketsPoliciesVirtualizedListProps) => {
- const { hasNextPage, isFetchingNextPage, fetchNextPage } = pagination
- const itemCount = hasNextPage ? items.length + 1 : items.length
- const scrollElement = useMainScrollContainer()
- const virtualizer = useVirtualizer({
- count: itemCount,
- estimateSize: () => 129,
- overscan: 5,
- getItemKey: (index) => items[index]?.bucket.name ?? `bucket-${index}`,
- getScrollElement: () => scrollElement,
- })
- const virtualItems = virtualizer.getVirtualItems()
- const lastItem = virtualItems[virtualItems.length - 1]
- const fetchNext = useStaticEffectEvent(() => {
- if (lastItem && lastItem.index >= items.length - 1 && hasNextPage && !isFetchingNextPage) {
- fetchNextPage()
- }
- })
- useEffect(fetchNext, [lastItem, fetchNext])
- return (
- <div
- style={{
- height: `${virtualizer.getTotalSize()}px`,
- width: '100%',
- position: 'relative',
- }}
- >
- {virtualItems.map((virtualRow) => {
- const isLoaderRow = virtualRow.index > items.length - 1
- const commonStyle = {
- position: 'absolute' as const,
- top: 0,
- left: 0,
- width: '100%',
- transform: `translateY(${virtualRow.start}px)`,
- }
- if (isLoaderRow) {
- return (
- <BucketsPoliciesLoader
- key={`loader-${virtualRow.index}`}
- data-index={virtualRow.index}
- ref={virtualizer.measureElement}
- className="pb-4"
- style={commonStyle}
- />
- )
- }
- const item = items[virtualRow.index]
- if (!item) return null
- return (
- <div
- key={virtualRow.key}
- data-index={virtualRow.index}
- ref={virtualizer.measureElement}
- className="pb-4"
- style={commonStyle}
- >
- <StoragePoliciesBucketRow
- table="objects"
- label={item.bucket.name}
- bucket={item.bucket}
- policies={item.policies}
- onSelectPolicyAdd={actions.addPolicy}
- onSelectPolicyEdit={actions.editPolicy}
- onSelectPolicyDelete={actions.deletePolicy}
- />
- </div>
- )
- })}
- </div>
- )
- }
- type BucketsPoliciesLoaderProps = HTMLAttributes<HTMLDivElement>
- const BucketsPoliciesLoader = forwardRef<HTMLDivElement, BucketsPoliciesLoaderProps>(
- (props: BucketsPoliciesLoaderProps, ref) => (
- <div ref={ref} {...props}>
- <p className="sr-only">Loading more...</p>
- <ShimmeringLoader />
- </div>
- )
- )
- BucketsPoliciesLoader.displayName = 'BucketsPoliciesLoader'
|