mirror of
https://github.com/fleetdm/fleet
synced 2026-05-23 08:58:41 +00:00
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
This commit is contained in:
parent
4a3ebc738a
commit
9593c7dec4
8 changed files with 202 additions and 66 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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(<GlobalActivityItem activity={activity} isPremiumTier />);
|
||||
|
||||
expect(
|
||||
screen.getByText((content, node) => {
|
||||
return (
|
||||
node?.innerHTML ===
|
||||
"<b>Test User </b>An end user turned on MDM features for <b>Test Host (personal)</b>."
|
||||
);
|
||||
return node?.innerHTML === "<b>Test Host</b> 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(<GlobalActivityItem activity={activity} isPremiumTier />);
|
||||
|
||||
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(<GlobalActivityItem activity={activity} isPremiumTier />);
|
||||
|
||||
expect(screen.getByText(/Test Host/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/is unenrolled from Fleet/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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{" "}
|
||||
<b>{activity.details?.host_display_name} (manual)</b>.
|
||||
<b>{activity.actor_full_name} </b>Mobile device management (MDM) was
|
||||
turned on for <b>{activity.details?.host_display_name} (manual)</b>.
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (isAndroid(platform) || isIPadOrIPhone(platform)) {
|
||||
return (
|
||||
<>
|
||||
<b>{host_display_name}</b> 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}
|
||||
<b>{activity.actor_full_name} </b>An end user turned on MDM features for{" "}
|
||||
{hostDisplayPrefixText}
|
||||
<b>
|
||||
{hostDisplayText} ({enrollmentTypeText})
|
||||
</b>
|
||||
|
|
@ -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 ? (
|
||||
<>
|
||||
<b>{actor_full_name}</b> told Fleet to unenroll{" "}
|
||||
<b>{host_display_name}.</b>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<b>{host_display_name}</b> 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"}{" "}
|
||||
<b>{activity.details?.host_display_name}</b>.
|
||||
{actor_full_name ? (
|
||||
<>
|
||||
<b>{actor_full_name}</b> told Fleet to turn off mobile device
|
||||
management (MDM) for
|
||||
</>
|
||||
) : (
|
||||
"Mobile device management (MDM) was turned off for"
|
||||
)}{" "}
|
||||
<b>{host_display_name}</b>.
|
||||
</>
|
||||
);
|
||||
},
|
||||
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -8,4 +8,8 @@
|
|||
right: 0;
|
||||
min-width: max-content;
|
||||
}
|
||||
|
||||
.actions-dropdown-select__menu {
|
||||
min-width: 200px;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1357,6 +1357,9 @@ const HostDetailsPage = ({
|
|||
hostId={host.id}
|
||||
hostPlatform={host.platform}
|
||||
hostName={host.display_name}
|
||||
isBYODEnrollment={isPersonalEnrollmentInMdm(
|
||||
host.mdm.enrollment_status
|
||||
)}
|
||||
onClose={toggleUnenrollMdmModal}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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 <b>{hostName}</b> next time this host
|
||||
checks in.
|
||||
</>
|
||||
);
|
||||
const successMessage =
|
||||
isIPadOrIPhone(hostPlatform) || isAndroid(hostPlatform) ? (
|
||||
<>
|
||||
<b>{hostName}</b> will unenrolled next time this host checks in.
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
MDM will be turned off for <b>{hostName}</b> 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 <b>{hostName}</b>. Please try again.
|
||||
</>
|
||||
);
|
||||
renderFlash("error", errorMessage);
|
||||
}
|
||||
setRequestState(undefined);
|
||||
};
|
||||
|
||||
const generateDescription = () => {
|
||||
if (isIPadOrIPhone(hostPlatform)) {
|
||||
return (
|
||||
<>
|
||||
Failed to turn off MDM for <b>{hostName}</b>.
|
||||
<p>Settings configured by Fleet will be removed.</p>
|
||||
{isBYODEnrollment ? (
|
||||
<p>
|
||||
To re-enroll, ask your end user to navigate to{" "}
|
||||
<b>
|
||||
Settings > General > VPN & Device Management > Sign
|
||||
in to Work or School Account...
|
||||
</b>{" "}
|
||||
on their host and to log in with their work email.
|
||||
</p>
|
||||
) : (
|
||||
<p>
|
||||
To re-enroll, make sure that the host is still in Apple Business
|
||||
Manager (ABM). The host will automatically enroll after it's
|
||||
reset.
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
onClose();
|
||||
if (isAndroid(hostPlatform)) {
|
||||
return (
|
||||
<>
|
||||
<p>Company data and OS settings (work profile) will be deleted.</p>
|
||||
<p>
|
||||
To re-enroll, go to <b>Hosts > Add hosts > Android</b> and
|
||||
share the link with end user.
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<p>Settings configured by Fleet will be removed.</p>
|
||||
<p>
|
||||
To turn on MDM again, ask the device user to follow the{" "}
|
||||
<b>Turn on MDM</b> instructions on their <b>My device</b> page.
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const renderModalContent = () => {
|
||||
|
|
@ -57,30 +113,16 @@ const UnenrollMdmModal = ({
|
|||
return <DataError />;
|
||||
}
|
||||
|
||||
const turnOnMDMInstructions = isIPadOrIPhone(hostPlatform) ? (
|
||||
<>
|
||||
invite the end user to{" "}
|
||||
<CustomLink
|
||||
text="enroll a BYOD iPhone or iPad"
|
||||
url="https://fleetdm.com/guides/enroll-byod-ios-ipados-hosts"
|
||||
newTab
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
ask the device user to follow the <b>Turn on MDM</b> instructions on
|
||||
their <b>My device</b> page.
|
||||
</>
|
||||
);
|
||||
const buttonText =
|
||||
isIPadOrIPhone(hostPlatform) || isAndroid(hostPlatform)
|
||||
? "Unenroll"
|
||||
: "Turn off";
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className={`${baseClass}__description`}>
|
||||
Settings configured by Fleet will be removed.
|
||||
<br />
|
||||
<br />
|
||||
To turn on MDM again, {turnOnMDMInstructions}
|
||||
</p>
|
||||
<div className={`${baseClass}__description`}>
|
||||
{generateDescription()}
|
||||
</div>
|
||||
<div className="modal-cta-wrap">
|
||||
<Button
|
||||
type="submit"
|
||||
|
|
@ -88,7 +130,7 @@ const UnenrollMdmModal = ({
|
|||
onClick={submitUnenrollMdm}
|
||||
isLoading={requestState === "unenrolling"}
|
||||
>
|
||||
Turn off
|
||||
{buttonText}
|
||||
</Button>
|
||||
<Button onClick={onClose} variant="inverse-alert">
|
||||
Cancel
|
||||
|
|
@ -98,12 +140,18 @@ const UnenrollMdmModal = ({
|
|||
);
|
||||
};
|
||||
|
||||
const title =
|
||||
isIPadOrIPhone(hostPlatform) || isAndroid(hostPlatform)
|
||||
? "Unenroll"
|
||||
: "Turn off MDM";
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Turn off MDM"
|
||||
title={title}
|
||||
onExit={onClose}
|
||||
className={baseClass}
|
||||
width="medium"
|
||||
isContentDisabled={requestState === "unenrolling"}
|
||||
>
|
||||
{renderModalContent()}
|
||||
</Modal>
|
||||
|
|
|
|||
Loading…
Reference in a new issue