fleet/frontend/pages/SoftwarePage/SoftwareVulnerabilities/SoftwareVulnerabilitiesTable/SoftwareVulnerabilitiesTable.tsx
RachelElysia c9e66b221e
Frontend: Lint warning cleanup part 1 (#43411)
## Issue
- First batch of @iansltx 's work of cleaning up lint warnings #43387 

## Description
- Quick PR review and grabbed as many confirmed low-risk quick wins as I
could `git checkout lint-cleanup <file/path/1> <file/path/2>`

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

## Release Notes

This release contains internal code improvements with one minor UI
tweak:

* **Style**
* Dropdown menu background color adjusted for clearer contrast in action
lists
* **Refactor**
* Improved type safety across the codebase with stricter TypeScript
annotations
  * Removed unused imports and constants to reduce code clutter
* Enhanced React hook dependency arrays for more consistent component
behavior
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Rachel Perkins <rachel@Rachels-MacBook-Pro.local>
Co-authored-by: Ian Littman <iansltx@gmail.com>
2026-04-10 19:49:52 -05:00

296 lines
8.6 KiB
TypeScript

/** software/vulnerabilities Vulnerabilities tab > Table */
import React, { useCallback, useContext, useMemo } from "react";
import { InjectedRouter } from "react-router";
import { Row } from "react-table";
import PATHS from "router/paths";
import { AppContext } from "context/app";
import {
GITHUB_NEW_ISSUE_LINK,
VULNERABILITIES_SEARCH_BOX_TOOLTIP,
} from "utilities/constants";
import { isIncompleteQuoteQuery } from "utilities/strings/stringUtils";
import CustomLink from "components/CustomLink";
import TableContainer from "components/TableContainer";
import LastUpdatedText from "components/LastUpdatedText";
import { ITableQueryData } from "components/TableContainer/TableContainer";
import TableCount from "components/TableContainer/TableCount";
import { SingleValue } from "react-select-5";
import DropdownWrapper from "components/forms/fields/DropdownWrapper";
import { CustomOptionType } from "components/forms/fields/DropdownWrapper/DropdownWrapper";
import EmptyVulnerabilitiesTable from "pages/SoftwarePage/components/tables/SoftwareVulnerabilitiesTable/EmptyVulnerabilitiesTable";
import {
IVulnerabilitiesResponse,
IVulnerabilitiesEmptyStateReason,
} from "services/entities/vulnerabilities";
import { getPathWithQueryParams } from "utilities/url";
import { getNextLocationPath } from "utilities/helpers";
import generateTableConfig from "./VulnerabilitiesTableConfig";
import { getExploitedVulnerabilitiesDropdownOptions } from "./helpers";
const baseClass = "software-vulnerabilities-table";
interface IRowProps extends Row {
original: {
cve?: string;
};
}
interface ISoftwareVulnerabilitiesTableProps {
router: InjectedRouter;
isSoftwareEnabled: boolean;
data?: IVulnerabilitiesResponse;
emptyStateReason?: IVulnerabilitiesEmptyStateReason;
query?: string;
perPage: number;
orderDirection: "asc" | "desc";
orderKey: string;
showExploitedVulnerabilitiesOnly: boolean;
currentPage: number;
teamId?: number;
isLoading: boolean;
}
const SoftwareVulnerabilitiesTable = ({
router,
isSoftwareEnabled,
data,
emptyStateReason,
query,
perPage,
orderDirection,
orderKey,
showExploitedVulnerabilitiesOnly,
currentPage,
teamId,
isLoading,
}: ISoftwareVulnerabilitiesTableProps) => {
const { isPremiumTier } = useContext(AppContext);
const determineQueryParamChange = useCallback(
(newTableQuery: ITableQueryData) => {
const changedEntry = Object.entries(newTableQuery).find(([key, val]) => {
switch (key) {
case "sortDirection":
return val !== orderDirection;
case "sortHeader":
return val !== orderKey;
case "pageIndex":
return val !== currentPage;
case "searchQuery":
return val !== query;
case "exploit":
return val !== showExploitedVulnerabilitiesOnly.toString();
default:
return false;
}
});
return changedEntry?.[0] ?? "";
},
[
currentPage,
orderDirection,
orderKey,
query,
showExploitedVulnerabilitiesOnly,
]
);
const generateNewQueryParams = useCallback(
(newTableQuery: ITableQueryData, changedParam: string) => {
return {
fleet_id: teamId,
exploit: showExploitedVulnerabilitiesOnly.toString(),
query: newTableQuery.searchQuery,
order_direction: newTableQuery.sortDirection,
order_key: newTableQuery.sortHeader,
page: changedParam === "pageIndex" ? newTableQuery.pageIndex : 0,
};
},
[teamId, showExploitedVulnerabilitiesOnly]
);
const onQueryChange = useCallback(
(newTableQuery: ITableQueryData) => {
// We don't want to start searching until a user completes their quote query
if (isIncompleteQuoteQuery(newTableQuery.searchQuery)) {
return;
}
// we want to determine which query param has changed in order to
// reset the page index to 0 if any other param has changed.
const changedParam = determineQueryParamChange(newTableQuery);
// if nothing has changed, don't update the route. this can happen when
// this handler is called on the inital render.
if (changedParam === "") return;
const newRoute = getNextLocationPath({
pathPrefix: PATHS.SOFTWARE_VULNERABILITIES,
routeTemplate: "",
queryParams: generateNewQueryParams(newTableQuery, changedParam),
});
router.replace(newRoute);
},
[determineQueryParamChange, generateNewQueryParams, router]
);
// determines if a user be able to search in the table
const searchable =
isSoftwareEnabled &&
(!!data?.vulnerabilities ||
query !== "" ||
showExploitedVulnerabilitiesOnly);
const vulnerabilitiesTableHeaders = useMemo(() => {
if (!data) return [];
return generateTableConfig(
isPremiumTier,
router,
{
includeName: true,
includeVulnerabilities: true,
includeIcon: true,
},
teamId
);
}, [data, isPremiumTier, router, teamId]);
const handleExploitedVulnFilterDropdownChange = (
isFilterExploited: string
) => {
router.replace(
getNextLocationPath({
pathPrefix: PATHS.SOFTWARE_VULNERABILITIES,
routeTemplate: "",
queryParams: {
query,
fleet_id: teamId,
order_direction: orderDirection,
order_key: orderKey,
exploit: isFilterExploited,
page: 0, // resets page index
},
})
);
};
const handleRowSelect = (row: IRowProps) => {
if (row.original.cve) {
const cveName = row.original.cve.toString();
const softwareVulnerabilityDetailsPath = getPathWithQueryParams(
PATHS.SOFTWARE_VULNERABILITY_DETAILS(cveName),
{
fleet_id: teamId,
}
);
router.push(softwareVulnerabilityDetailsPath);
}
};
const renderVulnerabilityCount = () => {
if (!data) return null;
const count = data?.count;
return (
<>
<TableCount name="items" count={count} />
{data?.vulnerabilities && data?.counts_updated_at && (
<LastUpdatedText
lastUpdatedAt={data.counts_updated_at}
customTooltipText={
<>
The last time software data was <br />
updated, including vulnerabilities <br />
and host counts.
</>
}
/>
)}
</>
);
};
const renderTableHelpText = () => {
return (
<div>
Seeing unexpected software or vulnerabilities?{" "}
<CustomLink
url={GITHUB_NEW_ISSUE_LINK}
text="File an issue on GitHub"
newTab
/>
</div>
);
};
// Exploited vulnerabilities is a premium feature
const renderExploitedVulnerabilitiesDropdown = () => {
return (
<DropdownWrapper
name="exploited-vuln-filter"
value={showExploitedVulnerabilitiesOnly.toString()}
className={`${baseClass}__exploited-vulnerabilities-filter`}
options={getExploitedVulnerabilitiesDropdownOptions(isPremiumTier)}
onChange={(newValue: SingleValue<CustomOptionType>) =>
newValue && handleExploitedVulnFilterDropdownChange(newValue.value)
}
variant="table-filter"
/>
);
};
return (
<div className={baseClass}>
<TableContainer
columnConfigs={vulnerabilitiesTableHeaders}
data={data?.vulnerabilities || []}
isLoading={isLoading}
resultsTitle="items"
emptyComponent={() => (
<EmptyVulnerabilitiesTable
isPremiumTier={isPremiumTier}
teamId={teamId}
exploitedFilter={showExploitedVulnerabilitiesOnly}
isSoftwareDisabled={!isSoftwareEnabled}
emptyStateReason={emptyStateReason}
/>
)}
defaultSearchQuery={query}
defaultSortHeader={orderKey}
defaultSortDirection={orderDirection}
pageIndex={currentPage}
manualSortBy
pageSize={perPage}
showMarkAllPages={false}
isAllPagesSelected={false}
disableNextPage={!data?.meta.has_next_results}
searchable={searchable}
searchQueryColumn="vulnerability"
inputPlaceHolder="Search by CVE"
searchToolTipText={VULNERABILITIES_SEARCH_BOX_TOOLTIP}
onQueryChange={onQueryChange}
customControl={
searchable ? renderExploitedVulnerabilitiesDropdown : undefined
}
stackControls
renderCount={renderVulnerabilityCount}
renderTableHelpText={renderTableHelpText}
disableMultiRowSelect
onSelectSingleRow={handleRowSelect}
/>
</div>
);
};
export default SoftwareVulnerabilitiesTable;