Skip to content

Commit 4dd30c3

Browse files
feat: vulnerability list page - add "published only" filter (#851)
Signed-off-by: Carlos Feria <2582866+carlosthe19916@users.noreply.github.com>
1 parent 8e9d99b commit 4dd30c3

File tree

11 files changed

+207
-16
lines changed

11 files changed

+207
-16
lines changed

client/src/app/components/FilterPanel/FilterControl.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@ import {
88
type IMultiselectFilterCategory,
99
type ISearchFilterCategory,
1010
type ISelectFilterCategory,
11+
type IToggleFilterCategory,
1112
} from "../FilterToolbar";
1213
import { AsyncMultiselectFilterControl } from "./AsyncMultiselectFilterControl";
1314
import { AutocompleteLabelFilterControl } from "./AutocompleteLabelFilterControl";
1415
import { CheckboxFilterControl } from "./CheckboxFilterControl";
1516
import { DateRangeFilter } from "./DateRangeFilter";
1617
import { RadioFilterControl } from "./RadioFilterControl";
1718
import { SearchFilterControl } from "./SearchFilterControl";
19+
import { ToggleFilterControl } from "./ToggleFilterControl";
1820

1921
export interface IFilterControlProps<TItem, TFilterCategoryKey extends string> {
2022
category: FilterCategory<TItem, TFilterCategoryKey>;
@@ -83,5 +85,13 @@ export const FilterControl = <TItem, TFilterCategoryKey extends string>({
8385
/>
8486
);
8587
}
88+
if (category.type === FilterType.toggle) {
89+
return (
90+
<ToggleFilterControl
91+
category={category as IToggleFilterCategory<TItem, TFilterCategoryKey>}
92+
{...props}
93+
/>
94+
);
95+
}
8696
return null;
8797
};
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import type React from "react";
2+
3+
import { Checkbox } from "@patternfly/react-core";
4+
5+
import { parseBooleanIfPossible } from "@app/utils/utils";
6+
7+
import type { IToggleFilterCategory } from "../FilterToolbar";
8+
import type { IFilterControlProps } from "./FilterControl";
9+
10+
export interface IToggleFilterControlProps<
11+
TItem,
12+
TFilterCategoryKey extends string,
13+
> extends IFilterControlProps<TItem, TFilterCategoryKey> {
14+
category: IToggleFilterCategory<TItem, TFilterCategoryKey>;
15+
}
16+
17+
export const ToggleFilterControl = <TItem, TFilterCategoryKey extends string>({
18+
category,
19+
filterValue,
20+
setFilterValue,
21+
isDisabled = false,
22+
}: React.PropsWithChildren<
23+
IToggleFilterControlProps<TItem, TFilterCategoryKey>
24+
>): React.JSX.Element | null => {
25+
return (
26+
<Checkbox
27+
id={`filter-control-${category.categoryKey}`}
28+
label={category.label}
29+
isChecked={parseBooleanIfPossible(filterValue?.[0])}
30+
onChange={(_e, value) => {
31+
if (value) {
32+
setFilterValue([String(true)]);
33+
} else {
34+
setFilterValue(null);
35+
}
36+
}}
37+
isDisabled={isDisabled}
38+
/>
39+
);
40+
};

client/src/app/components/FilterToolbar/FilterControl.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type * as React from "react";
22

33
import { AsyncMultiselectFilterControl } from "./AsyncMultiselectFilterControl";
44
import { AutocompleteLabelFilterControl } from "./AutocompleteLabelFilterControl";
5+
import { ToggleFilterControl } from "./ToggleFilterControl";
56
import { DateRangeFilter } from "./DateRangeFilter";
67
import {
78
type FilterCategory,
@@ -11,6 +12,7 @@ import {
1112
type IMultiselectFilterCategory,
1213
type ISearchFilterCategory,
1314
type ISelectFilterCategory,
15+
type IToggleFilterCategory,
1416
} from "./FilterToolbar";
1517
import { MultiselectFilterControl } from "./MultiselectFilterControl";
1618
import { SearchFilterControl } from "./SearchFilterControl";
@@ -85,5 +87,13 @@ export const FilterControl = <TItem, TFilterCategoryKey extends string>({
8587
/>
8688
);
8789
}
90+
if (category.type === FilterType.toggle) {
91+
return (
92+
<ToggleFilterControl
93+
category={category as IToggleFilterCategory<TItem, TFilterCategoryKey>}
94+
{...props}
95+
/>
96+
);
97+
}
8898
return null;
8999
};

client/src/app/components/FilterToolbar/FilterToolbar.tsx

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
DropdownList,
88
MenuToggle,
99
type SelectOptionProps,
10+
ToolbarGroup,
1011
ToolbarItem,
1112
ToolbarToggleGroup,
1213
} from "@patternfly/react-core";
@@ -22,6 +23,7 @@ export enum FilterType {
2223
numsearch = "numsearch",
2324
dateRange = "dateRange",
2425
autocompleteLabel = "autocompleteLabel",
26+
toggle = "toggle",
2527
}
2628

