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:
+19
@@ -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.
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+79
@@ -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";
|
||||
+175
@@ -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 };
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user