fix(frontend): only download result for apps

This commit is contained in:
Ruben Fiszel
2023-05-11 22:13:19 +02:00
parent 8e785d8ba6
commit 6bbd9374cb
4 changed files with 293 additions and 45 deletions
+24
View File
@@ -3675,6 +3675,30 @@ paths:
application/json:
schema: {}
/w/{workspace}/jobs/completed/get_result_maybe/{id}:
get:
summary: get completed job result if job is completed
operationId: getCompletedJobResultMaybe
tags:
- job
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/JobId"
responses:
"200":
description: result
content:
application/json:
schema:
type: object
properties:
completed:
type: boolean
result: {}
required:
- completed
- result
/w/{workspace}/jobs/completed/delete/{id}:
post:
summary: delete completed job (erase content but keep run id)
+29
View File
@@ -74,6 +74,10 @@ pub fn workspaced_service() -> Router {
.route("/completed/list", get(list_completed_jobs))
.route("/completed/get/:id", get(get_completed_job))
.route("/completed/get_result/:id", get(get_completed_job_result))
.route(
"/completed/get_result_maybe/:id",
get(get_completed_job_result_maybe),
)
.route("/completed/delete/:id", post(delete_completed_job))
.route("/flow/resume/:id", post(resume_suspended_flow_as_owner))
.route(
@@ -2073,6 +2077,31 @@ async fn get_completed_job_result(
Ok(Json(result))
}
#[derive(Serialize)]
struct CompletedJobResult {
completed: bool,
result: Option<serde_json::Value>,
}
async fn get_completed_job_result_maybe(
Extension(db): Extension<DB>,
Path((w_id, id)): Path<(String, Uuid)>,
) -> error::JsonResult<CompletedJobResult> {
let result_o = sqlx::query_scalar!(
"SELECT result FROM completed_job WHERE id = $1 AND workspace_id = $2",
id,
w_id,
)
.fetch_optional(&db)
.await?;
if let Some(result) = result_o {
Ok(Json(CompletedJobResult { completed: true, result }))
} else {
Ok(Json(CompletedJobResult { completed: false, result: None }))
}
}
async fn delete_completed_job(
authed: Authed,
Extension(user_db): Extension<UserDB>,
@@ -0,0 +1,190 @@
<script lang="ts">
import { JobService } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { onDestroy } from 'svelte'
import type { Preview } from '$lib/gen/models/Preview'
import { createEventDispatcher } from 'svelte'
export let isLoading = false
export let job: { completed: boolean; result: any; id: string } | undefined = undefined
export let workspaceOverride: string | undefined = undefined
export let notfound = false
const dispatch = createEventDispatcher()
$: workspace = workspaceOverride ?? $workspaceStore
let syncIteration: number = 0
let errorIteration = 0
let ITERATIONS_BEFORE_SLOW_REFRESH = 10
let ITERATIONS_BEFORE_SUPER_SLOW_REFRESH = 100
let lastStartedAt: number = Date.now()
let currentId: string | undefined = undefined
$: isLoading = currentId !== undefined
export async function abstractRun(fn: () => Promise<string>) {
try {
isLoading = true
clearCurrentJob()
const startedAt = Date.now()
const testId = await fn()
if (lastStartedAt < startedAt) {
lastStartedAt = startedAt
if (testId) {
try {
await watchJob(testId)
} catch {
if (currentId === testId) {
currentId = undefined
}
}
}
}
return testId
} catch (err) {
// if error happens on submitting the job, reset UI state so the user can try again
isLoading = false
currentId = undefined
job = undefined
throw err
}
}
export async function runScriptByPath(
path: string | undefined,
args: Record<string, any>
): Promise<string> {
return abstractRun(() =>
JobService.runScriptByPath({
workspace: $workspaceStore!,
path: path ?? '',
requestBody: args
})
)
}
export async function runFlowByPath(
path: string | undefined,
args: Record<string, any>
): Promise<string> {
return abstractRun(() =>
JobService.runFlowByPath({
workspace: $workspaceStore!,
path: path ?? '',
requestBody: args
})
)
}
export async function runPreview(
path: string | undefined,
code: string,
lang: 'deno' | 'go' | 'python3' | 'bash',
args: Record<string, any>,
tag: string | undefined
): Promise<string> {
return abstractRun(() =>
JobService.runScriptPreview({
workspace: $workspaceStore!,
requestBody: {
path,
content: code,
args,
language: lang as Preview.language,
tag
}
})
)
}
export async function cancelJob() {
const id = currentId
if (id) {
currentId = undefined
try {
await JobService.cancelQueuedJob({
workspace: $workspaceStore ?? '',
id,
requestBody: {}
})
} catch (err) {
console.error(err)
}
}
}
export async function clearCurrentJob() {
if (currentId) {
job = undefined
await cancelJob()
}
}
export async function watchJob(testId: string) {
syncIteration = 0
errorIteration = 0
currentId = testId
job = undefined
const isCompleted = await loadTestJob(testId)
if (!isCompleted) {
setTimeout(() => {
syncer(testId)
}, 50)
}
}
async function loadTestJob(id: string): Promise<boolean> {
let isCompleted = false
if (currentId === id) {
try {
let maybe_job = await JobService.getCompletedJobResultMaybe({
workspace: workspace ?? '',
id
})
if (maybe_job.completed) {
isCompleted = true
if (currentId === id) {
job = { ...maybe_job, id }
dispatch('done', job)
currentId = undefined
}
}
notfound = false
} catch (err) {
errorIteration += 1
if (errorIteration == 5) {
notfound = true
await clearCurrentJob()
dispatch('doneError', err)
}
console.warn(err)
}
return isCompleted
} else {
return true
}
}
async function syncer(id: string): Promise<void> {
if (currentId != id) {
return
}
syncIteration++
await loadTestJob(id)
let nextIteration = 50
if (syncIteration > ITERATIONS_BEFORE_SLOW_REFRESH) {
nextIteration = 500
} else if (syncIteration > ITERATIONS_BEFORE_SUPER_SLOW_REFRESH) {
nextIteration = 2000
}
setTimeout(() => syncer(id), nextIteration)
}
onDestroy(async () => {
currentId = undefined
})
</script>
@@ -3,8 +3,7 @@
import Alert from '$lib/components/common/alert/Alert.svelte'
import LightweightSchemaForm from '$lib/components/LightweightSchemaForm.svelte'
import Popover from '$lib/components/Popover.svelte'
import TestJobLoader from '$lib/components/TestJobLoader.svelte'
import { AppService, type CompletedJob } from '$lib/gen'
import { AppService } from '$lib/gen'
import { classNames, defaultIfEmptyString, emptySchema, sendUserToast } from '$lib/utils'
import { deepEqual } from 'fast-equals'
import { Bug } from 'lucide-svelte'
@@ -16,6 +15,7 @@
import InputValue from './InputValue.svelte'
import RefreshButton from './RefreshButton.svelte'
import { clearErrorByComponentId, selectId } from '../../editor/appUtils'
import ResultJobLoader from '$lib/components/ResultJobLoader.svelte'
// Component props
export let id: string
@@ -65,7 +65,6 @@
$runnableComponents = $runnableComponents
let args: Record<string, any> | undefined = undefined
let testIsLoading = false
let runnableInputValues: Record<string, any> = {}
let executeTimeout: NodeJS.Timeout | undefined = undefined
@@ -96,7 +95,7 @@
}
$: (runnableInputValues || extraQueryParams || args) &&
testJobLoader &&
resultJobLoader &&
refreshIfAutoRefresh('arg changed')
$: refreshOn =
@@ -113,8 +112,7 @@
}
// Test job internal state
let testJob: CompletedJob | undefined = undefined
let testJobLoader: TestJobLoader | undefined = undefined
let resultJobLoader: ResultJobLoader | undefined = undefined
let schemaStripped: Schema | undefined =
autoRefresh || forceSchemaDisplay ? emptySchema() : undefined
@@ -171,7 +169,7 @@
$worldStore,
$runnableComponents
)
await setResult(r)
await setResult(r, undefined)
$state = $state
} catch (e) {
@@ -190,18 +188,20 @@
$jobs = [{ job, component: id, error }, ...$jobs]
}
loading = false
donePromise?.()
return
} else if (noBackend) {
if (!noToast) {
sendUserToast('This app is not connected to a windmill backend, it is a static preview')
}
donePromise?.()
return
}
if (runnable?.type === 'runnableByName' && !runnable.inlineScript) {
return
}
if (!testJobLoader) {
if (!resultJobLoader) {
console.warn('No test job loader')
return
}
@@ -209,7 +209,7 @@
loading = true
try {
let njob = await testJobLoader?.abstractRun(() => {
let njob = await resultJobLoader?.abstractRun(() => {
const nonStaticRunnableInputs = {}
const staticRunnableInputs = {}
Object.keys(fields ?? {}).forEach((k) => {
@@ -255,7 +255,7 @@
$jobs = [{ job: njob, component: id }, ...$jobs]
}
} catch (e) {
setResult({ error: e.body ?? e.message })
setResult({ error: e.body ?? e.message }, undefined)
loading = false
}
}
@@ -264,36 +264,42 @@
try {
await executeComponent()
} catch (e) {
setResult({ error: e.body ?? e.message })
setResult({ error: e.body ?? e.message }, undefined)
}
}
let lastStartedAt: number = -1
function recordError(error: string) {
if (testJob) {
$errorByComponent[testJob.id] = {
error: error,
componentId: id
}
function recordError(error: string, jobId: string) {
$errorByComponent[jobId] = {
error: error,
componentId: id
}
}
async function setResult(res: any) {
async function setResult(res: any, jobId: string | undefined) {
const hasRes = res !== undefined && res !== null
if (transformer) {
$worldStore.newOutput(id, 'raw', res)
res = await eval_like(
transformer.content,
computeGlobalContext($worldStore, { result: res }),
false,
$state,
$mode == 'dnd',
$componentControl,
$worldStore,
$runnableComponents
)
try {
$worldStore.newOutput(id, 'raw', res)
res = await eval_like(
transformer.content,
computeGlobalContext($worldStore, { result: res }),
false,
$state,
$mode == 'dnd',
$componentControl,
$worldStore,
$runnableComponents
)
} catch (err) {
res = {
error: {
name: 'TransformerError',
message: 'An error occured in the transformer',
stack: err.message
}
}
}
if (hasRes && res === undefined) {
res = {
@@ -312,7 +318,7 @@
result = res
if (res?.error) {
recordError(res.error)
jobId && recordError(res.error, jobId)
dispatch('handleError', res.error.message)
} else {
dispatch('success')
@@ -346,7 +352,7 @@
executeComponent(true, inlineScript).catch(reject)
})
p.cancel = () => {
testJobLoader?.cancelJob()
resultJobLoader?.cancelJob()
loading = false
rejectCb(new Error('Canceled'))
}
@@ -371,6 +377,8 @@
delete $runnableComponents[id]
$runnableComponents = $runnableComponents
})
let lastJobId: string | undefined = undefined
</script>
{#each Object.entries(fields ?? {}) as [key, v] (key)}
@@ -396,21 +404,18 @@
{/each}
{/if}
<TestJobLoader
<ResultJobLoader
workspaceOverride={workspace}
on:done={(e) => {
if (testJob) {
const startedAt = new Date(testJob.started_at).getTime()
if (startedAt > lastStartedAt) {
lastStartedAt = startedAt
setResult(e.detail.result)
}
}
lastJobId = e.detail.id
setResult(e.detail.result, e.detail.id)
loading = false
}}
bind:isLoading={testIsLoading}
bind:job={testJob}
bind:this={testJobLoader}
on:doneError={(e) => {
setResult({ error: e.detail }, e.detail.id)
loading = false
}}
bind:this={resultJobLoader}
/>
{#if render}
@@ -444,7 +449,7 @@
<Alert type="error" title="Error during execution">
<div class="flex flex-col gap-2">
An error occured, please contact the app author.
<span class="font-semibold">Job id: {testJob?.id}</span>
<span class="font-semibold">Job id: {lastJobId}</span>
</div>
</Alert>
</div>