Add Vitals section to Host details (#37604)

<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**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

<img width="1447" height="1052" alt="Screenshot 2025-12-22 at 3 53
11 PM"
src="https://github.com/user-attachments/assets/9ea7f2e0-163b-427b-8224-65952896af7e"
/>

#### After

<img width="1441" height="1319" alt="Screenshot 2025-12-22 at 4 14
02 PM"
src="https://github.com/user-attachments/assets/365afd5d-309c-4020-a3a8-260d8cf0d7c9"
/>


### My device page

#### Before

<img width="1444" height="572" alt="Screenshot 2025-12-22 at 3 53 19 PM"
src="https://github.com/user-attachments/assets/aee900f0-02e7-4146-8eef-060dd80befd7"
/>

#### After

<img width="1450" height="866" alt="Screenshot 2025-12-22 at 4 14 20 PM"
src="https://github.com/user-attachments/assets/8a77a33c-f564-4bc5-912e-543bf5806dae"
/>
This commit is contained in:
Nico 2025-12-22 18:15:06 -03:00 committed by GitHub
parent 8cf232513e
commit fbe21a951e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 565 additions and 636 deletions

View file

@ -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);

View file

@ -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}
/>
<AboutCard
className={defaultCardClass}
aboutData={aboutData}
<VitalsCard
className={fullWidthCardClass}
vitalsData={vitalsData}
munki={deviceMacAdminsData?.munki}
/>
<UserCard
className={defaultCardClass}
className={fullWidthCardClass}
canWriteEndUser={false}
endUsers={host.end_users ?? []}
disableFullNameTooltip

View file

