diff --git a/changes/9413-macOS-settings-indicator b/changes/9413-macOS-settings-indicator new file mode 100644 index 00000000000..38994b7fb21 --- /dev/null +++ b/changes/9413-macOS-settings-indicator @@ -0,0 +1 @@ +- Create a macOS settings indicator and modal on the host details and device user pages. diff --git a/frontend/components/DiskSpaceGraph/DiskSpaceGraph.tsx b/frontend/components/DiskSpaceGraph/DiskSpaceGraph.tsx index 8d55e54757c..19c81961e1a 100644 --- a/frontend/components/DiskSpaceGraph/DiskSpaceGraph.tsx +++ b/frontend/components/DiskSpaceGraph/DiskSpaceGraph.tsx @@ -8,6 +8,7 @@ interface IDiskSpaceGraphProps { percentDiskSpaceAvailable: number; id: string; platform: string; + tooltipPosition?: "top" | "bottom"; } const DiskSpaceGraph = ({ @@ -16,6 +17,7 @@ const DiskSpaceGraph = ({ percentDiskSpaceAvailable, id, platform, + tooltipPosition = "top", }: IDiskSpaceGraphProps): JSX.Element => { const getDiskSpaceIndicatorColor = (): string => { // return space-dependent graph colors for mac and windows hosts, green for linux @@ -65,7 +67,7 @@ const DiskSpaceGraph = ({ {diskSpaceTooltipText && (
{value} diff --git a/frontend/components/TableContainer/DataTable/TruncatedTextCell/_styles.scss b/frontend/components/TableContainer/DataTable/TruncatedTextCell/_styles.scss index 6386b9cf48d..9d9a8655e60 100644 --- a/frontend/components/TableContainer/DataTable/TruncatedTextCell/_styles.scss +++ b/frontend/components/TableContainer/DataTable/TruncatedTextCell/_styles.scss @@ -1,4 +1,7 @@ .truncated-cell { + .text-muted { + color: $ui-fleet-black-50; + } .data-table__truncated-text { &--cell { display: inline-block; diff --git a/frontend/components/icons/Pending.tsx b/frontend/components/icons/Pending.tsx new file mode 100644 index 00000000000..bc4e1720c72 --- /dev/null +++ b/frontend/components/icons/Pending.tsx @@ -0,0 +1,31 @@ +import React from "react"; +import { COLORS, Colors } from "styles/var/colors"; +import { ICON_SIZES, IconSizes } from "styles/var/icon_sizes"; + +interface IPendingProps { + size?: IconSizes; + color?: Colors; +} + +const Pending = ({ + size = "medium", + color = "ui-fleet-black-50", +}: IPendingProps) => { + return ( + + + + ); +}; + +export default Pending; diff --git a/frontend/components/icons/index.ts b/frontend/components/icons/index.ts index 4bd1753978c..7329147edc5 100644 --- a/frontend/components/icons/index.ts +++ b/frontend/components/icons/index.ts @@ -16,6 +16,7 @@ import EmptyTeams from "./EmptyTeams"; import ExternalLink from "./ExternalLink"; import Issue from "./Issue"; import Plus from "./Plus"; +import Pending from "./Pending"; import LowDiskSpaceHosts from "./LowDiskSpaceHosts"; import MissingHosts from "./MissingHosts"; @@ -67,6 +68,7 @@ export const ICON_MAP = { clipboard: Clipboard, eye: Eye, pencil: Pencil, + pending: Pending, trash: TrashCan, success: Success, error: Error, diff --git a/frontend/interfaces/host.ts b/frontend/interfaces/host.ts index a83882e4698..8c9639c9822 100644 --- a/frontend/interfaces/host.ts +++ b/frontend/interfaces/host.ts @@ -7,7 +7,7 @@ import softwareInterface, { ISoftware } from "./software"; import hostQueryResult from "./campaign"; import queryStatsInterface, { IQueryStats } from "./query_stats"; import { ILicense, IDeviceGlobalConfig } from "./config"; -import { MdmEnrollmentStatus } from "./mdm"; +import { IMacSettings, MdmEnrollmentStatus } from "./mdm"; export default PropTypes.shape({ created_at: PropTypes.string, @@ -90,6 +90,7 @@ export interface IHostMdmData { encryption_key_available: boolean; enrollment_status: MdmEnrollmentStatus | null; server_url: string; + profiles?: IMacSettings; id?: number; name?: string; } diff --git a/frontend/interfaces/mdm.ts b/frontend/interfaces/mdm.ts index df540313938..1d5e01bf58c 100644 --- a/frontend/interfaces/mdm.ts +++ b/frontend/interfaces/mdm.ts @@ -67,3 +67,16 @@ export interface IMdmProfile { export interface IMdmProfilesResponse { profiles: IMdmProfile[] | null; } + +export type MacMdmProfileStatus = "success" | "pending" | "failed"; +export type MacMdmProfileOperationType = "remove" | "install"; + +export type IHostMacMdmProfile = { + profile_id: number; + name: string; + operation_type: MacMdmProfileOperationType; + status: MacMdmProfileStatus; + detail: string; +}; +export type IMacSettings = IHostMacMdmProfile[]; +export type MacSettingsStatus = "Failing" | "Latest" | "Pending"; diff --git a/frontend/pages/admin/IntegrationsPage/IntegrationNavItems.tsx b/frontend/pages/admin/IntegrationsPage/IntegrationNavItems.tsx index ca2a69cc5bb..71f58793424 100644 --- a/frontend/pages/admin/IntegrationsPage/IntegrationNavItems.tsx +++ b/frontend/pages/admin/IntegrationsPage/IntegrationNavItems.tsx @@ -2,7 +2,7 @@ import PATHS from "router/paths"; import { ISideNavItem } from "../components/SideNav/SideNav"; import Integrations from "./cards/Integrations"; -import Mdm from "./cards/Mdm/Mdm"; +import Mdm from "./cards/MdmSettings/MdmSettings"; const INTEGRATION_SETTINGS_NAV_ITEMS: ISideNavItem[] = [ // TODO: types diff --git a/frontend/pages/admin/IntegrationsPage/cards/Mdm/Mdm.tsx b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/MdmSettings.tsx similarity index 99% rename from frontend/pages/admin/IntegrationsPage/cards/Mdm/Mdm.tsx rename to frontend/pages/admin/IntegrationsPage/cards/MdmSettings/MdmSettings.tsx index a9ddfdbf297..a73d8187f99 100644 --- a/frontend/pages/admin/IntegrationsPage/cards/Mdm/Mdm.tsx +++ b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/MdmSettings.tsx @@ -25,7 +25,7 @@ interface IABMKeys { decodedPrivate: string; } -const baseClass = "mdm-integrations"; +const baseClass = "mdm-settings"; const Mdm = (): JSX.Element => { const { isPremiumTier, config } = useContext(AppContext); diff --git a/frontend/pages/admin/IntegrationsPage/cards/Mdm/_styles.scss b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/_styles.scss similarity index 93% rename from frontend/pages/admin/IntegrationsPage/cards/Mdm/_styles.scss rename to frontend/pages/admin/IntegrationsPage/cards/MdmSettings/_styles.scss index 11170c653f5..474d8b2f3de 100644 --- a/frontend/pages/admin/IntegrationsPage/cards/Mdm/_styles.scss +++ b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/_styles.scss @@ -1,4 +1,4 @@ -.mdm-integrations { +.mdm-settings { display: flex; flex-direction: column; width: 65%; @@ -21,7 +21,7 @@ margin-bottom: 0; } - .mdm-integrations__edit-team-btn { + .mdm-settings-team-btn { margin-left: 12px; .children-wrapper { diff --git a/frontend/pages/admin/IntegrationsPage/cards/Mdm/components/EditTeamModal.tsx b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/EditTeamModal.tsx similarity index 100% rename from frontend/pages/admin/IntegrationsPage/cards/Mdm/components/EditTeamModal.tsx rename to frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/EditTeamModal.tsx diff --git a/frontend/pages/admin/IntegrationsPage/cards/Mdm/components/RequestCSRModal/RequestCSRModal.tsx b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/RequestCSRModal/RequestCSRModal.tsx similarity index 100% rename from frontend/pages/admin/IntegrationsPage/cards/Mdm/components/RequestCSRModal/RequestCSRModal.tsx rename to frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/RequestCSRModal/RequestCSRModal.tsx diff --git a/frontend/pages/admin/IntegrationsPage/cards/Mdm/components/RequestCSRModal/_styles.scss b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/RequestCSRModal/_styles.scss similarity index 100% rename from frontend/pages/admin/IntegrationsPage/cards/Mdm/components/RequestCSRModal/_styles.scss rename to frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/RequestCSRModal/_styles.scss diff --git a/frontend/pages/admin/IntegrationsPage/cards/Mdm/components/RequestCSRModal/index.ts b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/RequestCSRModal/index.ts similarity index 100% rename from frontend/pages/admin/IntegrationsPage/cards/Mdm/components/RequestCSRModal/index.ts rename to frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/RequestCSRModal/index.ts diff --git a/frontend/pages/hosts/ManageHostsPage/HostTableConfig.tsx b/frontend/pages/hosts/ManageHostsPage/HostTableConfig.tsx index 1a81e564d17..66d48c70d43 100644 --- a/frontend/pages/hosts/ManageHostsPage/HostTableConfig.tsx +++ b/frontend/pages/hosts/ManageHostsPage/HostTableConfig.tsx @@ -30,7 +30,7 @@ import { IUser } from "interfaces/user"; import PATHS from "router/paths"; import permissionUtils from "utilities/permissions"; import { DEFAULT_EMPTY_CELL_VALUE } from "utilities/constants"; -import getHostStatusTooltipText from "../helpers"; +import { getHostStatusTooltipText } from "../helpers"; interface IGetToggleAllRowsSelectedProps { checked: boolean; diff --git a/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tsx b/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tsx index edb321539cf..d439e7e540e 100644 --- a/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tsx +++ b/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tsx @@ -41,6 +41,7 @@ import FleetIcon from "../../../../../assets/images/fleet-avatar-24x24@2x.png"; import PolicyDetailsModal from "../cards/Policies/HostPoliciesTable/PolicyDetailsModal"; import AutoEnrollMdmModal from "./AutoEnrollMdmModal"; import ManualEnrollMdmModal from "./ManualEnrollMdmModal"; +import MacSettingsModal from "../MacSettingsModal"; const baseClass = "device-user"; @@ -75,6 +76,7 @@ const DeviceUserPage = ({ null ); const [showPolicyDetailsModal, setShowPolicyDetailsModal] = useState(false); + const [showMacSettingsModal, setShowMacSettingsModal] = useState(false); const [globalConfig, setGlobalConfig] = useState( null ); @@ -93,7 +95,7 @@ const DeviceUserPage = ({ } ); - const { data: macadmins, refetch: refetchMacadmins } = useQuery( + const { data: deviceMacAdminsData, refetch: refetchMacadmins } = useQuery( ["macadmins", deviceAuthToken], () => deviceUserAPI.loadHostDetailsExtension(deviceAuthToken, "macadmins"), { @@ -199,6 +201,8 @@ const DeviceUserPage = ({ "percent_disk_space_available", "gigs_disk_space_available", "team_name", + "platform", + "mdm", ]) ); @@ -232,6 +236,11 @@ const DeviceUserPage = ({ }, [showPolicyDetailsModal, setShowPolicyDetailsModal, setSelectedPolicy] ); + + const toggleMacSettingsModal = useCallback(() => { + setShowMacSettingsModal(!showMacSettingsModal); + }, [showMacSettingsModal, setShowMacSettingsModal]); + const onCancelPolicyDetailsModal = useCallback(() => { setShowPolicyDetailsModal(!showPolicyDetailsModal); setSelectedPolicy(null); @@ -310,10 +319,13 @@ const DeviceUserPage = ({ statusClassName={statusClassName} titleData={titleData} diskEncryption={hostDiskEncryption} + isPremiumTier={isPremiumTier} + toggleMacSettingsModal={toggleMacSettingsModal} + hostMacSettings={host?.mdm.profiles} + mdmName={deviceMacAdminsData?.mobile_device_management?.name} showRefetchSpinner={showRefetchSpinner} onRefetchHost={onRefetchHost} renderActionButtons={renderActionButtons} - isPremiumTier={isPremiumTier} deviceUser /> @@ -336,7 +348,7 @@ const DeviceUserPage = ({ @@ -369,6 +381,12 @@ const DeviceUserPage = ({ policy={selectedPolicy} /> )} + {showMacSettingsModal && ( + + )}
); }; diff --git a/frontend/pages/hosts/details/DeviceUserPage/_styles.scss b/frontend/pages/hosts/details/DeviceUserPage/_styles.scss index d7f31a8959f..8320eab6a1e 100644 --- a/frontend/pages/hosts/details/DeviceUserPage/_styles.scss +++ b/frontend/pages/hosts/details/DeviceUserPage/_styles.scss @@ -91,6 +91,8 @@ margin-right: $pad-xxlarge; .info-flex__data { + display: flex; + gap: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; @@ -101,8 +103,14 @@ vertical-align: sub; } - .total-issues-count { - margin-left: $pad-small; + .icon { + width: 16px; + height: 16px; + align-self: center; + } + + &__text { + padding-left: $pad-xsmall; } } } diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx index ff193a70954..4273011c188 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx @@ -62,6 +62,7 @@ import parseOsVersion from "./modals/OSPolicyModal/helpers"; import DiskEncryptionKeyModal from "./modals/DiskEncryptionKeyModal"; import HostActionDropdown from "./HostActionsDropdown/HostActionsDropdown"; +import MacSettingsModal from "../MacSettingsModal"; const baseClass = "host-details"; @@ -127,6 +128,7 @@ const HostDetailsPage = ({ const [showQueryHostModal, setShowQueryHostModal] = useState(false); const [showPolicyDetailsModal, setPolicyDetailsModal] = useState(false); const [showOSPolicyModal, setShowOSPolicyModal] = useState(false); + const [showMacSettingsModal, setShowMacSettingsModal] = useState(false); const [showUnenrollMdmModal, setShowUnenrollMdmModal] = useState(false); const [showDiskEncryptionModal, setShowDiskEncryptionModal] = useState(false); const [selectedPolicy, setSelectedPolicy] = useState( @@ -145,7 +147,6 @@ const HostDetailsPage = ({ ] = useState({}); const [usersState, setUsersState] = useState<{ username: string }[]>([]); const [usersSearchString, setUsersSearchString] = useState(""); - const [hideEditMdm, setHideEditMdm] = useState(false); const { data: fleetQueries, error: fleetQueriesError } = useQuery< IFleetQueriesResponse, @@ -388,6 +389,10 @@ const HostDetailsPage = ({ setShowOSPolicyModal(!showOSPolicyModal); }, [showOSPolicyModal, setShowOSPolicyModal]); + const toggleMacSettingsModal = useCallback(() => { + setShowMacSettingsModal(!showMacSettingsModal); + }, [showMacSettingsModal, setShowMacSettingsModal]); + const onCancelPolicyDetailsModal = useCallback(() => { setPolicyDetailsModal(!showPolicyDetailsModal); setSelectedPolicy(null); @@ -620,6 +625,9 @@ const HostDetailsPage = ({ isPremiumTier={isPremiumTier} isOnlyObserver={isOnlyObserver} toggleOSPolicyModal={toggleOSPolicyModal} + toggleMacSettingsModal={toggleMacSettingsModal} + hostMacSettings={host?.mdm.profiles} + mdmName={mdm?.name} showRefetchSpinner={showRefetchSpinner} onRefetchHost={onRefetchHost} renderActionButtons={renderActionButtons} @@ -737,15 +745,15 @@ const HostDetailsPage = ({ osPolicyLabel={osPolicyLabel} /> )} - {showUnenrollMdmModal && !!host && ( - { - setHideEditMdm(true); - }} + {showMacSettingsModal && ( + )} + {showUnenrollMdmModal && !!host && ( + + )} {showDiskEncryptionModal && host && ( void; - onSuccess: () => void; } const baseClass = "unenroll-mdm-modal"; -const UnenrollMdmModal = ({ - hostId, - onClose, - onSuccess, -}: IUnenrollMdmModalProps) => { +const UnenrollMdmModal = ({ hostId, onClose }: IUnenrollMdmModalProps) => { const [requestState, setRequestState] = useState< undefined | "unenrolling" | "error" >(undefined); @@ -31,7 +26,6 @@ const UnenrollMdmModal = ({ try { await mdmAPI.unenrollHostFromMdm(hostId, 5000); renderFlash("success", "Successfully turned off MDM."); - onSuccess(); onClose(); } catch (unenrollMdmError: unknown) { console.log(unenrollMdmError); diff --git a/frontend/pages/hosts/details/MacSettingsIndicator/MacSettingsIndicator.tests.tsx b/frontend/pages/hosts/details/MacSettingsIndicator/MacSettingsIndicator.tests.tsx new file mode 100644 index 00000000000..115e6406f11 --- /dev/null +++ b/frontend/pages/hosts/details/MacSettingsIndicator/MacSettingsIndicator.tests.tsx @@ -0,0 +1,65 @@ +import React from "react"; +import { fireEvent, render, screen } from "@testing-library/react"; +import MacSettingsIndicator from "./MacSettingsIndicator"; + +describe("MacSettingsIndicator", () => { + it("Renders the text and icon", () => { + const indicatorText = "test text"; + render( + + ); + const renderedIndicatorText = screen.getByText(indicatorText); + const renderedIcon = screen.getByTestId("icon"); + + expect(renderedIndicatorText).toBeInTheDocument(); + expect(renderedIcon).toBeInTheDocument(); + }); + + it("Renders text, icon, and tooltip", () => { + const indicatorText = "test text"; + const tooltipText = "test tooltip text"; + render( + + ); + const renderedIndicatorText = screen.getByText(indicatorText); + const renderedIcon = screen.getByTestId("icon"); + const renderedTooltipText = screen.getByText(tooltipText); + + expect(renderedIndicatorText).toBeInTheDocument(); + expect(renderedIcon).toBeInTheDocument(); + expect(renderedTooltipText).toBeInTheDocument(); + }); + + it("Renders text, icon, and onClick", () => { + const indicatorText = "test text"; + const onClick = () => { + const newDiv = document.createElement("div"); + newDiv.appendChild(document.createTextNode("onClick called")); + document.body.appendChild(newDiv); + }; + render( + { + onClick(); + }} + /> + ); + + const renderedIndicatorText = screen.getByText(indicatorText); + const renderedIcon = screen.getByTestId("icon"); + const renderedButton = screen.getByRole("button"); + + expect(renderedIndicatorText).toBeInTheDocument(); + expect(renderedIcon).toBeInTheDocument(); + expect(renderedButton).toBeInTheDocument(); + + fireEvent.click(renderedButton); + expect(screen.getByText("onClick called")).toBeInTheDocument(); + }); +}); diff --git a/frontend/pages/hosts/details/MacSettingsIndicator/MacSettingsIndicator.tsx b/frontend/pages/hosts/details/MacSettingsIndicator/MacSettingsIndicator.tsx new file mode 100644 index 00000000000..473745ee334 --- /dev/null +++ b/frontend/pages/hosts/details/MacSettingsIndicator/MacSettingsIndicator.tsx @@ -0,0 +1,106 @@ +import React from "react"; +import ReactTooltip from "react-tooltip"; +import { IconNames } from "components/icons"; +import Icon from "components/Icon"; +import Button from "components/buttons/Button"; + +const baseClass = "settings-indicator"; + +export interface IMacSettingsIndicator { + indicatorText: string; + iconName: IconNames; + onClick?: () => void; + tooltip?: { + tooltipText: string | null; + position?: "top" | "bottom"; + }; +} + +const MacSettingsIndicator = ({ + indicatorText, + iconName, + onClick, + tooltip, +}: IMacSettingsIndicator): JSX.Element => { + const getIndicatorTextWrapped = () => { + if (onClick && tooltip?.tooltipText) { + return ( + <> + + + + + {tooltip.tooltipText} + + + ); + } + + // onclick without tooltip + if (onClick) { + return ( + + ); + } + + // tooltip without onclick + if (tooltip?.tooltipText) { + return ( + <> + + {indicatorText} + + + {tooltip.tooltipText} + + + ); + } + + // no tooltip, no onclick + return indicatorText; + }; + + return ( + + + {getIndicatorTextWrapped()} + + ); +}; + +export default MacSettingsIndicator; diff --git a/frontend/pages/hosts/details/MacSettingsIndicator/_styles.scss b/frontend/pages/hosts/details/MacSettingsIndicator/_styles.scss new file mode 100644 index 00000000000..fce8265c95e --- /dev/null +++ b/frontend/pages/hosts/details/MacSettingsIndicator/_styles.scss @@ -0,0 +1,18 @@ +.settings-indicator { + display: flex; + gap: 4px; + + &__button { + font-weight: normal; + } + + .icon { + width: 16px; + height: 16px; + align-self: center; + } + + .__react_component_tooltip { + white-space: normal; + } +} diff --git a/frontend/pages/hosts/details/MacSettingsIndicator/index.ts b/frontend/pages/hosts/details/MacSettingsIndicator/index.ts new file mode 100644 index 00000000000..47e97520648 --- /dev/null +++ b/frontend/pages/hosts/details/MacSettingsIndicator/index.ts @@ -0,0 +1 @@ +export { default } from "./MacSettingsIndicator"; diff --git a/frontend/pages/hosts/details/MacSettingsModal/MacSettingsModal.tsx b/frontend/pages/hosts/details/MacSettingsModal/MacSettingsModal.tsx new file mode 100644 index 00000000000..2f0c18551d7 --- /dev/null +++ b/frontend/pages/hosts/details/MacSettingsModal/MacSettingsModal.tsx @@ -0,0 +1,32 @@ +import React from "react"; +import Button from "components/buttons/Button"; +import Modal from "components/Modal"; +import { IMacSettings } from "interfaces/mdm"; +import MacSettingsTable from "./MacSettingsTable"; + +interface IMacSettingsModalProps { + hostMacSettings?: IMacSettings; + onClose: () => void; +} + +const baseClass = "mac-settings-modal"; + +const MacSettingsModal = ({ + hostMacSettings, + onClose, +}: IMacSettingsModalProps) => { + return ( + + <> + +
+ +
+ +
+ ); +}; + +export default MacSettingsModal; diff --git a/frontend/pages/hosts/details/MacSettingsModal/MacSettingsTable/MacSettingsTable.tsx b/frontend/pages/hosts/details/MacSettingsModal/MacSettingsTable/MacSettingsTable.tsx new file mode 100644 index 00000000000..65b21ac39be --- /dev/null +++ b/frontend/pages/hosts/details/MacSettingsModal/MacSettingsTable/MacSettingsTable.tsx @@ -0,0 +1,32 @@ +import React from "react"; +import TableContainer from "components/TableContainer"; +import EmptyTable from "components/EmptyTable"; +import { IMacSettings } from "interfaces/mdm"; + +import tableHeaders from "./MacSettingsTableConfig"; + +const baseClass = "macsettings-table"; + +interface IMacSettingsTableProps { + hostMacSettings?: IMacSettings; +} + +const MacSettingsTable = ({ hostMacSettings }: IMacSettingsTableProps) => { + return ( +
+ +
+ ); +}; + +export default MacSettingsTable; diff --git a/frontend/pages/hosts/details/MacSettingsModal/MacSettingsTable/MacSettingsTableConfig.tsx b/frontend/pages/hosts/details/MacSettingsModal/MacSettingsTable/MacSettingsTableConfig.tsx new file mode 100644 index 00000000000..3a789712955 --- /dev/null +++ b/frontend/pages/hosts/details/MacSettingsModal/MacSettingsTable/MacSettingsTableConfig.tsx @@ -0,0 +1,136 @@ +import TextCell from "components/TableContainer/DataTable/TextCell"; +import React from "react"; +import { + IHostMacMdmProfile, + MacMdmProfileOperationType, + MacMdmProfileStatus, +} from "interfaces/mdm"; +import { DEFAULT_EMPTY_CELL_VALUE } from "utilities/constants"; +import TruncatedTextCell from "components/TableContainer/DataTable/TruncatedTextCell"; +import MacSettingsIndicator from "../../MacSettingsIndicator"; +import { IMacSettingsIndicator } from "../../MacSettingsIndicator/MacSettingsIndicator"; + +interface IHeaderProps { + column: { + title: string; + isSortedDesc: boolean; + }; +} + +interface ICellProps { + cell: { + value: string; + }; + row: { + original: IHostMacMdmProfile; + }; +} + +interface IDataColumn { + Header: ((props: IHeaderProps) => JSX.Element) | string; + Cell: (props: ICellProps) => JSX.Element; + id?: string; + title?: string; + accessor?: string; + disableHidden?: boolean; + disableSortBy?: boolean; + sortType?: string; +} + +const PROFILE_DISPLAY_CONFIG: Record< + MacMdmProfileOperationType, + Record +> = { + install: { + pending: { + indicatorText: "Enforcing (pending)", + iconName: "pending", + tooltip: { + tooltipText: "Setting will be enforced when the host comes online.", + }, + }, + success: { + indicatorText: "Applied", + iconName: "success", + tooltip: { tooltipText: "Host applied the setting." }, + }, + failed: { + indicatorText: "Failed", + iconName: "error", + tooltip: undefined, + }, + }, + remove: { + pending: { + indicatorText: "Removing enforcement (pending)", + iconName: "pending", + tooltip: { + tooltipText: "Enforcement will be removed when the host comes online.", + }, + }, + success: null, // should not be reached + failed: { + indicatorText: "Failed", + iconName: "error", + tooltip: undefined, + }, + }, +}; + +const tableHeaders: IDataColumn[] = [ + { + title: "Name", + Header: "Name", + disableSortBy: true, + accessor: "name", + Cell: (cellProps: ICellProps): JSX.Element => ( + + ), + }, + { + title: "Status", + Header: "Status", + disableSortBy: true, + accessor: "statusText", + Cell: (cellProps: ICellProps) => { + const { status, operation_type } = cellProps.row.original; + const options = PROFILE_DISPLAY_CONFIG[operation_type]?.[status]; + if (options) { + const { indicatorText, iconName } = options; + const tooltip = { + tooltipText: options.tooltip?.tooltipText ?? null, + position: "top" as const, + }; + return ( + + ); + } + + // graceful error - this state should not be reached based on the API spec + return ; + }, + }, + { + title: "Error", + Header: "Error", + disableSortBy: true, + accessor: "detail", + Cell: (cellProps: ICellProps): JSX.Element => { + const profile = cellProps.row.original; + return ( + + ); + }, + }, +]; + +export default tableHeaders; diff --git a/frontend/pages/hosts/details/MacSettingsModal/MacSettingsTable/_styles.scss b/frontend/pages/hosts/details/MacSettingsModal/MacSettingsTable/_styles.scss new file mode 100644 index 00000000000..53e57edea51 --- /dev/null +++ b/frontend/pages/hosts/details/MacSettingsModal/MacSettingsTable/_styles.scss @@ -0,0 +1,7 @@ +.macsettings-table { + .statusText { + &__cell { + white-space: nowrap; + } + } +} diff --git a/frontend/pages/hosts/details/MacSettingsModal/MacSettingsTable/index.ts b/frontend/pages/hosts/details/MacSettingsModal/MacSettingsTable/index.ts new file mode 100644 index 00000000000..420c41a651e --- /dev/null +++ b/frontend/pages/hosts/details/MacSettingsModal/MacSettingsTable/index.ts @@ -0,0 +1 @@ +export { default } from "./MacSettingsTable"; diff --git a/frontend/pages/hosts/details/MacSettingsModal/index.ts b/frontend/pages/hosts/details/MacSettingsModal/index.ts new file mode 100644 index 00000000000..c061f22f7a8 --- /dev/null +++ b/frontend/pages/hosts/details/MacSettingsModal/index.ts @@ -0,0 +1 @@ +export { default } from "./MacSettingsModal"; diff --git a/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tsx b/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tsx index 07ad382220a..8815b9a2dd9 100644 --- a/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tsx +++ b/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tsx @@ -8,9 +8,14 @@ import DiskSpaceGraph from "components/DiskSpaceGraph"; import HumanTimeDiffWithDateTip from "components/HumanTimeDiffWithDateTip"; import { humanHostMemory, wrapFleetHelper } from "utilities/helpers"; import { DEFAULT_EMPTY_CELL_VALUE } from "utilities/constants"; -import getHostStatusTooltipText from "pages/hosts/helpers"; +import { + getHostStatusTooltipText, + getMacSettingsStatus, +} from "pages/hosts/helpers"; import StatusIndicator from "components/StatusIndicator"; +import { IMacSettings } from "interfaces/mdm"; import IssueIcon from "../../../../../../assets/images/icon-issue-fleet-black-50-16x16@2x.png"; +import MacSettingsIndicator from "../../MacSettingsIndicator"; const baseClass = "host-summary"; @@ -18,6 +23,7 @@ interface IHostDiskEncryptionProps { enabled?: boolean; tooltip?: string; } + interface IHostSummaryProps { statusClassName: string; titleData: any; // TODO: create interfaces for this and use consistently across host pages and related helpers @@ -25,6 +31,9 @@ interface IHostSummaryProps { isPremiumTier?: boolean; isOnlyObserver?: boolean; toggleOSPolicyModal?: () => void; + toggleMacSettingsModal?: () => void; + hostMacSettings?: IMacSettings; + mdmName?: string; showRefetchSpinner: boolean; onRefetchHost: ( evt: React.MouseEvent @@ -40,6 +49,9 @@ const HostSummary = ({ isPremiumTier, isOnlyObserver, toggleOSPolicyModal, + toggleMacSettingsModal, + hostMacSettings, + mdmName, showRefetchSpinner, onRefetchHost, renderActionButtons, @@ -108,7 +120,7 @@ const HostSummary = ({ Failing policies ({titleData.issues.failing_policies_count})
- + {titleData.issues.total_issues_count} @@ -128,6 +140,41 @@ const HostSummary = ({ ); + const renderMacSettingsIndicator = () => { + const STATUS_DISPLAY_OPTIONS = { + Latest: { + iconName: "success", + tooltipText: "Host applied the latest settings", + }, + Pending: { + iconName: "pending", + tooltipText: "Host will apply the latest settings when it comes online", + }, + Failing: { + iconName: "error", + tooltipText: + "Host failed to apply the latest settings. Click to view error(s).", + }, + } as const; + + const macSettingsStatus = getMacSettingsStatus(hostMacSettings); + + const iconName = STATUS_DISPLAY_OPTIONS[macSettingsStatus].iconName; + const tooltipText = STATUS_DISPLAY_OPTIONS[macSettingsStatus].tooltipText; + + return ( +
+ macOS settings + +
+ ); + }; + const renderSummary = () => { const { status, id } = titleData; return ( @@ -139,13 +186,23 @@ const HostSummary = ({ tooltip={{ id, tooltipText: getHostStatusTooltipText(status), + position: "bottom", }} /> + {titleData.issues?.total_issues_count > 0 && isPremiumTier && renderIssues()} + {isPremiumTier && renderHostTeam()} + + {titleData.platform === "darwin" && + isPremiumTier && + mdmName === "Fleet" && // show if 1 - host is enrolled in Fleet MDM, and + hostMacSettings && // 2 - host has at least one setting (profile) enforced + renderMacSettingsIndicator()} +
Disk space
+ {typeof diskEncryption?.enabled === "boolean" && diskEncryption?.tooltip ? (
Disk encryption - + {diskEncryption.enabled ? "On" : "Off"}
@@ -184,7 +246,7 @@ const HostSummary = ({ `${titleData.os_version}` ) : (