diff --git a/packages/builder/assets/MagicWand.svelte b/packages/builder/assets/MagicWand.svelte new file mode 100644 index 00000000000..14a06ebaecf --- /dev/null +++ b/packages/builder/assets/MagicWand.svelte @@ -0,0 +1,29 @@ + + + + + + + + diff --git a/packages/builder/package.json b/packages/builder/package.json index f9e6becbabd..aec0b509f08 100644 --- a/packages/builder/package.json +++ b/packages/builder/package.json @@ -67,6 +67,7 @@ "@spectrum-css/vars": "^3.0.1", "@zerodevx/svelte-json-view": "^1.0.7", "codemirror": "^5.65.16", + "cron-parser": "^4.9.0", "dayjs": "^1.10.8", "downloadjs": "1.4.7", "fast-json-patch": "^3.1.1", diff --git a/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte b/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte index 66fb8d9cbae..bc65c234e91 100644 --- a/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte +++ b/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte @@ -1050,7 +1050,7 @@ {:else if value.customType === "cron"} onChange({ [key]: e.detail })} - value={inputData[key]} + cronExpression={inputData[key]} /> {:else if value.customType === "automationFields"} - import { Button, Select, Input, Label } from "@budibase/bbui" + import { + Select, + InlineAlert, + Input, + Label, + Layout, + notifications, + } from "@budibase/bbui" import { onMount, createEventDispatcher } from "svelte" import { flags } from "stores/builder" + import { licensing } from "stores/portal" + import { API } from "api" + import MagicWand from "../../../../assets/MagicWand.svelte" + import { helpers, REBOOT_CRON } from "@budibase/shared-core" const dispatch = createEventDispatcher() - export let value + export let cronExpression + let error + let nextExecutions + // AI prompt + let aiCronPrompt = "" + let loadingAICronExpression = false + + $: aiEnabled = + $licensing.customAIConfigsEnabled || $licensing.budibaseAIEnabled $: { - const exists = CRON_EXPRESSIONS.some(cron => cron.value === value) - const customIndex = CRON_EXPRESSIONS.findIndex( - cron => cron.label === "Custom" - ) - - if (!exists && customIndex === -1) { - CRON_EXPRESSIONS[0] = { label: "Custom", value: value } - } else if (exists && customIndex !== -1) { - CRON_EXPRESSIONS.splice(customIndex, 1) + if (cronExpression) { + try { + nextExecutions = helpers.cron + .getNextExecutionDates(cronExpression) + .join("\n") + } catch (err) { + nextExecutions = null + } } } const onChange = e => { - if (value !== REBOOT_CRON) { + if (e.detail !== REBOOT_CRON) { error = helpers.cron.validate(e.detail).err } - if (e.detail === value || error) { + if (e.detail === cronExpression || error) { return } - value = e.detail + cronExpression = e.detail dispatch("change", e.detail) } + const updatePreset = e => { + aiCronPrompt = "" + onChange(e) + } + + const updateCronExpression = e => { + aiCronPrompt = "" + cronExpression = null + nextExecutions = null + onChange(e) + } + let touched = false - let presets = false const CRON_EXPRESSIONS = [ { @@ -64,45 +93,130 @@ }) } }) + + async function generateAICronExpression() { + loadingAICronExpression = true + try { + const response = await API.generateCronExpression({ + prompt: aiCronPrompt, + }) + cronExpression = response.message + dispatch("change", response.message) + } catch (err) { + notifications.error(err.message) + } finally { + loadingAICronExpression = false + } + } - + + + + + {#if aiEnabled} + + + {#if aiCronPrompt} + + + + {/if} + + {/if} (touched = true)} updateOnChange={false} /> - {#if touched && !value} + {#if touched && !cronExpression} Please specify a CRON expression {/if} - - (presets = !presets)} - >{presets ? "Hide" : "Show"} Presets - {#if presets} - - {/if} - - + {#if nextExecutions} + + {/if} + diff --git a/packages/frontend-core/src/api/ai.js b/packages/frontend-core/src/api/ai.js new file mode 100644 index 00000000000..7fa756a19eb --- /dev/null +++ b/packages/frontend-core/src/api/ai.js @@ -0,0 +1,11 @@ +export const buildAIEndpoints = API => ({ + /** + * Generates a cron expression from a prompt + */ + generateCronExpression: async ({ prompt }) => { + return await API.post({ + url: "/api/ai/cron", + body: { prompt }, + }) + }, +}) diff --git a/packages/frontend-core/src/api/index.js b/packages/frontend-core/src/api/index.js index 066ab16f6e8..f8315cbd2d4 100644 --- a/packages/frontend-core/src/api/index.js +++ b/packages/frontend-core/src/api/index.js @@ -2,6 +2,7 @@ import { Helpers } from "@budibase/bbui" import { Header } from "@budibase/shared-core" import { ApiVersion } from "../constants" import { buildAnalyticsEndpoints } from "./analytics" +import { buildAIEndpoints } from "./ai" import { buildAppEndpoints } from "./app" import { buildAttachmentEndpoints } from "./attachments" import { buildAuthEndpoints } from "./auth" @@ -268,6 +269,7 @@ export const createAPIClient = config => { // Attach all endpoints return { ...API, + ...buildAIEndpoints(API), ...buildAnalyticsEndpoints(API), ...buildAppEndpoints(API), ...buildAttachmentEndpoints(API), diff --git a/packages/pro b/packages/pro index e2fe0f9cc85..aca9828117b 160000 --- a/packages/pro +++ b/packages/pro @@ -1 +1 @@ -Subproject commit e2fe0f9cc856b4ee1a97df96d623b2d87d4e8733 +Subproject commit aca9828117bb97f54f40ee359f1a3f6e259174e7 diff --git a/packages/server/src/api/routes/index.ts b/packages/server/src/api/routes/index.ts index 2079eb01fd3..e05bc44893d 100644 --- a/packages/server/src/api/routes/index.ts +++ b/packages/server/src/api/routes/index.ts @@ -33,6 +33,7 @@ import rowActionRoutes from "./rowAction" export { default as staticRoutes } from "./static" export { default as publicRoutes } from "./public" +const aiRoutes = pro.ai const appBackupRoutes = pro.appBackups const environmentVariableRoutes = pro.environmentVariables @@ -67,6 +68,7 @@ export const mainRoutes: Router[] = [ debugRoutes, environmentVariableRoutes, rowActionRoutes, + aiRoutes, // these need to be handled last as they still use /api/:tableId // this could be breaking as koa may recognise other routes as this tableRoutes, diff --git a/packages/shared-core/src/helpers/cron.ts b/packages/shared-core/src/helpers/cron.ts index ca1f1badb76..15abda3483b 100644 --- a/packages/shared-core/src/helpers/cron.ts +++ b/packages/shared-core/src/helpers/cron.ts @@ -1,4 +1,5 @@ import cronValidate from "cron-validate" +import cronParser from "cron-parser" const INPUT_CRON_START = "(Input cron: " const ERROR_SWAPS = { @@ -30,6 +31,19 @@ function improveErrors(errors: string[]): string[] { return finalErrors } +export function getNextExecutionDates( + cronExpression: string, + limit: number = 4 +): string[] { + const parsed = cronParser.parseExpression(cronExpression) + const nextRuns = [] + for (let i = 0; i < limit; i++) { + nextRuns.push(parsed.next().toString()) + } + + return nextRuns +} + export function validate( cronExpression: string ): { valid: false; err: string[] } | { valid: true } { diff --git a/yarn.lock b/yarn.lock index 46ac61d119d..0cddbf981fd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -817,23 +817,23 @@ tslib "^2.2.0" "@azure/msal-browser@^3.11.1": - version "3.23.0" - resolved "https://registry.yarnpkg.com/@azure/msal-browser/-/msal-browser-3.23.0.tgz#446aaf268247e5943f464f007d3aa3a04abfe95b" - integrity sha512-+QgdMvaeEpdtgRTD7AHHq9aw8uga7mXVHV1KshO1RQ2uI5B55xJ4aEpGlg/ga3H+0arEVcRfT4ZVmX7QLXiCVw== + version "3.24.0" + resolved "https://registry.yarnpkg.com/@azure/msal-browser/-/msal-browser-3.24.0.tgz#3208047672d0b0c943b0bef5f995d510d6582ae4" + integrity sha512-JGNV9hTYAa7lsum9IMIibn2kKczAojNihGo1hi7pG0kNrcKej530Fl6jxwM05A44/6I079CSn6WxYxbVhKUmWg== dependencies: - "@azure/msal-common" "14.14.2" + "@azure/msal-common" "14.15.0" -"@azure/msal-common@14.14.2": - version "14.14.2" - resolved "https://registry.yarnpkg.com/@azure/msal-common/-/msal-common-14.14.2.tgz#583b4ac9c089953718d7a5e2f3b8df2d4dbb17f4" - integrity sha512-XV0P5kSNwDwCA/SjIxTe9mEAsKB0NqGNSuaVrkCCE2lAyBr/D6YtD80Vkdp4tjWnPFwjzkwldjr1xU/facOJog== +"@azure/msal-common@14.15.0": + version "14.15.0" + resolved "https://registry.yarnpkg.com/@azure/msal-common/-/msal-common-14.15.0.tgz#0e27ac0bb88fe100f4f8d1605b64d5c268636a55" + integrity sha512-ImAQHxmpMneJ/4S8BRFhjt1MZ3bppmpRPYYNyzeQPeFN288YKbb8TmmISQEbtfkQ1BPASvYZU5doIZOPBAqENQ== "@azure/msal-node@^2.9.2": - version "2.13.1" - resolved "https://registry.yarnpkg.com/@azure/msal-node/-/msal-node-2.13.1.tgz#f144371275b7c3cbe564762b84772a9732457a47" - integrity sha512-sijfzPNorKt6+9g1/miHwhj6Iapff4mPQx1azmmZExgzUROqWTM1o3ACyxDja0g47VpowFy/sxTM/WsuCyXTiw== + version "2.14.0" + resolved "https://registry.yarnpkg.com/@azure/msal-node/-/msal-node-2.14.0.tgz#7881895d41b03d8b9b38a29550ba3bbb15f73b3c" + integrity sha512-rrfzIpG3Q1rHjVYZmHAEDidWAZZ2cgkxlIcMQ8dHebRISaZ2KCV33Q8Vs+uaV6lxweROabNxKFlR2lIKagZqYg== dependencies: - "@azure/msal-common" "14.14.2" + "@azure/msal-common" "14.15.0" jsonwebtoken "^9.0.0" uuid "^8.3.0" @@ -9167,6 +9167,13 @@ cron-parser@^4.2.1: dependencies: luxon "^3.2.1" +cron-parser@^4.9.0: + version "4.9.0" + resolved "https://registry.yarnpkg.com/cron-parser/-/cron-parser-4.9.0.tgz#0340694af3e46a0894978c6f52a6dbb5c0f11ad5" + integrity sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q== + dependencies: + luxon "^3.2.1" + cron-validate@1.4.5: version "1.4.5" resolved "https://registry.yarnpkg.com/cron-validate/-/cron-validate-1.4.5.tgz#eceb221f7558e6302e5f84c7b3a454fdf4d064c3"