Fleet UI: Enable and disable automation slider (#4554)

This commit is contained in:
RachelElysia
2022-03-11 11:56:14 -06:00
committed by GitHub
parent a401c8f97e
commit bb6db2434e
13 changed files with 228 additions and 226 deletions
@@ -0,0 +1 @@
* Add the ability to turn on and off software automations and failing policies automations from the UI
@@ -127,6 +127,7 @@ describe("Policies flow (seeded)", () => {
cy.findByRole("button", { name: /manage automations/i }).click();
});
cy.getAttached(".manage-automations-modal").within(() => {
cy.getAttached(".fleet-slider").click();
cy.getAttached(".fleet-checkbox__input").check({ force: true });
});
cy.getAttached("#webhook-url").click().type("www.foo.com/bar");
+13 -3
View File
@@ -197,15 +197,25 @@ describe(
cy.loginWithCySession("anna@organization.com", "user123#");
cy.visit("/software/manage");
});
it("allows global admin to click 'Manage automations' button", () => {
it("allows global admin to update software vulnerability automation", () => {
cy.getAttached(".manage-software-page__header-wrap").within(() => {
cy.findByRole("button", { name: /manage automations/i }).click();
});
cy.getAttached(".manage-automations-modal__button-wrap").within(() => {
cy.getAttached(".manage-automations-modal").within(() => {
cy.getAttached(".fleet-slider").click();
});
cy.getAttached("#webhook-url").click().type("www.foo.com/bar");
cy.findByRole("button", { name: /^Save$/ }).click();
// Confirm manage automations webhook was added successfully
cy.findByText(/updated vulnerability automations/i).should("exist");
cy.getAttached(".button-wrap").within(() => {
cy.findByRole("button", {
name: /cancel/i,
name: /manage automations/i,
}).click();
});
cy.getAttached(".manage-automations-modal").within(() => {
cy.getAttached(".fleet-slider--active").should("exist");
});
});
});
describe("Query pages", () => {
+13 -3
View File
@@ -114,18 +114,28 @@ describe("Premium tier - Admin user", () => {
});
describe("Manage software page", () => {
beforeEach(() => cy.visit("/software/manage"));
it("allows global admin to click 'Manage automations' button", () => {
it("allows global admin to update software vulnerability automation", () => {
cy.getAttached(".manage-software-page__header-wrap").within(() => {
cy.getAttached(".Select").within(() => {
cy.findByText(/all teams/i).should("exist");
});
cy.findByRole("button", { name: /manage automations/i }).click();
});
cy.getAttached(".manage-automations-modal__button-wrap").within(() => {
cy.getAttached(".manage-automations-modal").within(() => {
cy.getAttached(".fleet-slider").click();
});
cy.getAttached("#webhook-url").click().type("www.foo.com/bar");
cy.findByRole("button", { name: /^Save$/ }).click();
// Confirm manage automations webhook was added successfully
cy.findByText(/updated vulnerability automations/i).should("exist");
cy.getAttached(".button-wrap").within(() => {
cy.findByRole("button", {
name: /cancel/i,
name: /manage automations/i,
}).click();
});
cy.getAttached(".manage-automations-modal").within(() => {
cy.getAttached(".fleet-slider--active").should("exist");
});
});
it("hides manage automations button since all teams not selected", () => {
cy.getAttached(".manage-software-page__header-wrap").within(() => {
@@ -37,4 +37,8 @@
&--checkbox {
margin-bottom: $pad-medium;
}
&--slider {
margin-bottom: 0;
}
}
@@ -1,13 +1,21 @@
import React from "react";
import PropTypes from "prop-types";
import classnames from "classnames";
import { pick } from "lodash";
import FormField from "components/forms/FormField";
import { IFormFieldProps } from "components/forms/FormField/FormField";
const Slider = (props) => {
const { onChange, value, inactiveText = "Off", activeText = "On" } = props;
const baseClass = "fleet-slider";
interface ISliderProps {
onChange: () => void;
value: boolean;
inactiveText: string;
activeText: string;
}
const baseClass = "fleet-slider";
const Slider = (props: ISliderProps): JSX.Element => {
const { onChange, value, inactiveText, activeText } = props;
const sliderBtnClass = classnames(baseClass, {
[`${baseClass}--active`]: value,
@@ -17,39 +25,38 @@ const Slider = (props) => {
[`${baseClass}__dot--active`]: value,
});
const handleClick = (evt) => {
const handleClick = (evt: React.MouseEvent) => {
evt.preventDefault();
return onChange(!value);
return onChange();
};
const formFieldProps = pick(props, ["hint", "label", "error", "name"]);
const formFieldProps = pick(props, [
"hint",
"label",
"error",
"name",
]) as IFormFieldProps;
return (
<FormField {...formFieldProps} type="slider">
<div className={`${baseClass}__wrapper`}>
<span className={`${baseClass}__label ${baseClass}__label--inactive`}>
{inactiveText}
</span>
<button
className={`button button--unstyled ${sliderBtnClass}`}
onClick={handleClick}
>
<div className={sliderDotClass} />
</button>
<span className={`${baseClass}__label ${baseClass}__label--active`}>
{activeText}
<span
className={`${baseClass}__label ${baseClass}__label--${
value ? "active" : "inactive"
}`}
>
{value ? activeText : inactiveText}
</span>
</div>
</FormField>
);
};
Slider.propTypes = {
value: PropTypes.bool,
onChange: PropTypes.func,
inactiveText: PropTypes.string,
activeText: PropTypes.string,
};
export default Slider;
@@ -1,19 +1,16 @@
.fleet-slider {
transition: background-color 150ms ease-in-out;
background-color: $core-fleet-black;
background-color: $ui-fleet-black-50;
border-radius: 12px;
border: 1px solid #eaeaea;
cursor: pointer;
display: inline-block;
height: 22px;
min-width: 40px;
height: 20px;
min-width: 35px;
position: relative;
width: 40px;
box-shadow: inset 0 1px 6px 0 rgba(0, 0, 0, 0.2);
width: 35px;
&:hover {
background-color: $core-fleet-black;
box-shadow: inset 0 1px 6px 0 rgba(0, 0, 0, 0.2);
background-color: $ui-fleet-black-50;
}
&--active {
@@ -31,31 +28,22 @@
}
&__dot {
@include size(14px);
@include position(absolute, 0 null null 5px);
@include size(16px);
@include position(absolute, 2px null null 2px);
transition: left 150ms ease-in-out;
margin-top: 3px;
border-radius: 50%;
background-color: $core-white;
box-shadow: 0 2px 3px 0 rgba(0, 0, 0, 0.25);
&--active {
left: 21px;
left: 17px;
}
}
&__label {
font-size: $small;
font-size: $x-small;
font-weight: $regular;
text-align: left;
vertical-align: text-bottom;
margin-right: 10px;
color: $core-fleet-blue;
&--active {
color: $core-vibrant-blue;
margin-right: 0;
margin-left: 10px;
}
margin-left: $pad-small;
}
}
@@ -517,6 +517,11 @@ const ManagePolicyPage = ({
failingPoliciesWebhook.destination_url) ||
""
}
enableFailingPoliciesWebhook={
(failingPoliciesWebhook &&
failingPoliciesWebhook.enable_failing_policies_webhook) ||
false
}
/>
)}
{showAddPolicyModal && (
@@ -7,6 +7,7 @@ import { size } from "lodash";
import Modal from "components/Modal";
import Button from "components/buttons/Button";
import Slider from "components/forms/fields/Slider";
// @ts-ignore
import Checkbox from "components/forms/fields/Checkbox";
// @ts-ignore
@@ -21,6 +22,7 @@ interface IManageAutomationsModalProps {
availablePolicies: IPolicy[];
currentAutomatedPolicies?: number[];
currentDestinationUrl?: string;
enableFailingPoliciesWebhook: boolean;
}
interface ICheckedPolicy {
@@ -92,11 +94,16 @@ const ManageAutomationsModal = ({
availablePolicies,
currentAutomatedPolicies,
currentDestinationUrl,
enableFailingPoliciesWebhook,
}: IManageAutomationsModalProps): JSX.Element => {
const [destination_url, setDestinationUrl] = useState<string>(
currentDestinationUrl || ""
);
const [errors, setErrors] = useState<{ [key: string]: string }>({});
const [
policyAutomationEnabled,
setPolicyAutomationEnabled,
] = useState<boolean>(enableFailingPoliciesWebhook);
const { policyItems, updatePolicyItems } = useCheckboxListStateManagement(
availablePolicies,
@@ -127,14 +134,13 @@ const ManageAutomationsModal = ({
policyItems
.filter((policy) => policy.isChecked)
.map((policy) => policy.id);
const enable_failing_policies_webhook = true; // Leave nearest component in case we decide to add disabling as a UI feature
// URL validation only needed if at least one policy is checked
if (valid || policy_ids.length === 0) {
if (valid || !enableFailingPoliciesWebhook) {
onCreateWebhookSubmit({
destination_url,
policy_ids,
enable_failing_policies_webhook,
enable_failing_policies_webhook: policyAutomationEnabled,
});
onReturnToApp();
@@ -152,55 +158,75 @@ const ManageAutomationsModal = ({
className={baseClass}
>
<div className={baseClass}>
{availablePolicies && availablePolicies.length > 0 ? (
<div className={`${baseClass}__policy-select-items`}>
<p> Choose which policies you would like to listen to:</p>
{policyItems &&
policyItems.map((policyItem) => {
const { isChecked, name, id } = policyItem;
return (
<div key={id} className={`${baseClass}__team-item`}>
<Checkbox
value={isChecked}
name={name}
onChange={() => updatePolicyItems(policyItem.id)}
>
{name}
</Checkbox>
</div>
);
})}
</div>
) : (
<div className={`${baseClass}__no-policies`}>
<b>You have no policies.</b>
<p>Add a policy to turn on automations.</p>
</div>
)}
<div className="tooltip-wrap tooltip-wrap--input">
<InputField
inputWrapperClass={`${baseClass}__url-input`}
name="webhook-url"
label={"Destination URL"}
type={"text"}
value={destination_url}
onChange={onURLChange}
error={errors.url}
hint={
'For each policy, Fleet will send a JSON payload to this URL with a list of the hosts that updated their answer to "No."'
<div className={`${baseClass}__software-select-items`}>
<Slider
value={policyAutomationEnabled}
onChange={() =>
setPolicyAutomationEnabled(!policyAutomationEnabled)
}
placeholder={"https://server.com/example"}
tooltip="Provide a URL to deliver a webhook request to."
inactiveText={"Vulnerability automations disabled"}
activeText={"Vulnerability automations enabled"}
/>
</div>
<Button
type="button"
variant="text-link"
onClick={togglePreviewPayloadModal}
>
Preview payload
</Button>
<div className={`${baseClass}__overlay-container`}>
<div className={`${baseClass}__policy-automation-enabled`}>
{availablePolicies && availablePolicies.length > 0 ? (
<div className={`${baseClass}__policy-select-items`}>
<p>
<strong>
Choose which policies you would like to listen to:
</strong>
</p>
{policyItems &&
policyItems.map((policyItem) => {
const { isChecked, name, id } = policyItem;
return (
<div key={id} className={`${baseClass}__team-item`}>
<Checkbox
value={isChecked}
name={name}
onChange={() => updatePolicyItems(policyItem.id)}
>
{name}
</Checkbox>
</div>
);
})}
</div>
) : (
<div className={`${baseClass}__no-policies`}>
<b>You have no policies.</b>
<p>Add a policy to turn on automations.</p>
</div>
)}
<div className="tooltip-wrap tooltip-wrap--input">
<InputField
inputWrapperClass={`${baseClass}__url-input`}
name="webhook-url"
label={"Destination URL"}
type={"text"}
value={destination_url}
onChange={onURLChange}
error={errors.url}
hint={
'For each policy, Fleet will send a JSON payload to this URL with a list of the hosts that updated their answer to "No."'
}
placeholder={"https://server.com/example"}
tooltip="Provide a URL to deliver a webhook request to."
/>
</div>
<Button
type="button"
variant="text-link"
onClick={togglePreviewPayloadModal}
>
Preview payload
</Button>
</div>
{!policyAutomationEnabled && (
<div className={`${baseClass}__overlay`} />
)}
</div>
<div className={`${baseClass}__button-wrap`}>
<Button
className={`${baseClass}__btn`}
@@ -26,6 +26,21 @@
color: $ui-error;
}
&__overlay-container {
display: grid;
}
&__policy-automation-enabled,
&__overlay {
grid-area: 1 / 1;
}
&__overlay {
background-color: $core-white;
z-index: 0;
opacity: 75%;
}
.tooltip-wrap {
display: flex;
align-items: center;
@@ -2,8 +2,7 @@ import React, { useState } from "react";
import Modal from "components/Modal";
import Button from "components/buttons/Button";
// @ts-ignore
import Checkbox from "components/forms/fields/Checkbox";
import Slider from "components/forms/fields/Slider";
// @ts-ignore
import InputField from "components/forms/fields/InputField";
@@ -22,75 +21,6 @@ interface IManageAutomationsModalProps {
currentDestinationUrl?: string;
}
interface ICheckedSoftwareAutomation {
name?: string;
accessor: string;
isChecked?: boolean;
}
const useCheckboxListStateManagement = (
softwareVulnerabilityWebhookEnabled = false
) => {
// Ability to add future software automations
const availableSoftwareAutomations: ICheckedSoftwareAutomation[] = [
{ accessor: "vulnerability", name: "Enable vulnerability automations" },
];
const currentSoftwareAutomations: ICheckedSoftwareAutomation[] = softwareVulnerabilityWebhookEnabled
? availableSoftwareAutomations
: [];
const [softwareAutomationsItems, setSoftwareAutomationsItems] = useState<
ICheckedSoftwareAutomation[]
>(() => {
return (
availableSoftwareAutomations &&
availableSoftwareAutomations.map(
(automation: ICheckedSoftwareAutomation) => {
return {
name: automation.name,
accessor: automation.accessor,
isChecked: currentSoftwareAutomations.some(
(currentSoftwareAutomationItem: ICheckedSoftwareAutomation) =>
currentSoftwareAutomationItem.accessor === automation.accessor
),
};
}
)
);
});
const updateSoftwareAutomationsItems = (
softwareAutomationAccessor: string
) => {
setSoftwareAutomationsItems((prevState) => {
const selectedSoftwareAutomation = softwareAutomationsItems.find(
(softwareAutomationItem) =>
softwareAutomationItem.accessor === softwareAutomationAccessor
);
const updatedSoftwareAutomation = selectedSoftwareAutomation && {
...selectedSoftwareAutomation,
isChecked:
!!selectedSoftwareAutomation && !selectedSoftwareAutomation.isChecked,
};
// this is replacing the softwareAutomation object with the updatedSoftwareAutomation we just created
const newState = prevState.map((currentSoftwareAutomation) => {
return currentSoftwareAutomation.accessor ===
softwareAutomationAccessor && updatedSoftwareAutomation
? updatedSoftwareAutomation
: currentSoftwareAutomation;
});
return newState;
});
};
return {
softwareAutomationsItems,
updateSoftwareAutomationsItems,
};
};
const validateWebhookURL = (url: string) => {
const errors: { [key: string]: string } = {};
@@ -117,10 +47,10 @@ const ManageAutomationsModal = ({
);
const [errors, setErrors] = useState<{ [key: string]: string }>({});
const {
softwareAutomationsItems,
updateSoftwareAutomationsItems,
} = useCheckboxListStateManagement(softwareVulnerabilityWebhookEnabled);
const [
softwareAutomationsEnabled,
setSoftwareAutomationsEnabled,
] = useState<boolean>(softwareVulnerabilityWebhookEnabled || false);
useDeepEffect(() => {
if (destination_url) {
@@ -135,12 +65,6 @@ const ManageAutomationsModal = ({
const handleSaveAutomation = (evt: React.MouseEvent<HTMLFormElement>) => {
evt.preventDefault();
// Ability to add future software automations
const vulnerabilityWebhook = softwareAutomationsItems.find(
(softwareAutomationItem) =>
softwareAutomationItem.accessor === "vulnerability"
);
const { valid, errors: newErrors } = validateWebhookURL(destination_url);
setErrors({
...errors,
@@ -148,10 +72,10 @@ const ManageAutomationsModal = ({
});
// URL validation only needed if software automation is checked
if (valid || !vulnerabilityWebhook?.isChecked) {
if (valid || !softwareAutomationsEnabled) {
onCreateWebhookSubmit({
destination_url,
enable_vulnerabilities_webhook: vulnerabilityWebhook?.isChecked,
enable_vulnerabilities_webhook: softwareAutomationsEnabled,
});
onReturnToApp();
@@ -170,56 +94,52 @@ const ManageAutomationsModal = ({
>
<div className={baseClass}>
<div className={`${baseClass}__software-select-items`}>
{softwareAutomationsItems &&
softwareAutomationsItems.map(
(softwareItem: ICheckedSoftwareAutomation) => {
const { isChecked, name, accessor } = softwareItem;
return (
<div key={accessor} className={`${baseClass}__team-item`}>
<Checkbox
value={isChecked}
name={name}
onChange={() =>
updateSoftwareAutomationsItems(softwareItem.accessor)
}
>
{name}
</Checkbox>
</div>
);
}
)}
</div>
<div className={`${baseClass}__software-automation-description`}>
<p>
A request will be sent to your configured <b>Destination URL</b> if
a detected vulnerability (CVE) was published in the last 2 days.
</p>
</div>
<div className="tooltip-wrap tooltip-wrap--input">
<InputField
inputWrapperClass={`${baseClass}__url-input`}
name="webhook-url"
label={"Destination URL"}
type={"text"}
value={destination_url}
onChange={onURLChange}
error={errors.url}
hint={
"For each new vulnerability detected, Fleet will send a JSON payload to this URL with a list of the affected hosts."
<Slider
value={softwareAutomationsEnabled}
onChange={() =>
setSoftwareAutomationsEnabled(!softwareAutomationsEnabled)
}
placeholder={"https://server.com/example"}
tooltip="Provide a URL to deliver a webhook request to."
inactiveText={"Vulnerability automations disabled"}
activeText={"Vulnerability automations enabled"}
/>
</div>
<Button
type="button"
variant="text-link"
onClick={togglePreviewPayloadModal}
>
Preview payload
</Button>
<div className={`${baseClass}__overlay-container`}>
<div className={`${baseClass}__software-automation-enabled`}>
<div className={`${baseClass}__software-automation-description`}>
<p>
A request will be sent to your configured <b>Destination URL</b>{" "}
if a detected vulnerability (CVE) was published in the last 2
days.
</p>
</div>
<div className="tooltip-wrap tooltip-wrap--input">
<InputField
inputWrapperClass={`${baseClass}__url-input`}
name="webhook-url"
label={"Destination URL"}
type={"text"}
value={destination_url}
onChange={onURLChange}
error={errors.url}
hint={
"For each new vulnerability detected, Fleet will send a JSON payload to this URL with a list of the affected hosts."
}
placeholder={"https://server.com/example"}
tooltip="Provide a URL to deliver a webhook request to."
/>
</div>
<Button
type="button"
variant="text-link"
onClick={togglePreviewPayloadModal}
>
Preview payload
</Button>
</div>
{!softwareAutomationsEnabled && (
<div className={`${baseClass}__overlay`} />
)}
</div>
<div className={`${baseClass}__button-wrap`}>
<Button
className={`${baseClass}__btn`}
@@ -26,6 +26,21 @@
color: $ui-error;
}
&__overlay-container {
display: grid;
}
&__software-automation-enabled,
&__overlay {
grid-area: 1 / 1;
}
&__overlay {
background-color: $core-white;
z-index: 0;
opacity: 75%;
}
&__software-automation-description {
p {
margin: $pad-large 0;