diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx index 30754603cd..9fe4efdb00 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx @@ -998,6 +998,7 @@ const HostDetailsPage = ({ onSetSelectedHostSWForInventoryVersions } hostTeamId={host.team_id || 0} + hostMdmEnrollmentStatus={host.mdm.enrollment_status} /> {isDarwinHost && macadmins?.munki?.version && ( void; isSoftwareEnabled?: boolean; isMyDevicePage?: boolean; + /** Used to show custom Software card header */ + hostMdmEnrollmentStatus?: MdmEnrollmentStatus | null; } const DEFAULT_SEARCH_QUERY = ""; @@ -121,6 +125,7 @@ const HostSoftware = ({ onShowInventoryVersions, isSoftwareEnabled = false, isMyDevicePage = false, + hostMdmEnrollmentStatus = null, }: IHostSoftwareProps) => { const { isPremiumTier } = useContext(AppContext); @@ -334,7 +339,11 @@ const HostSoftware = ({ > {renderHostSoftware()} @@ -344,7 +353,13 @@ const HostSoftware = ({ return (
{!isAndroid(platform) && ( - + )} {renderHostSoftware()}
diff --git a/frontend/pages/hosts/details/cards/Software/HostSoftwareTable/HostSoftwareTable.tests.tsx b/frontend/pages/hosts/details/cards/Software/HostSoftwareTable/HostSoftwareTable.tests.tsx index 8a7c93f210..eda6eaf699 100644 --- a/frontend/pages/hosts/details/cards/Software/HostSoftwareTable/HostSoftwareTable.tests.tsx +++ b/frontend/pages/hosts/details/cards/Software/HostSoftwareTable/HostSoftwareTable.tests.tsx @@ -65,4 +65,31 @@ describe("HostSoftwareTable", () => { }); expect(screen.getByRole("button", { name: /filter/i })).toBeInTheDocument(); }); + + it("renders VulnsNotSupported when vulns filter applied and platform is iPad/iPhone", () => { + renderWithContext({ + platform: "ipados", + vulnFilters: { vulnerable: true }, + data: createMockGetHostSoftwareResponse({ + count: 0, + software: [], + }), + }); + expect( + screen.getByText(/vulnerabilities are not supported/i) + ).toBeInTheDocument(); + }); + + // This includes empty state for BYOD iphone/ipads + it("renders generic empty state when no filters are applied and platform is iPad/iPhone", () => { + renderWithContext({ + platform: "ipados", + data: createMockGetHostSoftwareResponse({ + count: 0, + software: [], + }), + }); + + expect(screen.getByText(/no software detected/i)).toBeInTheDocument(); + }); }); diff --git a/frontend/pages/hosts/details/cards/Software/HostSoftwareTable/HostSoftwareTable.tsx b/frontend/pages/hosts/details/cards/Software/HostSoftwareTable/HostSoftwareTable.tsx index 7e07fc999b..183e3f0e47 100644 --- a/frontend/pages/hosts/details/cards/Software/HostSoftwareTable/HostSoftwareTable.tsx +++ b/frontend/pages/hosts/details/cards/Software/HostSoftwareTable/HostSoftwareTable.tsx @@ -42,6 +42,27 @@ const baseClass = "host-software-table"; interface IHostSoftwareRowProps extends Row { original: IHostSoftware; } + +interface IEmptyComponentProps { + hasVulnFilters: boolean; + platform: HostPlatform; + searchQuery: string; +} + +const EmptyComponent = React.memo( + ({ hasVulnFilters, platform, searchQuery }: IEmptyComponentProps) => { + const vulnFilterAndNotSupported = + hasVulnFilters && isIPadOrIPhone(platform); + return vulnFilterAndNotSupported ? ( + + ) : ( + + ); + } +); + interface IHostSoftwareTableProps { tableConfig: any; // TODO: type data?: IGetHostSoftwareResponse | IGetDeviceSoftwareResponse; @@ -149,17 +170,6 @@ const HostSoftwareTable = ({ return ; }, [count, isSoftwareNotDetected]); - const memoizedEmptyComponent = useCallback(() => { - const vulnFilterAndNotSupported = isIPadOrIPhone(platform); - return vulnFilterAndNotSupported ? ( - - ) : ( - - ); - }, [platform, searchQuery]); - // Determines if a user should be able to filter or search in the table const hasData = data && data.software.length > 0; const hasQuery = searchQuery !== ""; @@ -223,7 +233,13 @@ const HostSoftwareTable = ({ pageSize={DEFAULT_PAGE_SIZE} inputPlaceHolder="Search by name or vulnerability (CVE)" onQueryChange={onQueryChange} - emptyComponent={memoizedEmptyComponent} + emptyComponent={() => ( + + )} customFiltersButton={ showFilterHeaders ? renderCustomFiltersButton : undefined } diff --git a/frontend/pages/hosts/details/cards/Software/helpers.tests.ts b/frontend/pages/hosts/details/cards/Software/helpers.tests.ts index 78739413db..6c58f2d02a 100644 --- a/frontend/pages/hosts/details/cards/Software/helpers.tests.ts +++ b/frontend/pages/hosts/details/cards/Software/helpers.tests.ts @@ -2,7 +2,7 @@ import { createMockHostSoftware, createMockHostSoftwarePackage, } from "__mocks__/hostMock"; -import { compareVersions, getUiStatus } from "./helpers"; +import { compareVersions, getUiStatus, getSoftwareSubheader } from "./helpers"; describe("compareVersions", () => { it("correctly compares patch increments", () => { @@ -212,3 +212,85 @@ describe("getUiStatus", () => { expect(getUiStatus(sw, true)).toBe("uninstalled"); }); }); + +describe("getSoftwareSubheader", () => { + test("iOS device, MDM status 'On (personal)', my device page", () => { + const result = getSoftwareSubheader({ + platform: "ios", + hostMdmEnrollmentStatus: "On (personal)", + isMyDevicePage: true, + }); + expect(result).toBe( + "Software installed on your work profile (Managed Apple Account)." + ); + }); + + test("iOS device, MDM status 'On (personal)', NOT my device page", () => { + const result = getSoftwareSubheader({ + platform: "ios", + hostMdmEnrollmentStatus: "On (personal)", + isMyDevicePage: false, + }); + expect(result).toBe( + "Software installed on work profile (Managed Apple Account)." + ); + }); + + test("iOS device, MDM status 'On (manual)', my device page", () => { + const result = getSoftwareSubheader({ + platform: "ios", + hostMdmEnrollmentStatus: "On (manual)", + isMyDevicePage: true, + }); + expect(result).toBe( + "Software installed on your device. Built-in apps (e.g. Calculator) aren't included." + ); + }); + + test("iOS device, MDM status 'On (manual)', NOT my device page", () => { + const result = getSoftwareSubheader({ + platform: "ios", + hostMdmEnrollmentStatus: "On (manual)", + isMyDevicePage: false, + }); + expect(result).toBe( + "Software installed on this host. Built-in apps (e.g. Calculator) aren't included." + ); + }); + + test("iOS device, MDM status not special, my device page", () => { + const result = getSoftwareSubheader({ + platform: "ios", + hostMdmEnrollmentStatus: "Off", + isMyDevicePage: true, + }); + expect(result).toBe("Software installed on your device."); + }); + + test("iOS device, MDM status not special, NOT my device page", () => { + const result = getSoftwareSubheader({ + platform: "ios", + hostMdmEnrollmentStatus: "Off", + isMyDevicePage: false, + }); + expect(result).toBe("Software installed on this host."); + }); + + test("default (NOT iOS device) my device page", () => { + const result = getSoftwareSubheader({ + platform: "windows", + hostMdmEnrollmentStatus: "Off", + isMyDevicePage: true, + }); + expect(result).toBe("Software installed on your device."); + }); + + test("default (NOT iOS device) NOT my device page", () => { + const result = getSoftwareSubheader({ + platform: "windows", + hostMdmEnrollmentStatus: "Off", + isMyDevicePage: false, + }); + expect(result).toBe("Software installed on this host."); + }); +}); diff --git a/frontend/pages/hosts/details/cards/Software/helpers.tsx b/frontend/pages/hosts/details/cards/Software/helpers.tsx index c257c22876..5283953211 100644 --- a/frontend/pages/hosts/details/cards/Software/helpers.tsx +++ b/frontend/pages/hosts/details/cards/Software/helpers.tsx @@ -1,6 +1,8 @@ import { QueryParams } from "utilities/url"; import { Row } from "react-table"; import { flatMap } from "lodash"; +import { HostPlatform, isIPadOrIPhone } from "interfaces/platform"; +import { MdmEnrollmentStatus } from "interfaces/mdm"; import { IHostSoftware, IHostSoftwareUiStatus, @@ -341,3 +343,35 @@ export const installStatusSortType = ( if (safeIndexA > safeIndexB) return 1; return 0; }; + +interface IGetSoftwareSubheader { + platform: HostPlatform; + hostMdmEnrollmentStatus: MdmEnrollmentStatus | null; + isMyDevicePage?: boolean; +} + +/** + * Returns a subheader string for the software page based on platform and MDM enrollment status. + * Handles iOS-specific cases for personal and manual MDM enrollment. + */ +export const getSoftwareSubheader = ({ + platform, + hostMdmEnrollmentStatus, + isMyDevicePage, +}: IGetSoftwareSubheader): string => { + if (isIPadOrIPhone(platform)) { + if (hostMdmEnrollmentStatus === "On (personal)") { + return isMyDevicePage + ? "Software installed on your work profile (Managed Apple Account)." + : "Software installed on work profile (Managed Apple Account)."; + } + if (hostMdmEnrollmentStatus === "On (manual)") { + return isMyDevicePage + ? "Software installed on your device. Built-in apps (e.g. Calculator) aren't included." + : "Software installed on this host. Built-in apps (e.g. Calculator) aren't included."; + } + } + return isMyDevicePage + ? "Software installed on your device." + : "Software installed on this host."; +};