@ -52,7 +52,7 @@ import permissions from "utilities/permissions";
import {
DOCUMENT_TITLE_SUFFIX,
HOST_SUMMARY_DATA,
HOST_ABOUT_DATA,
HOST_VITALS_DATA,
HOST_OSQUERY_DATA,
DEFAULT_USE_QUERY_OPTIONS,
} from "utilities/constants";
@ -96,7 +96,7 @@ import { IShowActivityDetailsData } from "components/ActivityItem/ActivityItem";
import CommandResultsModal from "pages/hosts/components/CommandDetailsModal";
import HostSummaryCard from "../cards/HostSummary";
import AboutCard from "../cards/About";
import VitalsCard from "../cards/About";
import UserCard from "../cards/User";
import ActivityCard from "../cards/Activity";
import AgentOptionsCard from "../cards/AgentOptions";
@ -141,7 +141,7 @@ const baseClass = "host-details";
const defaultCardClass = `${baseClass}__card`;
const fullWidthCardClass = `${baseClass}__card--full-width`;
const doubleHeightCardClass = `${baseClass}__card--double-height`;
const tripleHeightCardClass = `${baseClass}__card--triple-height`;
export const REFETCH_HOST_DETAILS_POLLING_INTERVAL = 2000; // 2 seconds
const BYOD_SW_INSTALL_LEARN_MORE_LINK =
@ -679,7 +679,7 @@ const HostDetailsPage = ({
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 osqueryData = normalizeEmptyValues(pick(host, HOST_OSQUERY_DATA));
@ -1306,39 +1306,22 @@ Observer plus must be checked against host's team id */
toggleBootstrapPackageModal={toggleBootstrapPackageModal}
hostSettings={host?.mdm.profiles ?? []}
osSettings={host?.mdm.os_settings}
className={fullWidthCardClass}
/>
<VitalsCard
className={fullWidthCardClass}
vitalsData={vitalsData}
munki={macadmins?.munki}
mdm={mdm}
osVersionRequirement={getOSVersionRequirementFromMDMConfig(
host.platform
)}
className={fullWidthCardClass}
/>
<AboutCard
className={defaultCardClass}
aboutData={aboutData}
munki={macadmins?.munki}
mdm={mdm}
/>
<UserCard
className={defaultCardClass}
endUsers={host.end_users ?? []}
canWriteEndUser={
isTeamMaintainerOrTeamAdmin ||
isGlobalAdmin ||
isGlobalMaintainer
}
onClickUpdateUser={(
e:
| React.MouseEvent<HTMLButtonElement>
| React.KeyboardEvent<HTMLButtonElement>
) => {
e.preventDefault();
setShowUpdateEndUserModal(true);
}}
/>
{showActivityCard && (
<ActivityCard
className={
showAgentOptionsCard
? doubleHeightCardClass
? tripleHeightCardClass
: defaultCardClass
}
activeTab={activeActivityTab}
@ -1392,6 +1375,23 @@ Observer plus must be checked against host's team id */
onCancel={onCancelActivity}
/>
)}
<UserCard
className={defaultCardClass}
endUsers={host.end_users ?? []}
canWriteEndUser={
isTeamMaintainerOrTeamAdmin ||
isGlobalAdmin ||
isGlobalMaintainer
}
onClickUpdateUser={(
e:
| React.MouseEvent<HTMLButtonElement>
| React.KeyboardEvent<HTMLButtonElement>
) => {
e.preventDefault();
setShowUpdateEndUserModal(true);
}}
/>
{showAgentOptionsCard && (
<AgentOptionsCard
className={defaultCardClass}

View file

@ -35,13 +35,13 @@
grid-column: span 2; // card will fill the whole row
}
&--double-height {
grid-row: span 2; // card will be 1 column x 2 rows
&--triple-height {
grid-row: span 3; // card will be 1 column x 3 rows
}
}
}
.about-card,
.vitals-card,
.agent-options-card {
.info {
&__item {

View file

@ -1,12 +1,14 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import { render, screen, waitFor } from "@testing-library/react";
import { createCustomRenderer } from "test/test-utils";
import createMockHost from "__mocks__/hostMock";
import { createMockHostMdmData } from "__mocks__/mdmMock";
import About from "./About";
import { DEFAULT_EMPTY_CELL_VALUE } from "utilities/constants";
import Vitals from "./About";
describe("About Card component", () => {
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(<About aboutData={mockHost} mdm={mockHost.mdm} />);
render(<Vitals vitalsData={mockHost} mdm={mockHost.mdm} />);
expect(screen.getByText("Hardware model")).toBeInTheDocument();
expect(screen.getByText("Pixel 6")).toBeInTheDocument();
@ -35,7 +37,7 @@ describe("About Card component", () => {
}),
});
render(<About aboutData={mockHost} mdm={mockHost.mdm} />);
render(<Vitals vitalsData={mockHost} mdm={mockHost.mdm} />);
expect(screen.getByText("Hardware model")).toBeInTheDocument();
expect(screen.getByText("Pixel 6")).toBeInTheDocument();
@ -57,7 +59,7 @@ describe("About Card component", () => {
}),
});
render(<About aboutData={mockHost} mdm={mockHost.mdm} />);
render(<Vitals vitalsData={mockHost} mdm={mockHost.mdm} />);
expect(screen.getByText("Enrollment ID")).toBeInTheDocument();
expect(screen.getAllByText("enrollment-id-12345")[0]).toBeInTheDocument();
@ -79,7 +81,7 @@ describe("About Card component", () => {
}),
});
render(<About aboutData={mockHost} mdm={mockHost.mdm} />);
render(<Vitals vitalsData={mockHost} mdm={mockHost.mdm} />);
expect(screen.getByText("Enrollment ID")).toBeInTheDocument();
expect(screen.getAllByText("enrollment-id-12345")[0]).toBeInTheDocument();
@ -101,7 +103,7 @@ describe("About Card component", () => {
}),
});
render(<About aboutData={mockHost} mdm={mockHost.mdm} />);
render(<Vitals vitalsData={mockHost} mdm={mockHost.mdm} />);
expect(screen.getByText("Hardware model")).toBeInTheDocument();
expect(screen.getByText("iPhone 12")).toBeInTheDocument();
@ -123,7 +125,7 @@ describe("About Card component", () => {
}),
});
render(<About aboutData={mockHost} mdm={mockHost.mdm} />);
render(<Vitals vitalsData={mockHost} mdm={mockHost.mdm} />);
expect(screen.getByText("Hardware model")).toBeInTheDocument();
expect(screen.getByText("IPad Pro")).toBeInTheDocument();
@ -147,7 +149,7 @@ describe("About Card component", () => {
}),
});
render(<About aboutData={mockHost} mdm={mockHost.mdm} />);
render(<Vitals vitalsData={mockHost} mdm={mockHost.mdm} />);
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(<About aboutData={mockHost} mdm={mockHost.mdm} />);
render(<Vitals vitalsData={mockHost} mdm={mockHost.mdm} />);
expect(screen.getByText("Hardware model")).toBeInTheDocument();
expect(screen.getByText("MacBook Pro")).toBeInTheDocument();
@ -197,7 +199,7 @@ describe("About Card component", () => {
}),
});
render(<About aboutData={mockHost} mdm={mockHost.mdm} />);
render(<Vitals vitalsData={mockHost} mdm={mockHost.mdm} />);
expect(screen.getByText("Hardware model")).toBeInTheDocument();
expect(screen.getByText("MacBook Pro")).toBeInTheDocument();
@ -223,7 +225,7 @@ describe("About Card component", () => {
}),
});
render(<About aboutData={mockHost} mdm={mockHost.mdm} />);
render(<Vitals vitalsData={mockHost} mdm={mockHost.mdm} />);
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(<Vitals vitalsData={mockHost} />);
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(<Vitals vitalsData={mockHost} />);
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(<Vitals vitalsData={mockHost} />);
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(<Vitals vitalsData={mockHost} />);
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(<Vitals vitalsData={mockHost} />);
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(<Vitals vitalsData={mockHost} />);
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(<Vitals vitalsData={mockHost} />);
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(<Vitals vitalsData={mockHost} />);
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(<Vitals vitalsData={mockHost} />);
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(<Vitals vitalsData={mockHost} />);
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(<Vitals vitalsData={mockHost} />);
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(<Vitals vitalsData={mockHost} />);
expect(screen.queryByText("Disk space")).not.toBeInTheDocument();
});
});

