Skip to content

Commit bd33b56

Browse files
new: STORIF-310 - Bucket tab filters created.
1 parent 4fcc96c commit bd33b56

File tree

13 files changed

+438
-15
lines changed

13 files changed

+438
-15
lines changed

packages/manager/cypress/e2e/core/objectStorage/object-storage.e2e.spec.ts

Lines changed: 158 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
*/
44

55
import { createBucket } from '@linode/api-v4/lib/object-storage';
6+
import { getNewRegionLabel } from '@linode/utilities';
67
import { authenticate } from 'support/api/authentication';
78
import {
89
interceptGetNetworkUtilization,
@@ -16,6 +17,7 @@ import {
1617
interceptGetBuckets,
1718
interceptUpdateBucketAccess,
1819
} from 'support/intercepts/object-storage';
20+
import { interceptGetRegions } from 'support/intercepts/regions';
1921
import { ui } from 'support/ui';
2022
import { cleanUp } from 'support/util/cleanup';
2123
import { chooseCluster } from 'support/util/clusters';
@@ -27,6 +29,11 @@ import {
2729
createObjectStorageBucketFactoryLegacy,
2830
} from 'src/factories';
2931

32+
import type {
33+
ObjectStorageCluster,
34+
Region,
35+
} from '@linode/api-v4/lib/object-storage';
36+
3037
/**
3138
* Create a bucket with the given label and cluster.
3239
*
@@ -58,6 +65,14 @@ const setUpBucket = (
5865
);
5966
};
6067

68+
const setupBuckets = (
69+
bucketsDetails: { cluster: ObjectStorageCluster; label: string }[]
70+
) => {
71+
return Promise.all(
72+
bucketsDetails.map(({ label, cluster }) => setUpBucket(label, cluster.id))
73+
);
74+
};
75+
6176
authenticate();
6277
beforeEach(() => {
6378
cy.tag('method:e2e');
@@ -78,12 +93,11 @@ describe('object storage end-to-end tests', () => {
7893
cy.tag('purpose:syntheticTesting');
7994
const bucketLabel = randomLabel();
8095
const bucketClusterObj = chooseCluster();
81-
const bucketCluster = bucketClusterObj.id;
8296
const bucketRegion = getRegionById(bucketClusterObj.region).label;
8397
const bucketHostname = `${bucketLabel}.${bucketClusterObj.domain}`;
8498
interceptGetBuckets().as('getBuckets');
8599
interceptCreateBucket().as('createBucket');
86-
interceptDeleteBucket(bucketLabel, bucketCluster).as('deleteBucket');
100+
interceptDeleteBucket().as('deleteBucket');
87101
interceptGetNetworkUtilization().as('getNetworkUtilization');
88102

89103
mockGetAccount(accountFactory.build({ capabilities: ['Object Storage'] }));
@@ -197,4 +211,146 @@ describe('object storage end-to-end tests', () => {
197211
cy.findByText('Bucket access updated successfully.');
198212
});
199213
});
214+
215+
/*
216+
* - Confirms that user can filter bucket list by region.
217+
*/
218+
it('can filter the list of buckets by region', () => {
219+
interceptGetBuckets().as('getBuckets');
220+
interceptGetRegions().as('getRegions');
221+
222+
const bucketsDetails = new Array(2).fill({}).map(() => ({
223+
label: randomLabel(),
224+
cluster: chooseCluster(),
225+
}));
226+
227+
cy.defer(
228+
() => setupBuckets(bucketsDetails),
229+
'creating Object Storage bucket'
230+
).then(() => {
231+
cy.visitWithLogin('/object-storage/buckets');
232+
cy.wait('@getBuckets');
233+
cy.wait('@getRegions').then(({ response }) => {
234+
const regions: Region[] = response?.body.data;
235+
236+
const selectedBucket = bucketsDetails[0];
237+
const selectedRegion = regions.find(
238+
(region) => region.id === selectedBucket.cluster.region
239+
);
240+
241+
const selectedRegionLabel = selectedRegion
242+
? getNewRegionLabel(selectedRegion)
243+
: '';
244+
245+
const regionSelect = ui.autocomplete
246+
.findByLabel('Region')
247+
.should('be.visible')
248+
.type(selectedRegionLabel);
249+
250+
ui.autocompletePopper
251+
.findByTitle(selectedRegionLabel, { exact: false })
252+
.should('be.visible')
253+
.click();
254+
255+
regionSelect.click();
256+
257+
cy.get('tbody').within(() => {
258+
cy.get('tr')
259+
.should('have.length', 1)
260+
.within(() => {
261+
cy.findByText(selectedBucket.label).should('be.visible');
262+
});
263+
});
264+
});
265+
});
266+
});
267+
268+
/*
269+
* - Confirms that user can filter bucket list by endpoint.
270+
*/
271+
it('can filter the list of buckets by endpoint', () => {
272+
interceptGetBuckets().as('getBuckets');
273+
interceptGetRegions().as('getRegions');
274+
275+
const bucketsDetails = new Array(2).fill({}).map(() => ({
276+
label: randomLabel(),
277+
cluster: chooseCluster(),
278+
}));
279+
280+
cy.defer(
281+
() => setupBuckets(bucketsDetails),
282+
'creating Object Storage bucket'
283+
).then(() => {
284+
cy.visitWithLogin('/object-storage/buckets');
285+
cy.wait(['@getBuckets', '@getRegions']);
286+
287+
const selectedBucket = bucketsDetails[0];
288+
const selectedEndpoint = selectedBucket.cluster.id;
289+
const endpointSelect = ui.autocomplete.findByLabel('Endpoint');
290+
endpointSelect.should('be.visible').type(selectedEndpoint);
291+
ui.autocompletePopper
292+
.findByTitle(selectedEndpoint, { exact: false })
293+
.should('be.visible')
294+
.click();
295+
endpointSelect.click();
296+
297+
cy.get('tbody').within(() => {
298+
cy.get('tr')
299+
.should('have.length', 1)
300+
.within(() => {
301+
cy.findByText(selectedBucket.label).should('be.visible');
302+
});
303+
});
304+
});
305+
});
306+
307+
/*
308+
* - Confirms that when region is selected, endpoint multiselect.
309+
* shows only endpoints related to the selected region.
310+
*/
311+
it('should filter list of endpoinds when region is selected', () => {
312+
interceptGetBuckets().as('getBuckets');
313+
interceptGetRegions().as('getRegions');
314+
315+
const bucketsDetails = new Array(2).fill({}).map(() => ({
316+
label: randomLabel(),
317+
cluster: chooseCluster(),
318+
}));
319+
320+
cy.defer(
321+
() => setupBuckets(bucketsDetails),
322+
'creating Object Storage bucket'
323+
).then(() => {
324+
cy.visitWithLogin('/object-storage/buckets');
325+
cy.wait('@getBuckets');
326+
cy.wait('@getRegions').then(({ response }) => {
327+
const regions: Region[] = response?.body.data;
328+
329+
const selectedBucket = bucketsDetails[0];
330+
const selectedRegion = regions.find(
331+
(region) => region.id === selectedBucket.cluster.region
332+
);
333+
334+
const selectedRegionLabel = selectedRegion
335+
? getNewRegionLabel(selectedRegion)
336+
: '';
337+
338+
const regionSelect = ui.autocomplete
339+
.findByLabel('Region')
340+
.should('be.visible')
341+
.type(selectedRegionLabel);
342+
ui.autocompletePopper
343+
.findByTitle(selectedRegionLabel, { exact: false })
344+
.should('be.visible')
345+
.click();
346+
regionSelect.click();
347+
348+
ui.autocomplete.findByLabel('Endpoint').should('be.visible').click();
349+
350+
ui.autocompletePopper
351+
.findByTitle(new RegExp('^.*-.*-.*\..*.'))
352+
.should('have.length', 1);
353+
});
354+
});
355+
});
200356
});

