diff --git a/.vscode/typescriptreact.code-snippets b/.vscode/typescriptreact.code-snippets new file mode 100644 index 0000000000..4bba69a4ac --- /dev/null +++ b/.vscode/typescriptreact.code-snippets @@ -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" + } +} diff --git a/assets/images/icon-filter.png b/assets/images/icon-filter.png new file mode 100644 index 0000000000..1c86fad06f Binary files /dev/null and b/assets/images/icon-filter.png differ diff --git a/changes/issue-5138-host-page-smaller-screen-width b/changes/issue-5138-host-page-smaller-screen-width new file mode 100644 index 0000000000..17b619fbaf --- /dev/null +++ b/changes/issue-5138-host-page-smaller-screen-width @@ -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. diff --git a/cypress/integration/all/app/hosts.spec.ts b/cypress/integration/all/app/hosts.spec.ts index ac62395919..f0ae1a5633 100644 --- a/cypress/integration/all/app/hosts.spec.ts +++ b/cypress/integration/all/app/hosts.spec.ts @@ -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(() => { diff --git a/cypress/integration/all/app/labelflow.spec.ts b/cypress/integration/all/app/labelflow.spec.ts index 05d0b56c05..b12ff0f235 100644 --- a/cypress/integration/all/app/labelflow.spec.ts +++ b/cypress/integration/all/app/labelflow.spec.ts @@ -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"); }); }); diff --git a/cypress/integration/all/app/policiesflow.spec.ts b/cypress/integration/all/app/policiesflow.spec.ts index 071f4902c0..cfa7a0a72d 100644 --- a/cypress/integration/all/app/policiesflow.spec.ts +++ b/cypress/integration/all/app/policiesflow.spec.ts @@ -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"); }); }); diff --git a/cypress/integration/free/admin.spec.ts b/cypress/integration/free/admin.spec.ts index d94542990d..36d07eaafe 100644 --- a/cypress/integration/free/admin.spec.ts +++ b/cypress/integration/free/admin.spec.ts @@ -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(() => { diff --git a/cypress/integration/free/maintainer.spec.ts b/cypress/integration/free/maintainer.spec.ts index 86389173b9..4b77500d44 100644 --- a/cypress/integration/free/maintainer.spec.ts +++ b/cypress/integration/free/maintainer.spec.ts @@ -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", () => { diff --git a/cypress/integration/free/observer.spec.ts b/cypress/integration/free/observer.spec.ts index 070041e863..d060ed64e3 100644 --- a/cypress/integration/free/observer.spec.ts +++ b/cypress/integration/free/observer.spec.ts @@ -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", () => { diff --git a/cypress/integration/premium/maintainer.spec.ts b/cypress/integration/premium/maintainer.spec.ts index 384633e71d..32e0985716 100644 --- a/cypress/integration/premium/maintainer.spec.ts +++ b/cypress/integration/premium/maintainer.spec.ts @@ -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", () => { diff --git a/cypress/integration/premium/observer.spec.ts b/cypress/integration/premium/observer.spec.ts index 1fe8a23cc7..2c24c8870a 100644 --- a/cypress/integration/premium/observer.spec.ts +++ b/cypress/integration/premium/observer.spec.ts @@ -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", () => { diff --git a/cypress/integration/premium/team_admin.spec.ts b/cypress/integration/premium/team_admin.spec.ts index ee3c35b1de..f0d1342077 100644 --- a/cypress/integration/premium/team_admin.spec.ts +++ b/cypress/integration/premium/team_admin.spec.ts @@ -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", () => { diff --git a/cypress/integration/premium/team_maintainer_observer.spec.ts b/cypress/integration/premium/team_maintainer_observer.spec.ts index aa8b95dbed..0f1e72c871 100644 --- a/cypress/integration/premium/team_maintainer_observer.spec.ts +++ b/cypress/integration/premium/team_maintainer_observer.spec.ts @@ -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" + ); }); }); }); diff --git a/frontend/components/MainContent/MainContent.tsx b/frontend/components/MainContent/MainContent.tsx index 218a74b2cb..26c9edf807 100644 --- a/frontend/components/MainContent/MainContent.tsx +++ b/frontend/components/MainContent/MainContent.tsx @@ -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; diff --git a/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx b/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx index 49b61f1421..bc35e10732 100644 --- a/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx +++ b/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx @@ -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( - ["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) => { - 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) => { - 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 ( + <> + + {label_type !== "builtin" && !isOnlyObserver && ( + <> + + + + )} + + ); } + + 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 = ( + + {`Hosts with ${name_only || name}`},
+ {version && `${version} installed`} +
+ ); + return ( -
-
- -
- {buttonText} - -
-
- - - {`Hosts with ${name_only || name}`},
- {version && `${version} installed`} -
-
-
-
+ ); }; const renderPoliciesFilterBlock = () => ( -
+ <> -
- Policy - {policy?.name} - -
-
+ + ); const renderSoftwareFilterBlock = () => { - if (softwareDetails) { - const { name, version } = softwareDetails; - const buttonText = name && version ? `${name} ${version}` : ""; - return ( -
-
- -
- {buttonText} - -
-
- - - {`Hosts with ${name}`},
- {`${version} installed`} -
-
-
-
- ); - } - return null; + if (!softwareDetails) return null; + + const { name, version } = softwareDetails; + const label = name && version ? `${name} ${version}` : ""; + const TooltipDescription = + name && version ? ( + + {`Hosts with ${name}`},
+ {`${version} installed`} +
+ ) : undefined; + + return ( + + ); }; const renderMDMSolutionFilterBlock = () => { - if (mdmSolutionDetails) { - const { name, server_url } = mdmSolutionDetails; - const buttonText = name ? `${name} ${server_url}` : `${server_url}`; - return ( -
-
- -
- {buttonText} - -
-
- - - Host enrolled - {name !== "Unknown" && ` to ${name}`} -
at {server_url} -
-
-
-
- ); - } - return null; + 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) { - const buttonText = () => { - switch (mdmEnrollmentStatus) { - case "automatic": - return "MDM enrolled (automatic)"; - case "manual": - return "MDM enrolled (manual)"; - default: - return "Unenrolled"; - } - }; - const tooltipText = () => { - switch (mdmEnrollmentStatus) { - case "automatic": - return ( - - Hosts automatically enrolled
- to an MDM solution the first time
- the host is used. Administrators
- might have a higher level of control
- over these hosts. -
- ); - case "manual": - return ( - - Hosts manually enrolled to an
- MDM solution by a user or
- administrator. -
- ); - default: - return ( - - Hosts not enrolled to
an MDM solution. -
- ); - } - }; - return ( -
-
- -
- {buttonText()} - -
-
- - {tooltipText()} - -
-
- ); + 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 = ( + + Hosts automatically enrolled
+ to an MDM solution the first time
+ the host is used. Administrators
+ might have a higher level of control
+ over these hosts. +
+ ); + break; + case "manual": + TooltipDescription = ( + + Hosts manually enrolled to an
+ MDM solution by a user or
+ administrator. +
+ ); + break; + default: + TooltipDescription = ( + + Hosts not enrolled to
an MDM solution. +
+ ); + } + + return ( + + ); }; const renderEditColumnsModal = () => { @@ -1512,41 +1439,6 @@ const ManageHostsPage = ({ /> ); - const renderHeaderLabelBlock = () => { - if (selectedLabel) { - const { - description, - display_text: displayText, - label_type: labelType, - } = selectedLabel; - - return ( -
-
- - {PLATFORM_LABEL_DISPLAY_NAMES[displayText] || displayText} - - {labelType !== "builtin" && !isOnlyObserver && ( - <> - - - - )} -
-
- {description} -
-
- ); - } - - return null; - }; - const renderHeader = () => (
@@ -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 (
- {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 = ( - + - ); - } else { - SidePanel = ( - - ); - } - - return SidePanel; +
+ ); }; - const renderStatusDropdown = () => ( - - ); - 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 = ({ )} - {renderSidePanel()} + {isAddLabel && ( + + + + )} {canEnrollHosts && showDeleteSecretModal && renderDeleteSecretModal()} {canEnrollHosts && showSecretEditorModal && renderSecretEditorModal()} diff --git a/frontend/pages/hosts/ManageHostsPage/_styles.scss b/frontend/pages/hosts/ManageHostsPage/_styles.scss index e547566202..9fcbac6a4a 100644 --- a/frontend/pages/hosts/ManageHostsPage/_styles.scss +++ b/frontend/pages/hosts/ManageHostsPage/_styles.scss @@ -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 { diff --git a/frontend/pages/hosts/ManageHostsPage/components/CustomLabelGroupHeading/CustomLabelGroupHeading.tsx b/frontend/pages/hosts/ManageHostsPage/components/CustomLabelGroupHeading/CustomLabelGroupHeading.tsx new file mode 100644 index 0000000000..e634d7331c --- /dev/null +++ b/frontend/pages/hosts/ManageHostsPage/components/CustomLabelGroupHeading/CustomLabelGroupHeading.tsx @@ -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 +) => { + const { data, selectProps } = props; + const { + labelQuery, + canAddNewLabels, + onAddLabel, + onChangeLabelQuery, + onClickLabelSeachInput, + onBlurLabelSearchInput, + } = selectProps; + const inputRef = useRef(null); + + const handleInputClick = ( + event: React.MouseEvent + ) => { + onClickLabelSeachInput(event); + inputRef.current?.focus(); + event.stopPropagation(); + }; + + return data.type === "platform" ? ( + +
+ {props.children} +
+
+ ) : ( + +
+ {props.children} +
+ {canAddNewLabels && ( + + )} +
+
+
+ { + // Stops the parent dropdown from picking up on input keypresses + event.stopPropagation(); + }} + onChange={onChangeLabelQuery} + onClick={handleInputClick} + onBlur={onBlurLabelSearchInput} + /> +
+
+ ); +}; + +export default CustomLabelGroupHeading; diff --git a/frontend/pages/hosts/ManageHostsPage/components/CustomLabelGroupHeading/_styles.scss b/frontend/pages/hosts/ManageHostsPage/components/CustomLabelGroupHeading/_styles.scss new file mode 100644 index 0000000000..f39bfe91b3 --- /dev/null +++ b/frontend/pages/hosts/ManageHostsPage/components/CustomLabelGroupHeading/_styles.scss @@ -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; + } + } + } +} diff --git a/frontend/pages/hosts/ManageHostsPage/components/CustomLabelGroupHeading/index.ts b/frontend/pages/hosts/ManageHostsPage/components/CustomLabelGroupHeading/index.ts new file mode 100644 index 0000000000..3af6fcc877 --- /dev/null +++ b/frontend/pages/hosts/ManageHostsPage/components/CustomLabelGroupHeading/index.ts @@ -0,0 +1 @@ +export { default } from "./CustomLabelGroupHeading"; diff --git a/frontend/pages/hosts/ManageHostsPage/components/FilterPill/FilterPill.tsx b/frontend/pages/hosts/ManageHostsPage/components/FilterPill/FilterPill.tsx new file mode 100644 index 0000000000..4b66eaed39 --- /dev/null +++ b/frontend/pages/hosts/ManageHostsPage/components/FilterPill/FilterPill.tsx @@ -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 ( +
+ <> + +
+ {icon && } + {label} + +
+
+ {tooltipDescription && ( + + {tooltipDescription} + + )} + +
+ ); +}; + +export default FilterPill; diff --git a/frontend/pages/hosts/ManageHostsPage/components/FilterPill/_styles.scss b/frontend/pages/hosts/ManageHostsPage/components/FilterPill/_styles.scss new file mode 100644 index 0000000000..5eef0af726 --- /dev/null +++ b/frontend/pages/hosts/ManageHostsPage/components/FilterPill/_styles.scss @@ -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; + } + } + } +} diff --git a/frontend/pages/hosts/ManageHostsPage/components/FilterPill/index.ts b/frontend/pages/hosts/ManageHostsPage/components/FilterPill/index.ts new file mode 100644 index 0000000000..60b2889159 --- /dev/null +++ b/frontend/pages/hosts/ManageHostsPage/components/FilterPill/index.ts @@ -0,0 +1 @@ +export { default } from "./FilterPill"; diff --git a/frontend/pages/hosts/ManageHostsPage/components/LabelFilterSelect/LabelFilterSelect.tsx b/frontend/pages/hosts/ManageHostsPage/components/LabelFilterSelect/LabelFilterSelect.tsx new file mode 100644 index 0000000000..dbbc2b593e --- /dev/null +++ b/frontend/pages/hosts/ManageHostsPage/components/LabelFilterSelect/LabelFilterSelect.tsx @@ -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