diff --git a/changes/issue-3603-turn-on-off-all-automations b/changes/issue-3603-turn-on-off-all-automations new file mode 100644 index 0000000000..2c35d40398 --- /dev/null +++ b/changes/issue-3603-turn-on-off-all-automations @@ -0,0 +1 @@ +* Add the ability to turn on and off software automations and failing policies automations from the UI \ No newline at end of file diff --git a/cypress/integration/all/app/policiesflow.spec.ts b/cypress/integration/all/app/policiesflow.spec.ts index d360707a79..39c90174a6 100644 --- a/cypress/integration/all/app/policiesflow.spec.ts +++ b/cypress/integration/all/app/policiesflow.spec.ts @@ -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"); diff --git a/cypress/integration/free/admin.spec.ts b/cypress/integration/free/admin.spec.ts index 65c7db28f2..a70503c0e3 100644 --- a/cypress/integration/free/admin.spec.ts +++ b/cypress/integration/free/admin.spec.ts @@ -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", () => { diff --git a/cypress/integration/premium/admin.spec.ts b/cypress/integration/premium/admin.spec.ts index a5a8990009..7ebb8b10af 100644 --- a/cypress/integration/premium/admin.spec.ts +++ b/cypress/integration/premium/admin.spec.ts @@ -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(() => { diff --git a/frontend/components/forms/FormField/_styles.scss b/frontend/components/forms/FormField/_styles.scss index 2cc938f3d2..38793e86b3 100644 --- a/frontend/components/forms/FormField/_styles.scss +++ b/frontend/components/forms/FormField/_styles.scss @@ -37,4 +37,8 @@ &--checkbox { margin-bottom: $pad-medium; } + + &--slider { + margin-bottom: 0; + } } diff --git a/frontend/components/forms/fields/Slider/Slider.jsx b/frontend/components/forms/fields/Slider/Slider.tsx similarity index 51% rename from frontend/components/forms/fields/Slider/Slider.jsx rename to frontend/components/forms/fields/Slider/Slider.tsx index 9b31e68a4d..7b43be0154 100644 --- a/frontend/components/forms/fields/Slider/Slider.jsx +++ b/frontend/components/forms/fields/Slider/Slider.tsx @@ -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 (
- - {inactiveText} - - - {activeText} + + {value ? activeText : inactiveText}
); }; -Slider.propTypes = { - value: PropTypes.bool, - onChange: PropTypes.func, - inactiveText: PropTypes.string, - activeText: PropTypes.string, -}; - export default Slider; diff --git a/frontend/components/forms/fields/Slider/_styles.scss b/frontend/components/forms/fields/Slider/_styles.scss index 8e41185854..9b79918c29 100644 --- a/frontend/components/forms/fields/Slider/_styles.scss +++ b/frontend/components/forms/fields/Slider/_styles.scss @@ -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; } } diff --git a/frontend/components/forms/fields/Slider/index.js b/frontend/components/forms/fields/Slider/index.ts similarity index 100% rename from frontend/components/forms/fields/Slider/index.js rename to frontend/components/forms/fields/Slider/index.ts diff --git a/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx b/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx index eec958ca7a..0d255773f1 100644 --- a/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx +++ b/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx @@ -517,6 +517,11 @@ const ManagePolicyPage = ({ failingPoliciesWebhook.destination_url) || "" } + enableFailingPoliciesWebhook={ + (failingPoliciesWebhook && + failingPoliciesWebhook.enable_failing_policies_webhook) || + false + } /> )} {showAddPolicyModal && ( diff --git a/frontend/pages/policies/ManagePoliciesPage/components/ManageAutomationsModal/ManageAutomationsModal.tsx b/frontend/pages/policies/ManagePoliciesPage/components/ManageAutomationsModal/ManageAutomationsModal.tsx index 10db0e5ae1..ff2d89aa34 100644 --- a/frontend/pages/policies/ManagePoliciesPage/components/ManageAutomationsModal/ManageAutomationsModal.tsx +++ b/frontend/pages/policies/ManagePoliciesPage/components/ManageAutomationsModal/ManageAutomationsModal.tsx @@ -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( currentDestinationUrl || "" ); const [errors, setErrors] = useState<{ [key: string]: string }>({}); + const [ + policyAutomationEnabled, + setPolicyAutomationEnabled, + ] = useState(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} >
- {availablePolicies && availablePolicies.length > 0 ? ( -
-

Choose which policies you would like to listen to:

- {policyItems && - policyItems.map((policyItem) => { - const { isChecked, name, id } = policyItem; - return ( -
- updatePolicyItems(policyItem.id)} - > - {name} - -
- ); - })} -
- ) : ( -
- You have no policies. -

Add a policy to turn on automations.

-
- )} -
- + + 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"} />
- - +
+
+ {availablePolicies && availablePolicies.length > 0 ? ( +
+

+ + Choose which policies you would like to listen to: + +

+ {policyItems && + policyItems.map((policyItem) => { + const { isChecked, name, id } = policyItem; + return ( +
+ updatePolicyItems(policyItem.id)} + > + {name} + +
+ ); + })} +
+ ) : ( +
+ You have no policies. +

Add a policy to turn on automations.

+
+ )} +
+ +
+ +
+ {!policyAutomationEnabled && ( +
+ )} +
- +
+
+
+

+ A request will be sent to your configured Destination URL{" "} + if a detected vulnerability (CVE) was published in the last 2 + days. +

+
+
+ +
+ +
+ {!softwareAutomationsEnabled && ( +
+ )} +