From 9f9b1361ad15740493f48415aac9273d47998714 Mon Sep 17 00:00:00 2001 From: Sebastian Kulpok Date: Thu, 12 Feb 2026 12:57:29 +0100 Subject: [PATCH] bug: [STORIF-221] - corrected a bug where an empty bucket list was displayed --- packages/api-v4/src/object-storage/buckets.ts | 19 -- .../enable-object-storage.spec.ts | 26 -- .../object-storage.smoke.spec.ts | 12 +- .../ObjectsTab/ObjectDetailsDrawer.tsx | 4 +- .../ObjectStorage/BucketDetail/index.tsx | 4 +- .../BucketDetailsDrawer.test.tsx | 20 +- .../BucketLanding/BucketLanding.test.tsx | 258 --------------- .../BucketLanding/BucketLanding.tsx | 307 ------------------ .../ObjectStorage/ObjectStorageLanding.tsx | 15 +- .../src/queries/object-storage/queries.ts | 74 ++--- .../src/queries/object-storage/requests.ts | 44 +-- packages/queries/src/regions/regions.ts | 3 +- 12 files changed, 56 insertions(+), 730 deletions(-) delete mode 100644 packages/manager/src/features/ObjectStorage/BucketLanding/BucketLanding.test.tsx delete mode 100644 packages/manager/src/features/ObjectStorage/BucketLanding/BucketLanding.tsx diff --git a/packages/api-v4/src/object-storage/buckets.ts b/packages/api-v4/src/object-storage/buckets.ts index f4b3e09b681..20e71533d77 100644 --- a/packages/api-v4/src/object-storage/buckets.ts +++ b/packages/api-v4/src/object-storage/buckets.ts @@ -60,25 +60,6 @@ export const getBuckets = (params?: Params, filters?: Filter) => setURL(`${API_ROOT}/object-storage/buckets`), ); -/** - * getBucketsInCluster - * - * Gets a list of a user's Object Storage Buckets in the specified cluster. - */ -export const getBucketsInCluster = ( - clusterId: string, - params?: Params, - filters?: Filter, -) => - Request>( - setMethod('GET'), - setParams(params), - setXFilter(filters), - setURL( - `${API_ROOT}/object-storage/buckets/${encodeURIComponent(clusterId)}`, - ), - ); - /** * getBucketsInRegion * diff --git a/packages/manager/cypress/e2e/core/objectStorage/enable-object-storage.spec.ts b/packages/manager/cypress/e2e/core/objectStorage/enable-object-storage.spec.ts index 708b19561b8..adb02280600 100644 --- a/packages/manager/cypress/e2e/core/objectStorage/enable-object-storage.spec.ts +++ b/packages/manager/cypress/e2e/core/objectStorage/enable-object-storage.spec.ts @@ -19,7 +19,6 @@ import { mockCreateAccessKey, mockGetAccessKeys, mockGetBuckets, - mockGetClusters, mockGetObjectStorageTypes, } from 'support/intercepts/object-storage'; import { mockGetProfile } from 'support/intercepts/profile'; @@ -101,29 +100,6 @@ describe('Object Storage enrollment', () => { }), ]; - // Clusters with special pricing are currently hardcoded rather than - // retrieved via API, so we have to mock the cluster API request to correspond - // with that hardcoded data. - // - // Because the IDs used in the mocks don't correspond with any actual clusters, - // we have to cast them as `ObjectStorageClusterID` to satisfy TypeScript. - const mockClusters: ObjectStorageCluster[] = [ - // Regions with special pricing. - objectStorageClusterFactory.build({ - id: 'br-gru-0' as ObjectStorageClusterID, - region: 'br-gru', - }), - objectStorageClusterFactory.build({ - id: 'id-cgk-1' as ObjectStorageClusterID, - region: 'id-cgk', - }), - // A region that does not have special pricing. - objectStorageClusterFactory.build({ - id: 'us-east-1', - region: 'us-east', - }), - ]; - const mockPrices: PriceType[] = [ { id: 'distributed_network_transfer', @@ -209,7 +185,6 @@ describe('Object Storage enrollment', () => { 'getObjectStorageTypes' ); mockGetAccountSettings(mockAccountSettings).as('getAccountSettings'); - mockGetClusters(mockClusters).as('getClusters'); mockGetBuckets([]).as('getBuckets'); mockGetRegions(mockRegions).as('getRegions'); mockGetAccessKeys([]); @@ -217,7 +192,6 @@ describe('Object Storage enrollment', () => { cy.visitWithLogin('/object-storage/buckets'); cy.wait([ '@getAccountSettings', - '@getClusters', '@getBuckets', '@getRegions', ]); diff --git a/packages/manager/cypress/e2e/core/objectStorage/object-storage.smoke.spec.ts b/packages/manager/cypress/e2e/core/objectStorage/object-storage.smoke.spec.ts index 9d36a170faf..c11d6fd890f 100644 --- a/packages/manager/cypress/e2e/core/objectStorage/object-storage.smoke.spec.ts +++ b/packages/manager/cypress/e2e/core/objectStorage/object-storage.smoke.spec.ts @@ -42,7 +42,7 @@ describe('object storage smoke tests', () => { mockGetAccount(accountFactory.build({ capabilities: ['Object Storage'] })); mockAppendFeatureFlags({ gecko2: false, - objMultiCluster: false, + objMultiCluster: true, objectStorageGen2: { enabled: false }, }).as('getFeatureFlags'); @@ -179,22 +179,22 @@ describe('object storage smoke tests', () => { */ it('can delete object storage bucket - smoke', () => { const bucketLabel = randomLabel(); - const bucketCluster = 'us-southeast-1'; + const region = 'us-southeast'; const bucketMock = objectStorageBucketFactory.build({ - cluster: bucketCluster, - hostname: `${bucketLabel}.${bucketCluster}.linodeobjects.com`, + region: region, + hostname: `${bucketLabel}.${region}.linodeobjects.com`, label: bucketLabel, objects: 0, }); mockGetAccount(accountFactory.build({ capabilities: ['Object Storage'] })); mockAppendFeatureFlags({ - objMultiCluster: false, + objMultiCluster: true, objectStorageGen2: { enabled: false }, }); mockGetBuckets([bucketMock]).as('getBuckets'); - mockDeleteBucket(bucketLabel, bucketCluster).as('deleteBucket'); + mockDeleteBucket(bucketLabel, region).as('deleteBucket'); cy.visitWithLogin('/object-storage/buckets'); cy.wait('@getBuckets'); diff --git a/packages/manager/src/features/ObjectStorage/BucketDetail/ObjectsTab/ObjectDetailsDrawer.tsx b/packages/manager/src/features/ObjectStorage/BucketDetail/ObjectsTab/ObjectDetailsDrawer.tsx index 88d566f93ac..09e39c87f89 100644 --- a/packages/manager/src/features/ObjectStorage/BucketDetail/ObjectsTab/ObjectDetailsDrawer.tsx +++ b/packages/manager/src/features/ObjectStorage/BucketDetail/ObjectsTab/ObjectDetailsDrawer.tsx @@ -6,7 +6,6 @@ import * as React from 'react'; import { CopyTooltip } from 'src/components/CopyTooltip/CopyTooltip'; import { Link } from 'src/components/Link'; -import { useIsObjectStorageGen2Enabled } from 'src/features/ObjectStorage/hooks/useIsObjectStorageGen2Enabled'; import { useObjectStorageBuckets } from 'src/queries/object-storage/queries'; import { formatDate } from 'src/utilities/formatDate'; @@ -40,9 +39,8 @@ export const ObjectDetailsDrawer = React.memo( let formattedLastModified; const { data: profile } = useProfile(); - const { isObjectStorageGen2Enabled } = useIsObjectStorageGen2Enabled(); const { data: bucketsData, isLoading: isLoadingEndpointData } = - useObjectStorageBuckets(isObjectStorageGen2Enabled); + useObjectStorageBuckets(); const isLoadingEndpoint = isLoadingEndpointData || !bucketsData; diff --git a/packages/manager/src/features/ObjectStorage/BucketDetail/index.tsx b/packages/manager/src/features/ObjectStorage/BucketDetail/index.tsx index b1110d69e7f..0daf5064431 100644 --- a/packages/manager/src/features/ObjectStorage/BucketDetail/index.tsx +++ b/packages/manager/src/features/ObjectStorage/BucketDetail/index.tsx @@ -9,7 +9,6 @@ import { SafeTabPanel } from 'src/components/Tabs/SafeTabPanel'; import { TabPanels } from 'src/components/Tabs/TabPanels'; import { Tabs } from 'src/components/Tabs/Tabs'; import { TanStackTabLinkList } from 'src/components/Tabs/TanStackTabLinkList'; -import { useIsObjectStorageGen2Enabled } from 'src/features/ObjectStorage/hooks/useIsObjectStorageGen2Enabled'; import { useFlags } from 'src/hooks/useFlags'; import { useTabs } from 'src/hooks/useTabs'; import { useCloudPulseServiceByServiceType } from 'src/queries/cloudpulse/services'; @@ -48,7 +47,6 @@ export const BucketDetailLanding = React.memo(() => { }); const { aclpServices, objectStorageContextualMetrics } = useFlags(); - const { isObjectStorageGen2Enabled } = useIsObjectStorageGen2Enabled(); const { isError: aclpServiceError, isLoading: aclServiceLoading } = useCloudPulseServiceByServiceType('objectstorage', true); @@ -57,7 +55,7 @@ export const BucketDetailLanding = React.memo(() => { isLoading, error, isPending, - } = useObjectStorageBuckets(isObjectStorageGen2Enabled); + } = useObjectStorageBuckets(); const bucket = bucketsData?.buckets.find(({ label }) => label === bucketName); diff --git a/packages/manager/src/features/ObjectStorage/BucketLanding/BucketDetailsDrawer.test.tsx b/packages/manager/src/features/ObjectStorage/BucketLanding/BucketDetailsDrawer.test.tsx index 88486866dba..26ac6c68455 100644 --- a/packages/manager/src/features/ObjectStorage/BucketLanding/BucketDetailsDrawer.test.tsx +++ b/packages/manager/src/features/ObjectStorage/BucketLanding/BucketDetailsDrawer.test.tsx @@ -98,9 +98,6 @@ describe('BucketDetailsDrawer: Legacy UI', () => { selectedBucket={bucket} /> ), - options: { - flags: { objMultiCluster: false }, - }, }); expect(screen.getByText(bucket.label)).toBeInTheDocument(); @@ -122,9 +119,6 @@ describe('BucketDetailsDrawer: Legacy UI', () => { selectedBucket={bucket} /> ), - options: { - flags: { objMultiCluster: false }, - }, }); expect(screen.queryByText(bucket.label)).not.toBeInTheDocument(); @@ -139,9 +133,6 @@ describe('BucketDetailsDrawer: Legacy UI', () => { selectedBucket={bucket} /> ), - options: { - flags: { objMultiCluster: false }, - }, }); expect(screen.getByTestId('cluster')).toHaveTextContent(region.id); @@ -156,9 +147,6 @@ describe('BucketDetailsDrawer: Legacy UI', () => { selectedBucket={undefined} /> ), - options: { - flags: { objMultiCluster: false }, - }, }); expect(screen.getByText('Bucket Detail')).toBeInTheDocument(); @@ -176,7 +164,7 @@ describe('BucketDetailsDrawer: Legacy UI', () => { /> ), options: { - flags: { objMultiCluster: false }, + flags: { objectStorageGen2: { enabled: true } }, }, }); @@ -198,7 +186,7 @@ describe('BucketDetailsDrawer: Legacy UI', () => { /> ), options: { - flags: { objMultiCluster: false }, + flags: { objectStorageGen2: { enabled: true } }, }, }); @@ -227,7 +215,7 @@ describe('BucketDetailDrawer: Gen2 UI', () => { /> ), options: { - flags: { objMultiCluster: false, objectStorageGen2: { enabled: true } }, + flags: { objectStorageGen2: { enabled: true } }, }, }); @@ -254,7 +242,7 @@ describe('BucketDetailDrawer: Gen2 UI', () => { /> ), options: { - flags: { objMultiCluster: false, objectStorageGen2: { enabled: true } }, + flags: { objectStorageGen2: { enabled: true } }, }, }); diff --git a/packages/manager/src/features/ObjectStorage/BucketLanding/BucketLanding.test.tsx b/packages/manager/src/features/ObjectStorage/BucketLanding/BucketLanding.test.tsx deleted file mode 100644 index 2db84b05ae7..00000000000 --- a/packages/manager/src/features/ObjectStorage/BucketLanding/BucketLanding.test.tsx +++ /dev/null @@ -1,258 +0,0 @@ -import { screen, waitFor } from '@testing-library/react'; -import * as React from 'react'; - -import { - objectStorageBucketFactory, - objectStorageClusterFactory, -} from 'src/factories/objectStorage'; -import { makeResourcePage } from 'src/mocks/serverHandlers'; -import { http, HttpResponse, server } from 'src/mocks/testServer'; -import { renderWithTheme } from 'src/utilities/testHelpers'; - -import { BucketLanding } from './BucketLanding'; - -const queryMocks = vi.hoisted(() => ({ - useNavigate: vi.fn(() => vi.fn()), - useOrderV2: vi.fn().mockReturnValue({}), - useSearch: vi.fn(), -})); - -vi.mock('@tanstack/react-router', async () => { - const actual = await vi.importActual('@tanstack/react-router'); - return { - ...actual, - useNavigate: queryMocks.useNavigate, - useSearch: queryMocks.useSearch, - }; -}); - -vi.mock('src/hooks/useOrderV2', async () => { - const actual = await vi.importActual('src/hooks/useOrderV2'); - return { - ...actual, - useOrderV2: queryMocks.useOrderV2, - }; -}); - -describe('ObjectStorageLanding', () => { - beforeAll(() => { - server.listen(); - queryMocks.useSearch.mockReturnValue({ - order: 'asc', - orderBy: 'label', - }); - }); - afterEach(() => server.resetHandlers()); - afterAll(() => server.close()); - - it('renders a loading state', () => { - // Mock Buckets - server.use( - http.get('*/object-storage/buckets', () => { - return HttpResponse.json(makeResourcePage([])); - }) - ); - - renderWithTheme(); - - screen.getByTestId('circle-progress'); - }); - - it('renders an empty state', async () => { - // Mock Clusters - server.use( - http.get('*/object-storage/clusters', () => { - const clusters = objectStorageClusterFactory.buildList(4); - return HttpResponse.json(makeResourcePage(clusters)); - }) - ); - - // Mock Buckets - server.use( - http.get('*/object-storage/buckets/*', () => { - return HttpResponse.json(makeResourcePage([])); - }) - ); - - renderWithTheme(); - - await screen.findByTestId('placeholder-button'); - }); - - it('renders per-cluster errors', async () => { - objectStorageBucketFactory.resetSequenceNumber(); - objectStorageClusterFactory.resetSequenceNumber(); - - const downCluster = objectStorageClusterFactory.build({ - region: 'us-west', - }); - - // Mock Clusters - server.use( - http.get('*/object-storage/clusters', () => { - const upClusters = objectStorageClusterFactory.buildList(1, { - region: 'ap-south-1', - }); - return HttpResponse.json( - makeResourcePage([downCluster, ...upClusters]) - ); - }) - ); - - // Mock Buckets - server.use( - http.get( - '*/object-storage/buckets/cluster-1', - () => { - return HttpResponse.json([{ reason: 'Cluster offline!' }], { - status: 500, - }); - }, - { - once: true, - } - ), - http.get('*/object-storage/buckets/*', () => { - return HttpResponse.json( - makeResourcePage( - objectStorageBucketFactory.buildList(2, { cluster: 'ap-south-1' }) - ) - ); - }) - ); - - renderWithTheme(); - - await screen.findByText( - /^There was an error loading buckets in US, Fremont, CA/ - ); - }); - - it('renders general error state', async () => { - // Mock Clusters - server.use( - http.get('*/object-storage/clusters', () => { - const clusters = objectStorageClusterFactory.buildList(1); - return HttpResponse.json(makeResourcePage(clusters)); - }) - ); - - // Mock Buckets - server.use( - http.get('*/object-storage/buckets/*', () => { - return HttpResponse.json([{ reason: 'Cluster offline!' }], { - status: 500, - }); - }) - ); - renderWithTheme(); - - await screen.findByText(/^There was an error retrieving your buckets/); - }); - - it('renders rows for each Bucket', async () => { - const buckets = objectStorageBucketFactory.buildList(2); - queryMocks.useOrderV2.mockReturnValue({ - order: 'asc', - orderBy: 'label', - sortedData: buckets, - }); - - // Mock Clusters - server.use( - http.get('*/object-storage/clusters', () => { - const clusters = objectStorageClusterFactory.buildList(1); - return HttpResponse.json(makeResourcePage(clusters)); - }) - ); - - // Mock Buckets - server.use( - http.get('*/object-storage/buckets/*', () => { - return HttpResponse.json(makeResourcePage(buckets)); - }) - ); - - renderWithTheme(); - - await screen.findByText(buckets[0].label); - await screen.findByText(buckets[1].label); - }); - - it('renders a "Total usage" section using base2 calculations if there is more than one Bucket', async () => { - const buckets = objectStorageBucketFactory.buildList(2, { - size: 1024 * 1024 * 1024 * 5, // 5 GB in base2 (5 GiB) - }); - - // Mock Clusters - server.use( - http.get('*/object-storage/clusters', () => { - const clusters = objectStorageClusterFactory.buildList(1); - return HttpResponse.json(makeResourcePage(clusters)); - }) - ); - - // Mock Buckets - server.use( - http.get('*/object-storage/buckets/*', () => { - return HttpResponse.json(makeResourcePage(buckets)); - }) - ); - - renderWithTheme(); - - await screen.findByText(/Total storage used: 10 GB/); - }); - - it('renders error notice for multiple regions', async () => { - objectStorageBucketFactory.resetSequenceNumber(); - objectStorageClusterFactory.resetSequenceNumber(); - - // Create multiple down clusters in different regions - const downClusters = [ - objectStorageClusterFactory.build({ region: 'us-west' }), - objectStorageClusterFactory.build({ region: 'ap-south' }), - objectStorageClusterFactory.build({ region: 'eu-west' }), - ]; - - // Mock Clusters - server.use( - http.get('*/object-storage/clusters', () => { - const upCluster = objectStorageClusterFactory.build({ - region: 'us-east', - }); - return HttpResponse.json( - makeResourcePage([...downClusters, upCluster]) - ); - }) - ); - - // Mock bucket errors for each down cluster - server.use( - ...downClusters.map((cluster) => - http.get(`*/object-storage/buckets/${cluster.id}`, () => { - return HttpResponse.json([{ reason: 'Cluster offline!' }], { - status: 500, - }); - }) - ), - // Mock successful response for up cluster - http.get('*/object-storage/buckets/*', () => { - return HttpResponse.json( - makeResourcePage( - objectStorageBucketFactory.buildList(1, { cluster: 'us-east' }) - ) - ); - }) - ); - - renderWithTheme(); - - await waitFor(() => { - const errorRegions = ['US, Fremont, CA', 'SG, Singapore', 'GB, London']; - for (const region of errorRegions) { - expect(screen.queryByText(region)).toBeInTheDocument(); - } - }); - }); -}); diff --git a/packages/manager/src/features/ObjectStorage/BucketLanding/BucketLanding.tsx b/packages/manager/src/features/ObjectStorage/BucketLanding/BucketLanding.tsx deleted file mode 100644 index 3bfbb0c81ae..00000000000 --- a/packages/manager/src/features/ObjectStorage/BucketLanding/BucketLanding.tsx +++ /dev/null @@ -1,307 +0,0 @@ -import { useProfile, useRegionsQuery } from '@linode/queries'; -import { CircleProgress, ErrorState, Notice, Typography } from '@linode/ui'; -import { readableBytes, useOpenClose } from '@linode/utilities'; -import Grid from '@mui/material/Grid'; -import * as React from 'react'; -import { makeStyles } from 'tss-react/mui'; - -import { DocumentTitleSegment } from 'src/components/DocumentTitle'; -import { Link } from 'src/components/Link'; -import { TransferDisplay } from 'src/components/TransferDisplay/TransferDisplay'; -import { TypeToConfirmDialog } from 'src/components/TypeToConfirmDialog/TypeToConfirmDialog'; -import { useOrderV2 } from 'src/hooks/useOrderV2'; -import { - useDeleteBucketMutation, - useObjectStorageBuckets, -} from 'src/queries/object-storage/queries'; -import { isBucketError } from 'src/queries/object-storage/requests'; -import { - sendDeleteBucketEvent, - sendDeleteBucketFailedEvent, -} from 'src/utilities/analytics/customEventAnalytics'; - -import { CancelNotice } from '../CancelNotice'; -import { BucketDetailsDrawer } from './BucketDetailsDrawer'; -import { BucketLandingEmptyState } from './BucketLandingEmptyState'; -import { BucketTable } from './BucketTable'; - -import type { - APIError, - ObjectStorageBucket, - ObjectStorageCluster, - ObjectStorageEndpoint, -} from '@linode/api-v4'; -import type { Theme } from '@mui/material/styles'; - -interface Props { - isCreateBucketDrawerOpen?: boolean; -} - -const useStyles = makeStyles()((theme: Theme) => ({ - copy: { - marginTop: theme.spacing(), - }, -})); - -export const BucketLanding = (props: Props) => { - const { isCreateBucketDrawerOpen } = props; - const { data: profile } = useProfile(); - - const isRestrictedUser = profile?.restricted; - - const { - data: objectStorageBucketsResponse, - error: bucketsErrors, - isLoading: areBucketsLoading, - } = useObjectStorageBuckets(); - - const { mutateAsync: deleteBucket } = useDeleteBucketMutation(); - - const { classes } = useStyles(); - - const removeBucketConfirmationDialog = useOpenClose(); - const [isLoading, setIsLoading] = React.useState(false); - const [error, setError] = React.useState(undefined); - const [bucketDetailDrawerOpen, setBucketDetailDrawerOpen] = - React.useState(false); - const [selectedBucket, setSelectedBucket] = React.useState< - ObjectStorageBucket | undefined - >(undefined); - - const handleClickDetails = (bucket: ObjectStorageBucket) => { - setBucketDetailDrawerOpen(true); - setSelectedBucket(bucket); - }; - - const closeBucketDetailDrawer = () => { - setBucketDetailDrawerOpen(false); - }; - - const handleClickRemove = (bucket: ObjectStorageBucket) => { - setSelectedBucket(bucket); - setError(undefined); - removeBucketConfirmationDialog.open(); - }; - - const removeBucket = () => { - // This shouldn't happen, but just in case (and to get TS to quit complaining...) - if (!selectedBucket) { - return; - } - - setError(undefined); - setIsLoading(true); - - const { cluster, label } = selectedBucket; - - deleteBucket({ cluster, label }) - .then(() => { - removeBucketConfirmationDialog.close(); - setIsLoading(false); - - // @analytics - sendDeleteBucketEvent(cluster); - }) - .catch((e) => { - // @analytics - sendDeleteBucketFailedEvent(cluster); - - setIsLoading(false); - setError(e); - }); - }; - - const { - handleOrderChange, - order, - orderBy, - sortedData: orderedData, - } = useOrderV2({ - data: objectStorageBucketsResponse?.buckets, - initialRoute: { - defaultOrder: { - order: 'asc', - orderBy: 'label', - }, - from: '/object-storage/buckets', - }, - preferenceKey: 'object-storage-buckets', - }); - - const closeRemoveBucketConfirmationDialog = React.useCallback(() => { - removeBucketConfirmationDialog.close(); - }, [removeBucketConfirmationDialog]); - - const unavailableClusters = - objectStorageBucketsResponse?.errors.map((error) => - isBucketError(error) ? error.cluster : error.endpoint - ) || []; - - if (isRestrictedUser) { - return ; - } - - if (bucketsErrors) { - return ( - - ); - } - - if (areBucketsLoading || objectStorageBucketsResponse === undefined) { - return ; - } - - if (objectStorageBucketsResponse?.buckets.length === 0) { - return ( - <> - {unavailableClusters.length > 0 && ( - - )} - - - ); - } - - const totalUsage = sumBucketUsage(objectStorageBucketsResponse.buckets); - const bucketLabel = selectedBucket ? selectedBucket.label : ''; - - return ( - - - {unavailableClusters.length > 0 && ( - - )} - - - {/* If there's more than one Bucket, display the total usage. */} - {objectStorageBucketsResponse.buckets.length > 1 ? ( - - Total storage used: {readableBytes(totalUsage).formatted} - - ) : null} - 1 ? 8 : 18} - /> - - - - - Warning: Deleting a bucket is permanent and - can’t be undone. - - - - A bucket must be empty before deleting it. Please{' '} - - delete all objects - - , or use{' '} - - another tool - {' '} - to force deletion. - - {/* If the user is attempting to delete their last Bucket, remind them - that they will still be billed unless they cancel Object Storage in - Account Settings. */} - {objectStorageBucketsResponse?.buckets.length === 1 && ( - - )} - - - - ); -}; - -const RenderEmpty = () => { - return ; -}; - -interface UnavailableClustersDisplayProps { - unavailableClusters: (ObjectStorageCluster | ObjectStorageEndpoint)[]; -} - -const UnavailableClustersDisplay = React.memo( - ({ unavailableClusters }: UnavailableClustersDisplayProps) => { - const { data: regions } = useRegionsQuery(); - - const regionsAffected = unavailableClusters.map( - (cluster) => - regions?.find((region) => region.id === cluster.region)?.label ?? - cluster.region - ); - - return ; - } -); - -interface BannerProps { - regionsAffected: string[]; -} - -const Banner = React.memo(({ regionsAffected }: BannerProps) => { - const moreThanOneRegionAffected = regionsAffected.length > 1; - - return ( - - - There was an error loading buckets in{' '} - {moreThanOneRegionAffected - ? 'the following regions:' - : `${regionsAffected[0]}.`} -
    - {moreThanOneRegionAffected && - regionsAffected.map((thisRegion) => ( -
  • {thisRegion}
  • - ))} -
- If you have buckets in{' '} - {moreThanOneRegionAffected ? 'these regions' : regionsAffected[0]}, you - may not see them listed below. -
-
- ); -}); - -export const sumBucketUsage = (buckets: ObjectStorageBucket[]) => { - return buckets.reduce((acc, thisBucket) => { - acc += thisBucket.size; - return acc; - }, 0); -}; diff --git a/packages/manager/src/features/ObjectStorage/ObjectStorageLanding.tsx b/packages/manager/src/features/ObjectStorage/ObjectStorageLanding.tsx index e54bd60784a..00dd326b68f 100644 --- a/packages/manager/src/features/ObjectStorage/ObjectStorageLanding.tsx +++ b/packages/manager/src/features/ObjectStorage/ObjectStorageLanding.tsx @@ -30,11 +30,6 @@ const SummaryLanding = React.lazy(() => default: module.SummaryLanding, })) ); -const BucketLanding = React.lazy(() => - import('./BucketLanding/BucketLanding').then((module) => ({ - default: module.BucketLanding, - })) -); const AccessKeyLanding = React.lazy(() => import('./AccessKeyLanding/AccessKeyLanding').then((module) => ({ default: module.AccessKeyLanding, @@ -179,13 +174,9 @@ export const ObjectStorageLanding = () => { )} - {isObjMultiClusterEnabled ? ( - - ) : ( - - )} + enabled, }); -export const useObjectStorageBuckets = (enabled = true) => { +export const useObjectStorageBuckets = (enabled: boolean = true) => { const flags = useFlags(); - const { data: account } = useAccount(); - const { data: allRegions } = useRegionsQuery(); - - const isObjMultiClusterEnabled = isFeatureEnabledV2( - 'Object Storage Access Key Regions', - Boolean(flags.objMultiCluster), - account?.capabilities ?? [] + const { data: account, isLoading: accountIsLoading } = useAccount(enabled); + + // TODO: always use regions query once dynamic Object Storage capability resolution is enabled + const isObjectStorageGen2Enabled = + account === undefined + ? undefined + : isFeatureEnabledV2( + 'Object Storage Endpoint Types', + Boolean(flags.objectStorageGen2?.enabled), + account.capabilities ?? [] + ); + const endpointsQueryEnabled = enabled && isObjectStorageGen2Enabled === true; + const regionsQueryEnabled = enabled && isObjectStorageGen2Enabled === false; + + const { data: allRegions, isLoading: regionsAreLoading } = + useRegionsQuery(regionsQueryEnabled); + const objRegions = allRegions?.filter((r) => + r.capabilities.includes('Object Storage') ); - const isObjectStorageGen2Enabled = isFeatureEnabledV2( - 'Object Storage Endpoint Types', - Boolean(flags.objectStorageGen2?.enabled), - account?.capabilities ?? [] - ); - - const endpointsQueryEnabled = enabled && isObjectStorageGen2Enabled; - const clustersQueryEnabled = enabled && !isObjMultiClusterEnabled; - // Endpoints contain all the regions that support Object Storage. - const { data: endpoints } = useObjectStorageEndpoints(endpointsQueryEnabled); - const { data: clusters } = useObjectStorageClusters(clustersQueryEnabled); - - const regions = - isObjMultiClusterEnabled && !isObjectStorageGen2Enabled - ? allRegions?.filter((r) => r.capabilities.includes('Object Storage')) - : undefined; - - const queryEnabled = - enabled && - ((isObjectStorageGen2Enabled && Boolean(endpoints)) || - (isObjMultiClusterEnabled && Boolean(regions)) || - Boolean(clusters)); + const { data: endpoints, isLoading: endpointsAreLoading } = + useObjectStorageEndpoints(endpointsQueryEnabled); - const queryFn = isObjectStorageGen2Enabled + const bucketsQueryEnabled = + (endpointsQueryEnabled && Boolean(endpoints)) || + (regionsQueryEnabled && Boolean(objRegions)); + const queryFn = endpointsQueryEnabled ? () => getAllBucketsFromEndpoints(endpoints) - : isObjMultiClusterEnabled - ? () => getAllBucketsFromRegions(regions) - : () => getAllBucketsFromClusters(clusters); + : () => getAllBucketsFromRegions(objRegions); - return useQuery>({ - enabled: queryEnabled, + const dependencyIsLoading = + accountIsLoading || regionsAreLoading || endpointsAreLoading; + + const bucketsQuery = useQuery< + BucketsResponseType + >({ + enabled: bucketsQueryEnabled, queryFn, queryKey: objectStorageQueries.buckets.queryKey, retry: false, }); + return { + ...bucketsQuery, + isLoading: bucketsQuery.isLoading || dependencyIsLoading, + }; }; export const useObjectStorageAccessKeys = (params: Params) => diff --git a/packages/manager/src/queries/object-storage/requests.ts b/packages/manager/src/queries/object-storage/requests.ts index fe5074fb622..4d9170f4c9c 100644 --- a/packages/manager/src/queries/object-storage/requests.ts +++ b/packages/manager/src/queries/object-storage/requests.ts @@ -1,6 +1,5 @@ import { getBuckets, - getBucketsInCluster, getBucketsInRegion, getClusters, getObjectStorageEndpoints, @@ -40,9 +39,8 @@ export const getAllObjectStorageEndpoints = () => * @deprecated This type will be deprecated and removed when OBJ Gen2 is in GA. */ export interface BucketError { - cluster: ObjectStorageCluster; error: APIError[]; - region?: Region; + region: Region; } /** @@ -74,47 +72,9 @@ export type BucketsResponseType = T extends true export function isBucketError( error: BucketError | BucketErrorGen2 ): error is BucketError { - return (error as BucketError).cluster !== undefined; + return (error as BucketError).region !== undefined; } -/** - * @deprecated This function is deprecated and will be removed in the future. - */ -export const getAllBucketsFromClusters = async ( - clusters: ObjectStorageCluster[] | undefined -) => { - if (clusters === undefined) { - return { buckets: [], errors: [] } as BucketsResponse; - } - - const promises = clusters.map((cluster) => - getAll((params) => - getBucketsInCluster(cluster.id, params) - )() - .then((data) => data.data) - .catch((error) => ({ - cluster, - error, - })) - ); - - const data = await Promise.all(promises); - - const bucketsPerCluster = data.filter((item) => - Array.isArray(item) - ) as ObjectStorageBucket[][]; - - const buckets = bucketsPerCluster.reduce((acc, val) => acc.concat(val), []); - - const errors = data.filter((item) => !Array.isArray(item)) as BucketError[]; - - if (errors.length === clusters.length) { - throw new Error('Unable to get Object Storage buckets.'); - } - - return { buckets, errors } as BucketsResponse; -}; - /** * @deprecated This function is deprecated and will be removed in the future. */ diff --git a/packages/queries/src/regions/regions.ts b/packages/queries/src/regions/regions.ts index 0a735b36aaf..2752869ef11 100644 --- a/packages/queries/src/regions/regions.ts +++ b/packages/queries/src/regions/regions.ts @@ -76,10 +76,11 @@ export const useRegionQuery = (regionId: string) => { }); }; -export const useRegionsQuery = () => +export const useRegionsQuery = (enabled: boolean = true) => useQuery({ ...regionQueries.regions, ...queryPresets.longLived, + enabled, select: (regions: Region[]) => regions.map((region) => ({ ...region,