mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-22 16:02:24 +00:00
fix(frontend): only download result for apps
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user