View file

@ -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
<br /> password when they start their computer.
</>
),
disabled: (
<>
The disk might be encrypted, but FileVault is off. The
<br /> disk can be accessed without entering a password.
</>
),
},
windows: {
enabled: (
<>
The disk is encrypted. If recently turned on,
<br /> 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 = (
<DataSet
title="Serial number"
value={<TooltipTruncatedText value={aboutData.hardware_serial} />}
value={<TooltipTruncatedText value={vitalsData.hardware_serial} />}
/>
);
@ -63,7 +146,7 @@ const About = ({ aboutData, munki, mdm, className }: IAboutProps) => {
Enrollment ID
</TooltipWrapper>
}
value={<TooltipTruncatedText value={aboutData.uuid} />}
value={<TooltipTruncatedText value={vitalsData.uuid} />}
/>
);
}
@ -79,7 +162,7 @@ const About = ({ aboutData, munki, mdm, className }: IAboutProps) => {
return (
<>
{DeviceIdDataSet}
<DataSet title="Hardware model" value={aboutData.hardware_model} />
<DataSet title="Hardware model" value={vitalsData.hardware_model} />
</>
);
}
@ -90,7 +173,7 @@ const About = ({ aboutData, munki, mdm, className }: IAboutProps) => {
return (
<>
{DeviceIdDataSet}
<DataSet title="Hardware model" value={aboutData.hardware_model} />
<DataSet title="Hardware model" value={vitalsData.hardware_model} />
</>
);
}
@ -99,11 +182,11 @@ const About = ({ aboutData, munki, mdm, className }: IAboutProps) => {
// (either Serial number or Enrollment ID).
return (
<>
<DataSet title="Hardware model" value={aboutData.hardware_model} />
<DataSet title="Hardware model" value={vitalsData.hardware_model} />
{DeviceIdDataSet}
<DataSet
title="Private IP address"
value={<TooltipTruncatedText value={aboutData.primary_ip} />}
value={<TooltipTruncatedText value={vitalsData.primary_ip} />}
/>
<DataSet
title={
@ -111,7 +194,7 @@ const About = ({ aboutData, munki, mdm, className }: IAboutProps) => {
Public IP address
</TooltipWrapper>
}
value={<TooltipTruncatedText value={aboutData.public_ip} />}
value={<TooltipTruncatedText value={vitalsData.public_ip} />}
/>
</>
);
@ -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={
<TooltipWrapper
tipContent={BATTERY_TOOLTIP[aboutData.batteries?.[0]?.health]}
tipContent={BATTERY_TOOLTIP[vitalsData.batteries?.[0]?.health]}
>
{aboutData.batteries?.[0]?.health}
{vitalsData.batteries?.[0]?.health}
</TooltipWrapper>
}
/>
@ -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 ? (
<TooltipWrapper tipContent="Includes internal and removable storage (e.g. microSD card).">
Disk space
</TooltipWrapper>
) : (
"Disk space"
);
return (
<DataSet
title={title}
value={
<DiskSpaceIndicator
gigsDiskSpaceAvailable={vitalsData.gigs_disk_space_available}
percentDiskSpaceAvailable={vitalsData.percent_disk_space_available}
gigsTotalDiskSpace={vitalsData.gigs_total_disk_space}
gigsAllDiskSpace={vitalsData.gigs_all_disk_space}
platform={platform}
tooltipPosition="bottom"
/>
}
/>
);
};
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 (
<DataSet
title="Disk encryption"
value={
<TooltipWrapper tipContent={tooltipMessage}>
{statusText}
</TooltipWrapper>
}
/>
);
};
const renderAgentSummary = () => {
if (isIosOrIpadosHost || isAndroidHost) {
return null;
}
const {
orbit_version,
osquery_version,
fleet_desktop_version,
} = vitalsData;
if (isChromeHost) {
return <DataSet title="Agent" value={osquery_version} />;
}
if (orbit_version !== DEFAULT_EMPTY_CELL_VALUE) {
return (
<DataSet
title="Agent"
value={
<TooltipWrapper
tipContent={
<>
osquery: {osquery_version}
<br />
Orbit: {orbit_version}
{fleet_desktop_version !== DEFAULT_EMPTY_CELL_VALUE && (
<>
<br />
Fleet Desktop: {fleet_desktop_version}
</>
)}
</>
}
>
{orbit_version}
</TooltipWrapper>
}
/>
);
}
return <DataSet title="Osquery" value={osquery_version} />;
};
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)}
<TooltipWrapperArchLinuxRolling />
</>
) : (
version
);
return (
<DataSet
title="Operating system"
value={versionForRender}
className={`${baseClass}__os-data-set`}
/>
);
}
const osVersionWithoutPrefix = removeOSPrefix(vitalsData.os_version);
const osVersionRequirementMet =
compareVersions(
osVersionWithoutPrefix,
osVersionRequirement.minimum_version
) >= 0;
return (
<DataSet
title="Operating system"
value={
<>
{!osVersionRequirementMet && (
<Icon name="error-outline" color="ui-fleet-black-75" />
)}
<TooltipWrapper
tipContent={
osVersionRequirementMet ? (
"Meets minimum version requirement."
) : (
<>
Does not meet minimum version requirement.
<br />
Deadline to update: {osVersionRequirement.deadline}
</>
)
}
>
{vitalsData.os_version}
</TooltipWrapper>
</>
}
/>
);
};
const classNames = classnames(baseClass, className);
return (
@ -202,13 +470,13 @@ const About = ({ aboutData, munki, mdm, className }: IAboutProps) => {
borderRadiusSize="xxlarge"
paddingSize="xlarge"
>
<CardHeader header="About" />
<CardHeader header="Vitals" />
<div className={`${baseClass}__info-grid`}>
<DataSet
title="Added to Fleet"
value={
<HumanTimeDiffWithFleetLaunchCutoff
timeString={aboutData.last_enrolled_at ?? "Unavailable"}
timeString={vitalsData.last_enrolled_at ?? "Unavailable"}
/>
}
/>
@ -217,19 +485,32 @@ const About = ({ aboutData, munki, mdm, className }: IAboutProps) => {
title="Last restarted"
value={
<HumanTimeDiffWithFleetLaunchCutoff
timeString={aboutData.last_restarted_at}
timeString={vitalsData.last_restarted_at}
/>
}
/>
)}
{renderDiskEncryptionSummary()}
{!isChromeHost && renderDiskSpaceSummary()}
{renderAgentSummary()}
{renderHardwareSerialAndIPs()}
{!isIosOrIpadosHost && (
<DataSet
title="Memory"
value={wrapFleetHelper(humanHostMemory, vitalsData.memory)}
/>
)}
{renderBattery()}
{!isIosOrIpadosHost && (
<DataSet title="Processor type" value={vitalsData.cpu_type} />
)}
{renderOperatingSystemSummary()}
{renderMunkiData()}
{renderMdmData()}
{renderGeolocation()}
{renderBattery()}
</div>
</Card>
);
};
export default About;
export default Vitals;

