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 { interface ITextCellProps {
value: string | number | boolean; value: string | number | boolean;
formatter?: (val: any) => string; // string, number, or null formatter?: (val: any) => string; // string, number, or null
greyed?: string; greyed?: boolean;
classes?: string; classes?: string;
} }
@ -20,7 +20,7 @@ const TextCell = ({
} }
return ( return (
<span className={`text-cell ${classes} ${greyed || ""}`}> <span className={`text-cell ${classes} ${greyed && "grey-cell"}`}>
{formatter(val)} {formatter(val)}
</span> </span>
); );

View File

@ -186,6 +186,10 @@
min-width: 100%; min-width: 100%;
text-align: left; text-align: left;
} }
.grey-cell {
color: $ui-fleet-black-50;
font-style: italic;
}
} }
.highlight-on-hover:hover { .highlight-on-hover:hover {
background-color: $ui-off-white; background-color: $ui-off-white;

View File

@ -14,10 +14,18 @@ export interface IMDMAggregateStatus {
unenrolled_hosts_count: number; unenrolled_hosts_count: number;
} }
export interface IMDMSolution {
id: number;
name: string | null;
server_url: string;
hosts_count: number;
}
export interface IMacadminAggregate { export interface IMacadminAggregate {
macadmins: { macadmins: {
counts_updated_at: string; counts_updated_at: string;
munki_versions: IMunkiAggregate[]; munki_versions: IMunkiAggregate[];
mobile_device_management_enrollment_status: IMDMAggregateStatus; 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({ const MDMCard = useInfoCard({
title: "Mobile device management (MDM) enrollment", title: "Mobile device management (MDM)",
showTitle: showMDMUI, showTitle: showMDMUI,
description: ( description: (
<p> <p>

View File

@ -1,14 +1,24 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { useQuery } from "react-query"; import { useQuery } from "react-query";
import { Tab, Tabs, TabList, TabPanel } from "react-tabs";
import macadminsAPI from "services/entities/macadmins"; 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 TableContainer from "components/TableContainer";
import Spinner from "components/Spinner"; import Spinner from "components/Spinner";
import TableDataError from "components/DataError"; import TableDataError from "components/DataError";
import LastUpdatedText from "components/LastUpdatedText"; import LastUpdatedText from "components/LastUpdatedText";
import generateTableHeaders from "./MDMTableConfig"; import {
generateSolutionsTableHeaders,
generateSolutionsDataSet,
} from "./MDMSolutionsTableConfig";
import generateEnrollmentTableHeaders from "./MDMEnrollmentTableConfig";
interface IMDMCardProps { interface IMDMCardProps {
showMDMUI: boolean; showMDMUI: boolean;
@ -18,13 +28,14 @@ interface IMDMCardProps {
} }
const DEFAULT_SORT_DIRECTION = "desc"; 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 PAGE_SIZE = 8;
const baseClass = "home-mdm"; const baseClass = "home-mdm";
const EmptyMDM = (): JSX.Element => ( const EmptyMDMEnrollment = (): JSX.Element => (
<div className={`${baseClass}__empty-mdm`}> <div className={`${baseClass}__empty-mdm`}>
<h1>Unable to detect MDM enrollment.</h1> <h1>Unable to detect MDM enrollment</h1>
<p> <p>
To see MDM versions, deploy&nbsp; To see MDM versions, deploy&nbsp;
<a <a
@ -39,15 +50,27 @@ const EmptyMDM = (): JSX.Element => (
</div> </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 = ({ const MDM = ({
showMDMUI, showMDMUI,
currentTeamId, currentTeamId,
setShowMDMUI, setShowMDMUI,
setTitleDetail, setTitleDetail,
}: IMDMCardProps): JSX.Element => { }: IMDMCardProps): JSX.Element => {
const [navTabIndex, setNavTabIndex] = useState<number>(0);
const [formattedMDMData, setFormattedMDMData] = useState< const [formattedMDMData, setFormattedMDMData] = useState<
IDataTableMDMFormat[] IDataTableMDMFormat[]
>([]); >([]);
const [solutions, setSolutions] = useState<IMDMSolution[] | null>([]);
const { isFetching: isMDMFetching, error: errorMDM } = useQuery< const { isFetching: isMDMFetching, error: errorMDM } = useQuery<
IMacadminAggregate, IMacadminAggregate,
@ -58,6 +81,7 @@ const MDM = ({
const { const {
counts_updated_at, counts_updated_at,
mobile_device_management_enrollment_status, mobile_device_management_enrollment_status,
mobile_device_management_solution,
} = data.macadmins; } = data.macadmins;
const { const {
enrolled_manual_hosts_count, enrolled_manual_hosts_count,
@ -84,13 +108,20 @@ const MDM = ({
}, },
{ status: "Unenrolled", hosts: unenrolled_hosts_count }, { status: "Unenrolled", hosts: unenrolled_hosts_count },
]); ]);
setSolutions(mobile_device_management_solution);
}, },
onError: () => { onError: () => {
setShowMDMUI(true); 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 // Renders opaque information as host information is loading
const opacity = showMDMUI ? { opacity: 1 } : { opacity: 0 }; const opacity = showMDMUI ? { opacity: 1 } : { opacity: 0 };
@ -103,26 +134,58 @@ const MDM = ({
</div> </div>
)} )}
<div style={opacity}> <div style={opacity}>
{errorMDM ? ( <TabsWrapper>
<TableDataError card /> <Tabs selectedIndex={navTabIndex} onSelect={onTabChange}>
) : ( <TabList>
<TableContainer <Tab>Solutions</Tab>
columns={tableHeaders} <Tab>Enrollment</Tab>
data={formattedMDMData} </TabList>
isLoading={isMDMFetching} <TabPanel>
defaultSortHeader={DEFAULT_SORT_HEADER} {errorMDM ? (
defaultSortDirection={DEFAULT_SORT_DIRECTION} <TableDataError card />
hideActionButton ) : (
resultsTitle={"MDM"} <TableContainer
emptyComponent={EmptyMDM} columns={solutionsTableHeaders}
showMarkAllPages={false} data={solutionsDataSet}
isAllPagesSelected={false} isLoading={isMDMFetching}
disableCount defaultSortHeader={SOLUTIONS_DEFAULT_SORT_HEADER}
disableActionButton defaultSortDirection={DEFAULT_SORT_DIRECTION}
disablePagination hideActionButton
pageSize={PAGE_SIZE} 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>
</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; margin-top: $pad-large;
position: relative; position: relative;
.data-table__wrapper {
overflow-x: auto;
}
.component__tabs-wrapper .table-container__header { .component__tabs-wrapper .table-container__header {
display: none; display: none;
} }
&__empty-mdm { &__empty-mdm {
margin: $pad-medium auto 0; width: 364px;
margin: $pad-large auto 0;
h1 { h1 {
font-size: $small; font-size: $small;
@ -36,39 +34,44 @@
table-layout: fixed; table-layout: fixed;
thead { thead {
.name__header { .name__header,
width: 60%; .status__header {
}
.version__header {
width: 30%; width: 30%;
padding-right: 0;
} }
.hosts_count__header { .server_url__header {
width: 30%;
}
.hosts_count__header,
.hosts__header {
border-right: 0; border-right: 0;
padding-right: 0; padding-right: 0;
width: 60px; width: 60px;
} }
.id__header { .linkToFilteredHosts__header {
padding: 0; width: 140px;
border-left: 0;
width: 40px;
} }
} }
tbody { tbody {
.name__cell, .name__cell {
.version__cell {
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.id__cell { .mdm-solution-link {
padding: 0; visibility: hidden;
width: 40px; text-overflow: none;
}
.vulnerabilities__cell {
img { 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; min-width: 542px;
} }
.grey-cell {
color: $ui-fleet-black-50;
font-style: italic;
}
.modal__modal_container { .modal__modal_container {
width: 400px; width: 400px;
a { a {

View File

@ -37,6 +37,7 @@ import {
import { IApiError } from "interfaces/errors"; import { IApiError } from "interfaces/errors";
import { IHost } from "interfaces/host"; import { IHost } from "interfaces/host";
import { ILabel, ILabelFormData } from "interfaces/label"; import { ILabel, ILabelFormData } from "interfaces/label";
import { IMDMSolution } from "interfaces/macadmins";
import { IOperatingSystemVersion } from "interfaces/operating_system"; import { IOperatingSystemVersion } from "interfaces/operating_system";
import { IPolicy } from "interfaces/policy"; import { IPolicy } from "interfaces/policy";
import { ISoftware } from "interfaces/software"; import { ISoftware } from "interfaces/software";
@ -248,6 +249,10 @@ const ManageHostsPage = ({
const [softwareDetails, setSoftwareDetails] = useState<ISoftware | null>( const [softwareDetails, setSoftwareDetails] = useState<ISoftware | null>(
null null
); );
const [
mdmSolutionDetails,
setMDMSolutionDetails,
] = useState<IMDMSolution | null>(null);
const [tableQueryData, setTableQueryData] = useState<ITableQueryProps>(); const [tableQueryData, setTableQueryData] = useState<ITableQueryProps>();
const [ const [
currentQueryOptions, currentQueryOptions,
@ -268,6 +273,11 @@ const ManageHostsPage = ({
queryParams?.software_id !== undefined queryParams?.software_id !== undefined
? parseInt(queryParams?.software_id, 10) ? parseInt(queryParams?.software_id, 10)
: undefined; : undefined;
const mdmId =
queryParams?.mdm_id !== undefined
? parseInt(queryParams?.mdm_id, 10)
: undefined;
const mdmEnrollmentStatus = queryParams?.mdm_enrollment_status;
const operatingSystemId = const operatingSystemId =
queryParams?.operating_system_id !== undefined queryParams?.operating_system_id !== undefined
? parseInt(queryParams?.operating_system_id, 10) ? parseInt(queryParams?.operating_system_id, 10)
@ -445,11 +455,15 @@ const ManageHostsPage = ({
} }
try { try {
const { hosts: returnedHosts, software } = await hostsAPI.loadHosts( const {
options hosts: returnedHosts,
); software,
mobile_device_management_solution,
} = await hostsAPI.loadHosts(options);
setHosts(returnedHosts); setHosts(returnedHosts);
software && setSoftwareDetails(software); software && setSoftwareDetails(software);
mobile_device_management_solution &&
setMDMSolutionDetails(mobile_device_management_solution);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
setHasHostErrors(true); setHasHostErrors(true);
@ -534,6 +548,8 @@ const ManageHostsPage = ({
policyId, policyId,
policyResponse, policyResponse,
softwareId, softwareId,
mdmId,
mdmEnrollmentStatus,
operatingSystemId, operatingSystemId,
page: tableQueryData ? tableQueryData.pageIndex : 0, page: tableQueryData ? tableQueryData.pageIndex : 0,
perPage: tableQueryData ? tableQueryData.pageSize : 100, perPage: tableQueryData ? tableQueryData.pageSize : 100,
@ -641,10 +657,18 @@ const ManageHostsPage = ({
const handleClearSoftwareFilter = () => { const handleClearSoftwareFilter = () => {
router.replace(PATHS.MANAGE_HOSTS); router.replace(PATHS.MANAGE_HOSTS);
setCurrentTeam(undefined);
setSoftwareDetails(null); setSoftwareDetails(null);
}; };
const handleClearMDMSolutionFilter = () => {
router.replace(PATHS.MANAGE_HOSTS);
setMDMSolutionDetails(null);
};
const handleClearMDMEnrollmentFilter = () => {
router.replace(PATHS.MANAGE_HOSTS);
};
const handleTeamSelect = (teamId: number) => { const handleTeamSelect = (teamId: number) => {
const { MANAGE_HOSTS } = PATHS; const { MANAGE_HOSTS } = PATHS;
const teamIdParam = getValidatedTeamId( const teamIdParam = getValidatedTeamId(
@ -769,10 +793,25 @@ const ManageHostsPage = ({
newQueryParams.policy_response = policyResponse; newQueryParams.policy_response = policyResponse;
} }
if (softwareId && !policyId) { if (softwareId && !policyId && !mdmId && !mdmEnrollmentStatus) {
newQueryParams.software_id = softwareId; 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; newQueryParams.operating_system_id = operatingSystemId;
} }
router.replace( router.replace(
@ -791,6 +830,8 @@ const ManageHostsPage = ({
policyId, policyId,
queryParams, queryParams,
softwareId, softwareId,
mdmId,
mdmEnrollmentStatus,
operatingSystemId, operatingSystemId,
sortBy, sortBy,
] ]
@ -1029,6 +1070,8 @@ const ManageHostsPage = ({
policyId, policyId,
policyResponse, policyResponse,
softwareId, softwareId,
mdmId,
mdmEnrollmentStatus,
}); });
toggleTransferHostModal(); toggleTransferHostModal();
@ -1074,6 +1117,8 @@ const ManageHostsPage = ({
policyId, policyId,
policyResponse, policyResponse,
softwareId, softwareId,
mdmId,
mdmEnrollmentStatus,
}); });
refetchLabels(); refetchLabels();
@ -1188,12 +1233,12 @@ const ManageHostsPage = ({
> >
{buttonText} {buttonText}
<Button <Button
className={`${baseClass}__clear-policies-filter`} className={`${baseClass}__clear-software-filter`}
onClick={handleClearSoftwareFilter} onClick={handleClearSoftwareFilter}
variant={"small-text-icon"} variant={"small-text-icon"}
title={buttonText} title={buttonText}
> >
<img src={CloseIcon} alt="Remove policy filter" /> <img src={CloseIcon} alt="Remove software filter" />
</Button> </Button>
</div> </div>
</span> </span>
@ -1216,6 +1261,126 @@ const ManageHostsPage = ({
return null; 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 = () => { const renderEditColumnsModal = () => {
if (!config || !currentUser) { if (!config || !currentUser) {
return null; return null;
@ -1404,6 +1569,8 @@ const ManageHostsPage = ({
policyId, policyId,
policyResponse, policyResponse,
softwareId, softwareId,
mdmId,
mdmEnrollmentStatus,
visibleColumns, visibleColumns,
}; };
@ -1471,22 +1638,47 @@ const ManageHostsPage = ({
selectedLabel && selectedLabel &&
selectedLabel.type !== "all" && selectedLabel.type !== "all" &&
selectedLabel.type !== "status"; selectedLabel.type !== "status";
if (policyId || softwareId || showSelectedLabel || operatingSystemId) { if (
policyId ||
softwareId ||
showSelectedLabel ||
mdmId ||
mdmEnrollmentStatus ||
operatingSystemId
) {
return ( return (
<div className={`${baseClass}__labels-active-filter-wrap`}> <div className={`${baseClass}__labels-active-filter-wrap`}>
{showSelectedLabel && renderHeaderLabelBlock()} {showSelectedLabel && renderHeaderLabelBlock()}
{!!policyId && {!!policyId &&
!softwareId && !softwareId &&
!mdmId &&
!mdmEnrollmentStatus &&
!showSelectedLabel && !showSelectedLabel &&
renderPoliciesFilterBlock()} renderPoliciesFilterBlock()}
{!!softwareId && {!!softwareId &&
!policyId && !policyId &&
!mdmId &&
!mdmEnrollmentStatus &&
!showSelectedLabel && !showSelectedLabel &&
renderSoftwareFilterBlock()} renderSoftwareFilterBlock()}
{!!mdmId &&
!policyId &&
!softwareId &&
!mdmEnrollmentStatus &&
!showSelectedLabel &&
renderMDMSolutionFilterBlock()}
{!!mdmEnrollmentStatus &&
!policyId &&
!softwareId &&
!mdmId &&
!showSelectedLabel &&
renderMDMEnrollmentFilterBlock()}
{!!operatingSystemId && {!!operatingSystemId &&
!policyId && !policyId &&
!softwareId && !softwareId &&
!showSelectedLabel && !showSelectedLabel &&
!mdmId &&
!mdmEnrollmentStatus &&
renderOSFilterBlock()} renderOSFilterBlock()}
</div> </div>
); );
@ -1587,10 +1779,18 @@ const ManageHostsPage = ({
!isHostsLoading && !isHostsLoading &&
teamSync teamSync
) { ) {
const { software_id, policy_id, operating_system_id } = queryParams || {}; const {
const includesSoftwareOrPolicyOrOSFilter = !!( software_id,
policy_id,
mdm_id,
mdm_enrollment_status,
operating_system_id,
} = queryParams || {};
const includesNameCardFilter = !!(
software_id || software_id ||
policy_id || policy_id ||
mdm_id ||
mdm_enrollment_status ||
operating_system_id operating_system_id
); );
@ -1598,7 +1798,7 @@ const ManageHostsPage = ({
<NoHosts <NoHosts
toggleAddHostsModal={toggleAddHostsModal} toggleAddHostsModal={toggleAddHostsModal}
canEnrollHosts={canEnrollHosts} canEnrollHosts={canEnrollHosts}
includesSoftwareOrPolicyFilter={includesSoftwareOrPolicyOrOSFilter} includesNameCardFilter={includesNameCardFilter}
/> />
); );
} }

View File

@ -199,6 +199,7 @@
&__policies-filter-block { &__policies-filter-block {
display: flex; display: flex;
align-items: center; align-items: center;
gap: $pad-medium;
p { p {
font-size: $xx-small; font-size: $xx-small;
@ -208,14 +209,15 @@
} }
&__policies-filter-name-card, &__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; display: inline-flex;
align-items: center; align-items: center;
padding: 6px 12px; padding: 6px 12px;
border: 1px solid $ui-fleet-black-25; border: 1px solid $ui-fleet-black-25;
border-radius: 4px; border-radius: 4px;
box-shadow: none; box-shadow: none;
margin-left: $pad-medium;
color: $core-fleet-black; color: $core-fleet-black;
font-size: $xx-small; font-size: $xx-small;
font-weight: $bold; font-weight: $bold;
@ -240,10 +242,6 @@
} }
} }
&__software-filter-name-card {
margin-left: 0px;
}
&__enroll-hosts { &__enroll-hosts {
padding: $pad-small; padding: $pad-small;
margin-right: $pad-small; margin-right: $pad-small;

View File

@ -9,7 +9,7 @@ import RoboDogImage from "../../../../../../assets/images/robo-dog-176x144@2x.pn
interface INoHostsProps { interface INoHostsProps {
toggleAddHostsModal: () => void; toggleAddHostsModal: () => void;
canEnrollHosts?: boolean; canEnrollHosts?: boolean;
includesSoftwareOrPolicyFilter?: boolean; includesNameCardFilter?: boolean;
} }
const baseClass = "no-hosts"; const baseClass = "no-hosts";
@ -17,10 +17,10 @@ const baseClass = "no-hosts";
const NoHosts = ({ const NoHosts = ({
toggleAddHostsModal, toggleAddHostsModal,
canEnrollHosts, canEnrollHosts,
includesSoftwareOrPolicyFilter, includesNameCardFilter,
}: INoHostsProps): JSX.Element => { }: INoHostsProps): JSX.Element => {
const renderContent = () => { const renderContent = () => {
if (includesSoftwareOrPolicyFilter) { if (includesNameCardFilter) {
return ( return (
<div> <div>
<h1>No hosts match the current criteria</h1> <h1>No hosts match the current criteria</h1>
@ -64,9 +64,7 @@ const NoHosts = ({
return ( return (
<div className={`${baseClass}`}> <div className={`${baseClass}`}>
<div className={`${baseClass}__inner`}> <div className={`${baseClass}__inner`}>
{!includesSoftwareOrPolicyFilter && ( {!includesNameCardFilter && <img src={RoboDogImage} alt="No Hosts" />}
<img src={RoboDogImage} alt="No Hosts" />
)}
{renderContent()} {renderContent()}
</div> </div>
</div> </div>

View File

@ -17,6 +17,8 @@ export interface IHostCountLoadOptions {
policyId?: number; policyId?: number;
policyResponse?: string; policyResponse?: string;
softwareId?: number; softwareId?: number;
mdmId?: number;
mdmEnrollmentStatus?: string;
operatingSystemId?: number; operatingSystemId?: number;
} }
@ -30,6 +32,8 @@ export default {
const policyResponse = options?.policyResponse || null; const policyResponse = options?.policyResponse || null;
const selectedLabels = options?.selectedLabels || []; const selectedLabels = options?.selectedLabels || [];
const softwareId = options?.softwareId || null; const softwareId = options?.softwareId || null;
const mdmId = options?.mdmId || null;
const mdmEnrollmentStatus = options?.mdmEnrollmentStatus || null;
const operatingSystemId = options?.operatingSystemId || null; const operatingSystemId = options?.operatingSystemId || null;
const labelPrefix = "labels/"; const labelPrefix = "labels/";
@ -70,7 +74,22 @@ export default {
queryString += `&software_id=${softwareId}`; 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}`; queryString += `&operating_system_id=${operatingSystemId}`;
} }

View File

@ -19,6 +19,8 @@ export interface ILoadHostsOptions {
policyId?: number; policyId?: number;
policyResponse?: string; policyResponse?: string;
softwareId?: number; softwareId?: number;
mdmId?: number;
mdmEnrollmentStatus?: string;
operatingSystemId?: number; operatingSystemId?: number;
device_mapping?: boolean; device_mapping?: boolean;
columns?: string; columns?: string;
@ -35,6 +37,8 @@ export interface IExportHostsOptions {
policyId?: number; policyId?: number;
policyResponse?: string; policyResponse?: string;
softwareId?: number; softwareId?: number;
mdmId?: number;
mdmEnrollmentStatus?: string;
operatingSystemId?: number; operatingSystemId?: number;
device_mapping?: boolean; device_mapping?: boolean;
columns?: string; columns?: string;
@ -101,9 +105,37 @@ const getPolicyParams = (
const getSoftwareParam = ( const getSoftwareParam = (
label?: string, label?: string,
policyId?: number, 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 = ( const getOperatingSystemParam = (
@ -156,6 +188,8 @@ export default {
const policyId = options?.policyId || null; const policyId = options?.policyId || null;
const policyResponse = options?.policyResponse || "passing"; const policyResponse = options?.policyResponse || "passing";
const softwareId = options?.softwareId || null; const softwareId = options?.softwareId || null;
const mdmId = options?.mdmId || null;
const mdmEnrollmentStatus = options?.mdmEnrollmentStatus || null;
const visibleColumns = options?.visibleColumns || null; const visibleColumns = options?.visibleColumns || null;
if (!sortBy.length) { if (!sortBy.length) {
@ -186,7 +220,7 @@ export default {
path += `&team_id=${teamId}`; 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) { if (label) {
const lid = label.substr(labelPrefix.length); const lid = label.substr(labelPrefix.length);
path += `&label_id=${parseInt(lid, 10)}`; path += `&label_id=${parseInt(lid, 10)}`;
@ -197,10 +231,18 @@ export default {
path += `&policy_response=${policyResponse}`; path += `&policy_response=${policyResponse}`;
} }
if (!label && !policyId && softwareId) { if (!label && !policyId && !mdmId && !mdmEnrollmentStatus && softwareId) {
path += `&software_id=${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) { if (visibleColumns) {
path += `&columns=${visibleColumns}`; path += `&columns=${visibleColumns}`;
} }
@ -217,6 +259,8 @@ export default {
policyId, policyId,
policyResponse = "passing", policyResponse = "passing",
softwareId, softwareId,
mdmId,
mdmEnrollmentStatus,
operatingSystemId, operatingSystemId,
device_mapping, device_mapping,
selectedLabels, selectedLabels,
@ -237,6 +281,20 @@ export default {
policy_id: policyParams.policy_id, policy_id: policyParams.policy_id,
policy_response: policyParams.policy_response, policy_response: policyParams.policy_response,
software_id: getSoftwareParam(label, policyId, softwareId), 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( operating_system_id: getOperatingSystemParam(
label, label,
policyId, policyId,
@ -248,6 +306,7 @@ export default {
}; };
const queryString = buildQueryStringFromParams(queryParams); const queryString = buildQueryStringFromParams(queryParams);
const endpoint = getHostEndpoint(selectedLabels); const endpoint = getHostEndpoint(selectedLabels);
const path = `${endpoint}?${queryString}`; const path = `${endpoint}?${queryString}`;
return sendRequest("GET", path); return sendRequest("GET", path);

View File

@ -8,6 +8,7 @@ export default {
const { MACADMINS } = endpoints; const { MACADMINS } = endpoints;
const queryString = buildQueryStringFromParams({ team_id: teamId }); const queryString = buildQueryStringFromParams({ team_id: teamId });
const path = `${MACADMINS}?${queryString}`; const path = `${MACADMINS}?${queryString}`;
return sendRequest("GET", path); 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 return `${teams.length + 1} teams`; // global role and one or more teams
}; };
export const greyCell = (roleOrTeamText: string): string => { export const greyCell = (roleOrTeamText: string): boolean => {
const GREYED_TEXT = ["Global", "Unassigned", "Various", "No Team"]; const GREYED_TEXT = ["Global", "Unassigned", "Various", "No Team", "Unknown"];
if ( return (
GREYED_TEXT.includes(roleOrTeamText) || GREYED_TEXT.includes(roleOrTeamText) || roleOrTeamText.includes(" teams")
roleOrTeamText.includes(" teams") );
) {
return "grey-cell";
}
return "";
}; };
const setupData = (formData: any) => { const setupData = (formData: any) => {