Files
Lucas Manuel Rodriguez b4e907801f Add Android to the OS list (#45834)
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 -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](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 -->
2026-05-20 10:43:10 -03:00

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;