Skip to content

Commit 7a8417c

Browse files
upcoming: [DPS-34193] - Add search and select to streams and destinations table (linode#12679)
1 parent 844abae commit 7a8417c

File tree

10 files changed

+267
-38
lines changed

10 files changed

+267
-38
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@linode/manager": Upcoming Features
3+
---
4+
5+
Add search and select inputs for Streams table. Add search input for Desitnations table ([#12679](https://github.com/linode/manager/pull/12679))

packages/manager/src/features/DataStream/Destinations/DestinationsLanding.test.tsx

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { renderWithTheme } from 'src/utilities/testHelpers';
1010
const loadingTestId = 'circle-progress';
1111

1212
describe('Destinations Landing Table', () => {
13-
it('should render destinations landing table with items PaginationFooter', async () => {
13+
it('should render destinations landing tab header and table with items PaginationFooter', async () => {
1414
server.use(
1515
http.get('*/monitor/streams/destinations', () => {
1616
return HttpResponse.json(
@@ -19,37 +19,47 @@ describe('Destinations Landing Table', () => {
1919
})
2020
);
2121

22-
const { getByText, queryByTestId, getByTestId } = renderWithTheme(
23-
<DestinationsLanding />
24-
);
22+
const { getByText, queryByTestId, getAllByTestId, getByPlaceholderText } =
23+
renderWithTheme(<DestinationsLanding />, {
24+
initialRoute: '/datastream/destinations',
25+
});
2526

2627
const loadingElement = queryByTestId(loadingTestId);
2728
if (loadingElement) {
2829
await waitForElementToBeRemoved(loadingElement);
2930
}
3031

32+
// search text input
33+
getByPlaceholderText('Search for a Destination');
34+
35+
// button
36+
getByText('Create Destination');
37+
3138
// Table column headers
3239
getByText('Name');
3340
getByText('Type');
3441
getByText('ID');
3542
getByText('Last Modified');
3643

3744
// PaginationFooter
38-
const paginationFooterSelectPageSizeInput = getByTestId(
45+
const paginationFooterSelectPageSizeInput = getAllByTestId(
3946
'textfield-input'
40-
) as HTMLInputElement;
47+
)[1] as HTMLInputElement;
4148
expect(paginationFooterSelectPageSizeInput.value).toBe('Show 25');
4249
});
4350

44-
it('should render images landing empty state', async () => {
51+
it('should render destinations landing empty state', async () => {
4552
server.use(
4653
http.get('*/monitor/streams/destinations', () => {
4754
return HttpResponse.json(makeResourcePage([]));
4855
})
4956
);
5057

5158
const { getByText, queryByTestId } = renderWithTheme(
52-
<DestinationsLanding />
59+
<DestinationsLanding />,
60+
{
61+
initialRoute: '/datastream/destinations',
62+
}
5363
);
5464

5565
const loadingElement = queryByTestId(loadingTestId);

packages/manager/src/features/DataStream/Destinations/DestinationsLanding.tsx

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { useDestinationsQuery } from '@linode/queries';
22
import { CircleProgress, ErrorState, Hidden } from '@linode/ui';
33
import { TableBody, TableHead, TableRow } from '@mui/material';
44
import Table from '@mui/material/Table';
5-
import { useNavigate } from '@tanstack/react-router';
5+
import { useNavigate, useSearch } from '@tanstack/react-router';
66
import * as React from 'react';
77

88
import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFooter';
@@ -20,9 +20,13 @@ import { usePaginationV2 } from 'src/hooks/usePaginationV2';
2020

2121
export const DestinationsLanding = () => {
2222
const navigate = useNavigate();
23-
23+
const destinationsUrl = '/datastream/destinations';
24+
const search = useSearch({
25+
from: destinationsUrl,
26+
shouldThrow: false,
27+
});
2428
const pagination = usePaginationV2({
25-
currentRoute: '/datastream/destinations',
29+
currentRoute: destinationsUrl,
2630
preferenceKey: DESTINATIONS_TABLE_PREFERENCE_KEY,
2731
});
2832

@@ -32,19 +36,23 @@ export const DestinationsLanding = () => {
3236
order: DESTINATIONS_TABLE_DEFAULT_ORDER,
3337
orderBy: DESTINATIONS_TABLE_DEFAULT_ORDER_BY,
3438
},
35-
from: '/datastream/destinations',
39+
from: destinationsUrl,
3640
},
3741
preferenceKey: `destinations-order`,
3842
});
3943

4044
const filter = {
4145
['+order']: order,
4246
['+order_by']: orderBy,
47+
...(search?.label !== undefined && {
48+
label: { '+contains': search?.label },
49+
}),
4350
};
4451

4552
const {
4653
data: destinations,
4754
isLoading,
55+
isFetching,
4856
error,
4957
} = useDestinationsQuery(
5058
{
@@ -54,6 +62,17 @@ export const DestinationsLanding = () => {
5462
filter
5563
);
5664

65+
const onSearch = (label: string) => {
66+
navigate({
67+
search: (prev) => ({
68+
...prev,
69+
page: undefined,
70+
label: label ? label : undefined,
71+
}),
72+
to: destinationsUrl,
73+
});
74+
};
75+
5776
const navigateToCreate = () => {
5877
navigate({ to: '/datastream/destinations/create' });
5978
};
@@ -78,8 +97,11 @@ export const DestinationsLanding = () => {
7897
<>
7998
<DataStreamTabHeader
8099
entity="Destination"
100+
isSearching={isFetching}
81101
loading={isLoading}
82102
onButtonClick={navigateToCreate}
103+
onSearch={onSearch}
104+
searchValue={search?.label ?? ''}
83105
/>
84106
<Table>
85107
<TableHead>

packages/manager/src/features/DataStream/Shared/DataStreamTabHeader/DataStreamTabHeader.test.tsx

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
import * as React from 'react';
22

33
import { DataStreamTabHeader } from 'src/features/DataStream/Shared/DataStreamTabHeader/DataStreamTabHeader';
4+
import { streamStatusOptions } from 'src/features/DataStream/Shared/types';
45
import { renderWithTheme } from 'src/utilities/testHelpers';
56

67
describe('DataStreamTabHeader', () => {
78
it('should render a create button', () => {
89
const { getByText } = renderWithTheme(
910
<DataStreamTabHeader entity="Stream" onButtonClick={() => null} />
1011
);
11-
expect(getByText('Create Stream')).toBeInTheDocument();
12+
13+
getByText('Create Stream');
1214
});
1315

1416
it('should render a disabled create button', () => {
@@ -25,4 +27,33 @@ describe('DataStreamTabHeader', () => {
2527
'true'
2628
);
2729
});
30+
31+
it('should render a search input', () => {
32+
const { getByPlaceholderText } = renderWithTheme(
33+
<DataStreamTabHeader
34+
entity="Stream"
35+
isSearching={false}
36+
onButtonClick={() => null}
37+
onSearch={() => null}
38+
searchValue={''}
39+
/>
40+
);
41+
42+
getByPlaceholderText('Search for a Stream');
43+
});
44+
45+
it('should render a select input', () => {
46+
const selectValue = streamStatusOptions[0].value;
47+
const { getByPlaceholderText, getByLabelText } = renderWithTheme(
48+
<DataStreamTabHeader
49+
entity="Stream"
50+
onSelect={() => null}
51+
selectList={streamStatusOptions}
52+
selectValue={selectValue}
53+
/>
54+
);
55+
56+
getByLabelText('Status');
57+
getByPlaceholderText('Select');
58+
});
2859
});

packages/manager/src/features/DataStream/Shared/DataStreamTabHeader/DataStreamTabHeader.tsx

Lines changed: 81 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,27 @@
1-
import { Button } from '@linode/ui';
1+
import { Autocomplete, Button } from '@linode/ui';
22
import Grid from '@mui/material/Grid';
33
import { styled, useTheme } from '@mui/material/styles';
44
import useMediaQuery from '@mui/material/useMediaQuery';
55
import * as React from 'react';
66

7+
import { DebouncedSearchTextField } from 'src/components/DebouncedSearchTextField';
8+
79
import type { Theme } from '@mui/material/styles';
10+
import type { LabelValueOption } from 'src/features/DataStream/Shared/types';
811

912
export interface DataStreamTabHeaderProps {
1013
buttonDataAttrs?: { [key: string]: boolean | string };
1114
createButtonText?: string;
1215
disabledCreateButton?: boolean;
1316
entity?: string;
17+
isSearching?: boolean;
1418
loading?: boolean;
1519
onButtonClick?: () => void;
20+
onSearch?: (label: string) => void;
21+
onSelect?: (status: string) => void;
22+
searchValue?: string;
23+
selectList?: LabelValueOption[];
24+
selectValue?: string;
1625
spacingBottom?: 0 | 4 | 16 | 24;
1726
}
1827

@@ -24,6 +33,12 @@ export const DataStreamTabHeader = ({
2433
loading,
2534
onButtonClick,
2635
spacingBottom = 24,
36+
isSearching,
37+
selectList,
38+
onSelect,
39+
selectValue,
40+
searchValue,
41+
onSearch,
2742
}: DataStreamTabHeaderProps) => {
2843
const theme = useTheme();
2944

@@ -35,6 +50,7 @@ export const DataStreamTabHeader = ({
3550
const customSmMdBetweenBreakpoint = useMediaQuery((theme: Theme) =>
3651
theme.breakpoints.between(customBreakpoint, 'md')
3752
);
53+
const searchLabel = `Search for a ${entity}`;
3854

3955
return (
4056
<StyledLandingHeaderGrid
@@ -53,23 +69,48 @@ export const DataStreamTabHeader = ({
5369
display: 'flex',
5470
flexWrap: xsDown ? 'wrap' : 'nowrap',
5571
gap: 3,
56-
justifyContent: 'flex-end',
72+
justifyContent:
73+
onSearch && searchValue !== undefined
74+
? 'space-between'
75+
: 'flex-end',
5776
flex: '1 1 auto',
5877

5978
marginLeft: customSmMdBetweenBreakpoint
6079
? theme.spacingFunction(16)
6180
: customXsDownBreakpoint
6281
? theme.spacingFunction(8)
6382
: undefined,
83+
marginRight: customSmMdBetweenBreakpoint
84+
? theme.spacingFunction(16)
85+
: customXsDownBreakpoint
86+
? theme.spacingFunction(8)
87+
: undefined,
6488
}}
6589
>
66-
{
67-
// @TODO (DPS-34192) Search input - both streams and destinations
68-
}
90+
{onSearch && searchValue !== undefined && (
91+
<DebouncedSearchTextField
92+
clearable
93+
hideLabel
94+
isSearching={isSearching}
95+
label={searchLabel}
96+
onSearch={onSearch}
97+
placeholder={searchLabel}
98+
value={searchValue}
99+
/>
100+
)}
69101
<StyledActions>
70-
{
71-
// @TODO (DPS-34193) Select status - only streams
72-
}
102+
{selectList && onSelect && (
103+
<Autocomplete
104+
label={'Status'}
105+
noMarginTop
106+
onChange={(_, option) => {
107+
onSelect(option?.value ?? '');
108+
}}
109+
options={selectList}
110+
placeholder="Select"
111+
value={selectList.find(({ value }) => value === selectValue)}
112+
/>
113+
)}
73114
{onButtonClick && (
74115
<Button
75116
buttonType="primary"
@@ -91,10 +132,42 @@ const StyledActions = styled('div')(({ theme }) => ({
91132
display: 'flex',
92133
gap: theme.spacingFunction(24),
93134
justifyContent: 'flex-end',
135+
marginLeft: 'auto',
136+
137+
'& .MuiAutocomplete-root > .MuiBox-root': {
138+
display: 'flex',
139+
140+
'& > .MuiBox-root': {
141+
margin: '0',
142+
143+
'& > .MuiInputLabel-root': {
144+
margin: 0,
145+
marginRight: theme.spacingFunction(12),
146+
},
147+
},
148+
},
94149
}));
95150

96151
const StyledLandingHeaderGrid = styled(Grid)(({ theme }) => ({
97152
'&:not(:first-of-type)': {
98153
marginTop: theme.spacingFunction(24),
99154
},
155+
156+
[theme.breakpoints.up('sm')]: {
157+
'& .MuiFormControl-fullWidth': {
158+
width: '180px',
159+
},
160+
},
161+
162+
[theme.breakpoints.up('md')]: {
163+
'& .MuiFormControl-fullWidth': {
164+
width: '235px',
165+
},
166+
},
167+
168+
[theme.breakpoints.up('lg')]: {
169+
'& .MuiFormControl-fullWidth': {
170+
width: '270px',
171+
},
172+
},
100173
}));

packages/manager/src/features/DataStream/Shared/types.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { destinationType } from '@linode/api-v4';
1+
import { destinationType, streamStatus } from '@linode/api-v4';
22

33
import type { CreateDestinationPayload } from '@linode/api-v4';
44

@@ -7,6 +7,11 @@ export interface DestinationTypeOption {
77
value: string;
88
}
99

10+
export interface LabelValueOption {
11+
label: string;
12+
value: string;
13+
}
14+
1015
export const destinationTypeOptions: DestinationTypeOption[] = [
1116
{
1217
value: destinationType.CustomHttps,
@@ -18,4 +23,15 @@ export const destinationTypeOptions: DestinationTypeOption[] = [
1823
},
1924
];
2025

26+
export const streamStatusOptions = [
27+
{
28+
value: streamStatus.Active,
29+
label: 'Enabled',
30+
},
31+
{
32+
value: streamStatus.Inactive,
33+
label: 'Disabled',
34+
},
35+
];
36+
2137
export type CreateDestinationForm = CreateDestinationPayload;

packages/manager/src/features/DataStream/Streams/StreamTableRow.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,9 @@ export const StreamTableRow = React.memo((props: StreamTableRowProps) => {
3636
const humanizeStreamStatus = (status: StreamStatus) => {
3737
switch (status) {
3838
case 'active':
39-
return 'Active';
39+
return 'Enabled';
4040
case 'inactive':
41-
return 'Inactive';
41+
return 'Disabled';
4242
default:
4343
return 'Unknown';
4444
}

0 commit comments

Comments
 (0)