Fleet UI: MDM pending hosts (#9427)

This commit is contained in:
RachelElysia 2023-01-24 12:55:43 -05:00 committed by GitHub
parent a1ccbf4c3b
commit c467aaad73
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 411 additions and 390 deletions

View File

@ -0,0 +1 @@
- Fleet Premium shows pending hosts on the dashboard and manage host page

View File

@ -32,6 +32,8 @@ const DEFAULT_HOST_MOCK: IHost = {
hardware_version: "",
hardware_serial: "",
computer_name: "9b20fc72a247",
mdm_enrollment_status: "Off",
mdm_server_url: "https://www.example.com/1",
public_ip: "",
primary_ip: "172.23.0.3",
primary_mac: "02:42:ac:17:00:03",

View File

@ -3,7 +3,7 @@ import { IMacadminsResponse } from "interfaces/host";
const DEFAULT_MAC_ADMINS_MOCK: IMacadminsResponse = {
macadmins: {
mobile_device_management: {
enrollment_status: "Enrolled (manual)",
enrollment_status: "On (manual)",
server_url: "https://kandji.com/2",
name: "Kandji",
id: 11,

View File

@ -24,7 +24,7 @@ const EmptyTable = ({
</div>
)}
<div className={`${baseClass}__inner`}>
{header && <h2>{header}</h2>}
{header && <h3>{header}</h3>}
{info && <p>{info}</p>}
{additionalInfo && <p>{additionalInfo}</p>}
</div>

View File

@ -13,8 +13,8 @@
flex-direction: column;
gap: $pad-small; // 4px from header to info text
h2,
h2 a {
h3,
h3 a {
text-align: center;
font-size: $small;
font-weight: $bold;

View File

@ -42,7 +42,7 @@
}
&__tip-text {
width: max-content;
max-width: 341px;
max-width: 360px;
padding: 6px;
color: $core-white;
background-color: $core-fleet-blue;
@ -58,6 +58,7 @@
opacity: 0;
transition: opacity 0.3s ease;
line-height: 1.375;
white-space: initial;
// invisible block to cover space so
// hover state can continue from text to bubble

View File

@ -1,6 +1,6 @@
import React from "react";
import PATHS from "router/paths";
import { Link, browserHistory } from "react-router";
import { Link } from "react-router";
import classnames from "classnames";
import Icon from "components/Icon";

View File

@ -7,6 +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";
export default PropTypes.shape({
created_at: PropTypes.string,
@ -202,7 +203,8 @@ export interface IHost {
users: IHostUser[];
device_users?: IDeviceUser[];
munki?: IMunkiData;
mdm?: IHostMdmData;
mdm_enrollment_status: MdmStatus;
mdm_server_url: string;
policies: IHostPolicy[];
query_results?: unknown[];
geolocation?: IGeoLocation;

View File

@ -13,8 +13,17 @@ export interface IMdmAppleBm {
renew_date: string;
}
export interface IMdmEnrollmentCardData {
status: "On (manual)" | "On (automatic)" | "Off";
export const MDM_STATUS = {
"On (manual)": "manual",
"On (automatic)": "automatic",
Off: "unenrolled",
Pending: "pending",
};
export type MdmStatus = keyof typeof MDM_STATUS;
export interface IMdmStatusCardData {
status: MdmStatus;
hosts: number;
}
@ -22,6 +31,7 @@ export interface IMdmAggregateStatus {
enrolled_manual_hosts_count: number;
enrolled_automated_hosts_count: number;
unenrolled_hosts_count: number;
pending_hosts_count?: number;
}
export interface IMdmSolution {
@ -31,15 +41,16 @@ export interface IMdmSolution {
hosts_count: number;
}
interface IMdmEnrollementStatus {
interface IMdmStatus {
enrolled_manual_hosts_count: number;
enrolled_automated_hosts_count: number;
unenrolled_hosts_count: number;
pending_hosts_count?: number;
hosts_count: number;
}
export interface IMdmSummaryResponse {
counts_updated_at: string;
mobile_device_management_enrollment_status: IMdmEnrollementStatus;
mobile_device_management_enrollment_status: IMdmStatus;
mobile_device_management_solution: IMdmSolution[] | null;
}

View File

@ -17,7 +17,7 @@ import {
IMunkiVersionsAggregate,
} from "interfaces/macadmins";
import {
IMdmEnrollmentCardData,
IMdmStatusCardData,
IMdmSolution,
IMdmSummaryResponse,
} from "interfaces/mdm";
@ -113,9 +113,7 @@ const DashboardPage = ({
const [showAddHostsModal, setShowAddHostsModal] = useState(false);
const [showOperatingSystemsUI, setShowOperatingSystemsUI] = useState(false);
const [showHostsUI, setShowHostsUI] = useState(false); // Hides UI on first load only
const [mdmEnrollmentData, setMdmEnrollmentData] = useState<
IMdmEnrollmentCardData[]
>([]);
const [mdmStatusData, setMdmStatusData] = useState<IMdmStatusCardData[]>([]);
const [mdmSolutions, setMdmSolutions] = useState<IMdmSolution[] | null>([]);
const [munkiIssuesData, setMunkiIssuesData] = useState<
@ -293,6 +291,7 @@ const DashboardPage = ({
enrolled_manual_hosts_count,
enrolled_automated_hosts_count,
unenrolled_hosts_count,
pending_hosts_count,
hosts_count,
} = mobile_device_management_enrollment_status;
@ -307,7 +306,7 @@ const DashboardPage = ({
whatToRetrieve={"MDM information"}
/>
);
setMdmEnrollmentData([
const statusData: IMdmStatusCardData[] = [
{
status: "On (manual)",
hosts: enrolled_manual_hosts_count,
@ -317,7 +316,13 @@ const DashboardPage = ({
hosts: enrolled_automated_hosts_count,
},
{ status: "Off", hosts: unenrolled_hosts_count },
]);
];
isPremiumTier &&
statusData.push({
status: "Pending",
hosts: pending_hosts_count || 0,
});
setMdmStatusData(statusData);
setMdmSolutions(mobile_device_management_solution);
setShowMdmCard(true);
},
@ -552,13 +557,13 @@ const DashboardPage = ({
titleDetail: mdmTitleDetail,
showTitle: !isMacAdminsFetching,
description: (
<p>MDM can be used to manage configuration on your workstations.</p>
<p>MDM is used to change settings and install software on your hosts.</p>
),
children: (
<Mdm
isFetching={isMdmFetching}
error={errorMdm}
mdmEnrollmentData={mdmEnrollmentData}
mdmStatusData={mdmStatusData}
mdmSolutions={mdmSolutions}
selectedPlatformLabelId={selectedPlatformLabelId}
/>

View File

@ -12,7 +12,7 @@ describe("MDM Card", () => {
<MDM
error={null}
isFetching={false}
mdmEnrollmentData={[]}
mdmStatusData={[]}
mdmSolutions={[
createMockMdmSolution(),
createMockMdmSolution({ id: 2 }),
@ -28,16 +28,17 @@ describe("MDM Card", () => {
<MDM
error={null}
isFetching={false}
mdmEnrollmentData={[
mdmStatusData={[
{ status: "On (automatic)", hosts: 10 },
{ status: "On (manual)", hosts: 5 },
{ status: "Off", hosts: 1 },
{ status: "Pending", hosts: 3 },
]}
mdmSolutions={[]}
/>
);
await user.click(screen.getByRole("tab", { name: "Enrollment" }));
await user.click(screen.getByRole("tab", { name: "Status" }));
expect(
screen.getByRole("row", {
@ -54,5 +55,10 @@ describe("MDM Card", () => {
name: /Off(.*?)1 host/i,
})
).toBeInTheDocument();
expect(
screen.getByRole("row", {
name: /Pending(.*?)3 host/i,
})
).toBeInTheDocument();
});
});

View File

@ -1,67 +1,67 @@
import React, { useState } from "react";
import { Tab, Tabs, TabList, TabPanel } from "react-tabs";
import { IMdmEnrollmentCardData, IMdmSolution } from "interfaces/mdm";
import { IMdmStatusCardData, IMdmSolution } from "interfaces/mdm";
import TabsWrapper from "components/TabsWrapper";
import TableContainer from "components/TableContainer";
import Spinner from "components/Spinner";
import TableDataError from "components/DataError";
import EmptyTable from "components/EmptyTable";
import CustomLink from "components/CustomLink";
import {
generateSolutionsTableHeaders,
generateSolutionsDataSet,
} from "./MDMSolutionsTableConfig";
import {
generateEnrollmentTableHeaders,
generateEnrollmentDataSet,
} from "./MDMEnrollmentTableConfig";
generateStatusTableHeaders,
generateStatusDataSet,
} from "./MDMStatusTableConfig";
interface IMdmCardProps {
error: Error | null;
isFetching: boolean;
mdmEnrollmentData: IMdmEnrollmentCardData[];
mdmStatusData: IMdmStatusCardData[];
mdmSolutions: IMdmSolution[] | null;
selectedPlatformLabelId?: number;
}
const DEFAULT_SORT_DIRECTION = "desc";
const SOLUTIONS_DEFAULT_SORT_HEADER = "hosts_count";
const ENROLLMENT_DEFAULT_SORT_DIRECTION = "asc";
const ENROLLMENT_DEFAULT_SORT_HEADER = "status";
const STATUS_DEFAULT_SORT_DIRECTION = "asc";
const STATUS_DEFAULT_SORT_HEADER = "status";
const PAGE_SIZE = 8;
const baseClass = "home-mdm";
const EmptyMdmEnrollment = (): JSX.Element => (
<div className={`${baseClass}__empty-mdm`}>
<h1>Unable to detect MDM enrollment</h1>
<p>
To see MDM versions, deploy&nbsp;
<a
href="https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer"
target="_blank"
rel="noopener noreferrer"
>
Fleet&apos;s osquery installer
</a>
.
</p>
</div>
const EmptyMdmStatus = (): JSX.Element => (
<EmptyTable
header="Unable to detect MDM enrollment"
info={
<>
To see MDM versions, deploy&nbsp;
<CustomLink
url="https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer"
newTab
text="Fleet's osquery installer"
/>
</>
}
/>
);
const EmptyMdmSolutions = (): JSX.Element => (
<div className={`${baseClass}__empty-mdm`}>
<h1>No MDM solutions detected</h1>
<p>
This report is updated every hour to protect the performance of your
devices.
</p>
</div>
<EmptyTable
header="No MDM solutions detected"
info="This report is updated every hour to protect the performance of your
devices."
/>
);
const Mdm = ({
isFetching,
error,
mdmEnrollmentData,
mdmStatusData,
mdmSolutions,
selectedPlatformLabelId,
}: IMdmCardProps): JSX.Element => {
@ -72,13 +72,13 @@ const Mdm = ({
};
const solutionsTableHeaders = generateSolutionsTableHeaders();
const enrollmentTableHeaders = generateEnrollmentTableHeaders();
const statusTableHeaders = generateStatusTableHeaders();
const solutionsDataSet = generateSolutionsDataSet(
mdmSolutions,
selectedPlatformLabelId
);
const enrollmentDataSet = generateEnrollmentDataSet(
mdmEnrollmentData,
const statusDataSet = generateStatusDataSet(
mdmStatusData,
selectedPlatformLabelId
);
@ -97,7 +97,7 @@ const Mdm = ({
<Tabs selectedIndex={navTabIndex} onSelect={onTabChange}>
<TabList>
<Tab>Solutions</Tab>
<Tab>Enrollment</Tab>
<Tab>Status</Tab>
</TabList>
<TabPanel>
{error ? (
@ -126,14 +126,14 @@ const Mdm = ({
<TableDataError card />
) : (
<TableContainer
columns={enrollmentTableHeaders}
data={enrollmentDataSet}
columns={statusTableHeaders}
data={statusDataSet}
isLoading={isFetching}
defaultSortHeader={ENROLLMENT_DEFAULT_SORT_HEADER}
defaultSortDirection={ENROLLMENT_DEFAULT_SORT_DIRECTION}
defaultSortHeader={STATUS_DEFAULT_SORT_HEADER}
defaultSortDirection={STATUS_DEFAULT_SORT_DIRECTION}
hideActionButton
resultsTitle={"MDM"}
emptyComponent={EmptyMdmEnrollment}
emptyComponent={EmptyMdmStatus}
showMarkAllPages={false}
isAllPagesSelected={false}
disableCount

View File

@ -1,145 +0,0 @@
import React from "react";
import { IMdmEnrollmentCardData } from "interfaces/mdm";
import TextCell from "components/TableContainer/DataTable/TextCell";
import TooltipWrapper from "components/TooltipWrapper";
import ViewAllHostsLink from "components/ViewAllHostsLink";
interface IMdmEnrollmentData extends IMdmEnrollmentCardData {
selectedPlatformLabelId?: number;
}
// NOTE: cellProps come from react-table
// more info here https://react-table.tanstack.com/docs/api/useTable#cell-properties
interface ICellProps {
cell: {
value: string;
};
row: {
original: IMdmEnrollmentData;
};
}
interface IHeaderProps {
column: {
title: string;
isSortedDesc: boolean;
};
}
interface IStringCellProps extends ICellProps {
cell: {
value: string;
};
}
interface IDataColumn {
title: string;
Header: ((props: IHeaderProps) => JSX.Element) | string;
accessor: string;
Cell: (props: ICellProps) => JSX.Element;
disableHidden?: boolean;
disableSortBy?: boolean;
}
const enrollmentTableHeaders = [
{
title: "Status",
Header: "Status",
disableSortBy: true,
accessor: "status",
Cell: (cellProps: IStringCellProps) => {
const tooltipText = (status: string): string => {
if (status === "On (automatic)") {
return `
<span>
Hosts automatically enrolled to an MDM solution <br />
using Apple Automated Device Enrollment (DEP) <br />
or Windows Autopilot. Administrators can block <br />
users from unenrolling these hosts from MDM.
</span>
`;
}
return `
<span>
Hosts manually enrolled to an MDM solution. Users <br />
can unenroll these hosts from MDM.
</span>
`;
};
if (cellProps.cell.value === "Off") {
return <TextCell value={cellProps.cell.value} />;
}
return (
<span className="name-container">
<TooltipWrapper tipContent={tooltipText(cellProps.cell.value)}>
{cellProps.cell.value}
</TooltipWrapper>
</span>
);
},
sortType: "caseInsensitive",
},
{
title: "Hosts",
Header: "Hosts",
disableSortBy: true,
accessor: "hosts",
Cell: (cellProps: ICellProps) => <TextCell value={cellProps.cell.value} />,
},
{
title: "",
Header: "",
disableSortBy: true,
disableGlobalFilter: true,
accessor: "linkToFilteredHosts",
Cell: (cellProps: IStringCellProps) => {
const statusParam = () => {
switch (cellProps.row.original.status) {
case "On (automatic)":
return "automatic";
case "On (manual)":
return "manual";
default:
return "unenrolled";
}
};
return (
<ViewAllHostsLink
queryParams={{ mdm_enrollment_status: statusParam() }}
className="mdm-solution-link"
platformLabelId={cellProps.row.original.selectedPlatformLabelId}
/>
);
},
disableHidden: true,
},
];
export const generateEnrollmentTableHeaders = (): IDataColumn[] => {
return enrollmentTableHeaders;
};
const enhanceEnrollmentData = (
enrollmentData: IMdmEnrollmentCardData[],
selectedPlatformLabelId?: number
): IMdmEnrollmentData[] => {
return Object.values(enrollmentData).map((data) => {
return {
...data,
selectedPlatformLabelId,
};
});
};
export const generateEnrollmentDataSet = (
enrollmentData: IMdmEnrollmentCardData[] | null,
selectedPlatformLabelId?: number
): IMdmEnrollmentData[] => {
if (!enrollmentData) {
return [];
}
return [...enhanceEnrollmentData(enrollmentData, selectedPlatformLabelId)];
};

View File

@ -0,0 +1,115 @@
import React from "react";
import { IMdmStatusCardData, MDM_STATUS } from "interfaces/mdm";
import TextCell from "components/TableContainer/DataTable/TextCell";
import TooltipWrapper from "components/TooltipWrapper";
import ViewAllHostsLink from "components/ViewAllHostsLink";
import { MDM_STATUS_TOOLTIP } from "utilities/constants";
interface IMdmStatusData extends IMdmStatusCardData {
selectedPlatformLabelId?: number;
}
// NOTE: cellProps come from react-table
// more info here https://react-table.tanstack.com/docs/api/useTable#cell-properties
interface ICellProps {
cell: {
value: string;
};
row: {
original: IMdmStatusData;
};
}
interface IHeaderProps {
column: {
title: string;
isSortedDesc: boolean;
};
}
interface IStringCellProps extends ICellProps {
cell: {
value: string;
};
}
interface IDataColumn {
title: string;
Header: ((props: IHeaderProps) => JSX.Element) | string;
accessor: string;
Cell: (props: ICellProps) => JSX.Element;
disableHidden?: boolean;
disableSortBy?: boolean;
}
const statusTableHeaders = [
{
title: "Status",
Header: "Status",
disableSortBy: true,
accessor: "status",
Cell: (cellProps: IStringCellProps) => (
<TooltipWrapper
position="top"
tipContent={MDM_STATUS_TOOLTIP[cellProps.cell.value]}
>
{cellProps.cell.value}
</TooltipWrapper>
),
sortType: "hasLength",
},
{
title: "Hosts",
Header: "Hosts",
disableSortBy: true,
accessor: "hosts",
Cell: (cellProps: ICellProps) => <TextCell value={cellProps.cell.value} />,
},
{
title: "",
Header: "",
disableSortBy: true,
disableGlobalFilter: true,
accessor: "linkToFilteredHosts",
Cell: (cellProps: IStringCellProps) => {
return (
<ViewAllHostsLink
queryParams={{
mdm_enrollment_status: MDM_STATUS[cellProps.row.original.status],
}}
className="mdm-solution-link"
platformLabelId={cellProps.row.original.selectedPlatformLabelId}
/>
);
},
disableHidden: true,
},
];
export const generateStatusTableHeaders = (): IDataColumn[] => {
return statusTableHeaders;
};
const enhanceStatusData = (
statusData: IMdmStatusCardData[],
selectedPlatformLabelId?: number
): IMdmStatusData[] => {
return Object.values(statusData).map((data) => {
return {
...data,
selectedPlatformLabelId,
};
});
};
export const generateStatusDataSet = (
statusData: IMdmStatusCardData[] | null,
selectedPlatformLabelId?: number
): IMdmStatusData[] => {
if (!statusData) {
return [];
}
return [...enhanceStatusData(statusData, selectedPlatformLabelId)];
};

View File

@ -5,23 +5,7 @@
.component__tabs-wrapper .table-container__header {
display: none;
}
&__empty-mdm {
width: 364px;
margin: $pad-large auto 0;
h1 {
font-size: $small;
font-weight: $bold;
margin-bottom: $pad-medium;
}
p {
color: $core-fleet-black;
font-weight: $regular;
font-size: $x-small;
margin: 0;
}
}
.data-table-block {
.data-table__table {
table-layout: fixed;

View File

@ -10,6 +10,9 @@ import TabsWrapper from "components/TabsWrapper";
import TableContainer from "components/TableContainer";
import Spinner from "components/Spinner";
import TableDataError from "components/DataError";
import EmptyTable from "components/EmptyTable";
import CustomLink from "components/CustomLink";
import munkiVersionsTableHeaders from "./MunkiVersionsTableConfig";
import munkiIssuesTableHeaders from "./MunkiIssuesTableConfig";
@ -25,33 +28,6 @@ const DEFAULT_SORT_HEADER = "hosts_count";
const PAGE_SIZE = 8;
const baseClass = "home-munki";
const EmptyMunkiIssues = (): JSX.Element => (
<div className={`${baseClass}__empty-munki`}>
<h2>No Munki issues detected</h2>
<p>
This report is updated every hour to protect the performance of your
devices.
</p>
</div>
);
const EmptyMunkiVersions = (): JSX.Element => (
<div className={`${baseClass}__empty-munki`}>
<h2>Unable to detect Munki versions</h2>
<p>
To see Munki versions, deploy&nbsp;
<a
href="https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer"
target="_blank"
rel="noopener noreferrer"
>
Fleet&apos;s osquery installer
</a>
.
</p>
</div>
);
const Munki = ({
errorMacAdmins,
isMacAdminsFetching,
@ -93,7 +69,13 @@ const Munki = ({
defaultSortDirection={DEFAULT_SORT_DIRECTION}
hideActionButton
resultsTitle={"Munki"}
emptyComponent={EmptyMunkiIssues}
emptyComponent={() => (
<EmptyTable
header="No Munki issues detected"
info="This report is updated every hour to protect the performance of your
devices."
/>
)}
showMarkAllPages={false}
isAllPagesSelected={false}
isClientSidePagination
@ -116,7 +98,22 @@ const Munki = ({
defaultSortDirection={DEFAULT_SORT_DIRECTION}
hideActionButton
resultsTitle={"Munki"}
emptyComponent={EmptyMunkiVersions}
emptyComponent={() => (
<EmptyTable
header="Unable to detect Munki versions"
info={
<>
To see Munki versions, deploy&nbsp;
<CustomLink
url="https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer"
text="Fleet's osquery installer"
newTab
/>
.
</>
}
/>
)}
showMarkAllPages={false}
isAllPagesSelected={false}
isClientSidePagination

View File

@ -8,22 +8,6 @@
.component__tabs-wrapper .table-container__header {
display: none;
}
&__empty-munki {
margin: $pad-medium auto 0;
h2 {
font-size: $small;
font-weight: $bold;
margin-bottom: $pad-medium;
}
p {
color: $core-fleet-black;
font-weight: $regular;
font-size: $x-small;
margin: 0;
}
}
.data-table-block {
.data-table__table {
table-layout: fixed;

View File

@ -19,6 +19,7 @@ import Spinner from "components/Spinner";
import TableDataError from "components/DataError";
import LastUpdatedText from "components/LastUpdatedText";
import CustomLink from "components/CustomLink";
import EmptyTable from "components/EmptyTable";
import generateTableHeaders from "./OperatingSystemsTableConfig";
@ -37,15 +38,13 @@ const PAGE_SIZE = 8;
const baseClass = "operating-systems";
const EmptyOperatingSystems = (platform: ISelectedPlatform): JSX.Element => (
<div className={`${baseClass}__empty-os`}>
<h1>{`No${
<EmptyTable
header={`No${
` ${PLATFORM_DISPLAY_NAMES[platform]}` || ""
} operating systems detected.`}</h1>
<p>
{`Did you add ${`${PLATFORM_DISPLAY_NAMES[platform]} ` || ""}hosts to
} operating systems detected.`}
info={`Did you add ${`${PLATFORM_DISPLAY_NAMES[platform]} ` || ""}hosts to
Fleet? Try again in about an hour as the system catches up.`}
</p>
</div>
/>
);
const OperatingSystems = ({

View File

@ -2,23 +2,6 @@
margin-top: $pad-large;
position: relative;
&__empty-os {
margin: 0 auto;
h1 {
font-size: $small;
font-weight: $bold;
margin-bottom: $pad-medium;
}
p {
color: $core-fleet-black;
font-weight: $regular;
font-size: $x-small;
margin: 0;
}
}
.data-table__wrapper {
overflow-x: auto;
}

View File

@ -188,7 +188,7 @@ const Mdm = (): JSX.Element => {
<>
<div className={`${baseClass}__section-description`}>
Connect Fleet to your Apple Business Manager account to
automatically enroll macOS hosts to Fleet when theyre first
automatically enroll macOS hosts to Fleet when they&apos;re first
unboxed.
</div>
<div className={`${baseClass}__section-instructions`}>
@ -204,7 +204,7 @@ const Mdm = (): JSX.Element => {
newTab
/>
<br />
If your organization doesnt have an account, select{" "}
If your organization doesn&apos;t have an account, select{" "}
<b>Enroll now</b>.
</p>
<p>

View File

@ -151,16 +151,45 @@ const allHostTableHeaders: IDataColumn[] = [
/>
),
accessor: "display_name",
Cell: (cellProps: ICellProps) => (
<LinkCell
value={cellProps.cell.value}
path={PATHS.HOST_DETAILS(cellProps.row.original.id)}
title={lastSeenTime(
cellProps.row.original.status,
cellProps.row.original.seen_time
)}
/>
),
Cell: (cellProps: ICellProps) => {
if (cellProps.row.original.mdm_enrollment_status === "Pending") {
return (
<>
<span
className="text-cell"
data-tip
data-for={`host__${cellProps.row.original.id}`}
>
{cellProps.cell.value}
</span>
<ReactTooltip
effect="solid"
backgroundColor="#3e4771"
id={`host__${cellProps.row.original.id}`}
data-html
>
<span className={`tooltip__tooltip-text`}>
This host was ordered using <br />
Apple Business Manager <br />
(ABM). You can&apos;t see host <br />
vitals until it&apos;s unboxed and <br />
automatically enrolls to Fleet.
</span>
</ReactTooltip>
</>
);
}
return (
<LinkCell
value={cellProps.cell.value}
path={PATHS.HOST_DETAILS(cellProps.row.original.id)}
title={lastSeenTime(
cellProps.row.original.status,
cellProps.row.original.seen_time
)}
/>
);
},
disableHidden: true,
},
{
@ -337,6 +366,53 @@ const allHostTableHeaders: IDataColumn[] = [
accessor: "primary_ip",
Cell: (cellProps: ICellProps) => <TextCell value={cellProps.cell.value} />,
},
{
title: "MDM status",
Header: (): JSX.Element => {
const titleWithToolTip = (
<TooltipWrapper
tipContent={`
The MDM server that updates settings on the host.<br/>
To filter by MDM server URL, head to the Dashboard page.
`}
>
MDM Status
</TooltipWrapper>
);
return <HeaderCell value={titleWithToolTip} disableSortBy />;
},
disableSortBy: true,
accessor: "mdm_enrollment_status",
Cell: (cellProps: ICellProps) => {
if (cellProps.cell.value)
return <TextCell value={cellProps.cell.value} />;
return <span className="text-muted">---</span>;
},
},
{
title: "MDM server URL",
Header: (): JSX.Element => {
const titleWithToolTip = (
<TooltipWrapper
tipContent={`
Settings can be updated remotely on hosts with MDM turned on.<br/>
To filter by MDM status, head to the Dashboard page.
`}
>
MDM server URL
</TooltipWrapper>
);
return <HeaderCell value={titleWithToolTip} disableSortBy />;
},
disableSortBy: true,
accessor: "mdm_server_url",
Cell: (cellProps: ICellProps) => {
if (cellProps.cell.value) {
return <TextCell value={cellProps.cell.value} />;
}
return <span className="text-muted">---</span>;
},
},
{
title: "Public IP address",
Header: (cellProps: IHeaderProps) => (
@ -499,6 +575,8 @@ const defaultHiddenColumns = [
"primary_mac",
"public_ip",
"cpu_type",
"mdm_server_url",
"mdm_enrollment_status",
"memory",
"uptime",
"uuid",
@ -531,7 +609,11 @@ const generateAvailableTableHeaders = (
}
// skip over column headers that are not shown in free admin/maintainer
} else if (permissionUtils.isFreeTier(config)) {
if (currentColumn.accessor === "team_name") {
if (
currentColumn.accessor === "team_name" ||
currentColumn.accessor === "mdm_server_url" ||
currentColumn.accessor === "mdm_enrollment_status"
) {
return columns;
}
} else if (

View File

@ -1,9 +1,8 @@
import React, { useState, useContext, useEffect, useCallback } from "react";
import { IconNames } from "components/icons";
import { useQuery } from "react-query";
import { InjectedRouter, Params } from "react-router/lib/Router";
import { RouteProps } from "react-router/lib/Route";
import { find, isEmpty, isEqual, omit } from "lodash";
import { find, isEmpty, isEqual, omit, invert } from "lodash";
import { format } from "date-fns";
import FileSaver from "file-saver";
@ -35,7 +34,7 @@ import {
import { IHost } from "interfaces/host";
import { ILabel } from "interfaces/label";
import { IMunkiIssuesAggregate } from "interfaces/macadmins";
import { IMdmSolution } from "interfaces/mdm";
import { IMdmSolution, MDM_STATUS } from "interfaces/mdm";
import {
formatOperatingSystemDisplayName,
IOperatingSystemVersion,
@ -1157,8 +1156,8 @@ const ManageHostsPage = ({
: `${name || ""}`
);
const TooltipDescription = (
<span className={`tooltip__tooltip-text`}>
{`Hosts with ${formatOperatingSystemDisplayName(name_only || name)}`},
<span className="tooltip__tooltip-text">
Hosts with {formatOperatingSystemDisplayName(name_only || name)},
<br />
{version && `${version} installed`}
</span>
@ -1218,7 +1217,7 @@ const ManageHostsPage = ({
const label = name ? `${name} ${server_url}` : `${server_url}`;
const TooltipDescription = (
<span className={`tooltip__tooltip-text`}>
<span className="tooltip__tooltip-text">
Host enrolled
{name !== "Unknown" && ` to ${name}`}
<br /> at {server_url}
@ -1237,54 +1236,52 @@ const ManageHostsPage = ({
const renderMDMEnrollmentFilterBlock = () => {
if (!mdmEnrollmentStatus) return null;
let label: string;
switch (mdmEnrollmentStatus) {
case "automatic":
label = "MDM status: On (automatic)";
break;
case "manual":
label = "MDM status: On (manual)";
break;
default:
label = "MDM status: Off";
}
const label = `MDM status: ${invert(MDM_STATUS)[mdmEnrollmentStatus]}`;
let TooltipDescription: JSX.Element;
switch (mdmEnrollmentStatus) {
case "automatic":
TooltipDescription = (
<span className={`tooltip__tooltip-text`}>
Hosts automatically enrolled in <br />
an MDM solution using Apple <br />
Automated Device Enrollment <br />
(DEP) or Windows Autopilot. <br />
Administrators can block users <br />
from unenrolling these hosts <br />
from MDM.
</span>
);
break;
case "manual":
TooltipDescription = (
<span className={`tooltip__tooltip-text`}>
Hosts manually enrolled to an <br />
MDM solution. Users can unenroll <br />
these hosts from MDM.
</span>
);
break;
default:
TooltipDescription = (
<span className={`tooltip__tooltip-text`}>
Hosts not enrolled to <br /> an MDM solution.
</span>
);
}
// More narrow tooltip than other MDM tooltip
const MDM_STATUS_PILL_TOOLTIP: Record<string, JSX.Element> = {
automatic: (
<span className="tooltip__tooltip-text">
MDM was turned on <br />
automatically using Apple <br />
Automated Device <br />
Enrollment (DEP) or <br />
Windows Autopilot. <br />
Administrators can block <br />
device users from turning
<br /> MDM off.
</span>
),
manual: (
<span className="tooltip__tooltip-text">
MDM was turned on <br />
manually. Device users <br />
can turn MDM off.
</span>
),
unenrolled: (
<span className="tooltip__tooltip-text">
Hosts with MDM off <br />
don&apos;t receive macOS <br />
settings and macOS <br />
update encouragement.
</span>
),
pending: (
<span className="tooltip__tooltip-text">
Hosts ordered using Apple <br />
Business Manager (ABM). <br />
They will automatically enroll <br />
to Fleet and turn on MDM <br />
when they&apos;re unboxed.
</span>
),
};
return (
<FilterPill
label={label}
tooltipDescription={TooltipDescription}
tooltipDescription={MDM_STATUS_PILL_TOOLTIP[mdmEnrollmentStatus]}
onClear={handleClearMDMEnrollmentFilter}
/>
);
@ -1296,7 +1293,7 @@ const ManageHostsPage = ({
<FilterPill
label={munkiIssueDetails.name}
tooltipDescription={
<span className={`tooltip__tooltip-text`}>
<span className="tooltip__tooltip-text">
Hosts that reported this Munki issue <br />
the last time Munki ran on each host.
</span>
@ -1310,7 +1307,7 @@ const ManageHostsPage = ({
const renderLowDiskSpaceFilterBlock = () => {
const TooltipDescription = (
<span className={`tooltip__tooltip-text`}>
<span className="tooltip__tooltip-text">
Hosts that have {lowDiskSpaceHosts} GB or less <br />
disk space available.
</span>

View File

@ -191,7 +191,9 @@
}
}
.device_mapping__cell {
.device_mapping__cell,
.mdm_enrollment_status__cell,
.mdm_server_url__cell {
.text-cell {
display: inline;
}

View File

@ -8,7 +8,7 @@ import CloseIcon from "../../../../../../assets/images/icon-close-vibrant-blue-1
interface IFilterPillProps {
label: string;
icon?: any; // TODO: figure out png image types
icon?: string;
tooltipDescription?: string | ReactNode;
className?: string;
onClear: () => void;

View File

@ -277,7 +277,7 @@ const DeviceUserPage = ({
) : (
<div className={`${baseClass} body-wrap`}>
{host?.platform === "darwin" &&
host?.mdm?.enrollment_status === "Unenrolled" && (
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

View File

@ -630,7 +630,7 @@ const HostDetailsPage = ({
<div className={`${baseClass}__wrapper`}>
<div className={`${baseClass}__header-links`}>
{host?.platform === "darwin" &&
host?.mdm?.enrollment_status === "Unenrolled" && (
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{" "}

View File

@ -2,9 +2,10 @@ import React from "react";
import ReactTooltip from "react-tooltip";
import HumanTimeDiffWithDateTip from "components/HumanTimeDiffWithDateTip";
import TooltipWrapper from "components/TooltipWrapper";
import { IHostMdmData, IMunkiData, IDeviceUser } from "interfaces/host";
import { humanHostLastRestart } from "utilities/helpers";
import { MDM_STATUS_TOOLTIP } from "utilities/constants";
interface IAboutProps {
aboutData: { [key: string]: any };
@ -51,15 +52,20 @@ const About = ({
};
const renderMdmData = () => {
if (!mdm || mdm.enrollment_status === "Off") {
if (!mdm) {
return null;
}
return (
<>
<div className="info-grid__block">
<span className="info-grid__header">MDM enrollment</span>
<span className="info-grid__header">MDM status</span>
<span className="info-grid__data">
{mdm.enrollment_status || "---"}
<TooltipWrapper
position="bottom"
tipContent={MDM_STATUS_TOOLTIP[mdm.enrollment_status]}
>
{mdm.enrollment_status}
</TooltipWrapper>
</span>
</div>
<div className="info-grid__block">

View File

@ -5,6 +5,7 @@ import { GITHUB_NEW_ISSUE_LINK } from "utilities/constants";
import TableContainer from "components/TableContainer";
import CustomLink from "components/CustomLink";
import EmptyTable from "components/EmptyTable";
import generateVulnTableHeaders from "./VulnTableConfig";
@ -18,19 +19,19 @@ interface IVulnerabilitiesProps {
const NoVulnsDetected = (): JSX.Element => {
return (
<div className={`${baseClass}__empty-vulnerabilities`}>
<div className="empty-vulnerabilities__inner">
<h1>No vulnerabilities detected for this software item.</h1>
<p>
<EmptyTable
header="No vulnerabilities detected for this software item."
info={
<>
Expecting to see vulnerabilities?{" "}
<CustomLink
url={GITHUB_NEW_ISSUE_LINK}
text="File an issue on GitHub"
newTab
/>
</p>
</div>
</div>
</>
}
/>
);
};

View File

@ -57,24 +57,4 @@
}
}
}
.vulnerabilities__empty-vulnerabilities {
.empty-vulnerabilities__inner {
display: flex;
flex-direction: column;
h1 {
font-size: $small;
font-weight: $bold;
margin-bottom: $pad-medium;
}
p {
color: $core-fleet-black;
font-weight: $regular;
font-size: $x-small;
margin: 0;
}
}
}
}

View File

@ -209,6 +209,14 @@ export const VULNERABLE_DROPDOWN_OPTIONS = [
},
];
// Keys from API
export const MDM_STATUS_TOOLTIP: Record<string, string> = {
"On (automatic)": `<span>MDM was turned on automatically using Apple Automated Device Enrollment (DEP) or Windows Autopilot. Administrators can block end users from turning MDM off.</span>`,
"On (manual)": `<span>MDM was turned on manually. End users can turn MDM off.</span>`,
Off: `<span>Hosts with MDM off don&apos;t receive macOS <br /> settings and macOS update encouragement.</span>`,
Pending: `<span>Hosts ordered via Apple Business Manager <br /> (ABM). These will automatically enroll to Fleet <br /> and turn on MDM when they&apos;re unboxed.</span>`,
};
export const DEFAULT_CREATE_USER_ERRORS = {
email: "",
name: "",