Dashboard: Show software count in dashboard's software modal (#3228)

This commit is contained in:
RachelElysia 2021-12-09 00:30:28 -06:00 committed by GitHub
parent 37ffb88ec8
commit b397b3a68e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 137 additions and 55 deletions

View File

@ -0,0 +1 @@
* Software modal on homepage shows exact software count with filterability

View File

@ -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<boolean>(false);
const [modalSoftwareState, setModalSoftwareState] = useState<ISoftware[]>([]);
const [navTabIndex, setNavTabIndex] = useState<number>(0);
const [isLoadingSoftware, setIsLoadingSoftware] = useState<boolean>(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.
</p>
<TableContainer
columns={tableHeaders}
data={modalSoftware || []}
columns={generateModalSoftwareTableHeaders()}
data={modalSoftwareState}
isLoading={isLoadingModalSoftware}
defaultSortHeader={"name"}
defaultSortDirection={"asc"}
hideActionButton
filteredCount={modalSoftwareState.length}
resultsTitle={"software items"}
emptyComponent={() =>
EmptySoftware(
@ -315,11 +332,12 @@ const Software = ({
showMarkAllPages={false}
isAllPagesSelected={false}
searchable
disableCount
disableActionButton
pageSize={MODAL_PAGE_SIZE}
onQueryChange={onModalSoftwareQueryChange}
customControl={renderStatusDropdown}
isClientSidePagination
isClientSideSearch
/>
</>
</Modal>

View File

@ -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 (
<>
<span
className={`vulnerabilities tooltip__tooltip-icon`}
data-tip
data-for={`vulnerabilities__${cellProps.row.original.id.toString()}`}
data-tip-disable={false}
>
<img alt="software vulnerabilities" src={IssueIcon} />
</span>
<ReactTooltip
place="bottom"
type="dark"
effect="solid"
backgroundColor="#3e4771"
id={`vulnerabilities__${cellProps.row.original.id.toString()}`}
data-html
>
<span className={`vulnerabilities tooltip__tooltip-text`}>
{vulnerabilities.length === 1
? "1 vulnerability detected"
: `${vulnerabilities.length} vulnerabilities detected`}
</span>
</ReactTooltip>
</>
);
},
},
];
const softwareTableHeaders = [
{
title: "Name",
Header: "Name",
disableSortBy: true,
accessor: "name",
Cell: (cellProps: ICellProps) => <TextCell value={cellProps.cell.value} />,
},
{
title: "Version",
Header: "Version",
disableSortBy: true,
accessor: "version",
Cell: (cellProps: ICellProps) => <TextCell value={cellProps.cell.value} />,
},
{
title: "Actions",
Header: "",
disableSortBy: true,
accessor: "id",
Cell: (cellProps: ICellProps) => {
return (
<Link
to={`${PATHS.MANAGE_HOSTS}?software_id=${cellProps.cell.value}`}
className="software-link"
>
<img alt="link to hosts filtered by software ID" src={Chevron} />
</Link>
);
},
},
];
// 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) => <TextCell value={cellProps.cell.value} />,
},
{
title: "Version",
Header: "Version",
disableSortBy: true,
accessor: "version",
Cell: (cellProps) => <TextCell value={cellProps.cell.value} />,
},
{
title: "Actions",
Header: "",
disableSortBy: true,
accessor: "id",
Cell: (cellProps) => {
return (
<Link
to={`${PATHS.MANAGE_HOSTS}?software_id=${cellProps.cell.value}`}
className="software-link"
>
<img alt="link to hosts filtered by software ID" src={Chevron} />
</Link>
);
},
},
];
const generateTableHeaders = (): IDataColumn[] => {
return softwareTableHeaders;
};
export default generateTableHeaders;
const generateModalSoftwareTableHeaders = (): IDataColumn[] => {
return vulnerabilityTableHeader.concat(softwareTableHeaders);
};
export { generateTableHeaders, generateModalSoftwareTableHeaders };

View File

@ -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);
}
}
}
}
}

View File

@ -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<ISoftware[]> => {
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}`;