mirror of
https://github.com/fleetdm/fleet
synced 2026-05-23 08:58:41 +00:00
move out manage host filters into their own component (#10749)
# Checklist for submitter This removes the filtering UI from the manage hosts page. Currently it keeps the same code (I felt it was too risky to move out the code AND do a big rewrite) but I have other work still in progress where I try to improve the code for filtering. Basically this is the first step to making the code for creating and maintaining filters a bit easier. - [x] Manual QA for all new/changed functionality
This commit is contained in:
parent
d425367c9e
commit
963d628588
8 changed files with 560 additions and 398 deletions
|
|
@ -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<ILabelsResponse, Error, ILabel[]>(
|
||||
["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 (
|
||||
<>
|
||||
<FilterPill
|
||||
label={pillLabel}
|
||||
tooltipDescription={description}
|
||||
onClear={handleClearRouteParam}
|
||||
/>
|
||||
{label_type !== "builtin" && !isOnlyObserver && (
|
||||
<>
|
||||
<Button onClick={onEditLabelClick} variant={"text-icon"}>
|
||||
<img src={PencilIcon} alt="Edit label" />
|
||||
</Button>
|
||||
<Button onClick={toggleDeleteLabelModal} variant={"text-icon"}>
|
||||
<img src={TrashIcon} alt="Delete label" />
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
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 = (
|
||||
<span className="tooltip__tooltip-text">
|
||||
Hosts with {formatOperatingSystemDisplayName(name_only || name)},
|
||||
<br />
|
||||
{version && `${version} installed`}
|
||||
</span>
|
||||
);
|
||||
|
||||
return (
|
||||
<FilterPill
|
||||
label={label}
|
||||
tooltipDescription={TooltipDescription}
|
||||
onClear={handleClearOSFilter}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const renderPoliciesFilterBlock = () => (
|
||||
<>
|
||||
<PoliciesFilter
|
||||
policyResponse={policyResponse}
|
||||
onChange={handleChangePoliciesFilter}
|
||||
/>
|
||||
<FilterPill
|
||||
icon={PolicyIcon}
|
||||
label={policy?.name ?? "..."}
|
||||
onClear={handleClearPoliciesFilter}
|
||||
className={`${baseClass}__policies-filter-pill`}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
const renderMacSettingsStatusFilterBlock = () => {
|
||||
const label = "macOS settings";
|
||||
return (
|
||||
<>
|
||||
<Dropdown
|
||||
value={macSettingsStatus}
|
||||
className={`${baseClass}__macsettings-dropdown`}
|
||||
options={MAC_SETTINGS_FILTER_OPTIONS}
|
||||
onChange={handleMacSettingsStatusDropdownChange}
|
||||
/>
|
||||
<FilterPill
|
||||
label={label}
|
||||
onClear={handleClearMacSettingsStatusFilter}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const renderSoftwareFilterBlock = () => {
|
||||
if (!softwareDetails) return null;
|
||||
|
||||
const { name, version } = softwareDetails;
|
||||
const label = `${name || "Unknown software"} ${version || ""}`;
|
||||
|
||||
const TooltipDescription = (
|
||||
<span className={`tooltip__tooltip-text`}>
|
||||
Hosts with {name || "Unknown software"},
|
||||
<br />
|
||||
{version || "version unknown"} installed
|
||||
</span>
|
||||
);
|
||||
|
||||
return (
|
||||
<FilterPill
|
||||
label={label}
|
||||
onClear={handleClearSoftwareFilter}
|
||||
tooltipDescription={TooltipDescription}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const renderMDMSolutionFilterBlock = () => {
|
||||
if (!mdmSolutionDetails) return null;
|
||||
|
||||
const { name, server_url } = mdmSolutionDetails;
|
||||
const label = name ? `${name} ${server_url}` : `${server_url}`;
|
||||
|
||||
const TooltipDescription = (
|
||||
<span className="tooltip__tooltip-text">
|
||||
Host enrolled
|
||||
{name !== "Unknown" && ` to ${name}`}
|
||||
<br /> at {server_url}
|
||||
</span>
|
||||
);
|
||||
|
||||
return (
|
||||
<FilterPill
|
||||
label={label}
|
||||
tooltipDescription={TooltipDescription}
|
||||
onClear={handleClearMDMSolutionFilter}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
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<string, JSX.Element> = {
|
||||
automatic: (
|
||||
<span className="tooltip__tooltip-text">
|
||||
MDM was turned on <br />
|
||||
automatically using Apple <br />
|
||||
Automated Device <br />
|
||||
Enrollment (DEP) or <br />
|
||||
Windows Autopilot. <br />
|
||||
Administrators can block <br />
|
||||
device users from turning
|
||||
<br /> MDM off.
|
||||
</span>
|
||||
),
|
||||
manual: (
|
||||
<span className="tooltip__tooltip-text">
|
||||
MDM was turned on <br />
|
||||
manually. Device users <br />
|
||||
can turn MDM off.
|
||||
</span>
|
||||
),
|
||||
unenrolled: (
|
||||
<span className="tooltip__tooltip-text">
|
||||
Hosts with MDM off <br />
|
||||
don't receive macOS <br />
|
||||
settings and macOS <br />
|
||||
update encouragement.
|
||||
</span>
|
||||
),
|
||||
pending: (
|
||||
<span className="tooltip__tooltip-text">
|
||||
Hosts ordered using Apple <br />
|
||||
Business Manager (ABM). <br />
|
||||
They will automatically enroll <br />
|
||||
to Fleet and turn on MDM <br />
|
||||
when they're unboxed.
|
||||
</span>
|
||||
),
|
||||
};
|
||||
|
||||
return (
|
||||
<FilterPill
|
||||
label={label}
|
||||
tooltipDescription={MDM_STATUS_PILL_TOOLTIP[mdmEnrollmentStatus]}
|
||||
onClear={handleClearMDMEnrollmentFilter}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const renderMunkiIssueFilterBlock = () => {
|
||||
if (munkiIssueDetails) {
|
||||
return (
|
||||
<FilterPill
|
||||
label={munkiIssueDetails.name}
|
||||
tooltipDescription={
|
||||
<span className="tooltip__tooltip-text">
|
||||
Hosts that reported this Munki issue <br />
|
||||
the last time Munki ran on each host.
|
||||
</span>
|
||||
}
|
||||
onClear={handleClearMunkiIssueFilter}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const renderLowDiskSpaceFilterBlock = () => {
|
||||
const TooltipDescription = (
|
||||
<span className="tooltip__tooltip-text">
|
||||
Hosts that have {lowDiskSpaceHosts} GB or less <br />
|
||||
disk space available.
|
||||
</span>
|
||||
);
|
||||
|
||||
return (
|
||||
<FilterPill
|
||||
label="Low disk space"
|
||||
tooltipDescription={TooltipDescription}
|
||||
onClear={handleClearLowDiskSpaceFilter}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<div className={`${baseClass}__labels-active-filter-wrap`}>
|
||||
{renderFilterPill()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
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 = ({
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
{renderActiveFilterBlock()}
|
||||
{/* TODO: look at improving the props API for this component. Im thinking
|
||||
some of the props can be defined inside HostsFilterBlock */}
|
||||
<HostsFilterBlock
|
||||
params={{
|
||||
policyResponse,
|
||||
policyId,
|
||||
policy,
|
||||
macSettingsStatus,
|
||||
softwareId,
|
||||
mdmId,
|
||||
mdmEnrollmentStatus,
|
||||
lowDiskSpaceHosts,
|
||||
osId,
|
||||
osName,
|
||||
osVersion,
|
||||
osVersions,
|
||||
munkiIssueId,
|
||||
munkiIssueDetails,
|
||||
softwareDetails,
|
||||
mdmSolutionDetails,
|
||||
}}
|
||||
selectedLabel={selectedLabel}
|
||||
isOnlyObserver={isOnlyObserver}
|
||||
handleClearRouteParam={handleClearRouteParam}
|
||||
handleClearFilter={handleClearFilter}
|
||||
onChangePoliciesFilter={handleChangePoliciesFilter}
|
||||
onChangeMacSettingsFilter={handleMacSettingsStatusDropdownChange}
|
||||
onClickEditLabel={onEditLabelClick}
|
||||
onClickDeleteLabel={toggleDeleteLabelModal}
|
||||
/>
|
||||
{renderNoEnrollSecretBanner()}
|
||||
{renderTable()}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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(<FilterPill label="Test Pill" onClear={noop} />);
|
||||
|
||||
expect(screen.getByText("Test Pill")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders an passed in icon properly", () => {
|
||||
render(<FilterPill label="Test Pill" icon={PolicyIcon} onClear={noop} />);
|
||||
|
||||
expect(screen.getByTestId("filter-pill__icon")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders a passed in string tooltip", () => {
|
||||
render(
|
||||
<FilterPill
|
||||
label="Test Pill"
|
||||
tooltipDescription="Test Tooltip"
|
||||
onClear={noop}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText("Test Tooltip")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders a passed in ReactNode tooltip", () => {
|
||||
render(
|
||||
<FilterPill
|
||||
label="Test Pill"
|
||||
tooltipDescription={<p>This is a ReactNode</p>}
|
||||
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(
|
||||
<FilterPill label="Test Pill" onClear={spy} />
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Remove filter" }));
|
||||
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
@ -40,7 +40,9 @@ const FilterPill = ({
|
|||
data-for={`filter-pill-tooltip-${label}`}
|
||||
>
|
||||
<div className={labelClasses}>
|
||||
{icon && <img src={icon} alt="" />}
|
||||
{icon && (
|
||||
<img src={icon} alt="" data-testid={`${baseClass}__icon`} />
|
||||
)}
|
||||
{label}
|
||||
<Button
|
||||
className={`${baseClass}__clear-filter`}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,420 @@
|
|||
import React from "react";
|
||||
import { invert } from "lodash";
|
||||
|
||||
import { ILabel } from "interfaces/label";
|
||||
import {
|
||||
formatOperatingSystemDisplayName,
|
||||
IOperatingSystemVersion,
|
||||
} from "interfaces/operating_system";
|
||||
import { IMdmSolution, MDM_ENROLLMENT_STATUS } from "interfaces/mdm";
|
||||
import { IMunkiIssuesAggregate } from "interfaces/macadmins";
|
||||
import { ISoftware } from "interfaces/software";
|
||||
import { IPolicy } from "interfaces/policy";
|
||||
|
||||
// TODO: should this be in interfaces hosts?
|
||||
import { MacSettingsStatusQueryParam } from "services/entities/hosts";
|
||||
|
||||
import {
|
||||
PLATFORM_LABEL_DISPLAY_NAMES,
|
||||
PolicyResponse,
|
||||
} from "utilities/constants";
|
||||
|
||||
// @ts-ignore
|
||||
import Dropdown from "components/forms/fields/Dropdown";
|
||||
import Button from "components/buttons/Button";
|
||||
|
||||
import FilterPill from "../FilterPill";
|
||||
import PoliciesFilter from "../PoliciesFilter";
|
||||
import { MAC_SETTINGS_FILTER_OPTIONS } from "../../constants";
|
||||
|
||||
import PencilIcon from "../../../../../../assets/images/icon-pencil-14x14@2x.png";
|
||||
import TrashIcon from "../../../../../../assets/images/icon-trash-14x14@2x.png";
|
||||
import PolicyIcon from "../../../../../../assets/images/icon-policy-fleet-black-12x12@2x.png";
|
||||
|
||||
const baseClass = "hosts-filter-block";
|
||||
|
||||
interface IHostsFilterBlockProps {
|
||||
/**
|
||||
* An object of params the the HostFilterBlock uses to render the correct
|
||||
* filter pills and dropdowns.
|
||||
*
|
||||
* TODO: improve as some of the request for this data can happen here or lower
|
||||
* in component tree.
|
||||
*/
|
||||
params: {
|
||||
munkiIssueDetails: IMunkiIssuesAggregate | null;
|
||||
policyResponse: PolicyResponse;
|
||||
policyId?: any;
|
||||
policy?: IPolicy;
|
||||
macSettingsStatus?: any;
|
||||
softwareId?: any;
|
||||
mdmId?: number;
|
||||
mdmEnrollmentStatus?: any;
|
||||
lowDiskSpaceHosts?: number;
|
||||
osId?: any;
|
||||
osName?: any;
|
||||
osVersion?: any;
|
||||
munkiIssueId?: number;
|
||||
osVersions?: IOperatingSystemVersion[];
|
||||
softwareDetails: ISoftware | null;
|
||||
mdmSolutionDetails: IMdmSolution | null;
|
||||
};
|
||||
selectedLabel?: ILabel;
|
||||
isOnlyObserver?: boolean;
|
||||
handleClearRouteParam: () => void;
|
||||
handleClearFilter: (omitParams: string[]) => void;
|
||||
onChangePoliciesFilter: (response: PolicyResponse) => void;
|
||||
onChangeMacSettingsFilter: (
|
||||
newMacSettingsStatus: MacSettingsStatusQueryParam
|
||||
) => void;
|
||||
onClickEditLabel: (evt: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
onClickDeleteLabel: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the filtering section of the Manage Hosts Page. This will handle rendering
|
||||
* the correct filter pills and any filter dropdowns associated with those pills.
|
||||
*/
|
||||
const HostsFilterBlock = ({
|
||||
params: {
|
||||
policyId,
|
||||
macSettingsStatus,
|
||||
softwareId,
|
||||
mdmId,
|
||||
mdmEnrollmentStatus,
|
||||
lowDiskSpaceHosts,
|
||||
osId,
|
||||
osName,
|
||||
osVersion,
|
||||
munkiIssueId,
|
||||
munkiIssueDetails,
|
||||
policyResponse,
|
||||
osVersions,
|
||||
softwareDetails,
|
||||
policy,
|
||||
mdmSolutionDetails,
|
||||
},
|
||||
selectedLabel,
|
||||
isOnlyObserver,
|
||||
handleClearRouteParam,
|
||||
handleClearFilter,
|
||||
onChangePoliciesFilter,
|
||||
onChangeMacSettingsFilter,
|
||||
onClickEditLabel,
|
||||
onClickDeleteLabel,
|
||||
}: IHostsFilterBlockProps) => {
|
||||
const renderLabelFilterPill = () => {
|
||||
if (selectedLabel) {
|
||||
const { description, display_text, label_type } = selectedLabel;
|
||||
const pillLabel =
|
||||
PLATFORM_LABEL_DISPLAY_NAMES[display_text] ?? display_text;
|
||||
|
||||
return (
|
||||
<>
|
||||
<FilterPill
|
||||
label={pillLabel}
|
||||
tooltipDescription={description}
|
||||
onClear={handleClearRouteParam}
|
||||
/>
|
||||
{label_type !== "builtin" && !isOnlyObserver && (
|
||||
<>
|
||||
<Button onClick={onClickEditLabel} variant={"text-icon"}>
|
||||
<img src={PencilIcon} alt="Edit label" />
|
||||
</Button>
|
||||
<Button onClick={onClickDeleteLabel} variant={"text-icon"}>
|
||||
<img src={TrashIcon} alt="Delete label" />
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
// TODO: Move formatOperatingSystemDisplayName into utils file
|
||||
const label = formatOperatingSystemDisplayName(
|
||||
name_only || version
|
||||
? `${name_only || ""} ${version || ""}`
|
||||
: `${name || ""}`
|
||||
);
|
||||
const TooltipDescription = (
|
||||
<span>
|
||||
Hosts with {formatOperatingSystemDisplayName(name_only || name)},
|
||||
<br />
|
||||
{version && `${version} installed`}
|
||||
</span>
|
||||
);
|
||||
|
||||
return (
|
||||
<FilterPill
|
||||
label={label}
|
||||
tooltipDescription={TooltipDescription}
|
||||
onClear={() => handleClearFilter(["os_id", "os_name", "os_version"])}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
// NOTE: good example of filter dropdown with pill
|
||||
const renderPoliciesFilterBlock = () => (
|
||||
<>
|
||||
<PoliciesFilter
|
||||
policyResponse={policyResponse}
|
||||
onChange={onChangePoliciesFilter}
|
||||
/>
|
||||
<FilterPill
|
||||
icon={PolicyIcon}
|
||||
label={policy?.name ?? "..."}
|
||||
onClear={() => handleClearFilter(["policy_id", "policy_response"])}
|
||||
className={`${baseClass}__policies-filter-pill`}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
const renderMacSettingsStatusFilterBlock = () => {
|
||||
const label = "macOS settings";
|
||||
return (
|
||||
<>
|
||||
<Dropdown
|
||||
value={macSettingsStatus}
|
||||
className={`${baseClass}__macsettings-dropdown`}
|
||||
options={MAC_SETTINGS_FILTER_OPTIONS}
|
||||
onChange={onChangeMacSettingsFilter}
|
||||
/>
|
||||
<FilterPill
|
||||
label={label}
|
||||
onClear={() => handleClearFilter(["macos_settings"])}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const renderSoftwareFilterBlock = () => {
|
||||
if (!softwareDetails) return null;
|
||||
|
||||
const { name, version } = softwareDetails;
|
||||
const label = `${name || "Unknown software"} ${version || ""}`;
|
||||
|
||||
const TooltipDescription = (
|
||||
<span>
|
||||
Hosts with {name || "Unknown software"},
|
||||
<br />
|
||||
{version || "version unknown"} installed
|
||||
</span>
|
||||
);
|
||||
|
||||
return (
|
||||
<FilterPill
|
||||
label={label}
|
||||
onClear={() => handleClearFilter(["software_id"])}
|
||||
tooltipDescription={TooltipDescription}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const renderMDMSolutionFilterBlock = () => {
|
||||
if (!mdmSolutionDetails) return null;
|
||||
|
||||
const { name, server_url } = mdmSolutionDetails;
|
||||
const label = name ? `${name} ${server_url}` : `${server_url}`;
|
||||
|
||||
const TooltipDescription = (
|
||||
<span>
|
||||
Host enrolled
|
||||
{name !== "Unknown" && ` to ${name}`}
|
||||
<br /> at {server_url}
|
||||
</span>
|
||||
);
|
||||
|
||||
return (
|
||||
<FilterPill
|
||||
label={label}
|
||||
tooltipDescription={TooltipDescription}
|
||||
onClear={() => handleClearFilter(["mdm_id"])}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const renderMDMEnrollmentFilterBlock = () => {
|
||||
if (!mdmEnrollmentStatus) return null;
|
||||
|
||||
const label = `MDM status: ${
|
||||
// TODO: move MDM_ENROLLMENT_STATUS to util file
|
||||
invert(MDM_ENROLLMENT_STATUS)[mdmEnrollmentStatus]
|
||||
}`;
|
||||
|
||||
// More narrow tooltip than other MDM tooltip
|
||||
const MDM_STATUS_PILL_TOOLTIP: Record<string, JSX.Element> = {
|
||||
automatic: (
|
||||
<span>
|
||||
MDM was turned on <br />
|
||||
automatically using Apple <br />
|
||||
Automated Device <br />
|
||||
Enrollment (DEP) or <br />
|
||||
Windows Autopilot. <br />
|
||||
Administrators can block <br />
|
||||
device users from turning
|
||||
<br /> MDM off.
|
||||
</span>
|
||||
),
|
||||
manual: (
|
||||
<span>
|
||||
MDM was turned on <br />
|
||||
manually. Device users <br />
|
||||
can turn MDM off.
|
||||
</span>
|
||||
),
|
||||
unenrolled: (
|
||||
<span>
|
||||
Hosts with MDM off <br />
|
||||
don't receive macOS <br />
|
||||
settings and macOS <br />
|
||||
update encouragement.
|
||||
</span>
|
||||
),
|
||||
pending: (
|
||||
<span>
|
||||
Hosts ordered using Apple <br />
|
||||
Business Manager (ABM). <br />
|
||||
They will automatically enroll <br />
|
||||
to Fleet and turn on MDM <br />
|
||||
when they're unboxed.
|
||||
</span>
|
||||
),
|
||||
};
|
||||
|
||||
return (
|
||||
<FilterPill
|
||||
label={label}
|
||||
tooltipDescription={MDM_STATUS_PILL_TOOLTIP[mdmEnrollmentStatus]}
|
||||
onClear={() => handleClearFilter(["mdm_enrollment_status"])}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const renderMunkiIssueFilterBlock = () => {
|
||||
if (munkiIssueDetails) {
|
||||
return (
|
||||
<FilterPill
|
||||
label={munkiIssueDetails.name}
|
||||
tooltipDescription={
|
||||
<span>
|
||||
Hosts that reported this Munki issue <br />
|
||||
the last time Munki ran on each host.
|
||||
</span>
|
||||
}
|
||||
onClear={() => handleClearFilter(["munki_issue_id"])}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const renderLowDiskSpaceFilterBlock = () => {
|
||||
const TooltipDescription = (
|
||||
<span>
|
||||
Hosts that have {lowDiskSpaceHosts} GB or less <br />
|
||||
disk space available.
|
||||
</span>
|
||||
);
|
||||
|
||||
return (
|
||||
<FilterPill
|
||||
label="Low disk space"
|
||||
tooltipDescription={TooltipDescription}
|
||||
onClear={() => handleClearFilter(["low_disk_space"])}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const showSelectedLabel = selectedLabel && selectedLabel.type !== "all";
|
||||
|
||||
if (
|
||||
showSelectedLabel ||
|
||||
policyId ||
|
||||
macSettingsStatus ||
|
||||
softwareId ||
|
||||
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 (
|
||||
<div className={`${baseClass}__labels-active-filter-wrap`}>
|
||||
{renderFilterPill()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default HostsFilterBlock;
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
.hosts-filter-block {
|
||||
&__labels-active-filter-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: $pad-medium;
|
||||
gap: $pad-small; // between multiple filter pills
|
||||
}
|
||||
|
||||
// NOTE: can probably be removed and placed anytime we have a dropdown. Need to
|
||||
// go through other filter dropdowns and see if thats the case
|
||||
&__policies-filter-pill {
|
||||
margin-left: $pad-medium;
|
||||
}
|
||||
|
||||
// NOTE: Look more into this styling
|
||||
&__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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./HostsFilterBlock";
|
||||
|
|
@ -143,6 +143,10 @@ export const createCustomRenderer = (renderOptions?: ICustomRenderOptions) => {
|
|||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* This is a convenince method that calls the render method from `@testing-library/react` and also
|
||||
* sets up the also `user-events`library and adds the user object to the returned object.
|
||||
*/
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const renderWithSetup = (component: JSX.Element) => {
|
||||
return {
|
||||
|
|
|
|||
Loading…
Reference in a new issue