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",