Fleet UI: Allow software table rows to be clickable (#8772)
This commit is contained in:
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user