From 577868ef2a3c5a197e33addd85362dd5e7bd2a73 Mon Sep 17 00:00:00 2001 From: tristantr Date: Mon, 25 May 2026 15:11:18 +0200 Subject: [PATCH] Wire recordings to real jobs with run-preview UX - Recording flow now fetches the real schema, runs the job, and polls getCompletedJobResultMaybe to surface success/failure before saving. - Drawer shows a sticky status box (loader / success / failure) with a result preview, a job link, and an in-context Save CTA. - Only successful runs can be saved as a recording. Failures show the error and offer re-run. - Filter cache/state/app_theme internal resource types (mirrors workspaces_export.rs filter). - Added "What is a recording?" explainer banner above the items list. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../workspaceSettings/DeployToHub.svelte | 251 ++++++++++++++++-- 1 file changed, 222 insertions(+), 29 deletions(-) diff --git a/frontend/src/lib/components/workspaceSettings/DeployToHub.svelte b/frontend/src/lib/components/workspaceSettings/DeployToHub.svelte index 7ad7494ab5..28c04e65fb 100644 --- a/frontend/src/lib/components/workspaceSettings/DeployToHub.svelte +++ b/frontend/src/lib/components/workspaceSettings/DeployToHub.svelte @@ -14,6 +14,7 @@ import { AppService, FlowService, + JobService, RawAppService, ResourceService, ScriptService, @@ -27,9 +28,11 @@ Copy, ExternalLink, Globe, + Loader2, Play, RotateCcw, - TriangleAlert + TriangleAlert, + X } from 'lucide-svelte' import type { Kind } from '$lib/utils_deployable' @@ -54,14 +57,7 @@ let loading = $state(false) let workspaceRateLimit = $state(undefined) - const FAKE_SCHEMA = { - type: 'object', - properties: { - customer: { type: 'string', description: 'Customer to run against' }, - includeArchived: { type: 'boolean', description: 'Include archived rows', default: false } - }, - required: ['customer'] - } + const EMPTY_SCHEMA = { type: 'object', properties: {}, required: [] } // MOCK: no backend endpoint exposes a hub-slug or hub version per workspace yet. let hubSlug = $derived($workspaceStore ?? '') let hubUrl = $derived(`https://hub.windmill.dev/workspaces/${hubSlug}`) @@ -77,6 +73,15 @@ let recordTarget = $state() let recordArgs = $state>({}) let recordValid = $state(true) + let recordSchema = $state>(EMPTY_SCHEMA) + let recordSchemaLoading = $state(false) + type RunState = 'idle' | 'running' | 'success' | 'failed' + let runState = $state('idle') + let runJobId = $state(undefined) + let runResult = $state(undefined) + let runError = $state(undefined) + // MOCK STORAGE: backend has no recordings table yet; we keep the job_id per item locally. + let recordings = $state>({}) let publishDrawer = $state() let publishTarget = $state() @@ -138,8 +143,10 @@ rec: 'none' }) } + // Mirrors backend/workspaces_export.rs: skip internal types not meant for export. + const HIDDEN_RESOURCE_TYPES = new Set(['app_theme', 'state', 'cache']) for (const r of resources) { - if (r.resource_type === 'app_theme') continue + if (HIDDEN_RESOURCE_TYPES.has(r.resource_type)) continue next.push({ key: `resource:${r.path}`, path: r.path, @@ -197,20 +204,104 @@ } } - function openRecord(it: DeployItem) { + async function openRecord(it: DeployItem) { recordTarget = it recordArgs = {} recordValid = true + recordSchema = EMPTY_SCHEMA + recordSchemaLoading = true + runState = 'idle' + runJobId = undefined + runResult = undefined + runError = undefined recordDrawer?.openDrawer() + const workspace = $workspaceStore + if (!workspace) { + recordSchemaLoading = false + return + } + try { + if (it.kind === 'script') { + const s = await ScriptService.getScriptByPath({ workspace, path: it.path }) + recordSchema = (s.schema as Record) ?? EMPTY_SCHEMA + } else if (it.kind === 'flow') { + const f = await FlowService.getFlowByPath({ workspace, path: it.path }) + recordSchema = (f.schema as Record) ?? EMPTY_SCHEMA + } + } catch (e: any) { + sendUserToast(`Failed to load schema: ${e?.message ?? e}`, true) + } finally { + recordSchemaLoading = false + } } - async function confirmRecord() { - // MOCK: recording feature not implemented backend-side. + async function runJob() { const it = recordTarget - if (!it) return - recordDrawer?.closeDrawer() - items = items.map((i) => (i.key === it.key ? { ...i, rec: 'recording' } : i)) - await delay(900) + const workspace = $workspaceStore + if (!it || !workspace) return + runState = 'running' + runJobId = undefined + runResult = undefined + runError = undefined + try { + let jobId: string + if (it.kind === 'script') { + jobId = await JobService.runScriptByPath({ + workspace, + path: it.path, + requestBody: recordArgs + }) + } else if (it.kind === 'flow') { + jobId = await JobService.runFlowByPath({ + workspace, + path: it.path, + requestBody: recordArgs + }) + } else { + runState = 'idle' + return + } + runJobId = jobId + await pollJobUntilComplete(workspace, jobId) + } catch (e: any) { + runState = 'failed' + runError = `Failed to start: ${e?.message ?? e}` + } + } + async function pollJobUntilComplete(workspace: string, jobId: string) { + for (let i = 0; i < 300; i++) { + await delay(1000) + try { + const r = await JobService.getCompletedJobResultMaybe({ + workspace, + id: jobId + }) + if (r.completed) { + runResult = r.result + if (r.success) { + runState = 'success' + } else { + runState = 'failed' + runError = typeof r.result === 'string' ? r.result : JSON.stringify(r.result) + } + return + } + } catch (e: any) { + runState = 'failed' + runError = `Polling failed: ${e?.message ?? e}` + return + } + } + runState = 'failed' + runError = 'Timed out after 5 minutes' + } + function saveRecording() { + const it = recordTarget + if (!it || !runJobId || runState !== 'success') return + // MOCK STORAGE: would persist {item_path, hub_version, job_id} server-side. + recordings = { ...recordings, [it.key]: runJobId } items = items.map((i) => (i.key === it.key ? { ...i, rec: 'recorded' } : i)) + sendUserToast(`Recording saved — job ${runJobId}`) + recordDrawer?.closeDrawer() } function openPublish(it: DeployItem) { @@ -300,6 +391,21 @@ {/if} + {#if phase === 'live'} +
+ +
+ What is a recording? + + A recording is one real execution of a script or flow, captured with its inputs, + logs, step outputs, and final result. Hub visitors can replay it step-by-step to + understand how the item works without needing access to your workspace. + +
+
+ {/if} {/snippet} @@ -317,6 +423,16 @@ Recorded v{hubVersion} + {#if recordings[it.key]} + + Job + + {/if} + + {:else if runState === 'failed'} + + Fix inputs and try again. Only successful runs can be saved as a recording. + + {/if} + + {/if} + + {#if recordSchemaLoading} + Loading schema… + {:else} + + {/if} {#snippet actions()} - - + {#if runState === 'success'} + + + {:else} + + {/if} {/snippet}