Skip to content

Commit 39e368b

Browse files
committed
fix(locations): surface account locations backed only by newly created buckets
1 parent 4f118bf commit 39e368b

3 files changed

Lines changed: 103 additions & 24 deletions

File tree

src/react/next-architecture/domain/business/locations.test.tsx

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
11
import { ShellHooksProvider } from '@scality/module-federation';
22
import { act, renderHook } from '@testing-library/react-hooks';
3+
import { rest } from 'msw';
4+
import { setupServer } from 'msw/node';
35
import type { PropsWithChildren } from 'react';
46
import { QueryClient } from 'react-query';
57
import { QueryClientProvider } from '../../../../QueryClientProvider';
68
import type { LocationTypeKey } from '../../../../types/config';
79
import * as DSRProvider from '../../../DataServiceRoleProvider';
8-
import { mockShellAlerts, mockShellHooks, WrapperAsStorageManager } from '../../../utils/testUtil';
10+
import { _ManagementContext } from '../../../ManagementProvider';
11+
import {
12+
mockShellAlerts,
13+
mockShellHooks,
14+
TEST_API_BASE_URL,
15+
TEST_MANAGEMENT_CLIENT,
16+
WrapperAsStorageManager,
17+
} from '../../../utils/testUtil';
918
import { MockedAccountsLocationsAdapter } from '../../adapters/accounts-locations/MockedAccountsLocationsAdapter';
1019
import {
1120
ACCOUNT_OWN_METRICS,
@@ -45,11 +54,23 @@ const queryClient = new QueryClient({
4554
},
4655
},
4756
});
57+
const server = setupServer(
58+
rest.get(`${TEST_API_BASE_URL}/api/v1/instance/:instanceId/status`, (_req, res, ctx) => res(ctx.json({}))),
59+
);
60+
61+
beforeAll(() => server.listen({ onUnhandledRequest: 'bypass' }));
62+
afterEach(() => server.resetHandlers());
63+
afterAll(() => server.close());
64+
4865
const Wrapper = ({ children }: PropsWithChildren<Record<string, never>>) => {
4966
return (
5067
<WrapperAsStorageManager isStorageManager={true}>
5168
<ShellHooksProvider shellHooks={mockShellHooks} shellAlerts={mockShellAlerts}>
52-
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
69+
<QueryClientProvider client={queryClient}>
70+
<_ManagementContext.Provider value={{ managementClient: TEST_MANAGEMENT_CLIENT }}>
71+
{children}
72+
</_ManagementContext.Provider>
73+
</QueryClientProvider>
5374
</ShellHooksProvider>
5475
</WrapperAsStorageManager>
5576
);
@@ -431,4 +452,61 @@ describe('useListLocationsForCurrentAccount', () => {
431452
};
432453
expect(result.current).toStrictEqual(expectedRes);
433454
});
455+
456+
it('shows a location backed only by a freshly created bucket, resolving the bucket location name to its id', async () => {
457+
// S
458+
jest.spyOn(DSRProvider, 'useCurrentAccount').mockReturnValue({
459+
account: {
460+
id: 'account-id-bucket-only',
461+
Name: 'BucketOnly',
462+
Roles: [],
463+
CreationDate: DEFAULT_METRICS_MESURED_ON,
464+
CanonicalId: 'canonical-id-bucket-only',
465+
},
466+
});
467+
// The account has no location metrics yet, but owns a bucket on `us-east-1`
468+
// whose location id (95dbedf5-...) differs from its name.
469+
server.use(
470+
rest.get(`${TEST_API_BASE_URL}/api/v1/instance/:instanceId/status`, (_req, res, ctx) =>
471+
res(
472+
ctx.json({
473+
metrics: {
474+
'item-counts': {
475+
bucketList: [
476+
{
477+
name: 'freshly-created-bucket',
478+
location: 'us-east-1',
479+
ownerCanonicalId: 'canonical-id-bucket-only',
480+
},
481+
],
482+
},
483+
},
484+
}),
485+
),
486+
),
487+
);
488+
489+
const { result, waitFor } = setupAndRenderHook();
490+
491+
// E
492+
await waitFor(() => {
493+
return result.current.locations.status === 'success';
494+
});
495+
496+
// V
497+
if (result.current.locations.status !== 'success') {
498+
throw new Error('expected locations to be successfully loaded');
499+
}
500+
const value = result.current.locations.value;
501+
// Keyed by the location id (objectId), not by the bucket location name.
502+
expect(Object.keys(value)).toEqual(['95dbedf5-9888-11ec-8565-1ac2af7d1e53']);
503+
const usEast1 = value['95dbedf5-9888-11ec-8565-1ac2af7d1e53'];
504+
expect(usEast1.name).toBe('us-east-1');
505+
expect(usEast1.usedCapacity.status).toBe('success');
506+
// A bucket-only location gets synthesized zero-usage capacity.
507+
expect(usEast1.usedCapacity).toMatchObject({
508+
status: 'success',
509+
value: { type: 'hasMetrics', usedCapacity: { current: 0, nonCurrent: 0 } },
510+
});
511+
});
434512
});

