diff --git a/changes/28137-my-device-page-layout-changes b/changes/28137-my-device-page-layout-changes new file mode 100644 index 0000000000..0abe4469ac --- /dev/null +++ b/changes/28137-my-device-page-layout-changes @@ -0,0 +1 @@ +* Updated "My device page" layout \ No newline at end of file diff --git a/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tsx b/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tsx index a25cdbb9ed..0edc2aa01d 100644 --- a/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tsx +++ b/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tsx @@ -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} /> - @@ -461,14 +457,14 @@ const DeviceUserPage = ({ onSelect={(i) => router.push(tabPaths[i])} > - - Details - {isPremiumTier && isSoftwareEnabled && hasSelfService && ( Self-service )} + + Details + {isSoftwareEnabled && ( Software @@ -482,7 +478,28 @@ const DeviceUserPage = ({ )} + {isPremiumTier && isSoftwareEnabled && hasSelfService && ( + + + + )} + )} - {isPremiumTier && isSoftwareEnabled && hasSelfService && ( - - - - )} {isSoftwareEnabled && ( - + + + + ); + +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( + + ); + 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( + + ); + expect(screen.getByText("My device")).toBeInTheDocument(); + expect(screen.getByText(/unavailable/i)).toBeInTheDocument(); + }); + it("does not render refetch button for Android", () => { + render( + + ); + expect(screen.queryByText("Refetch")).not.toBeInTheDocument(); + }); + + it("disables refetch button when host is offline", () => { + render( + + ); + const refetchButton = screen.getByRole("button", { name: /refetch/i }); + expect(refetchButton).toBeDisabled(); + }); + + it("shows refetch spinner text when fetching", () => { + render( + + ); + expect(screen.getByText(/Fetching fresh vitals/i)).toBeInTheDocument(); + }); + + it("calls onRefetchHost when refetch button is clicked", () => { + const onRefetchHost = jest.fn(); + render( + + ); + fireEvent.click(screen.getByText("Refetch")); + expect(onRefetchHost).toHaveBeenCalled(); + }); + + it("shows tooltip when host is offline", () => { + render( + + ); + expect(screen.getByText(/an offline host/i)).toBeInTheDocument(); + }); + + it("renders device status tag and tooltip if hostMdmDeviceStatus is set", () => { + render( + + ); + expect(screen.getByText(/a locked host/i)).toBeInTheDocument(); + }); +}); diff --git a/frontend/pages/hosts/details/cards/HostHeader/HostHeader.tsx b/frontend/pages/hosts/details/cards/HostHeader/HostHeader.tsx new file mode 100644 index 0000000000..3352e4286d --- /dev/null +++ b/frontend/pages/hosts/details/cards/HostHeader/HostHeader.tsx @@ -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 + ) => 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 ( + + {tooltip} + + ); + }; + + return ( + <> + + + + {buttonText} + + {tooltip && renderTooltip()} + + > + ); +}; + +interface IHostSummaryProps { + summaryData: any; // TODO: create interfaces for this and use consistently across host pages and related helpers + showRefetchSpinner: boolean; + onRefetchHost: ( + evt: 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(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 ( + + ); + }; + + const lastFetched = 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 ( + <> + + {tag.title} + + + + {tag.generateTooltip(platform)} + + + > + ); + }; + + return ( + + + + + + {deviceUser + ? "My device" + : summaryData.display_name || DEFAULT_EMPTY_CELL_VALUE} + + + + {renderDeviceStatusTag()} + + + {"Last fetched"} {lastFetched} + + + {renderRefetch()} + + + {renderActionDropdown()} + + ); +}; + +export default HostHeader; diff --git a/frontend/pages/hosts/details/cards/HostHeader/_styles.scss b/frontend/pages/hosts/details/cards/HostHeader/_styles.scss new file mode 100644 index 0000000000..3f009fd771 --- /dev/null +++ b/frontend/pages/hosts/details/cards/HostHeader/_styles.scss @@ -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; + } + } + } + } +} diff --git a/frontend/pages/hosts/details/cards/HostSummary/helpers.tsx b/frontend/pages/hosts/details/cards/HostHeader/helpers.tsx similarity index 100% rename from frontend/pages/hosts/details/cards/HostSummary/helpers.tsx rename to frontend/pages/hosts/details/cards/HostHeader/helpers.tsx diff --git a/frontend/pages/hosts/details/cards/HostHeader/index.ts b/frontend/pages/hosts/details/cards/HostHeader/index.ts new file mode 100644 index 0000000000..2b632b5dcc --- /dev/null +++ b/frontend/pages/hosts/details/cards/HostHeader/index.ts @@ -0,0 +1 @@ +export { default } from "./HostHeader"; diff --git a/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tests.tsx b/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tests.tsx index 8b07eb8863..173dd79816 100644 --- a/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tests.tsx +++ b/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tests.tsx @@ -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( - null} - /> - ); + render(); 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(); + 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(); + 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(); + expect(screen.getByText("Disk encryption")).toBeInTheDocument(); + expect(screen.getByText("On")).toBeInTheDocument(); + }); + + it("renders 'Off' for Windows when disabled", () => { + const render = createCustomRenderer({ + context: { + app: { + isPremiumTier: true, + isGlobalAdmin: true, + currentUser: createMockUser(), + }, + }, + }); + const summaryData = createMockHostSummary({ + platform: "windows", + disk_encryption_enabled: false, + }); + render(); + expect(screen.getByText("Disk encryption")).toBeInTheDocument(); + expect(screen.getByText("Off")).toBeInTheDocument(); + }); + + it("renders Chromebook message for Chrome platform", () => { + const render = createCustomRenderer({ + context: { + app: { + isPremiumTier: true, + isGlobalAdmin: true, + currentUser: createMockUser(), + }, + }, + }); + const summaryData = createMockHostSummary({ platform: "chrome" }); + render(); + expect(screen.getByText("Always on")).toBeInTheDocument(); + }); + }); + describe("Agent data", () => { it("with all info present, render Agent header with orbit_version and tooltip with all 3 data points", async () => { const render = createCustomRenderer({ @@ -50,14 +127,7 @@ describe("Host Summary section", () => { const osqueryVersion = summaryData.osquery_version as string; const fleetdVersion = summaryData.fleet_desktop_version as string; - render( - null} - /> - ); + render(); 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( - null} - /> - ); + render(); expect(screen.getByText("Agent")).toBeInTheDocument(); @@ -127,20 +190,14 @@ describe("Host Summary section", () => { const fleetdChromeVersion = summaryData.osquery_version as string; - const { user } = render( - null} - /> - ); + const { user } = render(); expect(screen.getByText("Agent")).toBeInTheDocument(); await user.hover(screen.getByText(new RegExp(fleetdChromeVersion, "i"))); expect(screen.queryByText("Osquery")).not.toBeInTheDocument(); }); }); + describe("iOS and iPadOS data", () => { it("for iOS, renders Team, Disk space, and Operating system data only", async () => { 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( - null} - isPremiumTier - /> - ); + render(); 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( - null} - isPremiumTier - /> - ); + render(); 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( - null} - isPremiumTier - /> - ); + render(); 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( + + ); + expect(screen.getByText("Bootstrap package")).toBeInTheDocument(); + }); + }); }); diff --git a/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tsx b/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tsx index 176f425311..d3ff993924 100644 --- a/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tsx +++ b/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tsx @@ -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 - ) => 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 ( - - {tooltip} - - ); - }; - - return ( - <> - - - - {buttonText} - - {tooltip && renderTooltip()} - - > - ); -}; +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 - ) => 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(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 ( - - ); - }; - const renderIssues = () => ( { - // 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 ( - - {!isIosOrIpadosHost && !isAndroidHost && ( + return ( + + {!isIosOrIpadosHost && !isAndroidHost && ( + + } + /> + )} + {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 && ( } /> )} - {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 && ( - - } + {bootstrapPackageData?.status && !isIosOrIpadosHost && !isAndroidHost && ( + - )} - {bootstrapPackageData?.status && - !isIosOrIpadosHost && - !isAndroidHost && ( - - } - /> - )} - {!isChromeHost && renderDiskSpaceSummary()} - {renderDiskEncryptionSummary()} - {!isIosOrIpadosHost && ( - - )} - {!isIosOrIpadosHost && ( - - )} - {renderOperatingSystemSummary()} - {renderAgentSummary()} - {isPremiumTier && - // TODO - refactor normalizeEmptyValues pattern - !!summaryData.maintenance_window && - summaryData.maintenance_window !== "---" && - renderMaintenanceWindow(summaryData.maintenance_window)} - - ); - }; - - const lastFetched = 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 ( - <> - - {tag.title} - - - - {tag.generateTooltip(platform)} - - - > - ); - }; - - return ( - - - - - - - {deviceUser - ? "My device" - : summaryData.display_name || DEFAULT_EMPTY_CELL_VALUE} - - - - {renderDeviceStatusTag()} - - - {"Last fetched"} {lastFetched} - - - {renderRefetch()} - - - {renderActionDropdown()} - - {renderSummary()} - + } + /> + )} + {!isChromeHost && renderDiskSpaceSummary()} + {renderDiskEncryptionSummary()} + {!isIosOrIpadosHost && ( + + )} + {!isIosOrIpadosHost && ( + + )} + {renderOperatingSystemSummary()} + {renderAgentSummary()} + {isPremiumTier && + // TODO - refactor normalizeEmptyValues pattern + !!summaryData.maintenance_window && + summaryData.maintenance_window !== "---" && + renderMaintenanceWindow(summaryData.maintenance_window)} + ); }; diff --git a/frontend/pages/hosts/details/cards/HostSummary/_styles.scss b/frontend/pages/hosts/details/cards/HostSummary/_styles.scss index 8adb501256..4812c37ce4 100644 --- a/frontend/pages/hosts/details/cards/HostSummary/_styles.scss +++ b/frontend/pages/hosts/details/cards/HostSummary/_styles.scss @@ -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; }