mirror of
https://github.com/empayre/fleet.git
synced 2024-11-06 08:55:24 +00:00
UI: 9274 unenroll mdm modal (#9539)
# Addresses #9274 https://www.loom.com/share/2edd946cbd424af2b960801cc505ac85 ## Button and permissions: - no permission, enrolled, online: <img width="1131" alt="no permission, enrolled, online" src="https://user-images.githubusercontent.com/61553566/215197330-abc1606d-bf0a-44ec-b2de-2ef687bd529b.png"> - permission, enrolled, online: <img width="1131" alt="permission, enrolled, online" src="https://user-images.githubusercontent.com/61553566/215197443-a1353b9b-10dd-408b-8295-56029f2df4c3.png"> - permission, enrolled, offline: <img width="1131" alt="permission, enrolled, offline" src="https://user-images.githubusercontent.com/61553566/215197544-b2a997a7-09e5-4f8a-b723-af587b61a90d.png"> - not enrolled: <img width="1131" alt="not enrolled" src="https://user-images.githubusercontent.com/61553566/215197630-87f99cb3-63a9-45ce-bc85-57a45d54cae0.png"> ## Modal - <img width="672" alt="modal" src="https://user-images.githubusercontent.com/61553566/215214640-96670a23-d927-4213-a8fa-89411279c075.png"> - <img width="672" alt="Screenshot 2023-01-27 at 2 12 42 PM" src="https://user-images.githubusercontent.com/61553566/215215098-40d29556-3b73-4f52-a4ae-cc8b09122f5d.png"> - <img width="672" alt="Screenshot 2023-01-27 at 2 17 48 PM" src="https://user-images.githubusercontent.com/61553566/215216304-b9362b13-f37f-4454-81b5-423f6fc72280.png"> - <img width="787" alt="success-shot" src="https://user-images.githubusercontent.com/61553566/215236373-be7b1970-662d-47e6-ac59-f51eff344fcd.png"> # Checklist for submitter If some of the following don't apply, delete the relevant line. - [x] Changes file added for user-visible changes in `changes/` - [x] Updated test inventory - [x] Manual QA for all new/changed functionality --------- Co-authored-by: Jacob Shandling <jacob@fleetdm.com>
This commit is contained in:
parent
180f7691ce
commit
f12780df45
1
changes/9274-unenroll-mdm-modal
Normal file
1
changes/9274-unenroll-mdm-modal
Normal file
@ -0,0 +1 @@
|
||||
- Implemented the ability for an authorized user to unenroll a host from MDM on its host details page. The host must be enrolled in MDM and online.
|
@ -12,7 +12,6 @@
|
||||
&__inner {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin: $pad-xxxlarge 0;
|
||||
}
|
||||
|
||||
.info {
|
||||
|
@ -7,7 +7,7 @@ import softwareInterface, { ISoftware } from "./software";
|
||||
import hostQueryResult from "./campaign";
|
||||
import queryStatsInterface, { IQueryStats } from "./query_stats";
|
||||
import { ILicense } from "./config";
|
||||
import { MdmStatus } from "./mdm";
|
||||
import { MdmEnrollmentStatus } from "./mdm";
|
||||
|
||||
export default PropTypes.shape({
|
||||
created_at: PropTypes.string,
|
||||
@ -87,10 +87,10 @@ export interface IMunkiData {
|
||||
}
|
||||
|
||||
export interface IHostMdmData {
|
||||
enrollment_status: string;
|
||||
enrollment_status: MdmEnrollmentStatus;
|
||||
server_url: string;
|
||||
id: number;
|
||||
name: string;
|
||||
id?: number;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export interface IMunkiIssue {
|
||||
@ -203,10 +203,7 @@ export interface IHost {
|
||||
users: IHostUser[];
|
||||
device_users?: IDeviceUser[];
|
||||
munki?: IMunkiData;
|
||||
mdm: {
|
||||
enrollment_status: MdmStatus;
|
||||
server_url: string;
|
||||
};
|
||||
mdm: IHostMdmData;
|
||||
policies: IHostPolicy[];
|
||||
query_results?: unknown[];
|
||||
geolocation?: IGeoLocation;
|
||||
|
@ -13,17 +13,17 @@ export interface IMdmAppleBm {
|
||||
renew_date: string;
|
||||
}
|
||||
|
||||
export const MDM_STATUS = {
|
||||
export const MDM_ENROLLMENT_STATUS = {
|
||||
"On (manual)": "manual",
|
||||
"On (automatic)": "automatic",
|
||||
Off: "unenrolled",
|
||||
Pending: "pending",
|
||||
};
|
||||
|
||||
export type MdmStatus = keyof typeof MDM_STATUS;
|
||||
export type MdmEnrollmentStatus = keyof typeof MDM_ENROLLMENT_STATUS;
|
||||
|
||||
export interface IMdmStatusCardData {
|
||||
status: MdmStatus;
|
||||
status: MdmEnrollmentStatus;
|
||||
hosts: number;
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,12 @@
|
||||
.dashboard-page {
|
||||
background-color: $ui-off-white;
|
||||
|
||||
.data-error {
|
||||
&__inner {
|
||||
margin: $pad-xxxlarge 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__wrapper {
|
||||
background-color: $ui-off-white;
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
|
||||
import { IMdmStatusCardData, MDM_STATUS } from "interfaces/mdm";
|
||||
import { IMdmStatusCardData, MDM_ENROLLMENT_STATUS } from "interfaces/mdm";
|
||||
|
||||
import TextCell from "components/TableContainer/DataTable/TextCell";
|
||||
import TooltipWrapper from "components/TooltipWrapper";
|
||||
@ -77,7 +77,8 @@ const statusTableHeaders = [
|
||||
return (
|
||||
<ViewAllHostsLink
|
||||
queryParams={{
|
||||
mdm_enrollment_status: MDM_STATUS[cellProps.row.original.status],
|
||||
mdm_enrollment_status:
|
||||
MDM_ENROLLMENT_STATUS[cellProps.row.original.status],
|
||||
}}
|
||||
className="mdm-solution-link"
|
||||
platformLabelId={cellProps.row.original.selectedPlatformLabelId}
|
||||
|
@ -152,7 +152,7 @@ const allHostTableHeaders: IDataColumn[] = [
|
||||
),
|
||||
accessor: "display_name",
|
||||
Cell: (cellProps: ICellProps) => {
|
||||
if (cellProps.row.original.mdm?.enrollment_status === "Pending") {
|
||||
if (cellProps.row.original.mdm.enrollment_status === "Pending") {
|
||||
return (
|
||||
<>
|
||||
<span
|
||||
|
@ -34,7 +34,7 @@ import {
|
||||
import { IHost } from "interfaces/host";
|
||||
import { ILabel } from "interfaces/label";
|
||||
import { IMunkiIssuesAggregate } from "interfaces/macadmins";
|
||||
import { IMdmSolution, MDM_STATUS } from "interfaces/mdm";
|
||||
import { IMdmSolution, MDM_ENROLLMENT_STATUS } from "interfaces/mdm";
|
||||
import {
|
||||
formatOperatingSystemDisplayName,
|
||||
IOperatingSystemVersion,
|
||||
@ -1236,7 +1236,9 @@ const ManageHostsPage = ({
|
||||
const renderMDMEnrollmentFilterBlock = () => {
|
||||
if (!mdmEnrollmentStatus) return null;
|
||||
|
||||
const label = `MDM status: ${invert(MDM_STATUS)[mdmEnrollmentStatus]}`;
|
||||
const label = `MDM status: ${
|
||||
invert(MDM_ENROLLMENT_STATUS)[mdmEnrollmentStatus]
|
||||
}`;
|
||||
|
||||
// More narrow tooltip than other MDM tooltip
|
||||
const MDM_STATUS_PILL_TOOLTIP: Record<string, JSX.Element> = {
|
||||
|
@ -271,7 +271,7 @@ const DeviceUserPage = ({
|
||||
);
|
||||
|
||||
const renderEnrollMdmModal = () => {
|
||||
return host?.mdm?.enrollment_status === "Pending" ? (
|
||||
return host?.mdm.enrollment_status === "Pending" ? (
|
||||
<AutoEnrollMdmModal onCancel={toggleEnrollMdmModal} />
|
||||
) : (
|
||||
<ManualEnrollMdmModal
|
||||
@ -290,7 +290,7 @@ const DeviceUserPage = ({
|
||||
) : (
|
||||
<div className={`${baseClass} body-wrap`}>
|
||||
{host?.platform === "darwin" &&
|
||||
host?.mdm?.enrollment_status === "Off" && (
|
||||
host?.mdm.enrollment_status === "Off" && (
|
||||
<InfoBanner color="yellow" cta={turnOnMdmButton} pageLevel>
|
||||
Mobile device management (MDM) is off. MDM allows your
|
||||
organization to change settings and install software. This
|
||||
|
@ -38,7 +38,6 @@ import TabsWrapper from "components/TabsWrapper";
|
||||
import MainContent from "components/MainContent";
|
||||
import InfoBanner from "components/InfoBanner";
|
||||
import BackLink from "components/BackLink";
|
||||
import Icon from "components/Icon";
|
||||
|
||||
import {
|
||||
normalizeEmptyValues,
|
||||
@ -59,6 +58,7 @@ import PacksCard from "../cards/Packs";
|
||||
import SelectQueryModal from "./modals/SelectQueryModal";
|
||||
import PolicyDetailsModal from "../cards/Policies/HostPoliciesTable/PolicyDetailsModal";
|
||||
import OSPolicyModal from "./modals/OSPolicyModal";
|
||||
import UnenrollMdmModal from "./modals/UnenrollMdmModal";
|
||||
import TransferHostModal from "../../components/TransferHostModal";
|
||||
import DeleteHostModal from "../../components/DeleteHostModal";
|
||||
|
||||
@ -66,6 +66,7 @@ import parseOsVersion from "./modals/OSPolicyModal/helpers";
|
||||
import DeleteIcon from "../../../../../assets/images/icon-action-delete-14x14@2x.png";
|
||||
import QueryIcon from "../../../../../assets/images/icon-action-query-16x16@2x.png";
|
||||
import TransferIcon from "../../../../../assets/images/icon-action-transfer-16x16@2x.png";
|
||||
import CloseIcon from "../../../../../assets/images/icon-action-close-16x15@2x.png";
|
||||
|
||||
const baseClass = "host-details";
|
||||
|
||||
@ -131,15 +132,12 @@ const HostDetailsPage = ({
|
||||
isPremiumTier && (isGlobalAdmin || isGlobalMaintainer);
|
||||
|
||||
const canDeleteHost = (user: IUser, host: IHost) => {
|
||||
if (
|
||||
return (
|
||||
isGlobalAdmin ||
|
||||
isGlobalMaintainer ||
|
||||
permissionUtils.isTeamAdmin(user, host.team_id) ||
|
||||
permissionUtils.isTeamMaintainer(user, host.team_id)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
);
|
||||
};
|
||||
|
||||
const [showDeleteHostModal, setShowDeleteHostModal] = useState(false);
|
||||
@ -147,6 +145,7 @@ const HostDetailsPage = ({
|
||||
const [showQueryHostModal, setShowQueryHostModal] = useState(false);
|
||||
const [showPolicyDetailsModal, setPolicyDetailsModal] = useState(false);
|
||||
const [showOSPolicyModal, setShowOSPolicyModal] = useState(false);
|
||||
const [showUnenrollMdmModal, setShowUnenrollMdmModal] = useState(false);
|
||||
const [selectedPolicy, setSelectedPolicy] = useState<IHostPolicy | null>(
|
||||
null
|
||||
);
|
||||
@ -163,6 +162,7 @@ const HostDetailsPage = ({
|
||||
] = useState<IHostDiskEncryptionProps>({});
|
||||
const [usersState, setUsersState] = useState<{ username: string }[]>([]);
|
||||
const [usersSearchString, setUsersSearchString] = useState("");
|
||||
const [hideEditMdm, setHideEditMdm] = useState<boolean>(false);
|
||||
|
||||
const { data: fleetQueries, error: fleetQueriesError } = useQuery<
|
||||
IFleetQueriesResponse,
|
||||
@ -235,7 +235,7 @@ const HostDetailsPage = ({
|
||||
const refetchExtensions = () => {
|
||||
deviceMapping !== null && refetchDeviceMapping();
|
||||
macadmins !== null && refetchMacadmins();
|
||||
mdm !== null && refetchMdm();
|
||||
mdm?.enrollment_status !== null && refetchMdm();
|
||||
};
|
||||
|
||||
const {
|
||||
@ -333,6 +333,19 @@ const HostDetailsPage = ({
|
||||
}
|
||||
);
|
||||
|
||||
const canEditMdm = (() => {
|
||||
const userHasPermission =
|
||||
!!currentUser &&
|
||||
!!host &&
|
||||
(isGlobalAdmin ||
|
||||
isGlobalMaintainer ||
|
||||
permissionUtils.isTeamMaintainerOrTeamAdmin(currentUser, host.team_id));
|
||||
const hostEnrolled = ["On (automatic)", "On (manual)"].includes(
|
||||
host?.mdm.enrollment_status ?? ""
|
||||
);
|
||||
return userHasPermission && hostEnrolled;
|
||||
})();
|
||||
|
||||
const featuresConfig = host?.team_id
|
||||
? teams?.find((t) => t.id === host.team_id)?.features
|
||||
: config?.features;
|
||||
@ -410,6 +423,10 @@ const HostDetailsPage = ({
|
||||
setSelectedPolicy(null);
|
||||
}, [showPolicyDetailsModal, setPolicyDetailsModal, setSelectedPolicy]);
|
||||
|
||||
const toggleUnenrollMdmModal = useCallback(() => {
|
||||
setShowUnenrollMdmModal(!showUnenrollMdmModal);
|
||||
}, [showUnenrollMdmModal, setShowUnenrollMdmModal]);
|
||||
|
||||
const onCreateNewPolicy = () => {
|
||||
const { NEW_POLICY } = PATHS;
|
||||
host?.team_name
|
||||
@ -521,7 +538,6 @@ const HostDetailsPage = ({
|
||||
|
||||
const renderActionButtons = () => {
|
||||
const isOnline = host?.status === "online";
|
||||
|
||||
return (
|
||||
<div className={`${baseClass}__action-button-container`}>
|
||||
{canTransferTeam && (
|
||||
@ -562,6 +578,19 @@ const HostDetailsPage = ({
|
||||
You can’t query <br /> an offline host.
|
||||
</span>
|
||||
</ReactTooltip>
|
||||
{canEditMdm && !hideEditMdm && (
|
||||
<Button
|
||||
onClick={toggleUnenrollMdmModal}
|
||||
variant="text-icon"
|
||||
className={`${baseClass}__unenroll-host-from-mdm-button`}
|
||||
disabled={!isOnline}
|
||||
>
|
||||
<>
|
||||
Turn off MDM{" "}
|
||||
<img src={CloseIcon} alt="Unenroll host from mdm icon" />
|
||||
</>
|
||||
</Button>
|
||||
)}
|
||||
{currentUser && host && canDeleteHost(currentUser, host) && (
|
||||
<Button
|
||||
onClick={() => setShowDeleteHostModal(true)}
|
||||
@ -630,7 +659,7 @@ const HostDetailsPage = ({
|
||||
<div className={`${baseClass}__wrapper`}>
|
||||
<div className={`${baseClass}__header-links`}>
|
||||
{host?.platform === "darwin" &&
|
||||
host?.mdm?.enrollment_status === "Off" && (
|
||||
host?.mdm.enrollment_status === "Off" && (
|
||||
<InfoBanner color="yellow" pageLevel>
|
||||
To change settings and install software, ask the end user to
|
||||
follow the <strong>Turn on MDM</strong> instructions on their{" "}
|
||||
@ -766,6 +795,15 @@ const HostDetailsPage = ({
|
||||
osPolicyLabel={osPolicyLabel}
|
||||
/>
|
||||
)}
|
||||
{showUnenrollMdmModal && !!host && (
|
||||
<UnenrollMdmModal
|
||||
hostId={host.id}
|
||||
onClose={toggleUnenrollMdmModal}
|
||||
onSuccess={() => {
|
||||
setHideEditMdm(true);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</MainContent>
|
||||
);
|
||||
|
@ -0,0 +1,79 @@
|
||||
import React, { useState, useContext } from "react";
|
||||
|
||||
import DataError from "components/DataError";
|
||||
import Button from "components/buttons/Button";
|
||||
import Modal from "components/Modal";
|
||||
import { NotificationContext } from "context/notification";
|
||||
|
||||
import mdmAPI from "services/entities/mdm";
|
||||
|
||||
interface IUnenrollMdmModalProps {
|
||||
hostId: number;
|
||||
onClose: () => void;
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
const baseClass = "unenroll-mdm-modal";
|
||||
|
||||
const UnenrollMdmModal = ({
|
||||
hostId,
|
||||
onClose,
|
||||
onSuccess,
|
||||
}: IUnenrollMdmModalProps) => {
|
||||
const [requestState, setRequestState] = useState<
|
||||
undefined | "unenrolling" | "error"
|
||||
>(undefined);
|
||||
|
||||
const { renderFlash } = useContext(NotificationContext);
|
||||
|
||||
const submitUnenrollMdm = async () => {
|
||||
setRequestState("unenrolling");
|
||||
try {
|
||||
await mdmAPI.unenrollHostFromMdm(hostId, 5000);
|
||||
renderFlash("success", "Successfully turned off MDM.");
|
||||
onSuccess();
|
||||
onClose();
|
||||
} catch (unenrollMdmError: unknown) {
|
||||
console.log(unenrollMdmError);
|
||||
setRequestState("error");
|
||||
}
|
||||
};
|
||||
|
||||
const renderModalContent = () => {
|
||||
if (requestState === "error") {
|
||||
return <DataError />;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<p className={`${baseClass}__description`}>
|
||||
Settings configured by Fleet will be removed.
|
||||
<br />
|
||||
<br />
|
||||
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>
|
||||
<div className="modal-cta-wrap">
|
||||
<Button
|
||||
type="submit"
|
||||
variant="alert"
|
||||
onClick={submitUnenrollMdm}
|
||||
isLoading={requestState === "unenrolling"}
|
||||
>
|
||||
Turn off
|
||||
</Button>
|
||||
<Button onClick={onClose} variant="inverse-alert">
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal title="Turn off MDM" onExit={onClose} className={baseClass}>
|
||||
{renderModalContent()}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default UnenrollMdmModal;
|
@ -0,0 +1 @@
|
||||
export { default } from "./UnenrollMdmModal";
|
@ -11,7 +11,7 @@ interface IAboutProps {
|
||||
aboutData: { [key: string]: any };
|
||||
deviceMapping?: IDeviceUser[];
|
||||
munki?: IMunkiData | null;
|
||||
mdm?: IHostMdmData | null;
|
||||
mdm?: IHostMdmData;
|
||||
wrapFleetHelper: (helperFn: (value: any) => string, value: string) => string;
|
||||
}
|
||||
|
||||
@ -52,7 +52,7 @@ const About = ({
|
||||
};
|
||||
|
||||
const renderMdmData = () => {
|
||||
if (!mdm) {
|
||||
if (!mdm?.enrollment_status) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
|
@ -7,4 +7,14 @@ export default {
|
||||
const { DEVICE_USER_MDM_ENROLLMENT_PROFILE } = endpoints;
|
||||
return sendRequest("GET", DEVICE_USER_MDM_ENROLLMENT_PROFILE(token));
|
||||
},
|
||||
unenrollHostFromMdm: (hostId: number, timeout?: number) => {
|
||||
const { HOST_MDM_UNENROLL } = endpoints;
|
||||
return sendRequest(
|
||||
"PATCH",
|
||||
HOST_MDM_UNENROLL(hostId),
|
||||
undefined,
|
||||
undefined,
|
||||
timeout
|
||||
);
|
||||
},
|
||||
};
|
||||
|
@ -10,7 +10,8 @@ const sendRequest = async (
|
||||
method: "GET" | "POST" | "PATCH" | "DELETE" | "HEAD",
|
||||
path: string,
|
||||
data?: unknown,
|
||||
responseType: AxiosResponseType = "json"
|
||||
responseType: AxiosResponseType = "json",
|
||||
timeout?: number
|
||||
): Promise<any> => {
|
||||
const { origin } = global.window.location;
|
||||
|
||||
@ -23,6 +24,7 @@ const sendRequest = async (
|
||||
url,
|
||||
data,
|
||||
responseType,
|
||||
timeout,
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
|
@ -34,11 +34,15 @@ export default {
|
||||
LOGIN: `/${API_VERSION}/fleet/login`,
|
||||
LOGOUT: `/${API_VERSION}/fleet/logout`,
|
||||
MACADMINS: `/${API_VERSION}/fleet/macadmins`,
|
||||
// TODO: Clean up MDM endpoints to be consistent and up to date
|
||||
MDM_APPLE: `/${API_VERSION}/fleet/mdm/apple`,
|
||||
MDM_APPLE_BM: `/${API_VERSION}/fleet/mdm/apple_bm`,
|
||||
MDM_APPLE_BM_KEYS: `/${API_VERSION}/fleet/mdm/apple_bm/keys`,
|
||||
MDM_SUMMARY: `/${API_VERSION}/fleet/hosts/summary/mdm`,
|
||||
// Should below 2 endpoints be consistent?
|
||||
HOST_MDM: (id: number) => `/${API_VERSION}/fleet/hosts/${id}/mdm`,
|
||||
HOST_MDM_UNENROLL: (id: number) =>
|
||||
`/${API_VERSION}/fleet/mdm/hosts/${id}/unenroll`,
|
||||
ME: `/${API_VERSION}/fleet/me`,
|
||||
OS_VERSIONS: `/${API_VERSION}/fleet/os_versions`,
|
||||
OSQUERY_OPTIONS: `/${API_VERSION}/fleet/spec/osquery_options`,
|
||||
|
Loading…
Reference in New Issue
Block a user