Skip to content

Commit 4fff92a

Browse files
Merge pull request #5930 from rottencandy/es-filter-4244
Add filter and scroll for EventSource selector
2 parents 30882b5 + d7a251d commit 4fff92a

File tree

5 files changed

+163
-51
lines changed

5 files changed

+163
-51
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import * as React from 'react';
2+
import { shallow } from 'enzyme';
3+
import ItemSelectorField from '../item-selector-field/ItemSelectorField';
4+
import { EmptyState } from '@patternfly/react-core';
5+
6+
jest.mock('formik', () => ({
7+
useField: jest.fn(() => [{}, {}]),
8+
useFormikContext: jest.fn(() => ({
9+
setFieldValue: jest.fn(),
10+
setFieldTouched: jest.fn(),
11+
validateForm: jest.fn(),
12+
})),
13+
}));
14+
describe('ItemSelectorField', () => {
15+
it('Should not render if showIfSingle is false and list contains single item', () => {
16+
const list = { ListItem: { name: 'ItemName', title: 'ItemName', iconUrl: 'DisplayIcon' } };
17+
const wrapper = shallow(<ItemSelectorField name="test" itemList={list} />);
18+
expect(wrapper.isEmptyRender()).toBe(true);
19+
});
20+
21+
it('Should display empty state if list is empty and filter is shown', () => {
22+
const list = {};
23+
const wrapper = shallow(<ItemSelectorField name="test" itemList={list} showFilter />);
24+
expect(wrapper.find(EmptyState)).toHaveLength(1);
25+
});
26+
});

