mirror of
https://github.com/fleetdm/fleet
synced 2026-05-24 01:18:42 +00:00
Fleet UI: Device user/Host details page layout changing including split out host header and summary card (#28598)
This commit is contained in:
parent
6b56dc80a9
commit
3b42be5571
13 changed files with 696 additions and 493 deletions
1
changes/28137-my-device-page-layout-changes
Normal file
1
changes/28137-my-device-page-layout-changes
Normal file
|
|
@ -0,0 +1 @@
|
|||
* Updated "My device page" layout
|
||||
|
|
@ -73,6 +73,7 @@ import {
|
|||
generateChromeProfilesValues,
|
||||
generateOtherEmailsValues,
|
||||
} from "../cards/User/helpers";
|
||||
import HostHeader from "../cards/HostHeader/HostHeader";
|
||||
|
||||
const baseClass = "device-user";
|
||||
|
||||
|
|
@ -443,16 +444,11 @@ const DeviceUserPage = ({
|
|||
diskIsEncrypted={host.disk_encryption_enabled}
|
||||
diskEncryptionKeyAvailable={host.mdm.encryption_key_available}
|
||||
/>
|
||||
<HostSummaryCard
|
||||
<HostHeader
|
||||
summaryData={summaryData}
|
||||
bootstrapPackageData={bootstrapPackageData}
|
||||
isPremiumTier={isPremiumTier}
|
||||
toggleOSSettingsModal={toggleOSSettingsModal}
|
||||
hostSettings={host?.mdm.profiles ?? []}
|
||||
showRefetchSpinner={showRefetchSpinner}
|
||||
onRefetchHost={onRefetchHost}
|
||||
renderActionDropdown={renderActionButtons}
|
||||
osSettings={host?.mdm.os_settings}
|
||||
deviceUser
|
||||
/>
|
||||
<TabNav className={`${baseClass}__tab-nav`}>
|
||||
|
|
@ -461,14 +457,14 @@ const DeviceUserPage = ({
|
|||
onSelect={(i) => router.push(tabPaths[i])}
|
||||
>
|
||||
<TabList>
|
||||
<Tab>
|
||||
<TabText>Details</TabText>
|
||||
</Tab>
|
||||
{isPremiumTier && isSoftwareEnabled && hasSelfService && (
|
||||
<Tab>
|
||||
<TabText>Self-service</TabText>
|
||||
</Tab>
|
||||
)}
|
||||
<Tab>
|
||||
<TabText>Details</TabText>
|
||||
</Tab>
|
||||
{isSoftwareEnabled && (
|
||||
<Tab>
|
||||
<TabText>Software</TabText>
|
||||
|
|
@ -482,7 +478,28 @@ const DeviceUserPage = ({
|
|||
</Tab>
|
||||
)}
|
||||
</TabList>
|
||||
{isPremiumTier && isSoftwareEnabled && hasSelfService && (
|
||||
<TabPanel>
|
||||
<SelfService
|
||||
contactUrl={orgContactURL}
|
||||
deviceToken={deviceAuthToken}
|
||||
isSoftwareEnabled
|
||||
pathname={location.pathname}
|
||||
queryParams={parseHostSoftwareQueryParams(location.query)}
|
||||
router={router}
|
||||
/>
|
||||
</TabPanel>
|
||||
)}
|
||||
<TabPanel className={`${baseClass}__details-panel`}>
|
||||
<HostSummaryCard
|
||||
className={fullWidthCardClass}
|
||||
summaryData={summaryData}
|
||||
bootstrapPackageData={bootstrapPackageData}
|
||||
isPremiumTier={isPremiumTier}
|
||||
toggleOSSettingsModal={toggleOSSettingsModal}
|
||||
hostSettings={host?.mdm.profiles ?? []}
|
||||
osSettings={host?.mdm.os_settings}
|
||||
/>
|
||||
<AboutCard
|
||||
className={
|
||||
showUsersCard ? defaultCardClass : fullWidthCardClass
|
||||
|
|
@ -520,18 +537,6 @@ const DeviceUserPage = ({
|
|||
/>
|
||||
)}
|
||||
</TabPanel>
|
||||
{isPremiumTier && isSoftwareEnabled && hasSelfService && (
|
||||
<TabPanel>
|
||||
<SelfService
|
||||
contactUrl={orgContactURL}
|
||||
deviceToken={deviceAuthToken}
|
||||
isSoftwareEnabled
|
||||
pathname={location.pathname}
|
||||
queryParams={parseHostSoftwareQueryParams(location.query)}
|
||||
router={router}
|
||||
/>
|
||||
</TabPanel>
|
||||
)}
|
||||
{isSoftwareEnabled && (
|
||||
<TabPanel>
|
||||
<SoftwareCard
|
||||
|
|
|
|||
|
|
@ -112,6 +112,7 @@ import {
|
|||
generateOtherEmailsValues,
|
||||
generateUsernameValues,
|
||||
} from "../cards/User/helpers";
|
||||
import HostHeader from "../cards/HostHeader";
|
||||
|
||||
const baseClass = "host-details";
|
||||
|
||||
|
|
@ -916,22 +917,15 @@ const HostDetailsPage = ({
|
|||
path={filteredHostsPath || PATHS.MANAGE_HOSTS}
|
||||
/>
|
||||
</div>
|
||||
<HostSummaryCard
|
||||
summaryData={summaryData}
|
||||
bootstrapPackageData={bootstrapPackageData}
|
||||
isPremiumTier={isPremiumTier}
|
||||
toggleOSSettingsModal={toggleOSSettingsModal}
|
||||
toggleBootstrapPackageModal={toggleBootstrapPackageModal}
|
||||
hostSettings={host?.mdm.profiles ?? []}
|
||||
showRefetchSpinner={showRefetchSpinner}
|
||||
onRefetchHost={onRefetchHost}
|
||||
renderActionDropdown={renderActionDropdown}
|
||||
osSettings={host?.mdm.os_settings}
|
||||
osVersionRequirement={getOSVersionRequirementFromMDMConfig(
|
||||
host.platform
|
||||
)}
|
||||
hostMdmDeviceStatus={hostMdmDeviceStatus}
|
||||
/>
|
||||
<div className={`${baseClass}__header-summary`}>
|
||||
<HostHeader
|
||||
summaryData={summaryData}
|
||||
showRefetchSpinner={showRefetchSpinner}
|
||||
onRefetchHost={onRefetchHost}
|
||||
renderActionDropdown={renderActionDropdown}
|
||||
hostMdmDeviceStatus={hostMdmDeviceStatus}
|
||||
/>
|
||||
</div>
|
||||
<TabNav className={`${baseClass}__tab-nav`}>
|
||||
<Tabs
|
||||
selectedIndex={getTabIndex(location.pathname)}
|
||||
|
|
@ -951,6 +945,19 @@ const HostDetailsPage = ({
|
|||
})}
|
||||
</TabList>
|
||||
<TabPanel className={`${baseClass}__details-panel`}>
|
||||
<HostSummaryCard
|
||||
summaryData={summaryData}
|
||||
bootstrapPackageData={bootstrapPackageData}
|
||||
isPremiumTier={isPremiumTier}
|
||||
toggleOSSettingsModal={toggleOSSettingsModal}
|
||||
toggleBootstrapPackageModal={toggleBootstrapPackageModal}
|
||||
hostSettings={host?.mdm.profiles ?? []}
|
||||
osSettings={host?.mdm.os_settings}
|
||||
osVersionRequirement={getOSVersionRequirementFromMDMConfig(
|
||||
host.platform
|
||||
)}
|
||||
className={fullWidthCardClass}
|
||||
/>
|
||||
<AboutCard
|
||||
className={
|
||||
showUsersCard ? defaultCardClass : fullWidthCardClass
|
||||
|
|
|
|||
|
|
@ -5,6 +5,12 @@
|
|||
gap: $pad-medium;
|
||||
}
|
||||
|
||||
&__header-summary {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $pad-medium;
|
||||
}
|
||||
|
||||
@media screen and (min-width: $break-md) {
|
||||
&__details-panel.react-tabs__tab-panel--selected {
|
||||
// Must be selected to show grid
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@
|
|||
|
||||
@include color-contrasted-sections;
|
||||
.header {
|
||||
flex: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,117 @@
|
|||
import React from "react";
|
||||
import { render, screen, fireEvent } from "@testing-library/react";
|
||||
import HostHeader from "./HostHeader";
|
||||
import { HostMdmDeviceStatusUIState } from "../../helpers";
|
||||
|
||||
const renderActionDropdown = jest.fn(() => <div data-testid="dropdown" />);
|
||||
|
||||
const defaultSummaryData = {
|
||||
platform: "darwin",
|
||||
status: "online",
|
||||
display_name: "Test Host",
|
||||
detail_updated_at: "2024-04-27T12:00:00Z",
|
||||
};
|
||||
|
||||
describe("HostHeader", () => {
|
||||
it("renders host display name and last fetched", () => {
|
||||
render(
|
||||
<HostHeader
|
||||
summaryData={defaultSummaryData}
|
||||
showRefetchSpinner={false}
|
||||
onRefetchHost={jest.fn()}
|
||||
renderActionDropdown={renderActionDropdown}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText("Test Host")).toBeInTheDocument();
|
||||
expect(screen.getByText(/Last fetched/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders 'My device' when deviceUser is true and unavailable when no last fetched date", () => {
|
||||
render(
|
||||
<HostHeader
|
||||
summaryData={{ ...defaultSummaryData, detail_updated_at: undefined }}
|
||||
showRefetchSpinner={false}
|
||||
onRefetchHost={jest.fn()}
|
||||
renderActionDropdown={renderActionDropdown}
|
||||
deviceUser
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText("My device")).toBeInTheDocument();
|
||||
expect(screen.getByText(/unavailable/i)).toBeInTheDocument();
|
||||
});
|
||||
it("does not render refetch button for Android", () => {
|
||||
render(
|
||||
<HostHeader
|
||||
summaryData={{ ...defaultSummaryData, platform: "android" }}
|
||||
showRefetchSpinner={false}
|
||||
onRefetchHost={jest.fn()}
|
||||
renderActionDropdown={renderActionDropdown}
|
||||
/>
|
||||
);
|
||||
expect(screen.queryByText("Refetch")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("disables refetch button when host is offline", () => {
|
||||
render(
|
||||
<HostHeader
|
||||
summaryData={{ ...defaultSummaryData, status: "offline" }}
|
||||
showRefetchSpinner={false}
|
||||
onRefetchHost={jest.fn()}
|
||||
renderActionDropdown={renderActionDropdown}
|
||||
/>
|
||||
);
|
||||
const refetchButton = screen.getByRole("button", { name: /refetch/i });
|
||||
expect(refetchButton).toBeDisabled();
|
||||
});
|
||||
|
||||
it("shows refetch spinner text when fetching", () => {
|
||||
render(
|
||||
<HostHeader
|
||||
summaryData={defaultSummaryData}
|
||||
showRefetchSpinner
|
||||
onRefetchHost={jest.fn()}
|
||||
renderActionDropdown={renderActionDropdown}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText(/Fetching fresh vitals/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("calls onRefetchHost when refetch button is clicked", () => {
|
||||
const onRefetchHost = jest.fn();
|
||||
render(
|
||||
<HostHeader
|
||||
summaryData={defaultSummaryData}
|
||||
showRefetchSpinner={false}
|
||||
onRefetchHost={onRefetchHost}
|
||||
renderActionDropdown={renderActionDropdown}
|
||||
/>
|
||||
);
|
||||
fireEvent.click(screen.getByText("Refetch"));
|
||||
expect(onRefetchHost).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("shows tooltip when host is offline", () => {
|
||||
render(
|
||||
<HostHeader
|
||||
summaryData={{ ...defaultSummaryData, status: "offline" }}
|
||||
showRefetchSpinner={false}
|
||||
onRefetchHost={jest.fn()}
|
||||
renderActionDropdown={renderActionDropdown}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText(/an offline host/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders device status tag and tooltip if hostMdmDeviceStatus is set", () => {
|
||||
render(
|
||||
<HostHeader
|
||||
summaryData={defaultSummaryData}
|
||||
showRefetchSpinner={false}
|
||||
onRefetchHost={jest.fn()}
|
||||
renderActionDropdown={renderActionDropdown}
|
||||
hostMdmDeviceStatus={"locked" as HostMdmDeviceStatusUIState}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText(/a locked host/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
223
frontend/pages/hosts/details/cards/HostHeader/HostHeader.tsx
Normal file
223
frontend/pages/hosts/details/cards/HostHeader/HostHeader.tsx
Normal file
|
|
@ -0,0 +1,223 @@
|
|||
import React, { useRef } from "react";
|
||||
import ReactTooltip from "react-tooltip";
|
||||
import classnames from "classnames";
|
||||
|
||||
import { isAndroid } from "interfaces/platform";
|
||||
|
||||
import Button from "components/buttons/Button";
|
||||
import Icon from "components/Icon/Icon";
|
||||
import { HumanTimeDiffWithFleetLaunchCutoff } from "components/HumanTimeDiffWithDateTip";
|
||||
import { DEFAULT_EMPTY_CELL_VALUE } from "utilities/constants";
|
||||
import { COLORS } from "styles/var/colors";
|
||||
import { useCheckTruncatedElement } from "hooks/useCheckTruncatedElement";
|
||||
import TooltipWrapper from "components/TooltipWrapper";
|
||||
|
||||
import { HostMdmDeviceStatusUIState } from "../../helpers";
|
||||
import { DEVICE_STATUS_TAGS, REFETCH_TOOLTIP_MESSAGES } from "./helpers";
|
||||
|
||||
const baseClass = "host-header";
|
||||
|
||||
interface IRefetchButtonProps {
|
||||
isDisabled: boolean;
|
||||
isFetching: boolean;
|
||||
tooltip?: React.ReactNode;
|
||||
onRefetchHost: (
|
||||
evt: React.MouseEvent<HTMLButtonElement, React.MouseEvent>
|
||||
) => void;
|
||||
}
|
||||
|
||||
const RefetchButton = ({
|
||||
isDisabled,
|
||||
isFetching,
|
||||
tooltip,
|
||||
onRefetchHost,
|
||||
}: IRefetchButtonProps) => {
|
||||
const classNames = classnames({
|
||||
tooltip: isDisabled,
|
||||
"refetch-spinner": isFetching,
|
||||
"refetch-btn": !isFetching,
|
||||
});
|
||||
|
||||
const buttonText = isFetching
|
||||
? "Fetching fresh vitals...this may take a moment"
|
||||
: "Refetch";
|
||||
|
||||
// add additonal props when we need to display a tooltip for the button
|
||||
const conditionalProps: { "data-tip"?: boolean; "data-for"?: string } = {};
|
||||
|
||||
if (tooltip) {
|
||||
conditionalProps["data-tip"] = true;
|
||||
conditionalProps["data-for"] = "refetch-tooltip";
|
||||
}
|
||||
|
||||
const renderTooltip = () => {
|
||||
return (
|
||||
<ReactTooltip
|
||||
place="top"
|
||||
effect="solid"
|
||||
id="refetch-tooltip"
|
||||
backgroundColor={COLORS["tooltip-bg"]}
|
||||
>
|
||||
<span className={`${baseClass}__tooltip-text`}>{tooltip}</span>
|
||||
</ReactTooltip>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={`${baseClass}__refetch`} {...conditionalProps}>
|
||||
<Button
|
||||
className={classNames}
|
||||
disabled={isDisabled}
|
||||
onClick={onRefetchHost}
|
||||
variant="text-icon"
|
||||
>
|
||||
<Icon name="refresh" color="core-fleet-blue" size="small" />
|
||||
{buttonText}
|
||||
</Button>
|
||||
{tooltip && renderTooltip()}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface IHostSummaryProps {
|
||||
summaryData: any; // TODO: create interfaces for this and use consistently across host pages and related helpers
|
||||
showRefetchSpinner: boolean;
|
||||
onRefetchHost: (
|
||||
evt: React.MouseEvent<HTMLButtonElement, React.MouseEvent>
|
||||
) => void;
|
||||
renderActionDropdown: () => JSX.Element | null;
|
||||
deviceUser?: boolean;
|
||||
hostMdmDeviceStatus?: HostMdmDeviceStatusUIState;
|
||||
}
|
||||
|
||||
const HostHeader = ({
|
||||
summaryData,
|
||||
showRefetchSpinner,
|
||||
onRefetchHost,
|
||||
renderActionDropdown,
|
||||
deviceUser,
|
||||
hostMdmDeviceStatus,
|
||||
}: IHostSummaryProps): JSX.Element => {
|
||||
const { platform } = summaryData;
|
||||
|
||||
const isAndroidHost = isAndroid(platform);
|
||||
const isIosOrIpadosHost = platform === "ios" || platform === "ipados";
|
||||
const hostDisplayName = useRef<HTMLHeadingElement>(null);
|
||||
const isTruncated = useCheckTruncatedElement(hostDisplayName);
|
||||
|
||||
const renderRefetch = () => {
|
||||
if (isAndroidHost) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isOnline = summaryData.status === "online";
|
||||
let isDisabled = false;
|
||||
let tooltip;
|
||||
|
||||
// we don't have a concept of "online" for iPads and iPhones, so always enable refetch
|
||||
if (!isIosOrIpadosHost) {
|
||||
// deviceStatus can be `undefined` in the case of the MyDevice Page not sending
|
||||
// this prop. When this is the case or when it is `unlocked`, we only take
|
||||
// into account the host being online or offline for correctly render the
|
||||
// refresh button. If we have a value for deviceStatus, we then need to also
|
||||
// take it account for rendering the button.
|
||||
if (
|
||||
hostMdmDeviceStatus === undefined ||
|
||||
hostMdmDeviceStatus === "unlocked"
|
||||
) {
|
||||
isDisabled = !isOnline;
|
||||
tooltip = !isOnline ? REFETCH_TOOLTIP_MESSAGES.offline : null;
|
||||
} else {
|
||||
isDisabled = true;
|
||||
tooltip = !isOnline
|
||||
? REFETCH_TOOLTIP_MESSAGES.offline
|
||||
: REFETCH_TOOLTIP_MESSAGES[hostMdmDeviceStatus];
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<RefetchButton
|
||||
isDisabled={isDisabled}
|
||||
isFetching={showRefetchSpinner}
|
||||
tooltip={tooltip}
|
||||
onRefetchHost={onRefetchHost}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const lastFetched = summaryData.detail_updated_at ? (
|
||||
<HumanTimeDiffWithFleetLaunchCutoff
|
||||
timeString={summaryData.detail_updated_at}
|
||||
/>
|
||||
) : (
|
||||
": unavailable"
|
||||
);
|
||||
|
||||
const renderDeviceStatusTag = () => {
|
||||
if (!hostMdmDeviceStatus || hostMdmDeviceStatus === "unlocked") return null;
|
||||
|
||||
const tag = DEVICE_STATUS_TAGS[hostMdmDeviceStatus];
|
||||
|
||||
const classNames = classnames(
|
||||
`${baseClass}__device-status-tag`,
|
||||
tag.tagType
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<span className={classNames} data-tip data-for="tag-tooltip">
|
||||
{tag.title}
|
||||
</span>
|
||||
<ReactTooltip
|
||||
place="top"
|
||||
effect="solid"
|
||||
id="tag-tooltip"
|
||||
backgroundColor={COLORS["tooltip-bg"]}
|
||||
>
|
||||
<span className={`${baseClass}__tooltip-text`}>
|
||||
{tag.generateTooltip(platform)}
|
||||
</span>
|
||||
</ReactTooltip>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="header title">
|
||||
<div className="title__inner">
|
||||
<div className="display-name-container">
|
||||
<TooltipWrapper
|
||||
disableTooltip={!isTruncated}
|
||||
tipContent={
|
||||
deviceUser
|
||||
? "My device"
|
||||
: summaryData.display_name || DEFAULT_EMPTY_CELL_VALUE
|
||||
}
|
||||
underline={false}
|
||||
position="top"
|
||||
showArrow
|
||||
>
|
||||
<h1 className="display-name" ref={hostDisplayName}>
|
||||
{deviceUser
|
||||
? "My device"
|
||||
: summaryData.display_name || DEFAULT_EMPTY_CELL_VALUE}
|
||||
</h1>
|
||||
</TooltipWrapper>
|
||||
|
||||
{renderDeviceStatusTag()}
|
||||
|
||||
<div className={`${baseClass}__last-fetched`}>
|
||||
{"Last fetched"} {lastFetched}
|
||||
|
||||
</div>
|
||||
{renderRefetch()}
|
||||
</div>
|
||||
</div>
|
||||
{renderActionDropdown()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HostHeader;
|
||||
71
frontend/pages/hosts/details/cards/HostHeader/_styles.scss
Normal file
71
frontend/pages/hosts/details/cards/HostHeader/_styles.scss
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
.host-header {
|
||||
&__device-status-tag {
|
||||
margin-left: $pad-small;
|
||||
background-color: $ui-warning;
|
||||
padding: $pad-xsmall;
|
||||
font-size: 10px;
|
||||
font-weight: $bold;
|
||||
border-radius: $border-radius;
|
||||
|
||||
&.warning {
|
||||
background-color: $ui-warning;
|
||||
}
|
||||
|
||||
&.error {
|
||||
color: $core-white;
|
||||
background-color: $core-vibrant-red;
|
||||
}
|
||||
}
|
||||
|
||||
&__last-fetched {
|
||||
font-size: $xx-small;
|
||||
color: $core-fleet-black;
|
||||
margin: 0;
|
||||
margin-left: $pad-medium;
|
||||
}
|
||||
|
||||
&__refetch {
|
||||
display: flex;
|
||||
margin-left: $pad-medium;
|
||||
margin-right: $pad-small;
|
||||
|
||||
.refetch-btn {
|
||||
font-size: $x-small;
|
||||
height: 38px;
|
||||
|
||||
&:hover {
|
||||
svg {
|
||||
path {
|
||||
fill: $core-vibrant-blue-over;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.refetch-spinner {
|
||||
color: $core-vibrant-blue;
|
||||
cursor: default;
|
||||
font-size: $x-small;
|
||||
height: 38px;
|
||||
opacity: 50%;
|
||||
filter: saturate(100%);
|
||||
margin-left: $pad-small;
|
||||
|
||||
.icon {
|
||||
animation: spin 2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
transform-origin: center center;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
transform-origin: center center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1
frontend/pages/hosts/details/cards/HostHeader/index.ts
Normal file
1
frontend/pages/hosts/details/cards/HostHeader/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./HostHeader";
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
import React from "react";
|
||||
import { noop } from "lodash";
|
||||
import { screen, fireEvent } from "@testing-library/react";
|
||||
import { createCustomRenderer } from "test/test-utils";
|
||||
|
||||
import createMockUser from "__mocks__/userMock";
|
||||
import { createMockHostSummary } from "__mocks__/hostMock";
|
||||
|
||||
import { BootstrapPackageStatus } from "interfaces/mdm";
|
||||
import HostSummary from "./HostSummary";
|
||||
|
||||
describe("Host Summary section", () => {
|
||||
|
|
@ -22,18 +22,95 @@ describe("Host Summary section", () => {
|
|||
});
|
||||
const summaryData = createMockHostSummary({});
|
||||
|
||||
render(
|
||||
<HostSummary
|
||||
summaryData={summaryData}
|
||||
showRefetchSpinner={false}
|
||||
onRefetchHost={noop}
|
||||
renderActionDropdown={() => null}
|
||||
/>
|
||||
);
|
||||
render(<HostSummary summaryData={summaryData} />);
|
||||
|
||||
expect(screen.queryByText("Issues")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Team data", () => {
|
||||
it("renders the team name when present", () => {
|
||||
const render = createCustomRenderer({
|
||||
context: {
|
||||
app: {
|
||||
isPremiumTier: true,
|
||||
isGlobalAdmin: true,
|
||||
currentUser: createMockUser(),
|
||||
},
|
||||
},
|
||||
});
|
||||
const summaryData = createMockHostSummary({ team_name: "Engineering" });
|
||||
render(<HostSummary summaryData={summaryData} isPremiumTier />);
|
||||
expect(screen.getByText("Team").nextElementSibling).toHaveTextContent(
|
||||
"Engineering"
|
||||
);
|
||||
});
|
||||
|
||||
it("renders 'No team' when team_name is '---'", () => {
|
||||
const render = createCustomRenderer({
|
||||
/* ...context... */
|
||||
});
|
||||
const summaryData = createMockHostSummary({ team_name: "---" });
|
||||
render(<HostSummary summaryData={summaryData} isPremiumTier />);
|
||||
expect(screen.getByText("No team")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
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({
|
||||
|
|
@ -50,14 +127,7 @@ describe("Host Summary section", () => {
|
|||
const osqueryVersion = summaryData.osquery_version as string;
|
||||
const fleetdVersion = summaryData.fleet_desktop_version as string;
|
||||
|
||||
render(
|
||||
<HostSummary
|
||||
summaryData={summaryData}
|
||||
showRefetchSpinner={false}
|
||||
onRefetchHost={noop}
|
||||
renderActionDropdown={() => null}
|
||||
/>
|
||||
);
|
||||
render(<HostSummary summaryData={summaryData} />);
|
||||
|
||||
expect(screen.getByText("Agent")).toBeInTheDocument();
|
||||
|
||||
|
|
@ -89,14 +159,7 @@ describe("Host Summary section", () => {
|
|||
const orbitVersion = summaryData.orbit_version as string;
|
||||
const osqueryVersion = summaryData.osquery_version as string;
|
||||
|
||||
render(
|
||||
<HostSummary
|
||||
summaryData={summaryData}
|
||||
showRefetchSpinner={false}
|
||||
onRefetchHost={noop}
|
||||
renderActionDropdown={() => null}
|
||||
/>
|
||||
);
|
||||
render(<HostSummary summaryData={summaryData} />);
|
||||
|
||||
expect(screen.getByText("Agent")).toBeInTheDocument();
|
||||
|
||||
|
|
@ -127,20 +190,14 @@ describe("Host Summary section", () => {
|
|||
|
||||
const fleetdChromeVersion = summaryData.osquery_version as string;
|
||||
|
||||
const { user } = render(
|
||||
<HostSummary
|
||||
summaryData={summaryData}
|
||||
showRefetchSpinner={false}
|
||||
onRefetchHost={noop}
|
||||
renderActionDropdown={() => null}
|
||||
/>
|
||||
);
|
||||
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 () => {
|
||||
const render = createCustomRenderer({
|
||||
|
|
@ -164,15 +221,7 @@ describe("Host Summary section", () => {
|
|||
const diskSpaceAvailable = summaryData.gigs_disk_space_available as string;
|
||||
const osVersion = summaryData.os_version as string;
|
||||
|
||||
render(
|
||||
<HostSummary
|
||||
summaryData={summaryData}
|
||||
showRefetchSpinner={false}
|
||||
onRefetchHost={noop}
|
||||
renderActionDropdown={() => null}
|
||||
isPremiumTier
|
||||
/>
|
||||
);
|
||||
render(<HostSummary summaryData={summaryData} isPremiumTier />);
|
||||
|
||||
expect(screen.getByText("Team").nextElementSibling).toHaveTextContent(
|
||||
teamName
|
||||
|
|
@ -183,7 +232,6 @@ describe("Host Summary section", () => {
|
|||
expect(
|
||||
screen.getByText("Operating system").nextElementSibling
|
||||
).toHaveTextContent(osVersion);
|
||||
expect(screen.queryByText("Refetch")).toBeInTheDocument();
|
||||
|
||||
expect(screen.queryByText("Status")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Memory")).not.toBeInTheDocument();
|
||||
|
|
@ -213,15 +261,7 @@ describe("Host Summary section", () => {
|
|||
const diskSpaceAvailable = summaryData.gigs_disk_space_available as string;
|
||||
const osVersion = summaryData.os_version as string;
|
||||
|
||||
render(
|
||||
<HostSummary
|
||||
summaryData={summaryData}
|
||||
showRefetchSpinner={false}
|
||||
onRefetchHost={noop}
|
||||
renderActionDropdown={() => null}
|
||||
isPremiumTier
|
||||
/>
|
||||
);
|
||||
render(<HostSummary summaryData={summaryData} isPremiumTier />);
|
||||
|
||||
expect(screen.getByText("Team").nextElementSibling).toHaveTextContent(
|
||||
teamName
|
||||
|
|
@ -232,7 +272,6 @@ describe("Host Summary section", () => {
|
|||
expect(
|
||||
screen.getByText("Operating system").nextElementSibling
|
||||
).toHaveTextContent(osVersion);
|
||||
expect(screen.queryByText("Refetch")).toBeInTheDocument();
|
||||
|
||||
expect(screen.queryByText("Status")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Memory")).not.toBeInTheDocument();
|
||||
|
|
@ -241,6 +280,7 @@ describe("Host Summary section", () => {
|
|||
expect(screen.queryByText("Osquery")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Maintenance window data", () => {
|
||||
it("renders maintenance window data with timezone", async () => {
|
||||
const render = createCustomRenderer({
|
||||
|
|
@ -261,18 +301,37 @@ describe("Host Summary section", () => {
|
|||
});
|
||||
const prettyStartTime = /Jun 24 at 8:48 PM/;
|
||||
|
||||
render(
|
||||
<HostSummary
|
||||
summaryData={summaryData}
|
||||
showRefetchSpinner={false}
|
||||
onRefetchHost={noop}
|
||||
renderActionDropdown={() => null}
|
||||
isPremiumTier
|
||||
/>
|
||||
);
|
||||
render(<HostSummary summaryData={summaryData} isPremiumTier />);
|
||||
|
||||
expect(screen.getByText("Scheduled maintenance")).toBeInTheDocument();
|
||||
expect(screen.getByText(prettyStartTime)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Bootstrap package data", () => {
|
||||
it("renders Bootstrap package indicator when status is present", () => {
|
||||
const toggleBootstrapPackageModal = jest.fn();
|
||||
const render = createCustomRenderer({
|
||||
context: {
|
||||
app: {
|
||||
isPremiumTier: true,
|
||||
isGlobalAdmin: true,
|
||||
currentUser: createMockUser(),
|
||||
},
|
||||
},
|
||||
});
|
||||
const summaryData = createMockHostSummary({ platform: "darwin" });
|
||||
const bootstrapPackageData = {
|
||||
status: "installed" as BootstrapPackageStatus,
|
||||
};
|
||||
render(
|
||||
<HostSummary
|
||||
summaryData={summaryData}
|
||||
bootstrapPackageData={bootstrapPackageData}
|
||||
toggleBootstrapPackageModal={toggleBootstrapPackageModal}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText("Bootstrap package")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -21,14 +21,12 @@ import {
|
|||
import getHostStatusTooltipText from "pages/hosts/helpers";
|
||||
|
||||
import TooltipWrapper from "components/TooltipWrapper";
|
||||
import Button from "components/buttons/Button";
|
||||
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 { HumanTimeDiffWithFleetLaunchCutoff } from "components/HumanTimeDiffWithDateTip";
|
||||
import {
|
||||
humanHostMemory,
|
||||
wrapFleetHelper,
|
||||
|
|
@ -40,83 +38,16 @@ import {
|
|||
DEFAULT_EMPTY_CELL_VALUE,
|
||||
} from "utilities/constants";
|
||||
import { useCheckTruncatedElement } from "hooks/useCheckTruncatedElement";
|
||||
import { COLORS } from "styles/var/colors";
|
||||
|
||||
import OSSettingsIndicator from "./OSSettingsIndicator";
|
||||
import BootstrapPackageIndicator from "./BootstrapPackageIndicator/BootstrapPackageIndicator";
|
||||
|
||||
import {
|
||||
HostMdmDeviceStatusUIState,
|
||||
generateLinuxDiskEncryptionSetting,
|
||||
generateWinDiskEncryptionSetting,
|
||||
} from "../../helpers";
|
||||
import { DEVICE_STATUS_TAGS, REFETCH_TOOLTIP_MESSAGES } from "./helpers";
|
||||
|
||||
const baseClass = "host-summary";
|
||||
|
||||
interface IRefetchButtonProps {
|
||||
isDisabled: boolean;
|
||||
isFetching: boolean;
|
||||
tooltip?: React.ReactNode;
|
||||
onRefetchHost: (
|
||||
evt: React.MouseEvent<HTMLButtonElement, React.MouseEvent>
|
||||
) => void;
|
||||
}
|
||||
|
||||
const RefetchButton = ({
|
||||
isDisabled,
|
||||
isFetching,
|
||||
tooltip,
|
||||
onRefetchHost,
|
||||
}: IRefetchButtonProps) => {
|
||||
const classNames = classnames({
|
||||
tooltip: isDisabled,
|
||||
"refetch-spinner": isFetching,
|
||||
"refetch-btn": !isFetching,
|
||||
});
|
||||
|
||||
const buttonText = isFetching
|
||||
? "Fetching fresh vitals...this may take a moment"
|
||||
: "Refetch";
|
||||
|
||||
// add additonal props when we need to display a tooltip for the button
|
||||
const conditionalProps: { "data-tip"?: boolean; "data-for"?: string } = {};
|
||||
|
||||
if (tooltip) {
|
||||
conditionalProps["data-tip"] = true;
|
||||
conditionalProps["data-for"] = "refetch-tooltip";
|
||||
}
|
||||
|
||||
const renderTooltip = () => {
|
||||
return (
|
||||
<ReactTooltip
|
||||
place="top"
|
||||
effect="solid"
|
||||
id="refetch-tooltip"
|
||||
backgroundColor={COLORS["tooltip-bg"]}
|
||||
>
|
||||
<span className={`${baseClass}__tooltip-text`}>{tooltip}</span>
|
||||
</ReactTooltip>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={`${baseClass}__refetch`} {...conditionalProps}>
|
||||
<Button
|
||||
className={classNames}
|
||||
disabled={isDisabled}
|
||||
onClick={onRefetchHost}
|
||||
variant="text-icon"
|
||||
>
|
||||
<Icon name="refresh" color="core-fleet-blue" size="small" />
|
||||
{buttonText}
|
||||
</Button>
|
||||
{tooltip && renderTooltip()}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
const baseClass = "host-summary-card";
|
||||
|
||||
interface IBootstrapPackageData {
|
||||
status?: BootstrapPackageStatus | "";
|
||||
|
|
@ -130,15 +61,9 @@ interface IHostSummaryProps {
|
|||
toggleOSSettingsModal?: () => void;
|
||||
toggleBootstrapPackageModal?: () => void;
|
||||
hostSettings?: IHostMdmProfile[];
|
||||
showRefetchSpinner: boolean;
|
||||
onRefetchHost: (
|
||||
evt: React.MouseEvent<HTMLButtonElement, React.MouseEvent>
|
||||
) => void;
|
||||
renderActionDropdown: () => JSX.Element | null;
|
||||
deviceUser?: boolean;
|
||||
osVersionRequirement?: IAppleDeviceUpdates;
|
||||
osSettings?: IOSSettings;
|
||||
hostMdmDeviceStatus?: HostMdmDeviceStatusUIState;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const DISK_ENCRYPTION_MESSAGES = {
|
||||
|
|
@ -198,16 +123,13 @@ const HostSummary = ({
|
|||
toggleOSSettingsModal,
|
||||
toggleBootstrapPackageModal,
|
||||
hostSettings,
|
||||
showRefetchSpinner,
|
||||
onRefetchHost,
|
||||
renderActionDropdown,
|
||||
deviceUser,
|
||||
osVersionRequirement,
|
||||
osSettings,
|
||||
hostMdmDeviceStatus,
|
||||
className,
|
||||
}: IHostSummaryProps): JSX.Element => {
|
||||
const hostDisplayName = useRef<HTMLHeadingElement>(null);
|
||||
const isTruncated = useCheckTruncatedElement(hostDisplayName);
|
||||
const classNames = classnames(baseClass, className);
|
||||
|
||||
const {
|
||||
status,
|
||||
|
|
@ -220,46 +142,6 @@ const HostSummary = ({
|
|||
const isChromeHost = platform === "chrome";
|
||||
const isIosOrIpadosHost = platform === "ios" || platform === "ipados";
|
||||
|
||||
const renderRefetch = () => {
|
||||
if (isAndroidHost) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isOnline = summaryData.status === "online";
|
||||
let isDisabled = false;
|
||||
let tooltip;
|
||||
|
||||
// we don't have a concept of "online" for iPads and iPhones, so always enable refetch
|
||||
if (!isIosOrIpadosHost) {
|
||||
// deviceStatus can be `undefined` in the case of the MyDevice Page not sending
|
||||
// this prop. When this is the case or when it is `unlocked`, we only take
|
||||
// into account the host being online or offline for correctly render the
|
||||
// refresh button. If we have a value for deviceStatus, we then need to also
|
||||
// take it account for rendering the button.
|
||||
if (
|
||||
hostMdmDeviceStatus === undefined ||
|
||||
hostMdmDeviceStatus === "unlocked"
|
||||
) {
|
||||
isDisabled = !isOnline;
|
||||
tooltip = !isOnline ? REFETCH_TOOLTIP_MESSAGES.offline : null;
|
||||
} else {
|
||||
isDisabled = true;
|
||||
tooltip = !isOnline
|
||||
? REFETCH_TOOLTIP_MESSAGES.offline
|
||||
: REFETCH_TOOLTIP_MESSAGES[hostMdmDeviceStatus];
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<RefetchButton
|
||||
isDisabled={isDisabled}
|
||||
isFetching={showRefetchSpinner}
|
||||
tooltip={tooltip}
|
||||
onRefetchHost={onRefetchHost}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const renderIssues = () => (
|
||||
<DataSet
|
||||
title="Issues"
|
||||
|
|
@ -470,187 +352,108 @@ const HostSummary = ({
|
|||
);
|
||||
};
|
||||
|
||||
const renderSummary = () => {
|
||||
// for windows and linux hosts we have to manually add a profile for disk encryption
|
||||
// as this is not currently included in the `profiles` value from the API
|
||||
// response for windows and linux hosts.
|
||||
if (
|
||||
platform === "windows" &&
|
||||
osSettings?.disk_encryption?.status &&
|
||||
isWindowsDiskEncryptionStatus(osSettings.disk_encryption.status)
|
||||
) {
|
||||
const winDiskEncryptionSetting: IHostMdmProfile = generateWinDiskEncryptionSetting(
|
||||
osSettings.disk_encryption.status,
|
||||
osSettings.disk_encryption.detail
|
||||
);
|
||||
hostSettings = hostSettings
|
||||
? [...hostSettings, winDiskEncryptionSetting]
|
||||
: [winDiskEncryptionSetting];
|
||||
}
|
||||
// for windows and linux hosts we have to manually add a profile for disk encryption
|
||||
// as this is not currently included in the `profiles` value from the API
|
||||
// response for windows and linux hosts.
|
||||
if (
|
||||
platform === "windows" &&
|
||||
osSettings?.disk_encryption?.status &&
|
||||
isWindowsDiskEncryptionStatus(osSettings.disk_encryption.status)
|
||||
) {
|
||||
const winDiskEncryptionSetting: IHostMdmProfile = generateWinDiskEncryptionSetting(
|
||||
osSettings.disk_encryption.status,
|
||||
osSettings.disk_encryption.detail
|
||||
);
|
||||
hostSettings = hostSettings
|
||||
? [...hostSettings, winDiskEncryptionSetting]
|
||||
: [winDiskEncryptionSetting];
|
||||
}
|
||||
|
||||
if (
|
||||
isDiskEncryptionSupportedLinuxPlatform(platform, os_version) &&
|
||||
osSettings?.disk_encryption?.status &&
|
||||
isLinuxDiskEncryptionStatus(osSettings.disk_encryption.status)
|
||||
) {
|
||||
const linuxDiskEncryptionSetting: IHostMdmProfile = generateLinuxDiskEncryptionSetting(
|
||||
osSettings.disk_encryption.status,
|
||||
osSettings.disk_encryption.detail
|
||||
);
|
||||
hostSettings = hostSettings
|
||||
? [...hostSettings, linuxDiskEncryptionSetting]
|
||||
: [linuxDiskEncryptionSetting];
|
||||
}
|
||||
if (
|
||||
isDiskEncryptionSupportedLinuxPlatform(platform, os_version) &&
|
||||
osSettings?.disk_encryption?.status &&
|
||||
isLinuxDiskEncryptionStatus(osSettings.disk_encryption.status)
|
||||
) {
|
||||
const linuxDiskEncryptionSetting: IHostMdmProfile = generateLinuxDiskEncryptionSetting(
|
||||
osSettings.disk_encryption.status,
|
||||
osSettings.disk_encryption.detail
|
||||
);
|
||||
hostSettings = hostSettings
|
||||
? [...hostSettings, linuxDiskEncryptionSetting]
|
||||
: [linuxDiskEncryptionSetting];
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
borderRadiusSize="xxlarge"
|
||||
paddingSize="xlarge"
|
||||
includeShadow
|
||||
className={`${baseClass}-card`}
|
||||
>
|
||||
{!isIosOrIpadosHost && !isAndroidHost && (
|
||||
return (
|
||||
<Card
|
||||
borderRadiusSize="xxlarge"
|
||||
paddingSize="xlarge"
|
||||
includeShadow
|
||||
className={classNames}
|
||||
>
|
||||
{!isIosOrIpadosHost && !isAndroidHost && (
|
||||
<DataSet
|
||||
title="Status"
|
||||
value={
|
||||
<StatusIndicator
|
||||
value={status || ""} // temporary work around of integration test bug
|
||||
tooltip={{
|
||||
tooltipText: getHostStatusTooltipText(status),
|
||||
position: "bottom",
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{summaryData.issues?.total_issues_count > 0 &&
|
||||
!isIosOrIpadosHost &&
|
||||
!isAndroidHost &&
|
||||
renderIssues()}
|
||||
{isPremiumTier && renderHostTeam()}
|
||||
{/* Rendering of OS Settings data */}
|
||||
{isOsSettingsDisplayPlatform(platform, os_version) &&
|
||||
isPremiumTier &&
|
||||
hostSettings &&
|
||||
hostSettings.length > 0 && (
|
||||
<DataSet
|
||||
title="Status"
|
||||
title="OS settings"
|
||||
value={
|
||||
<StatusIndicator
|
||||
value={status || ""} // temporary work around of integration test bug
|
||||
tooltip={{
|
||||
tooltipText: getHostStatusTooltipText(status),
|
||||
position: "bottom",
|
||||
}}
|
||||
<OSSettingsIndicator
|
||||
profiles={hostSettings}
|
||||
onClick={toggleOSSettingsModal}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{summaryData.issues?.total_issues_count > 0 &&
|
||||
!isIosOrIpadosHost &&
|
||||
!isAndroidHost &&
|
||||
renderIssues()}
|
||||
{isPremiumTier && renderHostTeam()}
|
||||
{/* Rendering of OS Settings data */}
|
||||
{isOsSettingsDisplayPlatform(platform, os_version) &&
|
||||
isPremiumTier &&
|
||||
hostSettings &&
|
||||
hostSettings.length > 0 && (
|
||||
<DataSet
|
||||
title="OS settings"
|
||||
value={
|
||||
<OSSettingsIndicator
|
||||
profiles={hostSettings}
|
||||
onClick={toggleOSSettingsModal}
|
||||
/>
|
||||
}
|
||||
{bootstrapPackageData?.status && !isIosOrIpadosHost && !isAndroidHost && (
|
||||
<DataSet
|
||||
title="Bootstrap package"
|
||||
value={
|
||||
<BootstrapPackageIndicator
|
||||
status={bootstrapPackageData.status}
|
||||
onClick={toggleBootstrapPackageModal}
|
||||
/>
|
||||
)}
|
||||
{bootstrapPackageData?.status &&
|
||||
!isIosOrIpadosHost &&
|
||||
!isAndroidHost && (
|
||||
<DataSet
|
||||
title="Bootstrap package"
|
||||
value={
|
||||
<BootstrapPackageIndicator
|
||||
status={bootstrapPackageData.status}
|
||||
onClick={toggleBootstrapPackageModal}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{!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 &&
|
||||
summaryData.maintenance_window !== "---" &&
|
||||
renderMaintenanceWindow(summaryData.maintenance_window)}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
const lastFetched = summaryData.detail_updated_at ? (
|
||||
<HumanTimeDiffWithFleetLaunchCutoff
|
||||
timeString={summaryData.detail_updated_at}
|
||||
/>
|
||||
) : (
|
||||
": unavailable"
|
||||
);
|
||||
|
||||
const renderDeviceStatusTag = () => {
|
||||
if (!hostMdmDeviceStatus || hostMdmDeviceStatus === "unlocked") return null;
|
||||
|
||||
const tag = DEVICE_STATUS_TAGS[hostMdmDeviceStatus];
|
||||
|
||||
const classNames = classnames(
|
||||
`${baseClass}__device-status-tag`,
|
||||
tag.tagType
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<span className={classNames} data-tip data-for="tag-tooltip">
|
||||
{tag.title}
|
||||
</span>
|
||||
<ReactTooltip
|
||||
place="top"
|
||||
effect="solid"
|
||||
id="tag-tooltip"
|
||||
backgroundColor={COLORS["tooltip-bg"]}
|
||||
>
|
||||
<span className={`${baseClass}__tooltip-text`}>
|
||||
{tag.generateTooltip(platform)}
|
||||
</span>
|
||||
</ReactTooltip>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<div className="header title">
|
||||
<div className="title__inner">
|
||||
<div className="display-name-container">
|
||||
<TooltipWrapper
|
||||
disableTooltip={!isTruncated}
|
||||
tipContent={
|
||||
deviceUser
|
||||
? "My device"
|
||||
: summaryData.display_name || DEFAULT_EMPTY_CELL_VALUE
|
||||
}
|
||||
underline={false}
|
||||
position="top"
|
||||
showArrow
|
||||
>
|
||||
<h1 className="display-name" ref={hostDisplayName}>
|
||||
{deviceUser
|
||||
? "My device"
|
||||
: summaryData.display_name || DEFAULT_EMPTY_CELL_VALUE}
|
||||
</h1>
|
||||
</TooltipWrapper>
|
||||
|
||||
{renderDeviceStatusTag()}
|
||||
|
||||
<div className={`${baseClass}__last-fetched`}>
|
||||
{"Last fetched"} {lastFetched}
|
||||
|
||||
</div>
|
||||
{renderRefetch()}
|
||||
</div>
|
||||
</div>
|
||||
{renderActionDropdown()}
|
||||
</div>
|
||||
{renderSummary()}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{!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 &&
|
||||
summaryData.maintenance_window !== "---" &&
|
||||
renderMaintenanceWindow(summaryData.maintenance_window)}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,108 +1,19 @@
|
|||
.host-summary {
|
||||
.host-summary-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $pad-medium;
|
||||
gap: $pad-medium $pad-xxlarge;
|
||||
padding: $pad-xxlarge;
|
||||
flex-wrap: wrap;
|
||||
|
||||
&__device-status-tag {
|
||||
margin-left: $pad-small;
|
||||
background-color: $ui-warning;
|
||||
padding: $pad-xsmall;
|
||||
font-size: 10px;
|
||||
font-weight: $bold;
|
||||
border-radius: $border-radius;
|
||||
|
||||
&.warning {
|
||||
background-color: $ui-warning;
|
||||
}
|
||||
|
||||
&.error {
|
||||
color: $core-white;
|
||||
background-color: $core-vibrant-red;
|
||||
}
|
||||
}
|
||||
|
||||
&__last-fetched {
|
||||
font-size: $xx-small;
|
||||
color: $core-fleet-black;
|
||||
margin: 0;
|
||||
margin-left: $pad-medium;
|
||||
}
|
||||
|
||||
&__refetch {
|
||||
// Properly vertically aligns host issue icon
|
||||
.host-issue {
|
||||
display: flex;
|
||||
margin-left: $pad-medium;
|
||||
margin-right: $pad-small;
|
||||
|
||||
.refetch-btn {
|
||||
font-size: $x-small;
|
||||
height: 38px;
|
||||
|
||||
&:hover {
|
||||
svg {
|
||||
path {
|
||||
fill: $core-vibrant-blue-over;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.refetch-spinner {
|
||||
color: $core-vibrant-blue;
|
||||
cursor: default;
|
||||
font-size: $x-small;
|
||||
height: 38px;
|
||||
opacity: 50%;
|
||||
filter: saturate(100%);
|
||||
margin-left: $pad-small;
|
||||
|
||||
.icon {
|
||||
animation: spin 2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
transform-origin: center center;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
transform-origin: center center;
|
||||
}
|
||||
}
|
||||
}
|
||||
gap: $pad-xsmall;
|
||||
}
|
||||
|
||||
.card {
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $pad-xxsmall;
|
||||
max-height: 20px;
|
||||
.premium-icon-tip {
|
||||
position: relative;
|
||||
top: 3px;
|
||||
}
|
||||
}
|
||||
.component__tooltip-wrapper__tip-text {
|
||||
max-width: 326px;
|
||||
}
|
||||
|
||||
display: flex;
|
||||
gap: $pad-medium $pad-xxlarge;
|
||||
padding: $pad-xxlarge;
|
||||
flex-wrap: wrap;
|
||||
|
||||
// Properly vertically aligns host issue icon
|
||||
.host-issue {
|
||||
display: flex;
|
||||
gap: $pad-xsmall;
|
||||
}
|
||||
|
||||
.no-team {
|
||||
color: $ui-fleet-black-50;
|
||||
}
|
||||
.no-team {
|
||||
color: $ui-fleet-black-50;
|
||||
}
|
||||
|
||||
.data-set dd {
|
||||
overflow: initial;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue