diff --git a/backend/windmill-api/src/jobs.rs b/backend/windmill-api/src/jobs.rs index 692a5696eb..b26868d46a 100644 --- a/backend/windmill-api/src/jobs.rs +++ b/backend/windmill-api/src/jobs.rs @@ -1722,7 +1722,6 @@ fn list_completed_jobs_query( sqlb.and_where("result @> ?".bind(&result.replace("'", "''"))); } - tracing::info!("{:?}", sqlb.sql()); sqlb } #[derive(Deserialize, Clone)] diff --git a/frontend/src/lib/components/apps/components/helpers/InputValue.svelte b/frontend/src/lib/components/apps/components/helpers/InputValue.svelte index 6a515b3c08..429e627172 100644 --- a/frontend/src/lib/components/apps/components/helpers/InputValue.svelte +++ b/frontend/src/lib/components/apps/components/helpers/InputValue.svelte @@ -5,6 +5,7 @@ import type { AppInput, EvalAppInput, UploadAppInput } from '../../inputType' import type { AppViewerContext } from '../../types' import { accessPropertyByPath } from '../../utils' + import { computeGlobalContext, eval_like } from './eval' type T = string | number | boolean | Record | undefined @@ -24,13 +25,13 @@ } } - const { worldStore } = getContext('AppViewerContext') + const { worldStore, state } = getContext('AppViewerContext') - $: state = $worldStore?.state + $: stateId = $worldStore?.state let timeout: NodeJS.Timeout | undefined = undefined const debounce_ms = 50 - function debounce(cb: () => void) { + function debounce(cb: () => Promise) { if (timeout) { clearTimeout(timeout) } @@ -40,20 +41,22 @@ $: lastInput && $worldStore && debounce(handleConnection) $: lastInput && lastInput.type == 'template' && + $stateId && $state && - debounce(() => (value = getValue(lastInput))) + debounce(async () => (value = await getValue(lastInput))) $: lastInput && lastInput.type == 'eval' && - $state && - debounce(() => (value = evalExpr(lastInput))) + $stateId && + state && + debounce(async () => (value = await evalExpr(lastInput))) - function handleConnection() { + async function handleConnection() { if (lastInput.type === 'connected') { $worldStore?.connect(lastInput, onValueChange) } else if (lastInput.type === 'static' || lastInput.type == 'template') { - value = getValue(lastInput) + value = await getValue(lastInput) } else if (lastInput.type == 'eval') { - value = evalExpr(lastInput as EvalAppInput) + value = await evalExpr(lastInput as EvalAppInput) } else if (lastInput.type == 'upload') { value = (lastInput as UploadAppInput).value } else { @@ -61,9 +64,14 @@ } } - function evalExpr(input: EvalAppInput) { + async function evalExpr(input: EvalAppInput) { try { - const r = eval_like(input.expr, computeGlobalContext()) + const r = await eval_like( + input.expr, + computeGlobalContext($worldStore, id, extraContext), + true, + $state + ) error = '' return r } catch (e) { @@ -72,26 +80,15 @@ } } - function computeGlobalContext() { - return { - ...Object.fromEntries( - Object.entries($worldStore?.outputsById ?? {}) - .filter(([k, _]) => k != id) - .map(([key, value]) => { - return [ - key, - Object.fromEntries(Object.entries(value ?? {}).map((x) => [x[0], x[1].peak()])) - ] - }) - ), - ...extraContext - } - } - - export function getValue(input: AppInput) { + async function getValue(input: AppInput) { if (input.type === 'template' && isCodeInjection(input.eval)) { try { - const r = eval_like('`' + input.eval + '`', computeGlobalContext()) + const r = await eval_like( + '`' + input.eval + '`', + computeGlobalContext($worldStore, id, extraContext), + true, + $state + ) error = '' return r } catch (e) { @@ -104,31 +101,6 @@ } } - function create_context_function_template(eval_string, context) { - return ` - return function (context) { - "use strict"; - ${ - Object.keys(context).length > 0 - ? `let ${Object.keys(context).map((key) => ` ${key} = context['${key}']`)};` - : `` - } - return ${eval_string}; - } - ` - } - - function make_context_evaluator(eval_string, context) { - let template = create_context_function_template(eval_string, context) - let functor = Function(template) - return functor() - } - - function eval_like(text, context = {}) { - let evaluator = make_context_evaluator(text, context) - return evaluator(context) - } - function onValueChange(newValue: any): void { if (lastInput.type === 'connected' && newValue !== undefined && newValue !== null) { const { connection } = lastInput diff --git a/frontend/src/lib/components/apps/components/helpers/RunnableComponent.svelte b/frontend/src/lib/components/apps/components/helpers/RunnableComponent.svelte index 549fb038e8..f0b7a1eff7 100644 --- a/frontend/src/lib/components/apps/components/helpers/RunnableComponent.svelte +++ b/frontend/src/lib/components/apps/components/helpers/RunnableComponent.svelte @@ -10,9 +10,11 @@ import { Bug, Loader2 } from 'lucide-svelte' import { getContext, onMount } from 'svelte' import { fade } from 'svelte/transition' + import { initOutput } from '../../editor/appUtils' import type { AppInputs, Runnable } from '../../inputType' import type { Output } from '../../rx' import type { AppViewerContext } from '../../types' + import { computeGlobalContext, eval_like } from './eval' import InputValue from './InputValue.svelte' import RefreshButton from './RefreshButton.svelte' @@ -42,7 +44,8 @@ noBackend, errorByComponent, mode, - stateId + stateId, + state } = getContext('AppViewerContext') onMount(() => { @@ -96,14 +99,7 @@ let testJob: CompletedJob | undefined = undefined let testJobLoader: TestJobLoader | undefined = undefined - $: outputs = $worldStore?.outputsById[id] as { - result: Output> - loading: Output - } - - $: if (outputs?.loading != undefined) { - outputs.loading.set(false, true) - } + $: outputs = initOutput($worldStore, id, { result: undefined, loading: false }) $: outputs?.loading?.set(testIsLoading) $: schemaStripped = stripSchema(fields, $stateId) @@ -144,6 +140,23 @@ ) async function executeComponent(noToast = false) { + if (runnable?.type === 'runnableByName' && runnable.inlineScript?.language === 'frontend') { + outputs?.loading?.set(true) + try { + const r = await eval_like( + runnable.inlineScript?.content, + computeGlobalContext($worldStore, id, {}), + false, + $state + ) + setResult(r) + $state = $state + } catch (e) { + sendUserToast('Error running frontend script: ' + e.message) + } + outputs?.loading?.set(false) + return + } if (noBackend) { if (!noToast) { sendUserToast('This app is not connected to a windmill backend, it is a static preview') @@ -216,6 +229,26 @@ } } + function setResult(res: any) { + outputs.result?.set(res) + result = res + + const previousJobId = Object.keys($errorByComponent).find( + (key) => $errorByComponent[key].componentId === id + ) + + if (previousJobId && !result?.error) { + delete $errorByComponent[previousJobId] + $errorByComponent = $errorByComponent + } + if (gotoUrl && gotoUrl != '' && result?.error == undefined) { + if (gotoNewTab) { + window.open(gotoUrl, '_blank') + } else { + goto(gotoUrl) + } + } + } $: result?.error && recordError(result.error) @@ -232,24 +265,7 @@ const startedAt = new Date(testJob.started_at).getTime() if (startedAt > lastStartedAt) { lastStartedAt = startedAt - outputs.result?.set(testJob?.result) - result = testJob.result - - const previousJobId = Object.keys($errorByComponent).find( - (key) => $errorByComponent[key].componentId === id - ) - - if (previousJobId && !result?.error) { - delete $errorByComponent[previousJobId] - $errorByComponent = $errorByComponent - } - if (gotoUrl && gotoUrl != '' && result?.error == undefined) { - if (gotoNewTab) { - window.open(gotoUrl, '_blank') - } else { - goto(gotoUrl) - } - } + setResult(e.detail.result) } } }} diff --git a/frontend/src/lib/components/apps/components/helpers/eval.ts b/frontend/src/lib/components/apps/components/helpers/eval.ts new file mode 100644 index 0000000000..cdee3d35c9 --- /dev/null +++ b/frontend/src/lib/components/apps/components/helpers/eval.ts @@ -0,0 +1,53 @@ +import { goto } from '$app/navigation' +import type { World } from '../../rx' + +export function computeGlobalContext( + world: World | undefined, + id: string | undefined, + extraContext: any = {} +) { + return { + ...Object.fromEntries( + Object.entries(world?.outputsById ?? {}) + .filter(([k, _]) => k != id) + .map(([key, value]) => { + return [ + key, + Object.fromEntries(Object.entries(value ?? {}).map((x) => [x[0], x[1].peak()])) + ] + }) + ), + ...extraContext + } +} + +function create_context_function_template(eval_string, context, noReturn: boolean) { + return ` +return async function (context, state, goto) { +"use strict"; +${ + Object.keys(context).length > 0 + ? `let ${Object.keys(context).map((key) => ` ${key} = context['${key}']`)};` + : `` +} +${noReturn ? `return ${eval_string}` : eval_string} +} +` +} + +function make_context_evaluator( + eval_string, + context, + noReturn: boolean +): (context, state, goto) => Promise { + let template = create_context_function_template(eval_string, context, noReturn) + let functor = Function(template) + return functor() +} + +export async function eval_like(text, context = {}, noReturn: boolean = true, state: any = {}) { + let evaluator = make_context_evaluator(text, context, noReturn) + return await evaluator(context, state, async (x) => { + await goto(x) + }) +} diff --git a/frontend/src/lib/components/apps/editor/AppEditor.svelte b/frontend/src/lib/components/apps/editor/AppEditor.svelte index 172f5cc60e..675684201a 100644 --- a/frontend/src/lib/components/apps/editor/AppEditor.svelte +++ b/frontend/src/lib/components/apps/editor/AppEditor.svelte @@ -88,7 +88,8 @@ openDebugRun: writable(undefined), focusedGrid, stateId: writable(0), - parentWidth: writable(0) + parentWidth: writable(0), + state: writable({}) }) setContext('AppEditorContext', { diff --git a/frontend/src/lib/components/apps/editor/AppPreview.svelte b/frontend/src/lib/components/apps/editor/AppPreview.svelte index ccf06d287f..5c1f4019fd 100644 --- a/frontend/src/lib/components/apps/editor/AppPreview.svelte +++ b/frontend/src/lib/components/apps/editor/AppPreview.svelte @@ -62,7 +62,8 @@ openDebugRun: writable(undefined), focusedGrid: writable(undefined), stateId: writable(0), - parentWidth: writable(0) + parentWidth: writable(0), + state: writable({}) }) setContext('AppEditorContext', { diff --git a/frontend/src/lib/components/apps/editor/appUtils.ts b/frontend/src/lib/components/apps/editor/appUtils.ts index e3206064c5..81116fb748 100644 --- a/frontend/src/lib/components/apps/editor/appUtils.ts +++ b/frontend/src/lib/components/apps/editor/appUtils.ts @@ -47,7 +47,6 @@ export function getNextGridItemId(app: App): string { } export function createNewGridItem(grid: GridItem[], id: string, data: AppComponent): GridItem { - const newComponent = { resizable: true, draggable: true, @@ -105,7 +104,6 @@ export function insertNewGridItem( app.subgrids = {} } - // We only want to set subgrids when we are not moving if (!keepId) { for (let i = 0; i < (data.numberOfSubgrids ?? 0); i++) { @@ -113,8 +111,9 @@ export function insertNewGridItem( } } - - const key = focusedGrid ? `${focusedGrid?.parentComponentId}-${focusedGrid?.subGridIndex ?? 0}` : undefined + const key = focusedGrid + ? `${focusedGrid?.parentComponentId}-${focusedGrid?.subGridIndex ?? 0}` + : undefined let grid = focusedGrid ? app.subgrids[key!] : app.grid const newItem = createNewGridItem(grid, id, data) @@ -177,8 +176,6 @@ export function deleteGridItem( return components } - - type AvailableSpace = { left: number right: number @@ -286,11 +283,17 @@ function isOverlapping(item1: any, item2: any) { } type Outputtable = { - -readonly [Property in keyof Type]: Output; -}; + -readonly [Property in keyof Type]: Output +} - -export function initOutput>(world: World, id: string, init: I): Outputtable { +export function initOutput>( + world: World | undefined, + id: string, + init: I +): Outputtable { + if (!world) { + return {} as any + } const output = world.outputsById[id] as Outputtable if (init) { for (const key in init) { diff --git a/frontend/src/lib/components/apps/editor/contextPanel/ContextPanel.svelte b/frontend/src/lib/components/apps/editor/contextPanel/ContextPanel.svelte index 954e7f80e1..68cd12dadf 100644 --- a/frontend/src/lib/components/apps/editor/contextPanel/ContextPanel.svelte +++ b/frontend/src/lib/components/apps/editor/contextPanel/ContextPanel.svelte @@ -1,4 +1,5 @@ - +{#if inlineScript.language != 'frontend'} + +{/if}
@@ -109,20 +121,23 @@ Delete {/if} - - - Open full editor - + {#if inlineScript.language != 'frontend'} + + + Open full editor + + {/if} +
diff --git a/frontend/src/lib/components/apps/editor/inlineScriptsPanel/InlineScriptEditorDrawer.svelte b/frontend/src/lib/components/apps/editor/inlineScriptsPanel/InlineScriptEditorDrawer.svelte index 40ed3b7e06..a91780f7cd 100644 --- a/frontend/src/lib/components/apps/editor/inlineScriptsPanel/InlineScriptEditorDrawer.svelte +++ b/frontend/src/lib/components/apps/editor/inlineScriptsPanel/InlineScriptEditorDrawer.svelte @@ -2,6 +2,7 @@ import { Button, Drawer, DrawerContent } from '$lib/components/common' import type Editor from '$lib/components/Editor.svelte' import ScriptEditor from '$lib/components/ScriptEditor.svelte' + import type { Preview } from '$lib/gen' import { faSave } from '@fortawesome/free-solid-svg-icons' import type { InlineScript } from '../../types' @@ -24,7 +25,7 @@ editor?.setCode(inlineScript.content) }} > - {#if inlineScript} + {#if inlineScript && inlineScript.language != 'frontend'} [] if (isPointerUp) { - citems = JSON.parse(JSON.stringify(initItems)) + try { + citems = JSON.parse(JSON.stringify(initItems)) + } catch (e) { + citems = JSON.parse(JSON.stringify(items)) + } initItems = undefined } else { if (initItems == undefined) { diff --git a/frontend/src/lib/components/apps/svelte-grid/MoveResize.svelte b/frontend/src/lib/components/apps/svelte-grid/MoveResize.svelte index b516f6da71..5041dc0487 100644 --- a/frontend/src/lib/components/apps/svelte-grid/MoveResize.svelte +++ b/frontend/src/lib/components/apps/svelte-grid/MoveResize.svelte @@ -50,7 +50,7 @@ let anima const inActivate = () => { - if (shadowElement && shadow) { + if (shadowElement && shadow != undefined) { let subgrid = shadowElement.closest('.subgrid') let irect = shadowElement.getBoundingClientRect() let shadowBound diff --git a/frontend/src/lib/components/apps/types.ts b/frontend/src/lib/components/apps/types.ts index 87bf06b923..8d0931ec20 100644 --- a/frontend/src/lib/components/apps/types.ts +++ b/frontend/src/lib/components/apps/types.ts @@ -44,14 +44,14 @@ export interface BaseAppComponent extends Partial { configuration: Record< string, GeneralAppInput & - ( - | StaticAppInput - | ConnectedAppInput - | UserAppInput - | RowAppInput - | EvalAppInput - | UploadAppInput - ) + ( + | StaticAppInput + | ConnectedAppInput + | UserAppInput + | RowAppInput + | EvalAppInput + | UploadAppInput + ) > card: boolean | undefined customCss?: ComponentCustomCSS @@ -84,9 +84,9 @@ export type GridItem = FilledItem<{ export type InlineScript = { content: string - language: Preview.language - path: string - schema: Schema + language: Preview.language | 'frontend' + path?: string + schema?: Schema } export type App = { @@ -134,6 +134,7 @@ export type AppViewerContext = { focusedGrid: Writable stateId: Writable parentWidth: Writable + state: Writable> } export type AppEditorContext = { diff --git a/frontend/src/lib/components/common/languageIcons/JavaScript.svelte b/frontend/src/lib/components/common/languageIcons/JavaScript.svelte new file mode 100644 index 0000000000..d8552b77c8 --- /dev/null +++ b/frontend/src/lib/components/common/languageIcons/JavaScript.svelte @@ -0,0 +1,11 @@ + + + + + + diff --git a/frontend/src/lib/components/common/languageIcons/LanguageIcon.svelte b/frontend/src/lib/components/common/languageIcons/LanguageIcon.svelte index 3880cc092f..10f5ed149b 100644 --- a/frontend/src/lib/components/common/languageIcons/LanguageIcon.svelte +++ b/frontend/src/lib/components/common/languageIcons/LanguageIcon.svelte @@ -4,18 +4,23 @@ import PostgresIcon from '$lib/components/icons/PostgresIcon.svelte' import type { SvelteComponent } from 'svelte' import { BashIcon, GoIcon, PythonIcon, TypeScriptIcon } from './' + import JavaScript from './JavaScript.svelte' - export let lang: SupportedLanguage | 'pgsql' | 'mysql' + export let lang: SupportedLanguage | 'pgsql' | 'mysql' | 'javascript' export let width = 30 export let height = 30 export let scale = 1 - const langToComponent: Record = { + const langToComponent: Record< + SupportedLanguage | 'pgsql' | 'mysql' | 'javascript', + typeof SvelteComponent + > = { go: GoIcon, python3: PythonIcon, deno: TypeScriptIcon, bash: BashIcon, pgsql: PostgresIcon, - mysql: MySQLIcon + mysql: MySQLIcon, + javascript: JavaScript } diff --git a/frontend/src/lib/components/flows/pickers/FlowScriptPicker.svelte b/frontend/src/lib/components/flows/pickers/FlowScriptPicker.svelte index 1b20ebf4fb..ff4f33e45e 100644 --- a/frontend/src/lib/components/flows/pickers/FlowScriptPicker.svelte +++ b/frontend/src/lib/components/flows/pickers/FlowScriptPicker.svelte @@ -6,7 +6,7 @@ export let disabled: boolean = false export let label: string - export let lang: SupportedLanguage | 'pgsql' | 'mysql' | undefined = undefined + export let lang: SupportedLanguage | 'pgsql' | 'mysql' | 'javascript' | undefined = undefined export let icon: IconDefinition | undefined = undefined export let iconColor: string | undefined = undefined