/** software/titles Software tab > Table software/versions Software tab > Table (version toggle on) */ import React, { useCallback, useMemo } from "react"; import { InjectedRouter } from "react-router"; import { Row } from "react-table"; import PATHS from "router/paths"; import { getNextLocationPath } from "utilities/helpers"; import { GITHUB_NEW_ISSUE_LINK } from "utilities/constants"; import { convertParamsToSnakeCase, getPathWithQueryParams, } from "utilities/url"; import { ISoftwareApiParams, ISoftwareTitlesResponse, ISoftwareVersionsResponse, } from "services/entities/software"; import { ISoftwareTitle, ISoftwareVersion } from "interfaces/software"; import TableContainer from "components/TableContainer"; import Slider from "components/forms/fields/Slider"; import CustomLink from "components/CustomLink"; import LastUpdatedText from "components/LastUpdatedText"; import { ITableQueryData } from "components/TableContainer/TableContainer"; import TableCount from "components/TableContainer/TableCount"; import Button from "components/buttons/Button"; import Icon from "components/Icon"; import TooltipWrapper from "components/TooltipWrapper"; import { SingleValue } from "react-select-5"; import DropdownWrapper from "components/forms/fields/DropdownWrapper"; import { CustomOptionType } from "components/forms/fields/DropdownWrapper/DropdownWrapper"; import EmptySoftwareTable from "pages/SoftwarePage/components/tables/EmptySoftwareTable"; import generateTitlesTableConfig from "./SoftwareTitlesTableConfig"; import generateVersionsTableConfig from "./SoftwareVersionsTableConfig"; import { ISoftwareDropdownFilterVal, ISoftwareVulnFiltersParams, SOFTWARE_TITLES_DROPDOWN_OPTIONS, buildSoftwareFilterQueryParams, buildSoftwareVulnFiltersQueryParams, getVulnFilterRenderDetails, } from "./helpers"; interface IRowProps extends Row { original: { id?: number; }; } type ITableConfigGenerator = (router: InjectedRouter, teamId?: number) => void; const isSoftwareTitles = ( data?: ISoftwareTitlesResponse | ISoftwareVersionsResponse ): data is ISoftwareTitlesResponse => { if (!data) return false; return (data as ISoftwareTitlesResponse).software_titles !== undefined; }; interface ISoftwareTableProps { router: InjectedRouter; data?: ISoftwareTitlesResponse | ISoftwareVersionsResponse; showVersions: boolean; installableSoftwareExists: boolean; isSoftwareEnabled: boolean; query: string; perPage: number; orderDirection: "asc" | "desc"; orderKey: string; softwareFilter: ISoftwareDropdownFilterVal; vulnFilters: ISoftwareVulnFiltersParams; currentPage: number; teamId?: number; isLoading: boolean; onAddFiltersClick: () => void; } const baseClass = "software-table"; const SoftwareTable = ({ router, data, showVersions, installableSoftwareExists, isSoftwareEnabled, query, perPage, orderDirection, orderKey, softwareFilter, vulnFilters, currentPage, teamId, isLoading, onAddFiltersClick, }: ISoftwareTableProps) => { const currentPath = showVersions ? PATHS.SOFTWARE_VERSIONS : PATHS.SOFTWARE_TITLES; const determineQueryParamChange = useCallback( (newTableQuery: ITableQueryData) => { const changedEntry = Object.entries(newTableQuery).find(([key, val]) => { switch (key) { case "searchQuery": return val !== query; case "sortDirection": return val !== orderDirection; case "sortHeader": return val !== orderKey; case "pageIndex": return val !== currentPage; default: return false; } }); return changedEntry?.[0] ?? ""; }, [currentPage, orderDirection, orderKey, query] ); const generateNewQueryParams = useCallback( (newTableQuery: ITableQueryData, changedParam: string) => { const newQueryParam: Record = { query: newTableQuery.searchQuery, team_id: teamId, order_direction: newTableQuery.sortDirection, order_key: newTableQuery.sortHeader, page: changedParam === "pageIndex" ? newTableQuery.pageIndex : 0, ...buildSoftwareVulnFiltersQueryParams(vulnFilters), }; // Only include these filters when not on “All teams” if (teamId !== undefined) { if (softwareFilter === "installableSoftware") { newQueryParam.available_for_install = "true"; } if (softwareFilter === "selfServiceSoftware") { newQueryParam.self_service = "true"; } } return newQueryParam; }, [softwareFilter, teamId, vulnFilters] ); // NOTE: this is called once on initial render and every time the query changes const onQueryChange = useCallback( (newTableQuery: ITableQueryData) => { // 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); // Note: There may be no changedParam on initial render, but we still may need // to strip unwanted params with generateNewQueryParams so do NOT early return const newRoute = getNextLocationPath({ pathPrefix: currentPath, routeTemplate: "", queryParams: generateNewQueryParams(newTableQuery, changedParam), }); router.replace(newRoute); }, [determineQueryParamChange, generateNewQueryParams, router, currentPath] ); let tableData: ISoftwareTitle[] | ISoftwareVersion[] | undefined; let generateTableConfig: ITableConfigGenerator; if (data === undefined) { tableData; generateTableConfig = () => []; } else if (isSoftwareTitles(data)) { tableData = data.software_titles; generateTableConfig = generateTitlesTableConfig; } else { tableData = data.software; generateTableConfig = generateVersionsTableConfig; } const softwareTableHeaders = useMemo(() => { if (!data) return []; return generateTableConfig(router, teamId); }, [generateTableConfig, data, router, teamId]); // Determines if a user should be able to filter or search in the table const hasData = tableData && tableData.length > 0; const hasQuery = query !== ""; const hasSoftwareFilter = softwareFilter !== "allSoftware"; const vulnFilterDetails = getVulnFilterRenderDetails(vulnFilters); const hasVulnFilters = vulnFilterDetails.filterCount > 0; const showFilterHeaders = isSoftwareEnabled && (hasData || hasQuery || hasSoftwareFilter || hasVulnFilters); const handleShowVersionsToggle = () => { const queryParams: Record = { query, team_id: teamId, order_direction: orderDirection, order_key: orderKey, page: 0, // resets page index ...buildSoftwareFilterQueryParams("allSoftware"), // Reset to all software ...buildSoftwareVulnFiltersQueryParams(vulnFilters), }; router.replace( getNextLocationPath({ pathPrefix: showVersions ? PATHS.SOFTWARE_TITLES : PATHS.SOFTWARE_VERSIONS, routeTemplate: "", queryParams, }) ); }; const handleCustomFilterDropdownChange = ( value: ISoftwareDropdownFilterVal ) => { const queryParams: ISoftwareApiParams = { query, teamId, orderDirection, orderKey, page: 0, // resets page index ...buildSoftwareVulnFiltersQueryParams(vulnFilters), ...buildSoftwareFilterQueryParams(value), }; router.replace( getNextLocationPath({ pathPrefix: currentPath, routeTemplate: "", queryParams: convertParamsToSnakeCase(queryParams), }) ); }; const handleRowSelect = (row: IRowProps) => { if (!row.original.id) return; const detailsPath = showVersions ? PATHS.SOFTWARE_VERSION_DETAILS(row.original.id.toString()) : PATHS.SOFTWARE_TITLE_DETAILS(row.original.id.toString()); router.push(getPathWithQueryParams(detailsPath, { team_id: teamId })); }; const renderSoftwareCount = () => { return ( <> {tableData && data?.counts_updated_at && ( The last time software data was
updated, including vulnerabilities
and host counts. } /> )} ); }; const renderCustomControls = () => { // Hidden when viewing versions table or viewing "All teams" // or Fleet Free if (showVersions || teamId === undefined) { return null; } return (
) => newValue && handleCustomFilterDropdownChange( newValue.value as ISoftwareDropdownFilterVal ) } variant="table-filter" />
); }; const renderCustomFiltersButton = () => { return ( ); }; const renderTableHelpText = () => (
Seeing unexpected software or vulnerabilities?{" "}
); return (
( )} defaultSortHeader={orderKey} defaultSortDirection={orderDirection} pageIndex={currentPage} defaultSearchQuery={query} manualSortBy pageSize={perPage} showMarkAllPages={false} isAllPagesSelected={false} disableNextPage={!data?.meta.has_next_results} searchable={showFilterHeaders} inputPlaceHolder="Search by name or vulnerability (CVE)" onQueryChange={onQueryChange} // additionalQueries serves as a trigger for the useDeepEffect hook // to fire onQueryChange for events happening outside of // the TableContainer. // This is necessary to remove unwanted query params from the URL additionalQueries={softwareFilter} customControl={showFilterHeaders ? renderCustomControls : undefined} customFiltersButton={ showFilterHeaders ? renderCustomFiltersButton : undefined } stackControls renderCount={renderSoftwareCount} renderTableHelpText={renderTableHelpText} disableMultiRowSelect onSelectSingleRow={handleRowSelect} />
); }; export default SoftwareTable;