1
0
Fork 0
mirror of synced 2024-08-05 05:11:43 +12:00

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
This commit is contained in:
Conor Webb 2024-05-20 15:13:08 +01:00 committed by GitHub
parent 8f2a55e5ea
commit 3c74d29cf6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 97 additions and 17 deletions

View file

@ -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
</div>
</div>
<div class="setting-spacing">
<Toggle
text={automation.disabled ? "Paused" : "Activated"}
on:change={automationStore.actions.toggleDisabled(
automation._id,
automation.disabled
)}
value={!automation.disabled}
/>
</div>
</div>
</div>
<div class="canvas" on:scroll={handleScroll}>

View file

@ -61,6 +61,7 @@
selected={automation._id === selectedAutomationId}
on:click={() => selectAutomation(automation._id)}
selectedBy={$userSelectedResourceMap[automation._id]}
disabled={automation.disabled}
>
<EditAutomationPopover {automation} />
</NavItem>

View file

@ -39,6 +39,15 @@
>Duplicate</MenuItem
>
<MenuItem icon="Edit" on:click={updateAutomationDialog.show}>Edit</MenuItem>
<MenuItem
icon={automation.disabled ? "CheckmarkCircle" : "Cancel"}
on:click={automationStore.actions.toggleDisabled(
automation._id,
automation.disabled
)}
>
{automation.disabled ? "Activate" : "Pause"}
</MenuItem>
<MenuItem icon="Delete" on:click={confirmDeleteDialog.show}>Delete</MenuItem>
</ActionMenu>

View file

@ -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);

View file

@ -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 || {}

View file

@ -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 = {

View file

@ -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")

View file

@ -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)

View file

@ -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<string, any>; timeout?: number },
{ getResponses }: { getResponses?: boolean } = {}
): Promise<any> {
if (automation.disabled) {
throw new Error("Automation is disabled")
}
if (
automation.definition != null &&
automation.definition.trigger != null &&

View file

@ -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

View file

@ -125,6 +125,7 @@ export interface Automation extends Document {
name: string
internal?: boolean
type?: string
disabled?: boolean
}
interface BaseIOStructure {