fix(frontend): Display transformer errors (#1971)

* fix(frontend): Display transformer errors

* feat(frontend): Display both script and transformer results

* feat(frontend): Merge main + remove debug code

* feat(frontend): refactor runnableComponent

* feat(frontend): fix empty object

* feat(frontend): improve error popover
This commit is contained in:
Faton Ramadani
2023-08-01 11:59:26 +02:00
committed by GitHub
parent 87326b7d16
commit d67cfa4aa9
5 changed files with 135 additions and 68 deletions
@@ -217,7 +217,9 @@
href="data:application/octet-stream;base64,{result.file}">Download</a
>
</div>
{:else if !forceJson && resultKind == 'error'}<div class="flex flex-col items-start">
{:else if !forceJson && resultKind == 'error' && result?.error}<div
class="flex flex-col items-start"
>
<span class="text-red-500 font-semibold text-sm whitespace-pre-wrap"
>{#if result.error.name || result.error.message}{result.error.name}: {result.error
.message}{:else}{JSON.stringify(result.error, null, 4)}{/if}</span
@@ -151,7 +151,14 @@
if (currentId === id) {
job = { ...maybe_job, id }
await tick()
dispatch('done', job)
if ('error' in job ?? {}) {
dispatch('doneError', {
id,
error: job.result.error
})
} else {
dispatch('done', job)
}
currentId = undefined
}
}
@@ -186,26 +186,14 @@
$worldStore,
$runnableComponents
)
await setResult(r, undefined)
$state = $state
const job = generateNextFrontendJobId()
const njobs = [{ job, component: id, result: r }, ...$jobs]
$jobs = $jobs?.length > 100 ? njobs.slice(0, 100) : njobs
await setResult(r, job)
$state = $state
} catch (e) {
sendUserToast(`Error running frontend script ${id}: ` + e.message, true)
// Manually add a fake job to the job list to show the error
const job = generateNextFrontendJobId()
const error = e.body ?? e.message
$errorByComponent[job] = {
error,
componentId: id
}
$jobs = [{ job, component: id, error }, ...$jobs]
await setResult({ error: { message: e.body ?? e.message } }, job)
}
loading = false
donePromise?.()
@@ -227,7 +215,7 @@
}
try {
let njob = await resultJobLoader?.abstractRun(async () => {
await resultJobLoader?.abstractRun(async () => {
const nonStaticRunnableInputs = {}
const staticRunnableInputs = {}
for (const k of Object.keys(fields ?? {})) {
@@ -272,12 +260,8 @@
requestBody
})
})
if (njob) {
const njobs = [{ job: njob, component: id }, ...$jobs]
$jobs = $jobs?.length > 100 ? njobs.slice(0, 100) : njobs
}
} catch (e) {
setResult({ error: e.body ?? e.message }, undefined)
updateResult({ error: e.body ?? e.message })
loading = false
}
}
@@ -290,7 +274,7 @@
executeComponent()
}
} catch (e) {
setResult({ error: e.body ?? e.message }, undefined)
updateResult({ error: e.body ?? e.message })
}
}
@@ -305,14 +289,50 @@
outputs.jobId?.set(jobId)
}
async function setResult(res: any, jobId: string | undefined) {
dispatch('done')
const hasRes = res !== undefined && res !== null
function recordJob(
jobId: string,
result?: string,
error?: string,
transformer?: { result?: string; error?: string }
) {
const job = {
...(result ? { result } : {}),
...(error ? { error } : {}),
...(transformer ? { transformer } : {}),
job: jobId,
component: id
}
if (error) {
recordError(error, jobId)
} else if (job?.transformer?.error) {
recordError(job.transformer.error, jobId)
}
const njobs = [job, ...$jobs]
// Only keep the last 100 jobs
$jobs = $jobs?.length > 100 ? njobs.slice(0, 100) : njobs
}
function getResultErrors(result: any | any[]): string | undefined {
const errorAsArray = Array.isArray(result) ? result.flat() : [result]
const hasErrors = errorAsArray.some((r) => r?.error)
if (!hasErrors) {
return undefined
}
return errorAsArray
.map((r) => r?.error?.message)
.filter(Boolean)
.join('\n')
}
async function runTransformer(res) {
if (transformer) {
try {
let raw = $worldStore.newOutput(id, 'raw', res)
res = await eval_like(
const transformerResult = await eval_like(
transformer.content,
computeGlobalContext($worldStore, {
iter: iterContext ? $iterContext : undefined,
@@ -326,9 +346,10 @@
$worldStore,
$runnableComponents
)
raw.set(res)
raw.set(transformerResult)
return transformerResult
} catch (err) {
res = {
return {
error: {
name: 'TransformerError',
message: 'An error occured in the transformer',
@@ -336,52 +357,51 @@
}
}
}
if (hasRes && res === undefined) {
res = {
error: {
name: 'TransformerError',
message: 'An error occured in the transformer',
stack: 'Transformer returned undefined'
}
}
}
}
}
// console.log('setr', id)
function updateResult(res) {
outputs.result?.set(res)
result = res
}
// Flows with loops can have multiple results
const errorAsArray = Array.isArray(result) ? result.flat() : [result]
async function setResult(res: any, jobId: string | undefined) {
dispatch('done')
const hasRes = res !== undefined && res !== null
// As soon as we have an error, we consider the component errored
const hasErrors = errorAsArray.some((r) => r?.error)
if (hasErrors) {
const errorMessages = errorAsArray
.map((r) => r?.error?.message)
.filter(Boolean)
.join('\n')
jobId && recordError(errorMessages, jobId)
dispatch('handleError', errorMessages)
} else {
dispatch('success')
if (!jobId && !hasRes) {
return
}
const previousJobId = Object.keys($errorByComponent).find(
(key) => $errorByComponent[key].componentId === id
)
const errors = getResultErrors(res)
if (previousJobId && !hasErrors) {
delete $errorByComponent[previousJobId]
$errorByComponent = $errorByComponent
console.log('errors', errors)
if (errors) {
const transformerResult = transformer
? { error: 'Transformer could not be run because of previous errors' }
: undefined
recordJob(jobId!, undefined, errors, transformerResult)
updateResult(res)
dispatch('handleError', errors)
return
}
const transformerResult = await runTransformer(res)
if (transformerResult?.error) {
recordJob(jobId!, res, undefined, transformerResult)
updateResult(transformerResult)
dispatch('handleError', transformerResult.error)
return
}
updateResult(transformerResult ?? res)
recordJob(jobId!, result, undefined, transformerResult)
$errorByComponent = clearErrorByComponentId(id, $errorByComponent)
dispatch('success')
donePromise?.()
}
@@ -477,7 +497,7 @@
loading = false
}}
on:doneError={(e) => {
setResult({ error: e.detail }, e.detail.id)
setResult({ error: e.detail.error }, e.detail.id)
loading = false
}}
bind:this={resultJobLoader}
@@ -514,10 +534,17 @@
<Alert type="error" title="Error during execution">
<div class="flex flex-col gap-2">
An error occured, please contact the app author.
{#if lastJobId && $errorByComponent[lastJobId]?.error}
<div class="font-bold">{$errorByComponent[lastJobId]?.error}</div>
{/if}
<a
href={`/run/${lastJobId}?workspace=${workspace}`}
class="font-semibold text-red-800 underline">Job id: {lastJobId}</a
class="font-semibold text-red-800 underline"
target="_blank"
>
Job id: {lastJobId}
</a>
</div>
</Alert>
</div>
@@ -632,6 +632,7 @@
<Skeleton layout={[[40]]} />
{/if}
{:else}
{@const jobResult = $jobs.find((j) => j.job == selectedJobId)}
<div class="flex flex-col h-full w-full gap-4 p-2 mb-4">
{#if job?.['running']}
<div class="flex flex-row-reverse w-full">
@@ -673,6 +674,28 @@
</div>
{/if}
</Pane>
{#if jobResult?.transformer}
<Pane size={50} minSize={10} class="text-sm text-secondary p-2">
<div class="font-bold mb-4">Transformer results</div>
{#if job != undefined && 'result' in job && job.result != undefined}
<pre class="overflow-x-auto break-words relative h-full px-2">
<DisplayResult
workspaceId={$workspaceStore}
jobId={selectedJobId}
result={jobResult?.transformer}
/>
</pre>
{:else if testIsLoading}
<div class="p-2"><Loader2 class="animate-spin" /> </div>
{:else if job != undefined && 'result' in job && job?.['result'] == undefined}
<div class="p-2 text-tertiary">Result is undefined</div>
{:else}
<div class="p-2 text-tertiary">
<Loader2 size={14} class="animate-spin mr-2" />
</div>
{/if}
</Pane>
{/if}
</Splitpanes>
{:else}
<div class="mt-10" />
+9 -1
View File
@@ -186,7 +186,15 @@ export type AppViewerContext = {
workspace: string
onchange: (() => void) | undefined
isEditor: boolean
jobs: Writable<{ job: string; component: string; result?: string; error?: string }[]>
jobs: Writable<
{
job: string
component: string
result?: string
error?: string
transformer?: { result?: string; error?: string }
}[]
>
noBackend: boolean
errorByComponent: Writable<Record<string, { error: string; componentId: string }>>
openDebugRun: Writable<((componentID: string) => void) | undefined>