From 18852aae66756192a40808575cc96f3d79383ef0 Mon Sep 17 00:00:00 2001
From: RachelElysia <71795832+RachelElysia@users.noreply.github.com>
Date: Mon, 15 Aug 2022 18:47:07 -0400
Subject: [PATCH] Fleet UI: macOS dashboard MDM solutions (#7014)
Co-authored-by: gillespi314 <73313222+gillespi314@users.noreply.github.com>
---
changes/issue-6928-mdm-solutions | 1 +
.../DataTable/TextCell/TextCell.tsx | 4 +-
.../TableContainer/DataTable/_styles.scss | 4 +
frontend/interfaces/macadmins.ts | 8 +
frontend/pages/Homepage/Homepage.tsx | 2 +-
frontend/pages/Homepage/cards/MDM/MDM.tsx | 115 +++++++--
.../cards/MDM/MDMEnrollmentTableConfig.tsx | 125 ++++++++++
.../cards/MDM/MDMSolutionsTableConfig.tsx | 121 ++++++++++
.../Homepage/cards/MDM/MDMTableConfig.tsx | 54 -----
.../pages/Homepage/cards/MDM/_styles.scss | 47 ++--
frontend/pages/UserSettingsPage/_styles.scss | 5 -
.../hosts/ManageHostsPage/ManageHostsPage.tsx | 224 +++++++++++++++++-
.../pages/hosts/ManageHostsPage/_styles.scss | 10 +-
.../components/NoHosts/NoHosts.tsx | 10 +-
frontend/services/entities/host_count.ts | 21 +-
frontend/services/entities/hosts.ts | 67 +++++-
frontend/services/entities/macadmins.ts | 1 +
.../services/mock_service/mocks/responses.ts | 6 +-
frontend/utilities/helpers.ts | 14 +-
19 files changed, 690 insertions(+), 149 deletions(-)
create mode 100644 changes/issue-6928-mdm-solutions
create mode 100644 frontend/pages/Homepage/cards/MDM/MDMEnrollmentTableConfig.tsx
create mode 100644 frontend/pages/Homepage/cards/MDM/MDMSolutionsTableConfig.tsx
delete mode 100644 frontend/pages/Homepage/cards/MDM/MDMTableConfig.tsx
diff --git a/changes/issue-6928-mdm-solutions b/changes/issue-6928-mdm-solutions
new file mode 100644
index 000000000..b8628cd94
--- /dev/null
+++ b/changes/issue-6928-mdm-solutions
@@ -0,0 +1 @@
+* MacOS dashboard view includes MDM solutions table and filters for hosts by MDM
\ No newline at end of file
diff --git a/frontend/components/TableContainer/DataTable/TextCell/TextCell.tsx b/frontend/components/TableContainer/DataTable/TextCell/TextCell.tsx
index 073b39a2e..90a5e9cc3 100644
--- a/frontend/components/TableContainer/DataTable/TextCell/TextCell.tsx
+++ b/frontend/components/TableContainer/DataTable/TextCell/TextCell.tsx
@@ -3,7 +3,7 @@ import React from "react";
interface ITextCellProps {
value: string | number | boolean;
formatter?: (val: any) => string; // string, number, or null
- greyed?: string;
+ greyed?: boolean;
classes?: string;
}
@@ -20,7 +20,7 @@ const TextCell = ({
}
return (
-
+
{formatter(val)}
);
diff --git a/frontend/components/TableContainer/DataTable/_styles.scss b/frontend/components/TableContainer/DataTable/_styles.scss
index 66949d661..8e3187e06 100644
--- a/frontend/components/TableContainer/DataTable/_styles.scss
+++ b/frontend/components/TableContainer/DataTable/_styles.scss
@@ -186,6 +186,10 @@
min-width: 100%;
text-align: left;
}
+ .grey-cell {
+ color: $ui-fleet-black-50;
+ font-style: italic;
+ }
}
.highlight-on-hover:hover {
background-color: $ui-off-white;
diff --git a/frontend/interfaces/macadmins.ts b/frontend/interfaces/macadmins.ts
index e60a380cb..5d6643204 100644
--- a/frontend/interfaces/macadmins.ts
+++ b/frontend/interfaces/macadmins.ts
@@ -14,10 +14,18 @@ export interface IMDMAggregateStatus {
unenrolled_hosts_count: number;
}
+export interface IMDMSolution {
+ id: number;
+ name: string | null;
+ server_url: string;
+ hosts_count: number;
+}
+
export interface IMacadminAggregate {
macadmins: {
counts_updated_at: string;
munki_versions: IMunkiAggregate[];
mobile_device_management_enrollment_status: IMDMAggregateStatus;
+ mobile_device_management_solution: IMDMSolution[] | null;
};
}
diff --git a/frontend/pages/Homepage/Homepage.tsx b/frontend/pages/Homepage/Homepage.tsx
index 67e4f5af0..95fd7623e 100644
--- a/frontend/pages/Homepage/Homepage.tsx
+++ b/frontend/pages/Homepage/Homepage.tsx
@@ -274,7 +274,7 @@ const Homepage = (): JSX.Element => {
});
const MDMCard = useInfoCard({
- title: "Mobile device management (MDM) enrollment",
+ title: "Mobile device management (MDM)",
showTitle: showMDMUI,
description: (
diff --git a/frontend/pages/Homepage/cards/MDM/MDM.tsx b/frontend/pages/Homepage/cards/MDM/MDM.tsx
index eb8cc00bf..33888c906 100644
--- a/frontend/pages/Homepage/cards/MDM/MDM.tsx
+++ b/frontend/pages/Homepage/cards/MDM/MDM.tsx
@@ -1,14 +1,24 @@
import React, { useState } from "react";
import { useQuery } from "react-query";
+import { Tab, Tabs, TabList, TabPanel } from "react-tabs";
import macadminsAPI from "services/entities/macadmins";
-import { IMacadminAggregate, IDataTableMDMFormat } from "interfaces/macadmins";
+import {
+ IMacadminAggregate,
+ IDataTableMDMFormat,
+ IMDMSolution,
+} from "interfaces/macadmins";
+import TabsWrapper from "components/TabsWrapper";
import TableContainer from "components/TableContainer";
import Spinner from "components/Spinner";
import TableDataError from "components/DataError";
import LastUpdatedText from "components/LastUpdatedText";
-import generateTableHeaders from "./MDMTableConfig";
+import {
+ generateSolutionsTableHeaders,
+ generateSolutionsDataSet,
+} from "./MDMSolutionsTableConfig";
+import generateEnrollmentTableHeaders from "./MDMEnrollmentTableConfig";
interface IMDMCardProps {
showMDMUI: boolean;
@@ -18,13 +28,14 @@ interface IMDMCardProps {
}
const DEFAULT_SORT_DIRECTION = "desc";
-const DEFAULT_SORT_HEADER = "hosts_count";
+const SOLUTIONS_DEFAULT_SORT_HEADER = "hosts_count";
+const ENROLLMENT_DEFAULT_SORT_HEADER = "hosts";
const PAGE_SIZE = 8;
const baseClass = "home-mdm";
-const EmptyMDM = (): JSX.Element => (
+const EmptyMDMEnrollment = (): JSX.Element => (
-
Unable to detect MDM enrollment.
+
Unable to detect MDM enrollment
To see MDM versions, deploy
(
);
+const EmptyMDMSolutions = (): JSX.Element => (
+
+
No MDM solutions detected
+
+ This report is updated every hour to protect the performance of your
+ devices.
+
+
+);
+
const MDM = ({
showMDMUI,
currentTeamId,
setShowMDMUI,
setTitleDetail,
}: IMDMCardProps): JSX.Element => {
+ const [navTabIndex, setNavTabIndex] = useState(0);
const [formattedMDMData, setFormattedMDMData] = useState<
IDataTableMDMFormat[]
>([]);
+ const [solutions, setSolutions] = useState([]);
const { isFetching: isMDMFetching, error: errorMDM } = useQuery<
IMacadminAggregate,
@@ -58,6 +81,7 @@ const MDM = ({
const {
counts_updated_at,
mobile_device_management_enrollment_status,
+ mobile_device_management_solution,
} = data.macadmins;
const {
enrolled_manual_hosts_count,
@@ -84,13 +108,20 @@ const MDM = ({
},
{ status: "Unenrolled", hosts: unenrolled_hosts_count },
]);
+ setSolutions(mobile_device_management_solution);
},
onError: () => {
setShowMDMUI(true);
},
});
- const tableHeaders = generateTableHeaders();
+ const onTabChange = (index: number) => {
+ setNavTabIndex(index);
+ };
+
+ const solutionsTableHeaders = generateSolutionsTableHeaders();
+ const enrollmentTableHeaders = generateEnrollmentTableHeaders();
+ const solutionsDataSet = generateSolutionsDataSet(solutions);
// Renders opaque information as host information is loading
const opacity = showMDMUI ? { opacity: 1 } : { opacity: 0 };
@@ -103,26 +134,58 @@ const MDM = ({
)}
- {errorMDM ? (
-
- ) : (
-
- )}
+
+
+
+ Solutions
+ Enrollment
+
+
+ {errorMDM ? (
+
+ ) : (
+
+ )}
+
+
+ {errorMDM ? (
+
+ ) : (
+
+ )}
+
+
+
);
diff --git a/frontend/pages/Homepage/cards/MDM/MDMEnrollmentTableConfig.tsx b/frontend/pages/Homepage/cards/MDM/MDMEnrollmentTableConfig.tsx
new file mode 100644
index 000000000..7114c057a
--- /dev/null
+++ b/frontend/pages/Homepage/cards/MDM/MDMEnrollmentTableConfig.tsx
@@ -0,0 +1,125 @@
+import React from "react";
+import { Link } from "react-router";
+
+import { IDataTableMDMFormat } from "interfaces/macadmins";
+
+import PATHS from "router/paths";
+import TextCell from "components/TableContainer/DataTable/TextCell";
+import TooltipWrapper from "components/TooltipWrapper";
+import Chevron from "../../../../../assets/images/icon-chevron-right-9x6@2x.png";
+
+// 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: IDataTableMDMFormat;
+ };
+}
+
+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 === "Enrolled (automatic)") {
+ return `
+
+ Hosts automatically enrolled to an MDM solution
+ the first time the host is used. Administrators
+ might have a higher level of control over these
+ hosts.
+
+ `;
+ }
+ return `
+
+ Hosts manually enrolled to an MDM solution by a
+ user or administrator.
+
+ `;
+ };
+
+ if (cellProps.cell.value === "Unenrolled") {
+ return ;
+ }
+ return (
+
+
+ {cellProps.cell.value}
+
+
+ );
+ },
+ sortType: "caseInsensitive",
+ },
+ {
+ title: "Hosts",
+ Header: "Hosts",
+ disableSortBy: true,
+ accessor: "hosts",
+ Cell: (cellProps: ICellProps) => ,
+ },
+ {
+ title: "",
+ Header: "",
+ disableSortBy: true,
+ disableGlobalFilter: true,
+ accessor: "linkToFilteredHosts",
+ Cell: (cellProps: IStringCellProps) => {
+ const statusParam = () => {
+ switch (cellProps.row.original.status) {
+ case "Enrolled (automatic)":
+ return "automatic";
+ case "Enrolled (manual)":
+ return "manual";
+ default:
+ return "unenrolled";
+ }
+ };
+ return (
+
+ View all hosts{" "}
+
+
+ );
+ },
+ disableHidden: true,
+ },
+];
+
+const generateEnrollmentTableHeaders = (): IDataColumn[] => {
+ return enrollmentTableHeaders;
+};
+
+export default generateEnrollmentTableHeaders;
diff --git a/frontend/pages/Homepage/cards/MDM/MDMSolutionsTableConfig.tsx b/frontend/pages/Homepage/cards/MDM/MDMSolutionsTableConfig.tsx
new file mode 100644
index 000000000..1c6970565
--- /dev/null
+++ b/frontend/pages/Homepage/cards/MDM/MDMSolutionsTableConfig.tsx
@@ -0,0 +1,121 @@
+import React from "react";
+import { Link } from "react-router";
+
+import { IMDMSolution } from "interfaces/macadmins";
+
+import PATHS from "router/paths";
+import { greyCell } from "utilities/helpers";
+import HeaderCell from "components/TableContainer/DataTable/HeaderCell";
+import TextCell from "components/TableContainer/DataTable/TextCell";
+import Chevron from "../../../../../assets/images/icon-chevron-right-9x6@2x.png";
+
+// 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: IMDMSolution;
+ };
+}
+
+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 solutionsTableHeaders = [
+ {
+ title: "Name",
+ Header: "Name",
+ disableSortBy: true,
+ accessor: "name",
+ Cell: (cellProps: ICellProps) => (
+
+ ),
+ },
+ {
+ title: "Server URL",
+ Header: "Server URL",
+ disableSortBy: true,
+ accessor: "server_url",
+ Cell: (cellProps: ICellProps) => ,
+ },
+ {
+ title: "Hosts",
+ Header: (cellProps: IHeaderProps) => (
+
+ ),
+ accessor: "hosts_count",
+ Cell: (cellProps: ICellProps) => ,
+ },
+ {
+ title: "",
+ Header: "",
+ disableSortBy: true,
+ disableGlobalFilter: true,
+ accessor: "linkToFilteredHosts",
+ Cell: (cellProps: IStringCellProps) => {
+ return (
+
+ View all hosts{" "}
+
+
+ );
+ },
+ disableHidden: true,
+ },
+];
+
+export const generateSolutionsTableHeaders = (): IDataColumn[] => {
+ return solutionsTableHeaders;
+};
+
+const enhanceSolutionsData = (solutions: IMDMSolution[]): IMDMSolution[] => {
+ return Object.values(solutions).map((solution) => {
+ return {
+ id: solution.id,
+ name: solution.name || "Unknown",
+ server_url: solution.server_url,
+ hosts_count: solution.hosts_count,
+ };
+ });
+};
+
+export const generateSolutionsDataSet = (
+ solutions: IMDMSolution[] | null
+): IMDMSolution[] => {
+ if (!solutions) {
+ return [];
+ }
+ return [...enhanceSolutionsData(solutions)];
+};
+
+export default { generateSolutionsTableHeaders, generateSolutionsDataSet };
diff --git a/frontend/pages/Homepage/cards/MDM/MDMTableConfig.tsx b/frontend/pages/Homepage/cards/MDM/MDMTableConfig.tsx
deleted file mode 100644
index 34cc75ed1..000000000
--- a/frontend/pages/Homepage/cards/MDM/MDMTableConfig.tsx
+++ /dev/null
@@ -1,54 +0,0 @@
-import React from "react";
-
-import { IDataTableMDMFormat } from "interfaces/macadmins";
-
-import TextCell from "components/TableContainer/DataTable/TextCell";
-
-// 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: IDataTableMDMFormat;
- };
-}
-
-interface IHeaderProps {
- column: {
- title: string;
- isSortedDesc: boolean;
- };
-}
-
-interface IDataColumn {
- title: string;
- Header: ((props: IHeaderProps) => JSX.Element) | string;
- accessor: string;
- Cell: (props: ICellProps) => JSX.Element;
- disableHidden?: boolean;
- disableSortBy?: boolean;
-}
-
-const munkiTableHeaders = [
- {
- title: "Status",
- Header: "Status",
- disableSortBy: true,
- accessor: "status",
- Cell: (cellProps: ICellProps) => ,
- },
- {
- title: "Hosts",
- Header: "Hosts",
- accessor: "hosts",
- Cell: (cellProps: ICellProps) => ,
- },
-];
-
-const generateTableHeaders = (): IDataColumn[] => {
- return munkiTableHeaders;
-};
-
-export default generateTableHeaders;
diff --git a/frontend/pages/Homepage/cards/MDM/_styles.scss b/frontend/pages/Homepage/cards/MDM/_styles.scss
index 90bedbb97..fe6342e7f 100644
--- a/frontend/pages/Homepage/cards/MDM/_styles.scss
+++ b/frontend/pages/Homepage/cards/MDM/_styles.scss
@@ -2,14 +2,12 @@
margin-top: $pad-large;
position: relative;
- .data-table__wrapper {
- overflow-x: auto;
- }
.component__tabs-wrapper .table-container__header {
display: none;
}
&__empty-mdm {
- margin: $pad-medium auto 0;
+ width: 364px;
+ margin: $pad-large auto 0;
h1 {
font-size: $small;
@@ -36,39 +34,44 @@
table-layout: fixed;
thead {
- .name__header {
- width: 60%;
- }
- .version__header {
+ .name__header,
+ .status__header {
width: 30%;
- padding-right: 0;
}
- .hosts_count__header {
+ .server_url__header {
+ width: 30%;
+ }
+ .hosts_count__header,
+ .hosts__header {
border-right: 0;
padding-right: 0;
width: 60px;
}
- .id__header {
- padding: 0;
- border-left: 0;
- width: 40px;
+ .linkToFilteredHosts__header {
+ width: 140px;
}
}
tbody {
- .name__cell,
- .version__cell {
+ .name__cell {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
- .id__cell {
- padding: 0;
- width: 40px;
- }
- .vulnerabilities__cell {
+ .mdm-solution-link {
+ visibility: hidden;
+ text-overflow: none;
img {
- transform: scale(0.5);
+ vertical-align: text-top;
+ width: 16px;
+ }
+ }
+ tr {
+ &:hover {
+ .mdm-solution-link {
+ visibility: visible;
+ text-overflow: none;
+ }
}
}
}
diff --git a/frontend/pages/UserSettingsPage/_styles.scss b/frontend/pages/UserSettingsPage/_styles.scss
index f3bf3273f..7bef743cb 100644
--- a/frontend/pages/UserSettingsPage/_styles.scss
+++ b/frontend/pages/UserSettingsPage/_styles.scss
@@ -17,11 +17,6 @@
min-width: 542px;
}
- .grey-cell {
- color: $ui-fleet-black-50;
- font-style: italic;
- }
-
.modal__modal_container {
width: 400px;
a {
diff --git a/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx b/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx
index cfa648ee1..2db084b54 100644
--- a/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx
+++ b/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx
@@ -37,6 +37,7 @@ import {
import { IApiError } from "interfaces/errors";
import { IHost } from "interfaces/host";
import { ILabel, ILabelFormData } from "interfaces/label";
+import { IMDMSolution } from "interfaces/macadmins";
import { IOperatingSystemVersion } from "interfaces/operating_system";
import { IPolicy } from "interfaces/policy";
import { ISoftware } from "interfaces/software";
@@ -248,6 +249,10 @@ const ManageHostsPage = ({
const [softwareDetails, setSoftwareDetails] = useState(
null
);
+ const [
+ mdmSolutionDetails,
+ setMDMSolutionDetails,
+ ] = useState(null);
const [tableQueryData, setTableQueryData] = useState();
const [
currentQueryOptions,
@@ -268,6 +273,11 @@ const ManageHostsPage = ({
queryParams?.software_id !== undefined
? parseInt(queryParams?.software_id, 10)
: undefined;
+ const mdmId =
+ queryParams?.mdm_id !== undefined
+ ? parseInt(queryParams?.mdm_id, 10)
+ : undefined;
+ const mdmEnrollmentStatus = queryParams?.mdm_enrollment_status;
const operatingSystemId =
queryParams?.operating_system_id !== undefined
? parseInt(queryParams?.operating_system_id, 10)
@@ -445,11 +455,15 @@ const ManageHostsPage = ({
}
try {
- const { hosts: returnedHosts, software } = await hostsAPI.loadHosts(
- options
- );
+ const {
+ hosts: returnedHosts,
+ software,
+ mobile_device_management_solution,
+ } = await hostsAPI.loadHosts(options);
setHosts(returnedHosts);
software && setSoftwareDetails(software);
+ mobile_device_management_solution &&
+ setMDMSolutionDetails(mobile_device_management_solution);
} catch (error) {
console.error(error);
setHasHostErrors(true);
@@ -534,6 +548,8 @@ const ManageHostsPage = ({
policyId,
policyResponse,
softwareId,
+ mdmId,
+ mdmEnrollmentStatus,
operatingSystemId,
page: tableQueryData ? tableQueryData.pageIndex : 0,
perPage: tableQueryData ? tableQueryData.pageSize : 100,
@@ -641,10 +657,18 @@ const ManageHostsPage = ({
const handleClearSoftwareFilter = () => {
router.replace(PATHS.MANAGE_HOSTS);
- setCurrentTeam(undefined);
setSoftwareDetails(null);
};
+ const handleClearMDMSolutionFilter = () => {
+ router.replace(PATHS.MANAGE_HOSTS);
+ setMDMSolutionDetails(null);
+ };
+
+ const handleClearMDMEnrollmentFilter = () => {
+ router.replace(PATHS.MANAGE_HOSTS);
+ };
+
const handleTeamSelect = (teamId: number) => {
const { MANAGE_HOSTS } = PATHS;
const teamIdParam = getValidatedTeamId(
@@ -769,10 +793,25 @@ const ManageHostsPage = ({
newQueryParams.policy_response = policyResponse;
}
- if (softwareId && !policyId) {
+ if (softwareId && !policyId && !mdmId && !mdmEnrollmentStatus) {
newQueryParams.software_id = softwareId;
}
- if (operatingSystemId && !softwareId && !policyId) {
+
+ if (mdmId && !policyId && !softwareId && !mdmEnrollmentStatus) {
+ newQueryParams.mdm_id = mdmId;
+ }
+
+ if (mdmEnrollmentStatus && !policyId && !softwareId && !mdmId) {
+ newQueryParams.mdm_enrollment_status = mdmEnrollmentStatus;
+ }
+
+ if (
+ operatingSystemId &&
+ !softwareId &&
+ !policyId &&
+ !mdmEnrollmentStatus &&
+ !mdmId
+ ) {
newQueryParams.operating_system_id = operatingSystemId;
}
router.replace(
@@ -791,6 +830,8 @@ const ManageHostsPage = ({
policyId,
queryParams,
softwareId,
+ mdmId,
+ mdmEnrollmentStatus,
operatingSystemId,
sortBy,
]
@@ -1029,6 +1070,8 @@ const ManageHostsPage = ({
policyId,
policyResponse,
softwareId,
+ mdmId,
+ mdmEnrollmentStatus,
});
toggleTransferHostModal();
@@ -1074,6 +1117,8 @@ const ManageHostsPage = ({
policyId,
policyResponse,
softwareId,
+ mdmId,
+ mdmEnrollmentStatus,
});
refetchLabels();
@@ -1188,12 +1233,12 @@ const ManageHostsPage = ({
>
{buttonText}
@@ -1216,6 +1261,126 @@ const ManageHostsPage = ({
return null;
};
+ const renderMDMSolutionFilterBlock = () => {
+ if (mdmSolutionDetails) {
+ const { name, server_url } = mdmSolutionDetails;
+ const buttonText = `${name !== "Unknown" && name} ${server_url}`;
+ return (
+
+
+
+
+ {buttonText}
+
+
+
+
+
+ Host enrolled
+ {name !== "Unknown" && ` to ${name}`}
+
at {server_url}
+
+
+
+
+ );
+ }
+ return null;
+ };
+
+ const renderMDMEnrollmentFilterBlock = () => {
+ if (mdmEnrollmentStatus) {
+ const buttonText = () => {
+ switch (mdmEnrollmentStatus) {
+ case "automatic":
+ return "MDM enrolled (automatic)";
+ case "manual":
+ return "MDM enrolled (manual)";
+ default:
+ return "Unenrolled";
+ }
+ };
+ const tooltipText = () => {
+ switch (mdmEnrollmentStatus) {
+ case "automatic":
+ return (
+
+ Hosts automatically enrolled
+ to an MDM solution the first time
+ the host is used. Administrators
+ might have a higher level of control
+ over these hosts.
+
+ );
+ case "manual":
+ return (
+
+ Hosts manually enrolled to an
+ MDM solution by a user or
+ administrator.
+
+ );
+ default:
+ return (
+
+ Hosts not enrolled to
an MDM solution.
+
+ );
+ }
+ };
+ return (
+
+
+
+
+ {buttonText()}
+
+
+
+
+ {tooltipText()}
+
+
+
+ );
+ }
+ return null;
+ };
+
const renderEditColumnsModal = () => {
if (!config || !currentUser) {
return null;
@@ -1404,6 +1569,8 @@ const ManageHostsPage = ({
policyId,
policyResponse,
softwareId,
+ mdmId,
+ mdmEnrollmentStatus,
visibleColumns,
};
@@ -1471,22 +1638,47 @@ const ManageHostsPage = ({
selectedLabel &&
selectedLabel.type !== "all" &&
selectedLabel.type !== "status";
- if (policyId || softwareId || showSelectedLabel || operatingSystemId) {
+ if (
+ policyId ||
+ softwareId ||
+ showSelectedLabel ||
+ mdmId ||
+ mdmEnrollmentStatus ||
+ operatingSystemId
+ ) {
return (
{showSelectedLabel && renderHeaderLabelBlock()}
{!!policyId &&
!softwareId &&
+ !mdmId &&
+ !mdmEnrollmentStatus &&
!showSelectedLabel &&
renderPoliciesFilterBlock()}
{!!softwareId &&
!policyId &&
+ !mdmId &&
+ !mdmEnrollmentStatus &&
!showSelectedLabel &&
renderSoftwareFilterBlock()}
+ {!!mdmId &&
+ !policyId &&
+ !softwareId &&
+ !mdmEnrollmentStatus &&
+ !showSelectedLabel &&
+ renderMDMSolutionFilterBlock()}
+ {!!mdmEnrollmentStatus &&
+ !policyId &&
+ !softwareId &&
+ !mdmId &&
+ !showSelectedLabel &&
+ renderMDMEnrollmentFilterBlock()}
{!!operatingSystemId &&
!policyId &&
!softwareId &&
!showSelectedLabel &&
+ !mdmId &&
+ !mdmEnrollmentStatus &&
renderOSFilterBlock()}
);
@@ -1587,10 +1779,18 @@ const ManageHostsPage = ({
!isHostsLoading &&
teamSync
) {
- const { software_id, policy_id, operating_system_id } = queryParams || {};
- const includesSoftwareOrPolicyOrOSFilter = !!(
+ const {
+ software_id,
+ policy_id,
+ mdm_id,
+ mdm_enrollment_status,
+ operating_system_id,
+ } = queryParams || {};
+ const includesNameCardFilter = !!(
software_id ||
policy_id ||
+ mdm_id ||
+ mdm_enrollment_status ||
operating_system_id
);
@@ -1598,7 +1798,7 @@ const ManageHostsPage = ({
);
}
diff --git a/frontend/pages/hosts/ManageHostsPage/_styles.scss b/frontend/pages/hosts/ManageHostsPage/_styles.scss
index c440d3378..e54756620 100644
--- a/frontend/pages/hosts/ManageHostsPage/_styles.scss
+++ b/frontend/pages/hosts/ManageHostsPage/_styles.scss
@@ -199,6 +199,7 @@
&__policies-filter-block {
display: flex;
align-items: center;
+ gap: $pad-medium;
p {
font-size: $xx-small;
@@ -208,14 +209,15 @@
}
&__policies-filter-name-card,
- &__software-filter-name-card {
+ &__software-filter-name-card,
+ &__mdm-solution-filter-name-card,
+ &__mdm-enrollment-status-filter-name-card {
display: inline-flex;
align-items: center;
padding: 6px 12px;
border: 1px solid $ui-fleet-black-25;
border-radius: 4px;
box-shadow: none;
- margin-left: $pad-medium;
color: $core-fleet-black;
font-size: $xx-small;
font-weight: $bold;
@@ -240,10 +242,6 @@
}
}
- &__software-filter-name-card {
- margin-left: 0px;
- }
-
&__enroll-hosts {
padding: $pad-small;
margin-right: $pad-small;
diff --git a/frontend/pages/hosts/ManageHostsPage/components/NoHosts/NoHosts.tsx b/frontend/pages/hosts/ManageHostsPage/components/NoHosts/NoHosts.tsx
index 87f53ee58..47c97d409 100644
--- a/frontend/pages/hosts/ManageHostsPage/components/NoHosts/NoHosts.tsx
+++ b/frontend/pages/hosts/ManageHostsPage/components/NoHosts/NoHosts.tsx
@@ -9,7 +9,7 @@ import RoboDogImage from "../../../../../../assets/images/robo-dog-176x144@2x.pn
interface INoHostsProps {
toggleAddHostsModal: () => void;
canEnrollHosts?: boolean;
- includesSoftwareOrPolicyFilter?: boolean;
+ includesNameCardFilter?: boolean;
}
const baseClass = "no-hosts";
@@ -17,10 +17,10 @@ const baseClass = "no-hosts";
const NoHosts = ({
toggleAddHostsModal,
canEnrollHosts,
- includesSoftwareOrPolicyFilter,
+ includesNameCardFilter,
}: INoHostsProps): JSX.Element => {
const renderContent = () => {
- if (includesSoftwareOrPolicyFilter) {
+ if (includesNameCardFilter) {
return (
No hosts match the current criteria
@@ -64,9 +64,7 @@ const NoHosts = ({
return (
- {!includesSoftwareOrPolicyFilter && (
-
- )}
+ {!includesNameCardFilter &&
}
{renderContent()}
diff --git a/frontend/services/entities/host_count.ts b/frontend/services/entities/host_count.ts
index 41f0f6366..e23ff0c54 100644
--- a/frontend/services/entities/host_count.ts
+++ b/frontend/services/entities/host_count.ts
@@ -17,6 +17,8 @@ export interface IHostCountLoadOptions {
policyId?: number;
policyResponse?: string;
softwareId?: number;
+ mdmId?: number;
+ mdmEnrollmentStatus?: string;
operatingSystemId?: number;
}
@@ -30,6 +32,8 @@ export default {
const policyResponse = options?.policyResponse || null;
const selectedLabels = options?.selectedLabels || [];
const softwareId = options?.softwareId || null;
+ const mdmId = options?.mdmId || null;
+ const mdmEnrollmentStatus = options?.mdmEnrollmentStatus || null;
const operatingSystemId = options?.operatingSystemId || null;
const labelPrefix = "labels/";
@@ -70,7 +74,22 @@ export default {
queryString += `&software_id=${softwareId}`;
}
- if (!label && !policyId && !softwareId && operatingSystemId) {
+ if (!label && !policyId && mdmId) {
+ queryString += `&mdm_id=${mdmId}`;
+ }
+
+ if (!label && !policyId && mdmEnrollmentStatus) {
+ queryString += `&mdm_enrollment_status=${mdmEnrollmentStatus}`;
+ }
+
+ if (
+ !label &&
+ !policyId &&
+ !softwareId &&
+ !mdmId &&
+ !mdmEnrollmentStatus &&
+ operatingSystemId
+ ) {
queryString += `&operating_system_id=${operatingSystemId}`;
}
diff --git a/frontend/services/entities/hosts.ts b/frontend/services/entities/hosts.ts
index 8defc9432..26241f770 100644
--- a/frontend/services/entities/hosts.ts
+++ b/frontend/services/entities/hosts.ts
@@ -19,6 +19,8 @@ export interface ILoadHostsOptions {
policyId?: number;
policyResponse?: string;
softwareId?: number;
+ mdmId?: number;
+ mdmEnrollmentStatus?: string;
operatingSystemId?: number;
device_mapping?: boolean;
columns?: string;
@@ -35,6 +37,8 @@ export interface IExportHostsOptions {
policyId?: number;
policyResponse?: string;
softwareId?: number;
+ mdmId?: number;
+ mdmEnrollmentStatus?: string;
operatingSystemId?: number;
device_mapping?: boolean;
columns?: string;
@@ -101,9 +105,37 @@ const getPolicyParams = (
const getSoftwareParam = (
label?: string,
policyId?: number,
- softwareId?: number
+ softwareId?: number,
+ mdmId?: number,
+ mdmEnrollmentStatus?: string
) => {
- return label === undefined && policyId === undefined ? softwareId : undefined;
+ return !label && !policyId && !mdmId && !mdmEnrollmentStatus
+ ? softwareId
+ : undefined;
+};
+
+const getMDMSolutionParam = (
+ label?: string,
+ policyId?: number,
+ softwareId?: number,
+ mdmId?: number,
+ mdmEnrollmentStatus?: string
+) => {
+ return !label && !policyId && !softwareId && !mdmEnrollmentStatus
+ ? mdmId
+ : undefined;
+};
+
+const getMDMEnrollmentStatusParam = (
+ label?: string,
+ policyId?: number,
+ softwareId?: number,
+ mdmId?: number,
+ mdmEnrollmentStatus?: string
+) => {
+ return !label && !policyId && !softwareId && !mdmId
+ ? mdmEnrollmentStatus
+ : undefined;
};
const getOperatingSystemParam = (
@@ -156,6 +188,8 @@ export default {
const policyId = options?.policyId || null;
const policyResponse = options?.policyResponse || "passing";
const softwareId = options?.softwareId || null;
+ const mdmId = options?.mdmId || null;
+ const mdmEnrollmentStatus = options?.mdmEnrollmentStatus || null;
const visibleColumns = options?.visibleColumns || null;
if (!sortBy.length) {
@@ -186,7 +220,7 @@ export default {
path += `&team_id=${teamId}`;
}
- // Label OR policy_id OR software_id are valid filters.
+ // label OR policy_id OR software_id OR mdm_id OR mdm_enrollment_status are valid filters.
if (label) {
const lid = label.substr(labelPrefix.length);
path += `&label_id=${parseInt(lid, 10)}`;
@@ -197,10 +231,18 @@ export default {
path += `&policy_response=${policyResponse}`;
}
- if (!label && !policyId && softwareId) {
+ if (!label && !policyId && !mdmId && !mdmEnrollmentStatus && softwareId) {
path += `&software_id=${softwareId}`;
}
+ if (!label && !policyId && !softwareId && !mdmEnrollmentStatus && mdmId) {
+ path += `&mdm_id=${mdmId}`;
+ }
+
+ if (!label && !policyId && !softwareId && !mdmId && mdmEnrollmentStatus) {
+ path += `&mdm_enrollment_status=${mdmEnrollmentStatus}`;
+ }
+
if (visibleColumns) {
path += `&columns=${visibleColumns}`;
}
@@ -217,6 +259,8 @@ export default {
policyId,
policyResponse = "passing",
softwareId,
+ mdmId,
+ mdmEnrollmentStatus,
operatingSystemId,
device_mapping,
selectedLabels,
@@ -237,6 +281,20 @@ export default {
policy_id: policyParams.policy_id,
policy_response: policyParams.policy_response,
software_id: getSoftwareParam(label, policyId, softwareId),
+ mdm_id: getMDMSolutionParam(
+ label,
+ policyId,
+ softwareId,
+ mdmId,
+ mdmEnrollmentStatus
+ ),
+ mdm_enrollment_status: getMDMEnrollmentStatusParam(
+ label,
+ policyId,
+ softwareId,
+ mdmId,
+ mdmEnrollmentStatus
+ ),
operating_system_id: getOperatingSystemParam(
label,
policyId,
@@ -248,6 +306,7 @@ export default {
};
const queryString = buildQueryStringFromParams(queryParams);
+
const endpoint = getHostEndpoint(selectedLabels);
const path = `${endpoint}?${queryString}`;
return sendRequest("GET", path);
diff --git a/frontend/services/entities/macadmins.ts b/frontend/services/entities/macadmins.ts
index 31bdb5c7c..8ed654919 100644
--- a/frontend/services/entities/macadmins.ts
+++ b/frontend/services/entities/macadmins.ts
@@ -8,6 +8,7 @@ export default {
const { MACADMINS } = endpoints;
const queryString = buildQueryStringFromParams({ team_id: teamId });
const path = `${MACADMINS}?${queryString}`;
+
return sendRequest("GET", path);
},
};
diff --git a/frontend/services/mock_service/mocks/responses.ts b/frontend/services/mock_service/mocks/responses.ts
index c3cbdcb5b..00b51d476 100644
--- a/frontend/services/mock_service/mocks/responses.ts
+++ b/frontend/services/mock_service/mocks/responses.ts
@@ -358,4 +358,8 @@ const labels = {
],
};
-export default { count, hosts, labels };
+export default {
+ count,
+ hosts,
+ labels,
+};
diff --git a/frontend/utilities/helpers.ts b/frontend/utilities/helpers.ts
index 3a10d2165..d3df030d4 100644
--- a/frontend/utilities/helpers.ts
+++ b/frontend/utilities/helpers.ts
@@ -547,16 +547,12 @@ export const generateTeam = (
return `${teams.length + 1} teams`; // global role and one or more teams
};
-export const greyCell = (roleOrTeamText: string): string => {
- const GREYED_TEXT = ["Global", "Unassigned", "Various", "No Team"];
+export const greyCell = (roleOrTeamText: string): boolean => {
+ const GREYED_TEXT = ["Global", "Unassigned", "Various", "No Team", "Unknown"];
- if (
- GREYED_TEXT.includes(roleOrTeamText) ||
- roleOrTeamText.includes(" teams")
- ) {
- return "grey-cell";
- }
- return "";
+ return (
+ GREYED_TEXT.includes(roleOrTeamText) || roleOrTeamText.includes(" teams")
+ );
};
const setupData = (formData: any) => {