Skip to content

Commit 7d67d23

Browse files
new: STORIF-310 - Bucket tab filters created.
1 parent a791f04 commit 7d67d23

File tree

13 files changed

+434
-16
lines changed

13 files changed

+434
-16
lines changed

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

Lines changed: 158 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@
22
* @file End-to-end tests for Object Storage operations.
33
*/
44

5-
import { createBucket } from '@linode/api-v4/lib/object-storage';
5+
import {
6+
createBucket,
7+
ObjectStorageCluster,
8+
Region,
9+
} from '@linode/api-v4/lib/object-storage';
10+
import { getNewRegionLabel } from '@linode/utilities';
611
import { authenticate } from 'support/api/authentication';
712
import {
813
interceptGetNetworkUtilization,
@@ -16,6 +21,7 @@ import {
1621
interceptGetBuckets,
1722
interceptUpdateBucketAccess,
1823
} from 'support/intercepts/object-storage';
24+
import { interceptGetRegions } from 'support/intercepts/regions';
1925
import { ui } from 'support/ui';
2026
import { cleanUp } from 'support/util/cleanup';
2127
import { chooseCluster } from 'support/util/clusters';
@@ -58,6 +64,14 @@ const setUpBucket = (
5864
);
5965
};
6066

67+
const setupBuckets = (
68+
bucketsDetails: { cluster: ObjectStorageCluster; label: string }[]
69+
) => {
70+
return Promise.all(
71+
bucketsDetails.map(({ label, cluster }) => setUpBucket(label, cluster.id))
72+
);
73+
};
74+
6175
authenticate();
6276
beforeEach(() => {
6377
cy.tag('method:e2e');
@@ -78,12 +92,11 @@ describe('object storage end-to-end tests', () => {
7892
cy.tag('purpose:syntheticTesting');
7993
const bucketLabel = randomLabel();
8094
const bucketClusterObj = chooseCluster();
81-
const bucketCluster = bucketClusterObj.id;
8295
const bucketRegion = getRegionById(bucketClusterObj.region).label;
8396
const bucketHostname = `${bucketLabel}.${bucketClusterObj.domain}`;
8497
interceptGetBuckets().as('getBuckets');
8598
interceptCreateBucket().as('createBucket');
86-
interceptDeleteBucket(bucketLabel, bucketCluster).as('deleteBucket');
99+
interceptDeleteBucket().as('deleteBucket');
87100
interceptGetNetworkUtilization().as('getNetworkUtilization');
88101

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

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)