Fleet UI: macOS dashboard MDM solutions (#7014)

Co-authored-by: gillespi314 <73313222+gillespi314@users.noreply.github.com>
This commit is contained in:
RachelElysia 2022-08-15 18:47:07 -04:00 committed by GitHub
parent 8d4ad6ce9f
commit 18852aae66
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 690 additions and 149 deletions

View File

@ -0,0 +1 @@
* MacOS dashboard view includes MDM solutions table and filters for hosts by MDM

View File

@ -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 (
<span className={`text-cell ${classes} ${greyed || ""}`}>
<span className={`text-cell ${classes} ${greyed && "grey-cell"}`}>
{formatter(val)}
</span>
);

View File

@ -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;

View File

@ -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;
};
}

View File

@ -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: (
<p>

View File

@ -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 => (
<div className={`${baseClass}__empty-mdm`}>
<h1>Unable to detect MDM enrollment.</h1>
<h1>Unable to detect MDM enrollment</h1>
<p>
To see MDM versions, deploy&nbsp;
<a
@ -39,15 +50,27 @@ const EmptyMDM = (): JSX.Element => (
</div>
);
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>
);
const MDM = ({
showMDMUI,
currentTeamId,
setShowMDMUI,
setTitleDetail,
}: IMDMCardProps): JSX.Element => {
const [navTabIndex, setNavTabIndex] = useState<number>(0);
const [formattedMDMData, setFormattedMDMData] = useState<
IDataTableMDMFormat[]
>([]);
const [solutions, setSolutions] = useState<IMDMSolution[] | null>([]);
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 = ({
</div>
)}
<div style={opacity}>
{errorMDM ? (
<TableDataError card />
) : (
<TableContainer
columns={tableHeaders}
data={formattedMDMData}
isLoading={isMDMFetching}
defaultSortHeader={DEFAULT_SORT_HEADER}
defaultSortDirection={DEFAULT_SORT_DIRECTION}
hideActionButton
resultsTitle={"MDM"}
emptyComponent={EmptyMDM}
showMarkAllPages={false}
isAllPagesSelected={false}
disableCount
disableActionButton
disablePagination
pageSize={PAGE_SIZE}
/>
)}
<TabsWrapper>
<Tabs selectedIndex={navTabIndex} onSelect={onTabChange}>
<TabList>
<Tab>Solutions</Tab>
<Tab>Enrollment</Tab>
</TabList>
<TabPanel>
{errorMDM ? (
<TableDataError card />
) : (
<TableContainer
columns={solutionsTableHeaders}
data={solutionsDataSet}
isLoading={isMDMFetching}
defaultSortHeader={SOLUTIONS_DEFAULT_SORT_HEADER}
defaultSortDirection={DEFAULT_SORT_DIRECTION}
hideActionButton
resultsTitle={"MDM"}
emptyComponent={EmptyMDMSolutions}
showMarkAllPages={false}
isAllPagesSelected={false}
isClientSidePagination
disableCount
disableActionButton
pageSize={PAGE_SIZE}
/>
)}
</TabPanel>
<TabPanel>
{errorMDM ? (
<TableDataError card />
) : (
<TableContainer
columns={enrollmentTableHeaders}
data={formattedMDMData}
isLoading={isMDMFetching}
defaultSortHeader={ENROLLMENT_DEFAULT_SORT_HEADER}
defaultSortDirection={DEFAULT_SORT_DIRECTION}
hideActionButton
resultsTitle={"MDM"}
emptyComponent={EmptyMDMEnrollment}
showMarkAllPages={false}
isAllPagesSelected={false}
disableCount
disableActionButton
disablePagination
pageSize={PAGE_SIZE}
/>
)}
</TabPanel>
</Tabs>
</TabsWrapper>
</div>
</div>
);

View File

@ -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 `
<span>
Hosts automatically enrolled to an MDM solution <br/>
the first time the host is used. Administrators <br />
might have a higher level of control over these <br />
hosts.
</span>
`;
}
return `
<span>
Hosts manually enrolled to an MDM solution by a<br />
user or administrator.
</span>
`;
};
if (cellProps.cell.value === "Unenrolled") {
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 "Enrolled (automatic)":
return "automatic";
case "Enrolled (manual)":
return "manual";
default:
return "unenrolled";
}
};
return (
<Link
to={`${PATHS.MANAGE_HOSTS}?mdm_enrollment_status=${statusParam()}`}
className={`mdm-solution-link`}
>
View all hosts{" "}
<img alt="link to hosts filtered by MDM solution" src={Chevron} />
</Link>
);
},
disableHidden: true,
},
];
const generateEnrollmentTableHeaders = (): IDataColumn[] => {
return enrollmentTableHeaders;
};
export default generateEnrollmentTableHeaders;

View File

@ -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) => (
<TextCell
greyed={greyCell(cellProps.cell.value)}
value={cellProps.cell.value}
/>
),
},
{
title: "Server URL",
Header: "Server URL",
disableSortBy: true,
accessor: "server_url",
Cell: (cellProps: ICellProps) => <TextCell value={cellProps.cell.value} />,
},
{
title: "Hosts",
Header: (cellProps: IHeaderProps) => (
<HeaderCell
value={cellProps.column.title}
isSortedDesc={cellProps.column.isSortedDesc}
/>
),
accessor: "hosts_count",
Cell: (cellProps: ICellProps) => <TextCell value={cellProps.cell.value} />,
},
{
title: "",
Header: "",
disableSortBy: true,
disableGlobalFilter: true,
accessor: "linkToFilteredHosts",
Cell: (cellProps: IStringCellProps) => {
return (
<Link
to={`${PATHS.MANAGE_HOSTS}?mdm_id=${cellProps.row.original.id}`}
className={`mdm-solution-link`}
>
View all hosts{" "}
<img alt="link to hosts filtered by MDM solution" src={Chevron} />
</Link>
);
},
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 };