packages/manager/cypress/support/intercepts/object-storage.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ export const interceptDeleteBucket = (
197197
apiMatcher(`object-storage/buckets/${cluster}/*`)
198198
);
199199
}
200-
return cy.intercept('DELETE', apiMatcher('object-storage/buckets/*'));
200+
return cy.intercept('DELETE', apiMatcher('object-storage/buckets/**/*'));
201201
};
202202

203203
/**

packages/manager/cypress/support/intercepts/regions.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,15 @@ import { makeResponse } from 'support/util/response';
1414
import type { Region, RegionAvailability } from '@linode/api-v4';
1515
import type { ExtendedRegion } from 'support/util/regions';
1616

17+
/**
18+
* Intercepts GET regions request.
19+
*
20+
* @returns Cypress chainable.
21+
*/
22+
export const interceptGetRegions = (): Cypress.Chainable<null> => {
23+
return cy.intercept('GET', apiMatcher('regions*'));
24+
};
25+
1726
/**
1827
* Intercepts GET request to fetch Linode regions and mocks response.
1928
*

packages/manager/src/features/ObjectStorage/BucketLanding/OMC_BucketLanding.tsx

Lines changed: 90 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import { useProfile } from '@linode/queries';
2-
import { CircleProgress, ErrorState, Notice, Typography } from '@linode/ui';
2+
import {
3+
Box,
4+
CircleProgress,
5+
ErrorState,
6+
Notice,
7+
Typography,
8+
} from '@linode/ui';
39
import { readableBytes, useOpenClose } from '@linode/utilities';
410
import Grid from '@mui/material/Grid';
511
import * as React from 'react';
@@ -21,10 +27,15 @@ import {
2127
} from 'src/utilities/analytics/customEventAnalytics';
2228

2329
import { CancelNotice } from '../CancelNotice';
30+
import { EndpointMultiselect } from '../Partials/EndpointMultiselect';
31+
import { RegionMultiselect } from '../Partials/RegionMultiselect';
32+
import { uniqueByKey } from '../utilities';
2433
import { BucketDetailsDrawer } from './BucketDetailsDrawer';
2534
import { BucketLandingEmptyState } from './BucketLandingEmptyState';
2635
import { BucketTable } from './BucketTable';
2736

37+
import type { EndpointMultiselectValue } from '../Partials/EndpointMultiselect';
38+
import type { RegionMultiselectValue } from '../Partials/RegionMultiselect';
2839
import type { APIError, ObjectStorageBucket } from '@linode/api-v4';
2940
import type { Theme } from '@mui/material/styles';
3041

@@ -62,6 +73,14 @@ export const OMC_BucketLanding = (props: Props) => {
6273
const [bucketDetailDrawerOpen, setBucketDetailDrawerOpen] =
6374
React.useState<boolean>(false);
6475

76+
const [selectedRegions, setSelectedRegions] = React.useState<
77+
RegionMultiselectValue[]
78+
>([]);
79+
80+
const [selectedEndpoints, setSelectedEndpoints] = React.useState<
81+
EndpointMultiselectValue[]
82+
>([]);
83+
6584
const [selectedBucket, setSelectedBucket] = React.useState<
6685
ObjectStorageBucket | undefined
6786
>(undefined);
@@ -144,6 +163,25 @@ export const OMC_BucketLanding = (props: Props) => {
144163
const totalUsage = sumBucketUsage(buckets);
145164
const bucketLabel = selectedBucket ? selectedBucket.label : '';
146165

166+
const endpointOptions = React.useMemo(
167+
() =>
168+
uniqueByKey(
169+
buckets.map((bucket) => ({
170+
label: bucket.s3_endpoint,
171+
})),
172+
'label'
173+
).filter((option) => {
174+
if (selectedRegions.length) {
175+
return selectedRegions.some((region) =>
176+
option.label.includes(region.value)
177+
);
178+
}
179+
180+
return true;
181+
}) as EndpointMultiselectValue[],
182+
[buckets, selectedRegions]
183+
);
184+
147185
const {
148186
handleOrderChange,
149187
order,
@@ -161,6 +199,20 @@ export const OMC_BucketLanding = (props: Props) => {
161199
preferenceKey: 'object-storage-buckets',
162200
});
163201

202+
const filteredData = orderedData?.filter((bucket) => {
203+
if (selectedEndpoints.length) {
204+
return selectedEndpoints.some(
205+
(endpoint) => bucket.s3_endpoint === endpoint.label
206+
);
207+
}
208+
209+
if (selectedRegions.length) {
210+
return selectedRegions.some((region) => bucket.region === region.value);
211+
}
212+
213+
return true;
214+
});
215+
164216
if (isRestrictedUser) {
165217
return <RenderEmpty />;
166218
}
@@ -190,16 +242,48 @@ export const OMC_BucketLanding = (props: Props) => {
190242
}
191243

192244
return (
193-
<React.Fragment>
245+
<>
194246
<DocumentTitleSegment
195247
segment={`${isCreateBucketDrawerOpen ? 'Create a Bucket' : 'Buckets'}`}
196248
/>
249+
197250
{unavailableRegionLabels && unavailableRegionLabels.length > 0 && (
198251
<UnavailableRegionsDisplay regionLabels={unavailableRegionLabels} />
199252
)}
253+
254+
<Typography gutterBottom variant="h3">
255+
Filter by
256+
</Typography>
257+
258+
<Box
259+
sx={(theme) => ({
260+
display: 'flex',
261+
gap: theme.spacingFunction(16),
262+
marginBottom: theme.spacingFunction(16),
263+
})}
264+
>
265+
<RegionMultiselect
266+
filterFn={(option) =>
267+
buckets.some((bucket) => bucket.region === option.value)
268+
}
269+
onChange={setSelectedRegions}
270+
showLabel={true}
271+
sx={{ flex: 1 }}
272+
values={selectedRegions}
273+
/>
274+
275+
<EndpointMultiselect
276+
onChange={setSelectedEndpoints}
277+
options={endpointOptions}
278+
showLabel={true}
279+
sx={{ flex: 1 }}
280+
values={selectedEndpoints}
281+
/>
282+
</Box>
283+
200284
<Grid size={12}>
201285
<BucketTable
202-
data={orderedData ?? []}
286+
data={filteredData ?? []}
203287
handleClickDetails={handleClickDetails}
204288
handleClickRemove={handleClickRemove}
205289
handleOrderChange={handleOrderChange}
@@ -217,6 +301,7 @@ export const OMC_BucketLanding = (props: Props) => {
217301
) : null}
218302
<TransferDisplay spacingTop={buckets.length > 1 ? 8 : 18} />
219303
</Grid>
304+
220305
<TypeToConfirmDialog
221306
entity={{
222307
action: 'deletion',
@@ -256,12 +341,13 @@ export const OMC_BucketLanding = (props: Props) => {
256341
Account Settings. */}
257342
{buckets.length === 1 && <CancelNotice className={classes.copy} />}
258343
</TypeToConfirmDialog>
344+
259345
<BucketDetailsDrawer
260346
onClose={closeBucketDetailDrawer}
261347
open={bucketDetailDrawerOpen}
262348
selectedBucket={selectedBucket}
263349
/>
264-
</React.Fragment>
350+
</>
265351
);
266352
};
267353

0 commit comments

Comments
 (0)