Resolves #45711 - [X] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. ## Testing - [x] Added/updated automated tests - [x] QA'd all new/changed functionality manually. <img width="1216" height="527" alt="Screenshot 2026-05-19 at 5 30 48 PM" src="https://github.com/user-attachments/assets/05a0a93f-3b5b-4ba9-80c5-b0a91b9de1f3" /> <img width="1216" height="527" alt="Screenshot 2026-05-19 at 5 30 37 PM" src="https://github.com/user-attachments/assets/3e23e88e-6a95-46f6-a68f-12fb98f1aefe" /> <img width="1216" height="527" alt="Screenshot 2026-05-19 at 5 29 45 PM" src="https://github.com/user-attachments/assets/417ca981-0781-4df5-811b-dafaabbd61a9" /> <img width="1216" height="527" alt="Screenshot 2026-05-19 at 5 29 30 PM" src="https://github.com/user-attachments/assets/cf13985d-5cb3-414a-9135-5cf4c5ee0dd1" /> <img width="1216" height="527" alt="Screenshot 2026-05-19 at 5 31 33 PM" src="https://github.com/user-attachments/assets/8500e7b2-cc7b-425d-b6b7-bbbf128faac4" /> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Fixed host listing so filtering by Android OS name and version returns matching hosts. * **New Features** * Android hosts are now included in the operating systems aggregation and UI, with a dedicated Android icon. * Android OS version information is captured during device enrollment and on status reports. <!-- review_stack_entry_start --> [](https://app.coderabbit.ai/change-stack/fleetdm/fleet/pull/45834?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack) <!-- review_stack_entry_end --> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
283 lines
7.5 KiB
TypeScript
283 lines
7.5 KiB
TypeScript
/** software/os OS tab > Table */
|
|
|
|
import React, { useCallback, useMemo } from "react";
|
|
import { InjectedRouter } from "react-router";
|
|
import { Row } from "react-table";
|
|
|
|
import PATHS from "router/paths";
|
|
|
|
import { GITHUB_NEW_ISSUE_LINK } from "utilities/constants";
|
|
|
|
import CustomLink from "components/CustomLink";
|
|
import TableContainer from "components/TableContainer";
|
|
import LastUpdatedText from "components/LastUpdatedText";
|
|
import { ITableQueryData } from "components/TableContainer/TableContainer";
|
|
import TableCount from "components/TableContainer/TableCount";
|
|
import { SingleValue } from "react-select-5";
|
|
import DropdownWrapper from "components/forms/fields/DropdownWrapper";
|
|
import { CustomOptionType } from "components/forms/fields/DropdownWrapper/DropdownWrapper";
|
|
|
|
import EmptySoftwareTable from "pages/SoftwarePage/components/tables/EmptySoftwareTable";
|
|
import { IOSVersionsResponse } from "services/entities/operating_systems";
|
|
|
|
import generateTableConfig from "pages/DashboardPage/cards/OperatingSystems/OSTableConfig";
|
|
import { getPathWithQueryParams } from "utilities/url";
|
|
import { getNextLocationPath } from "utilities/helpers";
|
|
import { SelectedPlatform } from "interfaces/platform";
|
|
|
|
const baseClass = "software-os-table";
|
|
|
|
interface IRowProps extends Row {
|
|
original: {
|
|
os_version_id?: string;
|
|
};
|
|
}
|
|
|
|
interface ISoftwareOSTableProps {
|
|
router: InjectedRouter;
|
|
isSoftwareEnabled: boolean;
|
|
data?: IOSVersionsResponse;
|
|
perPage: number;
|
|
orderDirection: "asc" | "desc";
|
|
orderKey: string;
|
|
currentPage: number;
|
|
teamId?: number;
|
|
isLoading: boolean;
|
|
platform?: SelectedPlatform;
|
|
}
|
|
|
|
const PLATFORM_FILTER_OPTIONS = [
|
|
{
|
|
disabled: false,
|
|
label: "All platforms",
|
|
value: "all",
|
|
},
|
|
{
|
|
disabled: false,
|
|
label: "macOS",
|
|
value: "darwin",
|
|
},
|
|
{
|
|
disabled: false,
|
|
label: "Windows",
|
|
value: "windows",
|
|
},
|
|
{
|
|
disabled: false,
|
|
label: "Linux",
|
|
value: "linux",
|
|
},
|
|
{
|
|
disabled: false,
|
|
label: "ChromeOS",
|
|
value: "chrome",
|
|
},
|
|
{
|
|
disabled: false,
|
|
label: "iOS",
|
|
value: "ios",
|
|
},
|
|
{
|
|
disabled: false,
|
|
label: "iPadOS",
|
|
value: "ipados",
|
|
},
|
|
{
|
|
disabled: false,
|
|
label: "Android",
|
|
value: "android",
|
|
},
|
|
];
|
|
|
|
const SoftwareOSTable = ({
|
|
router,
|
|
isSoftwareEnabled,
|
|
data,
|
|
perPage,
|
|
orderDirection,
|
|
orderKey,
|
|
currentPage,
|
|
teamId,
|
|
isLoading,
|
|
platform = "all",
|
|
}: ISoftwareOSTableProps) => {
|
|
const determineQueryParamChange = useCallback(
|
|
(newTableQuery: ITableQueryData) => {
|
|
const changedEntry = Object.entries(newTableQuery).find(([key, val]) => {
|
|
switch (key) {
|
|
case "sortDirection":
|
|
return val !== orderDirection;
|
|
case "sortHeader":
|
|
return val !== orderKey;
|
|
case "pageIndex":
|
|
return val !== currentPage;
|
|
case "platform":
|
|
return val !== platform;
|
|
default:
|
|
return false;
|
|
}
|
|
});
|
|
return changedEntry?.[0] ?? "";
|
|
},
|
|
[platform, currentPage, orderDirection, orderKey]
|
|
);
|
|
|
|
const generateNewQueryParams = useCallback(
|
|
(newTableQuery: ITableQueryData, changedParam: string) => {
|
|
return {
|
|
fleet_id: teamId,
|
|
order_direction: newTableQuery.sortDirection,
|
|
order_key: newTableQuery.sortHeader,
|
|
page: changedParam === "pageIndex" ? newTableQuery.pageIndex : 0,
|
|
platform,
|
|
};
|
|
},
|
|
[teamId, platform]
|
|
);
|
|
|
|
const onQueryChange = useCallback(
|
|
(newTableQuery: ITableQueryData) => {
|
|
// we want to determine which query param has changed in order to
|
|
// reset the page index to 0 if any other param has changed.
|
|
const changedParam = determineQueryParamChange(newTableQuery);
|
|
// if nothing has changed, don't update the route. this can happen when
|
|
// this handler is called on the initial render.
|
|
if (changedParam === "") return;
|
|
|
|
const newRoute = getNextLocationPath({
|
|
pathPrefix: PATHS.SOFTWARE_OS,
|
|
routeTemplate: "",
|
|
queryParams: generateNewQueryParams(newTableQuery, changedParam),
|
|
});
|
|
|
|
router.replace(newRoute);
|
|
},
|
|
[determineQueryParamChange, generateNewQueryParams, router]
|
|
);
|
|
|
|
const softwareTableHeaders = useMemo(() => {
|
|
if (!data) return [];
|
|
return generateTableConfig(teamId, router, {
|
|
includeName: true,
|
|
includeVulnerabilities: true,
|
|
includeIcon: true,
|
|
});
|
|
}, [data, router, teamId]);
|
|
|
|
const handleRowSelect = (row: IRowProps) => {
|
|
const path = getPathWithQueryParams(
|
|
PATHS.SOFTWARE_OS_DETAILS(Number(row.original.os_version_id)),
|
|
{ fleet_id: teamId }
|
|
);
|
|
|
|
router.push(path);
|
|
};
|
|
|
|
const hasData = data?.os_versions && data?.os_versions.length > 0;
|
|
const hasPlatformFilter = platform !== "all";
|
|
|
|
const isTrulyEmpty = !hasData && !hasPlatformFilter;
|
|
const controlsDisabled = !isSoftwareEnabled || isTrulyEmpty;
|
|
|
|
const renderSoftwareCount = () => {
|
|
if (!data) return null;
|
|
|
|
return (
|
|
<>
|
|
<TableCount name="items" count={data?.count} />
|
|
{!controlsDisabled && data?.counts_updated_at && (
|
|
<LastUpdatedText
|
|
lastUpdatedAt={data.counts_updated_at}
|
|
customTooltipText={
|
|
<>
|
|
The last time software data was <br />
|
|
updated, including vulnerabilities <br />
|
|
and host counts.
|
|
</>
|
|
}
|
|
/>
|
|
)}
|
|
</>
|
|
);
|
|
};
|
|
|
|
const renderTableHelpText = () => {
|
|
return (
|
|
<div>
|
|
Seeing unexpected software or vulnerabilities?{" "}
|
|
<CustomLink
|
|
url={GITHUB_NEW_ISSUE_LINK}
|
|
text="File an issue on GitHub"
|
|
newTab
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const handlePlatformFilterDropdownChange = (
|
|
platformSelected: SingleValue<CustomOptionType>
|
|
) => {
|
|
router?.replace(
|
|
getNextLocationPath({
|
|
pathPrefix: PATHS.SOFTWARE_OS,
|
|
queryParams: {
|
|
fleet_id: teamId,
|
|
order_direction: orderDirection,
|
|
order_key: orderKey,
|
|
page: 0,
|
|
platform: platformSelected?.value,
|
|
},
|
|
})
|
|
);
|
|
};
|
|
|
|
const renderPlatformDropdown = () => {
|
|
return (
|
|
<DropdownWrapper
|
|
name="os-platform-dropdown"
|
|
value={platform || "all"}
|
|
className={`${baseClass}__platform-dropdown`}
|
|
options={PLATFORM_FILTER_OPTIONS}
|
|
onChange={handlePlatformFilterDropdownChange}
|
|
variant="table-filter"
|
|
isDisabled={controlsDisabled}
|
|
/>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<div className={baseClass}>
|
|
<TableContainer
|
|
columnConfigs={softwareTableHeaders}
|
|
data={data?.os_versions ?? []}
|
|
isLoading={isLoading}
|
|
resultsTitle="items"
|
|
emptyComponent={() => (
|
|
<EmptySoftwareTable
|
|
tableName="operating systems"
|
|
isSoftwareDisabled={!isSoftwareEnabled}
|
|
noSearchQuery // non-searchable table renders not detecting by default
|
|
/>
|
|
)}
|
|
defaultSortHeader={orderKey}
|
|
defaultSortDirection={orderDirection}
|
|
pageIndex={currentPage}
|
|
manualSortBy
|
|
pageSize={perPage}
|
|
showMarkAllPages={false}
|
|
isAllPagesSelected={false}
|
|
customControl={renderPlatformDropdown}
|
|
disableNextPage={!data?.meta.has_next_results}
|
|
searchable={false}
|
|
onQueryChange={onQueryChange}
|
|
renderCount={renderSoftwareCount}
|
|
renderTableHelpText={renderTableHelpText}
|
|
disableMultiRowSelect
|
|
onSelectSingleRow={handleRowSelect}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default SoftwareOSTable;
|