feat(studio): list existing analytics buckets (#39500)

* specialized dialog for analytics (and vector) bucket creation

* handle missing wrapper extension and iceberg wrapper integration

* bucket creation improvements

* extension and integration checks

* dialog header consistency

* simplify bucket dialog creation button trigger

* fix subtitle ternary

* example bucket without dynamic route

* create dynamic segment

* fancy connection table

* improve bucket page

* connection table polish

* polish

* delete bucket modal

* improve missing installations design

* new table modal

* fix copy icon

* copy all env button placeholder

* list all analytics buckets

* specialised bucket modal

* shared installer hooks and components

* make iceberg wrapper not dependent on wrapper extension

There are cases where the Iceberg Wrapper can be installed but the Wrapper extension has since been removed

* better admonition copywriting

* todo

* remove later features

* more removal of later work

* remove old comment

* remove unused content

* Nits and hook up create analytics bucket logic

* Nit

---------

Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
This commit is contained in:
Danny WhiteandJoshen Lim authored and GitHub committed 2025-10-15 11:48:14 +11:00
1 parent 6617418e1a
commit bf3f26d0a6
15 files changed
+560 -44

No files matched your search

@@ -80,7 +80,7 @@ export const AnalyticBucketDetails = ({ bucket }: { bucket: Bucket }) => {
.find((w) => w.name === snakeCase(`${bucket.name}_fdw`))
}, [data, bucket.name])
const extensionState = useIcebergWrapperExtension()
const { state: extensionState } = useIcebergWrapperExtension()
const integration = INTEGRATIONS.find((i) => i.id === 'iceberg_wrapper' && i.type === 'wrapper')
@@ -13,7 +13,7 @@ export const useIcebergWrapperExtension = () => {
if (!integration || integration.type !== 'wrapper') {
// This should never happen
return 'not-found'
return { extension: undefined, state: 'not-found' }
}
const wrapperMeta = integration.meta
@@ -23,13 +23,13 @@ export const useIcebergWrapperExtension = () => {
const hasRequiredVersion =
(wrappersExtension?.installed_version ?? '') >= (wrapperMeta?.minimumExtensionVersion ?? '')
const state = isExtensionsLoading
const state: 'loading' | 'installed' | 'needs-upgrade' | 'not-installed' = isExtensionsLoading
? 'loading'
: isWrappersExtensionInstalled
? hasRequiredVersion
? 'installed'
: 'needs-upgrade'
: ('not-installed' as const)
: 'not-installed'
return state
return { extension: wrappersExtension, state }
}
@@ -1,6 +1,175 @@
import { MoreVertical, Search, Trash2 } from 'lucide-react'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { useState } from 'react'
import { useParams } from 'common'
import { ScaffoldHeader, ScaffoldSection, ScaffoldSectionTitle } from 'components/layouts/Scaffold'
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
import { Bucket, useBucketsQuery } from 'data/storage/buckets-query'
import {
Button,
Card,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from 'ui'
import { TimestampInfo } from 'ui-patterns'
import { Input } from 'ui-patterns/DataInputs/Input'
import { CreateSpecializedBucketModal } from './CreateSpecializedBucketModal'
import { DeleteBucketModal } from './DeleteBucketModal'
import { EmptyBucketState } from './EmptyBucketState'
export const AnalyticsBuckets = () => {
// Placeholder component - will be implemented in a later PR
return <EmptyBucketState bucketType="analytics" />
const router = useRouter()
const { ref } = useParams()
const [modal, setModal] = useState<'edit' | 'empty' | 'delete' | null>(null)
const [selectedBucket, setSelectedBucket] = useState<Bucket>()
const [filterString, setFilterString] = useState('')
const { data: buckets = [], isLoading: isLoadingBuckets } = useBucketsQuery({ projectRef: ref })
const analyticsBuckets = buckets
.filter((bucket) => !('type' in bucket) || bucket.type === 'ANALYTICS')
.filter((bucket) =>
filterString.length === 0
? true
: bucket.name.toLowerCase().includes(filterString.toLowerCase())
)
return (
<>
{!isLoadingBuckets &&
buckets.filter((bucket) => !('type' in bucket) || bucket.type === 'ANALYTICS').length ===
0 ? (
<EmptyBucketState bucketType="analytics" />
) : (
// Override the default first:pt-12 to match other storage types
<ScaffoldSection isFullWidth className="gap-y-4 first:pt-8">
<ScaffoldHeader className="py-0">
<ScaffoldSectionTitle>Buckets</ScaffoldSectionTitle>
</ScaffoldHeader>
<div className="flex flex-grow justify-between gap-x-2 items-center">
<Input
size="tiny"
className="flex-grow lg:flex-grow-0 w-52"
placeholder="Search for a bucket"
value={filterString}
onChange={(e) => setFilterString(e.target.value)}
icon={<Search size={12} />}
/>
<CreateSpecializedBucketModal
buttonType="primary"
buttonClassName="w-fit"
bucketType="analytics"
/>
</div>
{isLoadingBuckets ? (
<GenericSkeletonLoader />
) : (
<Card>
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Created at</TableHead>
<TableHead />
</TableRow>
</TableHeader>
<TableBody>
{analyticsBuckets.length === 0 && filterString.length > 0 && (
<TableRow>
<TableCell colSpan={3}>
<p className="text-sm text-foreground">No results found</p>
<p className="text-sm text-foreground-light">
Your search for "{filterString}" did not return any results
</p>
</TableCell>
</TableRow>
)}
{analyticsBuckets.map((bucket) => (
<TableRow
key={bucket.id}
className="cursor-pointer"
onClick={(event) => {
const url = `/project/${ref}/storage/analytics/buckets/${bucket.id}`
if (event.metaKey) window.open(url, '_blank')
else router.push(url)
}}
>
<TableCell>
<p className="text-foreground">{bucket.name}</p>
</TableCell>
<TableCell>
<p className="text-foreground-light">
<TimestampInfo
utcTimestamp={bucket.created_at}
className="text-sm text-foreground-light"
/>
</p>
</TableCell>
<TableCell>
<div className="flex justify-end gap-2">
<Button asChild type="default">
<Link
href={`/project/${ref}/storage/analytics/buckets/${encodeURIComponent(bucket.id)}`}
onClick={(e) => e.stopPropagation()}
>
View contents
</Link>
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
type="default"
className="px-1"
icon={<MoreVertical />}
onClick={(e) => e.stopPropagation()}
/>
</DropdownMenuTrigger>
<DropdownMenuContent side="bottom" align="end" className="w-40">
<DropdownMenuItem
className="flex items-center space-x-2"
onClick={(e) => {
e.stopPropagation()
setModal('delete')
setSelectedBucket(bucket)
}}
>
<Trash2 size={12} />
<p>Delete bucket</p>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Card>
)}
</ScaffoldSection>
)}
{selectedBucket && (
<DeleteBucketModal
visible={modal === `delete`}
bucket={selectedBucket}
onClose={() => setModal(null)}
/>
)}
</>
)
}
@@ -53,6 +53,7 @@ import { Admonition } from 'ui-patterns/admonition'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { useIsNewStorageUIEnabled } from '../App/FeaturePreview/FeaturePreviewContext'
import { inverseValidBucketNameRegex, validBucketNameRegex } from './CreateBucketModal.utils'
import { BUCKET_TYPES } from './Storage.constants'
import { convertFromBytes, convertToBytes } from './StorageSettings/StorageSettings.utils'
const FormSchema = z
@@ -131,6 +132,8 @@ export const CreateBucketModal = ({
const { value, unit } = convertFromBytes(data?.fileSizeLimit ?? 0)
const formattedGlobalUploadLimit = `${value} ${unit}`
const config = BUCKET_TYPES['files']
const form = useForm<CreateBucketForm>({
resolver: zodResolver(FormSchema),
defaultValues: {
@@ -149,7 +152,7 @@ export const CreateBucketModal = ({
const isStandardBucket = form.watch('type') === 'STANDARD'
const hasFileSizeLimit = form.watch('has_file_size_limit')
const [hasAllowedMimeTypes, setHasAllowedMimeTypes] = useState(false)
const icebergWrapperExtensionState = useIcebergWrapperExtension()
const { state: icebergWrapperExtensionState } = useIcebergWrapperExtension()
const icebergCatalogEnabled = data?.features?.icebergCatalog?.enabled
const onSubmit: SubmitHandler<CreateBucketForm> = async (values) => {
@@ -264,7 +267,7 @@ export const CreateBucketModal = ({
<DialogContent>
<DialogHeader>
<DialogTitle>Create storage bucket</DialogTitle>
<DialogTitle>Create a {isStorageV2 ? config.singularName : 'storage'} bucket</DialogTitle>
</DialogHeader>
<DialogSectionSeparator />
@@ -279,8 +282,8 @@ export const CreateBucketModal = ({
render={({ field }) => (
<FormItemLayout
name="name"
label="Name of bucket"
labelOptional="Buckets cannot be renamed once created."
label="Bucket name"
labelOptional="Cannot be changed after creation"
>
<FormControl_Shadcn_>
<Input_Shadcn_
@@ -0,0 +1,296 @@
import { zodResolver } from '@hookform/resolvers/zod'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { Plus } from 'lucide-react'
import { useRouter } from 'next/router'
import { useState } from 'react'
import { SubmitHandler, useForm } from 'react-hook-form'
import { toast } from 'sonner'
import z from 'zod'
import { useParams } from 'common'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { InlineLink } from 'components/ui/InlineLink'
import { useProjectStorageConfigQuery } from 'data/config/project-storage-config-query'
import { useDatabaseExtensionEnableMutation } from 'data/database-extensions/database-extension-enable-mutation'
import { useBucketCreateMutation } from 'data/storage/bucket-create-mutation'
import { useIcebergWrapperCreateMutation } from 'data/storage/iceberg-wrapper-create-mutation'
import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { DOCS_URL, IS_PLATFORM } from 'lib/constants'
import {
Button,
cn,
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogSection,
DialogSectionSeparator,
DialogTitle,
DialogTrigger,
Form_Shadcn_,
FormControl_Shadcn_,
FormField_Shadcn_,
Input_Shadcn_,
} from 'ui'
import { Admonition } from 'ui-patterns'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { useIcebergWrapperExtension } from './AnalyticBucketDetails/useIcebergWrapper'
import { inverseValidBucketNameRegex, validBucketNameRegex } from './CreateBucketModal.utils'
import { BUCKET_TYPES } from './Storage.constants'
const FormSchema = z
.object({
name: z
.string()
.trim()
.min(1, 'Please provide a name for your bucket')
.max(100, 'Bucket name should be below 100 characters')
.refine(
(value) => !value.endsWith(' '),
'The name of the bucket cannot end with a whitespace'
)
.refine(
(value) => value !== 'public',
'"public" is a reserved name. Please choose another name'
),
})
.superRefine((data, ctx) => {
if (!validBucketNameRegex.test(data.name)) {
const [match] = data.name.match(inverseValidBucketNameRegex) ?? []
ctx.addIssue({
path: ['name'],
code: z.ZodIssueCode.custom,
message: !!match
? `Bucket name cannot contain the "${match}" character`
: 'Bucket name contains an invalid special character',
})
}
})
const formId = 'create-specialized-storage-bucket-form'
export type CreateSpecializedBucketForm = z.infer<typeof FormSchema>
interface CreateSpecializedBucketModalProps {
bucketType: 'analytics' | 'vectors'
buttonSize?: 'tiny' | 'small'
buttonType?: 'default' | 'primary'
buttonClassName?: string
disabled?: boolean
tooltip?: {
content: {
side?: 'top' | 'bottom' | 'left' | 'right'
text?: string
}
}
}
export const CreateSpecializedBucketModal = ({
bucketType,
buttonSize = 'tiny',
buttonType = 'default',
buttonClassName,
disabled = false,
tooltip,
}: CreateSpecializedBucketModalProps) => {
const router = useRouter()
const { ref } = useParams()
const { data: org } = useSelectedOrganizationQuery()
const { data: project } = useSelectedProjectQuery()
const { can: canCreateBuckets } = useAsyncCheckPermissions(PermissionAction.STORAGE_WRITE, '*')
const { extension: wrappersExtension, state: wrappersExtensionState } =
useIcebergWrapperExtension()
const [visible, setVisible] = useState(false)
const { data } = useProjectStorageConfigQuery({ projectRef: ref }, { enabled: IS_PLATFORM })
const icebergCatalogEnabled = data?.features?.icebergCatalog?.enabled
const { mutate: sendEvent } = useSendEventMutation()
const { mutateAsync: createBucket, isLoading: isCreatingBucket } = useBucketCreateMutation({
// [Joshen] Silencing the error here as it's being handled in onSubmit
onError: () => {},
})
const { mutateAsync: createIcebergWrapper, isLoading: isCreatingIcebergWrapper } =
useIcebergWrapperCreateMutation()
const { mutateAsync: enableExtension, isLoading: isEnablingExtension } =
useDatabaseExtensionEnableMutation()
const config = BUCKET_TYPES['analytics']
const isCreating = isCreatingBucket || isEnablingExtension || isCreatingIcebergWrapper
const form = useForm<CreateSpecializedBucketForm>({
resolver: zodResolver(FormSchema),
defaultValues: {
name: '',
},
})
const onSubmit: SubmitHandler<CreateSpecializedBucketForm> = async (values) => {
if (!ref) return console.error('Project ref is required')
if (!project) return console.error('Project details is required')
if (!wrappersExtension) return console.error('Unable to find wrappers extension')
if (wrappersExtensionState === 'needs-upgrade') {
// [Joshen] Double check if this is the right CTA
return toast.error(
<p className="prose max-w-full text-sm">
Wrappers extensions needs to be updated to create an Iceberg Wrapper. Update the extension
by disabling and enabling the <code className="text-xs">wrappers</code> extension first in
the{' '}
<InlineLink href={`/project/${ref}/database/extensions?filter=wrappers`}>
database extensions page
</InlineLink>{' '}
before creating an Analytics bucket.
</p>
)
}
// Determine bucket type based on the bucketType prop
// [Danny] Change STANDARD to VECTORS when ready
const bucketTypeValue = bucketType === 'analytics' ? 'ANALYTICS' : 'STANDARD'
try {
await createBucket({
projectRef: ref,
id: values.name,
type: bucketTypeValue,
isPublic: false, // Specialized buckets are not public by default
file_size_limit: undefined, // Specialized buckets do not have a file size limit
allowed_mime_types: undefined, // Specialized buckets do not have allowed MIME types
})
sendEvent({
action: 'storage_bucket_created',
properties: { bucketType: bucketTypeValue },
groups: { project: ref ?? 'Unknown', organization: org?.slug ?? 'Unknown' },
})
if (wrappersExtensionState === 'not-installed') {
await enableExtension({
projectRef: project?.ref,
connectionString: project?.connectionString,
name: wrappersExtension.name,
schema: wrappersExtension.schema ?? 'extensions',
version: wrappersExtension.default_version,
})
await createIcebergWrapper({ bucketName: values.name })
} else if (wrappersExtensionState === 'installed') {
await createIcebergWrapper({ bucketName: values.name })
}
toast.success(`Created bucket “${values.name}”`)
form.reset()
setVisible(false)
router.push(`/project/${ref}/storage/${bucketType}/${values.name}`)
} catch (error: any) {
toast.error(`Failed to create bucket: ${error.message}`)
}
}
const handleClose = () => {
form.reset()
setVisible(false)
}
return (
<Dialog
open={visible}
onOpenChange={(open) => {
if (!open) handleClose()
}}
>
<DialogTrigger asChild>
<ButtonTooltip
block
size={buttonSize}
type={buttonType}
className={buttonClassName}
icon={<Plus size={14} />}
disabled={!canCreateBuckets || !icebergCatalogEnabled || disabled}
style={{ justifyContent: 'start' }}
onClick={() => setVisible(true)}
tooltip={{
content: {
side: tooltip?.content?.side || 'bottom',
className: cn(!icebergCatalogEnabled ? 'w-72 text-center' : ''),
text: !icebergCatalogEnabled
? 'Analytics buckets are not enabled for your project. Please contact support to enable it.'
: !canCreateBuckets
? 'You need additional permissions to create buckets'
: tooltip?.content?.text,
},
}}
>
New bucket
</ButtonTooltip>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Create {config.singularName} bucket</DialogTitle>
</DialogHeader>
<DialogSectionSeparator />
<Form_Shadcn_ {...form}>
<form id={formId} onSubmit={form.handleSubmit(onSubmit)}>
<DialogSection className="flex flex-col gap-y-4">
<FormField_Shadcn_
key="name"
name="name"
control={form.control}
render={({ field }) => (
<FormItemLayout
name="name"
label="Bucket name"
labelOptional="Cannot be changed after creation"
description="Must be between 3–63 characters. Only lowercase letters, numbers, dots, and hyphens are allowed."
>
<FormControl_Shadcn_>
<Input_Shadcn_
id="name"
data-1p-ignore
data-lpignore="true"
data-form-type="other"
data-bwignore
{...field}
placeholder="Enter bucket name"
/>
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
{bucketType === 'analytics' && (
<Admonition type="default">
<p>
Supabase will install the{' '}
{wrappersExtensionState !== 'installed' ? 'Wrappers extension and ' : ''}
Iceberg Wrapper integration on your behalf.{' '}
<InlineLink href={`${DOCS_URL}/guides/database/extensions/wrappers/iceberg`}>
Learn more
</InlineLink>
.
</p>
</Admonition>
)}
</DialogSection>
</form>
</Form_Shadcn_>
<DialogFooter>
<Button type="default" disabled={isCreating} onClick={() => setVisible(false)}>
Cancel
</Button>
<Button form={formId} htmlType="submit" loading={isCreating} disabled={isCreating}>
Create
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
@@ -156,7 +156,7 @@ export const DeleteBucketModal = ({ visible, bucket, onClose }: DeleteBucketModa
id="confirm"
autoComplete="off"
{...field}
placeholder="Type in name of bucket"
placeholder="Type bucket name"
/>
</FormControl_Shadcn_>
</FormItemLayout>
@@ -213,8 +213,8 @@ export const EditBucketModal = ({ visible, bucket, onClose }: EditBucketModalPro
<FormItemLayout
hideMessage
name="name"
label="Name of bucket"
labelOptional="Buckets cannot be renamed once created."
label="Bucket name"
labelOptional="Cannot be changed after creation"
>
<FormControl_Shadcn_>
<Input_Shadcn_ id="name" {...field} disabled />
@@ -1,5 +1,6 @@
import { BucketAdd } from 'icons'
import { CreateBucketModal } from './CreateBucketModal'
import { CreateSpecializedBucketModal } from './CreateSpecializedBucketModal'
import { BUCKET_TYPES } from './Storage.constants'
interface EmptyBucketStateProps {
@@ -10,19 +11,35 @@ export const EmptyBucketState = ({ bucketType }: EmptyBucketStateProps) => {
const config = BUCKET_TYPES[bucketType]
return (
<aside className="mt-12 border border-dashed w-full bg-surface-100 rounded-lg px-4 py-10 flex flex-col gap-y-4 items-center text-center gap-1 text-balance">
<div className="flex flex-col gap-3 items-center text-center">
<BucketAdd size={24} strokeWidth={1.5} className="text-foreground-muted" />
<div className="flex flex-col gap-1">
<h3>Create {config.label}</h3>
<p className="text-foreground-light text-sm">{config.valueProp}</p>
</div>
<aside className="mt-12 border border-dashed w-full bg-surface-100 rounded-lg px-4 py-10 flex flex-col gap-y-3 items-center text-center gap-1 text-balance">
<BucketAdd size={24} strokeWidth={1.5} className="text-foreground-muted" />
<div className="flex flex-col items-center text-center">
<h3>
Create {config.article} {config.singularName} bucket
</h3>
<p className="text-foreground-light text-sm">{config.valueProp}</p>
</div>
{/* [Joshen] We can render the individual bucket modals here instead - where each modal has its own trigger */}
{/* [Danny] CreateBucketModal needs to be split into CreateFileBucketModal, CreateAnalyticsBucketModal, CreateVectorsBucketModal */}
{bucketType === 'files' && (
{bucketType === 'files' ? (
<CreateBucketModal buttonSize="tiny" buttonType="primary" buttonClassName="w-fit" />
) : (
<CreateSpecializedBucketModal
bucketType={bucketType}
buttonSize="tiny"
buttonType="primary"
buttonClassName="w-fit"
disabled={bucketType === 'vectors'}
tooltip={
bucketType === 'vectors'
? {
content: {
side: 'bottom',
text: 'Vector buckets are not supported yet',
},
}
: undefined
}
/>
)}
</aside>
)
@@ -62,6 +62,7 @@ export const FilesBuckets = () => {
buckets.filter((bucket) => !('type' in bucket) || bucket.type === 'STANDARD').length === 0 ? (
<EmptyBucketState bucketType="files" />
) : (
// Add !pt-8 to override the default first:pt-12
<ScaffoldSection isFullWidth className="gap-y-4">
<div className="flex flex-grow justify-between gap-x-2 items-center">
<Input
@@ -65,21 +65,24 @@ export const CONTEXT_MENU_KEYS = {
export const BUCKET_TYPES = {
files: {
displayName: 'Files',
label: 'a file bucket',
singularName: 'file',
article: 'a',
description: 'General file storage for most types of digital content.',
valueProp: 'Store images, videos, documents, and any other file type.',
docsUrl: `${DOCS_URL}/guides/storage/buckets/fundamentals`,
},
analytics: {
displayName: 'Analytics',
label: 'an analytics bucket',
singularName: 'analytics',
article: 'an',
description: 'Purpose-built storage for analytical workloads.',
valueProp: 'Store large datasets for analytics and reporting.',
docsUrl: `${DOCS_URL}/guides/storage/analytics/introduction`,
},
vectors: {
displayName: 'Vectors',
label: 'a vector bucket',
singularName: 'vector',
article: 'a',
description: 'Purpose-built storage for vector data.',
valueProp: 'Store, index, and query your vector embeddings at scale.',
docsUrl: `${DOCS_URL}/guides/storage/vectors`,
@@ -54,7 +54,7 @@ describe(`CreateBucketModal`, () => {
expect(screen.getByRole(`dialog`)).toBeInTheDocument()
})
const nameInput = screen.getByLabelText(`Name of bucket`)
const nameInput = screen.getByLabelText(`Bucket name`)
await userEvent.type(nameInput, `test`)
const standardOption = screen.getByLabelText(`Standard bucket`)
@@ -73,7 +73,7 @@ describe(`EditBucketModal`, () => {
await userEvent.click(openButton)
await screen.findByRole(`dialog`)
const nameInput = screen.getByLabelText(`Name of bucket`)
const nameInput = screen.getByLabelText(`Bucket name`)
expect(nameInput).toHaveValue(`test`)
expect(nameInput).toBeDisabled()
@@ -1,8 +1,6 @@
import { useRouter } from 'next/router'
import { PropsWithChildren, useEffect } from 'react'
import { PropsWithChildren } from 'react'
import { useParams } from 'common'
import { useIsNewStorageUIEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { BUCKET_TYPES } from 'components/interfaces/Storage/Storage.constants'
import { useStorageV2Page } from 'components/interfaces/Storage/Storage.utils'
import { DocsButton } from 'components/ui/DocsButton'
@@ -15,10 +13,7 @@ export const StorageBucketsLayout = ({
children,
}: PropsWithChildren<{ title?: string; hideSubtitle?: boolean }>) => {
const { ref } = useParams()
const router = useRouter()
const page = useStorageV2Page()
const isStorageV2 = useIsNewStorageUIEnabled()
const config = !!page && page !== 's3' ? BUCKET_TYPES[page] : undefined
const navigationItems =
@@ -39,15 +34,11 @@ export const StorageBucketsLayout = ({
]
: []
useEffect(() => {
if (!isStorageV2) router.replace(`/project/${ref}/storage/buckets`)
}, [isStorageV2, ref, router])
return (
<PageLayout
title={title || (config?.displayName ?? 'Storage')}
subtitle={
hideSubtitle ? config?.description || 'Manage your storage buckets and files.' : null
!hideSubtitle ? config?.description || 'Manage your storage buckets and files.' : null
}
navigationItems={navigationItems}
secondaryActions={config?.docsUrl ? [<DocsButton key="docs" href={config.docsUrl} />] : []}
@@ -1,6 +1,9 @@
import { ReactNode } from 'react'
import { useRouter } from 'next/router'
import { ReactNode, useEffect } from 'react'
import { useParams } from 'common'
import { useIsNewStorageUIEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { useSelectedBucket } from 'components/interfaces/Storage/StorageExplorer/useSelectedBucket'
import { StorageMenu } from 'components/interfaces/Storage/StorageMenu'
import { StorageMenuV2 } from 'components/interfaces/Storage/StorageMenuV2'
import { withAuth } from 'hooks/misc/withAuth'
@@ -12,7 +15,42 @@ export interface StorageLayoutProps {
}
const StorageLayout = ({ title, children }: StorageLayoutProps) => {
const router = useRouter()
const isStorageV2 = useIsNewStorageUIEnabled()
const { ref, featurePreviewModal, bucketId } = useParams()
const { bucket } = useSelectedBucket()
// [Joshen] Handle redirects between existing UI and feature preview
useEffect(() => {
const { pathname } = router
const suffix = !!featurePreviewModal ? `?featurePreviewModal=${featurePreviewModal}` : ''
if (isStorageV2) {
if (pathname.endsWith('/storage/settings')) {
router.push(`/project/${ref}/storage/files/settings${suffix}`)
} else if (pathname.endsWith('/storage/policies')) {
router.push(`/project/${ref}/storage/files/policies${suffix}`)
} else if (pathname.endsWith('/storage/buckets/[bucketId]')) {
if (!!bucket && bucket.type === 'STANDARD') {
router.push(`/project/${ref}/storage/files/buckets/${bucketId}${suffix}`)
} else if (!!bucket && bucket.type === 'ANALYTICS') {
router.push(`/project/${ref}/storage/analytics/buckets/${bucketId}${suffix}`)
}
}
} else {
if (pathname.endsWith('/files/settings')) {
router.push(`/project/${ref}/storage/settings${suffix}`)
} else if (pathname.endsWith('/files/policies')) {
router.push(`/project/${ref}/storage/policies${suffix}`)
} else if (
pathname.endsWith('/files/buckets/[bucketId]') ||
pathname.endsWith('/analytics/buckets/[bucketId]')
) {
router.push(`/project/${ref}/storage/buckets/${bucketId}${suffix}`)
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isStorageV2])
return (
<ProjectLayout
@@ -33,9 +33,7 @@ const PageLayout: NextPageWithLayout = () => {
) : (
<StorageExplorer bucket={bucket} />
)
) : (
<div />
)}
) : null}
</div>
)
}