From 9593c7dec4ce75cb99feca456e68cf0ec2187369 Mon Sep 17 00:00:00 2001 From: Gabriel Hernandez Date: Mon, 22 Sep 2025 15:41:24 +0100 Subject: [PATCH] update UI to support unenrolling android and ios and ipados devices (#32974) resolves #31821, resolves #32120 this updates the UI to support unenrolling android and ios and ipad devices. This includes: **updating the host details page to include and unenroll action in the host actions dropdown** **Updating the unenroll modal to have dynamic content depending on the device we are unenrolling** **updating the global activities to have different messages for mdm enroll and mdm unenroll actions** - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. - [x] Added/updated automated tests - [x] QA'd all new/changed functionality manually --- frontend/interfaces/activity.ts | 4 +- .../GlobalActivityItem.tests.tsx | 40 +++++- .../GlobalActivityItem/GlobalActivityItem.tsx | 71 ++++++++--- .../HostActionsDropdown.tsx | 2 + .../HostActionsDropdown/_styles.scss | 4 + .../HostActionsDropdown/helpers.tsx | 24 +++- .../HostDetailsPage/HostDetailsPage.tsx | 3 + .../UnenrollMdmModal/UnenrollMdmModal.tsx | 120 ++++++++++++------ 8 files changed, 202 insertions(+), 66 deletions(-) diff --git a/frontend/interfaces/activity.ts b/frontend/interfaces/activity.ts index fd2baa00d1..8077d65bcf 100644 --- a/frontend/interfaces/activity.ts +++ b/frontend/interfaces/activity.ts @@ -207,12 +207,12 @@ export interface IActivityDetails { labels_exclude_any?: ILabelSoftwareTitle[]; labels_include_any?: ILabelSoftwareTitle[]; location?: string; // name of location associated with VPP token - mdm_platform?: "microsoft" | "apple"; + mdm_platform?: "microsoft" | "apple" | "android" | "ios" | "ipados"; minimum_version?: string; name?: string; pack_id?: number; pack_name?: string; - platform?: Platform; // software platform + platform?: Platform; // OS platform policy_id?: number; policy_name?: string; profile_identifier?: string; diff --git a/frontend/pages/DashboardPage/cards/ActivityFeed/GlobalActivityItem/GlobalActivityItem.tests.tsx b/frontend/pages/DashboardPage/cards/ActivityFeed/GlobalActivityItem/GlobalActivityItem.tests.tsx index 519c9f7054..b01219b352 100644 --- a/frontend/pages/DashboardPage/cards/ActivityFeed/GlobalActivityItem/GlobalActivityItem.tests.tsx +++ b/frontend/pages/DashboardPage/cards/ActivityFeed/GlobalActivityItem/GlobalActivityItem.tests.tsx @@ -1036,23 +1036,20 @@ describe("Activity Feed", () => { ).toBeInTheDocument(); }); - it("renders a 'mdm_enrolled' type for apple with host display name and personal enrollment provided", () => { + it("renders a 'mdm_enrolled' type for android or apple personal devices with actor full name provided", () => { const activity = createMockActivity({ type: ActivityType.MdmEnrolled, details: { host_display_name: "Test Host", enrollment_id: "test-enrollment-id", - mdm_platform: "apple", + platform: "android", }, }); render(); expect( screen.getByText((content, node) => { - return ( - node?.innerHTML === - "Test User An end user turned on MDM features for Test Host (personal)." - ); + return node?.innerHTML === "Test Host enrolled to Fleet."; }) ).toBeInTheDocument(); }); @@ -1583,4 +1580,35 @@ describe("Activity Feed", () => { ).toBeInTheDocument(); expect(screen.getByText(/HYDRANT_TEST/)).toBeInTheDocument(); }); + + it("renders an mdm unenroll activity with an actor name for ios, ipados, and android devices", () => { + const activity = createMockActivity({ + type: ActivityType.MdmUnenrolled, + actor_full_name: "Test User", + details: { + platform: "ios", + host_display_name: "Test Host", + }, + }); + render(); + + expect(screen.getByText(/Test User/)).toBeInTheDocument(); + expect(screen.getByText(/told Fleet to unenroll/)).toBeInTheDocument(); + expect(screen.getByText(/Test Host/)).toBeInTheDocument(); + }); + + it("renders an mdm unenroll activity with no actor name for ios, ipados, and android devices", () => { + const activity = createMockActivity({ + type: ActivityType.MdmUnenrolled, + actor_full_name: undefined, + details: { + platform: "ios", + host_display_name: "Test Host", + }, + }); + render(); + + expect(screen.getByText(/Test Host/)).toBeInTheDocument(); + expect(screen.getByText(/is unenrolled from Fleet/)).toBeInTheDocument(); + }); }); diff --git a/frontend/pages/DashboardPage/cards/ActivityFeed/GlobalActivityItem/GlobalActivityItem.tsx b/frontend/pages/DashboardPage/cards/ActivityFeed/GlobalActivityItem/GlobalActivityItem.tsx index a44a7e168b..dce7dc03f5 100644 --- a/frontend/pages/DashboardPage/cards/ActivityFeed/GlobalActivityItem/GlobalActivityItem.tsx +++ b/frontend/pages/DashboardPage/cards/ActivityFeed/GlobalActivityItem/GlobalActivityItem.tsx @@ -4,6 +4,8 @@ import React from "react"; import { ActivityType, IActivity } from "interfaces/activity"; import { AppleDisplayPlatform, + isAndroid, + isIPadOrIPhone, PLATFORM_DISPLAY_NAMES, } from "interfaces/platform"; import { getInstallStatusPredicate } from "interfaces/software"; @@ -272,12 +274,24 @@ const TAGGED_TEMPLATES = { ); return <>{hostDisplayName} enrolled in Fleet.; }, + mdmEnrolled: (activity: IActivity) => { - if (activity.details?.mdm_platform === "microsoft") { + const { mdm_platform, platform = "", host_display_name, host_serial } = + activity.details || {}; + + if (mdm_platform === "microsoft") { return ( <> - Mobile device management (MDM) was turned on for{" "} - {activity.details?.host_display_name} (manual). + {activity.actor_full_name} Mobile device management (MDM) was + turned on for {activity.details?.host_display_name} (manual). + + ); + } + + if (isAndroid(platform) || isIPadOrIPhone(platform)) { + return ( + <> + {host_display_name} enrolled to Fleet. ); } @@ -285,24 +299,21 @@ const TAGGED_TEMPLATES = { // note: if mdm_platform is missing, we assume this is Apple MDM for backwards // compatibility let enrollmentTypeText = ""; - if (activity.details?.enrollment_id) { - enrollmentTypeText = "personal"; - } else if (activity.details?.installed_from_dep) { + if (activity.details?.installed_from_dep) { enrollmentTypeText = "automatic"; } else { enrollmentTypeText = "manual"; } - const hostDisplayText = - activity.details?.host_display_name || activity.details?.host_serial; - - const hostDisplayPrefixText = activity.details?.host_display_name + const hostDisplayText = host_display_name || host_serial; + const hostDisplayPrefixText = host_display_name ? "" : "a host with serial number "; return ( <> - An end user turned on MDM features for {hostDisplayPrefixText} + {activity.actor_full_name} An end user turned on MDM features for{" "} + {hostDisplayPrefixText} {hostDisplayText} ({enrollmentTypeText}) @@ -310,16 +321,39 @@ const TAGGED_TEMPLATES = { ); }, + mdmUnenrolled: (activity: IActivity) => { + const { actor_full_name } = activity; + const { platform = "", host_display_name } = activity.details || {}; + + if (isAndroid(platform) || isIPadOrIPhone(platform)) { + return actor_full_name ? ( + <> + {actor_full_name} told Fleet to unenroll{" "} + {host_display_name}. + + ) : ( + <> + {host_display_name} is unenrolled from Fleet. + + ); + } + return ( <> - {activity.actor_full_name - ? " told Fleet to turn off mobile device management (MDM) for" - : "Mobile device management (MDM) was turned off for"}{" "} - {activity.details?.host_display_name}. + {actor_full_name ? ( + <> + {actor_full_name} told Fleet to turn off mobile device + management (MDM) for + + ) : ( + "Mobile device management (MDM) was turned off for" + )}{" "} + {host_display_name}. ); }, + editedAppleosMinVersion: ( applePlatform: AppleDisplayPlatform, activity: IActivity @@ -1823,7 +1857,12 @@ const GlobalActivityItem = ({ ) : ( DEFAULT_ACTOR_DISPLAY ); - + // MdmEnrolled and MdmUnenroll activities have more complicated logic to + // determine if we display the actor name so we will handle that in the + // template function + case ActivityType.MdmUnenrolled: + case ActivityType.MdmEnrolled: + return null; default: return DEFAULT_ACTOR_DISPLAY; } diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tsx index c71a98fe6e..d43f9de907 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tsx @@ -41,6 +41,7 @@ const HostActionsDropdown = ({ isGlobalMaintainer = false, isMacMdmEnabledAndConfigured = false, isWindowsMdmEnabledAndConfigured = false, + isAndroidMdmEnabledAndConfigured = false, currentUser, config: globalConfig, } = useContext(AppContext); @@ -69,6 +70,7 @@ const HostActionsDropdown = ({ isConnectedToFleetMdm, isMacMdmEnabledAndConfigured, isWindowsMdmEnabledAndConfigured, + isAndroidMdmEnabledAndConfigured, doesStoreEncryptionKey: doesStoreEncryptionKey ?? false, hostMdmDeviceStatus, hostScriptsEnabled, diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/_styles.scss b/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/_styles.scss index eda3a72674..1659d8f801 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/_styles.scss +++ b/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/_styles.scss @@ -8,4 +8,8 @@ right: 0; min-width: max-content; } + + .actions-dropdown-select__menu { + min-width: 200px; + } } diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/helpers.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/helpers.tsx index f163655112..b115d2be63 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/helpers.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/helpers.tsx @@ -66,7 +66,6 @@ const DEFAULT_OPTIONS = [ }, ] as const; -// eslint-disable-next-line import/prefer-default-export interface IHostActionConfigOptions { hostPlatform: string; isPremiumTier: boolean; @@ -81,6 +80,7 @@ interface IHostActionConfigOptions { isConnectedToFleetMdm?: boolean; isMacMdmEnabledAndConfigured: boolean; isWindowsMdmEnabledAndConfigured: boolean; + isAndroidMdmEnabledAndConfigured: boolean; doesStoreEncryptionKey: boolean; hostMdmDeviceStatus: HostMdmDeviceStatusUIState; hostScriptsEnabled: boolean | null; @@ -108,14 +108,12 @@ const canTurnOffMdm = (config: IHostActionConfigOptions) => { isEnrolledInMdm, isConnectedToFleetMdm, isMacMdmEnabledAndConfigured, - hostMdmEnrollmentStatus, + isAndroidMdmEnabledAndConfigured, } = config; return ( - !isAndroid(hostPlatform) && // TODO(android): confirm can't turn off MDM for windows, iOS, iPadOS? - isAppleDevice(hostPlatform) && - isMacMdmEnabledAndConfigured && + ((isAndroid(hostPlatform) && isAndroidMdmEnabledAndConfigured) || + (isAppleDevice(hostPlatform) && isMacMdmEnabledAndConfigured)) && isEnrolledInMdm && - hostMdmEnrollmentStatus !== "On (personal)" && // can't turn off MDM for personally enrolled hosts isConnectedToFleetMdm && (isGlobalAdmin || isGlobalMaintainer || isTeamAdmin || isTeamMaintainer) ); @@ -336,6 +334,19 @@ export const getDropdownOptionTooltipContent = ( return undefined; }; +/** for ios, ipad, and android we want to display different text for mdmOff. + * The functionality is the same, but the action is called unenroll on those platforms. + */ +const formatTurnOffOptionLabel = ( + options: IDropdownOption[], + hostPlatform: string +) => { + const option = options.find((opt) => opt.value === "mdmOff"); + if (option && (isIPadOrIPhone(hostPlatform) || isAndroid(hostPlatform))) { + option.label = "Unenroll"; + } +}; + const modifyOptions = ( options: IDropdownOption[], { @@ -396,6 +407,7 @@ const modifyOptions = ( } } disableOptions(optionsToDisable); + formatTurnOffOptionLabel(options, hostPlatform); return options; }; diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx index 613dfcb758..157db12217 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx @@ -1357,6 +1357,9 @@ const HostDetailsPage = ({ hostId={host.id} hostPlatform={host.platform} hostName={host.display_name} + isBYODEnrollment={isPersonalEnrollmentInMdm( + host.mdm.enrollment_status + )} onClose={toggleUnenrollMdmModal} /> )} diff --git a/frontend/pages/hosts/details/HostDetailsPage/modals/UnenrollMdmModal/UnenrollMdmModal.tsx b/frontend/pages/hosts/details/HostDetailsPage/modals/UnenrollMdmModal/UnenrollMdmModal.tsx index ce902622c9..b21e4042dd 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/modals/UnenrollMdmModal/UnenrollMdmModal.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/modals/UnenrollMdmModal/UnenrollMdmModal.tsx @@ -6,13 +6,13 @@ import Modal from "components/Modal"; import { NotificationContext } from "context/notification"; import mdmAPI from "services/entities/mdm"; -import { isIPadOrIPhone } from "interfaces/platform"; -import CustomLink from "components/CustomLink"; +import { isAndroid, isIPadOrIPhone } from "interfaces/platform"; interface IUnenrollMdmModalProps { hostId: number; hostPlatform: string; hostName: string; + isBYODEnrollment?: boolean; onClose: () => void; } @@ -22,6 +22,7 @@ const UnenrollMdmModal = ({ hostId, hostPlatform, hostName, + isBYODEnrollment = false, onClose, }: IUnenrollMdmModalProps) => { const [requestState, setRequestState] = useState< @@ -34,22 +35,77 @@ const UnenrollMdmModal = ({ setRequestState("unenrolling"); try { await mdmAPI.unenrollHostFromMdm(hostId, 5000); - renderFlash( - "success", - <> - MDM will be turned off for {hostName} next time this host - checks in. - - ); + const successMessage = + isIPadOrIPhone(hostPlatform) || isAndroid(hostPlatform) ? ( + <> + {hostName} will unenrolled next time this host checks in. + + ) : ( + <> + MDM will be turned off for {hostName} next time this host + checks in. + + ); + renderFlash("success", successMessage); + onClose(); } catch (unenrollMdmError: unknown) { - renderFlash( - "error", + const errorMessage = + isIPadOrIPhone(hostPlatform) || isAndroid(hostPlatform) ? ( + "Couldn't unenroll. Please try again." + ) : ( + <> + Failed to turn off MDM for {hostName}. Please try again. + + ); + renderFlash("error", errorMessage); + } + setRequestState(undefined); + }; + + const generateDescription = () => { + if (isIPadOrIPhone(hostPlatform)) { + return ( <> - Failed to turn off MDM for {hostName}. +

Settings configured by Fleet will be removed.

+ {isBYODEnrollment ? ( +

+ To re-enroll, ask your end user to navigate to{" "} + + Settings > General > VPN & Device Management > Sign + in to Work or School Account... + {" "} + on their host and to log in with their work email. +

+ ) : ( +

+ To re-enroll, make sure that the host is still in Apple Business + Manager (ABM). The host will automatically enroll after it's + reset. +

+ )} ); } - onClose(); + if (isAndroid(hostPlatform)) { + return ( + <> +

Company data and OS settings (work profile) will be deleted.

+

+ To re-enroll, go to Hosts > Add hosts > Android and + share the link with end user. +

+ + ); + } + return ( + <> +

Settings configured by Fleet will be removed.

+

+ To turn on MDM again, ask the device user to follow the{" "} + Turn on MDM instructions on their My device page. +

+ + ); }; const renderModalContent = () => { @@ -57,30 +113,16 @@ const UnenrollMdmModal = ({ return ; } - const turnOnMDMInstructions = isIPadOrIPhone(hostPlatform) ? ( - <> - invite the end user to{" "} - - - ) : ( - <> - ask the device user to follow the Turn on MDM instructions on - their My device page. - - ); + const buttonText = + isIPadOrIPhone(hostPlatform) || isAndroid(hostPlatform) + ? "Unenroll" + : "Turn off"; return ( <> -

- Settings configured by Fleet will be removed. -
-
- To turn on MDM again, {turnOnMDMInstructions} -

+
+ {generateDescription()} +