From 3c74d29cf6ff371cc206dfc56f55244ae430a7d2 Mon Sep 17 00:00:00 2001 From: Conor Webb <126772285+ConorWebb96@users.noreply.github.com> Date: Mon, 20 May 2024 15:13:08 +0100 Subject: [PATCH] Added the ability to disable automations (#13667) * Added disabling functionality for automations * Removed external trigger automations that are disabled from selectable bindings * Added new popover option for disabling automations * Added toggle UI Inside automation screen * Added subtle styling to automation list for disabled functionality. * Fixed linting error * Removed duplicate bbui import * Fixed store function spacing * Fixed linting issues. * Added the requested changes to how disable is handled. * Fixed linting issues. * Minor UI tweaks based on feedback. * Added logic to prevent crons type automations from running when disabled. * Removing webhook disable, causes trigger url to be re-generated. * Add unit test to ensure disabled automations are filtered out of the active queue * Fixed lint issues * Reverted disabled unit test * Added error throw for disabled automations * Add test for when a disabled automation gets triggered * Added try, catch for trigger function - error handling * Fixed linting issues --- .../FlowChart/FlowChart.svelte | 12 ++++++- .../AutomationPanel/AutomationPanel.svelte | 1 + .../EditAutomationPopover.svelte | 9 +++++ .../src/components/common/NavItem.svelte | 5 +++ .../actions/TriggerAutomation.svelte | 4 ++- .../builder/src/stores/builder/automations.js | 23 +++++++++++++ .../server/src/api/controllers/automation.ts | 34 ++++++++++++------- .../src/api/routes/tests/automation.spec.ts | 17 ++++++++++ packages/server/src/automations/triggers.ts | 7 ++-- packages/server/src/automations/utils.ts | 1 + .../types/src/documents/app/automation.ts | 1 + 11 files changed, 97 insertions(+), 17 deletions(-) diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte index 22303dec1f..d68e57ca36 100644 --- a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte +++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte @@ -9,7 +9,7 @@ import TestDataModal from "./TestDataModal.svelte" import { flip } from "svelte/animate" import { fly } from "svelte/transition" - import { Icon, notifications, Modal } from "@budibase/bbui" + import { Icon, notifications, Modal, Toggle } from "@budibase/bbui" import { ActionStepID } from "constants/backend/automations" import UndoRedoControl from "components/common/UndoRedoControl.svelte" @@ -73,6 +73,16 @@ Test details +
+ +
diff --git a/packages/builder/src/components/automation/AutomationPanel/AutomationPanel.svelte b/packages/builder/src/components/automation/AutomationPanel/AutomationPanel.svelte index ac1c4f91cb..7898e13ec8 100644 --- a/packages/builder/src/components/automation/AutomationPanel/AutomationPanel.svelte +++ b/packages/builder/src/components/automation/AutomationPanel/AutomationPanel.svelte @@ -61,6 +61,7 @@ selected={automation._id === selectedAutomationId} on:click={() => selectAutomation(automation._id)} selectedBy={$userSelectedResourceMap[automation._id]} + disabled={automation.disabled} > diff --git a/packages/builder/src/components/automation/AutomationPanel/EditAutomationPopover.svelte b/packages/builder/src/components/automation/AutomationPanel/EditAutomationPopover.svelte index 1bc4b0f18e..9465374ae2 100644 --- a/packages/builder/src/components/automation/AutomationPanel/EditAutomationPopover.svelte +++ b/packages/builder/src/components/automation/AutomationPanel/EditAutomationPopover.svelte @@ -39,6 +39,15 @@ >Duplicate Edit + + {automation.disabled ? "Activate" : "Pause"} + Delete diff --git a/packages/builder/src/components/common/NavItem.svelte b/packages/builder/src/components/common/NavItem.svelte index 66b21e95a1..5cc6db65a0 100644 --- a/packages/builder/src/components/common/NavItem.svelte +++ b/packages/builder/src/components/common/NavItem.svelte @@ -25,6 +25,7 @@ export let selectedBy = null export let compact = false export let hovering = false + export let disabled = false const scrollApi = getContext("scroll") const dispatch = createEventDispatcher() @@ -74,6 +75,7 @@ class:scrollable class:highlighted class:selectedBy + class:disabled on:dragend on:dragstart on:dragover @@ -165,6 +167,9 @@ --avatars-background: var(--spectrum-global-color-gray-300); color: var(--ink); } + .nav-item.disabled span { + color: var(--spectrum-global-color-gray-700); + } .nav-item:hover, .hovering { background-color: var(--spectrum-global-color-gray-200); diff --git a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/TriggerAutomation.svelte b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/TriggerAutomation.svelte index 0a52a693c3..5cd5658063 100644 --- a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/TriggerAutomation.svelte +++ b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/TriggerAutomation.svelte @@ -24,7 +24,9 @@ parameters } $: automations = $automationStore.automations - .filter(a => a.definition.trigger?.stepId === TriggerStepID.APP) + .filter( + a => a.definition.trigger?.stepId === TriggerStepID.APP && !a.disabled + ) .map(automation => { const schema = Object.entries( automation.definition.trigger.inputs.fields || {} diff --git a/packages/builder/src/stores/builder/automations.js b/packages/builder/src/stores/builder/automations.js index cbe48cef33..ff53dda86b 100644 --- a/packages/builder/src/stores/builder/automations.js +++ b/packages/builder/src/stores/builder/automations.js @@ -82,6 +82,7 @@ const automationActions = store => ({ steps: [], trigger, }, + disabled: false, } const response = await store.actions.save(automation) await store.actions.fetch() @@ -134,6 +135,28 @@ const automationActions = store => ({ }) await store.actions.fetch() }, + toggleDisabled: async automationId => { + let automation + try { + automation = store.actions.getDefinition(automationId) + if (!automation) { + return + } + automation.disabled = !automation.disabled + await store.actions.save(automation) + notifications.success( + `Automation ${ + automation.disabled ? "enabled" : "disabled" + } successfully` + ) + } catch (error) { + notifications.error( + `Error ${ + automation && automation.disabled ? "enabling" : "disabling" + } automation` + ) + } + }, updateBlockInputs: async (block, data) => { // Create new modified block let newBlock = { diff --git a/packages/server/src/api/controllers/automation.ts b/packages/server/src/api/controllers/automation.ts index 5b199341b1..a49fe834d3 100644 --- a/packages/server/src/api/controllers/automation.ts +++ b/packages/server/src/api/controllers/automation.ts @@ -274,20 +274,28 @@ export async function trigger(ctx: UserCtx) { let hasCollectStep = sdk.automations.utils.checkForCollectStep(automation) if (hasCollectStep && (await features.isSyncAutomationsEnabled())) { - const response: AutomationResults = await triggers.externalTrigger( - automation, - { - fields: ctx.request.body.fields, - timeout: - ctx.request.body.timeout * 1000 || env.AUTOMATION_THREAD_TIMEOUT, - }, - { getResponses: true } - ) + try { + const response: AutomationResults = await triggers.externalTrigger( + automation, + { + fields: ctx.request.body.fields, + timeout: + ctx.request.body.timeout * 1000 || env.AUTOMATION_THREAD_TIMEOUT, + }, + { getResponses: true } + ) - let collectedValue = response.steps.find( - step => step.stepId === AutomationActionStepId.COLLECT - ) - ctx.body = collectedValue?.outputs + let collectedValue = response.steps.find( + step => step.stepId === AutomationActionStepId.COLLECT + ) + ctx.body = collectedValue?.outputs + } catch (err: any) { + if (err.message) { + ctx.throw(400, err.message) + } else { + throw err + } + } } else { if (ctx.appId && !dbCore.isProdAppID(ctx.appId)) { ctx.throw(400, "Only apps in production support this endpoint") diff --git a/packages/server/src/api/routes/tests/automation.spec.ts b/packages/server/src/api/routes/tests/automation.spec.ts index 7885e97fbf..711cfb8d4f 100644 --- a/packages/server/src/api/routes/tests/automation.spec.ts +++ b/packages/server/src/api/routes/tests/automation.spec.ts @@ -206,6 +206,23 @@ describe("/automations", () => { expect(res.body.value).toEqual([1, 2, 3]) }) + it("should throw an error when attempting to trigger a disabled automation", async () => { + mocks.licenses.useSyncAutomations() + let automation = collectAutomation() + automation = await config.createAutomation({ + ...automation, + disabled: true, + }) + + const res = await request + .post(`/api/automations/${automation._id}/trigger`) + .set(config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(400) + + expect(res.body.message).toEqual("Automation is disabled") + }) + it("triggers an asynchronous automation", async () => { let automation = newAutomation() automation = await config.createAutomation(automation) diff --git a/packages/server/src/automations/triggers.ts b/packages/server/src/automations/triggers.ts index 08e3199a11..223b8d2eb6 100644 --- a/packages/server/src/automations/triggers.ts +++ b/packages/server/src/automations/triggers.ts @@ -36,10 +36,10 @@ async function queueRelevantRowAutomations( await context.doInAppContext(event.appId, async () => { let automations = await getAllAutomations() - // filter down to the correct event type + // filter down to the correct event type and enabled automations automations = automations.filter(automation => { const trigger = automation.definition.trigger - return trigger && trigger.event === eventType + return trigger && trigger.event === eventType && !automation.disabled }) for (let automation of automations) { @@ -94,6 +94,9 @@ export async function externalTrigger( params: { fields: Record; timeout?: number }, { getResponses }: { getResponses?: boolean } = {} ): Promise { + if (automation.disabled) { + throw new Error("Automation is disabled") + } if ( automation.definition != null && automation.definition.trigger != null && diff --git a/packages/server/src/automations/utils.ts b/packages/server/src/automations/utils.ts index 360802df6a..4d7e169f52 100644 --- a/packages/server/src/automations/utils.ts +++ b/packages/server/src/automations/utils.ts @@ -196,6 +196,7 @@ export async function enableCronTrigger(appId: any, automation: Automation) { if ( isCronTrigger(automation) && !isRebootTrigger(automation) && + !automation.disabled && trigger?.inputs.cron ) { const cronExp = trigger.inputs.cron diff --git a/packages/types/src/documents/app/automation.ts b/packages/types/src/documents/app/automation.ts index 481a051e1c..6d1753dc28 100644 --- a/packages/types/src/documents/app/automation.ts +++ b/packages/types/src/documents/app/automation.ts @@ -125,6 +125,7 @@ export interface Automation extends Document { name: string internal?: boolean type?: string + disabled?: boolean } interface BaseIOStructure {