2729
export type FilterValue = string[] | undefined | null;
@@ -62,6 +64,16 @@ export interface IBasicFilterCategory<
6264
* Main operator for filter value. Defaults depends on the implementation of each categorykey
6365
*/
6466
operator?: "=" | "!=" | "~" | ">" | ">=" | "<" | "<=";
67+
/**
68+
* For dynamic operator. Returns the operator value based on the filterValue
69+
*/
70+
getOperator?: (
71+
filterValue: FilterValue,
72+
) => "=" | "!=" | "~" | ">" | ">=" | "<" | "<=";
73+
/**
74+
* Whether or not it will always be
75+
*/
76+
showOutsideDropdown?: boolean;
6577
}
6678

6779
export interface IMultiselectFilterCategory<
@@ -94,11 +106,17 @@ export interface ISearchFilterCategory<TItem, TFilterCategoryKey extends string>
94106
placeholderText: string;
95107
}
96108

109+
export interface IToggleFilterCategory<TItem, TFilterCategoryKey extends string>
110+
extends IBasicFilterCategory<TItem, TFilterCategoryKey> {
111+
label: string;
112+
}
113+
97114
export type FilterCategory<TItem, TFilterCategoryKey extends string> =
98115
| IMultiselectFilterCategory<TItem, TFilterCategoryKey>
99116
| IAsyncMultiselectFilterCategory<TItem, TFilterCategoryKey>
100117
| ISelectFilterCategory<TItem, TFilterCategoryKey>
101118
| ISearchFilterCategory<TItem, TFilterCategoryKey>
119+
| IToggleFilterCategory<TItem, TFilterCategoryKey>
102120
| IBasicFilterCategory<TItem, TFilterCategoryKey>;
103121