View file

@ -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;
}
}
}

View file

@ -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(<HostSummary summaryData={summaryData} />);
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(<HostSummary summaryData={summaryData} />);
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(<HostSummary summaryData={summaryData} />);
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(<HostSummary summaryData={summaryData} />);
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(<HostSummary summaryData={summaryData} />);
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(<HostSummary summaryData={summaryData} />);
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(<HostSummary summaryData={summaryData} isPremiumTier />);
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(<HostSummary summaryData={summaryData} isPremiumTier />);
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(<HostSummary summaryData={summaryData} />);
// 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(<HostSummary summaryData={summaryData} />);
// 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(<HostSummary summaryData={summaryData} />);
// 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(<HostSummary summaryData={summaryData} />);
// Disk space field should not be rendered for any negative value
expect(screen.queryByText("Disk space")).not.toBeInTheDocument();
});
});
});

View file

@ -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
<br /> password when they start their computer.
</>
),
disabled: (
<>
The disk might be encrypted, but FileVault is off. The
<br /> disk can be accessed without entering a password.
</>
),
},
windows: {
enabled: (
<>
The disk is encrypted. If recently turned on,
<br /> 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 ? (
<TooltipWrapper tipContent="Includes internal and removable storage (e.g. microSD card).">
Disk space
</TooltipWrapper>
) : (
"Disk space"
);
return (
<DataSet
title={title}
value={
<DiskSpaceIndicator
gigsDiskSpaceAvailable={summaryData.gigs_disk_space_available}
percentDiskSpaceAvailable={summaryData.percent_disk_space_available}
gigsTotalDiskSpace={summaryData.gigs_total_disk_space}
gigsAllDiskSpace={summaryData.gigs_all_disk_space}
platform={platform}
tooltipPosition="bottom"
/>
}
/>
);
};
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 (
<DataSet
title="Disk encryption"
value={
<TooltipWrapper tipContent={tooltipMessage}>
{statusText}
</TooltipWrapper>
}
/>
);
};
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)}
<TooltipWrapperArchLinuxRolling />
</>
) : (
version
);
return (
<DataSet
title="Operating system"
value={versionForRender}
className={`${baseClass}__os-data-set`}
/>
);
}
const osVersionWithoutPrefix = removeOSPrefix(summaryData.os_version);
const osVersionRequirementMet =
compareVersions(
osVersionWithoutPrefix,
osVersionRequirement.minimum_version
) >= 0;
return (
<DataSet
title="Operating system"
value={
<>
{!osVersionRequirementMet && (
<Icon name="error-outline" color="ui-fleet-black-75" />
)}
<TooltipWrapper
tipContent={
osVersionRequirementMet ? (
"Meets minimum version requirement."
) : (
<>
Does not meet minimum version requirement.
<br />
Deadline to update: {osVersionRequirement.deadline}
</>
)
}
>
{summaryData.os_version}
</TooltipWrapper>
</>
}
/>
);
};
const renderAgentSummary = () => {
if (isIosOrIpadosHost || isAndroidHost) {
return null;
}
if (isChromeHost) {
return <DataSet title="Agent" value={summaryData.osquery_version} />;
}
if (summaryData.orbit_version !== DEFAULT_EMPTY_CELL_VALUE) {
return (
<DataSet
title="Agent"
value={
<TooltipWrapper
tipContent={
<>
osquery: {summaryData.osquery_version}
<br />
Orbit: {summaryData.orbit_version}
{summaryData.fleet_desktop_version !==
DEFAULT_EMPTY_CELL_VALUE && (
<>
<br />
Fleet Desktop: {summaryData.fleet_desktop_version}
</>
)}
</>
}
>
{summaryData.orbit_version}
</TooltipWrapper>
}
/>
);
}
return <DataSet title="Osquery" value={summaryData.osquery_version} />;
};
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 && (
<DataSet
title="Bootstrap package"
@ -469,19 +215,6 @@ const HostSummary = ({
}
/>
)}
{!isChromeHost && renderDiskSpaceSummary()}
{renderDiskEncryptionSummary()}
{!isIosOrIpadosHost && (
<DataSet
title="Memory"
value={wrapFleetHelper(humanHostMemory, summaryData.memory)}
/>
)}
{!isIosOrIpadosHost && (
<DataSet title="Processor type" value={summaryData.cpu_type} />
)}
{renderOperatingSystemSummary()}
{renderAgentSummary()}
{isPremiumTier &&
// TODO - refactor normalizeEmptyValues pattern
!!summaryData.maintenance_window &&

View file

@ -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 = [