diff --git a/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx b/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx
index e598a19cfa..8926de0224 100644
--- a/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx
+++ b/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx
@@ -2,7 +2,7 @@ import React, { useState, useContext, useEffect, useCallback } from "react";
import { useQuery } from "react-query";
import { InjectedRouter, Params } from "react-router/lib/Router";
import { RouteProps } from "react-router/lib/Route";
-import { find, isEmpty, isEqual, omit, invert } from "lodash";
+import { find, isEmpty, isEqual, omit } from "lodash";
import { format } from "date-fns";
import FileSaver from "file-saver";
@@ -35,11 +35,8 @@ import {
import { IHost } from "interfaces/host";
import { ILabel } from "interfaces/label";
import { IMunkiIssuesAggregate } from "interfaces/macadmins";
-import { IMdmSolution, MDM_ENROLLMENT_STATUS } from "interfaces/mdm";
-import {
- formatOperatingSystemDisplayName,
- IOperatingSystemVersion,
-} from "interfaces/operating_system";
+import { IMdmSolution } from "interfaces/mdm";
+import { IOperatingSystemVersion } from "interfaces/operating_system";
import { IPolicy, IStoredPolicyResponse } from "interfaces/policy";
import { ISoftware } from "interfaces/software";
import { ITeam } from "interfaces/team";
@@ -49,7 +46,6 @@ import sortUtils from "utilities/sort";
import {
HOSTS_SEARCH_BOX_PLACEHOLDER,
HOSTS_SEARCH_BOX_TOOLTIP,
- PLATFORM_LABEL_DISPLAY_NAMES,
PolicyResponse,
} from "utilities/constants";
@@ -74,27 +70,22 @@ import {
DEFAULT_SORT_DIRECTION,
DEFAULT_PAGE_SIZE,
HOST_SELECT_STATUSES,
- MAC_SETTINGS_FILTER_OPTIONS,
} from "./constants";
import { isAcceptableStatus, getNextLocationPath } from "./helpers";
import DeleteSecretModal from "../../../components/EnrollSecrets/DeleteSecretModal";
import SecretEditorModal from "../../../components/EnrollSecrets/SecretEditorModal";
import AddHostsModal from "../../../components/AddHostsModal";
import EnrollSecretModal from "../../../components/EnrollSecrets/EnrollSecretModal";
-import PoliciesFilter from "./components/PoliciesFilter";
// @ts-ignore
import EditColumnsModal from "./components/EditColumnsModal/EditColumnsModal";
import TransferHostModal from "../components/TransferHostModal";
import DeleteHostModal from "../components/DeleteHostModal";
import DeleteLabelModal from "./components/DeleteLabelModal";
import EditColumnsIcon from "../../../../assets/images/icon-edit-columns-16x16@2x.png";
-import PencilIcon from "../../../../assets/images/icon-pencil-14x14@2x.png";
-import TrashIcon from "../../../../assets/images/icon-trash-14x14@2x.png";
import CloseIconBlack from "../../../../assets/images/icon-close-fleet-black-16x16@2x.png";
-import PolicyIcon from "../../../../assets/images/icon-policy-fleet-black-12x12@2x.png";
import DownloadIcon from "../../../../assets/images/icon-download-12x12@2x.png";
import LabelFilterSelect from "./components/LabelFilterSelect";
-import FilterPill from "./components/FilterPill";
+import HostsFilterBlock from "./components/HostsFilterBlock";
interface IManageHostsProps {
route: RouteProps;
@@ -280,18 +271,13 @@ const ManageHostsPage = ({
const canEnrollGlobalHosts = isGlobalAdmin || isGlobalMaintainer;
const canAddNewLabels = (isGlobalAdmin || isGlobalMaintainer) ?? false;
- const {
- isLoading: isLoadingLabels,
- data: labels,
- error: labelsError,
- refetch: refetchLabels,
- } = useQuery(
- ["labels"],
- () => labelsAPI.loadAll(),
- {
- select: (data: ILabelsResponse) => data.labels,
- }
- );
+ const { data: labels, refetch: refetchLabels } = useQuery<
+ ILabelsResponse,
+ Error,
+ ILabel[]
+ >(["labels"], () => labelsAPI.loadAll(), {
+ select: (data: ILabelsResponse) => data.labels,
+ });
const {
isLoading: isGlobalSecretsLoading,
@@ -623,38 +609,6 @@ const ManageHostsPage = ({
);
};
- const handleClearPoliciesFilter = () => {
- handleClearFilter(["policy_id", "policy_response"]);
- };
-
- const handleClearOSFilter = () => {
- handleClearFilter(["os_id", "os_name", "os_version"]);
- };
-
- const handleClearMacSettingsStatusFilter = () => {
- handleClearFilter(["macos_settings"]);
- };
-
- const handleClearSoftwareFilter = () => {
- handleClearFilter(["software_id"]);
- };
-
- const handleClearMDMSolutionFilter = () => {
- handleClearFilter(["mdm_id"]);
- };
-
- const handleClearMDMEnrollmentFilter = () => {
- handleClearFilter(["mdm_enrollment_status"]);
- };
-
- const handleClearMunkiIssueFilter = () => {
- handleClearFilter(["munki_issue_id"]);
- };
-
- const handleClearLowDiskSpaceFilter = () => {
- handleClearFilter(["low_disk_space"]);
- };
-
const handleTeamSelect = (teamId: number) => {
const { MANAGE_HOSTS } = PATHS;
@@ -1105,247 +1059,6 @@ const ManageHostsPage = ({
/>
);
- const renderLabelFilterPill = () => {
- if (selectedLabel) {
- const { description, display_text, label_type } = selectedLabel;
- const pillLabel =
- PLATFORM_LABEL_DISPLAY_NAMES[display_text] ?? display_text;
-
- return (
- <>
-
- {label_type !== "builtin" && !isOnlyObserver && (
- <>
-
-
- >
- )}
- >
- );
- }
-
- return null;
- };
-
- const renderOSFilterBlock = () => {
- if (!osId && !(osName && osVersion)) return null;
-
- let os: IOperatingSystemVersion | undefined;
- if (osId) {
- os = osVersions?.find((v) => v.os_id === osId);
- } else if (osName && osVersion) {
- const name: string = osName;
- const vers: string = osVersion;
-
- os = osVersions?.find(
- ({ name_only, version }) =>
- name_only.toLowerCase() === name.toLowerCase() &&
- version.toLowerCase() === vers.toLowerCase()
- );
- }
- if (!os) return null;
-
- const { name, name_only, version } = os;
- const label = formatOperatingSystemDisplayName(
- name_only || version
- ? `${name_only || ""} ${version || ""}`
- : `${name || ""}`
- );
- const TooltipDescription = (
-
- Hosts with {formatOperatingSystemDisplayName(name_only || name)},
-
- {version && `${version} installed`}
-
- );
-
- return (
-
- );
- };
-
- const renderPoliciesFilterBlock = () => (
- <>
-
-
- >
- );
-
- const renderMacSettingsStatusFilterBlock = () => {
- const label = "macOS settings";
- return (
- <>
-
-
- >
- );
- };
-
- const renderSoftwareFilterBlock = () => {
- if (!softwareDetails) return null;
-
- const { name, version } = softwareDetails;
- const label = `${name || "Unknown software"} ${version || ""}`;
-
- const TooltipDescription = (
-
- Hosts with {name || "Unknown software"},
-
- {version || "version unknown"} installed
-
- );
-
- return (
-
- );
- };
-
- const renderMDMSolutionFilterBlock = () => {
- if (!mdmSolutionDetails) return null;
-
- const { name, server_url } = mdmSolutionDetails;
- const label = name ? `${name} ${server_url}` : `${server_url}`;
-
- const TooltipDescription = (
-
- Host enrolled
- {name !== "Unknown" && ` to ${name}`}
-
at {server_url}
-
- );
-
- return (
-
- );
- };
-
- const renderMDMEnrollmentFilterBlock = () => {
- if (!mdmEnrollmentStatus) return null;
-
- const label = `MDM status: ${
- invert(MDM_ENROLLMENT_STATUS)[mdmEnrollmentStatus]
- }`;
-
- // More narrow tooltip than other MDM tooltip
- const MDM_STATUS_PILL_TOOLTIP: Record = {
- automatic: (
-
- MDM was turned on
- automatically using Apple
- Automated Device
- Enrollment (DEP) or
- Windows Autopilot.
- Administrators can block
- device users from turning
-
MDM off.
-
- ),
- manual: (
-
- MDM was turned on
- manually. Device users
- can turn MDM off.
-
- ),
- unenrolled: (
-
- Hosts with MDM off
- don't receive macOS
- settings and macOS
- update encouragement.
-
- ),
- pending: (
-
- Hosts ordered using Apple
- Business Manager (ABM).
- They will automatically enroll
- to Fleet and turn on MDM
- when they're unboxed.
-
- ),
- };
-
- return (
-
- );
- };
-
- const renderMunkiIssueFilterBlock = () => {
- if (munkiIssueDetails) {
- return (
-
- Hosts that reported this Munki issue
- the last time Munki ran on each host.
-
- }
- onClear={handleClearMunkiIssueFilter}
- />
- );
- }
- return null;
- };
-
- const renderLowDiskSpaceFilterBlock = () => {
- const TooltipDescription = (
-
- Hosts that have {lowDiskSpaceHosts} GB or less
- disk space available.
-
- );
-
- return (
-
- );
- };
-
const renderEditColumnsModal = () => {
if (!config || !currentUser) {
return null;
@@ -1570,76 +1283,6 @@ const ManageHostsPage = ({
);
}, [isHostCountLoading, filteredHostCount]);
- const renderActiveFilterBlock = () => {
- const showSelectedLabel = selectedLabel && selectedLabel.type !== "all";
-
- if (
- showSelectedLabel ||
- policyId ||
- macSettingsStatus ||
- softwareId ||
- showSelectedLabel ||
- mdmId ||
- mdmEnrollmentStatus ||
- lowDiskSpaceHosts ||
- osId ||
- (osName && osVersion) ||
- munkiIssueId
- ) {
- const renderFilterPill = () => {
- switch (true) {
- // backend allows for pill combos (label + low disk space) OR
- // (label + mdm solution) OR (label + mdm enrollment status)
- case showSelectedLabel && !!lowDiskSpaceHosts:
- return (
- <>
- {renderLabelFilterPill()} {renderLowDiskSpaceFilterBlock()}
- >
- );
- case showSelectedLabel && !!mdmId:
- return (
- <>
- {renderLabelFilterPill()} {renderMDMSolutionFilterBlock()}
- >
- );
-
- case showSelectedLabel && !!mdmEnrollmentStatus:
- return (
- <>
- {renderLabelFilterPill()} {renderMDMEnrollmentFilterBlock()}
- >
- );
- case showSelectedLabel:
- return renderLabelFilterPill();
- case !!policyId:
- return renderPoliciesFilterBlock();
- case !!macSettingsStatus:
- return renderMacSettingsStatusFilterBlock();
- case !!softwareId:
- return renderSoftwareFilterBlock();
- case !!mdmId:
- return renderMDMSolutionFilterBlock();
- case !!mdmEnrollmentStatus:
- return renderMDMEnrollmentFilterBlock();
- case !!osId || (!!osName && !!osVersion):
- return renderOSFilterBlock();
- case !!munkiIssueId:
- return renderMunkiIssueFilterBlock();
- case !!lowDiskSpaceHosts:
- return renderLowDiskSpaceFilterBlock();
- default:
- return null;
- }
- };
-
- return (
-
- {renderFilterPill()}
-
- );
- }
- };
-
const renderCustomControls = () => {
// we filter out the status labels as we dont want to display them in the label
// filter select dropdown.
@@ -1901,7 +1544,36 @@ const ManageHostsPage = ({
)}
- {renderActiveFilterBlock()}
+ {/* TODO: look at improving the props API for this component. Im thinking
+ some of the props can be defined inside HostsFilterBlock */}
+
{renderNoEnrollSecretBanner()}
{renderTable()}
diff --git a/frontend/pages/hosts/ManageHostsPage/_styles.scss b/frontend/pages/hosts/ManageHostsPage/_styles.scss
index 1f1e6b8b8c..7335c03bba 100644
--- a/frontend/pages/hosts/ManageHostsPage/_styles.scss
+++ b/frontend/pages/hosts/ManageHostsPage/_styles.scss
@@ -248,34 +248,6 @@
margin-left: $pad-small;
}
- &__macsettings-dropdown {
- width: 137px;
-
- .Select-value {
- display: flex;
- align-items: center;
-
- &::before {
- position: relative;
- content: url(../assets/images/icon-filter-v2-black-16x16@2x.png);
- transform: scale(0.5);
- left: -8px;
- top: 4px;
- }
- }
- }
-
- &__labels-active-filter-wrap {
- display: flex;
- align-items: center;
- margin-bottom: $pad-medium;
- gap: $pad-small; // between multiple filter pills
- }
-
- &__policies-filter-pill {
- margin-left: $pad-medium;
- }
-
&__enroll-hosts {
padding: $pad-small;
margin-right: $pad-small;
diff --git a/frontend/pages/hosts/ManageHostsPage/components/FilterPill/FilterPill.tests.tsx b/frontend/pages/hosts/ManageHostsPage/components/FilterPill/FilterPill.tests.tsx
new file mode 100644
index 0000000000..ad390871f4
--- /dev/null
+++ b/frontend/pages/hosts/ManageHostsPage/components/FilterPill/FilterPill.tests.tsx
@@ -0,0 +1,59 @@
+import React from "react";
+import { noop } from "lodash";
+import { render, screen } from "@testing-library/react";
+
+import { renderWithSetup } from "test/test-utils";
+
+import FilterPill from "./FilterPill";
+
+import PolicyIcon from "../../../../../../assets/images/icon-policy-fleet-black-12x12@2x.png";
+
+describe("Filter Pill Component", () => {
+ it("renders the pill text", () => {
+ render();
+
+ expect(screen.getByText("Test Pill")).toBeInTheDocument();
+ });
+
+ it("renders an passed in icon properly", () => {
+ render();
+
+ expect(screen.getByTestId("filter-pill__icon")).toBeInTheDocument();
+ });
+
+ it("renders a passed in string tooltip", () => {
+ render(
+
+ );
+
+ expect(screen.getByText("Test Tooltip")).toBeInTheDocument();
+ });
+
+ it("renders a passed in ReactNode tooltip", () => {
+ render(
+ This is a ReactNode
}
+ onClear={noop}
+ />
+ );
+
+ expect(screen.getByText("This is a ReactNode")).toBeInTheDocument();
+ });
+
+ it("calls the onCancel callback when a user clicks on the remove button", async () => {
+ const spy = jest.fn();
+
+ const { user } = renderWithSetup(
+
+ );
+
+ await user.click(screen.getByRole("button", { name: "Remove filter" }));
+
+ expect(spy).toHaveBeenCalled();
+ });
+});
diff --git a/frontend/pages/hosts/ManageHostsPage/components/FilterPill/FilterPill.tsx b/frontend/pages/hosts/ManageHostsPage/components/FilterPill/FilterPill.tsx
index 62d7964360..3251236ab2 100644
--- a/frontend/pages/hosts/ManageHostsPage/components/FilterPill/FilterPill.tsx
+++ b/frontend/pages/hosts/ManageHostsPage/components/FilterPill/FilterPill.tsx
@@ -40,7 +40,9 @@ const FilterPill = ({
data-for={`filter-pill-tooltip-${label}`}
>
- {icon &&

}
+ {icon && (
+

+ )}
{label}