Skip to content

Commit 2d3c1cb

Browse files
Merge pull request #13233 from upalatucci/filter-toolbar-additional-search
OCPBUGS-30077: Add additional search filters in the toolbar
2 parents 96e1aa7 + fb8052e commit 2d3c1cb

7 files changed

Lines changed: 189 additions & 21 deletions

File tree

frontend/packages/console-dynamic-plugin-sdk/docs/api.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -840,15 +840,15 @@ Component that generates filter for list page.
840840
// See implementation for more details on RowFilter and FilterValue types
841841
const [staticData, filteredData, onFilterChange] = useListPageFilter(
842842
data,
843-
rowFilters,
843+
[...rowFilters, ...searchFilters],
844844
staticFilters,
845845
);
846846
// ListPageFilter updates filter state based on user interaction and resulting filtered data can be rendered in an independent component.
847847
return (
848848
<>
849-
<ListPageHeader .../>
849+
<ListPageHeader />
850850
<ListPagBody>
851-
<ListPageFilter data={staticData} onFilterChange={onFilterChange} />
851+
<ListPageFilter data={staticData} onFilterChange={onFilterChange} rowFilters={rowFilters} rowSearchFilters={searchFilters} />
852852
<List data={filteredData} />
853853
</ListPageBody>
854854
</>
@@ -877,6 +877,7 @@ Component that generates filter for list page.
877877
| `columnLayout` | (optional) column layout object |
878878
| `hideColumnManagement` | (optional) flag to hide the column management |
879879
| `nameFilter` | (optional) a unique name key for name filter. This may be useful if there are multiple `ListPageFilter` components rendered at once. |
880+
| `rowSearchFilters` | (optional) An array of RowSearchFilters elements that define search text filters added on top of Name and Label filters |
880881
881882
882883

frontend/packages/console-dynamic-plugin-sdk/src/api/dynamic-core-api.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -281,20 +281,21 @@ export const ListPageCreateDropdown: React.FC<ListPageCreateDropdownProps> = req
281281
* @param {ColumnLayout} [columnLayout] - (optional) column layout object
282282
* @param {boolean} [hideColumnManagement] - (optional) flag to hide the column management
283283
* @param {string} [nameFilter] - (optional) a unique name key for name filter. This may be useful if there are multiple `ListPageFilter` components rendered at once.
284+
* @param {RowSearchFilter[]} [rowSearchFilters] - (optional) An array of RowSearchFilters elements that define search text filters added on top of Name and Label filters
284285
* @example
285286
* ```tsx
286287
* // See implementation for more details on RowFilter and FilterValue types
287288
* const [staticData, filteredData, onFilterChange] = useListPageFilter(
288289
* data,
289-
* rowFilters,
290+
* [...rowFilters, ...searchFilters],
290291
* staticFilters,
291292
* );
292293
* // ListPageFilter updates filter state based on user interaction and resulting filtered data can be rendered in an independent component.
293294
* return (
294295
* <>
295-
* <ListPageHeader .../>
296+
* <ListPageHeader />
296297
* <ListPagBody>
297-
* <ListPageFilter data={staticData} onFilterChange={onFilterChange} />
298+
* <ListPageFilter data={staticData} onFilterChange={onFilterChange} rowFilters={rowFilters} rowSearchFilters={searchFilters} />
298299
* <List data={filteredData} />
299300
* </ListPageBody>
300301
* </>

frontend/packages/console-dynamic-plugin-sdk/src/extensions/console-types.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,12 @@ export type RowReducerFilter<R = any> = RowFilterBase<R> & {
401401

402402
export type RowFilter<R = any> = RowMatchFilter<R> | RowReducerFilter<R>;
403403

404+
export type RowSearchFilter<R = any> = Omit<RowFilterBase<R>, 'items' | 'defaultSelected'> & {
405+
placeholder?: string;
406+
};
407+
408+
export type AnyRowFilter<R = any> = RowFilter<R> | RowSearchFilter<R>;
409+
404410
export type ColumnLayout = {
405411
id: string;
406412
columns: ManagedColumn[];
@@ -420,7 +426,7 @@ export type OnFilterChange = (type: string, value: FilterValue) => void;
420426
export type ListPageFilterProps<D = any> = {
421427
data: D;
422428
loaded: boolean;
423-
rowFilters?: RowFilter[];
429+
rowFilters?: RowFilter<D>[];
424430
labelFilter?: string;
425431
labelPath?: string;
426432
nameFilterTitle?: string;
@@ -432,11 +438,12 @@ export type ListPageFilterProps<D = any> = {
432438
onFilterChange: OnFilterChange;
433439
hideColumnManagement?: boolean;
434440
nameFilter?: string;
441+
rowSearchFilters?: RowSearchFilter<D>[];
435442
};
436443

437444
export type UseListPageFilter = <D, R>(
438445
data: D[],
439-
rowFilters?: RowFilter<R>[],
446+
rowFilters?: AnyRowFilter<R>[],
440447
staticFilters?: { [key: string]: FilterValue },
441448
) => [D[], D[], OnFilterChange];
442449

frontend/public/components/factory/ListPage/ListPageFilter.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const ListPageFilter: React.FC<ListPageFilterProps> = ({
1919
onFilterChange,
2020
hideColumnManagement,
2121
nameFilter,
22+
rowSearchFilters,
2223
}) =>
2324
loaded &&
2425
!_.isEmpty(data) && (
@@ -36,6 +37,7 @@ const ListPageFilter: React.FC<ListPageFilterProps> = ({
3637
columnLayout={columnLayout}
3738
hideColumnManagement={hideColumnManagement}
3839
textFilter={nameFilter}
40+
rowSearchFilters={rowSearchFilters}
3941
/>
4042
);
4143

frontend/public/components/factory/table-filters.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as _ from 'lodash-es';
22
import * as fuzzy from 'fuzzysearch';
33
import { nodeStatus, volumeSnapshotStatus } from '@console/app/src/status';
44
import { getNodeRoles, getLabelsAsString } from '@console/shared';
5-
import { Alert, FilterValue, RowFilter, Rule } from '@console/dynamic-plugin-sdk';
5+
import { Alert, AnyRowFilter, FilterValue, Rule } from '@console/dynamic-plugin-sdk';
66
import { routeStatus } from '../routes';
77
import { secretTypeFilterReducer } from '../secret';
88
import { roleType } from '../RBAC';
@@ -235,13 +235,16 @@ export const tableFilters = (isExactSearch: boolean): FilterMap => {
235235
},
236236
};
237237
};
238-
const rowFiltersToFilterFuncs = (rowFilters: RowFilter[]): FilterMap => {
238+
const rowFiltersToFilterFuncs = (rowFilters: AnyRowFilter[]): FilterMap => {
239239
return (rowFilters || [])
240240
.filter((f) => f.type && _.isFunction(f.filter))
241241
.reduce((acc, f) => ({ ...acc, [f.type]: f.filter }), {} as FilterMap);
242242
};
243243

244-
export const getAllTableFilters = (rowFilters: RowFilter[], isExactMatch?: boolean): FilterMap => ({
244+
export const getAllTableFilters = (
245+
rowFilters: AnyRowFilter[],
246+
isExactMatch?: boolean,
247+
): FilterMap => ({
245248
...tableFilters(isExactMatch),
246249
...rowFiltersToFilterFuncs(rowFilters),
247250
});

frontend/public/components/filter-toolbar.tsx

Lines changed: 92 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
ColumnLayout,
3030
OnFilterChange,
3131
FilterValue,
32+
RowSearchFilter,
3233
} from '@console/dynamic-plugin-sdk';
3334
import {
3435
Dropdown as DropdownInternal,
@@ -43,6 +44,7 @@ import { TextFilter } from './factory';
4344
import { filterList } from '@console/dynamic-plugin-sdk/src/app/k8s/actions/k8s';
4445
import useRowFilterFix from './useRowFilterFix';
4546
import useLabelSelectionFix from './useLabelSelectionFix';
47+
import useSearchFilters from './useSearchFilters';
4648

4749
/**
4850
* Housing both the row filter and name/label filter in the same file.
@@ -82,6 +84,7 @@ export const FilterToolbar: React.FC<FilterToolbarProps> = ({
8284
reduxIDs,
8385
onFilterChange,
8486
labelPath,
87+
rowSearchFilters = [],
8588
}) => {
8689
const dispatch = useDispatch();
8790
const location = useLocation();
@@ -90,36 +93,61 @@ export const FilterToolbar: React.FC<FilterToolbarProps> = ({
9093

9194
const translatedNameFilterTitle = nameFilterTitle ?? t('public~Name');
9295

96+
const { searchFiltersObject, searchFiltersState, changeSearchFiltersState } = useSearchFilters(
97+
rowSearchFilters,
98+
uniqueFilterName,
99+
);
100+
93101
const translateFilterType = (value: string) => {
94102
switch (value) {
95103
case 'Name':
96104
return translatedNameFilterTitle;
97105
case 'Label':
98106
return t('public~Label');
99107
default:
100-
return value;
108+
return searchFiltersObject?.[value]?.filterGroupName || value;
101109
}
102110
};
103-
const filterDropdownItems = {
104-
NAME: translatedNameFilterTitle,
105-
LABEL: t('public~Label'),
111+
112+
const filterDropdownItems: Record<string, string> = {
113+
...Object.keys(searchFiltersObject || {}).reduce(
114+
(acc, key) => ({
115+
...acc,
116+
[key]: searchFiltersObject[key].filterGroupName,
117+
}),
118+
{},
119+
),
106120
};
107121

122+
if (!hideLabelFilter && !hideNameLabelFilters) {
123+
filterDropdownItems.LABEL = t('public~Label');
124+
}
125+
126+
if (!hideNameLabelFilters) {
127+
filterDropdownItems.NAME = translatedNameFilterTitle;
128+
}
129+
108130
// use unique name only when only when more than 1 table is in the view
109131
const nameFilterQueryArgumentKey = uniqueFilterName
110132
? `${uniqueFilterName}-${textFilter}`
111133
: textFilter;
112134
const labelFilterQueryArgumentKey = uniqueFilterName
113135
? `${uniqueFilterName}-${labelFilter}`
114136
: labelFilter;
137+
115138
const params = new URLSearchParams(location.search);
116-
const [filterType, setFilterType] = React.useState(FilterType.NAME);
117139
const [isOpen, setOpen] = React.useState(false);
118140
const [nameInputText, setNameInputText] = React.useState(
119141
params.get(nameFilterQueryArgumentKey) ?? '',
120142
);
121143
const [labelInputText, setLabelInputText] = React.useState('');
122144

145+
const [filterType, setFilterType] = React.useState(
146+
nameInputText || !hideNameLabelFilters
147+
? FilterType.NAME
148+
: Object.keys(searchFiltersState)?.[0] || rowSearchFilters?.[0]?.type,
149+
);
150+
123151
// Generate rowFilter items and counts. Memoize to minimize re-renders.
124152
const generatedRowFilters = useDeepCompareMemoize(
125153
(rowFilters ?? []).map((rowFilter) => ({
@@ -213,6 +241,27 @@ export const FilterToolbar: React.FC<FilterToolbarProps> = ({
213241
[onFilterChange, reduxIDs, dispatch],
214242
);
215243

244+
const applyTextFilter = React.useCallback(
245+
(value: string, filterName: string) => {
246+
applyFilters(filterName, { selected: [value] });
247+
},
248+
[applyFilters],
249+
);
250+
251+
const searchRowFilters = rowSearchFilters.map((searchFilter) => (
252+
<ToolbarFilter
253+
key={searchFilter.type}
254+
categoryName={translateFilterType(searchFilter.type)}
255+
deleteChip={() => {
256+
changeSearchFiltersState(searchFilter.type, '');
257+
applyTextFilter('', searchFilter.type);
258+
}}
259+
chips={searchFiltersState[searchFilter.type] ? [searchFiltersState[searchFilter.type]] : []}
260+
>
261+
<></>
262+
</ToolbarFilter>
263+
));
264+
216265
const applyRowFilter = (selected: string[]) => {
217266
generatedRowFilters?.forEach?.(({ items, type }) => {
218267
const all = items?.map?.(({ id }) => id) ?? [];
@@ -250,6 +299,7 @@ export const FilterToolbar: React.FC<FilterToolbarProps> = ({
250299
);
251300

252301
const debounceApplyNameFilter = useDebounceCallback(applyNameFilter, 250);
302+
const debounceApplyTextFilter = useDebounceCallback(applyTextFilter, 250);
253303

254304
const clearAll = () => {
255305
updateRowFilterSelected(selectedRowFilters);
@@ -261,6 +311,13 @@ export const FilterToolbar: React.FC<FilterToolbarProps> = ({
261311
setLabelInputText('');
262312
applyLabelFilters([]);
263313
}
314+
315+
if (rowSearchFilters.length > 0) {
316+
Object.keys(searchFiltersState).forEach((key) => {
317+
changeSearchFiltersState(key, '');
318+
applyTextFilter('', key);
319+
});
320+
}
264321
};
265322

266323
// Run once on mount to apply filters from query params
@@ -271,6 +328,12 @@ export const FilterToolbar: React.FC<FilterToolbarProps> = ({
271328
if (!hideNameLabelFilters) {
272329
applyFilters(textFilter, { selected: [nameInputText] });
273330
}
331+
332+
if (rowSearchFilters.length > 0) {
333+
Object.keys(searchFiltersState).forEach((key) => {
334+
applyFilters(key, { selected: [searchFiltersState[key]] });
335+
});
336+
}
274337
// eslint-disable-next-line react-hooks/exhaustive-deps
275338
}, []);
276339

@@ -287,6 +350,9 @@ export const FilterToolbar: React.FC<FilterToolbarProps> = ({
287350
// eslint-disable-next-line react-hooks/exhaustive-deps
288351
}, [rowFiltersInitialized, labelSelectionInitialized]);
289352

353+
const showSearchFilters = Object.keys(filterDropdownItems).length !== 0;
354+
355+
const showSearchFiltersDropdown = Object.keys(filterDropdownItems).length > 1;
290356
return (
291357
<Toolbar
292358
className="co-toolbar-no-padding pf-m-toggle-group-container"
@@ -347,8 +413,9 @@ export const FilterToolbar: React.FC<FilterToolbarProps> = ({
347413
)}
348414
</ToolbarItem>
349415
)}
350-
{!hideNameLabelFilters && (
416+
{showSearchFilters && (
351417
<ToolbarItem className="co-filter-search--full-width">
418+
{searchRowFilters}
352419
<ToolbarFilter
353420
deleteChipGroup={() => {
354421
setLabelInputText('');
@@ -370,15 +437,15 @@ export const FilterToolbar: React.FC<FilterToolbarProps> = ({
370437
categoryName={translatedNameFilterTitle}
371438
>
372439
<div className="pf-v5-c-input-group co-filter-group">
373-
{!hideLabelFilter && (
440+
{showSearchFiltersDropdown && (
374441
<DropdownInternal
375442
items={filterDropdownItems}
376-
onChange={(type) => setFilterType(FilterType[type])}
443+
onChange={(type) => setFilterType(FilterType[type] || type)}
377444
selectedKey={filterType}
378445
title={translateFilterType(filterType)}
379446
/>
380447
)}
381-
{filterType === FilterType.LABEL ? (
448+
{filterType === FilterType.LABEL && (
382449
<AutocompleteInput
383450
className="co-text-node"
384451
onSuggestionSelect={(selected) => {
@@ -391,7 +458,9 @@ export const FilterToolbar: React.FC<FilterToolbarProps> = ({
391458
data={data}
392459
labelPath={labelPath}
393460
/>
394-
) : (
461+
)}
462+
463+
{filterType === FilterType.NAME && (
395464
<TextFilter
396465
data-test="name-filter-input"
397466
value={nameInputText}
@@ -402,6 +471,18 @@ export const FilterToolbar: React.FC<FilterToolbarProps> = ({
402471
placeholder={nameFilterPlaceholder ?? t('public~Search by name...')}
403472
/>
404473
)}
474+
475+
{searchFiltersObject[filterType] && (
476+
<TextFilter
477+
data-test={`${filterType}-filter-input`}
478+
value={searchFiltersState[filterType]}
479+
onChange={(_event, value: string) => {
480+
changeSearchFiltersState(filterType, value);
481+
debounceApplyTextFilter(value, filterType);
482+
}}
483+
placeholder={searchFiltersObject[filterType].placeholder}
484+
/>
485+
)}
405486
</div>
406487
</ToolbarFilter>
407488
</ToolbarFilter>
@@ -471,6 +552,7 @@ type FilterToolbarProps = {
471552
// Used when multiple tables are in the same page
472553
uniqueFilterName?: string;
473554
onFilterChange?: OnFilterChange;
555+
rowSearchFilters?: RowSearchFilter[];
474556
};
475557

476558
FilterToolbar.displayName = 'FilterToolbar';

0 commit comments

Comments
 (0)