frontend/packages/console-shared/src/components/formik-fields/item-selector-field/ItemSelectorField.scss

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,29 @@
1+
.odc-item-selector-filter {
2+
display: flex;
3+
padding-bottom: var(--pf-global--spacer--md);
4+
5+
&__input {
6+
max-width: 20em;
7+
}
8+
9+
&__count {
10+
color: var(--pf-global--Color--200);
11+
margin-left: auto;
12+
}
13+
}
14+
115
.odc-item-selector-field {
216
display: inline-flex;
317
flex-direction: column;
418
flex-flow: wrap;
519
background: var(--pf-global--Color--light-200);
620
padding: 4px;
721

22+
&__scrollbar {
23+
max-height: 260px;
24+
overflow-y: auto;
25+
}
26+
827
&__success-icon {
928
color: var(--pf-global--palette--green-700);
1029
font-size: var(--pf-global--FontSize--md);

frontend/packages/console-shared/src/components/formik-fields/item-selector-field/ItemSelectorField.tsx

Lines changed: 108 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,19 @@
11
import * as React from 'react';
22
import * as _ from 'lodash';
3+
import * as fuzzy from 'fuzzysearch';
34
import { useField, useFormikContext, FormikValues } from 'formik';
45
import { LoadingInline } from '@console/internal/components/utils';
5-
import { FormGroup } from '@patternfly/react-core';
6-
import { getFieldId } from '@console/shared';
6+
import {
7+
FormGroup,
8+
EmptyState,
9+
Title,
10+
EmptyStatePrimary,
11+
Button,
12+
TextInput,
13+
EmptyStateBody,
14+
pluralize,
15+
} from '@patternfly/react-core';
16+
import { getFieldId, useDebounceCallback } from '@console/shared';
717
import SelectorCard from './SelectorCard';
818
import './ItemSelectorField.scss';
919

@@ -26,6 +36,9 @@ interface ItemSelectorFieldProps {
2636
label?: string;
2737
autoSelect?: boolean;
2838
onSelect?: (name: string) => void;
39+
showIfSingle?: boolean;
40+
showFilter?: boolean;
41+
showCount?: boolean;
2942
}
3043

3144
const ItemSelectorField: React.FC<ItemSelectorFieldProps> = ({
@@ -36,10 +49,15 @@ const ItemSelectorField: React.FC<ItemSelectorFieldProps> = ({
3649
onSelect,
3750
label,
3851
autoSelect,
52+
showIfSingle = false,
53+
showFilter = false,
54+
showCount = false,
3955
}) => {
4056
const [selected, { error: selectedError, touched: selectedTouched }] = useField(name);
4157
const { setFieldValue, setFieldTouched, validateForm } = useFormikContext<FormikValues>();
42-
const itemCount = _.keys(itemList).length;
58+
const [filteredList, setFilteredList] = React.useState(itemList);
59+
const [filterText, setFilterText] = React.useState('');
60+
const itemCount = _.keys(filteredList).length;
4361

4462
const handleItemChange = React.useCallback(
4563
(item: string) => {
@@ -52,30 +70,57 @@ const ItemSelectorField: React.FC<ItemSelectorFieldProps> = ({
5270
);
5371

5472
React.useEffect(() => {
55-
if (!selected.value && itemCount === 1) {
56-
const image = _.find(itemList);
73+
if (!selected.value && _.keys(itemList).length === 1) {
74+
const image = _.find(filteredList);
5775
handleItemChange(image.name);
5876
}
5977
if (!selected.value && recommended) {
6078
handleItemChange(recommended);
6179
setFieldTouched(name, false);
6280
}
63-
if (!selected.value && autoSelect && !_.isEmpty(itemList)) {
64-
const image = _.get(_.keys(itemList), 0);
65-
handleItemChange(itemList[image]?.name);
81+
if (!selected.value && autoSelect && !_.isEmpty(filteredList)) {
82+
const image = _.get(_.keys(filteredList), 0);
83+
handleItemChange(filteredList[image]?.name);
6684
}
6785
}, [
6886
autoSelect,
69-
itemCount,
7087
itemList,
88+
filteredList,
7189
handleItemChange,
7290
selected.value,
7391
recommended,
7492
name,
7593
setFieldTouched,
7694
]);
7795

78-
if (itemCount === 1) {
96+
const filterSources = React.useCallback(
97+
(text: string) => {
98+
const subList = _.pickBy(itemList, (val) =>
99+
fuzzy(text.toLowerCase(), val.title.toLowerCase()),
100+
);
101+
if (selected.value) {
102+
subList[selected.value] = itemList[selected.value];
103+
}
104+
setFilteredList(subList);
105+
},
106+
[itemList, selected.value],
107+
);
108+
109+
const debounceFilterText = useDebounceCallback<(text: string) => void>(filterSources, [
110+
filterSources,
111+
]);
112+
113+
const handleFilterChange = (text: string) => {
114+
setFilterText(text);
115+
debounceFilterText(text);
116+
};
117+
118+
const handleClearFilter = () => {
119+
setFilterText('');
120+
filterSources('');
121+
};
122+
123+
if (!showIfSingle && itemCount === 1) {
79124
return null;
80125
}
81126

@@ -94,20 +139,59 @@ const ItemSelectorField: React.FC<ItemSelectorFieldProps> = ({
94139
{loadingItems ? (
95140
<LoadingInline />
96141
) : (
97-
<div id="item-selector-field" className="odc-item-selector-field">
98-
{_.values(itemList).map((item) => (
99-
<SelectorCard
100-
key={item.name}
101-
title={item.title}
102-
iconUrl={item.iconUrl}
103-
name={item.name}
104-
displayName={item.displayName}
105-
selected={selected.value === item.name}
106-
recommended={recommended === item.name}
107-
onChange={handleItemChange}
108-
/>
109-
))}
110-
</div>
142+
<>
143+
{showFilter && (
144+
<div className="odc-item-selector-filter">
145+
<TextInput
146+
className="odc-item-selector-filter__input"
147+
onChange={handleFilterChange}
148+
value={filterText}
149+
placeholder="Filter by type..."
150+
aria-label="Filter by type"
151+
/>
152+
{showCount && (
153+
<span className="odc-item-selector-filter__count">
154+
{pluralize(itemCount, 'item')}
155+
</span>
156+
)}
157+
</div>
158+
)}
159+
{showFilter && itemCount === 0 ? (
160+
<EmptyState>
161+
<Title headingLevel="h2" size="lg">
162+
No results match the filter criteria
163+
</Title>
164+
<EmptyStateBody>
165+
No Event Source types are being shown due to the filters being applied.
166+
</EmptyStateBody>
167+
<EmptyStatePrimary>
168+
<Button variant="link" onClick={handleClearFilter}>
169+
Clear filter
170+
</Button>
171+
</EmptyStatePrimary>
172+
</EmptyState>
173+
) : (
174+
<div
175+
id="item-selector-field"
176+
className={`odc-item-selector-field ${
177+
showFilter ? 'odc-item-selector-field__scrollbar' : ''
178+
}`}
179+
>
180+
{_.values(filteredList).map((item) => (
181+
<SelectorCard
182+
key={item.name}
183+
title={item.title}
184+
iconUrl={item.iconUrl}
185+
name={item.name}
186+
displayName={item.displayName}
187+
selected={selected.value === item.name}
188+
recommended={recommended === item.name}
189+
onChange={handleItemChange}
190+
/>
191+
))}
192+
</div>
193+
)}
194+
</>
111195
)}
112196
</FormGroup>
113197
);

frontend/packages/knative-plugin/src/components/add/event-sources/EventSourcesSelector.tsx

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -62,22 +62,18 @@ const EventSourcesSelector: React.FC<EventSourcesSelectorProps> = ({ eventSource
6262
],
6363
);
6464

65-
const itemSelectorField = (
66-
<ItemSelectorField
67-
itemList={eventSourceList}
68-
loadingItems={!eventSourceItems}
69-
name="type"
70-
onSelect={handleItemChange}
71-
autoSelect
72-
/>
73-
);
74-
75-
return eventSourceItems > 1 ? (
65+
return (
7666
<FormSection title="Type" fullWidth extraMargin>
77-
{itemSelectorField}
67+
<ItemSelectorField
68+
itemList={eventSourceList}
69+
loadingItems={!eventSourceItems}
70+
name="type"
71+
onSelect={handleItemChange}
72+
showIfSingle
73+
showFilter
74+
showCount
75+
/>
7876
</FormSection>
79-
) : (
80-
itemSelectorField
8177
);
8278
};
8379

frontend/packages/knative-plugin/src/components/add/event-sources/__tests__/EventSourcesSelector.spec.tsx

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -31,19 +31,6 @@ describe('EventSourcesSelector', () => {
3131
wrapper = shallow(<EventSourcesSelector eventSourceList={eventSourceList} />);
3232
});
3333

34-
it('should not render FormSection if no more than one eventSource present', () => {
35-
const eventSourceList = {
36-
SinkBinding: {
37-
title: 'sinkBinding',
38-
iconUrl: 'sinkBindingIcon',
39-
name: 'SinkBinding',
40-
displayName: 'Sink Binding',
41-
},
42-
};
43-
wrapper = shallow(<EventSourcesSelector eventSourceList={eventSourceList} />);
44-
expect(wrapper.find(FormSection).exists()).toBe(false);
45-
});
46-
4734
it('should render FormSection if more than one eventSource present', () => {
4835
const eventSourceList = {
4936
SinkBinding: {

0 commit comments

Comments
 (0)