mirror of
https://github.com/fleetdm/fleet
synced 2026-05-23 00:49:03 +00:00
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:
parent
8cf232513e
commit
fbe21a951e
10 changed files with 565 additions and 636 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 &&
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
Loading…
Reference in a new issue