BucketTable.tsx167 lines · main
1import { FilesBucket as FilesBucketIcon } from 'icons'
2import { ChevronRight } from 'lucide-react'
3import { useRouter } from 'next/navigation'
4import {
5 Badge,
6 TableCell,
7 TableHead,
8 TableHeader,
9 TableRow,
10 Tooltip,
11 TooltipContent,
12 TooltipTrigger,
13} from 'ui'
14
15import { PUBLIC_BUCKET_TOOLTIP } from '@/components/interfaces/Storage/Storage.constants'
16import { useBucketPolicyCount } from '@/components/interfaces/Storage/useBucketPolicyCount'
17import {
18 VirtualizedTableCell,
19 VirtualizedTableHead,
20 VirtualizedTableHeader,
21 VirtualizedTableRow,
22} from '@/components/ui/VirtualizedTable'
23import { Bucket } from '@/data/storage/buckets-query'
24import { formatBytes } from '@/lib/helpers'
25import { createNavigationHandler } from '@/lib/navigation'
26
27type BucketTableMode = 'standard' | 'virtualized'
28
29type BucketTableHeaderProps = {
30 mode: BucketTableMode
31 hasBuckets?: boolean
32}
33
34export const BucketTableHeader = ({ mode, hasBuckets = true }: BucketTableHeaderProps) => {
35 const BucketTableHeader = mode === 'standard' ? TableHeader : VirtualizedTableHeader
36 const BucketTableRow = mode === 'standard' ? TableRow : VirtualizedTableRow
37 const BucketTableHead = mode === 'standard' ? TableHead : VirtualizedTableHead
38
39 const stickyClasses = 'sticky top-0 z-10 bg-200'
40
41 return (
42 <BucketTableHeader>
43 <BucketTableRow>
44 {hasBuckets && (
45 <BucketTableHead className={`${stickyClasses} w-2 pr-1`}>
46 <span className="sr-only">Icon</span>
47 </BucketTableHead>
48 )}
49 <BucketTableHead className={stickyClasses}>Name</BucketTableHead>
50 <BucketTableHead className={stickyClasses}>Policies</BucketTableHead>
51 <BucketTableHead className={stickyClasses}>File size limit</BucketTableHead>
52 <BucketTableHead className={stickyClasses}>Allowed MIME types</BucketTableHead>
53 <BucketTableHead className={stickyClasses}>
54 <span className="sr-only">Actions</span>
55 </BucketTableHead>
56 </BucketTableRow>
57 </BucketTableHeader>
58 )
59}
60
61type BucketTableEmptyStateProps = {
62 mode: BucketTableMode
63 filterString: string
64}
65
66export const BucketTableEmptyState = ({ mode, filterString }: BucketTableEmptyStateProps) => {
67 const BucketTableRow = mode === 'standard' ? TableRow : VirtualizedTableRow
68 const BucketTableCell = mode === 'standard' ? TableCell : VirtualizedTableCell
69
70 return (
71 <BucketTableRow className="[&>td]:hover:bg-inherit">
72 <BucketTableCell colSpan={5}>
73 <p className="text-sm text-foreground">No results found</p>
74 <p className="text-sm text-foreground-lighter">
75 Your search for “{filterString}” did not return any results
76 </p>
77 </BucketTableCell>
78 </BucketTableRow>
79 )
80}
81
82type BucketTableRowProps = {
83 mode: BucketTableMode
84 bucket: Bucket
85 projectRef: string
86 formattedGlobalUploadLimit: string
87}
88
89export const BucketTableRow = ({
90 mode,
91 bucket,
92 projectRef,
93 formattedGlobalUploadLimit,
94}: BucketTableRowProps) => {
95 const router = useRouter()
96 const { getPolicyCount } = useBucketPolicyCount()
97
98 const BucketTableRow = mode === 'standard' ? TableRow : VirtualizedTableRow
99 const BucketTableCell = mode === 'standard' ? TableCell : VirtualizedTableCell
100
101 const handleBucketNavigation = createNavigationHandler(
102 `/project/${projectRef}/storage/files/buckets/${encodeURIComponent(bucket.id)}`,
103 router
104 )
105
106 return (
107 <BucketTableRow
108 key={bucket.id}
109 data-bucket-id={bucket.id}
110 className="relative cursor-pointer h-16 group inset-focus"
111 onClick={handleBucketNavigation}
112 onAuxClick={handleBucketNavigation}
113 onKeyDown={handleBucketNavigation}
114 tabIndex={0}
115 >
116 <BucketTableCell className="w-2 pr-1">
117 <FilesBucketIcon aria-label="bucket icon" size={16} className="text-foreground-muted" />
118 </BucketTableCell>
119 <BucketTableCell className="flex-1">
120 <div className="flex items-center gap-2.5">
121 <p className="whitespace-nowrap max-w-[512px] truncate">{bucket.id}</p>
122 {bucket.public && (
123 <Tooltip>
124 <TooltipTrigger asChild>
125 <Badge variant="warning" className="flex">
126 Public
127 </Badge>
128 </TooltipTrigger>
129 <TooltipContent side="top">{PUBLIC_BUCKET_TOOLTIP}</TooltipContent>
130 </Tooltip>
131 )}
132 </div>
133 </BucketTableCell>
134
135 <BucketTableCell>
136 <p className="text-foreground-light">{getPolicyCount(bucket.id)}</p>
137 </BucketTableCell>
138
139 <BucketTableCell>
140 <p
141 className={`whitespace-nowrap ${bucket.file_size_limit ? 'text-foreground-light' : 'text-foreground-muted'}`}
142 >
143 {bucket.file_size_limit
144 ? formatBytes(bucket.file_size_limit)
145 : `Unset (${formattedGlobalUploadLimit})`}
146 </p>
147 </BucketTableCell>
148
149 <BucketTableCell>
150 <p
151 className={bucket.allowed_mime_types ? 'text-foreground-light' : 'text-foreground-muted'}
152 >
153 {bucket.allowed_mime_types ? bucket.allowed_mime_types.join(', ') : 'Any'}
154 </p>
155 </BucketTableCell>
156
157 <BucketTableCell>
158 <div className="flex justify-end items-center h-full">
159 <ChevronRight aria-hidden={true} size={14} className="text-foreground-muted/60" />
160 </div>
161 <button tabIndex={-1} className="sr-only">
162 Go to bucket details
163 </button>
164 </BucketTableCell>
165 </BucketTableRow>
166 )
167}