View File

@ -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) => <TextCell value={cellProps.cell.value} />,
},
{
title: "Hosts",
Header: "Hosts",
accessor: "hosts",
Cell: (cellProps: ICellProps) => <TextCell value={cellProps.cell.value} />,
},
];
const generateTableHeaders = (): IDataColumn[] => {
return munkiTableHeaders;
};
export default generateTableHeaders;

View File

@ -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;
}
}
}
}

View File

@ -17,11 +17,6 @@
min-width: 542px;
}
.grey-cell {
color: $ui-fleet-black-50;
font-style: italic;
}
.modal__modal_container {
width: 400px;
a {

View File

@ -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<ISoftware | null>(
null
);
const [
mdmSolutionDetails,
setMDMSolutionDetails,
] = useState<IMDMSolution | null>(null);
const [tableQueryData, setTableQueryData] = useState<ITableQueryProps>();
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}
<Button
className={`${baseClass}__clear-policies-filter`}
className={`${baseClass}__clear-software-filter`}
onClick={handleClearSoftwareFilter}
variant={"small-text-icon"}
title={buttonText}
>
<img src={CloseIcon} alt="Remove policy filter" />
<img src={CloseIcon} alt="Remove software filter" />
</Button>
</div>
</span>
@ -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 (
<div className={`${baseClass}__mdm-solution-filter-block`}>
<div>
<span
data-tip
data-for="mdm-solution-filter-tooltip"
data-tip-disable={!name || !server_url}
>
<div
className={`${baseClass}__mdm-solution-filter-name-card tooltip`}
>
{buttonText}
<Button
className={`${baseClass}__clear-mdm-solution-filter`}
onClick={handleClearMDMSolutionFilter}
variant={"small-text-icon"}
title={buttonText}
>
<img src={CloseIcon} alt="Remove MDM solution filter" />
</Button>
</div>
</span>
<ReactTooltip
place="bottom"
effect="solid"
backgroundColor="#3e4771"
id="mdm-solution-filter-tooltip"
data-html
>
<span className={`tooltip__tooltip-text`}>
Host enrolled
{name !== "Unknown" && ` to ${name}`}
<br /> at {server_url}
</span>
</ReactTooltip>
</div>
</div>
);
}
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 (
<span className={`tooltip__tooltip-text`}>
Hosts automatically enrolled <br />
to an MDM solution the first time <br />
the host is used. Administrators <br />
might have a higher level of control <br />
over these hosts.
</span>
);
case "manual":
return (
<span className={`tooltip__tooltip-text`}>
Hosts manually enrolled to an <br />
MDM solution by a user or <br />
administrator.
</span>
);
default:
return (
<span className={`tooltip__tooltip-text`}>
Hosts not enrolled to <br /> an MDM solution.
</span>
);
}
};
return (
<div className={`${baseClass}__mdm-enrollment-status-filter-block`}>
<div>
<span data-tip data-for="mdm-enrollment-status-filter-tooltip">
<div
className={`${baseClass}__mdm-enrollment-status-filter-name-card tooltip`}
>
{buttonText()}
<Button
className={`${baseClass}__clear-mdm-enrollment-status-filter`}
onClick={handleClearMDMEnrollmentFilter}
variant={"small-text-icon"}
title={buttonText()}
>
<img src={CloseIcon} alt="Remove MDM enrollment filter" />
</Button>
</div>
</span>
<ReactTooltip
place="bottom"
effect="solid"
backgroundColor="#3e4771"
id="mdm-enrollment-status-filter-tooltip"
data-html
>
{tooltipText()}
</ReactTooltip>
</div>
</div>
);
}
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 (
<div className={`${baseClass}__labels-active-filter-wrap`}>
{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()}
</div>
);
@ -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 = ({
<NoHosts
toggleAddHostsModal={toggleAddHostsModal}
canEnrollHosts={canEnrollHosts}
includesSoftwareOrPolicyFilter={includesSoftwareOrPolicyOrOSFilter}
includesNameCardFilter={includesNameCardFilter}
/>
);
}

View File

@ -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;

View File

@ -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 (
<div>
<h1>No hosts match the current criteria</h1>
@ -64,9 +64,7 @@ const NoHosts = ({
return (
<div className={`${baseClass}`}>
<div className={`${baseClass}__inner`}>
{!includesSoftwareOrPolicyFilter && (
<img src={RoboDogImage} alt="No Hosts" />
)}
{!includesNameCardFilter && <img src={RoboDogImage} alt="No Hosts" />}
{renderContent()}
</div>
</div>

View File

@ -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}`;
}

View File

@ -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);

View File

@ -8,6 +8,7 @@ export default {
const { MACADMINS } = endpoints;
const queryString = buildQueryStringFromParams({ team_id: teamId });
const path = `${MACADMINS}?${queryString}`;
return sendRequest("GET", path);
},
};

View File

@ -358,4 +358,8 @@ const labels = {
],
};
export default { count, hosts, labels };
export default {
count,
hosts,
labels,
};

View File

@ -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) => {