From fd7a586a13fadca187f84abe42801e94afbf5ea2 Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Wed, 15 Jul 2026 13:22:13 -0700 Subject: [PATCH] Fleet UI: Restore dashboard cards hidden despite the API returning their data (#49349) --- changes/49297-free-tier-linux-summary-cards | 2 + .../pages/DashboardPage/DashboardPage.tsx | 13 +- .../cards/OperatingSystems/OSTable.tests.tsx | 65 ++++++ .../cards/OperatingSystems/OSTable.tsx | 10 +- .../MetricsHostCounts.tests.tsx | 219 ++++++++++++++++++ .../MetricsHostCounts/MetricsHostCounts.tsx | 19 +- .../services/entities/operating_systems.ts | 1 + 7 files changed, 316 insertions(+), 13 deletions(-) create mode 100644 changes/49297-free-tier-linux-summary-cards create mode 100644 frontend/pages/DashboardPage/sections/MetricsHostCounts/MetricsHostCounts.tests.tsx diff --git a/changes/49297-free-tier-linux-summary-cards b/changes/49297-free-tier-linux-summary-cards new file mode 100644 index 0000000000..032d46aae7 --- /dev/null +++ b/changes/49297-free-tier-linux-summary-cards @@ -0,0 +1,2 @@ +- Fixed the "Missing hosts" summary card not showing on the Fleet Free dashboard when a platform other than "All" was selected. +- Added an "Operating systems" card to the dashboard when Linux or Android is selected. diff --git a/frontend/pages/DashboardPage/DashboardPage.tsx b/frontend/pages/DashboardPage/DashboardPage.tsx index 279dc1941d..2f9bf3fac3 100644 --- a/frontend/pages/DashboardPage/DashboardPage.tsx +++ b/frontend/pages/DashboardPage/DashboardPage.tsx @@ -242,8 +242,12 @@ const DashboardPage = ({ router, location }: IDashboardProps): JSX.Element => { select: (data: IHostSummary) => data, onSuccess: (data: IHostSummary) => { setLabels(data.builtin_labels); + setMissingCount(data.missing_30_days_count || 0); + // low_disk_space_count and dep_assign_error_count are Premium-only. + // The backend nulls out low_disk_space_count for non-Premium callers, + // and the linked filters (`?low_disk_space=`, ABM issue filters) are + // also Premium-gated, so their cards stay hidden on Free. if (isPremiumTier) { - setMissingCount(data.missing_30_days_count || 0); setLowDiskSpaceCount(data.low_disk_space_count || 0); setAbmIssueCount(data.dep_assign_error_count || 0); } @@ -806,7 +810,11 @@ const DashboardPage = ({ router, location }: IDashboardProps): JSX.Element => { {showMdmCard &&
{MDMCard}
} ); - const linuxLayout = () => null; + const linuxLayout = () => ( + <> +
{OperatingSystemsCard}
+ + ); const chromeLayout = () => ( <> @@ -830,6 +838,7 @@ const DashboardPage = ({ router, location }: IDashboardProps): JSX.Element => { const androidLayout = () => ( <> +
{OperatingSystemsCard}
{showMdmCard &&
{MDMCard}
} ); diff --git a/frontend/pages/DashboardPage/cards/OperatingSystems/OSTable.tests.tsx b/frontend/pages/DashboardPage/cards/OperatingSystems/OSTable.tests.tsx index 27203f9e56..702e8a8627 100644 --- a/frontend/pages/DashboardPage/cards/OperatingSystems/OSTable.tests.tsx +++ b/frontend/pages/DashboardPage/cards/OperatingSystems/OSTable.tests.tsx @@ -54,4 +54,69 @@ describe("Dashboard OS table", () => { expect(screen.getByText("4.5.6")).toBeInTheDocument(); expect(screen.getByText("567")).toBeInTheDocument(); }); + + it("does not render a Name column for non-Linux platforms", () => { + render( + + ); + + expect(screen.queryByText("Name")).not.toBeInTheDocument(); + expect( + screen.queryByText("Microsoft Windows 11 Enterprise 22H2") + ).not.toBeInTheDocument(); + }); + + it("renders a Name column showing the distro name on Linux", () => { + render( + + ); + + expect(screen.getByText("Name")).toBeInTheDocument(); + expect(screen.getByText("Ubuntu")).toBeInTheDocument(); + expect(screen.getByText("Debian GNU/Linux")).toBeInTheDocument(); + expect(screen.getByText("24.04.1")).toBeInTheDocument(); + expect(screen.getByText("13.4")).toBeInTheDocument(); + }); }); diff --git a/frontend/pages/DashboardPage/cards/OperatingSystems/OSTable.tsx b/frontend/pages/DashboardPage/cards/OperatingSystems/OSTable.tsx index d7e849e5e5..a264c0440f 100644 --- a/frontend/pages/DashboardPage/cards/OperatingSystems/OSTable.tsx +++ b/frontend/pages/DashboardPage/cards/OperatingSystems/OSTable.tsx @@ -40,8 +40,14 @@ const OSTable = ({ isLoading, }: IOSTableProps) => { const columnConfigs = useMemo( - () => generateTableHeaders(currentTeamId, undefined), - [currentTeamId] + // Linux is the only platform where the distro name ("Ubuntu", "Debian", + // ...) isn't obvious from the Version column alone, so it gets the extra + // Name column that other platforms don't need. + () => + generateTableHeaders(currentTeamId, undefined, { + includeName: selectedPlatform === "linux", + }), + [currentTeamId, selectedPlatform] ); const showPaginationControls = osVersions.length > PAGE_SIZE; diff --git a/frontend/pages/DashboardPage/sections/MetricsHostCounts/MetricsHostCounts.tests.tsx b/frontend/pages/DashboardPage/sections/MetricsHostCounts/MetricsHostCounts.tests.tsx new file mode 100644 index 0000000000..c73935a0bc --- /dev/null +++ b/frontend/pages/DashboardPage/sections/MetricsHostCounts/MetricsHostCounts.tests.tsx @@ -0,0 +1,219 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; + +import { PlatformValueOptions } from "utilities/constants"; + +import MetricsHostCounts from "./MetricsHostCounts"; + +// Render react-router's as a plain anchor so the summary cards +// (which wrap themselves in a linkable ) can render without a +// surrounding . +jest.mock("react-router", () => ({ + Link: ({ to, children }: { to: string; children: React.ReactNode }) => ( + {children} + ), +})); + +const TOTAL_HOSTS_TITLE = "Total hosts"; +const MISSING_HOSTS_TITLE = "Missing hosts"; +const LOW_DISK_SPACE_TITLE = "Low disk space hosts"; +const ABM_ISSUE_TITLE = "AB issue"; + +interface IRenderCase { + platform: PlatformValueOptions; + totalHosts: boolean; + missingHosts: boolean; + lowDiskSpaceHosts: boolean; +} + +const renderMetrics = ({ + platform, + isPremiumTier, + abmIssueCount = 0, +}: { + platform: PlatformValueOptions; + isPremiumTier: boolean; + abmIssueCount?: number; +}) => + render( + + ); + +const expectCards = ({ + totalHosts, + missingHosts, + lowDiskSpaceHosts, +}: Omit) => { + expect(!!screen.queryByText(TOTAL_HOSTS_TITLE)).toBe(totalHosts); + expect(!!screen.queryByText(MISSING_HOSTS_TITLE)).toBe(missingHosts); + expect(!!screen.queryByText(LOW_DISK_SPACE_TITLE)).toBe(lowDiskSpaceHosts); +}; + +// Missing hosts renders on both tiers; Low disk space is Premium-only. +// Both are hidden on iOS, iPadOS, and Android (no missing/low-disk-space +// data model for those platforms). Total hosts renders only on "all". +describe("MetricsHostCounts", () => { + describe("Premium tier", () => { + const premiumCases: IRenderCase[] = [ + { + platform: "all", + totalHosts: true, + missingHosts: true, + lowDiskSpaceHosts: true, + }, + { + platform: "darwin", + totalHosts: false, + missingHosts: true, + lowDiskSpaceHosts: true, + }, + { + platform: "windows", + totalHosts: false, + missingHosts: true, + lowDiskSpaceHosts: true, + }, + { + platform: "linux", + totalHosts: false, + missingHosts: true, + lowDiskSpaceHosts: true, + }, + { + platform: "chrome", + totalHosts: false, + missingHosts: true, + lowDiskSpaceHosts: true, + }, + { + platform: "ios", + totalHosts: false, + missingHosts: false, + lowDiskSpaceHosts: false, + }, + { + platform: "ipados", + totalHosts: false, + missingHosts: false, + lowDiskSpaceHosts: false, + }, + { + platform: "android", + totalHosts: false, + missingHosts: false, + lowDiskSpaceHosts: false, + }, + ]; + + it.each(premiumCases)( + "$platform: shows Total=$totalHosts, Missing=$missingHosts, LowDisk=$lowDiskSpaceHosts", + ({ platform, ...expected }) => { + renderMetrics({ platform, isPremiumTier: true }); + expectCards(expected); + } + ); + }); + + describe("Free tier", () => { + // Free never shows Low disk space (Premium-only). Missing hosts renders + // on the same platforms as Premium. + const freeCases: IRenderCase[] = [ + { + platform: "all", + totalHosts: true, + missingHosts: true, + lowDiskSpaceHosts: false, + }, + { + platform: "darwin", + totalHosts: false, + missingHosts: true, + lowDiskSpaceHosts: false, + }, + { + platform: "windows", + totalHosts: false, + missingHosts: true, + lowDiskSpaceHosts: false, + }, + { + platform: "linux", + totalHosts: false, + missingHosts: true, + lowDiskSpaceHosts: false, + }, + { + platform: "chrome", + totalHosts: false, + missingHosts: true, + lowDiskSpaceHosts: false, + }, + { + platform: "ios", + totalHosts: false, + missingHosts: false, + lowDiskSpaceHosts: false, + }, + { + platform: "ipados", + totalHosts: false, + missingHosts: false, + lowDiskSpaceHosts: false, + }, + { + platform: "android", + totalHosts: false, + missingHosts: false, + lowDiskSpaceHosts: false, + }, + ]; + + it.each(freeCases)( + "$platform: shows Total=$totalHosts, Missing=$missingHosts, LowDisk=$lowDiskSpaceHosts", + ({ platform, ...expected }) => { + renderMetrics({ platform, isPremiumTier: false }); + expectCards(expected); + } + ); + }); + + describe("ABM issue card", () => { + it("renders on Premium when abmIssueCount > 0", () => { + renderMetrics({ + platform: "darwin", + isPremiumTier: true, + abmIssueCount: 2, + }); + expect(screen.getByText(ABM_ISSUE_TITLE)).toBeInTheDocument(); + }); + + it("does not render when abmIssueCount is 0", () => { + renderMetrics({ + platform: "darwin", + isPremiumTier: true, + abmIssueCount: 0, + }); + expect(screen.queryByText(ABM_ISSUE_TITLE)).not.toBeInTheDocument(); + }); + + // DashboardPage never sets abmIssueCount on Free, but the component + // itself doesn't tier-gate this card — protecting that invariant here. + it("guards purely on count, not tier (Free with count > 0 would render, but the parent never populates it)", () => { + renderMetrics({ + platform: "darwin", + isPremiumTier: false, + abmIssueCount: 2, + }); + expect(screen.getByText(ABM_ISSUE_TITLE)).toBeInTheDocument(); + }); + }); +}); diff --git a/frontend/pages/DashboardPage/sections/MetricsHostCounts/MetricsHostCounts.tsx b/frontend/pages/DashboardPage/sections/MetricsHostCounts/MetricsHostCounts.tsx index c9080dfc8d..41940efdb1 100644 --- a/frontend/pages/DashboardPage/sections/MetricsHostCounts/MetricsHostCounts.tsx +++ b/frontend/pages/DashboardPage/sections/MetricsHostCounts/MetricsHostCounts.tsx @@ -67,18 +67,19 @@ const MetricsHostCounts = ({ /> ) : null; + const showMissingAndLowDiskHosts = + selectedPlatform !== "ios" && + selectedPlatform !== "ipados" && + selectedPlatform !== "android"; + return (
{selectedPlatform === "all" && TotalHostsCard} - {isPremiumTier && - selectedPlatform !== "ios" && - selectedPlatform !== "ipados" && - selectedPlatform !== "android" && ( - <> - {MissingHostsCard} - {LowDiskSpaceHostsCard} - - )} + {showMissingAndLowDiskHosts && MissingHostsCard} + {/* Low disk space is Premium-only: `low_disk_space_count` is null for + non-Premium callers and the linked filter is Premium-gated. */} + {isPremiumTier && showMissingAndLowDiskHosts && LowDiskSpaceHostsCard} + {/* ABM issue count is only populated on Premium (see DashboardPage). */} {ABMIssueHostsCard}
); diff --git a/frontend/services/entities/operating_systems.ts b/frontend/services/entities/operating_systems.ts index 4153024be5..3cafc4b25f 100644 --- a/frontend/services/entities/operating_systems.ts +++ b/frontend/services/entities/operating_systems.ts @@ -9,6 +9,7 @@ import { buildQueryStringFromParams } from "utilities/url"; export const OS_VERSIONS_API_SUPPORTED_PLATFORMS = [ "darwin", "windows", + "linux", "chrome", "ios", "ipados",