diff --git a/changes/issue-2935-show-software-count b/changes/issue-2935-show-software-count new file mode 100644 index 000000000..f690d32ee --- /dev/null +++ b/changes/issue-2935-show-software-count @@ -0,0 +1 @@ +* Software modal on homepage shows exact software count with filterability \ No newline at end of file diff --git a/frontend/pages/Homepage/cards/Software/Software.tsx b/frontend/pages/Homepage/cards/Software/Software.tsx index 092ee09f1..43cc3940b 100644 --- a/frontend/pages/Homepage/cards/Software/Software.tsx +++ b/frontend/pages/Homepage/cards/Software/Software.tsx @@ -1,16 +1,20 @@ -import React, { useState } from "react"; +import React, { useState, useEffect } from "react"; import { useQuery } from "react-query"; import { Tab, Tabs, TabList, TabPanel } from "react-tabs"; import softwareAPI from "services/entities/software"; -import { ISoftware } from "interfaces/software"; +import { ISoftware } from "interfaces/software"; // @ts-ignore +import debounce from "utilities/debounce"; import Modal from "components/Modal"; import TabsWrapper from "components/TabsWrapper"; import TableContainer from "components/TableContainer"; // @ts-ignore import Dropdown from "components/forms/fields/Dropdown"; -import { generateTableHeaders } from "./SoftwareTableConfig"; +import { + generateTableHeaders, + generateModalSoftwareTableHeaders, +} from "./SoftwareTableConfig"; interface ITableQueryProps { pageIndex: number; @@ -114,6 +118,7 @@ const Software = ({ isModalSoftwareVulnerable, setIsModalSoftwareVulnerable, ] = useState(false); + const [modalSoftwareState, setModalSoftwareState] = useState([]); const [navTabIndex, setNavTabIndex] = useState(0); const [isLoadingSoftware, setIsLoadingSoftware] = useState(true); const [ @@ -183,7 +188,6 @@ const Software = ({ setIsLoadingModalSoftware(true); return softwareAPI.load({ page: modalSoftwarePageIndex, - perPage: MODAL_PAGE_SIZE, query: modalSoftwareSearchText, orderKey: "id", orderDir: "desc", @@ -217,16 +221,28 @@ const Software = ({ } }; - const onModalSoftwareQueryChange = async ({ - pageIndex, - searchQuery, - }: ITableQueryProps) => { - setModalSoftwareSearchText(searchQuery); + const onModalSoftwareQueryChange = debounce( + async ({ pageIndex, searchQuery }: ITableQueryProps) => { + setModalSoftwareSearchText(searchQuery); - if (pageIndex !== modalSoftwarePageIndex) { - setModalSoftwarePageIndex(pageIndex); - } - }; + if (pageIndex !== modalSoftwarePageIndex) { + setModalSoftwarePageIndex(pageIndex); + } + }, + { leading: false, trailing: true } + ); + + useEffect(() => { + setModalSoftwareState(() => { + return ( + modalSoftware?.filter((softwareItem) => { + return softwareItem.name + .toLowerCase() + .includes(modalSoftwareSearchText.toLowerCase()); + }) || [] + ); + }); + }, [modalSoftware, modalSoftwareSearchText]); const renderStatusDropdown = () => { return ( @@ -300,12 +316,13 @@ const Software = ({ it installed.

