Fleet UI: Allow software table rows to be clickable (#8772)

This commit is contained in:
RachelElysia
2022-11-22 11:13:33 -05:00
committed by GitHub
parent ac5d04cde7
commit 05a6f06d12
9 changed files with 123 additions and 15 deletions
@@ -0,0 +1 @@
- Software tables now have clickable rows to direct to view all hosts filtered by software
@@ -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<string, string | number | boolean>;
renderCount?: () => JSX.Element | null;
renderFooter?: () => JSX.Element | null;
@@ -488,6 +488,7 @@ const DashboardPage = ({
navTabIndex={softwareNavTabIndex}
onTabChange={onSoftwareTabChange}
onQueryChange={onSoftwareQueryChange}
router={router}
/>
),
});
@@ -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}
/>
)}
</TabPanel>
@@ -108,6 +134,8 @@ const Software = ({
disableActionButton
pageSize={SOFTWARE_DEFAULT_PAGE_SIZE}
onQueryChange={onQueryChange}
disableMultiRowSelect
onSelectSingleRow={handleRowSelect}
/>
)}
</TabPanel>
@@ -662,6 +662,7 @@ const HostDetailsPage = ({
featuresConfig?.enable_software_inventory
}
deviceType={host?.platform === "darwin" ? "macos" : ""}
router={router}
/>
{host?.platform === "darwin" && macadmins && (
<MunkiIssuesCard
@@ -1,8 +1,12 @@
import React, { useEffect, useMemo, useState } from "react";
import { useDebouncedCallback } from "use-debounce";
import { InjectedRouter } from "react-router";
import { Row } from "react-table";
import PATHS from "router/paths";
import { ISoftware } from "interfaces/software";
import { VULNERABLE_DROPDOWN_OPTIONS } from "utilities/constants";
import { buildQueryStringFromParams } from "utilities/url";
// @ts-ignore
import Dropdown from "components/forms/fields/Dropdown";
@@ -28,6 +32,13 @@ interface ISoftwareTableProps {
deviceUser?: boolean;
deviceType?: string;
softwareInventoryEnabled?: boolean;
router?: InjectedRouter;
}
interface IRowProps extends Row {
original: {
id?: number;
};
}
const SoftwareTable = ({
@@ -36,6 +47,7 @@ const SoftwareTable = ({
deviceUser,
deviceType,
softwareInventoryEnabled,
router,
}: ISoftwareTableProps): JSX.Element => {
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 (
<Dropdown
@@ -125,6 +152,8 @@ const SoftwareTable = ({
isClientSidePagination
pageSize={20}
isClientSideFilter
disableMultiRowSelect={!deviceUser && !!router} // device user cannot view hosts by software
onSelectSingleRow={handleRowSelect}
/>
</div>
)}
@@ -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 = (
<span>{name}</span>
);
}
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 (
<Link to={`${PATHS.SOFTWARE_DETAILS(id.toString())}`}>
<Button onClick={onClickSoftware} variant="text-link">
{bundle ? renderBundleTooltip(name, bundle) : name}
</Link>
</Button>
);
},
sortType: "caseInsensitive",
@@ -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}
/>
)}
</div>
@@ -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 (
<Link to={`${PATHS.SOFTWARE_DETAILS(id.toString())}`}>
<Button onClick={onClickSoftware} variant="text-link">
{bundle ? renderBundleTooltip(name, bundle) : name}
</Link>
</Button>
);
},
sortType: "caseInsensitive",