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 (
+ <>
+
+
+
+
+
+ {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 (
+
+
+
+ );
+ },
+ },
+];
+
// 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 (
-
-
-
- );
- },
- },
- ];
+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}`;