mirror of
https://github.com/empayre/fleet.git
synced 2024-11-06 08:55:24 +00:00
Dashboard: Show software count in dashboard's software modal (#3228)
This commit is contained in:
parent
37ffb88ec8
commit
b397b3a68e
1
changes/issue-2935-show-software-count
Normal file
1
changes/issue-2935-show-software-count
Normal file
@ -0,0 +1 @@
|
||||
* Software modal on homepage shows exact software count with filterability
|
@ -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>
|
||||
|
@ -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 };
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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}`;
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user