EmptySoftware( @@ -315,11 +332,12 @@ const Software = ({ showMarkAllPages={false} isAllPagesSelected={false} searchable - disableCount disableActionButton pageSize={MODAL_PAGE_SIZE} onQueryChange={onModalSoftwareQueryChange} customControl={renderStatusDropdown} + isClientSidePagination + isClientSideSearch /> diff --git a/frontend/pages/Homepage/cards/Software/SoftwareTableConfig.tsx b/frontend/pages/Homepage/cards/Software/SoftwareTableConfig.tsx index d497e6b6f..f7f199684 100644 --- a/frontend/pages/Homepage/cards/Software/SoftwareTableConfig.tsx +++ b/frontend/pages/Homepage/cards/Software/SoftwareTableConfig.tsx @@ -1,12 +1,15 @@ import React from "react"; import { Link } from "react-router"; +import ReactTooltip from "react-tooltip"; +import { isEmpty } from "lodash"; + import PATHS from "router/paths"; import { ISoftware } from "interfaces/software"; -import HeaderCell from "components/TableContainer/DataTable/HeaderCell/HeaderCell"; import TextCell from "components/TableContainer/DataTable/TextCell"; import Chevron from "../../../../../assets/images/icon-chevron-blue-16x16@2x.png"; +import IssueIcon from "../../../../../assets/images/icon-issue-fleet-black-50-16x16@2x.png"; interface IHeaderProps { column: { @@ -33,41 +36,88 @@ interface IDataColumn { disableSortBy?: boolean; } +const vulnerabilityTableHeader = [ + { + title: "Vulnerabilities", + Header: "", + disableSortBy: true, + accessor: "vulnerabilities", + Cell: (cellProps: ICellProps) => { + const vulnerabilities = cellProps.cell.value; + if (isEmpty(vulnerabilities)) { + return <>; + } + return ( + <> + + software vulnerabilities + + + + {vulnerabilities.length === 1 + ? "1 vulnerability detected" + : `${vulnerabilities.length} vulnerabilities detected`} + + + + ); + }, + }, +]; + +const softwareTableHeaders = [ + { + title: "Name", + Header: "Name", + disableSortBy: true, + accessor: "name", + Cell: (cellProps: ICellProps) => , + }, + { + title: "Version", + Header: "Version", + disableSortBy: true, + accessor: "version", + Cell: (cellProps: ICellProps) => , + }, + { + title: "Actions", + Header: "", + disableSortBy: true, + accessor: "id", + Cell: (cellProps: ICellProps) => { + return ( + + link to hosts filtered by software ID + + ); + }, + }, +]; + // NOTE: cellProps come from react-table // more info here https://react-table.tanstack.com/docs/api/useTable#cell-properties -export const generateTableHeaders = (): IDataColumn[] => { - return [ - { - title: "Name", - Header: "Name", - disableSortBy: true, - accessor: "name", - Cell: (cellProps) => , - }, - { - title: "Version", - Header: "Version", - disableSortBy: true, - accessor: "version", - Cell: (cellProps) => , - }, - { - title: "Actions", - Header: "", - disableSortBy: true, - accessor: "id", - Cell: (cellProps) => { - return ( - - link to hosts filtered by software ID - - ); - }, - }, - ]; +const generateTableHeaders = (): IDataColumn[] => { + return softwareTableHeaders; }; -export default generateTableHeaders; +const generateModalSoftwareTableHeaders = (): IDataColumn[] => { + return vulnerabilityTableHeader.concat(softwareTableHeaders); +}; + +export { generateTableHeaders, generateModalSoftwareTableHeaders }; diff --git a/frontend/pages/Homepage/cards/Software/_styles.scss b/frontend/pages/Homepage/cards/Software/_styles.scss index 98cbb1986..d8fe1f7be 100644 --- a/frontend/pages/Homepage/cards/Software/_styles.scss +++ b/frontend/pages/Homepage/cards/Software/_styles.scss @@ -22,6 +22,8 @@ display: none; } &__software-modal { + width: 780px; + .data-table__wrapper { margin-top: $pad-large; } @@ -89,8 +91,13 @@ table-layout: fixed; thead { + .vulnerabilities__header { + width: 4%; + border-right: 0; + padding-right: 0; + } .name__header { - width: 70%; + width: 60%; } .version__header { width: 30%; @@ -111,7 +118,8 @@ } tbody { - td { + .name__cell, + .version__cell { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; @@ -120,6 +128,11 @@ padding: 0; width: 40px; } + .vulnerabilities__cell { + img { + transform: scale(0.5); + } + } } } } diff --git a/frontend/services/entities/software.ts b/frontend/services/entities/software.ts index e44099bfa..a521063f6 100644 --- a/frontend/services/entities/software.ts +++ b/frontend/services/entities/software.ts @@ -4,7 +4,7 @@ import { ISoftware } from "interfaces/software"; interface IGetSoftwareProps { page: number; - perPage: number; + perPage?: number; orderKey: string; orderDir: "asc" | "desc"; query: string; @@ -32,7 +32,7 @@ export default { teamId, }: ISoftwareParams): Promise => { const { SOFTWARE } = endpoints; - const pagination = `page=${page}&per_page=${perPage}`; + const pagination = perPage ? `page=${page}&per_page=${perPage}` : ""; const sort = `order_key=${orderKey}&order_direction=${orderDir}`; let path = `${SOFTWARE}?${pagination}&${sort}`;