mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-26 00:03:59 +00:00
feat: add native result streaming (#6242)
* feat: add stream output feature to SSE job updates Adds stream_output field to JobUpdate struct that extracts log lines starting with '[wm_stream]:' from job logs. Regular logs now exclude stream lines, which are captured separately for specialized handling. - Added stream_output: Option<String> field to JobUpdate struct - Created extract_stream_output_from_logs() function to filter stream lines - Modified get_job_update_data() to use stream extraction logic - SSE clients now receive both new_logs and stream_output in job updates Co-authored-by: Ruben Fiszel <rubenfiszel@users.noreply.github.com> * feat: rename stream_output to stream and handle newlines as \n - Renamed `stream_output` field to `stream` in JobUpdate struct - Updated extract_stream_output_from_logs to extract_stream_from_logs - Changed stream output to join with literal \n instead of actual newlines - Stream lines are properly excluded from regular new_logs 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Ruben Fiszel <rubenfiszel@users.noreply.github.com> * decision tree nits * push ee ref * push ee ref * fix: fix id renaming in apps * remove duplicate caching (#6285) * feat: migrate audit log ids to bigints (blocking migration for EE) * fix(mcp): add proper check for mcp routes (#6282) * add proper check for mcp routes * cleaner * apply to flow * fix add checks scopes --------- Co-authored-by: dieriba <dieriba.pro@gmail.com> * chore(main): release 1.514.0 (#6283) * chore(main): release 1.514.0 * Apply automatic changes --------- Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com> * fix: pin tokio to 1.46.1 and aws-sdks-ts * pin rustls to 0.23.29 + pin aws-sdk * chore(main): release 1.514.1 (#6288) * chore(main): release 1.514.1 * Apply automatic changes --------- Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com> * fix: improve docker logs collection in docker mode * support $res: string in form inputs of arrays * fix import nit * fix: fix DynSelect * nits * fix: resource-type-ts-parser (#6289) * fix: resource types as arg in typescript handle imported defined types * Update nix flake (#6291) * merge * Small UI fixes (#6294) * fix step history not refreshing with staticInputs * fix array of obj not showing up in json editor in test this step * datatable scales correctly in DisplayResult and scrolling is much more usable * avoid next button disapearing and changing layout / hurting ux * nits * fix bug when renaming module A to B then module C to A, C takes the schema of A * fix bug with comments in sql repl * fix aggrid theme randomly not loading * bindable script * better delete button in db manager * property select doesnt exist * fix all warnings * delete $flowStateStore[id] on delete * feat(cli): generate cursor rules on init (#6270) * create cursor rules on init * change gen * add missing resource-type command * add resource type command in guidance * add schema option * revert * nit * nit * add flow guidance * nit * chore(main): release 1.515.0 (#6292) * chore(main): release 1.515.0 * Apply automatic changes --------- Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com> * fix: improved logs for script * nits logs * chore(main): release 1.515.1 (#6295) * chore(main): release 1.515.1 * Apply automatic changes --------- Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com> * merge * even more indexer tracings * add more tracing logs * feat: prevent too large results (>500Mb) from OOMing database * nit naming * feat: add CA certificate update at startup via environment variable (#6280) * feat: add CA certificate update at startup via environment variable Add support for running 'update-ca-certificates' at binary startup when RUN_UPDATE_CA_CERTIFICATE_AT_START environment variable is set to "true". - Check for RUN_UPDATE_CA_CERTIFICATE_AT_START env var on startup - Execute update-ca-certificates command if env var is set to "true" - Log success/failure appropriately with tracing - Continue startup even if CA certificate update fails - Non-blocking implementation with proper error handling Fixes #6279 Co-authored-by: Ruben Fiszel <rubenfiszel@users.noreply.github.com> * refactor: extract CA certificate update logic into separate function Extract the CA certificate update logic from windmill_main() into a dedicated update_ca_certificates_if_requested() function for better code organization and maintainability. Co-authored-by: Ruben Fiszel <rubenfiszel@users.noreply.github.com> * improvements --------- Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> Co-authored-by: Ruben Fiszel <rubenfiszel@users.noreply.github.com> Co-authored-by: Alexander Petric <alpetric@users.noreply.github.com> Co-authored-by: Alexander Petric <alex@windmill.dev> * fix: indexer collection of job logs before indexing (#6300) * Add flume as dependecy for indexer * Update ee-repo-ref * Remove flags from cargo.toml * Update ee-repo-ref * Update ee-repo-ref * fix rust sdk build error (#6305) Signed-off-by: pyranota <pyra@duck.com> * fix broken audit logs filter (#6304) * rename to from to * goto fix * default to false if field not present operator settings (#6301) * git sync UI improvements (#6303) * ui improvements round 1 * modal cleanup * init * UI refactor * UI cleanup + refactor * legacy cleanup * success model -> github actions, non-ee warnings * sqlx * npm check * ee warning everywhere * last comments * formatting * no hardcoded theme * claude review improvemenets * fix: no process relative imports for scripts with codebase * fix: sqs oidc authentication disconnect #6307 * handle metadata for new scripts happen after commit * handle_deployment_metadata in a task * nits * chore: add windmill-utils-internal package (#6299) * add utils package * naming * cleaning * add docs * remove log * use autogenerated types * remove old * fix * cleaning * add docs * chore(main): release 1.516.0 (#6298) * chore(main): release 1.516.0 * Apply automatic changes --------- Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com> * merge * indexer improvements * upgrade tantivy to 0.24.2 * use tantivy fork * nit warnings * fix oss build * improve indexer * chore: use windmill-utils-internal for cli (#6297) * add utils package * naming * cleaning * simplify assignPath * rename old files * same for locks * create on confirm * default true * use replaceinlinescripts from utils * use extractscriptfromflows * make it compile * cleaning * use argsigtojson * fix * fix missing await * cleaner * cleaning * cleaning * use in frontend * add docs * testing * remove log * use autogenerated types * remove old * fix * cleaning * adapt usage * draft * better build script * fix build * revert to default creation * add docs * remove and rename * make everything work * add await * only if not installed * add vs code setting * add to publish action * fix bc * safer use of sep * fix * do not rename on push * no publish on release * use published package on frontend * nit * Add dependencies to run sqlx prepare to nix flake (#6309) * feat(cli): wmill-lock.yaml v2 for easier git merge diffs * merge * merge * all * all * rm warnings * fix styling on aichatinput (#6312) * fix: use with_capacity back presusre for tantivy directory multipart writes (#6313) * use with capacity for tantivy directory multi part uploads * Update ee repo ref * Update ee-repo-ref * Update ee-repo-ref * chore(main): release 1.517.0 (#6310) * chore(main): release 1.517.0 * Apply automatic changes --------- Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com> * fix typo on cli build (#6314) * cleanup * feat(utils): add flow.yaml validation function (#6316) * add validateflow function * cleaner code * preprocess json * cleaning * create specific package * cleaning * add tests * fix: cleanup concurrency_counter automatically + remove orphans keys automatically * fix: add disabled support to resource picker in schema forms * fix: add wm_labels to tracing spans * all * merge * all * fix: delete empty git connection (#6318) * fix checks * bun handling * all * all? * all * all * update * all * update * check * fix history * all * all * all * Remove leftover debug tracing statements - Remove commented debug trace in jobs.rs for stream output - Remove commented debug trace in result_stream.rs for stream processing Co-authored-by: Ruben Fiszel <rubenfiszel@users.noreply.github.com> * fix test * all * handle iter * fix --------- Signed-off-by: pyranota <pyra@duck.com> Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> Co-authored-by: Ruben Fiszel <rubenfiszel@users.noreply.github.com> Co-authored-by: Ruben Fiszel <ruben@windmill.dev> Co-authored-by: centdix <40307056+centdix@users.noreply.github.com> Co-authored-by: dieriba <dieriba.pro@gmail.com> Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com> Co-authored-by: wendrul <53628737+wendrul@users.noreply.github.com> Co-authored-by: Diego Imbert <70353967+diegoimbert@users.noreply.github.com> Co-authored-by: Alexander Petric <alpetric@users.noreply.github.com> Co-authored-by: Alexander Petric <alex@windmill.dev> Co-authored-by: pyranota <92104930+pyranota@users.noreply.github.com>
This commit is contained in:
co-authored by
Ruben Fiszel
Ruben Fiszel
centdix
dieriba
rubenfiszel
wendrul
Diego Imbert
Alexander Petric
Alexander Petric
pyranota
parent
138a2a6379
commit
161dbbc6d6
@@ -39,6 +39,7 @@
|
||||
import { getContext, hasContext, createEventDispatcher, onDestroy } from 'svelte'
|
||||
import { toJsonStr } from '$lib/utils'
|
||||
import { userStore } from '$lib/stores'
|
||||
import ResultStreamDisplay from './ResultStreamDisplay.svelte'
|
||||
|
||||
const IMG_MAX_SIZE = 10000000
|
||||
const TABLE_MAX_SIZE = 5000000
|
||||
@@ -83,12 +84,14 @@
|
||||
noControls?: boolean
|
||||
drawerOpen?: boolean
|
||||
nodeId?: string | undefined
|
||||
loading?: boolean | undefined
|
||||
language?: string | undefined
|
||||
appPath?: string | undefined
|
||||
customUi?: DisplayResultUi | undefined
|
||||
isTest?: boolean
|
||||
externalToolbarAvailable?: boolean
|
||||
forceJson?: boolean
|
||||
result_stream?: string | undefined
|
||||
fixTableSizingToParent?: boolean
|
||||
copilot_fix?: import('svelte').Snippet
|
||||
children?: import('svelte').Snippet
|
||||
@@ -111,9 +114,11 @@
|
||||
isTest = true,
|
||||
externalToolbarAvailable = false,
|
||||
forceJson = $bindable(false),
|
||||
result_stream = undefined,
|
||||
fixTableSizingToParent = false,
|
||||
copilot_fix,
|
||||
children
|
||||
children,
|
||||
loading = false
|
||||
}: Props = $props()
|
||||
let enableHtml = $state(false)
|
||||
let s3FileDisplayRawMode = $state(false)
|
||||
@@ -487,7 +492,15 @@
|
||||
</script>
|
||||
|
||||
<HighlightTheme />
|
||||
{#if is_render_all}
|
||||
|
||||
{#if result_stream && result == undefined}
|
||||
<div class="flex flex-col w-full gap-2">
|
||||
<div class="flex items-center gap-2 text-tertiary">
|
||||
<Loader2 class="animate-spin" size={16} /> Streaming result
|
||||
</div>
|
||||
<ResultStreamDisplay {result_stream} />
|
||||
</div>
|
||||
{:else if is_render_all}
|
||||
<div class="flex flex-col w-full gap-2">
|
||||
{#if !noControls}
|
||||
<div class="text-tertiary text-sm">
|
||||
@@ -690,7 +703,7 @@
|
||||
{:else if !forceJson && resultKind === 'plain'}<div class="h-full text-2xs"
|
||||
><pre class="whitespace-pre-wrap"
|
||||
>{typeof result === 'string' ? result : result?.['result']}</pre
|
||||
>{#if !noControls}
|
||||
>{#if !noControls && !loading}
|
||||
<div class="flex">
|
||||
<Button
|
||||
on:click={() =>
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
interface Props {
|
||||
waitingForExecutor?: boolean
|
||||
result: any
|
||||
result_stream?: string
|
||||
logs: string | undefined
|
||||
col?: boolean
|
||||
noBorder?: boolean
|
||||
@@ -31,6 +32,7 @@
|
||||
let {
|
||||
waitingForExecutor = false,
|
||||
result,
|
||||
result_stream,
|
||||
logs = $bindable(),
|
||||
col = false,
|
||||
noBorder = false,
|
||||
@@ -105,8 +107,8 @@
|
||||
>
|
||||
<div class="bg-surface {col ? '' : 'max-h-80'} p-1 overflow-auto relative">
|
||||
<span class="text-tertiary">Result</span>
|
||||
{#if result !== undefined}
|
||||
<DisplayResult {workspaceId} {jobId} {filename} {result} />
|
||||
{#if result !== undefined || result_stream !== undefined}
|
||||
<DisplayResult {workspaceId} {jobId} {filename} {result} {result_stream} />
|
||||
{:else if loading}
|
||||
<Loader2 class="animate-spin" />
|
||||
{:else}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
suspendStatus: Writable<Record<string, { job: Job; nb: number }>>
|
||||
hideJobId?: boolean
|
||||
extra?: import('svelte').Snippet
|
||||
result_streams?: Record<string, string | undefined>
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -32,7 +33,8 @@
|
||||
innerModules,
|
||||
suspendStatus,
|
||||
hideJobId,
|
||||
extra
|
||||
extra,
|
||||
result_streams
|
||||
}: Props = $props()
|
||||
</script>
|
||||
|
||||
@@ -105,6 +107,9 @@
|
||||
<Loader2 class="animate-spin mt-0.5" /></span
|
||||
></div
|
||||
>
|
||||
{#if mod.job && result_streams?.[mod.job]}
|
||||
<pre class="text-xs text-primary">{result_streams?.[mod.job]}</pre>
|
||||
{/if}
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -83,10 +83,17 @@
|
||||
subflowParentsDurationStatuses?: Writable<Record<string, DurationStatus>>[]
|
||||
isForloopSelected?: boolean
|
||||
parentRecursiveRefresh?: Record<string, (clear, root) => Promise<void>>
|
||||
job?: Job | undefined
|
||||
job?: (Job & { result_stream?: string }) | undefined
|
||||
rightColumnSelect?: 'timeline' | 'node_status' | 'node_definition' | 'user_states'
|
||||
localModuleStates?: Writable<Record<string, GraphModuleState>>
|
||||
localDurationStatuses?: Writable<Record<string, DurationStatus>>
|
||||
onResultStreamUpdate?: ({
|
||||
jobId,
|
||||
result_stream
|
||||
}: {
|
||||
jobId: string
|
||||
result_stream?: string
|
||||
}) => void
|
||||
customUi?: {
|
||||
tagLabel?: string | undefined
|
||||
}
|
||||
@@ -119,8 +126,24 @@
|
||||
rightColumnSelect = $bindable('timeline'),
|
||||
localModuleStates = writable({}),
|
||||
localDurationStatuses = writable({}),
|
||||
customUi
|
||||
customUi,
|
||||
onResultStreamUpdate = undefined
|
||||
}: Props = $props()
|
||||
|
||||
let resultStreams: Record<string, string | undefined> = $state({})
|
||||
|
||||
if (onResultStreamUpdate == undefined) {
|
||||
onResultStreamUpdate = ({
|
||||
jobId,
|
||||
result_stream
|
||||
}: {
|
||||
jobId: string
|
||||
result_stream?: string
|
||||
}) => {
|
||||
resultStreams[jobId] = result_stream
|
||||
}
|
||||
}
|
||||
|
||||
let recursiveRefresh: Record<string, (clear, root) => Promise<void>> = $state({})
|
||||
|
||||
// Add support for the input args assets shown as an asset node
|
||||
@@ -516,6 +539,9 @@
|
||||
jobLoader?.watchJob(jobId, {
|
||||
change(newJob) {
|
||||
setJob(newJob, true)
|
||||
},
|
||||
resultStreamUpdate({ id, result_stream }: { id: string; result_stream?: string }) {
|
||||
onResultStreamUpdate?.({ jobId: id, result_stream })
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -959,6 +985,7 @@
|
||||
<DisplayResult
|
||||
workspaceId={job?.workspace_id}
|
||||
{jobId}
|
||||
result_stream={job?.result_stream}
|
||||
result={jobResults}
|
||||
language={job?.language}
|
||||
/>
|
||||
@@ -976,6 +1003,7 @@
|
||||
{innerModules}
|
||||
{suspendStatus}
|
||||
{hideJobId}
|
||||
result_streams={resultStreams}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -1067,6 +1095,7 @@
|
||||
storedListJobs[j] = job
|
||||
innerJobLoaded(job, j, false, force)
|
||||
}}
|
||||
{onResultStreamUpdate}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -1142,6 +1171,7 @@
|
||||
{reducedPolling}
|
||||
{workspaceId}
|
||||
jobId={failedRetry}
|
||||
{onResultStreamUpdate}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
@@ -1174,6 +1204,7 @@
|
||||
let { force, job } = e.detail
|
||||
onJobsLoaded(mod, job, force)
|
||||
}}
|
||||
{onResultStreamUpdate}
|
||||
/>
|
||||
{:else if mod.flow_jobs?.length == 0 && mod.job == '00000000-0000-0000-0000-000000000000'}
|
||||
<div class="text-secondary">no subflow (empty loop?)</div>
|
||||
@@ -1205,6 +1236,7 @@
|
||||
let { job, force } = e.detail
|
||||
onJobsLoaded(mod, job, force)
|
||||
}}
|
||||
{onResultStreamUpdate}
|
||||
/>
|
||||
{/if}
|
||||
{:else}
|
||||
@@ -1428,6 +1460,7 @@
|
||||
waitingForExecutor={node.type == 'WaitingForExecutor'}
|
||||
refreshLog={node.type == 'InProgress'}
|
||||
col
|
||||
result_stream={resultStreams[node.job_id ?? '']}
|
||||
result={node.result}
|
||||
tag={node.tag}
|
||||
logs={node.logs}
|
||||
|
||||
@@ -23,11 +23,12 @@
|
||||
cancel?: ({ id }: { id: string }) => void
|
||||
started?: ({ id }: { id: string }) => void
|
||||
running?: ({ id }: { id: string }) => void
|
||||
resultStreamUpdate?: ({ id, result_stream }: { id: string; result_stream?: string }) => void
|
||||
}
|
||||
|
||||
interface Props {
|
||||
isLoading?: boolean
|
||||
job?: Job | undefined
|
||||
job?: (Job & { result_stream?: string }) | undefined
|
||||
noCode?: boolean
|
||||
noLogs?: boolean
|
||||
workspaceOverride?: string | undefined
|
||||
@@ -35,7 +36,6 @@
|
||||
allowConcurentRequests?: boolean
|
||||
jobUpdateLastFetch?: Date | undefined
|
||||
toastError?: boolean
|
||||
lazyLogs?: boolean
|
||||
onlyResult?: boolean
|
||||
// If you want to find out progress of subjobs of a flow, check job.flow_status.progress
|
||||
scriptProgress?: number | undefined
|
||||
@@ -52,7 +52,6 @@
|
||||
notfound = $bindable(false),
|
||||
jobUpdateLastFetch = $bindable(undefined),
|
||||
toastError = false,
|
||||
lazyLogs = false,
|
||||
onlyResult = false,
|
||||
scriptProgress = $bindable(undefined),
|
||||
noLogs = false,
|
||||
@@ -74,6 +73,7 @@
|
||||
let errorIteration = 0
|
||||
|
||||
let logOffset = 0
|
||||
let resultStreamOffset = 0
|
||||
let lastCallbacks: Callbacks | undefined = undefined
|
||||
|
||||
let finished: string[] = []
|
||||
@@ -194,6 +194,9 @@
|
||||
if (logOffset == 0) {
|
||||
logOffset = job?.logs?.length ? job.logs?.length + 1 : 0
|
||||
}
|
||||
if (resultStreamOffset == 0) {
|
||||
resultStreamOffset = job?.result_stream?.length ? job.result_stream?.length + 1 : 0
|
||||
}
|
||||
}
|
||||
export async function getLogs() {
|
||||
if (job) {
|
||||
@@ -285,6 +288,7 @@
|
||||
let startedWatchingJob: number | undefined = undefined
|
||||
export async function watchJob(testId: string, callbacks?: Callbacks) {
|
||||
logOffset = 0
|
||||
resultStreamOffset = 0
|
||||
syncIteration = 0
|
||||
errorIteration = 0
|
||||
currentId = testId
|
||||
@@ -334,7 +338,7 @@
|
||||
|
||||
function updateJobFromProgress(
|
||||
previewJobUpdates: GetJobUpdatesResponse,
|
||||
job: Job,
|
||||
job: Job & { result_stream?: string },
|
||||
callbacks: Callbacks | undefined
|
||||
) {
|
||||
// Clamp number between two values with the following line:
|
||||
@@ -357,10 +361,26 @@
|
||||
}
|
||||
}
|
||||
|
||||
if (previewJobUpdates.new_result_stream) {
|
||||
if (!job.result_stream) {
|
||||
job.result_stream = previewJobUpdates.new_result_stream
|
||||
} else {
|
||||
job.result_stream = job.result_stream.concat(previewJobUpdates.new_result_stream)
|
||||
}
|
||||
callbacks?.resultStreamUpdate?.({
|
||||
id: job.id,
|
||||
result_stream: job.result_stream
|
||||
})
|
||||
}
|
||||
|
||||
if (previewJobUpdates.log_offset) {
|
||||
logOffset = previewJobUpdates.log_offset ?? 0
|
||||
}
|
||||
|
||||
if (previewJobUpdates.stream_offset) {
|
||||
resultStreamOffset = previewJobUpdates.stream_offset ?? 0
|
||||
}
|
||||
|
||||
if (previewJobUpdates.flow_status) {
|
||||
job.flow_status = previewJobUpdates.flow_status as FlowStatus
|
||||
}
|
||||
@@ -415,7 +435,7 @@
|
||||
job = await JobService.getJob({
|
||||
workspace: workspace!,
|
||||
id,
|
||||
noLogs: lazyLogs || onlyResult || noLogs,
|
||||
noLogs: onlyResult || noLogs,
|
||||
noCode
|
||||
})
|
||||
}
|
||||
@@ -504,6 +524,7 @@
|
||||
callbacks?: Callbacks
|
||||
): Promise<boolean> {
|
||||
let isCompleted = false
|
||||
let resultOnlyResultStream: string = ''
|
||||
if (isCurrentJob(id)) {
|
||||
try {
|
||||
// First load the job to get initial state
|
||||
@@ -511,11 +532,18 @@
|
||||
job = await JobService.getJob({
|
||||
workspace: workspace!,
|
||||
id,
|
||||
noLogs: lazyLogs || noLogs,
|
||||
noLogs: noLogs,
|
||||
noCode
|
||||
})
|
||||
}
|
||||
|
||||
if (!onlyResult) {
|
||||
callbacks?.resultStreamUpdate?.({
|
||||
id,
|
||||
result_stream: undefined
|
||||
})
|
||||
}
|
||||
|
||||
// If job is already completed, don't start SSE
|
||||
if (job?.type === 'CompletedJob') {
|
||||
isCompleted = true
|
||||
@@ -555,6 +583,12 @@
|
||||
if (startedWatchingJob && startedWatchingJob > Date.now() - 5000) {
|
||||
params.set('fast', 'true')
|
||||
}
|
||||
if (noLogs) {
|
||||
params.set('no_logs', 'true')
|
||||
}
|
||||
if (resultStreamOffset) {
|
||||
params.set('stream_offset', resultStreamOffset.toString())
|
||||
}
|
||||
|
||||
const sseUrl = `/api/w/${workspace}/jobs_u/getupdate_sse/${id}?${params.toString()}`
|
||||
|
||||
@@ -598,6 +632,17 @@
|
||||
callbacks?.running?.({ id })
|
||||
}
|
||||
|
||||
if (onlyResult && previewJobUpdates.new_result_stream) {
|
||||
resultOnlyResultStream = resultOnlyResultStream.concat(
|
||||
previewJobUpdates.new_result_stream
|
||||
)
|
||||
// console.log('resultOnlyResultStream', resultOnlyResultStream)
|
||||
callbacks?.resultStreamUpdate?.({
|
||||
id,
|
||||
result_stream: resultOnlyResultStream
|
||||
})
|
||||
}
|
||||
|
||||
// Check if job is completed
|
||||
if (previewJobUpdates.completed) {
|
||||
currentEventSource?.close()
|
||||
@@ -611,8 +656,9 @@
|
||||
})
|
||||
clearCurrentId()
|
||||
} else {
|
||||
const njob = previewJobUpdates.job as Job
|
||||
const njob = previewJobUpdates.job as Job & { result_stream?: string }
|
||||
njob.logs = job?.logs ?? ''
|
||||
njob.result_stream = job?.result_stream ?? ''
|
||||
job = njob
|
||||
onJobCompleted(id, job, callbacks)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<script lang="ts">
|
||||
let { result_stream } = $props()
|
||||
</script>
|
||||
|
||||
<pre class="text-xs">{result_stream}</pre>
|
||||
@@ -26,6 +26,7 @@
|
||||
configuration: RichConfigurations
|
||||
}
|
||||
|
||||
let result_stream: string | undefined = $state(undefined)
|
||||
let {
|
||||
id,
|
||||
componentInput,
|
||||
@@ -57,6 +58,7 @@
|
||||
})
|
||||
|
||||
let css = $state(initCss($app.css?.displaycomponent, customCss))
|
||||
let loading = $state(false)
|
||||
</script>
|
||||
|
||||
{#each Object.keys(components['displaycomponent'].initialData.configuration) as key (key)}
|
||||
@@ -78,7 +80,15 @@
|
||||
/>
|
||||
{/each}
|
||||
|
||||
<RunnableWrapper {outputs} {render} {componentInput} {id} bind:initializing bind:result>
|
||||
<RunnableWrapper
|
||||
{outputs}
|
||||
{render}
|
||||
{componentInput}
|
||||
{id}
|
||||
bind:initializing
|
||||
bind:result
|
||||
bind:loading
|
||||
>
|
||||
<div class="flex flex-col w-full h-full component-wrapper">
|
||||
<div
|
||||
class={twMerge(
|
||||
@@ -103,8 +113,10 @@
|
||||
)}
|
||||
>
|
||||
<DisplayResult
|
||||
{loading}
|
||||
workspaceId={workspace}
|
||||
{result}
|
||||
{result_stream}
|
||||
{requireHtmlApproval}
|
||||
disableExpand={resolvedConfig?.hideDetails}
|
||||
appPath={$userStore ? undefined : $appPath}
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
let result: any = $state(noBackend ? runnable.noBackendValue : undefined)
|
||||
|
||||
export function onSuccess() {
|
||||
if (runnable.recomputeIds) {
|
||||
runnable.recomputeIds.forEach((id) => $runnableComponents?.[id]?.cb?.map((cb) => cb()))
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
extraQueryParams?: Record<string, any>
|
||||
autoRefresh?: boolean
|
||||
result?: any
|
||||
result_stream?: string
|
||||
forceSchemaDisplay?: boolean
|
||||
wrapperClass?: string
|
||||
wrapperStyle?: string
|
||||
@@ -72,6 +73,7 @@
|
||||
extraQueryParams = {},
|
||||
autoRefresh = true,
|
||||
result = $bindable(undefined),
|
||||
result_stream = $bindable(undefined),
|
||||
forceSchemaDisplay = false,
|
||||
wrapperClass = '',
|
||||
wrapperStyle = '',
|
||||
@@ -226,6 +228,15 @@
|
||||
loading = false
|
||||
dispatch('done', { id, result })
|
||||
},
|
||||
resultStreamUpdate({
|
||||
id,
|
||||
result_stream: nresult_stream
|
||||
}: {
|
||||
id: string
|
||||
result_stream?: string
|
||||
}) {
|
||||
setResult(nresult_stream, id)
|
||||
},
|
||||
cancel({ id }: { id: string }) {
|
||||
onCancel?.()
|
||||
let jobId = id
|
||||
|
||||
@@ -42,11 +42,12 @@
|
||||
<div class="break-words relative h-full px-1">
|
||||
<DisplayResult bind:drawerOpen={resultDrawerOpen} result={frontendJob} />
|
||||
</div>
|
||||
{:else if testJob != undefined && 'result' in testJob && testJob.result != undefined}
|
||||
{:else if testJob != undefined && (testJob.type == 'CompletedJob' || testJob.result_stream)}
|
||||
<div class="break-words relative h-full px-1">
|
||||
<DisplayResult
|
||||
bind:drawerOpen={resultDrawerOpen}
|
||||
workspaceId={testJob?.workspace_id}
|
||||
result_stream={testJob?.result_stream}
|
||||
jobId={testJob?.id}
|
||||
result={testJob.result}
|
||||
language={testJob?.language}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
children?: import('svelte').Snippet<[any]>
|
||||
content?: import('svelte').Snippet
|
||||
onSelectedChange?: (value: string) => void
|
||||
onTabClick?: (value: string) => void
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -27,7 +28,8 @@
|
||||
values = undefined,
|
||||
children,
|
||||
content,
|
||||
onSelectedChange
|
||||
onSelectedChange,
|
||||
onTabClick
|
||||
}: Props = $props()
|
||||
|
||||
const selectedStore = writable(selected)
|
||||
@@ -37,6 +39,7 @@
|
||||
update: (value: string) => {
|
||||
selectedStore.set(value)
|
||||
selected = value
|
||||
onTabClick?.(value)
|
||||
},
|
||||
hashNavigation
|
||||
})
|
||||
@@ -55,9 +58,11 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
selected && untrack(() => updateSelected())
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
$selectedStore && untrack(() => onSelectedChange?.($selectedStore))
|
||||
})
|
||||
|
||||
@@ -111,7 +111,6 @@
|
||||
<Button
|
||||
{...btn.buttonProps}
|
||||
startIcon={{ icon: btn.buttonProps.startIcon }}
|
||||
on:click={btn.buttonProps.onClick}
|
||||
btnClasses="hidden md:flex items-center gap-1"
|
||||
>
|
||||
{btn.label}
|
||||
@@ -119,7 +118,6 @@
|
||||
<Button
|
||||
{...btn.buttonProps}
|
||||
startIcon={{ icon: btn.buttonProps.startIcon }}
|
||||
on:click={btn.buttonProps.onClick}
|
||||
iconOnly
|
||||
btnClasses="flex md:hidden items-center gap-1"
|
||||
>
|
||||
|
||||
@@ -13,7 +13,8 @@ export const forbiddenIds: string[] = [
|
||||
'new',
|
||||
'in',
|
||||
'failure',
|
||||
'preprocessor'
|
||||
'preprocessor',
|
||||
'as'
|
||||
]
|
||||
|
||||
export function numberToChars(n: number) {
|
||||
|
||||
@@ -20,7 +20,11 @@
|
||||
<div
|
||||
class={twMerge(
|
||||
'rounded-full w-2 h-2',
|
||||
'success' in job && job.success ? 'bg-green-400' : 'bg-red-400'
|
||||
'success' in job && job.success
|
||||
? 'bg-green-400'
|
||||
: job.type === 'CompletedJob'
|
||||
? 'bg-red-400'
|
||||
: 'bg-yellow-400'
|
||||
)}
|
||||
title={'success' in job && job.success ? 'Success' : 'Failed'}
|
||||
></div>
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
disableMock?: boolean
|
||||
disableHistory?: boolean
|
||||
lastJob?: Job
|
||||
testJob?: Job
|
||||
testJob?: Job & { result_stream?: string }
|
||||
derivedHistoryOpen?: boolean // derived from historyOpen
|
||||
historyOffset?: any
|
||||
clazz?: string
|
||||
@@ -116,7 +116,7 @@
|
||||
workspace_id: string
|
||||
success: boolean
|
||||
}
|
||||
) & { preview?: boolean })
|
||||
) & { preview?: boolean; result_stream?: string; result?: unknown })
|
||||
| undefined
|
||||
|
||||
let jsonView = $state(false)
|
||||
@@ -135,9 +135,9 @@
|
||||
}
|
||||
|
||||
function selectJob(nJob: SelectedJob | undefined) {
|
||||
if (nJob && 'result' in nJob) {
|
||||
if (nJob && (nJob.result_stream || nJob.type == 'CompletedJob')) {
|
||||
selectedJob = nJob
|
||||
} else if (job && 'result' in job) {
|
||||
} else if (job && (job.result_stream || job.type == 'CompletedJob')) {
|
||||
selectedJob = job
|
||||
} else {
|
||||
selectedJob = undefined
|
||||
@@ -145,7 +145,7 @@
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (!job || !('result' in job)) {
|
||||
if (!job || !(job.result_stream || job.type == 'CompletedJob')) {
|
||||
return
|
||||
}
|
||||
selectJob(job)
|
||||
@@ -266,11 +266,13 @@
|
||||
return { ...lastJob, preview: false }
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
}) as SelectedJob | undefined
|
||||
|
||||
let popoverHeight = $derived(customHeight ?? (clientHeight > 0 ? clientHeight : 0))
|
||||
|
||||
const isLoadingAndNotMock = $derived(isLoading && !mock?.enabled)
|
||||
const isLoadingAndNotMock = $derived(
|
||||
isLoading && job?.result_stream === undefined && !mock?.enabled
|
||||
)
|
||||
|
||||
const copilot_fix_render = $derived(copilot_fix)
|
||||
</script>
|
||||
@@ -650,6 +652,7 @@
|
||||
bind:forceJson
|
||||
workspaceId={undefined}
|
||||
jobId={undefined}
|
||||
result_stream={undefined}
|
||||
result={mock?.return_value}
|
||||
externalToolbarAvailable
|
||||
fixTableSizingToParent
|
||||
@@ -669,7 +672,7 @@
|
||||
pureViewer={false}
|
||||
/>
|
||||
{/if}
|
||||
{:else if selectedJob != undefined && 'result' in selectedJob}
|
||||
{:else if selectedJob != undefined && (selectedJob.result_stream || selectedJob.type == 'CompletedJob')}
|
||||
{#if fullResult}
|
||||
<div class="break-words relative h-full">
|
||||
{#key selectedJob}
|
||||
@@ -679,6 +682,7 @@
|
||||
workspaceId={selectedJob?.workspace_id}
|
||||
jobId={selectedJob?.id}
|
||||
result={selectedJob?.result}
|
||||
result_stream={selectedJob?.result_stream}
|
||||
externalToolbarAvailable
|
||||
fixTableSizingToParent
|
||||
on:toolbar-location-changed={({ detail }) => {
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
|
||||
let { id, blankLink = false, workspace }: Props = $props()
|
||||
|
||||
let job: Job | undefined = $state(undefined)
|
||||
let job: (Job & { result_stream?: string }) | undefined = $state(undefined)
|
||||
|
||||
let result: any = $state()
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
let jobLoader: JobLoader | undefined = $state(undefined)
|
||||
</script>
|
||||
|
||||
<JobLoader lazyLogs workspaceOverride={workspace} bind:job={currentJob} bind:this={jobLoader} />
|
||||
<JobLoader noLogs workspaceOverride={workspace} bind:job={currentJob} bind:this={jobLoader} />
|
||||
|
||||
<div class="p-4 flex flex-col gap-2 items-start h-full">
|
||||
{#if job}
|
||||
@@ -239,7 +239,7 @@
|
||||
{:else}
|
||||
<Skeleton layout={[[5]]} />
|
||||
{/if}
|
||||
{:else if job !== undefined && 'result' in job && job.result !== undefined}
|
||||
{:else if job !== undefined && (job.result_stream || (job.type == 'CompletedJob' && job.result !== undefined))}
|
||||
<DisplayResult
|
||||
workspaceId={job?.workspace_id}
|
||||
jobId={job?.id}
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
interface Props {
|
||||
lang: Preview['language'] | undefined
|
||||
previewIsLoading?: boolean
|
||||
previewJob: Job | undefined
|
||||
previewJob: (Job & { result_stream?: string; result?: any; success?: boolean }) | undefined
|
||||
pastPreviews?: CompletedJob[]
|
||||
editor?: Editor | undefined
|
||||
diffEditor?: DiffEditor | undefined
|
||||
@@ -102,6 +102,7 @@
|
||||
result={drawerContent.content}
|
||||
customUi={customUi?.displayResult}
|
||||
language={lang}
|
||||
result_stream={previewJob?.result_stream}
|
||||
/>
|
||||
{:else if drawerContent?.mode === 'plain'}
|
||||
<pre
|
||||
@@ -113,7 +114,6 @@
|
||||
{/if}
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
<div class="h-full flex flex-col">
|
||||
<Tabs bind:selected={selectedTab} class="pt-1" wrapperClass="flex-none">
|
||||
<Tab value="logs" size="xs">Logs & Result</Tab>
|
||||
@@ -151,7 +151,7 @@
|
||||
</Pane>
|
||||
<Pane>
|
||||
{@render children?.()}
|
||||
{#if previewJob != undefined && 'result' in previewJob}
|
||||
{#if previewJob != undefined && (previewJob.result_stream || previewJob.result)}
|
||||
<div class="relative w-full h-full p-2">
|
||||
<div class="relative h-full">
|
||||
<DisplayResult
|
||||
@@ -161,6 +161,7 @@
|
||||
result={previewJob.result}
|
||||
customUi={customUi?.displayResult}
|
||||
language={lang}
|
||||
result_stream={previewJob?.result_stream}
|
||||
fixTableSizingToParent
|
||||
>
|
||||
{#snippet copilot_fix()}
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
import JobArgs from '$lib/components/JobArgs.svelte'
|
||||
import FlowProgressBar from '$lib/components/flows/FlowProgressBar.svelte'
|
||||
import JobProgressBar from '$lib/components/jobs/JobProgressBar.svelte'
|
||||
import Tabs from '$lib/components/common/tabs/Tabs.svelte'
|
||||
import Tabs from '$lib/components/common/tabs/TabsV2.svelte'
|
||||
import Badge from '$lib/components/common/badge/Badge.svelte'
|
||||
import { goto } from '$lib/navigation'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
@@ -97,7 +97,7 @@
|
||||
} from '$lib/components/flows/FlowAssetsHandler.svelte'
|
||||
import JobAssetsViewer from '$lib/components/assets/JobAssetsViewer.svelte'
|
||||
|
||||
let job: Job | undefined = $state()
|
||||
let job: (Job & { result?: any; result_stream?: string }) | undefined = $state()
|
||||
let jobUpdateLastFetch: Date | undefined = $state()
|
||||
|
||||
let scriptProgress: number | undefined = $state(undefined)
|
||||
@@ -123,6 +123,8 @@
|
||||
let lastJobId: string | undefined = $state(undefined)
|
||||
let concurrencyKey: string | undefined = $state(undefined)
|
||||
|
||||
let manuallySetLogs: boolean = $state(false)
|
||||
|
||||
setContext(
|
||||
'FlowGraphAssetContext',
|
||||
initFlowGraphAssetsCtx({ getModules: () => job?.raw_flow?.modules ?? [] })
|
||||
@@ -173,7 +175,7 @@
|
||||
|
||||
// If we get results, focus on that tab. Else, focus on logs
|
||||
function initView(): void {
|
||||
if (job && 'result' in job && job.result != undefined) {
|
||||
if (job && (job.result || job.result_stream)) {
|
||||
viewTab = 'result'
|
||||
} else if (viewTab == 'result') {
|
||||
viewTab = 'logs'
|
||||
@@ -182,6 +184,11 @@
|
||||
|
||||
async function getJob() {
|
||||
await jobLoader?.watchJob($page.params.run, {
|
||||
change(job: Job & { result_stream?: string }) {
|
||||
if (!manuallySetLogs && viewTab == 'logs' && job.result_stream) {
|
||||
viewTab = 'result'
|
||||
}
|
||||
},
|
||||
done(job) {
|
||||
if (job?.['result'] != undefined) {
|
||||
viewTab = 'result'
|
||||
@@ -366,13 +373,7 @@
|
||||
runImmediatelyLoading = false
|
||||
}
|
||||
}
|
||||
$effect(() => {
|
||||
job?.logs == undefined &&
|
||||
job &&
|
||||
viewTab == 'logs' &&
|
||||
isNotFlow(job?.job_kind) &&
|
||||
jobLoader?.getLogs()
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
job?.id && lastJobId !== job.id && untrack(() => getConcurrencyKey(job))
|
||||
})
|
||||
@@ -427,7 +428,6 @@
|
||||
{/if}
|
||||
{#if !job || (job?.job_kind != 'flow' && job?.job_kind != 'flownode' && job?.job_kind != 'flowpreview')}
|
||||
<JobLoader
|
||||
lazyLogs
|
||||
bind:scriptProgress
|
||||
bind:this={jobLoader}
|
||||
bind:isLoading={testIsLoading}
|
||||
@@ -933,7 +933,12 @@
|
||||
{/if}
|
||||
<!-- Logs and outputs-->
|
||||
<div class="mr-2 sm:mr-0 mt-12">
|
||||
<Tabs bind:selected={viewTab}>
|
||||
<Tabs
|
||||
bind:selected={viewTab}
|
||||
onTabClick={(value) => {
|
||||
manuallySetLogs = value == 'logs'
|
||||
}}
|
||||
>
|
||||
<Tab value="result">Result</Tab>
|
||||
<Tab value="logs">Logs</Tab>
|
||||
<Tab value="stats">Metrics</Tab>
|
||||
@@ -975,9 +980,10 @@
|
||||
<div class="w-full">
|
||||
<MemoryFootprintViewer jobId={job.id} bind:jobUpdateLastFetch />
|
||||
</div>
|
||||
{:else if job !== undefined && 'result' in job && job.result !== undefined}
|
||||
{:else if job !== undefined && (job.result || job.result_stream)}
|
||||
<DisplayResult
|
||||
workspaceId={job?.workspace_id}
|
||||
result_stream={job.result_stream}
|
||||
jobId={job?.id}
|
||||
result={job.result}
|
||||
language={job?.language}
|
||||
|
||||
Reference in New Issue
Block a user