104122
export type IFilterValues<TFilterCategoryKey extends string> = Partial<
@@ -138,10 +156,18 @@ export const FilterToolbar = <TItem, TFilterCategoryKey extends string>({
138156
}: React.PropsWithChildren<
139157
IFilterToolbarProps<TItem, TFilterCategoryKey>
140158
>): React.JSX.Element | null => {
159+
const filteredFilterCategories = showFiltersSideBySide
160+
? filterCategories
161+
: filterCategories.filter((item) => !item.showOutsideDropdown);
162+
141163
const [isCategoryDropdownOpen, setIsCategoryDropdownOpen] =
142164
React.useState(false);
143165
const [currentFilterCategoryKey, setCurrentFilterCategoryKey] =
144-
React.useState(filterCategories[0].categoryKey);
166+
React.useState(
167+
filteredFilterCategories[0]?.categoryKey as
168+
| TFilterCategoryKey
169+
| undefined,
170+
);
145171

146172
const onCategorySelect = (
147173
category: FilterCategory<TItem, TFilterCategoryKey>,
@@ -155,11 +181,11 @@ export const FilterToolbar = <TItem, TFilterCategoryKey extends string>({
155181
newValue: FilterValue,
156182
) => setFilterValues({ ...filterValues, [category.categoryKey]: newValue });
157183

158-
const currentFilterCategory = filterCategories.find(
184+
const currentFilterCategory = filteredFilterCategories.find(
159185
(category) => category.categoryKey === currentFilterCategoryKey,
160186
);
161187

162-
const filterGroups = filterCategories.reduce((groups, category) => {
188+
const filterGroups = filteredFilterCategories.reduce((groups, category) => {
163189
if (category.filterGroup && !groups.includes(category.filterGroup)) {
164190
groups.push(category.filterGroup);
165191
}
@@ -171,7 +197,7 @@ export const FilterToolbar = <TItem, TFilterCategoryKey extends string>({
171197
return filterGroups.map((filterGroup) => (
172198
<DropdownGroup label={filterGroup} key={filterGroup}>
173199
<DropdownList>
174-
{filterCategories
200+
{filteredFilterCategories
175201
.filter(
176202
(filterCategory) => filterCategory.filterGroup === filterGroup,
177203
)
@@ -191,7 +217,7 @@ export const FilterToolbar = <TItem, TFilterCategoryKey extends string>({
191217
));
192218
}
193219

194-
return filterCategories.map((category) => (
220+
return filteredFilterCategories.map((category) => (
195221
<DropdownItem
196222
id={`filter-category-${category.categoryKey}`}
197223
key={category.categoryKey}
@@ -232,7 +258,7 @@ export const FilterToolbar = <TItem, TFilterCategoryKey extends string>({
232258
</ToolbarItem>
233259
)}
234260

235-
{filterCategories.map((category) => (
261+
{filteredFilterCategories.map((category) => (
236262
<FilterControl<TItem, TFilterCategoryKey>
237263
key={category.categoryKey}
238264
category={category}
@@ -246,6 +272,24 @@ export const FilterToolbar = <TItem, TFilterCategoryKey extends string>({
246272
/>
247273
))}
248274
</ToolbarToggleGroup>
275+
{!showFiltersSideBySide && (
276+
<ToolbarGroup variant="filter-group" alignSelf="center">
277+
{filterCategories
278+
.filter((item) => item.showOutsideDropdown)
279+
.map((category) => (
280+
<FilterControl<TItem, TFilterCategoryKey>
281+
key={category.categoryKey}
282+
category={category}
283+
filterValue={filterValues[category.categoryKey]}
284+
setFilterValue={(newValue) =>
285+
setFilterValue(category, newValue)
286+
}
287+
showToolbarItem
288+
isDisabled={isDisabled}
289+
/>
290+
))}
291+
</ToolbarGroup>
292+
)}
249293
{pagination ? (
250294
<ToolbarItem variant="pagination">{pagination}</ToolbarItem>
251295
) : null}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import type React from "react";
2+
3+
import { Checkbox, ToolbarFilter } from "@patternfly/react-core";
4+
5+
import { parseBooleanIfPossible } from "@app/utils/utils";
6+
7+
import type { IFilterControlProps } from "./FilterControl";
8+
import type { IToggleFilterCategory } from "./FilterToolbar";
9+
10+
export interface IToggleFilterControlProps<
11+
TItem,
12+
TFilterCategoryKey extends string,
13+
> extends IFilterControlProps<TItem, TFilterCategoryKey> {
14+
category: IToggleFilterCategory<TItem, TFilterCategoryKey>;
15+
}
16+
17+
export const ToggleFilterControl = <TItem, TFilterCategoryKey extends string>({
18+
category,
19+
filterValue,
20+
setFilterValue,
21+
showToolbarItem,
22+
isDisabled = false,
23+
}: React.PropsWithChildren<
24+
IToggleFilterControlProps<TItem, TFilterCategoryKey>
25+
>): React.JSX.Element | null => {
26+
return (
27+
<ToolbarFilter
28+
labels={filterValue?.map((value) => ({ key: value, node: value })) || []}
29+
deleteLabel={() => setFilterValue(null)}
30+
categoryName={category.title}
31+
showToolbarItem={showToolbarItem}
32+
>
33+
<Checkbox
34+
id={`filter-control-${category.categoryKey}`}
35+
label={category.label}
36+
isChecked={parseBooleanIfPossible(filterValue?.[0])}
37+
onChange={(_e, value) => {
38+
if (value) {
39+
setFilterValue([String(true)]);
40+
} else {
41+
setFilterValue(null);
42+
}
43+
}}
44+
isDisabled={isDisabled}
45+
/>
46+
</ToolbarFilter>
47+
);
48+
};

client/src/app/hooks/table-controls/filtering/getFilterHubRequestParams.ts

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -112,21 +112,30 @@ export const getFilterHubRequestParams = <
112112
if (filterCategory.type === "numsearch" && serverFilterValue[0]) {
113113
pushOrMergeFilter(filters, {
114114
field: serverFilterField,
115-
operator: filterCategory.operator ?? "=",
115+
operator:
116+
filterCategory.operator ??
117+
filterCategory.getOperator?.(serverFilterValue) ??
118+
"=",
116119
value: Number(serverFilterValue[0]),
117120
});
118121
}
119122
if (filterCategory.type === "search" && serverFilterValue[0]) {
120123
pushOrMergeFilter(filters, {
121124
field: serverFilterField,
122-
operator: filterCategory.operator ?? "~",
125+
operator:
126+
filterCategory.operator ??
127+
filterCategory.getOperator?.(serverFilterValue) ??
128+
"~",
123129
value: serverFilterValue[0],
124130
});
125131
}
126132
if (filterCategory.type === "select") {
127133
pushOrMergeFilter(filters, {
128134
field: serverFilterField,
129-
operator: filterCategory.operator ?? "=",
135+
operator:
136+
filterCategory.operator ??
137+
filterCategory.getOperator?.(serverFilterValue) ??
138+
"=",
130139
value: serverFilterValue[0],
131140
});
132141
}
@@ -136,7 +145,10 @@ export const getFilterHubRequestParams = <
136145
) {
137146
pushOrMergeFilter(filters, {
138147
field: serverFilterField,
139-
operator: filterCategory.operator ?? "=",
148+
operator:
149+
filterCategory.operator ??
150+
filterCategory.getOperator?.(serverFilterValue) ??
151+
"=",
140152
value: {
141153
list: serverFilterValue,
142154
operator: getFilterLogicOperator(filterCategory, "OR"),
@@ -164,6 +176,16 @@ export const getFilterHubRequestParams = <
164176
// Do nothing as labels do not follow the pattern {field}{operator}{value}
165177
// It is expected for the app to add manually those fields to the REST API
166178
}
179+
if (filterCategory.type === "toggle") {
180+
pushOrMergeFilter(filters, {
181+
field: serverFilterField,
182+
operator:
183+
filterCategory.operator ??
184+
filterCategory.getOperator?.(serverFilterValue) ??
185+
"=",
186+
value: serverFilterValue[0],
187+
});
188+
}
167189
}
168190
}
169191
if (implicitFilters) {

client/src/app/pages/search/components/SearchTabs.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export interface SearchTabsProps {
5151
>;
5252
vulnerabilityFilterPanelProps: IFilterPanelProps<
5353
VulnerabilitySummary,
54-
"" | "base_severity" | "published"
54+
"" | "base_severity" | "published" | "publishedOnly"
5555
>;
5656
};
5757

0 commit comments

Comments
 (0)