From 05a6f06d12e7d37a29b5ac99882fad2b2ebb8040 Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Tue, 22 Nov 2022 11:13:33 -0500 Subject: [PATCH] Fleet UI: Allow software table rows to be clickable (#8772) --- ...ue-8389-add-clickable-rows-software-tables | 1 + .../TableContainer/TableContainer.tsx | 7 +++- .../pages/DashboardPage/DashboardPage.tsx | 1 + .../DashboardPage/cards/Software/Software.tsx | 28 +++++++++++++++ .../HostDetailsPage/HostDetailsPage.tsx | 1 + .../hosts/details/cards/Software/Software.tsx | 35 +++++++++++++++++-- .../cards/Software/SoftwareTableConfig.tsx | 20 ++++++++--- .../ManageSoftwarePage/ManageSoftwarePage.tsx | 25 +++++++++++-- .../SoftwareTableConfig.tsx | 20 ++++++++--- 9 files changed, 123 insertions(+), 15 deletions(-) create mode 100644 changes/issue-8389-add-clickable-rows-software-tables diff --git a/changes/issue-8389-add-clickable-rows-software-tables b/changes/issue-8389-add-clickable-rows-software-tables new file mode 100644 index 0000000000..d93b778ed2 --- /dev/null +++ b/changes/issue-8389-add-clickable-rows-software-tables @@ -0,0 +1 @@ +- Software tables now have clickable rows to direct to view all hosts filtered by software diff --git a/frontend/components/TableContainer/TableContainer.tsx b/frontend/components/TableContainer/TableContainer.tsx index cc23e08032..9099a172a4 100644 --- a/frontend/components/TableContainer/TableContainer.tsx +++ b/frontend/components/TableContainer/TableContainer.tsx @@ -21,6 +21,11 @@ export interface ITableQueryData { sortHeader: string; sortDirection: string; } +interface IRowProps extends Row { + original: { + id?: number; + }; +} interface ITableContainerProps { columns: any; // TODO: Figure out type @@ -73,7 +78,7 @@ interface ITableContainerProps { onPrimarySelectActionClick?: (selectedItemIds: number[]) => void; customControl?: () => JSX.Element; stackControls?: boolean; - onSelectSingleRow?: (value: Row) => void; + onSelectSingleRow?: (value: Row | IRowProps) => void; filters?: Record; renderCount?: () => JSX.Element | null; renderFooter?: () => JSX.Element | null; diff --git a/frontend/pages/DashboardPage/DashboardPage.tsx b/frontend/pages/DashboardPage/DashboardPage.tsx index e827a602dc..7eb21c6565 100644 --- a/frontend/pages/DashboardPage/DashboardPage.tsx +++ b/frontend/pages/DashboardPage/DashboardPage.tsx @@ -488,6 +488,7 @@ const DashboardPage = ({ navTabIndex={softwareNavTabIndex} onTabChange={onSoftwareTabChange} onQueryChange={onSoftwareQueryChange} + router={router} /> ), }); diff --git a/frontend/pages/DashboardPage/cards/Software/Software.tsx b/frontend/pages/DashboardPage/cards/Software/Software.tsx index c61e8f9948..d014f3f57a 100644 --- a/frontend/pages/DashboardPage/cards/Software/Software.tsx +++ b/frontend/pages/DashboardPage/cards/Software/Software.tsx @@ -1,10 +1,16 @@ import React from "react"; import { Tab, Tabs, TabList, TabPanel } from "react-tabs"; +import { Row } from "react-table"; +import { InjectedRouter } from "react-router"; +import PATHS from "router/paths"; + +import { buildQueryStringFromParams } from "utilities/url"; import TabsWrapper from "components/TabsWrapper"; import TableContainer from "components/TableContainer"; import TableDataError from "components/DataError"; import Spinner from "components/Spinner"; + import generateTableHeaders from "./SoftwareTableConfig"; import EmptySoftware from "../../../software/components/EmptySoftware"; @@ -18,6 +24,13 @@ interface ISoftwareCardProps { navTabIndex: any; onTabChange: any; onQueryChange: any; + router: InjectedRouter; +} + +interface IRowProps extends Row { + original: { + id?: number; + }; } const SOFTWARE_DEFAULT_SORT_DIRECTION = "desc"; @@ -35,9 +48,20 @@ const Software = ({ onTabChange, onQueryChange, software, + router, }: ISoftwareCardProps): JSX.Element => { const tableHeaders = generateTableHeaders(); + const handleRowSelect = (row: IRowProps) => { + const queryParams = { software_id: row.original.id }; + + const path = queryParams + ? `${PATHS.MANAGE_HOSTS}?${buildQueryStringFromParams(queryParams)}` + : PATHS.MANAGE_HOSTS; + + router.push(path); + }; + // Renders opaque information as host information is loading const opacity = isSoftwareFetching ? { opacity: 0 } : { opacity: 1 }; @@ -80,6 +104,8 @@ const Software = ({ disableActionButton pageSize={SOFTWARE_DEFAULT_PAGE_SIZE} onQueryChange={onQueryChange} + disableMultiRowSelect + onSelectSingleRow={handleRowSelect} /> )} @@ -108,6 +134,8 @@ const Software = ({ disableActionButton pageSize={SOFTWARE_DEFAULT_PAGE_SIZE} onQueryChange={onQueryChange} + disableMultiRowSelect + onSelectSingleRow={handleRowSelect} /> )} diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx index 17a17c0e81..ccedafb575 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx @@ -662,6 +662,7 @@ const HostDetailsPage = ({ featuresConfig?.enable_software_inventory } deviceType={host?.platform === "darwin" ? "macos" : ""} + router={router} /> {host?.platform === "darwin" && macadmins && ( { const [searchString, setSearchString] = useState(""); const [filterVuln, setFilterVuln] = useState(false); @@ -58,14 +70,29 @@ const SoftwareTable = ({ const tableSoftware = useMemo(() => generateSoftwareTableData(software), [ software, ]); - const tableHeaders = useMemo(() => generateSoftwareTableHeaders(deviceUser), [ - deviceUser, - ]); + const tableHeaders = useMemo( + () => generateSoftwareTableHeaders(deviceUser, router), + [deviceUser, router] + ); const onVulnFilterChange = (value: boolean) => { setFilterVuln(value); }; + const handleRowSelect = (row: IRowProps) => { + if (deviceUser || !router) { + return; + } + + const queryParams = { software_id: row.original.id }; + + const path = queryParams + ? `${PATHS.MANAGE_HOSTS}?${buildQueryStringFromParams(queryParams)}` + : PATHS.MANAGE_HOSTS; + + router.push(path); + }; + const renderVulnFilterDropdown = () => { return ( )} diff --git a/frontend/pages/hosts/details/cards/Software/SoftwareTableConfig.tsx b/frontend/pages/hosts/details/cards/Software/SoftwareTableConfig.tsx index 1c7db958b0..dc8865980a 100644 --- a/frontend/pages/hosts/details/cards/Software/SoftwareTableConfig.tsx +++ b/frontend/pages/hosts/details/cards/Software/SoftwareTableConfig.tsx @@ -1,12 +1,13 @@ import React from "react"; -import { Link } from "react-router"; +import { InjectedRouter } from "react-router"; import ReactTooltip from "react-tooltip"; import { formatDistanceToNow } from "date-fns"; import { ISoftware } from "interfaces/software"; - import PATHS from "router/paths"; + +import Button from "components/buttons/Button"; import HeaderCell from "components/TableContainer/DataTable/HeaderCell/HeaderCell"; import TextCell from "components/TableContainer/DataTable/TextCell"; import TooltipWrapper from "components/TooltipWrapper"; @@ -133,7 +134,8 @@ export const generateSoftwareTableData = ( // NOTE: cellProps come from react-table // more info here https://react-table.tanstack.com/docs/api/useTable#cell-properties export const generateSoftwareTableHeaders = ( - deviceUser = false + deviceUser = false, + router?: InjectedRouter ): IDataColumn[] => { const tableHeaders: IDataColumn[] = [ { @@ -156,10 +158,18 @@ export const generateSoftwareTableHeaders = ( {name} ); } + + const onClickSoftware = (e: React.MouseEvent) => { + // Allows for button to be clickable in a clickable row + e.stopPropagation(); + + router?.push(PATHS.SOFTWARE_DETAILS(id.toString())); + }; + return ( - + ); }, sortType: "caseInsensitive", diff --git a/frontend/pages/software/ManageSoftwarePage/ManageSoftwarePage.tsx b/frontend/pages/software/ManageSoftwarePage/ManageSoftwarePage.tsx index 1d0e014365..c0a0016428 100644 --- a/frontend/pages/software/ManageSoftwarePage/ManageSoftwarePage.tsx +++ b/frontend/pages/software/ManageSoftwarePage/ManageSoftwarePage.tsx @@ -5,6 +5,8 @@ import React, { useMemo, useState, } from "react"; +import { Row } from "react-table"; +import PATHS from "router/paths"; import { useQuery } from "react-query"; import { InjectedRouter } from "react-router/lib/Router"; import { useDebouncedCallback } from "use-debounce"; @@ -30,6 +32,7 @@ import { GITHUB_NEW_ISSUE_LINK, VULNERABLE_DROPDOWN_OPTIONS, } from "utilities/constants"; +import { buildQueryStringFromParams, QueryParams } from "utilities/url"; import Button from "components/buttons/Button"; // @ts-ignore @@ -86,6 +89,13 @@ interface ISoftwareAutomations { interface IHeaderButtonsState extends ITeamsDropdownState { isLoading: boolean; } + +interface IRowProps extends Row { + original: { + id?: number; + }; +} + const DEFAULT_SORT_DIRECTION = "desc"; const DEFAULT_PAGE_SIZE = 20; @@ -507,9 +517,18 @@ const ManageSoftwarePage = ({ softwareCount; const softwareTableHeaders = useMemo( - () => generateSoftwareTableHeaders(isPremiumTier), - [isPremiumTier] + () => generateSoftwareTableHeaders(router, isPremiumTier), + [isPremiumTier, router] ); + const handleRowSelect = (row: IRowProps) => { + const queryParams = { software_id: row.original.id }; + + const path = queryParams + ? `${PATHS.MANAGE_HOSTS}?${buildQueryStringFromParams(queryParams)}` + : PATHS.MANAGE_HOSTS; + + router.push(path); + }; return !availableTeams || !globalConfig || @@ -555,6 +574,8 @@ const ManageSoftwarePage = ({ renderFooter={renderTableFooter} disableActionButton hideActionButton + disableMultiRowSelect + onSelectSingleRow={handleRowSelect} /> )} diff --git a/frontend/pages/software/ManageSoftwarePage/SoftwareTableConfig.tsx b/frontend/pages/software/ManageSoftwarePage/SoftwareTableConfig.tsx index 91d680e4a5..4b9c038810 100644 --- a/frontend/pages/software/ManageSoftwarePage/SoftwareTableConfig.tsx +++ b/frontend/pages/software/ManageSoftwarePage/SoftwareTableConfig.tsx @@ -1,13 +1,14 @@ import React from "react"; import { Column } from "react-table"; +import { InjectedRouter } from "react-router"; import ReactTooltip from "react-tooltip"; -import { Link } from "react-router"; import { formatSoftwareType, ISoftware } from "interfaces/software"; import { IVulnerability } from "interfaces/vulnerability"; import PATHS from "router/paths"; import { formatFloatAsPercentage } from "utilities/helpers"; +import Button from "components/buttons/Button"; import HeaderCell from "components/TableContainer/DataTable/HeaderCell"; import TextCell from "components/TableContainer/DataTable/TextCell"; import TooltipWrapper from "components/TooltipWrapper"; @@ -178,7 +179,10 @@ const generateVulnColumnHeader = () => { }; }; -const generateTableHeaders = (isPremiumTier?: boolean): Column[] => { +const generateTableHeaders = ( + router: InjectedRouter, + isPremiumTier?: boolean +): Column[] => { const softwareTableHeaders = [ { title: "Name", @@ -187,10 +191,18 @@ const generateTableHeaders = (isPremiumTier?: boolean): Column[] => { accessor: "name", Cell: (cellProps: IStringCellProps): JSX.Element => { const { id, name, bundle_identifier: bundle } = cellProps.row.original; + + const onClickSoftware = (e: React.MouseEvent) => { + // Allows for button to be clickable in a clickable row + e.stopPropagation(); + + router?.push(PATHS.SOFTWARE_DETAILS(id.toString())); + }; + return ( - + ); }, sortType: "caseInsensitive",