mirror of
https://github.com/empayre/fleet.git
synced 2024-11-06 17:05:18 +00:00
Manage Host Page: Custom host count based on filters, labels, etc (#2377)
* Host count includes label id parameter * Remove page and pageSize from hostCountAPI call * Only retrieve hostCount on filter change
This commit is contained in:
parent
89f4234819
commit
2000b1b263
1
changes/issue-2336-filtered-host-count
Normal file
1
changes/issue-2336-filtered-host-count
Normal file
@ -0,0 +1 @@
|
||||
* Manage host page displays total host count with filters applied.
|
@ -18,7 +18,7 @@ describe("Premium tier - Observer user", () => {
|
||||
cy.visit("/hosts/manage");
|
||||
|
||||
// Ensure page is loaded
|
||||
cy.wait(5000); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||
cy.wait(3000); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||
cy.contains("All hosts");
|
||||
|
||||
cy.get("thead").within(() => {
|
||||
@ -62,7 +62,11 @@ describe("Premium tier - Observer user", () => {
|
||||
cy.visit("/hosts/manage");
|
||||
cy.wait(3000); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||
|
||||
// Ensure the page is loaded and teams are visible
|
||||
cy.findByText("Hosts").should("exist");
|
||||
cy.contains(".table-container .data-table__table th", "Team").should(
|
||||
"be.visible"
|
||||
);
|
||||
|
||||
// Nav restrictions
|
||||
cy.findByText(/settings/i).should("not.exist");
|
||||
@ -74,11 +78,6 @@ describe("Premium tier - Observer user", () => {
|
||||
cy.visit("/schedule/manage");
|
||||
cy.findByText(/you do not have permissions/i).should("exist");
|
||||
|
||||
cy.visit("/hosts/manage");
|
||||
cy.contains(".table-container .data-table__table th", "Team").should(
|
||||
"be.visible"
|
||||
);
|
||||
|
||||
// On the Profile page, they should…
|
||||
// See Global in the Team section and Observer in the Role section
|
||||
cy.visit("/profile");
|
||||
|
@ -9,7 +9,8 @@ describe(
|
||||
cy.login();
|
||||
cy.seedPremium();
|
||||
cy.seedQueries();
|
||||
cy.addDockerHost();
|
||||
cy.addDockerHost("apples");
|
||||
cy.addDockerHost("oranges");
|
||||
cy.logout();
|
||||
});
|
||||
afterEach(() => {
|
||||
@ -20,7 +21,7 @@ describe(
|
||||
cy.login("marco@organization.com", "user123#");
|
||||
cy.visit("/hosts/manage");
|
||||
|
||||
// Ensure page is loaded
|
||||
// Ensure page is loaded and teams are visible
|
||||
cy.contains("Hosts");
|
||||
|
||||
// On the Hosts page, they should…
|
||||
@ -198,6 +199,11 @@ describe(
|
||||
cy.findByRole("button", { name: /schedule a query/i }).click();
|
||||
// TODO: Write e2e test for team maintainer to schedule a query
|
||||
|
||||
cy.visit("/hosts/manage");
|
||||
cy.contains(".table-container .data-table__table th", "Team").should(
|
||||
"be.visible"
|
||||
);
|
||||
|
||||
// On the Profile page, they should…
|
||||
// See 2 Teams in the Team section and Various in the Role section
|
||||
cy.visit("/profile");
|
||||
|
@ -59,7 +59,7 @@ interface ITableContainerProps {
|
||||
secondarySelectActions?: IActionButtonProps[]; // TODO create table actions interface
|
||||
customControl?: () => JSX.Element;
|
||||
onSelectSingleRow?: (value: Row) => void;
|
||||
getCustomCount?: (data: any) => number;
|
||||
filteredCount?: number;
|
||||
}
|
||||
|
||||
const baseClass = "table-container";
|
||||
@ -103,7 +103,7 @@ const TableContainer = ({
|
||||
secondarySelectActions,
|
||||
customControl,
|
||||
onSelectSingleRow,
|
||||
getCustomCount,
|
||||
filteredCount,
|
||||
}: ITableContainerProps): JSX.Element => {
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [sortHeader, setSortHeader] = useState(defaultSortHeader || "");
|
||||
@ -198,6 +198,7 @@ const TableContainer = ({
|
||||
prevSearchQuery,
|
||||
]);
|
||||
|
||||
const displayCount = filteredCount || data.length;
|
||||
return (
|
||||
<div className={wrapperClasses}>
|
||||
{wideSearch && searchable && (
|
||||
@ -218,7 +219,8 @@ const TableContainer = ({
|
||||
resultsTitle,
|
||||
pageIndex,
|
||||
pageSize,
|
||||
getCustomCount ? getCustomCount(data) : data.length
|
||||
displayCount,
|
||||
filteredCount
|
||||
)}
|
||||
{resultsHtml}
|
||||
</p>
|
||||
|
@ -4,7 +4,8 @@ const generateResultsCountText = (
|
||||
name: string = DEFAULT_RESULTS_NAME,
|
||||
pageIndex: number,
|
||||
pageSize: number,
|
||||
resultsCount: number
|
||||
resultsCount: number,
|
||||
filteredCount?: number
|
||||
): string => {
|
||||
if (resultsCount === 0) return `No ${name}`;
|
||||
// If there is 1 result and the last 3 letters in the result
|
||||
@ -26,8 +27,10 @@ const generateResultsCountText = (
|
||||
return `${resultsCount} ${name.slice(0, -1)}`;
|
||||
}
|
||||
|
||||
if (pageSize === resultsCount) return `${pageSize}+ ${name}`;
|
||||
if (pageIndex !== 0 && resultsCount <= pageSize)
|
||||
// If there is no filtered count, return pageSize+ name
|
||||
if (pageSize === resultsCount && !filteredCount)
|
||||
return `${pageSize}+ ${name}`;
|
||||
if (pageIndex !== 0 && resultsCount <= pageSize && !filteredCount)
|
||||
return `${pageSize}+ ${name}`;
|
||||
return `${resultsCount} ${name}`;
|
||||
};
|
||||
|
@ -17,6 +17,7 @@ export default {
|
||||
ACTIVITIES: "/v1/fleet/activities",
|
||||
GLOBAL_SCHEDULE: "/v1/fleet/global/schedule",
|
||||
HOSTS: "/v1/fleet/hosts",
|
||||
HOSTS_COUNT: "/v1/fleet/hosts/count",
|
||||
HOSTS_DELETE: "/v1/fleet/hosts/delete",
|
||||
HOSTS_TRANSFER: "/v1/fleet/hosts/transfer",
|
||||
HOSTS_TRANSFER_BY_FILTER: "/v1/fleet/hosts/transfer/filter",
|
||||
|
@ -14,6 +14,9 @@ import hostsAPI, {
|
||||
IHostLoadOptions,
|
||||
ISortOption,
|
||||
} from "services/entities/hosts";
|
||||
import hostCountAPI, {
|
||||
IHostCountLoadOptions,
|
||||
} from "services/entities/host_count";
|
||||
|
||||
import PATHS from "router/paths";
|
||||
import { AppContext } from "context/app";
|
||||
@ -175,6 +178,9 @@ const ManageHostsPage = ({
|
||||
const [hosts, setHosts] = useState<IHost[]>();
|
||||
const [isHostsLoading, setIsHostsLoading] = useState<boolean>(false);
|
||||
const [hasHostErrors, setHasHostErrors] = useState<boolean>(false);
|
||||
const [filteredHostCount, setFilteredHostCount] = useState<number>();
|
||||
const [isHostCountLoading, setIsHostCountLoading] = useState<boolean>(false);
|
||||
const [hasHostCountErrors, setHasHostCountErrors] = useState<boolean>(false);
|
||||
const [sortBy, setSortBy] = useState<ISortOption[]>(initialSortBy);
|
||||
const [policy, setPolicy] = useState<IPolicy>();
|
||||
const [tableQueryData, setTableQueryData] = useState<ITableQueryProps>();
|
||||
@ -250,14 +256,6 @@ const ManageHostsPage = ({
|
||||
}
|
||||
);
|
||||
|
||||
// const toggleEnrollSecretModal = () => {
|
||||
// setShowEnrollSecretModal(!showEnrollSecretModal);
|
||||
// };
|
||||
|
||||
// const toggleAddHostModal = () => {
|
||||
// setShowAddHostModal(!showAddHostModal);
|
||||
// };
|
||||
|
||||
const toggleDeleteLabelModal = () => {
|
||||
setShowDeleteLabelModal(!showDeleteLabelModal);
|
||||
};
|
||||
@ -310,6 +308,30 @@ const ManageHostsPage = ({
|
||||
}
|
||||
};
|
||||
|
||||
const retrieveHostCount = async (options: IHostCountLoadOptions = {}) => {
|
||||
setIsHostCountLoading(true);
|
||||
|
||||
options = {
|
||||
...options,
|
||||
teamId: getValidatedTeamId(
|
||||
teams || [],
|
||||
options.teamId as number,
|
||||
currentUser,
|
||||
isOnGlobalTeam as boolean
|
||||
),
|
||||
};
|
||||
|
||||
try {
|
||||
const { count: returnedHostCount } = await hostCountAPI.load(options);
|
||||
setFilteredHostCount(returnedHostCount);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setHasHostCountErrors(true);
|
||||
} finally {
|
||||
setIsHostCountLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// triggered every time the route is changed
|
||||
// which means every filter click and text search
|
||||
useDeepEffect(() => {
|
||||
@ -345,6 +367,40 @@ const ManageHostsPage = ({
|
||||
retrieveHosts(options);
|
||||
}, [location, tableQueryData, labels]);
|
||||
|
||||
useDeepEffect(() => {
|
||||
// set the team object in context
|
||||
const teamId = parseInt(queryParams?.team_id, 10) || 0;
|
||||
const selectedTeam = find(teams, ["id", teamId]);
|
||||
setCurrentTeam(selectedTeam);
|
||||
|
||||
// set selected label
|
||||
const slugToFind =
|
||||
(selectedFilters.length > 0 &&
|
||||
selectedFilters.find((f) => f.includes(LABEL_SLUG_PREFIX))) ||
|
||||
selectedFilters[0];
|
||||
|
||||
const selected = find(labels, ["slug", slugToFind]) as ILabel;
|
||||
setSelectedLabel(selected);
|
||||
|
||||
// get the hosts
|
||||
const options: IHostLoadOptions = {
|
||||
selectedLabels: selectedFilters,
|
||||
globalFilter: searchQuery,
|
||||
sortBy,
|
||||
teamId: selectedTeam?.id,
|
||||
policyId,
|
||||
policyResponse,
|
||||
};
|
||||
|
||||
retrieveHostCount(options);
|
||||
}, [
|
||||
queryParams.team_id,
|
||||
searchQuery,
|
||||
policyId,
|
||||
policyResponse,
|
||||
selectedFilters,
|
||||
]);
|
||||
|
||||
const handleLabelChange = ({ slug }: ILabel) => {
|
||||
if (!slug) {
|
||||
console.error("Slug was missing. This should not happen.");
|
||||
@ -1085,7 +1141,7 @@ const ManageHostsPage = ({
|
||||
return null;
|
||||
}
|
||||
|
||||
if (hasHostErrors) {
|
||||
if (hasHostErrors || hasHostCountErrors) {
|
||||
return <TableDataError />;
|
||||
}
|
||||
|
||||
@ -1114,7 +1170,7 @@ const ManageHostsPage = ({
|
||||
currentTeam
|
||||
)}
|
||||
data={hosts}
|
||||
isLoading={isHostsLoading}
|
||||
isLoading={isHostsLoading || isHostCountLoading}
|
||||
manualSortBy
|
||||
defaultSortHeader={(sortBy[0] && sortBy[0].key) || DEFAULT_SORT_HEADER}
|
||||
defaultSortDirection={
|
||||
@ -1139,6 +1195,7 @@ const ManageHostsPage = ({
|
||||
toggleAllPagesSelected={toggleAllMatchingHosts}
|
||||
searchable
|
||||
customControl={renderStatusDropdown}
|
||||
filteredCount={filteredHostCount}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
79
frontend/services/entities/host_count.ts
Normal file
79
frontend/services/entities/host_count.ts
Normal file
@ -0,0 +1,79 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import sendRequest from "services";
|
||||
import endpoints from "fleet/endpoints";
|
||||
import { IHost } from "interfaces/host";
|
||||
|
||||
export interface ISortOption {
|
||||
key: string;
|
||||
direction: string;
|
||||
}
|
||||
|
||||
export interface IHostCountLoadOptions {
|
||||
page?: number;
|
||||
perPage?: number;
|
||||
sortBy?: ISortOption[];
|
||||
status?: string;
|
||||
globalFilter?: string;
|
||||
teamId?: number;
|
||||
policyId?: number;
|
||||
policyResponse?: string;
|
||||
selectedLabels?: string[];
|
||||
}
|
||||
|
||||
export default {
|
||||
// hostCount.load share similar variables and parameters with hosts.loadAll
|
||||
load: (options: IHostCountLoadOptions | undefined) => {
|
||||
const { HOSTS_COUNT } = endpoints;
|
||||
const sortBy = options?.sortBy || [];
|
||||
const globalFilter = options?.globalFilter || "";
|
||||
const teamId = options?.teamId || null;
|
||||
const policyId = options?.policyId || null;
|
||||
const policyResponse = options?.policyResponse || null;
|
||||
const selectedLabels = options?.selectedLabels || [];
|
||||
|
||||
let orderKeyParam = "";
|
||||
let orderDirection = "";
|
||||
if (sortBy.length !== 0) {
|
||||
const sortItem = sortBy[0];
|
||||
orderKeyParam += `order_key=${sortItem.key}`;
|
||||
orderDirection = `&order_direction=${sortItem.direction}`;
|
||||
}
|
||||
|
||||
let searchQuery = "";
|
||||
if (globalFilter !== "") {
|
||||
searchQuery = `&query=${globalFilter}`;
|
||||
}
|
||||
|
||||
const labelPrefix = "labels/";
|
||||
|
||||
// Handle multiple filters
|
||||
const label = selectedLabels.find((f) => f.includes(labelPrefix));
|
||||
const status = selectedLabels.find((f) => !f.includes(labelPrefix));
|
||||
const isValidStatus =
|
||||
status === "new" ||
|
||||
status === "online" ||
|
||||
status === "offline" ||
|
||||
status === "mia";
|
||||
|
||||
let path = `${HOSTS_COUNT}?${orderKeyParam}${orderDirection}${searchQuery}`;
|
||||
|
||||
if (status && isValidStatus) {
|
||||
path += `&status=${status}`;
|
||||
}
|
||||
|
||||
if (label) {
|
||||
path += `&label_id=${parseInt(label.substr(labelPrefix.length), 10)}`;
|
||||
}
|
||||
|
||||
if (teamId) {
|
||||
path += `&team_id=${teamId}`;
|
||||
}
|
||||
|
||||
if (!label && policyId) {
|
||||
path += `&policy_id=${policyId}`;
|
||||
path += `&policy_response=${policyResponse || "passing"}`; // TODO confirm whether there should be a default if there is an id but no response specified
|
||||
}
|
||||
|
||||
return sendRequest("GET", path);
|
||||
},
|
||||
};
|
Loading…
Reference in New Issue
Block a user