Joshen/depr 262 bucketid route needs to handle if a bucket doesnt exist (#40430)

* Handle landing on a bucket id that doesnt exist in project

* Standardize re routing behaviour for files bucket

* refactor

---------

Co-authored-by: Alaister Young <a@alaisteryoung.com>
This commit is contained in:
Joshen LimandAlaister Young authored and GitHub committed 2025-11-14 15:25:59 +08:00
1 parent 7e783dcb0a
commit 8b5a087eb7
9 files changed
+113 -66

No files matched your search

@@ -11,7 +11,6 @@ import {
convertKVStringArrayToJson,
formatWrapperTables,
} from 'components/interfaces/Integrations/Wrappers/Wrappers.utils'
import { useSelectedBucket } from 'components/interfaces/Storage/StorageExplorer/useSelectedBucket'
import {
ScaffoldContainer,
ScaffoldSection,
@@ -25,7 +24,6 @@ import {
} from 'data/database-extensions/database-extensions-query'
import { useReplicationPipelineStatusQuery } from 'data/etl/pipeline-status-query'
import { useStartPipelineMutation } from 'data/etl/start-pipeline-mutation'
import { AnalyticsBucket } from 'data/storage/analytics-buckets-query'
import { useIcebergNamespacesQuery } from 'data/storage/iceberg-namespaces-query'
import { useIcebergWrapperCreateMutation } from 'data/storage/iceberg-wrapper-create-mutation'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
@@ -34,6 +32,7 @@ import { Button, Card, CardContent } from 'ui'
import { Admonition } from 'ui-patterns/admonition'
import { GenericTableLoader } from 'ui-patterns/ShimmeringLoader'
import { DeleteAnalyticsBucketModal } from '../DeleteAnalyticsBucketModal'
import { useSelectedAnalyticsBucket } from '../useSelectedAnalyticsBucket'
import { BucketHeader } from './BucketHeader'
import { ConnectTablesDialog } from './ConnectTablesDialog'
import { NamespaceWithTables } from './NamespaceWithTables'
@@ -48,12 +47,11 @@ export const AnalyticBucketDetails = () => {
const { data: project } = useSelectedProjectQuery()
const { state: extensionState } = useIcebergWrapperExtension()
const {
bucket: _bucket,
data: bucket,
error: bucketError,
isSuccess: isSuccessBucket,
isError: isErrorBucket,
} = useSelectedBucket()
const bucket = _bucket as undefined | AnalyticsBucket
} = useSelectedAnalyticsBucket()
const [modal, setModal] = useState<'delete' | null>(null)
// [Joshen] Namespaces are now created asynchronously when the pipeline is started, so long poll after
@@ -332,7 +330,7 @@ export const AnalyticBucketDetails = () => {
</div>
<Button
type="danger"
disabled={!isSuccessBucket}
disabled={!bucket?.id || !isSuccessBucket}
onClick={() => setModal('delete')}
>
Delete bucket
@@ -0,0 +1,18 @@
import { useParams } from 'common'
import { useIsAnalyticsBucketsEnabled } from 'data/config/project-storage-config-query'
import { useAnalyticsBucketsQuery } from 'data/storage/analytics-buckets-query'
export const useSelectedAnalyticsBucket = () => {
const { ref, bucketId } = useParams()
const hasIcebergEnabled = useIsAnalyticsBucketsEnabled({ projectRef: ref })
return useAnalyticsBucketsQuery(
{ projectRef: ref },
{
enabled: hasIcebergEnabled,
select(data) {
return data.find((x) => x.id === bucketId)
},
}
)
}
@@ -0,0 +1,15 @@
import { useParams } from 'common'
import { useBucketsQuery } from 'data/storage/buckets-query'
export const useSelectedBucket = () => {
const { ref, bucketId } = useParams()
return useBucketsQuery(
{ projectRef: ref },
{
select(data) {
return data.find((b) => b.id === bucketId)
},
}
)
}
@@ -1,40 +0,0 @@
import { useParams } from 'common'
import { useIsAnalyticsBucketsEnabled } from 'data/config/project-storage-config-query'
import { useAnalyticsBucketsQuery } from 'data/storage/analytics-buckets-query'
import { useBucketsQuery } from 'data/storage/buckets-query'
import { useStorageV2Page } from '../Storage.utils'
export const useSelectedBucket = () => {
const { ref, bucketId } = useParams()
const page = useStorageV2Page()
const hasIcebergEnabled = useIsAnalyticsBucketsEnabled({ projectRef: ref })
const {
data: analyticsBuckets = [],
isSuccess: isSuccessAnalyticsBuckets,
isError: isErrorAnalyticsBuckets,
error: errorAnalyticsBuckets,
} = useAnalyticsBucketsQuery({ projectRef: ref })
const {
data: buckets = [],
isSuccess: isSuccessBuckets,
isError: isErrorBuckets,
error: errorBuckets,
} = useBucketsQuery({ projectRef: ref })
const isSuccess = hasIcebergEnabled
? isSuccessBuckets && isSuccessAnalyticsBuckets
: isSuccessBuckets
const isError = hasIcebergEnabled ? isErrorBuckets || isErrorAnalyticsBuckets : isErrorBuckets
const error = hasIcebergEnabled ? errorBuckets || errorAnalyticsBuckets : errorBuckets
const bucket =
page === 'files'
? buckets.find((b) => b.id === bucketId)
: page === 'analytics'
? analyticsBuckets.find((b: any) => b.id === bucketId)
: buckets.find((b) => b.id === bucketId)
return { bucket, isSuccess, isError, error }
}
@@ -49,10 +49,13 @@ import { DeleteVectorTableModal } from './DeleteVectorTableModal'
import { getVectorBucketFDWSchemaName } from './VectorBuckets.utils'
import { useS3VectorsWrapperExtension } from './useS3VectorsWrapper'
import { useS3VectorsWrapperInstance } from './useS3VectorsWrapperInstance'
import { useSelectedVectorBucket } from './useSelectedVectorBuckets'
export const VectorBucketDetails = () => {
const router = useRouter()
const { ref: projectRef, bucketId } = useParams()
// [Joshen] Use the list buckets to verify that the bucket exists first before fetching bucket details
const { data: _bucket, isSuccess } = useSelectedVectorBucket()
const [filterString, setFilterString] = useState('')
const [showDeleteModal, setShowDeleteModal] = useState(false)
@@ -63,7 +66,10 @@ export const VectorBucketDetails = () => {
error: bucketError,
isSuccess: isSuccessBucket,
isError: isErrorBucket,
} = useVectorBucketQuery({ projectRef, vectorBucketName: bucketId })
} = useVectorBucketQuery(
{ projectRef, vectorBucketName: bucketId },
{ enabled: isSuccess && !!_bucket }
)
const { data, isLoading: isLoadingIndexes } = useVectorBucketsIndexesQuery({
projectRef,
@@ -0,0 +1,15 @@
import { useParams } from 'common'
import { useVectorBucketsQuery } from 'data/storage/vector-buckets-query'
export const useSelectedVectorBucket = () => {
const { ref: projectRef, bucketId } = useParams()
return useVectorBucketsQuery(
{ projectRef },
{
select(data) {
return data.vectorBuckets.find((x) => x.vectorBucketName === bucketId)
},
}
)
}
@@ -1,5 +1,10 @@
import { useRouter } from 'next/router'
import { useEffect } from 'react'
import { toast } from 'sonner'
import { useParams } from 'common'
import { AnalyticBucketDetails } from 'components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails'
import { useSelectedAnalyticsBucket } from 'components/interfaces/Storage/AnalyticsBuckets/useSelectedAnalyticsBucket'
import { BUCKET_TYPES } from 'components/interfaces/Storage/Storage.constants'
import DefaultLayout from 'components/layouts/DefaultLayout'
import { PageLayout } from 'components/layouts/PageLayout/PageLayout'
@@ -11,8 +16,18 @@ import type { NextPageWithLayout } from 'types'
const AnalyticsBucketPage: NextPageWithLayout = () => {
const config = BUCKET_TYPES.analytics
const { bucketId } = useParams()
const router = useRouter()
const { ref, bucketId } = useParams()
const { data: project } = useSelectedProjectQuery()
const { data: bucket, isSuccess } = useSelectedAnalyticsBucket()
useEffect(() => {
if (isSuccess && !bucket) {
toast.info(`Bucket "${bucketId}" does not exist in your project`)
router.push(`/project/${ref}/storage/analytics`)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isSuccess])
return (
<PageLayout
@@ -1,14 +1,16 @@
import { ChevronDown, FolderOpen, Settings, Shield, Trash2 } from 'lucide-react'
import Link from 'next/link'
import { useState } from 'react'
import { useRouter } from 'next/router'
import { useEffect, useState } from 'react'
import { toast } from 'sonner'
import { useParams } from 'common'
import { DeleteBucketModal } from 'components/interfaces/Storage/DeleteBucketModal'
import { EditBucketModal } from 'components/interfaces/Storage/EditBucketModal'
import { EmptyBucketModal } from 'components/interfaces/Storage/EmptyBucketModal'
import { useSelectedBucket } from 'components/interfaces/Storage/FilesBuckets/useSelectedBucket'
import StorageBucketsError from 'components/interfaces/Storage/StorageBucketsError'
import { StorageExplorer } from 'components/interfaces/Storage/StorageExplorer/StorageExplorer'
import { useSelectedBucket } from 'components/interfaces/Storage/StorageExplorer/useSelectedBucket'
import DefaultLayout from 'components/layouts/DefaultLayout'
import { PageLayout } from 'components/layouts/PageLayout/PageLayout'
import StorageLayout from 'components/layouts/StorageLayout/StorageLayout'
@@ -29,15 +31,24 @@ import {
} from 'ui'
const BucketPage: NextPageWithLayout = () => {
const router = useRouter()
const { bucketId, ref } = useParams()
const { data: project } = useSelectedProjectQuery()
const { projectRef } = useStorageExplorerStateSnapshot()
const { bucket, error, isSuccess, isError } = useSelectedBucket()
const { data: bucket, error, isSuccess, isError } = useSelectedBucket()
const [modal, setModal] = useState<'edit' | 'empty' | 'delete' | null>(null)
const { getPolicyCount } = useStoragePolicyCounts(bucket ? [bucket as Bucket] : [])
const policyCount = bucket ? getPolicyCount(bucket.id) : 0
useEffect(() => {
if (isSuccess && !bucket) {
toast.info(`Bucket "${bucketId}" does not exist in your project`)
router.push(`/project/${ref}/storage/files`)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isSuccess])
// [Joshen] Checking against projectRef from storage explorer to check if the store has initialized
if (!project || !projectRef || !isSuccess) return null
@@ -45,15 +56,6 @@ const BucketPage: NextPageWithLayout = () => {
return <StorageBucketsError error={error as any} />
}
// If the bucket is not found or the bucket type is ANALYTICS or VECTOR, show an error message
if (!bucket || ('type' in bucket && bucket.type !== 'STANDARD')) {
return (
<div className="flex h-full w-full items-center justify-center">
<p className="text-sm text-foreground-light">Bucket "{bucketId}" cannot be found</p>
</div>
)
}
return (
<>
<PageLayout
@@ -67,8 +69,8 @@ const BucketPage: NextPageWithLayout = () => {
className="[&>div:first-child]:!border-b-0" // Override the border-b from ScaffoldContainer
title={
<div className="flex items-center gap-2 min-w-0 flex-1">
<span className="truncate">{bucket.name}</span>
{bucket.public && (
<span className="truncate">{bucketId}</span>
{bucket?.public && (
<Badge variant="warning" size="small" className="flex-shrink-0">
Public
</Badge>
@@ -134,9 +136,11 @@ const BucketPage: NextPageWithLayout = () => {
</>
}
>
<div className="flex-1 min-h-0 px-6 pb-6">
<StorageExplorer bucket={bucket} />
</div>
{!!bucket && (
<div className="flex-1 min-h-0 px-6 pb-6">
<StorageExplorer bucket={bucket} />
</div>
)}
</PageLayout>
{bucket && (
@@ -1,5 +1,10 @@
import { useRouter } from 'next/router'
import { useEffect } from 'react'
import { toast } from 'sonner'
import { useParams } from 'common'
import { BUCKET_TYPES } from 'components/interfaces/Storage/Storage.constants'
import { useSelectedVectorBucket } from 'components/interfaces/Storage/VectorBuckets/useSelectedVectorBuckets'
import { VectorBucketDetails } from 'components/interfaces/Storage/VectorBuckets/VectorBucketDetails'
import DefaultLayout from 'components/layouts/DefaultLayout'
import { PageLayout } from 'components/layouts/PageLayout/PageLayout'
@@ -11,9 +16,20 @@ import type { NextPageWithLayout } from 'types'
const VectorsBucketPage: NextPageWithLayout = () => {
const config = BUCKET_TYPES['vectors']
const { bucketId } = useParams()
const router = useRouter()
const { ref, bucketId } = useParams()
const { projectRef } = useStorageExplorerStateSnapshot()
const { data: bucket, isSuccess } = useSelectedVectorBucket()
useEffect(() => {
if (isSuccess && !bucket) {
toast.info(`Bucket "${bucketId}" does not exist in your project`)
router.push(`/project/${ref}/storage/vectors`)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isSuccess])
return (
<PageLayout
title={bucketId}