src/react/next-architecture/domain/business/locations.ts

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ export const useListLocationsForCurrentAccount = ({
171171

172172
const { bucketList, status: bucketListStatus } = useBucketList();
173173

174-
const bucketLocationIds = useMemo(() => {
174+
const bucketLocationNames = useMemo(() => {
175175
if (bucketListStatus === 'error') {
176176
return [];
177177
}
@@ -230,33 +230,34 @@ export const useListLocationsForCurrentAccount = ({
230230
};
231231
}
232232

233-
const accountLocationsKey = Array.from(
234-
new Set([...Object.keys(accountLocationData), ...bucketLocationIds]),
235-
);
236-
237233
if (allLocations.locations.status !== 'success') {
238234
return allLocations;
239235
}
240236

237+
const metricsByLocationId = accountLocationData ?? {};
238+
const bucketLocationNameSet = new Set(bucketLocationNames);
241239
const allLocationsValue = Object.values(allLocations.locations.value);
242240
const locations: Record<string, Location> = {};
243-
accountLocationsKey.forEach((locationId) => {
244-
const locationDefinition = allLocationsValue.find(
245-
(l) => l.id === locationId,
246-
);
247241

248-
if (locationDefinition) {
249-
const usedCapacityValue: LatestUsedCapacity =
250-
locationId in accountLocationData ? accountLocationData[locationId] : ZERO_USED_CAPACITY;
242+
allLocationsValue.forEach((locationDefinition) => {
243+
const hasMetrics = locationDefinition.id in metricsByLocationId;
244+
const isBackedByBucket = bucketLocationNameSet.has(locationDefinition.name);
251245

252-
locations[locationId] = {
253-
...locationDefinition,
254-
usedCapacity: {
255-
status: 'success',
256-
value: usedCapacityValue,
257-
},
258-
};
246+
if (!hasMetrics && !isBackedByBucket) {
247+
return;
259248
}
249+
250+
const usedCapacityValue: LatestUsedCapacity = hasMetrics
251+
? metricsByLocationId[locationDefinition.id]
252+
: ZERO_USED_CAPACITY;
253+
254+
locations[locationDefinition.id] = {
255+
...locationDefinition,
256+
usedCapacity: {
257+
status: 'success',
258+
value: usedCapacityValue,
259+
},
260+
};
260261
});
261262

262263
return {

src/react/queries/instanceStatusQuery.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { useMemo } from 'react';
33
import { useQuery } from 'react-query';
44
import type { InlineResponse200 } from '../../js/managementClient/api';
55
import type { ApiError } from '../../types/actions';
6-
import type { Capabilities } from '../../types/stats';
6+
import type { BucketList, Capabilities } from '../../types/stats';
77
import { notFalsyTypeGuard } from '../../types/typeGuards';
88
import { useErrorHandler } from '../ErrorProvider';
99
import { useManagementClient } from '../ManagementProvider';
@@ -52,9 +52,9 @@ export const useInstanceStatusQuery = (options?: InstanceStatusQueryOptions) =>
5252
*/
5353
export const useBucketList = () => {
5454
const { data, status, isFetching, error } = useInstanceStatusQuery();
55-
const rawBucketList = data?.metrics?.['item-counts']?.bucketList;
55+
const rawBucketList = data?.metrics?.['item-counts']?.bucketList as BucketList | undefined;
5656

57-
const bucketList = useMemo(() => {
57+
const bucketList = useMemo((): BucketList => {
5858
return rawBucketList || [];
5959
}, [JSON.stringify(rawBucketList)]);
6060

0 commit comments

Comments
 (0)