UI - Show software details My device page (#25022)

## #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

<img width="1350" alt="Screenshot 2024-12-26 at 10 27 44 AM"
src="https://github.com/user-attachments/assets/5cefc45a-b0ef-41d9-84e6-21ac17aaeffe"
/>
<img width="1350" alt="Screenshot 2024-12-26 at 10 27 19 AM"
src="https://github.com/user-attachments/assets/e0866961-31a4-4bd3-82e8-18f72cf4dc30"
/>
<img width="1350" alt="Screenshot 2024-12-26 at 10 27 37 AM"
src="https://github.com/user-attachments/assets/2bf6c880-664d-4315-8a40-8de61a5e4748"
/>


- [x] Changes file added for user-visible changes in `changes/`, 
- [x] Manual QA for all new/changed functionality

---------

Co-authored-by: Jacob Shandling <jacob@fleetdm.com>
This commit is contained in:
jacobshandling
2024-12-26 14:51:28 -08:00
committed by GitHub
co-authored by Jacob Shandling
parent e94aa2d8f1
commit 32c42c301f
13 changed files with 84 additions and 21 deletions
@@ -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.
@@ -122,7 +122,7 @@ const TargetsInput = ({
disablePagination
disableMultiRowSelect
onClickRow={handleRowSelect}
keyboardSelectableRow
keyboardSelectableRows
/>
</div>
)}
@@ -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 (
@@ -94,7 +94,7 @@ interface ITableContainerProps<T = any> {
*/
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<string, string | number | boolean>;
@@ -162,7 +162,7 @@ const TableContainer = <T,>({
stackControls,
onSelectSingleRow,
onClickRow,
keyboardSelectableRow,
keyboardSelectableRows,
renderCount,
renderTableHelpText,
setExportRows,
@@ -514,7 +514,7 @@ const TableContainer = <T,>({
secondarySelectActions={secondarySelectActions}
onSelectSingleRow={onSelectSingleRow}
onClickRow={onClickRow}
keyboardSelectableRow={keyboardSelectableRow}
keyboardSelectableRows={keyboardSelectableRows}
onResultsCountChange={setClientFilterCount}
isClientSidePagination={isClientSidePagination}
onClientSidePaginationChange={onClientSidePaginationChange}
@@ -257,7 +257,8 @@
transition: 250ms;
text-overflow: none;
}
&:hover {
&:hover,
&:focus-visible {
.row-hover-link {
opacity: 1;
}
@@ -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"}
</span>
)}
<Icon
name="chevron-right"
className={`${baseClass}__icon`}
color="core-fleet-blue"
/>
{!excludeChevron && (
<Icon
name="chevron-right"
className={`${baseClass}__icon`}
color="core-fleet-blue"
/>
)}
</Link>
);
};
@@ -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<IHostSoftware | null>(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}
/>
</TabPanel>
)}
@@ -508,6 +515,13 @@ const DeviceUserPage = ({
}}
/>
)}
{selectedSoftwareDetails && !!host && (
<SoftwareDetailsModal
hostDisplayName={host.display_name}
software={selectedSoftwareDetails}
onExit={() => setSelectedSoftwareDetails(null)}
/>
)}
</div>
);
};
@@ -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<IHostSoftware>;
type ITableHeaderProps = IHeaderProps<IHostSoftware>;
@@ -84,6 +85,21 @@ export const generateSoftwareTableHeaders = (): ISoftwareTableConfig[] => {
return <VulnerabilitiesCell vulnerabilities={vulnerabilities} />;
},
},
{
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: () => (
<ViewAllHostsLink
rowHover
noLink
excludeChevron
customText="Show details"
/>
),
},
];
return tableHeaders;
@@ -42,7 +42,7 @@ interface IHostSoftwareProps {
queryParams: ReturnType<typeof parseHostSoftwareQueryParams>;
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}
/>
)}
</>
@@ -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 (
<div className={baseClass}>
<TableContainer
@@ -242,6 +258,10 @@ const HostSoftwareTable = ({
isAllPagesSelected={false}
searchable={showFilterHeaders}
manualSortBy
keyboardSelectableRows
// my device page row clickability
disableMultiRowSelect={isMyDevicePage}
onClickRow={isMyDevicePage ? onClickMyDeviceRow : undefined}
/>
</div>
);
@@ -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 = ({
<div className={`${baseClass}__row`}>
<DataSet
className={`${baseClass}__file-path-data-set`}
title="File path"
title={`File path${installed_paths.length > 1 ? "s" : ""}`}
value={
<div className={`${baseClass}__file-path-values`}>
{installed_paths.map((path) => (
<TooltipTruncatedText value={path} />
<span>{path}</span>
))}
</div>
}
@@ -41,9 +41,8 @@
> span {
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
white-space: initial;
overflow-wrap: anywhere;
}
}
@@ -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;