[bucketId].tsx191 lines · main
1import { useParams } from 'common'
2import { ChevronDown, FolderOpen, Settings, Shield, Trash2 } from 'lucide-react'
3import Link from 'next/link'
4import { useRouter } from 'next/router'
5import { parseAsBoolean, useQueryState } from 'nuqs'
6import { useEffect } from 'react'
7import { toast } from 'sonner'
8import {
9 Badge,
10 Button,
11 DropdownMenu,
12 DropdownMenuContent,
13 DropdownMenuItem,
14 DropdownMenuSeparator,
15 DropdownMenuTrigger,
16 Tooltip,
17 TooltipContent,
18 TooltipTrigger,
19} from 'ui'
20
21import { DeleteBucketModal } from '@/components/interfaces/Storage/DeleteBucketModal'
22import { EditBucketModal } from '@/components/interfaces/Storage/EditBucketModal'
23import { EmptyBucketModal } from '@/components/interfaces/Storage/EmptyBucketModal'
24import { useSelectedBucket } from '@/components/interfaces/Storage/FilesBuckets/useSelectedBucket'
25import { PublicBucketWarning } from '@/components/interfaces/Storage/PublicBucketWarning'
26import { PUBLIC_BUCKET_TOOLTIP } from '@/components/interfaces/Storage/Storage.constants'
27import StorageBucketsError from '@/components/interfaces/Storage/StorageBucketsError'
28import { StorageExplorer } from '@/components/interfaces/Storage/StorageExplorer/StorageExplorer'
29import { useBucketPolicyCount } from '@/components/interfaces/Storage/useBucketPolicyCount'
30import DefaultLayout from '@/components/layouts/DefaultLayout'
31import { PageLayout } from '@/components/layouts/PageLayout/PageLayout'
32import StorageLayout from '@/components/layouts/StorageLayout/StorageLayout'
33import { StorageExplorerStateContextProvider } from '@/state/storage-explorer'
34import type { NextPageWithLayout } from '@/types'
35
36const BucketPage: NextPageWithLayout = () => {
37 const router = useRouter()
38 const { bucketId, ref } = useParams()
39 const { data: bucket, error, isSuccess, isError } = useSelectedBucket()
40
41 const [showEditModal, setShowEditModal] = useQueryState(
42 'edit',
43 parseAsBoolean.withDefault(false).withOptions({ history: 'push', clearOnDefault: true })
44 )
45 const [showEmptyModal, setShowEmptyModal] = useQueryState(
46 'empty',
47 parseAsBoolean.withDefault(false).withOptions({ history: 'push', clearOnDefault: true })
48 )
49 const [showDeleteModal, setShowDeleteModal] = useQueryState(
50 'delete',
51 parseAsBoolean.withDefault(false).withOptions({ history: 'push', clearOnDefault: true })
52 )
53
54 const { getPolicyCount } = useBucketPolicyCount()
55 const policyCount = bucket ? getPolicyCount(bucket.id) : 0
56
57 useEffect(() => {
58 if (isSuccess && !bucket) {
59 toast.info(`Bucket "${bucketId}" does not exist in your project`)
60 router.push(`/project/${ref}/storage/files`)
61 }
62 // eslint-disable-next-line react-hooks/exhaustive-deps
63 }, [isSuccess])
64
65 if (isError) {
66 return <StorageBucketsError error={error} />
67 }
68
69 return (
70 <StorageExplorerStateContextProvider key={`storage-explorer-state-${ref}`}>
71 <PageLayout
72 size="full"
73 isCompact
74 className="[&>div:first-child]:border-b-0!" // Override the border-b from ScaffoldContainer
75 title={
76 <div className="flex items-center gap-2 min-w-0 flex-1">
77 <span className="truncate">{bucketId}</span>
78 {bucket?.public && (
79 <Tooltip>
80 <TooltipTrigger asChild>
81 <Badge variant="warning" className="flex shrink-0">
82 Public
83 </Badge>
84 </TooltipTrigger>
85 <TooltipContent side="bottom">{PUBLIC_BUCKET_TOOLTIP}</TooltipContent>
86 </Tooltip>
87 )}
88 </div>
89 }
90 breadcrumbs={[
91 {
92 label: 'Files',
93 href: `/project/${ref}/storage/files`,
94 },
95 {
96 label: 'Buckets',
97 href: `/project/${ref}/storage/files`,
98 },
99 ]}
100 primaryActions={
101 <>
102 <Button
103 asChild
104 type="default"
105 icon={<Shield size={14} />}
106 iconRight={
107 policyCount > 0 ? (
108 <span className="w-4 h-4 bg-surface-200 text-foreground-light text-xs rounded-full flex items-center justify-center font-medium">
109 {policyCount}
110 </span>
111 ) : undefined
112 }
113 >
114 <Link
115 href={`/project/${ref}/storage/files/policies?search=${encodeURIComponent(bucket?.name ?? '')}`}
116 >
117 Policies
118 </Link>
119 </Button>
120 <DropdownMenu>
121 <DropdownMenuTrigger asChild>
122 <Button type="default" iconRight={<ChevronDown size={14} />}>
123 Edit bucket
124 </Button>
125 </DropdownMenuTrigger>
126 <DropdownMenuContent align="end" className="w-40">
127 <DropdownMenuItem
128 className="flex items-center space-x-2"
129 onClick={() => setShowEditModal(true)}
130 >
131 <Settings size={12} />
132 <p>Bucket settings</p>
133 </DropdownMenuItem>
134 <DropdownMenuSeparator />
135 <DropdownMenuItem
136 className="flex items-center space-x-2"
137 onClick={() => setShowEmptyModal(true)}
138 >
139 <FolderOpen size={12} />
140 <p>Empty bucket</p>
141 </DropdownMenuItem>
142 <DropdownMenuItem
143 className="flex items-center space-x-2"
144 onClick={() => setShowDeleteModal(true)}
145 >
146 <Trash2 size={12} />
147 <p>Delete bucket</p>
148 </DropdownMenuItem>
149 </DropdownMenuContent>
150 </DropdownMenu>
151 </>
152 }
153 >
154 <div className="flex-1 min-h-0 px-6 pb-6 flex flex-col gap-4">
155 {ref && bucketId && <PublicBucketWarning projectRef={ref} bucketId={bucketId} />}
156 <div className="flex-1 min-h-0">
157 <StorageExplorer />
158 </div>
159 </div>
160 </PageLayout>
161
162 {bucket && (
163 <>
164 <EditBucketModal
165 visible={showEditModal}
166 bucket={bucket}
167 onClose={() => setShowEditModal(false)}
168 />
169 <EmptyBucketModal
170 visible={showEmptyModal}
171 bucket={bucket}
172 onClose={() => setShowEmptyModal(false)}
173 />
174 <DeleteBucketModal
175 visible={showDeleteModal}
176 bucket={bucket}
177 onClose={() => setShowDeleteModal(false)}
178 />
179 </>
180 )}
181 </StorageExplorerStateContextProvider>
182 )
183}
184
185BucketPage.getLayout = (page) => (
186 <DefaultLayout>
187 <StorageLayout title="Buckets">{page}</StorageLayout>
188 </DefaultLayout>
189)
190
191export default BucketPage