UI: Enter button presses action button for forms/modals (#4939)

This commit is contained in:
RachelElysia 2022-04-07 21:07:38 -04:00 committed by GitHub
parent e675afc6cb
commit d1860ad86d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 298 additions and 229 deletions

View file

@ -0,0 +1 @@
* Pressing enter submits forms app-wide

View file

@ -49,7 +49,7 @@ describe("Labels flow", () => {
cy.findByText(/show all mac usernames/i).click();
});
cy.getAttached(".manage-hosts__label-block button").last().click();
cy.getAttached(".manage-hosts__modal-buttons > .button--alert")
cy.getAttached(".delete-label-modal")
.contains("button", /delete/i)
.click();
cy.getAttached(".host-side-panel").within(() => {

View file

@ -1,4 +1,4 @@
import React from "react";
import React, { useEffect } from "react";
import Modal from "components/Modal";
import Button from "components/buttons/Button";
// @ts-ignore
@ -31,6 +31,20 @@ const EnrollSecretModal = ({
setSelectedSecret,
globalSecrets,
}: IEnrollSecretModal): JSX.Element => {
useEffect(() => {
const listener = (event: KeyboardEvent) => {
if (event.code === "Enter" || event.code === "NumpadEnter") {
event.preventDefault();
onReturnToApp();
}
};
document.addEventListener("keydown", listener);
return () => {
document.removeEventListener("keydown", listener);
};
}, []);
const renderTeam = () => {
if (typeof selectedTeam === "string") {
selectedTeam = parseInt(selectedTeam, 10);

View file

@ -91,6 +91,25 @@ const LabelForm = ({
debounceSQL(query);
}, [query]);
useEffect(() => {
const listener = (event: KeyboardEvent) => {
if (event.code === "Enter" || event.code === "NumpadEnter") {
event.preventDefault();
handleSubmit({
name,
query,
description,
platform,
});
}
};
document.addEventListener("keydown", listener);
return () => {
document.removeEventListener("keydown", listener);
};
}, [name, query, description, platform]);
const onLoad = (editor: IAceEditor) => {
editor.setOptions({
enableLinking: true,

View file

@ -37,6 +37,7 @@ const EditPackForm = ({
const onChangePackName = (value: string) => {
setPackName(value);
setErrors({});
};
const onChangePackDescription = (value: string) => {

View file

@ -1,4 +1,4 @@
import React, { useCallback, useState } from "react";
import React, { useCallback, useState, useEffect } from "react";
import { INewMembersBody, ITeam } from "interfaces/team";
import endpoints from "fleet/endpoints";
@ -41,7 +41,7 @@ const AddMemberModal = ({
}, [selectedMembers, onSubmit]);
return (
<Modal onExit={onCancel} title={"Add Members"} className={baseClass}>
<Modal onExit={onCancel} title={"Add members"} className={baseClass}>
<form className={`${baseClass}__form`}>
<p className="title">Add team members</p>
<AutocompleteDropdown

View file

@ -1,4 +1,4 @@
import React from "react";
import React, { useEffect } from "react";
import Modal from "components/Modal";
import Button from "components/buttons/Button";
@ -18,8 +18,21 @@ const RemoveMemberModal = ({
onSubmit,
onCancel,
}: IDeleteTeamModalProps): JSX.Element => {
useEffect(() => {
const listener = (event: KeyboardEvent) => {
if (event.code === "Enter" || event.code === "NumpadEnter") {
event.preventDefault();
onSubmit();
}
};
document.addEventListener("keydown", listener);
return () => {
document.removeEventListener("keydown", listener);
};
}, []);
return (
<Modal title={"Delete team"} onExit={onCancel} className={baseClass}>
<Modal title={"Remove team member"} onExit={onCancel} className={baseClass}>
<form className={`${baseClass}__form`}>
<p>
You are about to remove{" "}

View file

@ -48,7 +48,6 @@ import Button from "components/buttons/Button"; // @ts-ignore
import Dropdown from "components/forms/fields/Dropdown";
import HostSidePanel from "components/side_panels/HostSidePanel"; // @ts-ignore
import LabelForm from "components/forms/LabelForm";
import Modal from "components/Modal";
import QuerySidePanel from "components/side_panels/QuerySidePanel";
import TableContainer from "components/TableContainer";
import TableDataError from "components/TableDataError";
@ -84,6 +83,7 @@ import PoliciesFilter from "./components/PoliciesFilter"; // @ts-ignore
import EditColumnsModal from "./components/EditColumnsModal/EditColumnsModal";
import TransferHostModal from "./components/TransferHostModal";
import DeleteHostModal from "./components/DeleteHostModal";
import DeleteLabelModal from "./components/DeleteLabelModal";
import SoftwareVulnerabilities from "./components/SoftwareVulnerabilities";
import EditColumnsIcon from "../../../../assets/images/icon-edit-columns-16x16@2x.png";
import PencilIcon from "../../../../assets/images/icon-pencil-14x14@2x.png";
@ -378,7 +378,6 @@ const ManageHostsPage = ({
setShowEnrollSecretModal(!showEnrollSecretModal);
};
// this is called when we click add or edit
const toggleSecretEditorModal = () => {
// open and closes add/edit modal
setShowSecretEditorModal(!showSecretEditorModal);
@ -402,6 +401,10 @@ const ManageHostsPage = ({
setShowAddHostsModal(!showAddHostsModal);
};
const toggleEditColumnsModal = () => {
setShowEditColumnsModal(!showEditColumnsModal);
};
const toggleAllMatchingHosts = (shouldSelect: boolean) => {
if (typeof shouldSelect !== "undefined") {
setIsAllMatchingHostsSelected(shouldSelect);
@ -674,25 +677,13 @@ const ManageHostsPage = ({
);
};
const onEditColumnsClick = () => {
setShowEditColumnsModal(true);
};
const onCancelColumns = () => {
setShowEditColumnsModal(false);
};
const onSaveColumns = (newHiddenColumns: string[]) => {
localStorage.setItem("hostHiddenColumns", JSON.stringify(newHiddenColumns));
setHiddenColumns(newHiddenColumns);
setShowEditColumnsModal(false);
};
const onCancelAddLabel = () => {
router.goBack();
};
const onCancelEditLabel = () => {
const onCancelLabel = () => {
router.goBack();
};
@ -1086,28 +1077,26 @@ const ManageHostsPage = ({
/>
);
const renderPoliciesFilterBlock = () => {
return (
<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>
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>
);
const renderSoftwareFilterBlock = () => {
if (softwareDetails) {
@ -1155,120 +1144,68 @@ const ManageHostsPage = ({
};
const renderEditColumnsModal = () => {
if (!showEditColumnsModal || !config || !currentUser) {
if (!config || !currentUser) {
return null;
}
return (
<Modal
title="Edit columns"
onExit={() => setShowEditColumnsModal(false)}
className={`${baseClass}__invite-modal`}
>
<EditColumnsModal
columns={generateAvailableTableHeaders(
config,
currentUser,
currentTeam
)}
hiddenColumns={hiddenColumns}
onSaveColumns={onSaveColumns}
onCancelColumns={onCancelColumns}
/>
</Modal>
);
};
const renderSecretEditorModal = () => {
if (!canEnrollHosts || !showSecretEditorModal) {
return null;
}
return (
<SecretEditorModal
selectedTeam={currentTeam?.id || 0}
teams={teams || []}
onSaveSecret={onSaveSecret}
toggleSecretEditorModal={toggleSecretEditorModal}
selectedSecret={selectedSecret}
<EditColumnsModal
columns={generateAvailableTableHeaders(
config,
currentUser,
currentTeam
)}
hiddenColumns={hiddenColumns}
onSaveColumns={onSaveColumns}
onCancelColumns={toggleEditColumnsModal}
/>
);
};
const renderDeleteSecretModal = () => {
if (!canEnrollHosts || !showDeleteSecretModal) {
return null;
}
const renderSecretEditorModal = () => (
<SecretEditorModal
selectedTeam={currentTeam?.id || 0}
teams={teams || []}
onSaveSecret={onSaveSecret}
toggleSecretEditorModal={toggleSecretEditorModal}
selectedSecret={selectedSecret}
/>
);
return (
<DeleteSecretModal
onDeleteSecret={onDeleteSecret}
selectedTeam={currentTeam?.id || 0}
teams={teams || []}
toggleDeleteSecretModal={toggleDeleteSecretModal}
/>
);
};
const renderDeleteSecretModal = () => (
<DeleteSecretModal
onDeleteSecret={onDeleteSecret}
selectedTeam={currentTeam?.id || 0}
teams={teams || []}
toggleDeleteSecretModal={toggleDeleteSecretModal}
/>
);
const renderEnrollSecretModal = () => {
if (!canEnrollHosts || !showEnrollSecretModal) {
return null;
}
const renderEnrollSecretModal = () => (
<EnrollSecretModal
selectedTeam={currentTeam?.id || 0}
teams={teams || []}
onReturnToApp={() => setShowEnrollSecretModal(false)}
toggleSecretEditorModal={toggleSecretEditorModal}
toggleDeleteSecretModal={toggleDeleteSecretModal}
setSelectedSecret={setSelectedSecret}
globalSecrets={globalSecrets}
/>
);
return (
<EnrollSecretModal
selectedTeam={currentTeam?.id || 0}
teams={teams || []}
onReturnToApp={() => setShowEnrollSecretModal(false)}
toggleSecretEditorModal={toggleSecretEditorModal}
toggleDeleteSecretModal={toggleDeleteSecretModal}
setSelectedSecret={setSelectedSecret}
globalSecrets={globalSecrets}
/>
);
};
const renderDeleteLabelModal = () => (
<DeleteLabelModal
onSubmit={onDeleteLabel}
onCancel={toggleDeleteLabelModal}
/>
);
const renderDeleteLabelModal = () => {
if (!showDeleteLabelModal) {
return false;
}
return (
<Modal
title="Delete label"
onExit={toggleDeleteLabelModal}
className={`${baseClass}_delete-label__modal`}
>
<>
<p>Are you sure you wish to delete this label?</p>
<div className={`${baseClass}__modal-buttons`}>
<Button onClick={toggleDeleteLabelModal} variant="inverse-alert">
Cancel
</Button>
<Button onClick={onDeleteLabel} variant="alert">
Delete
</Button>
</div>
</>
</Modal>
);
};
const renderAddHostsModal = () => {
if (!showAddHostsModal) {
return null;
}
return (
<AddHostsModal
onCancel={toggleAddHostsModal}
selectedTeam={addHostsTeam}
/>
);
};
const renderAddHostsModal = () => (
<AddHostsModal onCancel={toggleAddHostsModal} selectedTeam={addHostsTeam} />
);
const renderTransferHostModal = () => {
if (!showTransferHostModal || !teams) {
if (!teams) {
return null;
}
@ -1282,20 +1219,14 @@ const ManageHostsPage = ({
);
};
const renderDeleteHostModal = () => {
if (!showDeleteHostModal) {
return null;
}
return (
<DeleteHostModal
selectedHostIds={selectedHostIds}
onSubmit={onDeleteHostSubmit}
onCancel={toggleDeleteHostModal}
isAllMatchingHostsSelected={isAllMatchingHostsSelected}
/>
);
};
const renderDeleteHostModal = () => (
<DeleteHostModal
selectedHostIds={selectedHostIds}
onSubmit={onDeleteHostSubmit}
onCancel={toggleDeleteHostModal}
isAllMatchingHostsSelected={isAllMatchingHostsSelected}
/>
);
const renderHeaderLabelBlock = () => {
if (selectedLabel) {
@ -1332,25 +1263,23 @@ const ManageHostsPage = ({
return null;
};
const renderHeader = () => {
return (
<div className={`${baseClass}__header`}>
<div className={`${baseClass}__text`}>
<div className={`${baseClass}__title`}>
{isFreeTier && <h1>Hosts</h1>}
{isPremiumTier &&
availableTeams &&
(availableTeams.length > 1 || isOnGlobalTeam) &&
renderTeamsFilterDropdown()}
{isPremiumTier &&
!isOnGlobalTeam &&
availableTeams &&
availableTeams.length === 1 && <h1>{availableTeams[0].name}</h1>}
</div>
const renderHeader = () => (
<div className={`${baseClass}__header`}>
<div className={`${baseClass}__text`}>
<div className={`${baseClass}__title`}>
{isFreeTier && <h1>Hosts</h1>}
{isPremiumTier &&
availableTeams &&
(availableTeams.length > 1 || isOnGlobalTeam) &&
renderTeamsFilterDropdown()}
{isPremiumTier &&
!isOnGlobalTeam &&
availableTeams &&
availableTeams.length === 1 && <h1>{availableTeams[0].name}</h1>}
</div>
</div>
);
};
</div>
);
const onExportHostsResults = async (
evt: React.MouseEvent<HTMLButtonElement>
@ -1455,7 +1384,7 @@ const ManageHostsPage = ({
return (
<div className="body-wrap">
<LabelForm
onCancel={onCancelAddLabel}
onCancel={onCancelLabel}
onOsqueryTableSelect={onOsqueryTableSelect}
handleSubmit={onSaveAddLabel}
baseError={labelsError?.message || ""}
@ -1470,7 +1399,7 @@ const ManageHostsPage = ({
<div className="body-wrap">
<LabelForm
selectedLabel={selectedLabel}
onCancel={onCancelEditLabel}
onCancel={onCancelLabel}
onOsqueryTableSelect={onOsqueryTableSelect}
handleSubmit={onEditLabel}
baseError={labelsError?.message || ""}
@ -1512,17 +1441,15 @@ const ManageHostsPage = ({
return SidePanel;
};
const renderStatusDropdown = () => {
return (
<Dropdown
value={getStatusSelected() || ALL_HOSTS_LABEL}
className={`${baseClass}__status_dropdown`}
options={HOST_SELECT_STATUSES}
searchable={false}
onChange={handleStatusDropdownChange}
/>
);
};
const renderStatusDropdown = () => (
<Dropdown
value={getStatusSelected() || ALL_HOSTS_LABEL}
className={`${baseClass}__status_dropdown`}
options={HOST_SELECT_STATUSES}
searchable={false}
onChange={handleStatusDropdownChange}
/>
);
const renderTable = () => {
if (
@ -1606,7 +1533,7 @@ const ManageHostsPage = ({
}
emptyComponent={EmptyHosts}
customControl={renderStatusDropdown}
onActionButtonClick={onEditColumnsClick}
onActionButtonClick={toggleEditColumnsModal}
onPrimarySelectActionClick={onDeleteHostsClick}
onQueryChange={onTableQueryChange}
toggleAllPagesSelected={toggleAllMatchingHosts}
@ -1703,14 +1630,14 @@ const ManageHostsPage = ({
</div>
)}
{renderSidePanel()}
{renderDeleteSecretModal()}
{renderSecretEditorModal()}
{renderEnrollSecretModal()}
{renderEditColumnsModal()}
{renderDeleteLabelModal()}
{renderAddHostsModal()}
{renderTransferHostModal()}
{renderDeleteHostModal()}
{canEnrollHosts && showDeleteSecretModal && renderDeleteSecretModal()}
{canEnrollHosts && showSecretEditorModal && renderSecretEditorModal()}
{canEnrollHosts && showEnrollSecretModal && renderEnrollSecretModal()}
{showEditColumnsModal && renderEditColumnsModal()}
{showDeleteLabelModal && renderDeleteLabelModal()}
{showAddHostsModal && renderAddHostsModal()}
{showTransferHostModal && renderTransferHostModal()}
{showDeleteHostModal && renderDeleteHostModal()}
</div>
);
};

View file

@ -0,0 +1,47 @@
import React, { useEffect } from "react";
import Modal from "components/Modal";
import Button from "components/buttons/Button";
const baseClass = "delete-label-modal";
interface IDeleteLabelModalProps {
onSubmit: () => void;
onCancel: () => void;
}
const DeleteLabelModal = ({
onSubmit,
onCancel,
}: IDeleteLabelModalProps): JSX.Element => {
useEffect(() => {
const listener = (event: KeyboardEvent) => {
if (event.code === "Enter" || event.code === "NumpadEnter") {
event.preventDefault();
onSubmit();
}
};
document.addEventListener("keydown", listener);
return () => {
document.removeEventListener("keydown", listener);
};
}, []);
return (
<Modal title="Delete label" onExit={onCancel} className={baseClass}>
<>
<p>Are you sure you wish to delete this label?</p>
<div className={`${baseClass}__btn-wrap`}>
<Button onClick={onCancel} variant="inverse-alert">
Cancel
</Button>
<Button onClick={onSubmit} variant="alert">
Delete
</Button>
</div>
</>
</Modal>
);
};
export default DeleteLabelModal;

View file

@ -0,0 +1,11 @@
.delete-label-modal {
&__btn-wrap {
display: flex;
flex-direction: row-reverse;
margin-top: $pad-xxlarge;
}
&__btn {
margin-left: 12px;
}
}

View file

@ -0,0 +1 @@
export { default } from "./DeleteLabelModal";

View file

@ -1,6 +1,7 @@
import React, { useState } from "react";
import PropTypes from "prop-types";
import Modal from "components/Modal";
import Checkbox from "../../../../../components/forms/fields/Checkbox";
import Button from "../../../../../components/buttons/Button";
@ -57,37 +58,43 @@ const EditColumnsModal = ({
);
return (
<div className={"edit-column-modal"}>
<p>Choose which columns you see:</p>
<div className={"modal-items"}>
{columnItems.map((column) => {
if (column.disableHidden) return null;
return (
<div key={column.accessor}>
<Checkbox
name={column.name}
value={column.isChecked}
onChange={() => updateColumnItems(column.accessor)}
>
<span>{column.name}</span>
</Checkbox>
</div>
);
})}
</div>
<div className={"button-actions"}>
<Button onClick={onCancelColumns} variant={"inverse"}>
Cancel
</Button>
<Button
className={"save-button"}
onClick={() => onSaveColumns(getHiddenColumns(columnItems))}
variant={"default"}
>
Save
</Button>
</div>
</div>
<Modal
title="Edit columns"
onExit={onCancelColumns}
className={"edit-columns-modal"}
>
<>
<p>Choose which columns you see:</p>
<div className={"modal-items"}>
{columnItems.map((column) => {
if (column.disableHidden) return null;
return (
<div key={column.accessor}>
<Checkbox
name={column.name}
value={column.isChecked}
onChange={() => updateColumnItems(column.accessor)}
>
<span>{column.name}</span>
</Checkbox>
</div>
);
})}
</div>
<div className={"button-actions"}>
<Button onClick={onCancelColumns} variant={"inverse"}>
Cancel
</Button>
<Button
className={"save-button"}
onClick={() => onSaveColumns(getHiddenColumns(columnItems))}
variant={"default"}
>
Save
</Button>
</div>
</>
</Modal>
);
};

View file

@ -1,4 +1,4 @@
import React, { useState } from "react";
import React, { useState, useEffect } from "react";
import { IPolicy } from "interfaces/policy";
import { IWebhookFailingPolicies } from "interfaces/webhook";
@ -120,7 +120,9 @@ const ManageAutomationsModal = ({
setDestinationUrl(value);
};
const handleSaveAutomation = (evt: React.MouseEvent<HTMLFormElement>) => {
const handleSaveAutomation = (
evt: React.MouseEvent<HTMLFormElement> | KeyboardEvent
) => {
evt.preventDefault();
const { valid, errors: newErrors } = validateWebhookURL(destination_url);
@ -147,6 +149,19 @@ const ManageAutomationsModal = ({
}
};
useEffect(() => {
const listener = (event: KeyboardEvent) => {
if (event.code === "Enter" || event.code === "NumpadEnter") {
event.preventDefault();
handleSaveAutomation(event);
}
};
document.addEventListener("keydown", listener);
return () => {
document.removeEventListener("keydown", listener);
};
}, [handleSaveAutomation]);
if (showPreviewPayloadModal) {
return <PreviewPayloadModal onCancel={togglePreviewPayloadModal} />;
}

View file

@ -1,6 +1,6 @@
/* This component is used for creating and editing both global and team scheduled queries */
import React, { useState, useCallback, useContext } from "react";
import React, { useState, useCallback, useContext, useEffect } from "react";
import { pull } from "lodash";
import { AppContext } from "context/app";
@ -218,6 +218,19 @@ const ScheduleEditorModal = ({
);
};
useEffect(() => {
const listener = (event: KeyboardEvent) => {
if (event.code === "Enter" || event.code === "NumpadEnter") {
event.preventDefault();
onFormSubmit();
}
};
document.addEventListener("keydown", listener);
return () => {
document.removeEventListener("keydown", listener);
};
}, [onFormSubmit]);
if (showPreviewDataModal) {
return <PreviewDataModal onCancel={togglePreviewDataModal} />;
}