Manage Host Page: Export hosts as CSV (#4917)

This commit is contained in:
RachelElysia 2022-04-04 14:53:14 -04:00 committed by GitHub
parent 0bca26be03
commit 53ca15e93b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 174 additions and 5 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 453 B

View File

@ -0,0 +1 @@
* Add ability to export host as CSV from the UI

View File

@ -1,4 +1,5 @@
import * as path from "path";
import { format } from "date-fns";
let hostname = "";
@ -28,6 +29,7 @@ describe("Hosts flow", () => {
cy.visit("/hosts/manage");
cy.getAttached(".manage-hosts").within(() => {
cy.getAttached(".manage-hosts__export-btn").click();
cy.contains("button", /add hosts/i).click();
});
cy.getAttached(".react-tabs").within(() => {
@ -45,6 +47,11 @@ describe("Hosts flow", () => {
// before each test run (seems to be related to issues with Cypress trashAssetsBeforeRun)
if (Cypress.platform !== "win32") {
// windows has issues with downloads location
const formattedTime = format(new Date(), "yyyy-MM-dd");
const filename = `Hosts ${formattedTime}.csv`;
cy.readFile(path.join(Cypress.config("downloadsFolder"), filename), {
timeout: 5000,
});
cy.readFile(
path.join(Cypress.config("downloadsFolder"), "secret.txt"),
{

View File

@ -8,6 +8,7 @@ import Button from "components/buttons/Button";
import CloseIcon from "../../../assets/images/icon-close-white-16x16@2x.png";
import CloseIconBlack from "../../../assets/images/icon-close-fleet-black-16x16@2x.png";
import ErrorIcon from "../../../assets/images/icon-error-white-16x16@2x.png";
const baseClass = "flash-message";
@ -60,13 +61,15 @@ const FlashMessage = ({
return null;
}
const alertIcon =
alertType === "success" ? "success-check" : "warning-filled";
return (
<div className={klass} id={klass}>
<div className={`${baseClass}__content`}>
<FleetIcon name={alertIcon} /> <span>{message}</span>
{alertType === "success" ? (
<FleetIcon name="success-check" />
) : (
<img alt="error icon" src={ErrorIcon} />
)}
<span>{message}</span>
{onUndoActionClick && undoAction && (
<Button
className={`${baseClass}__undo`}

View File

@ -76,6 +76,11 @@
.fleeticon {
font-size: $small;
}
img {
width: 16px;
height: 16px;
}
}
&__undo {

View File

@ -17,6 +17,7 @@ export default {
HOSTS: "/v1/fleet/hosts",
HOSTS_COUNT: "/v1/fleet/hosts/count",
HOSTS_DELETE: "/v1/fleet/hosts/delete",
HOSTS_REPORT: "/v1/fleet/hosts/report",
HOSTS_TRANSFER: "/v1/fleet/hosts/transfer",
HOSTS_TRANSFER_BY_FILTER: "/v1/fleet/hosts/transfer/filter",
INVITES: "/v1/fleet/invites",

View File

@ -4,6 +4,8 @@ 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";
import enrollSecretsAPI from "services/entities/enroll_secret";
import labelsAPI from "services/entities/labels";
@ -89,6 +91,7 @@ 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";
interface IManageHostsProps {
route: RouteProps;
@ -117,6 +120,7 @@ interface ITableQueryProps {
sortDirection: string;
}
const CSV_HOSTS_TITLE = "Hosts";
const baseClass = "manage-hosts";
const ManageHostsPage = ({
@ -1348,6 +1352,74 @@ const ManageHostsPage = ({
);
};
const onExportHostsResults = async (
evt: React.MouseEvent<HTMLButtonElement>
) => {
evt.preventDefault();
let options = {
selectedLabels: selectedFilters,
globalFilter: searchQuery,
sortBy,
teamId: currentTeam?.id,
policyId,
policyResponse,
softwareId,
};
options = {
...options,
teamId: getValidatedTeamId(
availableTeams || [],
options.teamId as number,
currentUser,
isOnGlobalTeam as boolean
),
};
if (queryParams.team_id) {
options.teamId = queryParams.team_id;
}
try {
const exportHostResults = await hostsAPI.exportHosts(options);
const formattedTime = format(new Date(), "yyyy-MM-dd");
const filename = `${CSV_HOSTS_TITLE} ${formattedTime}.csv`;
const file = new global.window.File([exportHostResults], filename, {
type: "text/csv",
});
FileSaver.saveAs(file);
} catch (error) {
console.error(error);
renderFlash("error", "Could not export hosts. Please try again.");
}
};
const renderHostCount = useCallback(() => {
const count = filteredHostCount;
return (
<div
className={`${baseClass}__count ${
isHostCountLoading ? "count-loading" : ""
}`}
>
<span>{`${count} host${count === 1 ? "" : "s"}`}</span>
<Button
className={`${baseClass}__export-btn`}
onClick={onExportHostsResults}
variant="text-link"
>
<>
Export hosts <img alt="" src={DownloadIcon} />
</>
</Button>
</div>
);
}, [isHostCountLoading, filteredHostCount]);
const renderActiveFilterBlock = () => {
const showSelectedLabel =
selectedLabel &&
@ -1528,7 +1600,7 @@ const ManageHostsPage = ({
showMarkAllPages
isAllPagesSelected={isAllMatchingHostsSelected}
searchable
filteredCount={filteredHostCount}
renderCount={renderHostCount}
searchToolTipText={
"Search hosts by hostname, UUID, machine serial or IP address"
}

View File

@ -259,4 +259,16 @@
.total-issues-count {
margin-left: $pad-small;
}
&__export-btn {
padding-left: $pad-medium;
img {
width: 13px;
height: 13px;
margin-left: 8px;
position: relative;
top: -2px;
}
}
}

View File

@ -50,6 +50,74 @@ export default {
},
});
},
exportHosts: (options: ILoadHostsOptions | undefined) => {
const { HOSTS_REPORT, LABEL_HOSTS } = endpoints;
const selectedLabels = options?.selectedLabels || [];
const globalFilter = options?.globalFilter || "";
const sortBy = options?.sortBy || [];
const teamId = options?.teamId || null;
const policyId = options?.policyId || null;
const policyResponse = options?.policyResponse || null;
const softwareId = options?.softwareId || null;
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}`;
}
let path = "";
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";
if (label) {
const lid = label.substr(labelPrefix.length);
path = `${LABEL_HOSTS(
parseInt(lid, 10)
)}?${searchQuery}${orderKeyParam}${orderDirection}`;
// connect status if applicable
if (status && isValidStatus) {
path += `&status=${status}`;
}
} else if (status && isValidStatus) {
path = `${HOSTS_REPORT}?&status=${status}${searchQuery}${orderKeyParam}${orderDirection}`;
} else {
path = `${HOSTS_REPORT}?${searchQuery}${orderKeyParam}${orderDirection}`;
}
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 sepcified
}
// TODO: consider how to check for mutually exclusive scenarios with label, policy and software
if (!label && !policyId && softwareId) {
path += `&software_id=${softwareId}`;
}
path += "&format=csv";
return sendRequest("GET", path);
},
loadHosts: (options: ILoadHostsOptions | undefined) => {
const { HOSTS, LABEL_HOSTS } = endpoints;
const page = options?.page || 0;