fleet/frontend/pages/SoftwarePage/components/VulnerabilitiesCell/VulnerabilitiesCell.tsx
RachelElysia 3dd1219a27
Fleet UI: Filter software/version tables by vulnerability score and exploitability (#21278)
## Issue
Story #19099 
Subtask #20706 

## Description
- Additions to Software > Software tab to filter software and versions
by vulnerable, known exploit, and CVSS score
- Includes a new "Add filters" button which has dynamic tooltip and
button text
- New responsive design to the table header controls
- New modal to customize vulnerability filters
- Handles edge case where user types in a custom CVSS score in URL

## TODO list
- [x] Design, confirm and build empty states
- [x] search bar is showing on empty state, fix this
- [x] Disabled state color for dropdown placeholder text
- [x] Add tests to the modal
- [ ] Test with API when API is ready (good flow to check, choose from
dropdown, then toggle versions on)

## Screen recording
TODO

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

<!-- Note that API documentation changes are now addressed by the
product design team. -->

- [x] Added/updated tests
- [ ] Manual QA for all new/changed functionality
2024-08-20 09:41:49 -04:00

114 lines
2.9 KiB
TypeScript

import React from "react";
import { uniqueId } from "lodash";
import ReactTooltip from "react-tooltip";
import TextCell from "components/TableContainer/DataTable/TextCell";
import { ISoftwareVulnerability } from "interfaces/software";
const NUM_VULNERABILITIES_IN_TOOLTIP = 3;
const baseClass = "vulnerabilities-cell";
const generateCell = (
vulnerabilities: ISoftwareVulnerability[] | string[] | null
) => {
if (vulnerabilities === null || vulnerabilities.length === 0) {
return <TextCell value="---" grey />;
}
let text = "";
let italicize = true;
if (vulnerabilities.length === 1) {
italicize = false;
text =
typeof vulnerabilities[0] === "string"
? vulnerabilities[0]
: vulnerabilities[0].cve;
} else {
text = `${vulnerabilities.length} vulnerabilities`;
}
return <TextCell value={text} italic={italicize} />;
};
const getName = (vulnerabiltiy: ISoftwareVulnerability | string) => {
return typeof vulnerabiltiy === "string" ? vulnerabiltiy : vulnerabiltiy.cve;
};
const condenseVulnerabilities = (
vulnerabilities: ISoftwareVulnerability[] | string[]
) => {
const condensed =
(vulnerabilities?.length &&
vulnerabilities
.slice(-NUM_VULNERABILITIES_IN_TOOLTIP)
.map(getName)
.reverse()) ||
[];
return vulnerabilities.length > NUM_VULNERABILITIES_IN_TOOLTIP
? condensed.concat(
`+${vulnerabilities.length - NUM_VULNERABILITIES_IN_TOOLTIP} more`
)
: condensed;
};
const generateTooltip = (
vulnerabilities: ISoftwareVulnerability[] | string[],
tooltipId: string
) => {
if (vulnerabilities.length <= 1) {
return null;
}
const condensedVulnerabilities = condenseVulnerabilities(vulnerabilities);
return (
<ReactTooltip
effect="solid"
backgroundColor="#3e4771"
id={tooltipId}
data-html
>
<ul className={`${baseClass}__vulnerability-list`}>
{condensedVulnerabilities.map((vulnerability) => {
const key =
typeof vulnerability === "string" ? vulnerability : uniqueId();
return <li key={key}>{vulnerability}</li>;
})}
</ul>
</ReactTooltip>
);
};
interface IVulnerabilitiesCellProps {
vulnerabilities: ISoftwareVulnerability[] | string[] | null;
}
const VulnerabilitiesCell = ({
vulnerabilities,
}: IVulnerabilitiesCellProps) => {
const tooltipId = uniqueId();
// only one vulnerability, no need for tooltip
const cell = generateCell(vulnerabilities);
if (vulnerabilities === null || vulnerabilities.length <= 1) {
return <>{cell}</>;
}
const vulnerabilityTooltip = generateTooltip(vulnerabilities, tooltipId);
return (
<>
<div
className={`${baseClass}__vulnerability-text-with-tooltip`}
data-tip
data-for={tooltipId}
>
{cell}
</div>
{vulnerabilityTooltip}
</>
);
};
export default VulnerabilitiesCell;