Skip to content

Commit 30026cf

Browse files
feat: [M3-10361] - Use Search v2 to power the Volumes Landing page (linode#12553)
* use search v2 * Added changeset: Improved search to the Volumes landing page * make search field clearable * update cypress test to use correct selector for search field component * gracefully handle errors when searching and filter by tags by default --------- Co-authored-by: Banks Nussman <[email protected]>
1 parent b12096d commit 30026cf

File tree

3 files changed

+48
-63
lines changed

3 files changed

+48
-63
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@linode/manager": Added
3+
---
4+
5+
Improved search to the Volumes landing page ([#12553](https://github.com/linode/manager/pull/12553))

packages/manager/cypress/e2e/core/volumes/search-volumes.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ describe('Search Volumes', () => {
5050
cy.findByText(volume2.label).should('not.exist');
5151

5252
// Clear search, confirm both volumes are shown.
53-
cy.findByTestId('clear-volumes-search').click();
53+
cy.findByLabelText('Clear').click();
5454
cy.findByText(volume1.label).should('be.visible');
5555
cy.findByText(volume2.label).should('be.visible');
5656

packages/manager/src/features/Volumes/VolumesLanding.tsx

Lines changed: 42 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,10 @@
11
import { useVolumeQuery, useVolumesQuery } from '@linode/queries';
2-
import {
3-
CircleProgress,
4-
CloseIcon,
5-
ErrorState,
6-
IconButton,
7-
InputAdornment,
8-
TextField,
9-
} from '@linode/ui';
2+
import { getAPIFilterFromQuery } from '@linode/search';
3+
import { CircleProgress, ErrorState, Stack } from '@linode/ui';
104
import { useNavigate, useParams, useSearch } from '@tanstack/react-router';
11-
import * as React from 'react';
12-
import { debounce } from 'throttle-debounce';
5+
import React from 'react';
136

7+
import { DebouncedSearchTextField } from 'src/components/DebouncedSearchTextField';
148
import { DocumentTitleSegment } from 'src/components/DocumentTitle';
159
import { useIsBlockStorageEncryptionFeatureEnabled } from 'src/components/Encryption/utils';
1610
import { LandingHeader } from 'src/components/LandingHeader';
@@ -21,6 +15,7 @@ import { TableCell } from 'src/components/TableCell';
2115
import { TableHead } from 'src/components/TableHead';
2216
import { TableRow } from 'src/components/TableRow';
2317
import { TableRowEmpty } from 'src/components/TableRowEmpty/TableRowEmpty';
18+
import { TableRowError } from 'src/components/TableRowError/TableRowError';
2419
import { TableSortCell } from 'src/components/TableSortCell';
2520
import { getRestrictedResourceText } from 'src/features/Account/utils';
2621
import { useOrderV2 } from 'src/hooks/useOrderV2';
@@ -46,31 +41,27 @@ import { VolumesLandingEmptyState } from './VolumesLandingEmptyState';
4641
import { VolumeTableRow } from './VolumeTableRow';
4742

4843
import type { Filter, Volume } from '@linode/api-v4';
49-
import type {
50-
VolumeAction,
51-
VolumesSearchParams,
52-
} from 'src/routes/volumes/index';
44+
import type { VolumeAction } from 'src/routes/volumes/index';
5345

5446
export const VolumesLanding = () => {
5547
const navigate = useNavigate();
5648
const params = useParams({ strict: false });
57-
const search: VolumesSearchParams = useSearch({
58-
from: '/volumes',
49+
const search = useSearch({
50+
from: '/volumes/',
51+
shouldThrow: false,
5952
});
6053
const pagination = usePaginationV2({
6154
currentRoute: '/volumes',
6255
preferenceKey: VOLUME_TABLE_PREFERENCE_KEY,
6356
searchParams: (prev) => ({
6457
...prev,
65-
query: search.query,
58+
query: search?.query,
6659
}),
6760
});
6861
const isVolumeCreationRestricted = useRestrictedGlobalGrantCheck({
6962
globalGrantType: 'add_volumes',
7063
});
7164

72-
const { query } = search;
73-
7465
const { handleOrderChange, order, orderBy } = useOrderV2({
7566
initialRoute: {
7667
defaultOrder: {
@@ -82,12 +73,17 @@ export const VolumesLanding = () => {
8273
preferenceKey: VOLUME_TABLE_PREFERENCE_KEY,
8374
});
8475

76+
const { filter: searchFilter, error: searchError } = getAPIFilterFromQuery(
77+
search?.query,
78+
{
79+
searchableFieldsWithoutOperator: ['label', 'tags'],
80+
}
81+
);
82+
8583
const filter: Filter = {
8684
['+order']: order,
8785
['+order_by']: orderBy,
88-
...(query && {
89-
label: { '+contains': query },
90-
}),
86+
...searchFilter,
9187
};
9288

9389
const {
@@ -120,22 +116,12 @@ export const VolumesLanding = () => {
120116
});
121117
};
122118

123-
const resetSearch = () => {
124-
navigate({
125-
search: (prev) => ({
126-
...prev,
127-
query: undefined,
128-
}),
129-
to: '/volumes',
130-
});
131-
};
132-
133-
const onSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
119+
const onSearch = (query: string) => {
134120
navigate({
135121
search: (prev) => ({
136122
...prev,
137123
page: undefined,
138-
query: e.target.value || undefined,
124+
query: query ? query : undefined,
139125
}),
140126
to: '/volumes',
141127
});
@@ -148,11 +134,13 @@ export const VolumesLanding = () => {
148134
});
149135
};
150136

137+
const numberOfColumns = isBlockStorageEncryptionFeatureEnabled ? 7 : 6;
138+
151139
if (isLoading) {
152140
return <CircleProgress />;
153141
}
154142

155-
if (error) {
143+
if (error && !search?.query) {
156144
return (
157145
<ErrorState
158146
errorText={
@@ -162,12 +150,12 @@ export const VolumesLanding = () => {
162150
);
163151
}
164152

165-
if (volumes?.results === 0 && !query) {
153+
if (volumes?.results === 0 && !search?.query) {
166154
return <VolumesLandingEmptyState />;
167155
}
168156

169157
return (
170-
<>
158+
<Stack spacing={2}>
171159
<DocumentTitleSegment segment="Volumes" />
172160
<LandingHeader
173161
breadcrumbProps={{
@@ -185,34 +173,17 @@ export const VolumesLanding = () => {
185173
docsLink="https://techdocs.akamai.com/cloud-computing/docs/block-storage"
186174
entity="Volume"
187175
onButtonClick={() => navigate({ to: '/volumes/create' })}
188-
spacingBottom={16}
189176
title="Volumes"
190177
/>
191-
<TextField
178+
<DebouncedSearchTextField
179+
clearable
180+
errorText={searchError?.message}
192181
hideLabel
193-
InputProps={{
194-
endAdornment: query && (
195-
<InputAdornment position="end">
196-
{isFetching && <CircleProgress size="sm" />}
197-
198-
<IconButton
199-
aria-label="Clear"
200-
data-testid="clear-volumes-search"
201-
onClick={resetSearch}
202-
size="small"
203-
>
204-
<CloseIcon />
205-
</IconButton>
206-
</InputAdornment>
207-
),
208-
sx: { mb: 3 },
209-
}}
182+
isSearching={isFetching}
210183
label="Search"
211-
onChange={debounce(400, (e) => {
212-
onSearch(e);
213-
})}
184+
onSearch={onSearch}
214185
placeholder="Search Volumes"
215-
value={query ?? ''}
186+
value={search?.query ?? ''}
216187
/>
217188
<Table>
218189
<TableHead>
@@ -250,8 +221,17 @@ export const VolumesLanding = () => {
250221
</TableRow>
251222
</TableHead>
252223
<TableBody>
224+
{search?.query && error && (
225+
<TableRowError
226+
colSpan={numberOfColumns}
227+
message={error[0].reason}
228+
/>
229+
)}
253230
{volumes?.data.length === 0 && (
254-
<TableRowEmpty colSpan={6} message="No volume found" />
231+
<TableRowEmpty
232+
colSpan={numberOfColumns}
233+
message="No volume found"
234+
/>
255235
)}
256236
{volumes?.data.map((volume) => (
257237
<VolumeTableRow
@@ -346,6 +326,6 @@ export const VolumesLanding = () => {
346326
volume={selectedVolume}
347327
volumeError={selectedVolumeError}
348328
/>
349-
</>
329+
</Stack>
350330
);
351331
};

0 commit comments

Comments
 (0)