mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-19 00:02:03 +00:00
feat(frontend): global recompute helper function (#5408)
* feat(frontend): global recompute helper function * fixes * remove unused import * filteredIds -> excludeIds * get(recomputeAllContext)? * deal with circulare recomputes * consistent naming
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, getContext, onDestroy, tick } from 'svelte'
|
||||
import { get } from 'svelte/store'
|
||||
import type {
|
||||
AppInput,
|
||||
EvalAppInput,
|
||||
@@ -32,7 +33,7 @@
|
||||
export let onDemandOnly: boolean = false
|
||||
export let exportValueFunction: boolean = false
|
||||
|
||||
const { componentControl, runnableComponents } = getContext<AppViewerContext>('AppViewerContext')
|
||||
const { componentControl, runnableComponents, recomputeAllContext } = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
const editorContext = getContext<AppEditorContext>('AppEditorContext')
|
||||
const iterContext = getContext<ListContext>('ListWrapperContext')
|
||||
@@ -264,6 +265,7 @@
|
||||
try {
|
||||
const context = computeGlobalContext(
|
||||
$worldStore,
|
||||
id,
|
||||
deepMergeWithPriority(fullContext, args ?? {})
|
||||
)
|
||||
const r = await eval_like(
|
||||
@@ -275,7 +277,8 @@
|
||||
$worldStore,
|
||||
$runnableComponents,
|
||||
false,
|
||||
groupContext?.id
|
||||
groupContext?.id,
|
||||
get(recomputeAllContext)?.onRefresh
|
||||
)
|
||||
error = ''
|
||||
return r
|
||||
@@ -294,14 +297,15 @@
|
||||
try {
|
||||
const r = await eval_like(
|
||||
'`' + input.eval.replaceAll('`', '\\`') + '`',
|
||||
computeGlobalContext($worldStore, fullContext),
|
||||
computeGlobalContext($worldStore, id, fullContext),
|
||||
$state,
|
||||
$mode == 'dnd',
|
||||
$componentControl,
|
||||
$worldStore,
|
||||
$runnableComponents,
|
||||
false,
|
||||
groupContext?.id
|
||||
groupContext?.id,
|
||||
get(recomputeAllContext)?.onRefresh
|
||||
)
|
||||
error = ''
|
||||
return r
|
||||
|
||||
@@ -78,7 +78,8 @@
|
||||
selectedComponent,
|
||||
app,
|
||||
connectingInput,
|
||||
bgRuns
|
||||
bgRuns,
|
||||
recomputeAllContext
|
||||
} = getContext<AppViewerContext>('AppViewerContext')
|
||||
const editorContext = getContext<AppEditorContext>('AppEditorContext')
|
||||
|
||||
@@ -286,7 +287,7 @@
|
||||
try {
|
||||
r = await eval_like(
|
||||
runnable.inlineScript?.content,
|
||||
computeGlobalContext($worldStore, {
|
||||
computeGlobalContext($worldStore, id, {
|
||||
iter: iterContext ? $iterContext : undefined,
|
||||
row: rowContext ? $rowContext : undefined,
|
||||
group: groupContext ? get(groupContext.context) : undefined
|
||||
@@ -297,14 +298,23 @@
|
||||
$worldStore,
|
||||
$runnableComponents,
|
||||
true,
|
||||
groupContext?.id
|
||||
groupContext?.id,
|
||||
get(recomputeAllContext)?.onRefresh
|
||||
)
|
||||
|
||||
await setResult(r, job)
|
||||
$state = $state
|
||||
} catch (e) {
|
||||
sendUserToast(`Error running frontend script ${id}: ` + e.message, true)
|
||||
r = { error: { message: e.body ?? e.message } }
|
||||
let additionalInfo = ''
|
||||
if (
|
||||
e.message.includes('Maximum call stack size exceeded') ||
|
||||
e.message.includes('too much recursion')
|
||||
) {
|
||||
additionalInfo =
|
||||
'This is likely due to a call to globalRecompute() in the frontend script. Please check your script for circular recomputes and disable the "Run on start and app refresh" toggle.'
|
||||
}
|
||||
sendUserToast(`Error running frontend script ${id}: ` + e.message + additionalInfo, true)
|
||||
r = { error: { message: (e.body ?? e.message) + additionalInfo } }
|
||||
await setResult(r, job)
|
||||
}
|
||||
loading = false
|
||||
@@ -519,7 +529,7 @@
|
||||
raw.set(res)
|
||||
const transformerResult = await eval_like(
|
||||
transformer.content,
|
||||
computeGlobalContext($worldStore, {
|
||||
computeGlobalContext($worldStore, id, {
|
||||
iter: iterContext ? $iterContext : undefined,
|
||||
row: rowContext ? $rowContext : undefined,
|
||||
group: groupContext ? get(groupContext.context) : undefined,
|
||||
@@ -531,7 +541,8 @@
|
||||
$worldStore,
|
||||
$runnableComponents,
|
||||
true,
|
||||
groupContext?.id
|
||||
groupContext?.id,
|
||||
get(recomputeAllContext)?.onRefresh
|
||||
)
|
||||
return transformerResult
|
||||
} catch (err) {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { sendUserToast } from '$lib/toast'
|
||||
import { waitJob } from '$lib/components/waitJob'
|
||||
import { base } from '$lib/base'
|
||||
|
||||
export function computeGlobalContext(world: World | undefined, extraContext: any = {}) {
|
||||
export function computeGlobalContext(world: World | undefined, id: string | undefined, extraContext: any = {}) {
|
||||
return {
|
||||
...Object.fromEntries(
|
||||
Object.entries(world?.outputsById ?? {})
|
||||
@@ -15,7 +15,8 @@ export function computeGlobalContext(world: World | undefined, extraContext: any
|
||||
]
|
||||
})
|
||||
),
|
||||
...extraContext
|
||||
...extraContext,
|
||||
id
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +27,7 @@ function create_context_function_template(
|
||||
) {
|
||||
let hasReturnAsLastLine = noReturn || eval_string.split('\n').some((x) => x.startsWith('return '))
|
||||
return `
|
||||
return async function (context, state, createProxy, goto, setTab, recompute, getAgGrid, setValue, setSelectedIndex, openModal, closeModal, open, close, validate, invalidate, validateAll, clearFiles, showToast, waitJob, askNewResource, downloadFile) {
|
||||
return async function (context, state, createProxy, goto, setTab, recompute, globalRecompute, getAgGrid, setValue, setSelectedIndex, openModal, closeModal, open, close, validate, invalidate, validateAll, clearFiles, showToast, waitJob, askNewResource, downloadFile) {
|
||||
"use strict";
|
||||
${contextKeys && contextKeys.length > 0
|
||||
? `let ${contextKeys.map((key) => ` ${key} = createProxy('${key}', context['${key}'])`)};`
|
||||
@@ -49,6 +50,7 @@ type WmFunctor = (
|
||||
goto,
|
||||
setTab,
|
||||
recompute,
|
||||
globalRecompute,
|
||||
getAgGrid,
|
||||
setValue,
|
||||
setSelectedIndex,
|
||||
@@ -120,7 +122,8 @@ export async function eval_like(
|
||||
worldStore: World | undefined,
|
||||
runnableComponents: Record<string, { cb?: (() => void)[] }>,
|
||||
noReturn: boolean,
|
||||
groupContextId: string | undefined
|
||||
groupContextId: string | undefined,
|
||||
globalRecomputeFunction: ((excludeIds?: string) => void) | undefined
|
||||
) {
|
||||
const createProxy = (name: string, obj: any) => {
|
||||
// console.log('Creating proxy', name, obj)
|
||||
@@ -198,6 +201,12 @@ export async function eval_like(
|
||||
(id) => {
|
||||
runnableComponents[id]?.cb?.forEach((f) => f())
|
||||
},
|
||||
() => {
|
||||
const callerId = ((context ?? {}) as any).id
|
||||
if (callerId) {
|
||||
globalRecomputeFunction?.(callerId)
|
||||
}
|
||||
},
|
||||
(id) => {
|
||||
return controlComponents[id]?.agGrid
|
||||
},
|
||||
@@ -268,9 +277,10 @@ export async function eval_like(
|
||||
}
|
||||
|
||||
if (typeof input === 'object' && input.s3) {
|
||||
const workspaceId = computeGlobalContext(worldStore).ctx.workspace
|
||||
const s3href = `${base}/api/w/${workspaceId}/job_helpers/download_s3_file?file_key=${input?.s3
|
||||
}${input?.storage ? `&storage=${input.storage}` : ''}`
|
||||
const workspaceId = ((context ?? {}) as any).ctx?.workspace
|
||||
const s3href = `${base}/api/w/${workspaceId}/job_helpers/download_s3_file?file_key=${
|
||||
input?.s3
|
||||
}${input?.storage ? `&storage=${input.storage}` : ''}`
|
||||
downloadFile(s3href, filename || input.s3)
|
||||
} else if (typeof input === 'string') {
|
||||
if (input.startsWith('data:')) {
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
}
|
||||
})
|
||||
|
||||
function onClick(stopAfterClear: boolean, source: string) {
|
||||
function onRefresh(stopAfterClear: boolean, source: string, excludeId?: string) {
|
||||
if (timeout) {
|
||||
clearInterval(timeout)
|
||||
timeout = undefined
|
||||
@@ -81,7 +81,7 @@
|
||||
if (stopAfterClear) return
|
||||
}
|
||||
if (firstLoad) {
|
||||
refresh('onClick ' + source)
|
||||
refresh('onClick ' + source, excludeId)
|
||||
}
|
||||
|
||||
if ($recomputeAllContext.interval) {
|
||||
@@ -109,11 +109,11 @@
|
||||
|
||||
function setInter(inter: number | undefined, source: string) {
|
||||
$recomputeAllContext.interval = inter
|
||||
onClick(!inter, 'setInter ' + source)
|
||||
onRefresh(!inter, 'setInter ' + source)
|
||||
}
|
||||
|
||||
let refreshing: string[] = []
|
||||
function refresh(reason: string) {
|
||||
function refresh(reason: string, excludeId: string | undefined = undefined) {
|
||||
let isFirstLoad = false
|
||||
if (!firstLoad && reason == 'all initialized') {
|
||||
console.log('refresh all first load', reason)
|
||||
@@ -129,8 +129,9 @@
|
||||
const promises = Object.keys($runnableComponents)
|
||||
.flatMap((id) => {
|
||||
if (
|
||||
!$runnableComponents?.[id]?.autoRefresh &&
|
||||
(!isFirstLoad || !$runnableComponents?.[id]?.refreshOnStart)
|
||||
excludeId === id ||
|
||||
(!$runnableComponents?.[id]?.autoRefresh &&
|
||||
(!isFirstLoad || !$runnableComponents?.[id]?.refreshOnStart))
|
||||
) {
|
||||
return
|
||||
}
|
||||
@@ -170,21 +171,21 @@
|
||||
if (progressTimer) clearInterval(progressTimer)
|
||||
}
|
||||
} else if (shouldRefresh) {
|
||||
timeout = setInterval(() => refresh('onClick interval'), $recomputeAllContext.interval)
|
||||
timeout = setInterval(() => refresh('onRefresh interval'), $recomputeAllContext.interval)
|
||||
startProgress()
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
$recomputeAllContext = {
|
||||
onClick: () => onClick(false, 'allContext'),
|
||||
onRefresh: (excludeIds) => onRefresh(false, 'allContext', excludeIds),
|
||||
setInter: (n) => setInter(n, 'all context')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<RecomputeAllButton
|
||||
on:click={() => onClick(false, 'button')}
|
||||
on:click={() => onRefresh(false, 'button')}
|
||||
interval={$recomputeAllContext.interval}
|
||||
{refreshing}
|
||||
componentNumber={$recomputeAllContext.componentNumber ?? 0}
|
||||
@@ -192,6 +193,6 @@
|
||||
progress={$recomputeAllContext.progress}
|
||||
on:setInter={(e) => {
|
||||
setInter(e.detail, 'button setInter')
|
||||
onClick(false, 'button setInter')
|
||||
onRefresh(false, 'button setInter')
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
<RecomputeAllButton
|
||||
interval={$recomputeAllContext.interval}
|
||||
componentNumber={$recomputeAllContext.componentNumber ?? 0}
|
||||
on:click={() => $recomputeAllContext.onClick?.()}
|
||||
on:click={() => $recomputeAllContext.onRefresh?.()}
|
||||
on:setInter={(e) => {
|
||||
$recomputeAllContext.setInter?.(e.detail)
|
||||
}}
|
||||
|
||||
@@ -284,7 +284,7 @@ export type AppViewerContext = {
|
||||
policy: Policy
|
||||
|
||||
recomputeAllContext: Writable<{
|
||||
onClick?: () => void
|
||||
onRefresh?: (excludeId?: string) => void
|
||||
componentNumber?: number | undefined
|
||||
interval?: number | undefined
|
||||
refreshing?: string[] | undefined
|
||||
|
||||
@@ -231,6 +231,12 @@ declare function setTab(id: string, index: number): void;
|
||||
*/
|
||||
declare function recompute(id: string): void;
|
||||
|
||||
/** recompute all components runnables and background runnables
|
||||
* Make sure to disable the toggle "Run on start and app refresh" in the
|
||||
* settings panel to avoid circular recomputes
|
||||
*/
|
||||
declare function globalRecompute(): void;
|
||||
|
||||
/** get the ag grid api from an AgGridTable
|
||||
* @param id component's id
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user