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{" "} + link to hosts filtered by MDM solution + + ); + }, + 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{" "} + link to hosts filtered by MDM solution + + ); + }, + 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 && ( - No Hosts - )} + {!includesNameCardFilter && No Hosts} {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) => {