From fbe21a951efdfc3767ad4416e8cda774c2b1e892 Mon Sep 17 00:00:00 2001 From: Nico <32375741+nulmete@users.noreply.github.com> Date: Mon, 22 Dec 2025 18:15:06 -0300 Subject: [PATCH] Add Vitals section to Host details (#37604) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Related issue:** Resolves #37603 ## Testing - [x] Added/updated automated tests - [ ] Where appropriate, [automated tests simulate multiple hosts and test for host isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing) (updates to one hosts's records do not affect another) - [x] QA'd all new/changed functionality manually ## Screenshots Note: these were taken on a 32" screen For reference, see [Figma](https://www.figma.com/design/v7WjL5zQuFIZerWYaSwy8o/-27322-Surface-custom-host-vitals?node-id=5636-4950&t=vTLKciuyExCbMZp6-0) design ### Host details page #### Before Screenshot 2025-12-22 at 3 53
11 PM #### After Screenshot 2025-12-22 at 4 14
02 PM ### My device page #### Before Screenshot 2025-12-22 at 3 53 19 PM #### After Screenshot 2025-12-22 at 4 14 20 PM --- frontend/interfaces/platform.ts | 3 + .../details/DeviceUserPage/DeviceUserPage.tsx | 14 +- .../HostDetailsPage/HostDetailsPage.tsx | 58 +-- .../details/HostDetailsPage/_styles.scss | 6 +- .../hosts/details/cards/About/About.tests.tsx | 202 ++++++++++- .../pages/hosts/details/cards/About/About.tsx | 331 ++++++++++++++++-- .../hosts/details/cards/About/_styles.scss | 12 +- .../cards/HostSummary/HostSummary.tests.tsx | 270 +------------- .../details/cards/HostSummary/HostSummary.tsx | 281 +-------------- frontend/utilities/constants.tsx | 24 +- 10 files changed, 565 insertions(+), 636 deletions(-) diff --git a/frontend/interfaces/platform.ts b/frontend/interfaces/platform.ts index 865959aae4..d2a5d3873f 100644 --- a/frontend/interfaces/platform.ts +++ b/frontend/interfaces/platform.ts @@ -159,6 +159,9 @@ export const isAndroid = ( platform: string | HostPlatform ): platform is "android" => platform === "android"; +export const isChrome = (platform: string | HostPlatform) => + platform === "chrome"; + /** isMobilePlatform checks if the platform is an iPad or iPhone or Android. */ export const isMobilePlatform = (platform: string | HostPlatform) => isIPadOrIPhone(platform) || isAndroid(platform); diff --git a/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tsx b/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tsx index 072c812a97..fda7199463 100644 --- a/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tsx +++ b/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tsx @@ -55,14 +55,14 @@ import PATHS from "router/paths"; import { DEFAULT_USE_QUERY_OPTIONS, DOCUMENT_TITLE_SUFFIX, - HOST_ABOUT_DATA, + HOST_VITALS_DATA, HOST_SUMMARY_DATA, } from "utilities/constants"; import UnsupportedScreenSize from "layouts/UnsupportedScreenSize"; import HostSummaryCard from "../cards/HostSummary"; -import AboutCard from "../cards/About"; +import VitalsCard from "../cards/About"; import SoftwareCard from "../cards/Software"; import PoliciesCard from "../cards/Policies"; import InfoModal from "./InfoModal"; @@ -366,7 +366,7 @@ const DeviceUserPage = ({ const summaryData = normalizeEmptyValues(pick(host, HOST_SUMMARY_DATA)); - const aboutData = normalizeEmptyValues(pick(host, HOST_ABOUT_DATA)); + const vitalsData = normalizeEmptyValues(pick(host, HOST_VITALS_DATA)); const { data: setupStepStatuses, @@ -744,13 +744,13 @@ const DeviceUserPage = ({ hostSettings={host?.mdm.profiles ?? []} osSettings={host?.mdm.os_settings} /> - + - - - | React.KeyboardEvent - ) => { - e.preventDefault(); - setShowUpdateEndUserModal(true); - }} /> {showActivityCard && ( )} + + | React.KeyboardEvent + ) => { + e.preventDefault(); + setShowUpdateEndUserModal(true); + }} + /> {showAgentOptionsCard && ( { +describe("Vitals Card component", () => { it("renders only the device Hardware model for Android hosts that were not enrolled in MDM personally", () => { const mockHost = createMockHost({ platform: "android", @@ -14,7 +16,7 @@ describe("About Card component", () => { hardware_serial: "", }); - render(); + render(); expect(screen.getByText("Hardware model")).toBeInTheDocument(); expect(screen.getByText("Pixel 6")).toBeInTheDocument(); @@ -35,7 +37,7 @@ describe("About Card component", () => { }), }); - render(); + render(); expect(screen.getByText("Hardware model")).toBeInTheDocument(); expect(screen.getByText("Pixel 6")).toBeInTheDocument(); @@ -57,7 +59,7 @@ describe("About Card component", () => { }), }); - render(); + render(); expect(screen.getByText("Enrollment ID")).toBeInTheDocument(); expect(screen.getAllByText("enrollment-id-12345")[0]).toBeInTheDocument(); @@ -79,7 +81,7 @@ describe("About Card component", () => { }), }); - render(); + render(); expect(screen.getByText("Enrollment ID")).toBeInTheDocument(); expect(screen.getAllByText("enrollment-id-12345")[0]).toBeInTheDocument(); @@ -101,7 +103,7 @@ describe("About Card component", () => { }), }); - render(); + render(); expect(screen.getByText("Hardware model")).toBeInTheDocument(); expect(screen.getByText("iPhone 12")).toBeInTheDocument(); @@ -123,7 +125,7 @@ describe("About Card component", () => { }), }); - render(); + render(); expect(screen.getByText("Hardware model")).toBeInTheDocument(); expect(screen.getByText("IPad Pro")).toBeInTheDocument(); @@ -147,7 +149,7 @@ describe("About Card component", () => { }), }); - render(); + render(); expect(screen.getByText("Enrollment ID")).toBeInTheDocument(); expect(screen.getAllByText("enrollment-id-12345")[0]).toBeInTheDocument(); @@ -171,7 +173,7 @@ describe("About Card component", () => { mdm: undefined, }); - render(); + render(); expect(screen.getByText("Hardware model")).toBeInTheDocument(); expect(screen.getByText("MacBook Pro")).toBeInTheDocument(); @@ -197,7 +199,7 @@ describe("About Card component", () => { }), }); - render(); + render(); expect(screen.getByText("Hardware model")).toBeInTheDocument(); expect(screen.getByText("MacBook Pro")).toBeInTheDocument(); @@ -223,7 +225,7 @@ describe("About Card component", () => { }), }); - render(); + render(); expect(screen.getByText("Hardware model")).toBeInTheDocument(); expect(screen.getByText("MacBook Pro")).toBeInTheDocument(); @@ -236,3 +238,177 @@ describe("About Card component", () => { expect(screen.queryByText("Enrollment ID")).not.toBeInTheDocument(); }); }); + +describe("Disk encryption data", () => { + it("renders 'On' for macOS when enabled", () => { + const mockHost = createMockHost({ + platform: "darwin", + disk_encryption_enabled: true, + }); + + render(); + + expect(screen.getByText("Disk encryption")).toBeInTheDocument(); + expect(screen.getByText("On")).toBeInTheDocument(); + }); + + it("renders 'Off' for Windows when disabled", () => { + const mockHost = createMockHost({ + platform: "windows", + disk_encryption_enabled: false, + }); + + render(); + + expect(screen.getByText("Disk encryption")).toBeInTheDocument(); + expect(screen.getByText("Off")).toBeInTheDocument(); + }); + + it("renders 'Unknown' when disk encryption status is undefined", () => { + const mockHost = createMockHost({ + platform: "darwin", + disk_encryption_enabled: undefined, + }); + + render(); + + expect(screen.getByText("Disk encryption")).toBeInTheDocument(); + expect(screen.getByText("Unknown")).toBeInTheDocument(); + }); + + it("renders 'Always on' for Chrome platform", () => { + const mockHost = createMockHost({ + platform: "chrome", + disk_encryption_enabled: true, + }); + + render(); + + expect(screen.getByText("Disk encryption")).toBeInTheDocument(); + expect(screen.getByText("Always on")).toBeInTheDocument(); + }); + + it("does not render disk encryption for unsupported platforms", () => { + const mockHost = createMockHost({ + platform: "android", + disk_encryption_enabled: true, + }); + + render(); + + expect(screen.queryByText("Disk encryption")).not.toBeInTheDocument(); + }); +}); + +describe("Agent data", () => { + it("with all info present, render Agent header with orbit_version and tooltip with all 3 data points", async () => { + const customRender = createCustomRenderer({}); + const mockHost = createMockHost({ + platform: "darwin", + orbit_version: "1.2.0", + osquery_version: "5.5.1", + fleet_desktop_version: "1.0.0", + }); + + const { user } = customRender(); + + expect(screen.getByText("Agent")).toBeInTheDocument(); + expect(screen.getByText("1.2.0")).toBeInTheDocument(); + + await user.hover(screen.getByText("1.2.0")); + + await waitFor(() => { + expect(screen.getByText(/osquery: 5.5.1/)).toBeInTheDocument(); + expect(screen.getByText(/Orbit: 1.2.0/)).toBeInTheDocument(); + expect(screen.getByText(/Fleet Desktop: 1.0.0/)).toBeInTheDocument(); + }); + }); + + it("omit fleet desktop from tooltip if no fleet desktop version", async () => { + const customRender = createCustomRenderer({}); + const mockHost = createMockHost({ + platform: "darwin", + orbit_version: "1.2.0", + osquery_version: "5.5.1", + fleet_desktop_version: DEFAULT_EMPTY_CELL_VALUE, + }); + + const { user } = customRender(); + + expect(screen.getByText("Agent")).toBeInTheDocument(); + + await user.hover(screen.getByText("1.2.0")); + + await waitFor(() => { + expect(screen.getByText(/osquery: 5.5.1/)).toBeInTheDocument(); + expect(screen.getByText(/Orbit: 1.2.0/)).toBeInTheDocument(); + expect(screen.queryByText(/Fleet desktop:/i)).not.toBeInTheDocument(); + }); + }); + + it("for Chromebooks, render Agent header with osquery_version that is the fleetd chrome version and no tooltip", async () => { + const customRender = createCustomRenderer({}); + const mockHost = createMockHost({ + platform: "chrome", + osquery_version: "fleetd-chrome 1.2.0", + }); + + const fleetdChromeVersion = mockHost.osquery_version as string; + + const { user } = customRender(); + + expect(screen.getByText("Agent")).toBeInTheDocument(); + await user.hover(screen.getByText(new RegExp(fleetdChromeVersion, "i"))); + expect(screen.queryByText("Osquery")).not.toBeInTheDocument(); + }); +}); + +describe("Disk space field visibility", () => { + it("hides disk space field when storage measurement is not supported (sentinel value -1)", () => { + const mockHost = createMockHost({ + gigs_disk_space_available: -1, + percent_disk_space_available: 0, + platform: "android", + }); + + render(); + + expect(screen.queryByText("Disk space")).not.toBeInTheDocument(); + }); + + it("shows disk space field for zero storage (disk full)", () => { + const mockHost = createMockHost({ + gigs_disk_space_available: 0, + percent_disk_space_available: 0, + platform: "android", + }); + + render(); + + expect(screen.getByText("Disk space")).toBeInTheDocument(); + }); + + it("renders disk space normally for positive values", () => { + const mockHost = createMockHost({ + gigs_disk_space_available: 25.5, + percent_disk_space_available: 50, + platform: "darwin", + }); + + render(); + + expect(screen.getByText("Disk space")).toBeInTheDocument(); + }); + + it("handles other negative values as not supported", () => { + const mockHost = createMockHost({ + gigs_disk_space_available: -10, + percent_disk_space_available: 0, + platform: "android", + }); + + render(); + + expect(screen.queryByText("Disk space")).not.toBeInTheDocument(); + }); +}); diff --git a/frontend/pages/hosts/details/cards/About/About.tsx b/frontend/pages/hosts/details/cards/About/About.tsx index a91a7c4dca..2ac7ef51b4 100644 --- a/frontend/pages/hosts/details/cards/About/About.tsx +++ b/frontend/pages/hosts/details/cards/About/About.tsx @@ -1,17 +1,31 @@ import React from "react"; import classnames from "classnames"; +import { IAppleDeviceUpdates } from "interfaces/config"; import { IHostMdmData, IMunkiData } from "interfaces/host"; -import { isAndroid, isIPadOrIPhone } from "interfaces/platform"; +import { + isAndroid, + isIPadOrIPhone, + isChrome, + platformSupportsDiskEncryption, + DiskEncryptionSupportedPlatform, +} from "interfaces/platform"; import { isBYODAccountDrivenUserEnrollment, MDM_ENROLLMENT_STATUS_UI_MAP, } from "interfaces/mdm"; +import { ROLLING_ARCH_LINUX_VERSIONS } from "interfaces/software"; import { DEFAULT_EMPTY_CELL_VALUE, MDM_STATUS_TOOLTIP, BATTERY_TOOLTIP, } from "utilities/constants"; +import { + humanHostMemory, + wrapFleetHelper, + removeOSPrefix, + compareVersions, +} from "utilities/helpers"; import { HumanTimeDiffWithFleetLaunchCutoff } from "components/HumanTimeDiffWithDateTip"; import TooltipWrapper from "components/TooltipWrapper"; @@ -19,19 +33,88 @@ import TooltipTruncatedText from "components/TooltipTruncatedText"; import Card from "components/Card"; import DataSet from "components/DataSet"; import CardHeader from "components/CardHeader"; +import TooltipWrapperArchLinuxRolling from "components/TooltipWrapperArchLinuxRolling"; +import Icon from "components/Icon/Icon"; -interface IAboutProps { - aboutData: { [key: string]: any }; +import DiskSpaceIndicator from "pages/hosts/components/DiskSpaceIndicator"; + +interface IVitalsProps { + vitalsData: { [key: string]: any }; munki?: IMunkiData | null; mdm?: IHostMdmData; + osVersionRequirement?: IAppleDeviceUpdates; className?: string; } -const baseClass = "about-card"; +const baseClass = "vitals-card"; -const About = ({ aboutData, munki, mdm, className }: IAboutProps) => { - const isIosOrIpadosHost = isIPadOrIPhone(aboutData.platform); - const isAndroidHost = isAndroid(aboutData.platform); +const DISK_ENCRYPTION_MESSAGES = { + darwin: { + enabled: ( + <> + The disk is encrypted. The user must enter their +
password when they start their computer. + + ), + disabled: ( + <> + The disk might be encrypted, but FileVault is off. The +
disk can be accessed without entering a password. + + ), + }, + windows: { + enabled: ( + <> + The disk is encrypted. If recently turned on, +
encryption could take awhile. + + ), + disabled: "The disk is unencrypted.", + }, + linux: { + enabled: "The disk is encrypted.", + unknown: "The disk may be encrypted.", + }, +}; + +const getHostDiskEncryptionTooltipMessage = ( + platform: DiskEncryptionSupportedPlatform, // TODO: improve this type + diskEncryptionEnabled = false +) => { + if (platform === "chrome") { + return "Fleet does not check for disk encryption on Chromebooks, as they are encrypted by default."; + } + + if ( + platform === "rhel" || + platform === "ubuntu" || + platform === "arch" || + platform === "archarm" || + platform === "manjaro" || + platform === "manjaro-arm" + ) { + return DISK_ENCRYPTION_MESSAGES.linux[ + diskEncryptionEnabled ? "enabled" : "unknown" + ]; + } + + // mac or windows + return DISK_ENCRYPTION_MESSAGES[platform][ + diskEncryptionEnabled ? "enabled" : "disabled" + ]; +}; + +const Vitals = ({ + vitalsData, + munki, + mdm, + osVersionRequirement, + className, +}: IVitalsProps) => { + const isIosOrIpadosHost = isIPadOrIPhone(vitalsData.platform); + const isAndroidHost = isAndroid(vitalsData.platform); + const isChromeHost = isChrome(vitalsData.platform); // Generate the device ID data set based on MDM enrollment status. This is // either the Enrollment ID for personal (BYOD) devices or the Serial number @@ -42,7 +125,7 @@ const About = ({ aboutData, munki, mdm, className }: IAboutProps) => { let deviceIdDataSet = ( } + value={} /> ); @@ -63,7 +146,7 @@ const About = ({ aboutData, munki, mdm, className }: IAboutProps) => { Enrollment ID } - value={} + value={} /> ); } @@ -79,7 +162,7 @@ const About = ({ aboutData, munki, mdm, className }: IAboutProps) => { return ( <> {DeviceIdDataSet} - + ); } @@ -90,7 +173,7 @@ const About = ({ aboutData, munki, mdm, className }: IAboutProps) => { return ( <> {DeviceIdDataSet} - + ); } @@ -99,11 +182,11 @@ const About = ({ aboutData, munki, mdm, className }: IAboutProps) => { // (either Serial number or Enrollment ID). return ( <> - + {DeviceIdDataSet} } + value={} /> { Public IP address } - value={} + value={} /> ); @@ -158,7 +241,7 @@ const About = ({ aboutData, munki, mdm, className }: IAboutProps) => { }; const renderGeolocation = () => { - const geolocation = aboutData.geolocation; + const geolocation = vitalsData.geolocation; if (!geolocation) { return null; @@ -172,9 +255,9 @@ const About = ({ aboutData, munki, mdm, className }: IAboutProps) => { const renderBattery = () => { if ( - aboutData.batteries === null || - typeof aboutData.batteries !== "object" || - aboutData.batteries?.[0]?.health === "Unknown" + vitalsData.batteries === null || + typeof vitalsData.batteries !== "object" || + vitalsData.batteries?.[0]?.health === "Unknown" ) { return null; } @@ -183,9 +266,9 @@ const About = ({ aboutData, munki, mdm, className }: IAboutProps) => { title="Battery condition" value={ - {aboutData.batteries?.[0]?.health} + {vitalsData.batteries?.[0]?.health} } /> @@ -194,6 +277,191 @@ const About = ({ aboutData, munki, mdm, className }: IAboutProps) => { // TODO(android): confirm visible fields using actual android device data + const { + platform, + os_version, + disk_encryption_enabled: diskEncryptionEnabled, + } = vitalsData; + + const renderDiskSpaceSummary = () => { + // Hide disk space field if storage measurement is not supported (sentinel value -1) + if ( + typeof vitalsData.gigs_disk_space_available === "number" && + vitalsData.gigs_disk_space_available < 0 + ) { + return null; + } + + const title = isAndroidHost ? ( + + Disk space + + ) : ( + "Disk space" + ); + + return ( + + } + /> + ); + }; + + const renderDiskEncryptionSummary = () => { + if (!platformSupportsDiskEncryption(platform, os_version)) { + return <>; + } + const tooltipMessage = getHostDiskEncryptionTooltipMessage( + platform, + diskEncryptionEnabled + ); + + let statusText; + switch (true) { + case isChromeHost: + statusText = "Always on"; + break; + case diskEncryptionEnabled === true: + statusText = "On"; + break; + case diskEncryptionEnabled === false: + statusText = "Off"; + break; + case (diskEncryptionEnabled === null || + diskEncryptionEnabled === undefined) && + platformSupportsDiskEncryption(platform, os_version): + statusText = "Unknown"; + break; + default: + // something unexpected happened on the way to this component, display whatever we got or + // "Unknown" to draw attention to the issue. + statusText = diskEncryptionEnabled || "Unknown"; + } + + return ( + + {statusText} + + } + /> + ); + }; + + const renderAgentSummary = () => { + if (isIosOrIpadosHost || isAndroidHost) { + return null; + } + + const { + orbit_version, + osquery_version, + fleet_desktop_version, + } = vitalsData; + + if (isChromeHost) { + return ; + } + + if (orbit_version !== DEFAULT_EMPTY_CELL_VALUE) { + return ( + + osquery: {osquery_version} +
+ Orbit: {orbit_version} + {fleet_desktop_version !== DEFAULT_EMPTY_CELL_VALUE && ( + <> +
+ Fleet Desktop: {fleet_desktop_version} + + )} + + } + > + {orbit_version} + + } + /> + ); + } + return ; + }; + + const renderOperatingSystemSummary = () => { + // No tooltip if minimum version is not set, including all Windows, Linux, ChromeOS, Android operating systems + if (!osVersionRequirement?.minimum_version) { + const version = vitalsData.os_version; + const versionForRender = ROLLING_ARCH_LINUX_VERSIONS.includes(version) ? ( + // wrap a tooltip around the "rolling" suffix + <> + {version.slice(0, -8)} + + + ) : ( + version + ); + return ( + + ); + } + + const osVersionWithoutPrefix = removeOSPrefix(vitalsData.os_version); + const osVersionRequirementMet = + compareVersions( + osVersionWithoutPrefix, + osVersionRequirement.minimum_version + ) >= 0; + + return ( + + {!osVersionRequirementMet && ( + + )} + + Does not meet minimum version requirement. +
+ Deadline to update: {osVersionRequirement.deadline} + + ) + } + > + {vitalsData.os_version} +
+ + } + /> + ); + }; + const classNames = classnames(baseClass, className); return ( @@ -202,13 +470,13 @@ const About = ({ aboutData, munki, mdm, className }: IAboutProps) => { borderRadiusSize="xxlarge" paddingSize="xlarge" > - +
} /> @@ -217,19 +485,32 @@ const About = ({ aboutData, munki, mdm, className }: IAboutProps) => { title="Last restarted" value={ } /> )} + {renderDiskEncryptionSummary()} + {!isChromeHost && renderDiskSpaceSummary()} + {renderAgentSummary()} {renderHardwareSerialAndIPs()} + {!isIosOrIpadosHost && ( + + )} + {renderBattery()} + {!isIosOrIpadosHost && ( + + )} + {renderOperatingSystemSummary()} {renderMunkiData()} {renderMdmData()} {renderGeolocation()} - {renderBattery()}
); }; -export default About; +export default Vitals; diff --git a/frontend/pages/hosts/details/cards/About/_styles.scss b/frontend/pages/hosts/details/cards/About/_styles.scss index e2eec3f20a..68b9f664f2 100644 --- a/frontend/pages/hosts/details/cards/About/_styles.scss +++ b/frontend/pages/hosts/details/cards/About/_styles.scss @@ -1,8 +1,8 @@ -.about-card { +.vitals-card { @include vertical-card-layout; .truncated-tooltip { - .about-card__device-mapping__source { + .vitals-card__device-mapping__source { color: inherit; } } @@ -44,16 +44,16 @@ // TooltipTruncatedText component. &__info-grid { display: grid; - grid-template-columns: repeat(2, minmax(150px, max-content)); + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: $gap-data-sets $pad-xxlarge; - // at the larger screen widths we want to have 3 columns. + // at the larger screen widths we want to have 6 columns. @media (min-width: $break-xxl) { - grid-template-columns: repeat(3, minmax(150px, max-content)); + grid-template-columns: repeat(6, minmax(150px, 1fr)); } } .text-muted { color: $ui-fleet-black-50; } -} +} \ No newline at end of file diff --git a/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tests.tsx b/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tests.tsx index e6bf326b7c..b362ac6dd9 100644 --- a/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tests.tsx +++ b/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tests.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { screen, waitFor } from "@testing-library/react"; +import { screen } from "@testing-library/react"; import { createCustomRenderer } from "test/test-utils"; import createMockUser from "__mocks__/userMock"; @@ -56,150 +56,8 @@ describe("Host Summary section", () => { }); }); - describe("Disk encryption data", () => { - it("renders 'On' for macOS when enabled", () => { - const render = createCustomRenderer({ - context: { - app: { - isPremiumTier: true, - isGlobalAdmin: true, - currentUser: createMockUser(), - }, - }, - }); - const summaryData = createMockHostSummary({ - platform: "darwin", - disk_encryption_enabled: true, - }); - render(); - expect(screen.getByText("Disk encryption")).toBeInTheDocument(); - expect(screen.getByText("On")).toBeInTheDocument(); - }); - - it("renders 'Off' for Windows when disabled", () => { - const render = createCustomRenderer({ - context: { - app: { - isPremiumTier: true, - isGlobalAdmin: true, - currentUser: createMockUser(), - }, - }, - }); - const summaryData = createMockHostSummary({ - platform: "windows", - disk_encryption_enabled: false, - }); - render(); - expect(screen.getByText("Disk encryption")).toBeInTheDocument(); - expect(screen.getByText("Off")).toBeInTheDocument(); - }); - - it("renders Chromebook message for Chrome platform", () => { - const render = createCustomRenderer({ - context: { - app: { - isPremiumTier: true, - isGlobalAdmin: true, - currentUser: createMockUser(), - }, - }, - }); - const summaryData = createMockHostSummary({ platform: "chrome" }); - render(); - expect(screen.getByText("Always on")).toBeInTheDocument(); - }); - }); - - describe("Agent data", () => { - it("with all info present, render Agent header with orbit_version and tooltip with all 3 data points", async () => { - const render = createCustomRenderer({ - context: { - app: { - isPremiumTier: true, - isGlobalAdmin: true, - currentUser: createMockUser(), - }, - }, - }); - const summaryData = createMockHostSummary(); - const orbitVersion = summaryData.orbit_version as string; - const osqueryVersion = summaryData.osquery_version as string; - const fleetdVersion = summaryData.fleet_desktop_version as string; - - const { user } = render(); - - expect(screen.getByText("Agent")).toBeInTheDocument(); - - await user.hover(screen.getByText(new RegExp(orbitVersion, "i"))); - - await waitFor(() => { - expect( - screen.getByText(new RegExp(osqueryVersion, "i")) - ).toBeInTheDocument(); - expect( - screen.getByText(new RegExp(fleetdVersion, "i")) - ).toBeInTheDocument(); - }); - }); - - it("omit fleet desktop from tooltip if no fleet desktop version", async () => { - const render = createCustomRenderer({ - context: { - app: { - isPremiumTier: true, - isGlobalAdmin: true, - currentUser: createMockUser(), - }, - }, - }); - const summaryData = createMockHostSummary({ - fleet_desktop_version: null, - }); - const orbitVersion = summaryData.orbit_version as string; - const osqueryVersion = summaryData.osquery_version as string; - - const { user } = render(); - - expect(screen.getByText("Agent")).toBeInTheDocument(); - - await user.hover(screen.getByText(new RegExp(orbitVersion, "i"))); - - await waitFor(() => { - expect( - screen.getByText(new RegExp(osqueryVersion, "i")) - ).toBeInTheDocument(); - expect(screen.queryByText(/Fleet desktop:/i)).not.toBeInTheDocument(); - }); - }); - - it("for Chromebooks, render Agent header with osquery_version that is the fleetd chrome version and no tooltip", async () => { - const render = createCustomRenderer({ - context: { - app: { - isPremiumTier: true, - isGlobalAdmin: true, - currentUser: createMockUser(), - }, - }, - }); - const summaryData = createMockHostSummary({ - platform: "chrome", - osquery_version: "fleetd-chrome 1.2.0", - }); - - const fleetdChromeVersion = summaryData.osquery_version as string; - - const { user } = render(); - - expect(screen.getByText("Agent")).toBeInTheDocument(); - await user.hover(screen.getByText(new RegExp(fleetdChromeVersion, "i"))); - expect(screen.queryByText("Osquery")).not.toBeInTheDocument(); - }); - }); - describe("iOS and iPadOS data", () => { - it("for iOS, renders Team, Disk space, and Operating system data only", async () => { + it("for iOS, renders Team data only", async () => { const render = createCustomRenderer({ context: { app: { @@ -218,28 +76,14 @@ describe("Host Summary section", () => { }); const teamName = summaryData.team_name as string; - const diskSpaceAvailable = summaryData.gigs_disk_space_available as string; - const osVersion = summaryData.os_version as string; render(); expect(screen.getByText("Team").nextElementSibling).toHaveTextContent( teamName ); - expect( - screen.getByText("Disk space").nextElementSibling - ).toHaveTextContent(`${diskSpaceAvailable} GB available`); - expect( - screen.getByText("Operating system").nextElementSibling - ).toHaveTextContent(osVersion); - - expect(screen.queryByText("Status")).not.toBeInTheDocument(); - expect(screen.queryByText("Memory")).not.toBeInTheDocument(); - expect(screen.queryByText("Processor type")).not.toBeInTheDocument(); - expect(screen.queryByText("Agent")).not.toBeInTheDocument(); - expect(screen.queryByText("Osquery")).not.toBeInTheDocument(); }); - it("for iPadOS, renders Team, Disk space, and Operating system data only", async () => { + it("for iPadOS, renders Team data only", async () => { const render = createCustomRenderer({ context: { app: { @@ -258,26 +102,12 @@ describe("Host Summary section", () => { }); const teamName = summaryData.team_name as string; - const diskSpaceAvailable = summaryData.gigs_disk_space_available as string; - const osVersion = summaryData.os_version as string; render(); expect(screen.getByText("Team").nextElementSibling).toHaveTextContent( teamName ); - expect( - screen.getByText("Disk space").nextElementSibling - ).toHaveTextContent(`${diskSpaceAvailable} GB available`); - expect( - screen.getByText("Operating system").nextElementSibling - ).toHaveTextContent(osVersion); - - expect(screen.queryByText("Status")).not.toBeInTheDocument(); - expect(screen.queryByText("Memory")).not.toBeInTheDocument(); - expect(screen.queryByText("Processor type")).not.toBeInTheDocument(); - expect(screen.queryByText("Agent")).not.toBeInTheDocument(); - expect(screen.queryByText("Osquery")).not.toBeInTheDocument(); }); }); @@ -334,98 +164,4 @@ describe("Host Summary section", () => { expect(screen.getByText("Bootstrap package")).toBeInTheDocument(); }); }); - - describe("Disk space field visibility", () => { - it("hides disk space field when storage measurement is not supported (sentinel value -1)", () => { - const render = createCustomRenderer({ - context: { - app: { - isPremiumTier: false, - isGlobalAdmin: true, - currentUser: createMockUser(), - }, - }, - }); - - const summaryData = createMockHostSummary({ - gigs_disk_space_available: -1, - percent_disk_space_available: 0, - platform: "android", - }); - - render(); - - // Disk space field should not be rendered at all - expect(screen.queryByText("Disk space")).not.toBeInTheDocument(); - }); - - it("shows disk space field for zero storage (disk full)", () => { - const render = createCustomRenderer({ - context: { - app: { - isPremiumTier: false, - isGlobalAdmin: true, - currentUser: createMockUser(), - }, - }, - }); - - const summaryData = createMockHostSummary({ - gigs_disk_space_available: 0, - percent_disk_space_available: 0, - platform: "android", - }); - - render(); - - // Disk space field should be rendered - expect(screen.getByText("Disk space")).toBeInTheDocument(); - }); - - it("renders disk space normally for positive values", () => { - const render = createCustomRenderer({ - context: { - app: { - isPremiumTier: false, - isGlobalAdmin: true, - currentUser: createMockUser(), - }, - }, - }); - - const summaryData = createMockHostSummary({ - gigs_disk_space_available: 25.5, - percent_disk_space_available: 50, - platform: "darwin", - }); - - render(); - - // Disk space field should be rendered with the value - expect(screen.getByText("Disk space")).toBeInTheDocument(); - }); - - it("handles other negative values as not supported", () => { - const render = createCustomRenderer({ - context: { - app: { - isPremiumTier: false, - isGlobalAdmin: true, - currentUser: createMockUser(), - }, - }, - }); - - const summaryData = createMockHostSummary({ - gigs_disk_space_available: -10, - percent_disk_space_available: 0, - platform: "android", - }); - - render(); - - // Disk space field should not be rendered for any negative value - expect(screen.queryByText("Disk space")).not.toBeInTheDocument(); - }); - }); }); diff --git a/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tsx b/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tsx index 0f83c1d87e..e2a3185cbb 100644 --- a/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tsx +++ b/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tsx @@ -8,37 +8,22 @@ import { isLinuxDiskEncryptionStatus, } from "interfaces/mdm"; import { IOSSettings, IHostMaintenanceWindow } from "interfaces/host"; -import { IAppleDeviceUpdates } from "interfaces/config"; import { - DiskEncryptionSupportedPlatform, isAndroid, isIPadOrIPhone, isDiskEncryptionSupportedLinuxPlatform, isOsSettingsDisplayPlatform, - platformSupportsDiskEncryption, } from "interfaces/platform"; -import { ROLLING_ARCH_LINUX_VERSIONS } from "interfaces/software"; import getHostStatusTooltipText from "pages/hosts/helpers"; -import TooltipWrapperArchLinuxRolling from "components/TooltipWrapperArchLinuxRolling"; import TooltipWrapper from "components/TooltipWrapper"; -import Icon from "components/Icon/Icon"; import Card from "components/Card"; import DataSet from "components/DataSet"; import StatusIndicator from "components/StatusIndicator"; import IssuesIndicator from "pages/hosts/components/IssuesIndicator"; -import DiskSpaceIndicator from "pages/hosts/components/DiskSpaceIndicator"; -import { - humanHostMemory, - wrapFleetHelper, - removeOSPrefix, - compareVersions, -} from "utilities/helpers"; -import { - DATE_FNS_FORMAT_STRINGS, - DEFAULT_EMPTY_CELL_VALUE, -} from "utilities/constants"; + +import { DATE_FNS_FORMAT_STRINGS } from "utilities/constants"; import OSSettingsIndicator from "./OSSettingsIndicator"; import BootstrapPackageIndicator from "./BootstrapPackageIndicator/BootstrapPackageIndicator"; @@ -62,68 +47,10 @@ interface IHostSummaryProps { toggleOSSettingsModal?: () => void; toggleBootstrapPackageModal?: () => void; hostSettings?: IHostMdmProfile[]; - osVersionRequirement?: IAppleDeviceUpdates; osSettings?: IOSSettings; className?: string; } -const DISK_ENCRYPTION_MESSAGES = { - darwin: { - enabled: ( - <> - The disk is encrypted. The user must enter their -
password when they start their computer. - - ), - disabled: ( - <> - The disk might be encrypted, but FileVault is off. The -
disk can be accessed without entering a password. - - ), - }, - windows: { - enabled: ( - <> - The disk is encrypted. If recently turned on, -
encryption could take awhile. - - ), - disabled: "The disk is unencrypted.", - }, - linux: { - enabled: "The disk is encrypted.", - unknown: "The disk may be encrypted.", - }, -}; - -const getHostDiskEncryptionTooltipMessage = ( - platform: DiskEncryptionSupportedPlatform, // TODO: improve this type - diskEncryptionEnabled = false -) => { - if (platform === "chrome") { - return "Fleet does not check for disk encryption on Chromebooks, as they are encrypted by default."; - } - - if ( - platform === "rhel" || - platform === "ubuntu" || - platform === "arch" || - platform === "archarm" || - platform === "manjaro" || - platform === "manjaro-arm" - ) { - return DISK_ENCRYPTION_MESSAGES.linux[ - diskEncryptionEnabled ? "enabled" : "unknown" - ]; - } - - // mac or windows - return DISK_ENCRYPTION_MESSAGES[platform][ - diskEncryptionEnabled ? "enabled" : "disabled" - ]; -}; - const HostSummary = ({ summaryData, bootstrapPackageData, @@ -131,21 +58,14 @@ const HostSummary = ({ toggleOSSettingsModal, toggleBootstrapPackageModal, hostSettings, - osVersionRequirement, osSettings, className, }: IHostSummaryProps): JSX.Element => { const classNames = classnames(baseClass, className); - const { - status, - platform, - os_version, - disk_encryption_enabled: diskEncryptionEnabled, - } = summaryData; + const { status, platform, os_version } = summaryData; const isAndroidHost = isAndroid(platform); - const isChromeHost = platform === "chrome"; const isIosOrIpadosHost = isIPadOrIPhone(platform); const renderIssues = () => ( @@ -177,179 +97,6 @@ const HostSummary = ({ /> ); - const renderDiskSpaceSummary = () => { - // Hide disk space field if storage measurement is not supported (sentinel value -1) - if ( - typeof summaryData.gigs_disk_space_available === "number" && - summaryData.gigs_disk_space_available < 0 - ) { - return null; - } - - const title = isAndroidHost ? ( - - Disk space - - ) : ( - "Disk space" - ); - - return ( - - } - /> - ); - }; - const renderDiskEncryptionSummary = () => { - if (!platformSupportsDiskEncryption(platform, os_version)) { - return <>; - } - const tooltipMessage = getHostDiskEncryptionTooltipMessage( - platform, - diskEncryptionEnabled - ); - - let statusText; - switch (true) { - case isChromeHost: - statusText = "Always on"; - break; - case diskEncryptionEnabled === true: - statusText = "On"; - break; - case diskEncryptionEnabled === false: - statusText = "Off"; - break; - case (diskEncryptionEnabled === null || - diskEncryptionEnabled === undefined) && - platformSupportsDiskEncryption(platform, os_version): - statusText = "Unknown"; - break; - default: - // something unexpected happened on the way to this component, display whatever we got or - // "Unknown" to draw attention to the issue. - statusText = diskEncryptionEnabled || "Unknown"; - } - - return ( - - {statusText} - - } - /> - ); - }; - - const renderOperatingSystemSummary = () => { - // No tooltip if minimum version is not set, including all Windows, Linux, ChromeOS, Android operating systems - if (!osVersionRequirement?.minimum_version) { - const version = summaryData.os_version; - const versionForRender = ROLLING_ARCH_LINUX_VERSIONS.includes(version) ? ( - // wrap a tooltip around the "rolling" suffix - <> - {version.slice(0, -8)} - - - ) : ( - version - ); - return ( - - ); - } - - const osVersionWithoutPrefix = removeOSPrefix(summaryData.os_version); - const osVersionRequirementMet = - compareVersions( - osVersionWithoutPrefix, - osVersionRequirement.minimum_version - ) >= 0; - - return ( - - {!osVersionRequirementMet && ( - - )} - - Does not meet minimum version requirement. -
- Deadline to update: {osVersionRequirement.deadline} - - ) - } - > - {summaryData.os_version} -
- - } - /> - ); - }; - - const renderAgentSummary = () => { - if (isIosOrIpadosHost || isAndroidHost) { - return null; - } - - if (isChromeHost) { - return ; - } - - if (summaryData.orbit_version !== DEFAULT_EMPTY_CELL_VALUE) { - return ( - - osquery: {summaryData.osquery_version} -
- Orbit: {summaryData.orbit_version} - {summaryData.fleet_desktop_version !== - DEFAULT_EMPTY_CELL_VALUE && ( - <> -
- Fleet Desktop: {summaryData.fleet_desktop_version} - - )} - - } - > - {summaryData.orbit_version} - - } - /> - ); - } - return ; - }; - const renderMaintenanceWindow = ({ starts_at, timezone, @@ -439,12 +186,7 @@ const HostSummary = ({ } /> )} - {summaryData.issues?.total_issues_count > 0 && - !isIosOrIpadosHost && - !isAndroidHost && - renderIssues()} {isPremiumTier && renderHostTeam()} - {/* Rendering of OS Settings data */} {isOsSettingsDisplayPlatform(platform, os_version) && hostSettings && hostSettings.length > 0 && ( @@ -458,6 +200,10 @@ const HostSummary = ({ } /> )} + {summaryData.issues?.total_issues_count > 0 && + !isIosOrIpadosHost && + !isAndroidHost && + renderIssues()} {bootstrapPackageData?.status && !isIosOrIpadosHost && !isAndroidHost && ( )} - {!isChromeHost && renderDiskSpaceSummary()} - {renderDiskEncryptionSummary()} - {!isIosOrIpadosHost && ( - - )} - {!isIosOrIpadosHost && ( - - )} - {renderOperatingSystemSummary()} - {renderAgentSummary()} {isPremiumTier && // TODO - refactor normalizeEmptyValues pattern !!summaryData.maintenance_window && diff --git a/frontend/utilities/constants.tsx b/frontend/utilities/constants.tsx index 99c16bdc67..cedcad6d56 100644 --- a/frontend/utilities/constants.tsx +++ b/frontend/utilities/constants.tsx @@ -409,25 +409,14 @@ export const HOST_SUMMARY_DATA: (keyof IHost)[] = [ "id", "status", "issues", - "memory", - "cpu_type", "platform", - "os_version", - "osquery_version", - "orbit_version", - "fleet_desktop_version", "detail_updated_at", - "percent_disk_space_available", - "gigs_disk_space_available", - "gigs_total_disk_space", - "gigs_all_disk_space", "team_name", - "disk_encryption_enabled", "display_name", // Not rendered on my device page "maintenance_window", // Not rendered on my device page ]; -export const HOST_ABOUT_DATA = [ +export const HOST_VITALS_DATA = [ "seen_time", "uptime", "last_enrolled_at", @@ -441,6 +430,17 @@ export const HOST_ABOUT_DATA = [ "last_restarted_at", "platform", "uuid", + "gigs_disk_space_available", + "percent_disk_space_available", + "gigs_total_disk_space", + "gigs_all_disk_space", + "disk_encryption_enabled", + "osquery_version", + "orbit_version", + "fleet_desktop_version", + "memory", + "cpu_type", + "os_version", ]; export const HOST_OSQUERY_DATA = [