Skip to content

Commit d99d7f7

Browse files
authored
Merge pull request #232 from devtron-labs/feat/filter-button
feat: add multi select filter button
2 parents 7ef87b1 + 4570076 commit d99d7f7

File tree

18 files changed

+314
-195
lines changed

18 files changed

+314
-195
lines changed

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@devtron-labs/devtron-fe-common-lib",
3-
"version": "0.2.2",
3+
"version": "0.2.3",
44
"description": "Supporting common component library",
55
"type": "module",
66
"main": "dist/index.js",

src/Common/Constants.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -521,3 +521,11 @@ export const DATE_TIME_FORMATS = {
521521
DD_MMM_YYYY_HH_MM: 'DD MMM YYYY, hh:mm',
522522
DD_MMM_YYYY: 'DD MMM YYYY',
523523
}
524+
525+
export const VULNERABILITIES_SORT_PRIORITY = {
526+
critical: 1,
527+
high: 2,
528+
medium: 3,
529+
low: 4,
530+
unknown: 5,
531+
}

src/Common/Security/ScanVulnerabilitiesTable.tsx

Lines changed: 71 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,45 @@
1414
* limitations under the License.
1515
*/
1616

17-
import React from 'react'
1817
import DOMPurify from 'dompurify'
1918
import { ScanVulnerabilitiesTableProps, VulnerabilityType } from '../Types'
2019
import './scanVulnerabilities.css'
20+
import { SortableTableHeaderCell } from '@Common/SortableTableHeaderCell'
21+
import { useMemo } from 'react'
22+
import { numberComparatorBySortOrder, stringComparatorBySortOrder } from '@Shared/Helpers'
23+
import { VulnerabilitiesTableSortKeys } from './types'
24+
import { VULNERABILITIES_SORT_PRIORITY } from '@Common/Constants'
25+
import { useStateFilters } from '@Common/Hooks'
26+
27+
// To be replaced with Scan V2 Modal Table
28+
export default function ScanVulnerabilitiesTable({
29+
vulnerabilities,
30+
hidePolicy,
31+
shouldStick,
32+
}: ScanVulnerabilitiesTableProps) {
33+
const { sortBy, sortOrder, handleSorting } = useStateFilters<VulnerabilitiesTableSortKeys>({
34+
initialSortKey: VulnerabilitiesTableSortKeys.SEVERITY,
35+
})
36+
37+
const sortedVulnerabilities = useMemo(
38+
() =>
39+
vulnerabilities.sort((a, b) => {
40+
if (sortBy === VulnerabilitiesTableSortKeys.PACKAGE) {
41+
return stringComparatorBySortOrder(a.package, b.package, sortOrder)
42+
}
43+
44+
return numberComparatorBySortOrder(
45+
VULNERABILITIES_SORT_PRIORITY[a.severity],
46+
VULNERABILITIES_SORT_PRIORITY[b.severity],
47+
sortOrder,
48+
)
49+
}),
50+
[sortBy, sortOrder, vulnerabilities],
51+
)
52+
53+
const triggerSeveritySorting = () => handleSorting(VulnerabilitiesTableSortKeys.SEVERITY)
54+
const triggerPackageSorting = () => handleSorting(VulnerabilitiesTableSortKeys.PACKAGE)
2155

22-
export default function ScanVulnerabilitiesTable({ vulnerabilities, hidePolicy, shouldStick }: ScanVulnerabilitiesTableProps) {
2356
const renderRow = (vulnerability: VulnerabilityType) => (
2457
<tr
2558
className="dc__security-tab__table-row cursor"
@@ -37,14 +70,21 @@ export default function ScanVulnerabilitiesTable({ vulnerabilities, hidePolicy,
3770
</a>
3871
</td>
3972
<td className="security-tab__cell-severity">
40-
<span className={`dc__fill-${vulnerability.severity?.toLowerCase()}`}>{vulnerability.severity}</span>
73+
<span
74+
className={`severity-chip severity-chip--${vulnerability.severity?.toLowerCase()} dc__capitalize dc__w-fit-content`}
75+
>
76+
{vulnerability.severity}
77+
</span>
4178
</td>
4279
<td className="security-tab__cell-package">{vulnerability.package}</td>
4380
{/* QUERY: Do we need to add DOMPurify at any other key for this table as well? */}
4481
<td className="security-tab__cell-current-ver">
45-
<p className="m-0 cn-9 fs-13 fw-4" dangerouslySetInnerHTML={{
46-
__html: DOMPurify.sanitize(vulnerability.version)
47-
}} />
82+
<p
83+
className="m-0 cn-9 fs-13 fw-4"
84+
dangerouslySetInnerHTML={{
85+
__html: DOMPurify.sanitize(vulnerability.version),
86+
}}
87+
/>
4888
</td>
4989
<td className="security-tab__cell-fixed-ver">{vulnerability.fixedVersion}</td>
5090
{!hidePolicy && (
@@ -60,17 +100,35 @@ export default function ScanVulnerabilitiesTable({ vulnerabilities, hidePolicy,
60100
return (
61101
<table className="security-tab__table">
62102
<tbody>
63-
<tr className={`security-tab__table-header ${shouldStick ? 'dc__position-sticky bcn-0 dc__zi-4 dc__top-0' : ''}`}>
103+
<tr
104+
className={`security-tab__table-header ${shouldStick ? 'dc__position-sticky bcn-0 dc__zi-4 dc__top-0' : ''}`}
105+
>
64106
<th className="security-cell-header security-tab__cell-cve">CVE</th>
65-
<th className="security-cell-header security-tab__cell-severity">Severity</th>
66-
<th className="security-cell-header security-tab__cell-package">Package</th>
107+
<th className="security-cell-header security-tab__cell-severity">
108+
<SortableTableHeaderCell
109+
title="Severity"
110+
isSorted={sortBy === VulnerabilitiesTableSortKeys.SEVERITY}
111+
isSortable
112+
sortOrder={sortOrder}
113+
triggerSorting={triggerSeveritySorting}
114+
disabled={false}
115+
/>
116+
</th>
117+
<th className="security-cell-header security-tab__cell-package">
118+
<SortableTableHeaderCell
119+
title="Package"
120+
isSorted={sortBy === VulnerabilitiesTableSortKeys.PACKAGE}
121+
isSortable
122+
sortOrder={sortOrder}
123+
triggerSorting={triggerPackageSorting}
124+
disabled={false}
125+
/>
126+
</th>
67127
<th className="security-cell-header security-tab__cell-current-ver">Current Version</th>
68128
<th className="security-cell-header security-tab__cell-fixed-ver">Fixed In Version</th>
69-
{!hidePolicy && (
70-
<th className="security-cell-header security-tab__cell-policy">Policy</th>
71-
)}
129+
{!hidePolicy && <th className="security-cell-header security-tab__cell-policy">Policy</th>}
72130
</tr>
73-
{vulnerabilities.map((vulnerability) => renderRow(vulnerability))}
131+
{sortedVulnerabilities.map((vulnerability) => renderRow(vulnerability))}
74132
</tbody>
75133
</table>
76134
)

src/Common/Security/types.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export enum VulnerabilitiesTableSortKeys {
2+
SEVERITY = 'severity',
3+
PACKAGE = 'package',
4+
}

src/Common/SegmentedBarChart/SegmentedBarChart.tsx

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,9 @@ const SegmentedBarChart: React.FC<SegmentedBarChartProps> = ({
2626
labelClassName,
2727
}) => {
2828
const total = entities.reduce((sum, entity) => entity.value + sum, 0)
29+
const filteredEntities = entities.filter((entity) => entity.value)
2930

30-
const calcSegmentWidth = (entity: Entity) => {
31-
if (!entity.value) {
32-
return '100%'
33-
}
34-
return `${(entity.value / total) * 100}%`
35-
}
31+
const calcSegmentWidth = (entity: Entity) => `${(entity.value / total) * 100}%`
3632

3733
return (
3834
<div className={`flexbox-col w-100 dc__gap-12 ${rootClassName}`}>
@@ -50,7 +46,7 @@ const SegmentedBarChart: React.FC<SegmentedBarChartProps> = ({
5046
))}
5147
</div>
5248
<div className="flexbox dc__gap-2">
53-
{entities?.map((entity, index, map) => (
49+
{filteredEntities?.map((entity, index, map) => (
5450
<div
5551
key={entity.label}
5652
className={`h-8 ${index === 0 ? 'dc__left-radius-4' : ''} ${

src/Common/Types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import React, { ReactNode, CSSProperties } from 'react'
1818
import { Placement } from 'tippy.js'
1919
import { ImageComment, ReleaseTag } from './ImageTags.Types'
2020
import { ACTION_STATE, DEPLOYMENT_WINDOW_TYPE, DockerConfigOverrideType, SortingOrder, TaskErrorObj } from '.'
21-
import { RegistryType } from '../Shared'
21+
import { RegistryType, Severity } from '../Shared'
2222

2323
/**
2424
* Generic response type object with support for overriding the result type
@@ -569,7 +569,7 @@ export enum DeploymentAppTypes {
569569

570570
export interface VulnerabilityType {
571571
name: string
572-
severity: 'CRITICAL' | 'MODERATE' | 'LOW'
572+
severity: Severity
573573
package: string
574574
version: string
575575
fixedVersion: string
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
/*
2+
* Copyright (c) 2024. Devtron Inc.
3+
*/
4+
5+
import React, { cloneElement, useEffect, useState } from 'react'
6+
import ReactSelect, { MenuListProps, components } from 'react-select'
7+
import { Option } from '@Common/MultiSelectCustomization'
8+
import { OptionType } from '@Common/Types'
9+
import { ReactComponent as ICCaretDown } from '../../../Assets/Icon/ic-caret-down.svg'
10+
import { getFilterStyle } from './utils'
11+
import { FilterButtonPropsType } from './types'
12+
13+
const ValueContainer = (props: any) => {
14+
const { selectProps, getValue, children } = props
15+
const selectedProjectLen = getValue().length
16+
17+
return (
18+
<components.ValueContainer {...props}>
19+
{!selectProps.inputValue &&
20+
(!selectProps.menuIsOpen ? (
21+
<div className="flexbox dc__gap-8 dc__align-items-center">
22+
<div className="fs-13 fw-4 cn-9">{selectProps.placeholder}</div>
23+
{selectedProjectLen > 0 && (
24+
<div className="bcb-5 dc__border-radius-4-imp pl-5 pr-5 cn-0 fs-12 fw-6 lh-18">
25+
{selectedProjectLen}
26+
</div>
27+
)}
28+
</div>
29+
) : (
30+
<span className="dc__position-abs cn-5 ml-2">{selectProps.placeholder}</span>
31+
))}
32+
33+
{cloneElement(children[1])}
34+
</components.ValueContainer>
35+
)
36+
}
37+
38+
const FilterSelectMenuList: React.FC<MenuListProps> = (props) => {
39+
const {
40+
children,
41+
// @ts-ignore NOTE: handleApplyFilter is passed from FilterButton
42+
selectProps: { handleApplyFilter },
43+
} = props
44+
45+
return (
46+
<components.MenuList {...props}>
47+
{children}
48+
<div className="p-8 dc__position-sticky dc__bottom-0 dc__border-top-n1 bcn-0">
49+
<button
50+
type="button"
51+
className="dc__unset-button-styles w-100 br-4 h-28 flex bcb-5 cn-0 fw-6 lh-28 fs-12 h-28 br-4 pt-5 pr-12 pb-5 pl-12"
52+
onClick={handleApplyFilter}
53+
aria-label="Apply filters"
54+
>
55+
Apply
56+
</button>
57+
</div>
58+
</components.MenuList>
59+
)
60+
}
61+
62+
const DropdownIndicator = (props) => (
63+
<components.DropdownIndicator {...props}>
64+
<ICCaretDown className="icon-dim-20 icon-n5" />
65+
</components.DropdownIndicator>
66+
)
67+
68+
// To be replaced with MultiSelectPicker
69+
const FilterButton: React.FC<FilterButtonPropsType> = ({
70+
placeholder,
71+
appliedFilters,
72+
options,
73+
disabled,
74+
handleApplyChange,
75+
getFormattedFilterLabelValue,
76+
menuAlignFromRight,
77+
controlWidth,
78+
}) => {
79+
const [menuIsOpen, setMenuIsOpen] = useState<boolean>(false)
80+
const [selectedOptions, setSelectedOptions] = useState<OptionType[]>(
81+
appliedFilters.map((filter) => ({ value: filter, label: getFormattedFilterLabelValue?.(filter) || filter })),
82+
)
83+
84+
useEffect(() => {
85+
setSelectedOptions(
86+
appliedFilters.map((filter) => ({
87+
value: filter,
88+
label: getFormattedFilterLabelValue?.(filter) || filter,
89+
})),
90+
)
91+
}, [appliedFilters])
92+
93+
const handleMenuOpen = () => {
94+
setMenuIsOpen(true)
95+
}
96+
97+
const handleMenuClose = () => {
98+
setMenuIsOpen(false)
99+
}
100+
101+
const handleSelectOnChange: React.ComponentProps<typeof ReactSelect>['onChange'] = (selected: OptionType[]) => {
102+
setSelectedOptions([...selected])
103+
}
104+
105+
const handleApply = () => {
106+
handleApplyChange(Object.values(selectedOptions).map((option) => option.value))
107+
handleMenuClose()
108+
}
109+
110+
return (
111+
<ReactSelect
112+
placeholder={placeholder}
113+
isMulti
114+
isDisabled={disabled}
115+
options={options}
116+
value={selectedOptions}
117+
onChange={handleSelectOnChange}
118+
closeMenuOnSelect={false}
119+
controlShouldRenderValue={false}
120+
hideSelectedOptions={false}
121+
maxMenuHeight={300}
122+
components={{
123+
MenuList: FilterSelectMenuList,
124+
IndicatorSeparator: null,
125+
ClearIndicator: null,
126+
DropdownIndicator,
127+
Option,
128+
ValueContainer,
129+
}}
130+
styles={getFilterStyle(controlWidth, menuAlignFromRight)}
131+
// @ts-ignore NOTE: passing this to use in FilterSelectMenuList
132+
handleApplyFilter={handleApply}
133+
menuIsOpen={menuIsOpen}
134+
onMenuOpen={handleMenuOpen}
135+
onMenuClose={handleMenuClose}
136+
/>
137+
)
138+
}
139+
140+
export default FilterButton
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default as FilterButton } from './FilterButton'
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { OptionType } from '@Common/Types'
2+
3+
export interface FilterButtonPropsType {
4+
placeholder: string
5+
appliedFilters: string[]
6+
options: OptionType[]
7+
handleApplyChange: (selectedOptions: string[]) => void
8+
disabled?: boolean
9+
getFormattedFilterLabelValue?: (identifier: string) => string
10+
menuAlignFromRight?: boolean
11+
controlWidth?: string
12+
}

0 commit comments

Comments
 (0)