From 32c42c301f34b8e77d74bbbe879f34a398779f39 Mon Sep 17 00:00:00 2001 From: jacobshandling <61553566+jacobshandling@users.noreply.github.com> Date: Thu, 26 Dec 2024 14:51:28 -0800 Subject: [PATCH] UI - Show software details My device page (#25022) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## #23315 - On device user page > Software, make rows clickable and on click, open the Software details modal to display information about the installation on the host. - Update Software details modal copy and allow long file paths to wrap https://github.com/user-attachments/assets/1e714c5e-1614-46c0-bb56-d6dc8ad4f8ae Screenshot 2024-12-26 at 10 27 44 AM Screenshot 2024-12-26 at 10 27 19 AM Screenshot 2024-12-26 at 10 27 37 AM - [x] Changes file added for user-visible changes in `changes/`, - [x] Manual QA for all new/changed functionality --------- Co-authored-by: Jacob Shandling --- ...23315-show-sw-install-details-on-my-device | 2 ++ .../LiveQuery/TargetsInput/TargetsInput.tsx | 2 +- .../TableContainer/DataTable/DataTable.tsx | 6 +++--- .../TableContainer/TableContainer.tsx | 6 +++--- .../components/TableContainer/_styles.scss | 3 ++- .../ViewAllHostsLink/ViewAllHostsLink.tsx | 16 +++++++++------ .../details/DeviceUserPage/DeviceUserPage.tsx | 14 +++++++++++++ .../Software/DeviceSoftwareTableConfig.tsx | 16 +++++++++++++++ .../details/cards/Software/HostSoftware.tsx | 5 ++++- .../HostSoftwareTable/HostSoftwareTable.tsx | 20 +++++++++++++++++++ .../SoftwareDetailsModal.tsx | 5 ++--- .../SoftwareDetailsModal/_styles.scss | 5 ++--- .../hosts/details/cards/Software/_styles.scss | 5 +++++ 13 files changed, 84 insertions(+), 21 deletions(-) create mode 100644 changes/23315-show-sw-install-details-on-my-device diff --git a/changes/23315-show-sw-install-details-on-my-device b/changes/23315-show-sw-install-details-on-my-device new file mode 100644 index 0000000000..fc5c476c8c --- /dev/null +++ b/changes/23315-show-sw-install-details-on-my-device @@ -0,0 +1,2 @@ +* Add the ability to click a software row on the my device page and see the details of that +software's installation on the host. diff --git a/frontend/components/LiveQuery/TargetsInput/TargetsInput.tsx b/frontend/components/LiveQuery/TargetsInput/TargetsInput.tsx index 6dc83482ee..70c5106227 100644 --- a/frontend/components/LiveQuery/TargetsInput/TargetsInput.tsx +++ b/frontend/components/LiveQuery/TargetsInput/TargetsInput.tsx @@ -122,7 +122,7 @@ const TargetsInput = ({ disablePagination disableMultiRowSelect onClickRow={handleRowSelect} - keyboardSelectableRow + keyboardSelectableRows /> )} diff --git a/frontend/components/TableContainer/DataTable/DataTable.tsx b/frontend/components/TableContainer/DataTable/DataTable.tsx index 70759de9eb..681b729d3f 100644 --- a/frontend/components/TableContainer/DataTable/DataTable.tsx +++ b/frontend/components/TableContainer/DataTable/DataTable.tsx @@ -46,7 +46,7 @@ interface IDataTableProps { sortDirection: any; onSort: any; // TODO: an event type disableMultiRowSelect: boolean; - keyboardSelectableRow?: boolean; + keyboardSelectableRows?: boolean; showMarkAllPages: boolean; isAllPagesSelected: boolean; // TODO: make dependent on showMarkAllPages toggleAllPagesSelected?: any; // TODO: an event type and make it dependent on showMarkAllPages @@ -95,7 +95,7 @@ const DataTable = ({ sortDirection, onSort, disableMultiRowSelect, - keyboardSelectableRow, + keyboardSelectableRows, showMarkAllPages, isAllPagesSelected, toggleAllPagesSelected, @@ -575,7 +575,7 @@ const DataTable = ({ }, })} // Can tab onto an entire row if a child element does not have the same onClick functionality as clicking the whole row - tabIndex={keyboardSelectableRow ? 0 : -1} + tabIndex={keyboardSelectableRows ? 0 : -1} > {row.cells.map((cell: any) => { return ( diff --git a/frontend/components/TableContainer/TableContainer.tsx b/frontend/components/TableContainer/TableContainer.tsx index 17e7c71bd8..db1a219627 100644 --- a/frontend/components/TableContainer/TableContainer.tsx +++ b/frontend/components/TableContainer/TableContainer.tsx @@ -94,7 +94,7 @@ interface ITableContainerProps { */ onClickRow?: (row: T) => void; /** Used if users can click the row and another child element does not have the same onClick functionality */ - keyboardSelectableRow?: boolean; + keyboardSelectableRows?: boolean; /** Use for clientside filtering: Use key global for filtering on any column, or use column id as * key */ filters?: Record; @@ -162,7 +162,7 @@ const TableContainer = ({ stackControls, onSelectSingleRow, onClickRow, - keyboardSelectableRow, + keyboardSelectableRows, renderCount, renderTableHelpText, setExportRows, @@ -514,7 +514,7 @@ const TableContainer = ({ secondarySelectActions={secondarySelectActions} onSelectSingleRow={onSelectSingleRow} onClickRow={onClickRow} - keyboardSelectableRow={keyboardSelectableRow} + keyboardSelectableRows={keyboardSelectableRows} onResultsCountChange={setClientFilterCount} isClientSidePagination={isClientSidePagination} onClientSidePaginationChange={onClientSidePaginationChange} diff --git a/frontend/components/TableContainer/_styles.scss b/frontend/components/TableContainer/_styles.scss index 9abf876944..b4ad02faea 100644 --- a/frontend/components/TableContainer/_styles.scss +++ b/frontend/components/TableContainer/_styles.scss @@ -257,7 +257,8 @@ transition: 250ms; text-overflow: none; } - &:hover { + &:hover, + &:focus-visible { .row-hover-link { opacity: 1; } diff --git a/frontend/components/ViewAllHostsLink/ViewAllHostsLink.tsx b/frontend/components/ViewAllHostsLink/ViewAllHostsLink.tsx index e94c84fd1a..15bf24a52b 100644 --- a/frontend/components/ViewAllHostsLink/ViewAllHostsLink.tsx +++ b/frontend/components/ViewAllHostsLink/ViewAllHostsLink.tsx @@ -13,9 +13,10 @@ interface IHostLinkProps { platformLabelId?: number; /** Shows right chevron without text */ condensed?: boolean; + excludeChevron?: boolean; responsive?: boolean; customText?: string; - /** Table links shows on row hover only */ + /** Table links shows on row hover and tab focus only */ rowHover?: boolean; // don't actually create a link, useful when click is handled by an ancestor noLink?: boolean; @@ -28,6 +29,7 @@ const ViewAllHostsLink = ({ className, platformLabelId, condensed = false, + excludeChevron = false, responsive = false, customText, rowHover = false, @@ -58,11 +60,13 @@ const ViewAllHostsLink = ({ {customText ?? "View all hosts"} )} - + {!excludeChevron && ( + + )} ); }; diff --git a/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tsx b/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tsx index f2f44a48a8..608b7a5790 100644 --- a/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tsx +++ b/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tsx @@ -16,6 +16,7 @@ import { } from "interfaces/host"; import { IHostPolicy } from "interfaces/policy"; import { IDeviceGlobalConfig } from "interfaces/config"; +import { IHostSoftware } from "interfaces/software"; import DeviceUserError from "components/DeviceUserError"; // @ts-ignore @@ -52,6 +53,7 @@ import OSSettingsModal from "../OSSettingsModal"; import BootstrapPackageModal from "../HostDetailsPage/modals/BootstrapPackageModal"; import { parseHostSoftwareQueryParams } from "../cards/Software/HostSoftware"; import SelfService from "../cards/Software/SelfService"; +import SoftwareDetailsModal from "../cards/Software/SoftwareDetailsModal"; import DeviceUserBanners from "./components/DeviceUserBanners"; const baseClass = "device-user"; @@ -118,6 +120,10 @@ const DeviceUserPage = ({ const [isTriggeringCreateLinuxKey, setIsTriggeringCreateLinuxKey] = useState( false ); + const [ + selectedSoftwareDetails, + setSelectedSoftwareDetails, + ] = useState(null); const { data: deviceMapping, refetch: refetchDeviceMapping } = useQuery( ["deviceMapping", deviceAuthToken], @@ -455,6 +461,7 @@ const DeviceUserPage = ({ platform={host.platform} hostTeamId={host.team_id || 0} isSoftwareEnabled={isSoftwareEnabled} + onShowSoftwareDetails={setSelectedSoftwareDetails} /> )} @@ -508,6 +515,13 @@ const DeviceUserPage = ({ }} /> )} + {selectedSoftwareDetails && !!host && ( + setSelectedSoftwareDetails(null)} + /> + )} ); }; diff --git a/frontend/pages/hosts/details/cards/Software/DeviceSoftwareTableConfig.tsx b/frontend/pages/hosts/details/cards/Software/DeviceSoftwareTableConfig.tsx index ea9e7cfb69..f1c0ee1a38 100644 --- a/frontend/pages/hosts/details/cards/Software/DeviceSoftwareTableConfig.tsx +++ b/frontend/pages/hosts/details/cards/Software/DeviceSoftwareTableConfig.tsx @@ -15,6 +15,7 @@ import VulnerabilitiesCell from "pages/SoftwarePage/components/VulnerabilitiesCe import VersionCell from "pages/SoftwarePage/components/VersionCell"; import { getVulnerabilities } from "pages/SoftwarePage/SoftwareTitles/SoftwareTable/SoftwareTitlesTableConfig"; import SoftwareNameCell from "components/TableContainer/DataTable/SoftwareNameCell"; +import ViewAllHostsLink from "components/ViewAllHostsLink"; type ISoftwareTableConfig = Column; type ITableHeaderProps = IHeaderProps; @@ -84,6 +85,21 @@ export const generateSoftwareTableHeaders = (): ISoftwareTableConfig[] => { return ; }, }, + { + Header: "", + // accessor ends up defining the classname for this column (`id__header` in this case), but is + // type restricted, so using "id" here, which is unsued by another column + accessor: "id", + disableSortBy: true, + Cell: () => ( + + ), + }, ]; return tableHeaders; diff --git a/frontend/pages/hosts/details/cards/Software/HostSoftware.tsx b/frontend/pages/hosts/details/cards/Software/HostSoftware.tsx index 7c52907796..1bcd30e514 100644 --- a/frontend/pages/hosts/details/cards/Software/HostSoftware.tsx +++ b/frontend/pages/hosts/details/cards/Software/HostSoftware.tsx @@ -42,7 +42,7 @@ interface IHostSoftwareProps { queryParams: ReturnType; pathname: string; hostTeamId: number; - onShowSoftwareDetails?: (software: IHostSoftware) => void; + onShowSoftwareDetails: (software: IHostSoftware) => void; isSoftwareEnabled?: boolean; hostScriptsEnabled?: boolean; isMyDevicePage?: boolean; @@ -329,6 +329,9 @@ const HostSoftware = ({ pagePath={pathname} hostSoftwareFilter={getHostSoftwareFilterFromQueryParams()} pathPrefix={pathname} + // for my device software details modal toggling + isMyDevicePage={isMyDevicePage} + onShowSoftwareDetails={onShowSoftwareDetails} /> )} diff --git a/frontend/pages/hosts/details/cards/Software/HostSoftwareTable/HostSoftwareTable.tsx b/frontend/pages/hosts/details/cards/Software/HostSoftwareTable/HostSoftwareTable.tsx index ca03de966e..e9bed373c3 100644 --- a/frontend/pages/hosts/details/cards/Software/HostSoftwareTable/HostSoftwareTable.tsx +++ b/frontend/pages/hosts/details/cards/Software/HostSoftwareTable/HostSoftwareTable.tsx @@ -23,6 +23,8 @@ import Dropdown from "components/forms/fields/Dropdown"; import EmptySoftwareTable from "pages/SoftwarePage/components/EmptySoftwareTable"; import TableCount from "components/TableContainer/TableCount"; import { VulnsNotSupported } from "pages/SoftwarePage/components/SoftwareVulnerabilitiesTable/SoftwareVulnerabilitiesTable"; +import { Row } from "react-table"; +import { IHostSoftware } from "interfaces/software"; const DEFAULT_PAGE_SIZE = 20; @@ -50,6 +52,9 @@ export const DROPDOWN_OPTIONS = [ }, ] as const; +interface IHostSoftwareRowProps extends Row { + original: IHostSoftware; +} interface IHostSoftwareTableProps { tableConfig: any; // TODO: type data?: IGetHostSoftwareResponse | IGetDeviceSoftwareResponse; @@ -64,6 +69,8 @@ interface IHostSoftwareTableProps { routeTemplate?: string; pathPrefix: string; hostSoftwareFilter: IHostSoftwareDropdownFilterVal; + isMyDevicePage?: boolean; + onShowSoftwareDetails: (software: IHostSoftware) => void; } const HostSoftwareTable = ({ @@ -80,6 +87,8 @@ const HostSoftwareTable = ({ routeTemplate, pathPrefix, hostSoftwareFilter, + isMyDevicePage, + onShowSoftwareDetails, }: IHostSoftwareTableProps) => { const handleFilterDropdownChange = useCallback( (val: IHostSoftwareDropdownFilterVal) => { @@ -221,6 +230,13 @@ const HostSoftwareTable = ({ const showFilterHeaders = hasData || hasQuery || hasSoftwareFilter; + const onClickMyDeviceRow = useCallback( + (row: IHostSoftwareRowProps) => { + onShowSoftwareDetails(row.original); + }, + [onShowSoftwareDetails] + ); + return (
); diff --git a/frontend/pages/hosts/details/cards/Software/SoftwareDetailsModal/SoftwareDetailsModal.tsx b/frontend/pages/hosts/details/cards/Software/SoftwareDetailsModal/SoftwareDetailsModal.tsx index 33726b8081..7ebd1abecd 100644 --- a/frontend/pages/hosts/details/cards/Software/SoftwareDetailsModal/SoftwareDetailsModal.tsx +++ b/frontend/pages/hosts/details/cards/Software/SoftwareDetailsModal/SoftwareDetailsModal.tsx @@ -18,7 +18,6 @@ import { dateAgo } from "utilities/date_format"; import { AppInstallDetails } from "components/ActivityDetails/InstallDetails/AppInstallDetails"; import { SoftwareInstallDetails } from "components/ActivityDetails/InstallDetails/SoftwareInstallDetails"; -import TooltipTruncatedText from "components/TooltipTruncatedText"; const baseClass = "software-details-modal"; @@ -68,11 +67,11 @@ const SoftwareDetailsInfo = ({
1 ? "s" : ""}`} value={
{installed_paths.map((path) => ( - + {path} ))}
} diff --git a/frontend/pages/hosts/details/cards/Software/SoftwareDetailsModal/_styles.scss b/frontend/pages/hosts/details/cards/Software/SoftwareDetailsModal/_styles.scss index 1b2374dd61..069b719638 100644 --- a/frontend/pages/hosts/details/cards/Software/SoftwareDetailsModal/_styles.scss +++ b/frontend/pages/hosts/details/cards/Software/SoftwareDetailsModal/_styles.scss @@ -41,9 +41,8 @@ > span { width: 100%; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; + white-space: initial; + overflow-wrap: anywhere; } } diff --git a/frontend/pages/hosts/details/cards/Software/_styles.scss b/frontend/pages/hosts/details/cards/Software/_styles.scss index 331c9861e9..1f028166cb 100644 --- a/frontend/pages/hosts/details/cards/Software/_styles.scss +++ b/frontend/pages/hosts/details/cards/Software/_styles.scss @@ -38,6 +38,11 @@ .Vulnerabilities__header { display: table-cell; } + // this is called `id` only due to typing concerns in the table config – it is for the + // "Show details" column + .id__header { + width: px-to-rem(90); + } @media (max-width: $break-xl) { .source__header { display: none;