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:
co-authored by
Jacob Shandling
parent
e94aa2d8f1
commit
32c42c301f
@@ -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>
|
||||
);
|
||||
|
||||
+2
-3
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user