Implement dropdown for label filters on manage hosts page (#7300)

* add label filter dropdown to the manage host page table

* increase label filter select width

* improve responsive styling for manage host page
This commit is contained in:
Gabriel Hernandez
2022-08-24 13:38:43 +01:00
committed by GitHub
parent dfd7d3f244
commit 041920bcaf
33 changed files with 13551 additions and 12218 deletions
+19
View File
@@ -0,0 +1,19 @@
{
"Fleet - React stateless component with TS": {
"scope": "typescriptreact",
"prefix": "rtsc",
"body": [
"import React from \"react\";",
"",
"interface I${TM_FILENAME_BASE}Props {}",
"",
"const $TM_FILENAME_BASE = ({}: I${TM_FILENAME_BASE}Props) => {",
" return <></>;",
"};",
"",
"export default $TM_FILENAME_BASE;",
"",
],
"description": "Creates a React stateless component with the typescrip interface setup"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 384 B

@@ -0,0 +1,2 @@
* This updates the manage host page for better usability on smaller screen widths. It remover the
labels sidebar and adds them into a table filter select dropdown.
+3 -7
View File
@@ -194,7 +194,7 @@ describe("Hosts flow", () => {
});
});
});
it("renders and searches the host's software, links to filter hosts by software", () => {
it("renders and searches the host's software, links to filter hosts by software", () => {
cy.getAttached(".react-tabs__tab-list").within(() => {
cy.findByText(/software/i).click();
});
@@ -223,11 +223,7 @@ describe("Hosts flow", () => {
});
cy.getAttached(".software-link").first().click({ force: true });
});
cy.getAttached(".manage-hosts__software-filter-block").within(() => {
cy.getAttached(".manage-hosts__software-filter-name-card").should(
"exist"
);
});
cy.findByText(/libacl1 2.2.53-6/i).should("exist");
cy.getAttached(".data-table").within(() => {
cy.findByText(hostname).should("exist");
});
@@ -248,7 +244,7 @@ describe("Hosts flow", () => {
cy.findByText(/failing 1 policy/i).should("exist");
cy.getAttached(".policy-link").first().click({ force: true });
});
cy.getAttached(".manage-hosts__policies-filter-name-card").should(
cy.findAllByText(/Is Filevault enabled on macOS devices/i).should(
"exist"
);
cy.getAttached(".data-table").within(() => {
+15 -10
View File
@@ -3,10 +3,12 @@ describe("Labels flow", () => {
Cypress.session.clearAllSavedSessions();
cy.setup();
cy.loginWithCySession();
cy.addDockerHost();
cy.viewport(1200, 660);
});
after(() => {
cy.logout();
cy.stopDockerHost();
});
describe("Manage hosts page", () => {
@@ -15,6 +17,7 @@ describe("Labels flow", () => {
cy.visit("/hosts/manage");
});
it("creates a custom label", () => {
cy.getAttached(".label-filter-select__control").click();
cy.findByRole("button", { name: /add label/i }).click();
cy.getAttached(".ace_content").type(
"{selectall}{backspace}SELECT * FROM users;"
@@ -31,10 +34,9 @@ describe("Labels flow", () => {
cy.findByText(/label created/i).should("exist");
});
it("edits a custom label", () => {
cy.getAttached(".host-side-panel").within(() => {
cy.findByText(/show all mac users/i).click();
});
cy.getAttached(".manage-hosts__label-block button").first().click();
cy.getAttached(".label-filter-select__control").click();
cy.findByText(/Show all MAC users/i).click();
cy.findByRole("button", { name: /edit label/i }).click();
// SQL and Platform are immutable fields
cy.findByLabelText(/name/i).clear().type("Show all mac usernames");
cy.findByLabelText(/description/i)
@@ -45,18 +47,18 @@ describe("Labels flow", () => {
cy.findByText(/label updated/i).should("exist");
});
it("deletes a custom label", () => {
cy.getAttached(".host-side-panel").within(() => {
cy.findByText(/show all mac usernames/i).click();
});
cy.getAttached(".manage-hosts__label-block button").last().click();
cy.getAttached(".label-filter-select__control").click();
cy.findByText(/Show all mac usernames/i).click();
cy.findByRole("button", { name: /delete label/i }).click();
cy.getAttached(".delete-label-modal")
.contains("button", /delete/i)
.click();
cy.getAttached(".host-side-panel").within(() => {
cy.getAttached(".label-filter-select__control").within(() => {
cy.findByText(/show all mac usernames/i).should("not.exist");
});
});
it("creates labels with special characters", () => {
cy.getAttached(".label-filter-select__control").click();
cy.findByRole("button", { name: /add label/i }).click();
cy.getAttached(".ace_content").type(
"{selectall}{backspace}SELECT * FROM users;"
@@ -75,7 +77,10 @@ describe("Labels flow", () => {
cy.findByText(/label created/i).should("exist");
});
it("searches labels with special characters", () => {
cy.getAttached("#tags-filter").type("{selectall}{backspace}**");
cy.getAttached(".label-filter-select__control").click();
cy.findByPlaceholderText(/filter labels by name.../i).type(
"{selectall}{backspace}**"
);
cy.findByText(/Special label/i).should("exist");
});
});
@@ -467,11 +467,11 @@ describe("Policies flow (seeded)", () => {
cy.getAttached(".button--text-link").click();
});
// confirm policy functionality on manage host page
cy.getAttached(".manage-hosts__policies-filter-block").within(() => {
cy.getAttached(".manage-hosts__labels-active-filter-wrap").within(() => {
cy.findByText(/filevault enabled/i).should("exist");
cy.findByText(/no/i).should("exist").click();
cy.findByText(/yes/i).should("exist");
cy.get('img[alt="Remove policy filter"]').click();
cy.get('img[alt="Remove filter"]').click();
cy.findByText(/filevault enabled'/i).should("not.exist");
});
});
+12 -20
View File
@@ -109,7 +109,9 @@ describe(
});
it("views all hosts for all platforms", () => {
cy.findByText(/view all hosts/i).click();
cy.get(".manage-hosts__label-block").should("not.exist");
cy.findByRole("status", { name: /hosts filtered by/i }).should(
"not.exist"
);
});
it("views all hosts for windows only", () => {
cy.getAttached(".homepage__platforms").within(() => {
@@ -117,11 +119,9 @@ describe(
cy.findByText(/windows/i).click();
});
cy.findByText(/view all hosts/i).click();
cy.getAttached(".manage-hosts__label-block").within(() => {
cy.getAttached(".title").within(() => {
cy.findByText(/windows/i).should("exist");
});
});
cy.findByRole("status", { name: /hosts filtered by Windows/i }).should(
"exist"
);
});
it("views all hosts for linux only", () => {
cy.getAttached(".homepage__platforms").within(() => {
@@ -129,11 +129,9 @@ describe(
cy.findByText(/linux/i).click();
});
cy.findByText(/view all hosts/i).click();
cy.getAttached(".manage-hosts__label-block").within(() => {
cy.getAttached(".title").within(() => {
cy.findByText(/linux/i).should("exist");
});
});
cy.findByRole("status", { name: /hosts filtered by Linux/i }).should(
"exist"
);
});
it("views all hosts for macOS only", () => {
cy.getAttached(".homepage__platforms").within(() => {
@@ -141,11 +139,9 @@ describe(
cy.findByText(/macos/i).click();
});
cy.findByText(/view all hosts/i).click();
cy.getAttached(".manage-hosts__label-block").within(() => {
cy.getAttached(".title").within(() => {
cy.findByText(/macos/i).should("exist");
});
});
cy.findByRole("status", { name: /hosts filtered by macOS/i }).should(
"exist"
);
});
});
describe("Manage hosts page", () => {
@@ -166,10 +162,6 @@ describe(
cy.contains("button", /save/i).click();
cy.contains("button", /done/i).click();
});
it("allows admin to open the 'Add label' form", () => {
cy.findByRole("button", { name: /add label/i }).click();
cy.findByRole("button", { name: /cancel/i }).click();
});
});
describe("Host details tests", () => {
beforeEach(() => {
+14 -17
View File
@@ -101,7 +101,9 @@ describe(
});
it("views all hosts for all platforms", () => {
cy.findByText(/view all hosts/i).click();
cy.get(".manage-hosts__label-block").should("not.exist");
cy.findByRole("status", { name: /hosts filtered by/i }).should(
"not.exist"
);
});
it("views all hosts for windows only", () => {
cy.getAttached(".homepage__platforms").within(() => {
@@ -109,11 +111,9 @@ describe(
cy.findByText(/windows/i).click();
});
cy.findByText(/view all hosts/i).click();
cy.getAttached(".manage-hosts__label-block").within(() => {
cy.getAttached(".title").within(() => {
cy.findByText(/windows/i).should("exist");
});
});
cy.findByRole("status", { name: /hosts filtered by Windows/i }).should(
"exist"
);
});
it("views all hosts for linux only", () => {
cy.getAttached(".homepage__platforms").within(() => {
@@ -121,11 +121,9 @@ describe(
cy.findByText(/linux/i).click();
});
cy.findByText(/view all hosts/i).click();
cy.getAttached(".manage-hosts__label-block").within(() => {
cy.getAttached(".title").within(() => {
cy.findByText(/linux/i).should("exist");
});
});
cy.findByRole("status", { name: /hosts filtered by Linux/i }).should(
"exist"
);
});
it("views all hosts for macOS only", () => {
cy.getAttached(".homepage__platforms").within(() => {
@@ -133,11 +131,9 @@ describe(
cy.findByText(/macos/i).click();
});
cy.findByText(/view all hosts/i).click();
cy.getAttached(".manage-hosts__label-block").within(() => {
cy.getAttached(".title").within(() => {
cy.findByText(/macos/i).should("exist");
});
});
cy.findByRole("status", { name: /hosts filtered by macOS/i }).should(
"exist"
);
});
});
describe("Manage hosts page", () => {
@@ -162,8 +158,9 @@ describe(
cy.contains("button", /done/i).click();
});
it("allows maintainer to open the 'Add label' form", () => {
cy.getAttached(".label-filter-select__control").click();
cy.findByRole("button", { name: /add label/i }).click();
cy.findByRole("button", { name: /cancel/i }).click();
cy.findByText(/New Label/i).should("exist");
});
});
describe("Host details tests", () => {
+12 -16
View File
@@ -96,7 +96,9 @@ describe("Free tier - Observer user", () => {
});
it("views all hosts for all platforms", () => {
cy.findByText(/view all hosts/i).click();
cy.get(".manage-hosts__label-block").should("not.exist");
cy.findByRole("status", { name: /hosts filtered by/i }).should(
"not.exist"
);
});
it("views all hosts for windows only", () => {
cy.getAttached(".homepage__platforms").within(() => {
@@ -104,11 +106,9 @@ describe("Free tier - Observer user", () => {
cy.findByText(/windows/i).click();
});
cy.findByText(/view all hosts/i).click();
cy.getAttached(".manage-hosts__label-block").within(() => {
cy.getAttached(".title").within(() => {
cy.findByText(/windows/i).should("exist");
});
});
cy.findByRole("status", { name: /hosts filtered by Windows/i }).should(
"exist"
);
});
it("views all hosts for linux only", () => {
cy.getAttached(".homepage__platforms").within(() => {
@@ -116,11 +116,9 @@ describe("Free tier - Observer user", () => {
cy.findByText(/linux/i).click();
});
cy.findByText(/view all hosts/i).click();
cy.getAttached(".manage-hosts__label-block").within(() => {
cy.getAttached(".title").within(() => {
cy.findByText(/linux/i).should("exist");
});
});
cy.findByRole("status", { name: /hosts filtered by linux/i }).should(
"exist"
);
});
it("views all hosts for macOS only", () => {
cy.getAttached(".homepage__platforms").within(() => {
@@ -128,11 +126,9 @@ describe("Free tier - Observer user", () => {
cy.findByText(/macos/i).click();
});
cy.findByText(/view all hosts/i).click();
cy.getAttached(".manage-hosts__label-block").within(() => {
cy.getAttached(".title").within(() => {
cy.findByText(/macos/i).should("exist");
});
});
cy.findByRole("status", { name: /hosts filtered by macOS/i }).should(
"exist"
);
});
});
describe("Manage hosts page", () => {
+14 -18
View File
@@ -93,19 +93,19 @@ describe("Premium tier - Maintainer user", () => {
});
it("views all hosts for all platforms", () => {
cy.findByText(/view all hosts/i).click();
cy.get(".manage-hosts__label-block").should("not.exist");
cy.findByRole("status", { name: /hosts filtered by/i }).should(
"not.exist"
);
});
it("views all hosts for windows only", () => {
cy.getAttached(".homepage__platforms").within(() => {
cy.getAttached(".Select-control").click();
cy.findByText(/windows/i).click();
cy.findByText(/Windows/i).click();
});
cy.findByText(/view all hosts/i).click();
cy.getAttached(".manage-hosts__label-block").within(() => {
cy.getAttached(".title").within(() => {
cy.findByText(/windows/i).should("exist");
});
});
cy.findByRole("status", { name: /hosts filtered by Windows/i }).should(
"exist"
);
});
it("views all hosts for linux only", () => {
cy.getAttached(".homepage__platforms").within(() => {
@@ -113,23 +113,19 @@ describe("Premium tier - Maintainer user", () => {
cy.findByText(/linux/i).click();
});
cy.findByText(/view all hosts/i).click();
cy.getAttached(".manage-hosts__label-block").within(() => {
cy.getAttached(".title").within(() => {
cy.findByText(/linux/i).should("exist");
});
});
cy.findByRole("status", { name: /hosts filtered by Linux/i }).should(
"exist"
);
});
it("views all hosts for macOS only", () => {
cy.getAttached(".homepage__platforms").within(() => {
cy.get(".homepage__platforms").within(() => {
cy.getAttached(".Select-control").click();
cy.findByText(/macos/i).click();
});
cy.findByText(/view all hosts/i).click();
cy.getAttached(".manage-hosts__label-block").within(() => {
cy.getAttached(".title").within(() => {
cy.findByText(/macos/i).should("exist");
});
});
cy.findByRole("status", { name: /hosts filtered by macOS/i }).should(
"exist"
);
});
});
describe("Manage hosts page", () => {
+12 -16
View File
@@ -94,7 +94,9 @@ describe("Premium tier - Observer user", () => {
});
it("views all hosts for all platforms", () => {
cy.findByText(/view all hosts/i).click();
cy.get(".manage-hosts__label-block").should("not.exist");
cy.findByRole("status", { name: /hosts filtered by/i }).should(
"not.exist"
);
});
it("views all hosts for windows only", () => {
cy.getAttached(".homepage__platforms").within(() => {
@@ -102,11 +104,9 @@ describe("Premium tier - Observer user", () => {
cy.findByText(/windows/i).click();
});
cy.findByText(/view all hosts/i).click();
cy.getAttached(".manage-hosts__label-block").within(() => {
cy.getAttached(".title").within(() => {
cy.findByText(/windows/i).should("exist");
});
});
cy.findByRole("status", { name: /hosts filtered by Windows/i }).should(
"exist"
);
});
it("views all hosts for linux only", () => {
cy.getAttached(".homepage__platforms").within(() => {
@@ -114,11 +114,9 @@ describe("Premium tier - Observer user", () => {
cy.findByText(/linux/i).click();
});
cy.findByText(/view all hosts/i).click();
cy.getAttached(".manage-hosts__label-block").within(() => {
cy.getAttached(".title").within(() => {
cy.findByText(/linux/i).should("exist");
});
});
cy.findByRole("status", { name: /hosts filtered by Linux/i }).should(
"exist"
);
});
it("views all hosts for macOS only", () => {
cy.getAttached(".homepage__platforms").within(() => {
@@ -126,11 +124,9 @@ describe("Premium tier - Observer user", () => {
cy.findByText(/macos/i).click();
});
cy.findByText(/view all hosts/i).click();
cy.getAttached(".manage-hosts__label-block").within(() => {
cy.getAttached(".title").within(() => {
cy.findByText(/macos/i).should("exist");
});
});
cy.findByRole("status", { name: /hosts filtered by macOS/i }).should(
"exist"
);
});
});
describe("Manage hosts page", () => {
+12 -16
View File
@@ -99,7 +99,9 @@ describe("Premium tier - Team Admin user", () => {
});
it("views all hosts for all platforms", () => {
cy.findByText(/view all hosts/i).click();
cy.get(".manage-hosts__label-block").should("not.exist");
cy.findByRole("status", { name: /hosts filtered by/i }).should(
"not.exist"
);
});
it("views all hosts for windows only", () => {
cy.getAttached(".homepage__platforms").within(() => {
@@ -107,11 +109,9 @@ describe("Premium tier - Team Admin user", () => {
cy.findByText(/windows/i).click();
});
cy.findByText(/view all hosts/i).click();
cy.getAttached(".manage-hosts__label-block").within(() => {
cy.getAttached(".title").within(() => {
cy.findByText(/windows/i).should("exist");
});
});
cy.findByRole("status", { name: /hosts filtered by Windows/i }).should(
"exist"
);
});
it("views all hosts for linux only", () => {
cy.getAttached(".homepage__platforms").within(() => {
@@ -119,11 +119,9 @@ describe("Premium tier - Team Admin user", () => {
cy.findByText(/linux/i).click();
});
cy.findByText(/view all hosts/i).click();
cy.getAttached(".manage-hosts__label-block").within(() => {
cy.getAttached(".title").within(() => {
cy.findByText(/linux/i).should("exist");
});
});
cy.findByRole("status", { name: /hosts filtered by Linux/i }).should(
"exist"
);
});
it("views all hosts for macOS only", () => {
cy.getAttached(".homepage__platforms").within(() => {
@@ -131,11 +129,9 @@ describe("Premium tier - Team Admin user", () => {
cy.findByText(/macos/i).click();
});
cy.findByText(/view all hosts/i).click();
cy.getAttached(".manage-hosts__label-block").within(() => {
cy.getAttached(".title").within(() => {
cy.findByText(/macos/i).should("exist");
});
});
cy.findByRole("status", { name: /hosts filtered by macOS/i }).should(
"exist"
);
});
});
describe("Manage hosts page", () => {
@@ -94,7 +94,9 @@ describe("Premium tier - Team observer/maintainer user", () => {
});
it("views all hosts for all platforms", () => {
cy.findByText(/view all hosts/i).click();
cy.get(".manage-hosts__label-block").should("not.exist");
cy.findByRole("status", { name: /hosts filtered by/i }).should(
"not.exist"
);
});
it("views all hosts for windows only", () => {
cy.getAttached(".homepage__platforms").within(() => {
@@ -102,11 +104,9 @@ describe("Premium tier - Team observer/maintainer user", () => {
cy.findByText(/windows/i).click();
});
cy.findByText(/view all hosts/i).click();
cy.getAttached(".manage-hosts__label-block").within(() => {
cy.getAttached(".title").within(() => {
cy.findByText(/windows/i).should("exist");
});
});
cy.findByRole("status", { name: /hosts filtered by Windows/i }).should(
"exist"
);
});
it("views all hosts for linux only", () => {
cy.getAttached(".homepage__platforms").within(() => {
@@ -114,11 +114,9 @@ describe("Premium tier - Team observer/maintainer user", () => {
cy.findByText(/linux/i).click();
});
cy.findByText(/view all hosts/i).click();
cy.getAttached(".manage-hosts__label-block").within(() => {
cy.getAttached(".title").within(() => {
cy.findByText(/linux/i).should("exist");
});
});
cy.findByRole("status", { name: /hosts filtered by Linux/i }).should(
"exist"
);
});
it("views all hosts for macOS only", () => {
cy.getAttached(".homepage__platforms").within(() => {
@@ -126,11 +124,9 @@ describe("Premium tier - Team observer/maintainer user", () => {
cy.findByText(/macos/i).click();
});
cy.findByText(/view all hosts/i).click();
cy.getAttached(".manage-hosts__label-block").within(() => {
cy.getAttached(".title").within(() => {
cy.findByText(/macos/i).should("exist");
});
});
cy.findByRole("status", { name: /hosts filtered by macOS/i }).should(
"exist"
);
});
});
});
@@ -5,7 +5,6 @@ import { formatDistanceToNow } from "date-fns";
import SandboxExpiryMessage from "components/Sandbox/SandboxExpiryMessage";
import SandboxGate from "components/Sandbox/SandboxGate";
import { AppContext } from "context/app";
import exp from "constants";
interface IMainContentProps {
children: ReactChild;
@@ -3,7 +3,6 @@ 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 } from "lodash";
import ReactTooltip from "react-tooltip";
import { format } from "date-fns";
import FileSaver from "file-saver";
@@ -55,7 +54,6 @@ import {
import Button from "components/buttons/Button";
// @ts-ignore
import Dropdown from "components/forms/fields/Dropdown";
import HostSidePanel from "components/side_panels/HostSidePanel";
import QuerySidePanel from "components/side_panels/QuerySidePanel";
import TableContainer from "components/TableContainer";
import TableDataError from "components/DataError";
@@ -83,7 +81,7 @@ import {
} from "./constants";
import { isAcceptableStatus, getNextLocationPath } from "./helpers";
import LabelForm from "../components/LabelForm";
import LabelForm from "./components/LabelForm";
import DeleteSecretModal from "../../../components/DeleteSecretModal";
import SecretEditorModal from "../../../components/SecretEditorModal";
import AddHostsModal from "../../../components/AddHostsModal";
@@ -99,10 +97,11 @@ 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 CloseIcon from "../../../../assets/images/icon-close-vibrant-blue-16x16@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";
interface IManageHostsProps {
route: RouteProps;
@@ -267,7 +266,7 @@ const ManageHostsPage = ({
const isAddLabel = location.hash === NEW_LABEL_HASH;
const isEditLabel = location.hash === EDIT_LABEL_HASH;
const routeTemplate = route && route.path ? route.path : "";
const routeTemplate = route?.path ?? "";
const policyId = queryParams?.policy_id;
const policyResponse: PolicyResponse = queryParams?.policy_response;
const softwareId =
@@ -292,20 +291,15 @@ const ManageHostsPage = ({
const canEnrollHosts =
isGlobalAdmin || isGlobalMaintainer || isTeamAdmin || isTeamMaintainer;
const canEnrollGlobalHosts = isGlobalAdmin || isGlobalMaintainer;
const canAddNewLabels = isGlobalAdmin || isGlobalMaintainer;
const canAddNewLabels = (isGlobalAdmin || isGlobalMaintainer) ?? false;
const {
isLoading: isLabelsLoading,
data: labels,
error: labelsError,
refetch: refetchLabels,
} = useQuery<ILabelsResponse, Error, ILabel[]>(
["labels"],
() => labelsAPI.loadAll(),
{
select: (data: ILabelsResponse) => data.labels,
}
);
const { data: labels, error: labelsError, refetch: refetchLabels } = useQuery<
ILabelsResponse,
Error,
ILabel[]
>(["labels"], () => labelsAPI.loadAll(), {
select: (data: ILabelsResponse) => data.labels,
});
const {
isLoading: isGlobalSecretsLoading,
@@ -712,9 +706,7 @@ const ManageHostsPage = ({
handleLabelChange(selected as ILabel);
};
const onAddLabelClick = (evt: React.MouseEvent<HTMLButtonElement>) => {
evt.preventDefault();
const onAddLabelClick = () => {
setLabelValidator(DEFAULT_CREATE_LABEL_ERRORS);
router.push(`${PATHS.MANAGE_HOSTS}${NEW_LABEL_HASH}`);
};
@@ -967,13 +959,6 @@ const ManageHostsPage = ({
});
};
const onLabelClick = (label: ILabel) => {
return (evt: React.MouseEvent<HTMLButtonElement>) => {
evt.preventDefault();
handleLabelChange(label);
};
};
const onOsqueryTableSelect = (tableName: string) => {
setSelectedOsqueryTable(tableName);
};
@@ -1001,6 +986,13 @@ const ManageHostsPage = ({
});
};
const onClearLabelFilter = () => {
const allHostsLabel = labels?.find((label) => label.name === "All Hosts");
if (allHostsLabel !== undefined) {
handleLabelChange(allHostsLabel);
}
};
const onDeleteLabel = async () => {
if (!selectedLabel) {
console.error("Label isn't available. This should not happen.");
@@ -1162,10 +1154,39 @@ const ManageHostsPage = ({
/>
);
const renderOSFilterBlock = () => {
if (!os_id && !(os_name && os_version)) {
return <></>;
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={onClearLabelFilter}
/>
{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 (!os_id && !(os_name && os_version)) return null;
let os: IOperatingSystemVersion | undefined;
if (os_id) {
os = osVersions?.find((v) => v.os_id === os_id);
@@ -1179,236 +1200,142 @@ const ManageHostsPage = ({
version.toLowerCase() === vers.toLowerCase()
);
}
if (!os) return null;
if (!os) {
return <></>;
}
const { name, name_only, version } = os;
const buttonText =
const label =
name_only || version
? `${name_only || ""} ${version || ""}`
: `${name || ""}`;
const TooltipDescription = (
<span className={`tooltip__tooltip-text`}>
{`Hosts with ${name_only || name}`},<br />
{version && `${version} installed`}
</span>
);
return (
<div className={`${baseClass}__software-filter-block`}>
<div>
<span
data-tip
data-for="software-filter-tooltip"
data-tip-disable={!name_only || !version || !name}
>
<div className={`${baseClass}__software-filter-name-card tooltip`}>
{buttonText}
<Button
className={`${baseClass}__clear-policies-filter`}
onClick={handleClearOSFilter}
variant={"small-text-icon"}
title={buttonText}
>
<img src={CloseIcon} alt="Remove os filter" />
</Button>
</div>
</span>
<ReactTooltip
place="bottom"
effect="solid"
backgroundColor="#3e4771"
id="software-filter-tooltip"
data-html
>
<span className={`tooltip__tooltip-text`}>
{`Hosts with ${name_only || name}`},<br />
{version && `${version} installed`}
</span>
</ReactTooltip>
</div>
</div>
<FilterPill
label={label}
tooltipDescription={TooltipDescription}
onClear={handleClearOSFilter}
/>
);
};
const renderPoliciesFilterBlock = () => (
<div className={`${baseClass}__policies-filter-block`}>
<>
<PoliciesFilter
policyResponse={policyResponse}
onChange={handleChangePoliciesFilter}
/>
<div className={`${baseClass}__policies-filter-name-card`}>
<img src={PolicyIcon} alt="Policy" />
{policy?.name}
<Button
className={`${baseClass}__clear-policies-filter`}
onClick={handleClearPoliciesFilter}
variant={"small-text-icon"}
title={policy?.name}
>
<img src={CloseIcon} alt="Remove policy filter" />
</Button>
</div>
</div>
<FilterPill
icon={PolicyIcon}
label={policy?.name ?? ""}
onClear={handleClearPoliciesFilter}
className={`${baseClass}__policies-filter-pill`}
/>
</>
);
const renderSoftwareFilterBlock = () => {
if (softwareDetails) {
const { name, version } = softwareDetails;
const buttonText = name && version ? `${name} ${version}` : "";
return (
<div className={`${baseClass}__software-filter-block`}>
<div>
<span
data-tip
data-for="software-filter-tooltip"
data-tip-disable={!name || !version}
>
<div
className={`${baseClass}__software-filter-name-card tooltip`}
>
{buttonText}
<Button
className={`${baseClass}__clear-software-filter`}
onClick={handleClearSoftwareFilter}
variant={"small-text-icon"}
title={buttonText}
>
<img src={CloseIcon} alt="Remove software filter" />
</Button>
</div>
</span>
<ReactTooltip
place="bottom"
effect="solid"
backgroundColor="#3e4771"
id="software-filter-tooltip"
data-html
>
<span className={`tooltip__tooltip-text`}>
{`Hosts with ${name}`},<br />
{`${version} installed`}
</span>
</ReactTooltip>
</div>
</div>
);
}
return null;
if (!softwareDetails) return null;
const { name, version } = softwareDetails;
const label = name && version ? `${name} ${version}` : "";
const TooltipDescription =
name && version ? (
<span className={`tooltip__tooltip-text`}>
{`Hosts with ${name}`},<br />
{`${version} installed`}
</span>
) : undefined;
return (
<FilterPill
label={label}
onClear={handleClearSoftwareFilter}
tooltipDescription={TooltipDescription}
/>
);
};
const renderMDMSolutionFilterBlock = () => {
if (mdmSolutionDetails) {
const { name, server_url } = mdmSolutionDetails;
const buttonText = name ? `${name} ${server_url}` : `${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;
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) {
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>
);
if (!mdmEnrollmentStatus) return null;
let label: string;
switch (mdmEnrollmentStatus) {
case "automatic":
label = "MDM enrolled (automatic)";
break;
case "manual":
label = "MDM enrolled (manual)";
break;
default:
label = "Unenrolled";
}
return null;
let TooltipDescription: JSX.Element;
switch (mdmEnrollmentStatus) {
case "automatic":
TooltipDescription = (
<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>
);
break;
case "manual":
TooltipDescription = (
<span className={`tooltip__tooltip-text`}>
Hosts manually enrolled to an <br />
MDM solution by a user or <br />
administrator.
</span>
);
break;
default:
TooltipDescription = (
<span className={`tooltip__tooltip-text`}>
Hosts not enrolled to <br /> an MDM solution.
</span>
);
}
return (
<FilterPill
label={label}
tooltipDescription={TooltipDescription}
onClear={handleClearMDMEnrollmentFilter}
/>
);
};
const renderEditColumnsModal = () => {
@@ -1512,41 +1439,6 @@ const ManageHostsPage = ({
/>
);
const renderHeaderLabelBlock = () => {
if (selectedLabel) {
const {
description,
display_text: displayText,
label_type: labelType,
} = selectedLabel;
return (
<div className={`${baseClass}__label-block`}>
<div className="title">
<span>
{PLATFORM_LABEL_DISPLAY_NAMES[displayText] || displayText}
</span>
{labelType !== "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>
</>
)}
</div>
<div className="description">
<span>{description}</span>
</div>
</div>
);
}
return null;
};
const renderHeader = () => (
<div className={`${baseClass}__header`}>
<div className={`${baseClass}__text`}>
@@ -1666,19 +1558,6 @@ const ManageHostsPage = ({
);
}, [isHostCountLoading, filteredHostCount]);
console.log(
"is active filter: ",
(!!os_id || (!!os_name && !!os_version)) &&
!policyId &&
!softwareId &&
!(
selectedLabel &&
selectedLabel.type !== "all" &&
selectedLabel.type !== "status"
) &&
!mdmId &&
!mdmEnrollmentStatus
);
const renderActiveFilterBlock = () => {
const showSelectedLabel =
selectedLabel &&
@@ -1695,7 +1574,7 @@ const ManageHostsPage = ({
) {
return (
<div className={`${baseClass}__labels-active-filter-wrap`}>
{showSelectedLabel && renderHeaderLabelBlock()}
{showSelectedLabel && renderLabelFilterPill()}
{!!policyId &&
!softwareId &&
!mdmId &&
@@ -1763,44 +1642,36 @@ const ManageHostsPage = ({
return false;
};
const renderSidePanel = () => {
let SidePanel;
const renderCustomControls = () => {
// we filter out the status labels as we dont want to display them in the label
// filter select dropdown.
// TODO: seperate labels and status into different data sets.
const selectedDropdownLabel =
selectedLabel?.type !== "all" && selectedLabel?.type !== "status"
? selectedLabel
: undefined;
if (isAddLabel) {
SidePanel = (
<QuerySidePanel
key="query-side-panel"
onOsqueryTableSelect={onOsqueryTableSelect}
selectedOsqueryTable={selectedOsqueryTable}
return (
<div className={`${baseClass}__filter-dropdowns`}>
<Dropdown
value={getStatusSelected() || ALL_HOSTS_LABEL}
className={`${baseClass}__status_dropdown`}
options={HOST_SELECT_STATUSES}
searchable={false}
onChange={handleStatusDropdownChange}
/>
);
} else {
SidePanel = (
<HostSidePanel
key="hosts-side-panel"
labels={labels}
onAddLabelClick={onAddLabelClick}
onLabelClick={onLabelClick}
selectedFilter={getLabelSelected() || getStatusSelected()}
canAddNewLabel={canAddNewLabels as boolean}
isLabelsLoading={isLabelsLoading}
<LabelFilterSelect
className={`${baseClass}__label-filter-dropdown`}
labels={labels ?? []}
canAddNewLabels={canAddNewLabels}
selectedLabel={selectedDropdownLabel ?? null}
onChange={handleLabelChange}
onAddLabel={onAddLabelClick}
/>
);
}
return SidePanel;
</div>
);
};
const renderStatusDropdown = () => (
<Dropdown
value={getStatusSelected() || ALL_HOSTS_LABEL}
className={`${baseClass}__status_dropdown`}
options={HOST_SELECT_STATUSES}
searchable={false}
onChange={handleStatusDropdownChange}
/>
);
const renderTable = () => {
if (
!config ||
@@ -1891,7 +1762,7 @@ const ManageHostsPage = ({
renderCount={renderHostCount}
searchToolTipText={HOSTS_SEARCH_BOX_TOOLTIP}
emptyComponent={EmptyHosts}
customControl={renderStatusDropdown}
customControl={renderCustomControls}
onActionButtonClick={toggleEditColumnsModal}
onPrimarySelectActionClick={onDeleteHostsClick}
onQueryChange={onTableQueryChange}
@@ -1992,7 +1863,15 @@ const ManageHostsPage = ({
)}
</>
</MainContent>
<SidePanelContent>{renderSidePanel()}</SidePanelContent>
{isAddLabel && (
<SidePanelContent>
<QuerySidePanel
key="query-side-panel"
onOsqueryTableSelect={onOsqueryTableSelect}
selectedOsqueryTable={selectedOsqueryTable}
/>
</SidePanelContent>
)}
{canEnrollHosts && showDeleteSecretModal && renderDeleteSecretModal()}
{canEnrollHosts && showSecretEditorModal && renderSecretEditorModal()}
@@ -1,5 +1,5 @@
.manage-hosts {
min-width: 768px;
.header-wrap {
display: flex;
align-items: center;
@@ -101,14 +101,68 @@
.table-container {
padding-top: $pad-small;
.table-container__header {
.table-container__header-left {
.controls {
&__header-left {
.controls {
display: flex;
align-items: center;
.manage-hosts__filter-dropdowns {
display: flex;
align-items: center;
}
}
}
// table header content responsive styles
@media (max-width: $break-990) {
&__header {
flex-direction: column;
}
.table-container__search {
order: 1;
width: 100%;
margin-bottom: $pad-small;
.table-container__search-input {
margin-left: 0;
& .search-field__input-wrapper {
width: auto;
}
}
}
.table-container__header-left {
order: 2;
display: flex;
flex-direction: column;
align-items: stretch;
.results-count {
order: 2;
}
.controls {
order: 1;
margin-bottom: $pad-large;
.manage-hosts__filter-dropdowns {
flex: 1;
.form-field--dropdown,
.manage-hosts__label-filter-dropdown {
flex: 1
}
.manage-hosts__status_dropdown {
width: auto;
}
}
}
}
}
.table-container__data-table-block {
.data-table-block {
.data-table {
@@ -188,58 +242,18 @@
}
}
&__labels-policies-wrap {
margin-bottom: $pad-medium;
&__label-filter-dropdown {
margin-left: $pad-small;
}
&__labels-active-filter-wrap {
display: flex;
align-items: center;
margin-bottom: $pad-medium;
}
&__policies-filter-block {
display: flex;
align-items: center;
gap: $pad-medium;
p {
font-size: $xx-small;
font-weight: $bold;
padding-left: $pad-medium;
}
}
&__policies-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;
color: $core-fleet-black;
font-size: $xx-small;
font-weight: $bold;
cursor: default;
img {
width: 12px;
height: 12px;
margin-right: 6px;
}
button {
height: auto;
padding: 0px;
border: none;
margin-left: $pad-small;
img {
padding: 0px;
margin: 0px;
}
}
&__policies-filter-pill {
margin-left: $pad-medium;
}
&__enroll-hosts {
@@ -0,0 +1,79 @@
import Button from "components/buttons/Button";
import { ILabel } from "interfaces/label";
import React, { useRef } from "react";
import { components, GroupHeadingProps } from "react-select-5";
import { IEmptyOption, IGroupOption } from "../LabelFilterSelect/helpers";
import PlusIcon from "../../../../../../assets/images/icon-plus-16x16@2x.png";
const baseClass = "custom-label-group-heading";
const CustomLabelGroupHeading = (
props: GroupHeadingProps<ILabel | IEmptyOption, false, IGroupOption>
) => {
const { data, selectProps } = props;
const {
labelQuery,
canAddNewLabels,
onAddLabel,
onChangeLabelQuery,
onClickLabelSeachInput,
onBlurLabelSearchInput,
} = selectProps;
const inputRef = useRef<HTMLInputElement | null>(null);
const handleInputClick = (
event: React.MouseEvent<HTMLInputElement, MouseEvent>
) => {
onClickLabelSeachInput(event);
inputRef.current?.focus();
event.stopPropagation();
};
return data.type === "platform" ? (
<components.GroupHeading {...props}>
<div className={`${baseClass}__labels-header`}>
<span className={`${baseClass}__label-title`}>{props.children}</span>
</div>
</components.GroupHeading>
) : (
<components.GroupHeading {...props}>
<div className={`${baseClass}__labels-header`}>
<span className={`${baseClass}__label-title`}>{props.children}</span>
<div className={`${baseClass}__add_new_label`}>
{canAddNewLabels && (
<Button
variant="text-icon"
onClick={onAddLabel}
className={`${baseClass}__add-label-btn`}
>
<>
<span>Add label</span>
<img src={PlusIcon} alt="Add label icon" />
</>
</Button>
)}
</div>
</div>
<div className={`${baseClass}__field`}>
<input
className={`${baseClass}__input`}
ref={inputRef}
value={labelQuery}
name="label-search-input"
type="text"
placeholder="Filter labels by name..."
onKeyDown={(event) => {
// Stops the parent dropdown from picking up on input keypresses
event.stopPropagation();
}}
onChange={onChangeLabelQuery}
onClick={handleInputClick}
onBlur={onBlurLabelSearchInput}
/>
</div>
</components.GroupHeading>
);
};
export default CustomLabelGroupHeading;
@@ -0,0 +1,115 @@
.custom-label-group-heading {
&__label-title {
color: $core-fleet-black;
font-size: $x-small;
font-weight: $bold;
text-transform: none;
}
&__labels-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 1rem;
}
&__add-label-btn {
display: flex;
align-items: center;
}
&__field {
position: relative;
&::before {
display: inline-block;
position: absolute;
padding: 5px 0 0 0; // centers spin
content: url(../assets/images/icon-search-fleet-black-16x16@2x.png);
transform: scale(0.5);
height: 20px;
top: 3px;
left: 8px;
}
}
&__input {
width: 100%;
line-height: 1.5;
background-color: $ui-light-grey;
border: solid 1px $ui-fleet-blue-15;
border-radius: 4px;
font-size: $small;
padding: $pad-xsmall 12px $pad-xsmall 42px;
color: $core-fleet-blue;
font-family: "Nunito Sans", sans-serif;
box-sizing: border-box;
height: 40px;
margin-bottom: $pad-medium;
&::placeholder {
color: $ui-fleet-black-50;
}
&:focus {
outline: none;
border-color: $core-vibrant-blue;
}
&--disabled {
color: $ui-fleet-black-50;
}
&--error {
color: $core-vibrant-red;
border: 1px solid $core-vibrant-red;
box-sizing: border-box;
border-radius: 4px;
&:focus {
border-color: $ui-error;
background-color: $core-white;
color: $core-fleet-black;
}
}
&__textarea {
min-height: 100px;
max-width: 100%;
display: block;
}
&__label {
display: block;
font-size: $medium;
font-weight: $regular;
color: $core-fleet-black;
margin-bottom: $pad-xsmall;
&--error {
font-weight: $bold;
color: $ui-error;
}
}
&__wrapper {
margin-bottom: $pad-medium;
}
&__hint {
font-size: $x-small;
font-weight: $regular;
line-height: 1.57;
letter-spacing: 1px;
color: $core-fleet-blue;
code {
color: $core-vibrant-blue;
background-color: $ui-gray;
padding: $pad-xxsmall;
font-family: "SourceCodePro", $monospace;
}
}
}
}
@@ -0,0 +1 @@
export { default } from "./CustomLabelGroupHeading";
@@ -0,0 +1,69 @@
import React, { ReactNode } from "react";
import ReactTooltip from "react-tooltip";
import classnames from "classnames";
import Button from "components/buttons/Button";
import CloseIcon from "../../../../../../assets/images/icon-close-vibrant-blue-16x16@2x.png";
interface IFilterPillProps {
label: string;
icon?: any; // TODO: figure out png image types
tooltipDescription?: string | ReactNode;
className?: string;
onClear: () => void;
}
const baseClass = "filter-pill";
const FilterPill = ({
label,
icon,
tooltipDescription,
className,
onClear,
}: IFilterPillProps) => {
const baseClasses = classnames(baseClass, className);
const labelClasses = classnames(`${baseClass}__label`, {
tooltip: tooltipDescription !== undefined && tooltipDescription !== "",
});
return (
<div
className={baseClasses}
role="status"
aria-label={`hosts filtered by ${label}`}
>
<>
<span data-tip={tooltipDescription} data-for="filter-pill-tooltip">
<div className={labelClasses}>
{icon && <img src={icon} alt="" />}
{label}
<Button
className={`${baseClass}__clear-filter`}
onClick={onClear}
variant={"small-text-icon"}
title={label}
>
<img src={CloseIcon} alt="Remove filter" />
</Button>
</div>
</span>
{tooltipDescription && (
<ReactTooltip
role="tooltip"
place="bottom"
effect="solid"
backgroundColor="#3e4771"
id="filter-pill-tooltip"
data-html
>
<span>{tooltipDescription}</span>
</ReactTooltip>
)}
</>
</div>
);
};
export default FilterPill;
@@ -0,0 +1,33 @@
.filter-pill {
&__label {
display: inline-flex;
align-items: center;
padding: 6px 12px;
border: 1px solid $ui-fleet-black-25;
border-radius: 4px;
box-shadow: none;
color: $core-fleet-black;
font-size: $xx-small;
font-weight: $bold;
cursor: default;
img {
width: 12px;
height: 12px;
margin-right: 6px;
}
button {
height: auto;
padding: 0px;
border: none;
margin-left: $pad-small;
img {
padding: 0px;
margin: 0px;
}
}
}
}
@@ -0,0 +1 @@
export { default } from "./FilterPill";
@@ -0,0 +1,175 @@
import React, { useMemo, useRef, useState } from "react";
import Select, { GroupBase, SelectInstance } from "react-select-5";
import classnames from "classnames";
import { ILabel } from "interfaces/label";
import { PLATFORM_LABEL_DISPLAY_NAMES } from "utilities/constants";
import CustomLabelGroupHeading from "../CustomLabelGroupHeading";
import { PLATFORM_TYPE_ICONS } from "./constants";
import { createDropdownOptions, IEmptyOption, IGroupOption } from "./helpers";
// Extending the react-select module to add custom props we need for our custom
// group heading. More info here:
// https://react-select.com/typescript#custom-select-props
declare module "react-select-5/dist/declarations/src/Select" {
export interface Props<
Option,
IsMulti extends boolean,
Group extends GroupBase<Option>
> {
labelQuery: string;
canAddNewLabels: boolean;
onAddLabel: () => void;
onChangeLabelQuery: (event: React.ChangeEvent<HTMLInputElement>) => void;
onClickLabelSeachInput: React.MouseEventHandler<HTMLInputElement>;
onBlurLabelSearchInput: React.FocusEventHandler<HTMLInputElement>;
}
}
/** A custom option label to show in the dropdown. Only used in this dropdown
* component. You will find focus and blur handlers in this component to help
* solve the problem of changing focus between the select dropdown and the
* label search input. */
const OptionLabel = (data: ILabel | IEmptyOption) => {
const isLabel = "display_text" in data;
const isPlatform = isLabel && data.type === "platform";
let labelText = isLabel ? data.display_text : data.label;
// the display names for platform options are slightly different then the display_text
// property, so we get the correct display name here
if (isLabel && isPlatform) {
labelText = PLATFORM_LABEL_DISPLAY_NAMES[data.display_text];
}
return (
<div className={"option-label"}>
{isPlatform && (
<img src={PLATFORM_TYPE_ICONS[data.display_text]} alt="" />
)}
<span>{labelText}</span>
</div>
);
};
const baseClass = "label-filter-select";
interface ILabelFilterSelectProps {
labels: ILabel[];
selectedLabel: ILabel | null;
canAddNewLabels: boolean;
className?: string;
onChange: (labelId: ILabel) => void;
onAddLabel: () => void;
}
const LabelFilterSelect = ({
labels,
selectedLabel,
canAddNewLabels,
className,
onChange,
onAddLabel,
}: ILabelFilterSelectProps) => {
const [labelQuery, setLabelQuery] = useState("");
// we need the Select to be a controlled component to enable our label input
// to work correctly. shouldOpenMenu now becomes our single source of truth if
// we want the menu to render open or closed.
const [shouldOpenMenu, setShouldOpenMenu] = useState(false);
const isLabelSearchInputFocusedRef = useRef(false);
const selectRef = useRef<
SelectInstance<ILabel | IEmptyOption, false, IGroupOption>
>(null);
const options = useMemo(() => createDropdownOptions(labels, labelQuery), [
labels,
labelQuery,
]);
const handleChange = (option: ILabel | IEmptyOption | null) => {
if (option === null) return;
if ("type" in option) {
setShouldOpenMenu(false);
setLabelQuery("");
selectRef.current?.blur();
onChange(option);
}
};
const handleLabelQueryChange = (
event: React.ChangeEvent<HTMLInputElement>
) => {
// We need to stop the key presses propagation to prevent the dropdown from
// picking up keypresses.
event.stopPropagation();
setLabelQuery(event.target.value);
};
const handleBlurSelect = () => {
if (!isLabelSearchInputFocusedRef.current) {
isLabelSearchInputFocusedRef.current = false;
setShouldOpenMenu(false);
}
};
const handleFocusSelect = () => {
setShouldOpenMenu(true);
};
const handleClickLabelSearchInput = () => {
isLabelSearchInputFocusedRef.current = true;
};
const handleBlurLabelSearchInput = () => {
isLabelSearchInputFocusedRef.current = false;
setShouldOpenMenu(false);
};
const getOptionLabel = (option: ILabel | IEmptyOption) => {
if ("display_text" in option) {
return option.display_text;
}
return option.label;
};
const getOptionValue = (option: ILabel | IEmptyOption) => {
if ("id" in option) {
return option.id.toString();
}
return option.label;
};
const classes = classnames(baseClass, className);
return (
<Select<ILabel | IEmptyOption, false, IGroupOption>
ref={selectRef}
name="input-filter-select"
options={options}
className={classes}
classNamePrefix={baseClass}
defaultMenuIsOpen={false}
placeholder={"Filter by operating system or label"}
formatOptionLabel={OptionLabel}
menuIsOpen={shouldOpenMenu}
value={selectedLabel}
isSearchable={false}
getOptionLabel={getOptionLabel}
getOptionValue={getOptionValue}
components={{ GroupHeading: CustomLabelGroupHeading }}
labelQuery={labelQuery}
canAddNewLabels={canAddNewLabels}
onChange={handleChange}
onBlur={handleBlurSelect}
onFocus={handleFocusSelect}
onAddLabel={onAddLabel}
onChangeLabelQuery={handleLabelQueryChange}
onClickLabelSeachInput={handleClickLabelSearchInput}
onBlurLabelSearchInput={handleBlurLabelSearchInput}
/>
);
};
export default LabelFilterSelect;
@@ -0,0 +1,108 @@
.label-filter-select {
width: 175px;
.label-filter-select__control {
border: 1px solid $ui-fleet-blue-15;
background-color: $ui-light-grey;
border-radius: $border-radius;
height: 40px;
&--is-focused,
&--menu-is-open,
&:hover {
box-shadow: none;
border: 1px solid $core-vibrant-blue;
}
}
.label-filter-select__value-container {
margin-left: 10px;
padding-left: 27px;
&::before {
display: inline-block;
position: absolute;
content: url(../assets/images/icon-filter.png);
top: 4px;
}
}
.label-filter-select__single-value,
.label-filter-select__placeholder {
color: $core-fleet-black;
font-size: $small;
}
.label-filter-select__placeholder {
width: 70px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@media (max-width: $break-990) {
width: auto;
}
}
.label-filter-select__indicator-separator {
display: none;
}
.label-filter-select__indicator {
color: $core-fleet-black;
}
.label-filter-select__menu {
width: 300px;
margin-top: 0;
}
.label-filter-select__menu-list {
max-height: 574px;
padding: $pad-small $pad-medium;
}
.label-filter-select__option {
padding: 10px 1rem;
&--is-selected,
&:active {
background-color: $ui-vibrant-blue-25;
}
&--is-focused {
background-color: $ui-vibrant-blue-10;
}
&--is-disabled {
&:active {
background-color: transparent;
}
}
}
.label-filter-select__group-heading {
padding: 0;
}
.option-label {
display: flex;
align-items: center;
span {
font-size: $x-small;
font-weight: $regular;
color: $core-fleet-black;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
img {
width: 16px;
height: 16px;
margin-right: $pad-small;
}
}
}
@@ -0,0 +1,16 @@
import linuxIcon from "../../../../../../assets/images/icon-linux-fleet-black-16x16@2x.png";
import darwinIcon from "../../../../../../assets/images/icon-darwin-fleet-black-16x16@2x.png";
import windowsIcon from "../../../../../../assets/images/icon-windows-fleet-black-16x16@2x.png";
export const EMPTY_OPTION = {
label: "No Matching Labels",
isDisabled: true,
};
export const PLATFORM_TYPE_ICONS: Record<string, any> = {
"All Linux": linuxIcon,
macOS: darwinIcon,
"MS Windows": windowsIcon,
};
export const FILTERED_LINUX = ["Red Hat Linux", "CentOS Linux", "Ubuntu Linux"];
@@ -0,0 +1,47 @@
import { ILabel } from "interfaces/label";
import { EMPTY_OPTION, FILTERED_LINUX } from "./constants";
export interface IEmptyOption {
label: string;
isDisabled: boolean;
}
export interface IGroupOption {
type: "platform" | "custom";
label: string;
options: ILabel[] | IEmptyOption[];
}
const createOptionGroup = (
type: "platform" | "custom",
label: string,
labels: ILabel[] | IEmptyOption[]
) => {
return {
type,
label,
options: labels,
};
};
export const createDropdownOptions = (labels: ILabel[], labelQuery: string) => {
const builtInLabels = labels.filter(
// we filter out All Hosts as that is included in hosts status dropdown filter
(label) =>
label.type === "platform" &&
label.name !== "All Hosts" &&
!FILTERED_LINUX.includes(label.name)
);
const customLabels = labels.filter(
(label) =>
label.label_type === "regular" &&
label.display_text.toLowerCase().includes(labelQuery)
);
const customGroupOptions =
customLabels.length !== 0 ? customLabels : [EMPTY_OPTION];
const options: IGroupOption[] = [
createOptionGroup("platform", "Platforms", builtInLabels),
createOptionGroup("custom", "Labels", customGroupOptions),
];
return options;
};
@@ -0,0 +1 @@
export { default } from "./LabelFilterSelect";
@@ -1,3 +1,4 @@
import { ILabel } from "interfaces/label";
import { isEmpty, reduce, trim, union } from "lodash";
import { buildQueryStringFromParams } from "utilities/url";
@@ -59,5 +60,3 @@ export const getNextLocationPath = ({
return queryString ? `/${nextLocation}?${queryString}` : `/${nextLocation}`;
};
export default { getNextLocationPath };
+1
View File
@@ -56,6 +56,7 @@
"react-router": "^3.0.0",
"react-router-transition": "0.1.1",
"react-select": "^1",
"react-select-5": "npm:react-select@^5.4.0",
"react-table": "7.7.0",
"react-tabs": "^3.2.0",
"react-tooltip": "^4.2.15",
+12510 -11705
View File
File diff suppressed because it is too large Load Diff