From c000bbca283f5d61cff8a39458764b2b2dd2b58f Mon Sep 17 00:00:00 2001 From: Guilhem Date: Thu, 9 Jul 2026 01:53:43 +0200 Subject: [PATCH] fix(frontend): scope raw-app, flow and script editors to the session workspace (#10015) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(frontend): scope raw-app/flow/script editors to the session workspace Co-Authored-By: Claude Opus 4.8 (1M context) * fix(frontend): scope flow and script editor operations to the session workspace Co-Authored-By: Claude Opus 4.8 (1M context) * fix(frontend): scope flow preview, inline-script creation and datatable schema to the session workspace Co-Authored-By: Claude Opus 4.8 (1M context) * fix(frontend): address Codex review — thread session workspace through flow resource pickers, script fetch, preview cancel/recording and path collision check Co-Authored-By: Claude Opus 4.8 (1M context) * fix(frontend): address Claude review — pass session workspace to preview FlowStatusViewer and align FlowChatManager guards Co-Authored-By: Claude Opus 4.8 (1M context) * fix(frontend): address Pi review — show acting workspace in script-not-found message and fetch picked script from it in EditorBar Co-Authored-By: Claude Opus 4.8 (1M context) * fix(frontend): address Codex review round 2 — thread session workspace into flow step test, raw-app inline runnable, inline editor toolbars and MCP OAuth path Co-Authored-By: Claude Opus 4.8 (1M context) * fix(frontend): address Codex review round 3 — thread session workspace into dynamic-input helpers and the flow-preview argument side panel (history/saved-inputs/captures) Co-Authored-By: Claude Opus 4.8 (1M context) * fix(frontend): address Codex review round 4 — thread session workspace into nested flow/script drawers, flow chat inputs and the flow input side tabs Co-Authored-By: Claude Opus 4.8 (1M context) * fix(frontend): address Codex review round 5 — thread session workspace into script-module fork/reload and key the raw-app schema cache by workspace Co-Authored-By: Claude Opus 4.8 (1M context) * fix(frontend): address Codex review round 6 — key the DB manager schema cache by acting workspace Co-Authored-By: Claude Opus 4.8 (1M context) * fix(frontend): address Codex review round 7 — thread session workspace into resource-valued arg pickers and the editor variable/resource helper drawers Co-Authored-By: Claude Opus 4.8 (1M context) * fix(frontend): scope the flow asset explorer's ResourceEditorDrawer to the acting workspace Co-Authored-By: Claude Opus 4.8 (1M context) * fix: thread acting workspace through flow asset explore controls Co-Authored-By: Claude Opus 4.8 (1M context) * fix: thread acting workspace through SQL REPL, secret args, helper forms, S3 inputs, saved inputs Co-Authored-By: Claude Opus 4.8 (1M context) --------- Co-authored-by: Claude Opus 4.8 (1M context) --- frontend/src/lib/components/ArgInput.svelte | 11 ++- .../lib/components/DBManagerContent.svelte | 38 ++++++--- .../src/lib/components/DBManagerDrawer.svelte | 32 +++++--- .../src/lib/components/DynamicInput.svelte | 12 ++- frontend/src/lib/components/EditorBar.svelte | 43 +++++----- .../src/lib/components/EditorHeader.svelte | 1 + .../lib/components/ExploreAssetButton.svelte | 45 ++++++----- .../FlowLoopIterationPreview.svelte | 16 +++- .../lib/components/FlowPreviewContent.svelte | 23 ++++-- .../src/lib/components/HistoricInputs.svelte | 9 ++- frontend/src/lib/components/JobLoader.svelte | 16 ++-- frontend/src/lib/components/ModuleTest.svelte | 11 ++- .../lib/components/ObjectResourceInput.svelte | 8 +- frontend/src/lib/components/Path.svelte | 59 ++++++++------ .../components/PathNameAutocomplete.svelte | 17 ++-- .../src/lib/components/ResourceEditor.svelte | 1 + .../src/lib/components/ResourceForm.svelte | 18 ++++- .../src/lib/components/S3FilePicker.svelte | 11 ++- .../lib/components/S3FilePickerInner.svelte | 19 +++-- .../src/lib/components/S3ObjectPicker.svelte | 10 ++- .../lib/components/SaveInputsButton.svelte | 22 ++--- .../lib/components/SavedInputsPicker.svelte | 18 +++-- frontend/src/lib/components/SchemaForm.svelte | 6 +- .../components/SchemaFormWithArgPicker.svelte | 9 ++- .../src/lib/components/ScriptBuilder.svelte | 2 + .../src/lib/components/ScriptEditor.svelte | 46 +++++++---- frontend/src/lib/components/SqlRepl.svelte | 20 +++-- .../src/lib/components/VariableEditor.svelte | 24 +++--- .../src/lib/components/VariableForm.svelte | 10 ++- .../apps/editor/AppEditorHeaderDeploy.svelte | 19 +++-- .../components/dbManagerDrawerModel.svelte.ts | 18 ++++- .../components/flows/FlowAssetsHandler.svelte | 19 +++-- .../components/flows/FlowHistoryInner.svelte | 14 +++- .../flows/common/FlowCardHeader.svelte | 10 ++- .../flows/content/FlowEditorDrawer.svelte | 9 ++- .../content/FlowEnvironmentVariables.svelte | 18 +++-- .../components/flows/content/FlowInput.svelte | 9 ++- .../flows/content/FlowInputsFlow.svelte | 10 ++- .../flows/content/FlowModuleComponent.svelte | 28 +++---- .../flows/content/FlowModuleScript.svelte | 21 +++-- .../flows/content/FlowModuleSuspend.svelte | 6 +- .../flows/content/FlowPathViewer.svelte | 24 +++--- .../flows/content/FlowResult.svelte | 7 +- .../flows/content/FlowSettings.svelte | 3 +- .../flows/content/McpOAuthConnect.svelte | 13 ++- .../flows/content/McpToolEditor.svelte | 17 +++- .../flows/content/ScriptEditorDrawer.svelte | 16 +++- .../flows/conversations/FlowChat.svelte | 12 +-- .../conversations/FlowChatInterface.svelte | 1 + .../conversations/FlowChatManager.svelte.ts | 33 +++++--- .../components/flows/flowStateUtils.svelte.ts | 21 +++-- .../flows/map/FlowModuleSchemaMap.svelte | 13 ++- .../pickers/WorkspaceScriptPicker.svelte | 8 +- .../pickers/WorkspaceScriptPickerQuick.svelte | 9 ++- .../flows/propPicker/StepHistory.svelte | 8 +- frontend/src/lib/components/flows/types.ts | 4 + .../src/lib/components/flows/utils.svelte.ts | 7 +- .../graph/renderers/nodes/AssetNode.svelte | 1 + .../raw_apps/DefaultDatabaseSelector.svelte | 81 ++++++++++--------- .../raw_apps/RawAppDataTableDrawer.svelte | 13 +-- .../components/raw_apps/RawAppEditor.svelte | 5 ++ .../raw_apps/RawAppEditorHeader.svelte | 1 + .../raw_apps/RawAppInlineScriptEditor.svelte | 14 +++- .../RawAppInlineScriptRunnable.svelte | 8 +- .../raw_apps/RawAppInlineScriptsPanel.svelte | 11 ++- .../raw_apps/RawAppInputsSpecEditor.svelte | 6 +- .../raw_apps/RawAppSharedUiDrawer.svelte | 8 +- .../raw_apps/RawAppTemplatePicker.svelte | 15 +++- .../raw_apps/datatableUtils.svelte.ts | 19 +++-- .../components/raw_apps/rawAppWorkspace.ts | 22 +++++ frontend/src/lib/components/secretArgUtils.ts | 7 +- .../components/triggers/CaptureTable.svelte | 15 +++- frontend/src/lib/path.ts | 6 +- frontend/src/lib/scripts.ts | 13 ++- 74 files changed, 791 insertions(+), 388 deletions(-) create mode 100644 frontend/src/lib/components/raw_apps/rawAppWorkspace.ts diff --git a/frontend/src/lib/components/ArgInput.svelte b/frontend/src/lib/components/ArgInput.svelte index 368cfa6d8a..de5517add3 100644 --- a/frontend/src/lib/components/ArgInput.svelte +++ b/frontend/src/lib/components/ArgInput.svelte @@ -1070,7 +1070,14 @@ {/if} {:else if inputCat == 'dynamic'} - + {:else if inputCat == 'resource-object' && resourceTypes == undefined} Loading resource types... {:else if inputCat == 'resource-object' && (resourceTypes == undefined || (format && format?.split('-').length > 1 && resourceTypes.includes(format?.substring('resource-'.length))))} @@ -1078,6 +1085,7 @@ 1 diff --git a/frontend/src/lib/components/DBManagerContent.svelte b/frontend/src/lib/components/DBManagerContent.svelte index c40a2f7428..9332d7ad26 100644 --- a/frontend/src/lib/components/DBManagerContent.svelte +++ b/frontend/src/lib/components/DBManagerContent.svelte @@ -39,6 +39,10 @@ /** Tables that are already added and should show as disabled */ disabledTables?: SelectedTable[] onImport?: (mode: 'schema_and_data' | 'schema_only') => void + /** Workspace the datatable/schema lookups run against. Defaults to the + * navigation `$workspaceStore`; pass the acting workspace when embedded in + * a session preview whose workspace differs from the top nav. */ + workspace?: string } let { @@ -51,10 +55,13 @@ multiSelectMode = false, selectedTables = $bindable([]), disabledTables = [], - onImport + onImport, + workspace = undefined }: Props = $props() - let dbSchema: DBSchema | undefined = $derived(input && $dbSchemas[getDbSchemasPath(input)]) + let ws = $derived(workspace ?? $workspaceStore) + + let dbSchema: DBSchema | undefined = $derived(input && $dbSchemas[schemaCacheKey(input)]) const outOfOrderModal = createAsyncConfirmationModal() @@ -67,28 +74,36 @@ } } + // Scope the shared `dbSchemas` cache by the acting workspace: a datatable of + // the same name can exist in both the nav and the acting workspace, so the + // bare resource path alone would let one workspace's schema be reused for the + // other while DB operations target the acting one. + function schemaCacheKey(input: DbInput): string { + return `${ws}:${getDbSchemasPath(input)}` + } + let colDefs = resource( - () => [input], + () => [input, ws], async () => { if (!input) return - return await loadAllTablesMetaData($workspaceStore, input) + return await loadAllTablesMetaData(ws, input) } ) let dbSchemasPromise = resource( - () => [input], + () => [input, ws], async () => { if (!input) return - const dbSchemasPath = getDbSchemasPath(input) + const dbSchemasPath = schemaCacheKey(input) if (input.type == 'database') { $dbSchemas[dbSchemasPath] = await getDbSchemas( input.resourceType, input.resourcePath, - $workspaceStore, + ws, (message: string) => sendUserToast(message, true) ) } else if (input.type == 'ducklake') { $dbSchemas[dbSchemasPath] = await getDucklakeSchema({ - workspace: $workspaceStore!, + workspace: ws!, ducklake: input.ducklake }) } @@ -130,7 +145,7 @@ }} /> -{#if dbSchema && $workspaceStore && input} +{#if dbSchema && ws && input} {@const _input = input} {@const dbType = getDbType(_input)} @@ -165,11 +180,11 @@ colDefs, tableKey, input: _input, - workspace: $workspaceStore + workspace: ws })} dbSchemaOps={dbSchemaOpsWithPreviewScripts({ input: _input, - workspace: $workspaceStore, + workspace: ws, confirmRunOutOfOrder: (pending) => outOfOrderModal.ask({ title: 'Run migration out of order', @@ -201,6 +216,7 @@ { replResultData = data }} diff --git a/frontend/src/lib/components/DBManagerDrawer.svelte b/frontend/src/lib/components/DBManagerDrawer.svelte index b303671fe8..712c4fbccf 100644 --- a/frontend/src/lib/components/DBManagerDrawer.svelte +++ b/frontend/src/lib/components/DBManagerDrawer.svelte @@ -35,13 +35,15 @@ let open = $derived(uriState.open) + // The workspace the drawer's DB operations run against — the acting workspace of + // the editor that opened it (set via openDrawer), else the nav workspace. + let ws = $derived(uriState.workspace ?? $workspaceStore) + // Load available datatables when drawer opens with datatable input const datatables = resource([], async () => { - if (!$workspaceStore) return [] + if (!ws) return [] try { - return (await WorkspaceService.listDataTables({ workspace: $workspaceStore })).map( - (d) => d.name - ) + return (await WorkspaceService.listDataTables({ workspace: ws })).map((d) => d.name) } catch (e) { console.error('Failed to load datatables:', e) return [] @@ -113,10 +115,10 @@ async function handleExportSchema() { const source = currentSourceIdentifier() - if (!source || !$workspaceStore) return + if (!source || !ws) return try { exportResult = await WorkspaceService.exportPgSchema({ - workspace: $workspaceStore, + workspace: ws, requestBody: { source } }) exportDrawerOpen = true @@ -126,13 +128,13 @@ } async function handleImportDatabase() { - if (!importSource || !$workspaceStore) return + if (!importSource || !ws) return const target = currentSourceIdentifier() if (!target) return importLoading = true try { await WorkspaceService.importPgDatabase({ - workspace: $workspaceStore, + workspace: ws, requestBody: { source: toSourceIdentifier(importSource), target, @@ -173,11 +175,12 @@ noPadding id="db-manager-drawer" > - {#if uriState.effectiveInput && $workspaceStore} + {#if uriState.effectiveInput && ws} {#key uriState.selectedDatatable} @@ -274,7 +277,12 @@
Source database - +
Import mode diff --git a/frontend/src/lib/components/DynamicInput.svelte b/frontend/src/lib/components/DynamicInput.svelte index f4e2ba9c75..9f6f3b8261 100644 --- a/frontend/src/lib/components/DynamicInput.svelte +++ b/frontend/src/lib/components/DynamicInput.svelte @@ -33,9 +33,17 @@ format: string otherArgs?: Record name: string + /** Workspace the helper script runs in; defaults to the nav workspace. */ + workspace?: string } - let { value = $bindable(), helperScript, format, otherArgs: otherArgs }: Props = $props() + let { + value = $bindable(), + helperScript, + format, + otherArgs: otherArgs, + workspace = undefined + }: Props = $props() let [inputType, entrypoint] = $derived(format.includes('-') ? format.split('-', 2) : [format, '']) @@ -173,7 +181,7 @@ {#if helperScript} - +
{#if inputType === 'dynmultiselect'} diff --git a/frontend/src/lib/components/EditorBar.svelte b/frontend/src/lib/components/EditorBar.svelte index f5062bc008..1e6a29388f 100644 --- a/frontend/src/lib/components/EditorBar.svelte +++ b/frontend/src/lib/components/EditorBar.svelte @@ -117,6 +117,10 @@ right?: import('svelte').Snippet openAiChat?: boolean moduleId?: string + // Workspace to scope variable/resource/data-table lookups to. Defaults to + // the nav `$workspaceStore`; an AI-session live editor passes the session's + // acting workspace (a fork) so the helper pickers hit the right workspace. + workspace?: string } let { @@ -141,9 +145,12 @@ showHistoryDrawer = $bindable(false), right, openAiChat = false, - moduleId = undefined + moduleId = undefined, + workspace = undefined }: Props = $props() + let ws = $derived(workspace ?? $workspaceStore) + let contextualVariablePicker: ItemPicker | undefined = $state() let variablePicker: ItemPicker | undefined = $state() let resourcePicker: ItemPicker | undefined = $state() @@ -350,12 +357,12 @@ }) async function loadVariables() { - return await VariableService.listVariable({ workspace: $workspaceStore ?? '' }) + return await VariableService.listVariable({ workspace: ws ?? '' }) } async function loadContextualVariables() { return await VariableService.listContextualVariables({ - workspace: $workspaceStore ?? 'NO_W' + workspace: ws ?? 'NO_W' }) } @@ -366,7 +373,7 @@ async function onScriptPick(e: { detail: { path: string } }) { codeObj = undefined codeViewer?.openDrawer?.() - codeObj = await getScriptByPath(e.detail.path ?? '') + codeObj = await getScriptByPath(e.detail.path ?? '', ws) } const dispatch = createEventDispatcher() @@ -423,7 +430,7 @@ async function resourceTypePickCallback(name: string) { if (!editor) return const resourceType = await ResourceService.getResourceType({ - workspace: $workspaceStore ?? 'NO_W', + workspace: ws ?? 'NO_W', path: name }) @@ -785,8 +792,7 @@ JsonNode ${windmillPathToCamelCaseName(path)} = JsonNode.Parse(await client.GetS buttons={{ 'Edit/View': (x) => resourceEditor?.initEdit(x) }} extraField="description" extraField2="resource_type" - loadItems={async () => - await ResourceService.listResource({ workspace: $workspaceStore ?? 'NO_W' })} + loadItems={async () => await ResourceService.listResource({ workspace: ws ?? 'NO_W' })} > {#snippet submission()}
@@ -812,12 +818,15 @@ JsonNode ${windmillPathToCamelCaseName(path)} = JsonNode.Parse(await client.GetS documentationLink="https://www.windmill.dev/docs/core_concepts/resources_and_types" itemName="Resource Type" extraField="name" - loadItems={async () => - await ResourceService.listResourceType({ workspace: $workspaceStore ?? 'NO_W' })} + loadItems={async () => await ResourceService.listResourceType({ workspace: ws ?? 'NO_W' })} /> {/if} - - + + {#if showDucklakePicker} - (await WorkspaceService.listDucklakes({ workspace: $workspaceStore ?? 'NO_W' })).map( - (path) => ({ path }) - )} + (await WorkspaceService.listDucklakes({ workspace: ws ?? 'NO_W' })).map((path) => ({ path }))} > {#snippet submission()}
@@ -885,9 +892,9 @@ JsonNode ${windmillPathToCamelCaseName(path)} = JsonNode.Parse(await client.GetS documentationLink="https://www.windmill.dev/docs/core_concepts/persistent_storage/data_tables" itemName="data table" loadItems={async () => - (await WorkspaceService.listDataTables({ workspace: $workspaceStore ?? 'NO_W' })).map( - (d) => ({ path: d.name }) - )} + (await WorkspaceService.listDataTables({ workspace: ws ?? 'NO_W' })).map((d) => ({ + path: d.name + }))} > {#snippet submission()}
@@ -923,7 +930,7 @@ JsonNode ${windmillPathToCamelCaseName(path)} = JsonNode.Parse(await client.GetS extraField2="resource_type" loadItems={async () => await ResourceService.listResource({ - workspace: $workspaceStore ?? 'NO_W', + workspace: ws ?? 'NO_W', resourceType: 'postgresql,mysql,bigquery' })} > diff --git a/frontend/src/lib/components/EditorHeader.svelte b/frontend/src/lib/components/EditorHeader.svelte index b6e0bb4c82..fdf18b4b50 100644 --- a/frontend/src/lib/components/EditorHeader.svelte +++ b/frontend/src/lib/components/EditorHeader.svelte @@ -220,6 +220,7 @@ {kind} size="sm" drawerOffset={4000} + workspaceOverride={workspaceId} /> {#if savedPath && path && path !== savedPath} @@ -60,18 +64,20 @@ on:click={async () => { if (asset.kind === 'resource' && isDbType(_resourceMetadata?.resource_type)) { let [resourcePath, specificTable] = asset.path.split('?table=') - dbManagerDrawer?.openDrawer({ - type: 'database', - resourceType: _resourceMetadata.resource_type, - resourcePath, - specificTable - }) + dbManagerDrawer?.openDrawer( + { + type: 'database', + resourceType: _resourceMetadata.resource_type, + resourcePath, + specificTable + }, + ws + ) } else if (asset.kind === 's3object' && isS3Uri(assetUri)) { s3FilePicker?.open(assetUri) } else if (asset.kind === 'volume') { - const storage = - (await VolumeService.getVolumeStorage({ workspace: $workspaceStore! })) ?? undefined - s3FilePicker?.open({ s3: `volumes/${$workspaceStore}/${asset.path}/`, storage }) + const storage = (await VolumeService.getVolumeStorage({ workspace: ws! })) ?? undefined + s3FilePicker?.open({ s3: `volumes/${ws}/${asset.path}/`, storage }) } else if (asset.kind === 'ducklake') { let ducklake = asset.path.split('/')[0] let specificTableSplit = asset.path.split('/')[1]?.split('.') as string[] | undefined @@ -79,7 +85,7 @@ specificTableSplit?.length === 2 ? [specificTableSplit[0], specificTableSplit[1]] : [undefined, specificTableSplit?.[0]] - dbManagerDrawer?.openDrawer({ type: 'ducklake', ducklake, specificSchema, specificTable }) + dbManagerDrawer?.openDrawer({ type: 'ducklake', ducklake, specificSchema, specificTable }, ws) } else if (asset.kind === 'datatable') { let datatable = asset.path.split('/')[0] let specificTableSplit = asset.path.split('/')[1]?.split('.') as string[] | undefined @@ -87,13 +93,16 @@ specificTableSplit?.length === 2 ? [specificTableSplit[0], specificTableSplit[1]] : [undefined, specificTableSplit?.[0]] - dbManagerDrawer?.openDrawer({ - type: 'database', - resourceType: 'postgresql', - resourcePath: `datatable://${datatable}`, - specificTable, - specificSchema - }) + dbManagerDrawer?.openDrawer( + { + type: 'database', + resourceType: 'postgresql', + resourcePath: `datatable://${datatable}`, + specificTable, + specificSchema + }, + ws + ) } onClick?.() }} diff --git a/frontend/src/lib/components/FlowLoopIterationPreview.svelte b/frontend/src/lib/components/FlowLoopIterationPreview.svelte index f92072b5fd..b4dde43289 100644 --- a/frontend/src/lib/components/FlowLoopIterationPreview.svelte +++ b/frontend/src/lib/components/FlowLoopIterationPreview.svelte @@ -73,7 +73,8 @@ runPreview(previewArgs, undefined) } - const { flowStateStore, pathStore } = getContext('FlowEditorContext') + const { flowStateStore, pathStore, opWorkspace } = + getContext('FlowEditorContext') const dispatch = createEventDispatcher() export async function runPreview( @@ -82,7 +83,15 @@ ) { progressBar?.reset() const newFlow = { value: { modules }, summary: '' } - jobId = await runFlowPreview(args, newFlow, $pathStore, restartedFrom) + jobId = await runFlowPreview( + args, + newFlow, + $pathStore, + restartedFrom, + undefined, + undefined, + opWorkspace?.() + ) isRunning = true } @@ -130,7 +139,7 @@ try { jobId && (await JobService.cancelQueuedJob({ - workspace: $workspaceStore ?? '', + workspace: opWorkspace?.() ?? $workspaceStore ?? '', id: jobId, requestBody: {} })) @@ -177,6 +186,7 @@ {#if jobId} { job = newJob diff --git a/frontend/src/lib/components/FlowPreviewContent.svelte b/frontend/src/lib/components/FlowPreviewContent.svelte index bf06628cc0..dd359fb006 100644 --- a/frontend/src/lib/components/FlowPreviewContent.svelte +++ b/frontend/src/lib/components/FlowPreviewContent.svelte @@ -133,8 +133,11 @@ fakeInitialPath, customUi, executionCount, - devTempScriptRefs + devTempScriptRefs, + opWorkspace } = $state(getContext('FlowEditorContext')) + // Acting workspace when previewing inside an AI session; else the nav workspace. + let opWs = $derived(opWorkspace?.() ?? $workspaceStore) const dispatch = createEventDispatcher() let renderCount: number = $state(0) @@ -193,14 +196,15 @@ lastPreviewFlow = JSON.stringify(flowStore.val) flowProgressBar?.reset() const newFlow = extractFlow(previewMode) - args = await processSecretArgs(args, flowStore.val.schema as any) + args = await processSecretArgs(args, flowStore.val.schema as any, opWs) newJobId = await runFlowPreview( args, newFlow, $pathStore, restartedFrom, conversationId, - devTempScriptRefs?.() + devTempScriptRefs?.(), + opWorkspace?.() ) jobId = newJobId isRunning = true @@ -286,7 +290,7 @@ subJobIds.map(async (subId) => { try { const subJob = await JobService.getJob({ - workspace: $workspaceStore!, + workspace: opWs!, id: subId }) flowRecording.addCompletedJob(subId, subJob) @@ -332,11 +336,11 @@ untrack(() => { for (const mod of modules) { if (mod.job) { - flowRecording.watchSubJob(mod.job, $workspaceStore!) + flowRecording.watchSubJob(mod.job, opWs!) } } if (job?.flow_status?.failure_module?.job) { - flowRecording.watchSubJob(job.flow_status.failure_module.job, $workspaceStore!) + flowRecording.watchSubJob(job.flow_status.failure_module.job, opWs!) } }) } @@ -347,7 +351,7 @@ try { jobId && (await JobService.cancelQueuedJob({ - workspace: $workspaceStore ?? '', + workspace: opWs ?? '', id: jobId, requestBody: {} })) @@ -514,6 +518,7 @@ runnableId={$initialPathStore} stablePathForCaptures={$initialPathStore || fakeInitialPath} runnableType={'FlowPath'} + workspace={opWs} previewArgs={previewArgs.val} on:openTriggers on:select={(e) => { @@ -562,6 +567,7 @@ { @@ -632,7 +638,7 @@
{ isRunning = false diff --git a/frontend/src/lib/components/HistoricInputs.svelte b/frontend/src/lib/components/HistoricInputs.svelte index 3c784f354f..85f2603005 100644 --- a/frontend/src/lib/components/HistoricInputs.svelte +++ b/frontend/src/lib/components/HistoricInputs.svelte @@ -17,6 +17,8 @@ placement?: 'bottom-start' | 'top-start' | 'bottom-end' | 'top-end' limitPayloadSize?: boolean searchArgs?: Record | undefined + /** Workspace to read run history from; defaults to the nav workspace. */ + workspace?: string } let { @@ -26,9 +28,12 @@ showAuthor = false, placement = 'top-end', limitPayloadSize = false, - searchArgs = undefined + searchArgs = undefined, + workspace = undefined }: Props = $props() + let ws = $derived(workspace ?? $workspaceStore) + let historicList: HistoricList | undefined = $state(undefined) const dispatch = createEventDispatcher() @@ -111,7 +116,7 @@ jobKinds: getJobKinds(runnableType), syncQueuedRunsCount: false, refreshRate: 10000, - currentWorkspace: $workspaceStore ?? '', + currentWorkspace: ws ?? '', skip: !runnableId, excludesEntrypointOverride: true }) satisfies UseJobLoaderArgs diff --git a/frontend/src/lib/components/JobLoader.svelte b/frontend/src/lib/components/JobLoader.svelte index a32eecf93a..c9eb2e7230 100644 --- a/frontend/src/lib/components/JobLoader.svelte +++ b/frontend/src/lib/components/JobLoader.svelte @@ -119,7 +119,7 @@ if (lastJobId && (job || lastCallbacks?.loadExtraLogs)) { plimit(() => JobService.getCompletedJobLogsTail({ - workspace: $workspaceStore!, + workspace: workspace!, id: lastJobId }) ).then((res) => { @@ -222,7 +222,7 @@ return abstractRun( () => JobService.runScriptByPath({ - workspace: $workspaceStore!, + workspace: workspace!, path: path ?? '', requestBody: args, skipPreprocessor: true @@ -239,7 +239,7 @@ return abstractRun( () => JobService.runScriptByHash({ - workspace: $workspaceStore!, + workspace: workspace!, hash: hash ?? '', requestBody: args, skipPreprocessor: true @@ -256,7 +256,7 @@ return abstractRun( () => JobService.runFlowByPath({ - workspace: $workspaceStore!, + workspace: workspace!, path: path ?? '', requestBody: args, skipPreprocessor: true @@ -274,7 +274,7 @@ return abstractRun( () => JobService.runFlowPreview({ - workspace: $workspaceStore!, + workspace: workspace!, requestBody: { args, value: flow.value, @@ -318,7 +318,7 @@ return abstractRun( () => JobService.runDynamicSelect({ - workspace: $workspaceStore!, + workspace: workspace!, requestBody: { entrypoint_function, args, runnable_ref } }), callbacks @@ -342,7 +342,7 @@ return abstractRun( () => JobService.runScriptPreview({ - workspace: $workspaceStore!, + workspace: workspace!, timeout, requestBody: { path, @@ -371,7 +371,7 @@ currentEventSource = undefined try { await JobService.cancelQueuedJob({ - workspace: $workspaceStore ?? '', + workspace: workspace ?? '', id, requestBody: {} }) diff --git a/frontend/src/lib/components/ModuleTest.svelte b/frontend/src/lib/components/ModuleTest.svelte index 785e111527..ed9b9a5694 100644 --- a/frontend/src/lib/components/ModuleTest.svelte +++ b/frontend/src/lib/components/ModuleTest.svelte @@ -39,9 +39,13 @@ stepsInputArgs, previewArgs, modulesTestStates, - devTempScriptRefs + devTempScriptRefs, + opWorkspace } = getContext('FlowEditorContext') + // Acting workspace when the flow editor runs in an AI session; else the nav workspace. + let opWs = $derived(opWorkspace?.() ?? $workspaceStore) + let jobLoader: JobLoader | undefined = $state(undefined) let jobProgressReset: () => void = () => {} let stepHistoryLoader = getStepHistoryLoaderContext() @@ -102,8 +106,8 @@ ) } else if (val.type == 'script') { const script = val.hash - ? await ScriptService.getScriptByHash({ workspace: $workspaceStore!, hash: val.hash }) - : await getScriptByPath(val.path) + ? await ScriptService.getScriptByHash({ workspace: opWs!, hash: val.hash }) + : await getScriptByPath(val.path, opWs) await jobLoader?.runPreview( val.path, script.content, @@ -202,6 +206,7 @@ void + /** Workspace the resource picker lists from; defaults to the nav workspace. */ + workspace?: string } let { @@ -32,7 +34,8 @@ editor = $bindable(undefined), disabled = false, datatableAsPgResource = false, - onClear = undefined + onClear = undefined, + workspace = undefined }: Props = $props() function isResource() { @@ -55,7 +58,7 @@
{#if format === 'resource-s3_object'} - + {:else if value == undefined || typeof value === 'string'} valueToPath(), (v) => { diff --git a/frontend/src/lib/components/Path.svelte b/frontend/src/lib/components/Path.svelte index b8c37011bd..27d6b45e6c 100644 --- a/frontend/src/lib/components/Path.svelte +++ b/frontend/src/lib/components/Path.svelte @@ -78,6 +78,11 @@ disableEditing?: boolean size?: 'sm' | 'md' drawerOffset?: number + /** Workspace the folder list and path-existence checks run against. + * Defaults to the navigation `$workspaceStore`; pass the session's acting + * workspace when the editor operates on a workspace other than the one the + * top nav points at (see the sessions preview / dev-workspace flows). */ + workspaceOverride?: string } let { @@ -94,9 +99,12 @@ hideUser = false, disableEditing = false, size = 'md', - drawerOffset = 0 + drawerOffset = 0, + workspaceOverride = undefined }: Props = $props() + let ws = $derived(workspaceOverride ?? $workspaceStore) + $effect.pre(() => { if (path == undefined) { path = '' @@ -203,7 +211,7 @@ folders = initialFolders.concat( ( await FolderService.listFolderNames({ - workspace: $workspaceStore! + workspace: ws! }) ) .filter((x) => !excludedFolders.includes(x)) @@ -244,74 +252,74 @@ async function pathExists(path: string, kind: PathKind): Promise { if (!path.length) return false if (kind == 'flow') { - return await FlowService.existsFlowByPath({ workspace: $workspaceStore!, path: path }) + return await FlowService.existsFlowByPath({ workspace: ws!, path: path }) } else if (kind == 'script') { return await ScriptService.existsScriptByPath({ - workspace: $workspaceStore!, + workspace: ws!, path: path }) } else if (kind == 'resource') { return await ResourceService.existsResource({ - workspace: $workspaceStore!, + workspace: ws!, path: path }) } else if (kind == 'variable') { return await VariableService.existsVariable({ - workspace: $workspaceStore!, + workspace: ws!, path: path }) } else if (kind == 'schedule') { - return await ScheduleService.existsSchedule({ workspace: $workspaceStore!, path: path }) + return await ScheduleService.existsSchedule({ workspace: ws!, path: path }) } else if (kind == 'app') { - return await AppService.existsApp({ workspace: $workspaceStore!, path: path }) + return await AppService.existsApp({ workspace: ws!, path: path }) } else if (kind == 'http_trigger') { return await HttpTriggerService.existsHttpTrigger({ - workspace: $workspaceStore!, + workspace: ws!, path: path }) } else if (kind == 'websocket_trigger') { return await WebsocketTriggerService.existsWebsocketTrigger({ - workspace: $workspaceStore!, + workspace: ws!, path: path }) } else if (kind == 'kafka_trigger') { return await KafkaTriggerService.existsKafkaTrigger({ - workspace: $workspaceStore!, + workspace: ws!, path: path }) } else if (kind == 'postgres_trigger') { return await PostgresTriggerService.existsPostgresTrigger({ - workspace: $workspaceStore!, + workspace: ws!, path: path }) } else if (kind == 'nats_trigger') { return await NatsTriggerService.existsNatsTrigger({ - workspace: $workspaceStore!, + workspace: ws!, path: path }) } else if (kind === 'mqtt_trigger') { return await MqttTriggerService.existsMqttTrigger({ - workspace: $workspaceStore!, + workspace: ws!, path: path }) } else if (kind == 'sqs_trigger') { return await SqsTriggerService.existsSqsTrigger({ - workspace: $workspaceStore!, + workspace: ws!, path: path }) } else if (kind === 'gcp_trigger') { return await GcpTriggerService.existsGcpTrigger({ - workspace: $workspaceStore!, + workspace: ws!, path: path }) } else if (kind === 'azure_trigger') { return await AzureTriggerService.existsAzureTrigger({ - workspace: $workspaceStore!, + workspace: ws!, path: path }) } else if (kind === 'email_trigger') { return await EmailTriggerService.existsEmailTrigger({ - workspace: $workspaceStore!, + workspace: ws!, path: path }) } else { @@ -398,7 +406,7 @@ }) }) $effect.pre(() => { - if ($workspaceStore && $userStore) { + if (ws && $userStore) { untrack(() => { loadFolders() initPath() @@ -412,30 +420,30 @@ ) let pathUsageInFlowsPromise = $derived( (kind == 'script' || kind == 'flow') && - $workspaceStore && + ws && initialPath && FlowService.listFlowPathsFromWorkspaceRunnable({ - workspace: $workspaceStore, + workspace: ws, path: initialPath, runnableKind: kind }) ) let pathUsageInAppsPromise = $derived( (kind == 'script' || kind == 'flow') && - $workspaceStore && + ws && initialPath && AppService.listAppPathsFromWorkspaceRunnable({ - workspace: $workspaceStore, + workspace: ws, path: initialPath, runnableKind: kind }) ) let pathUsageInScriptsPromise = $derived( kind == 'script' && - $workspaceStore && + ws && initialPath && ScriptService.listScriptPathsFromWorkspaceRunnable({ - workspace: $workspaceStore, + workspace: ws, path: initialPath }) ) @@ -525,6 +533,7 @@ bind:this={inputP} bind:value={meta.name} prefix={`${meta.ownerKind?.charAt(0) ?? ''}/${meta.owner ?? ''}/`} + workspace={ws} {size} {error} {autofocus} diff --git a/frontend/src/lib/components/PathNameAutocomplete.svelte b/frontend/src/lib/components/PathNameAutocomplete.svelte index 9c31ef980a..10d5c10640 100644 --- a/frontend/src/lib/components/PathNameAutocomplete.svelte +++ b/frontend/src/lib/components/PathNameAutocomplete.svelte @@ -101,6 +101,10 @@ error?: string | boolean textInputClass?: string onkeyup?: (e: KeyboardEvent) => void + /** Workspace whose paths feed the autocomplete. Defaults to the navigation + * `$workspaceStore`; pass the acting workspace when the editor operates on + * a workspace other than the one the top nav points at. */ + workspace?: string } let { @@ -113,9 +117,12 @@ size = 'md', error, textInputClass, - onkeyup + onkeyup, + workspace = undefined }: Props = $props() + let ws = $derived(workspace ?? $workspaceStore) + let inputEl: TextInput | undefined = $state(undefined) export function focus() { inputEl?.focus() @@ -258,12 +265,12 @@ async function loadPaths(workspace: string) { const paths = await fetchWorkspacePaths(workspace) // Guard against workspace changing during the in-flight fetch. - if ($workspaceStore === workspace) allPaths = paths + if (ws === workspace) allPaths = paths } $effect(() => { - const ws = $workspaceStore - if (ws) void loadPaths(ws) + const w = ws + if (w) void loadPaths(w) }) $effect(() => { @@ -300,7 +307,7 @@ function onInputFocus() { hasFocus = true // Opportunistic refresh if the cache is stale. - if ($workspaceStore) void loadPaths($workspaceStore) + if (ws) void loadPaths(ws) } function onInputBlur() { setTimeout(() => { diff --git a/frontend/src/lib/components/ResourceEditor.svelte b/frontend/src/lib/components/ResourceEditor.svelte index 8804ad8ac5..7acc4e485b 100644 --- a/frontend/src/lib/components/ResourceEditor.svelte +++ b/frontend/src/lib/components/ResourceEditor.svelte @@ -402,6 +402,7 @@ {loadingSchema} {resourceToEdit} onLoadResourceType={() => resourceTypeResource.refetch()} + workspace={selected} /> {/key} {/if} diff --git a/frontend/src/lib/components/ResourceForm.svelte b/frontend/src/lib/components/ResourceForm.svelte index ced60bf4dc..ef226501b7 100644 --- a/frontend/src/lib/components/ResourceForm.svelte +++ b/frontend/src/lib/components/ResourceForm.svelte @@ -42,6 +42,9 @@ loadingSchema: boolean resourceToEdit: Resource | undefined onLoadResourceType?: () => void + /** Workspace the path is validated against and the connection is tested in; + * defaults to the nav workspace. */ + workspace?: string | undefined } let { @@ -62,9 +65,12 @@ resourceSchema, loadingSchema, resourceToEdit, - onLoadResourceType + onLoadResourceType, + workspace = undefined }: Props = $props() + let ws = $derived(workspace ?? $workspaceStore) + let editDescription = $state(false) let rawCode: string | undefined = $state(undefined) let textFileContent: string = $state('') @@ -131,11 +137,12 @@ {/if}
@@ -218,7 +225,11 @@ {#if resourceToEdit?.resource_type === 'nats' || resourceToEdit?.resource_type === 'kafka'} {:else} - + {/if} {#if resource_type === 'git_repository' && $workspaceStore && ($userStore?.is_admin || $userStore?.is_super_admin)} {/if} {:else if !can_write} diff --git a/frontend/src/lib/components/S3FilePicker.svelte b/frontend/src/lib/components/S3FilePicker.svelte index 55bb25c5ef..8829660ea3 100644 --- a/frontend/src/lib/components/S3FilePicker.svelte +++ b/frontend/src/lib/components/S3FilePicker.svelte @@ -18,6 +18,9 @@ selectedFileKey?: { s3: string; storage?: string } | undefined folderOnly?: boolean regexFilter?: RegExp | undefined + /** Workspace to browse S3 storage in — the acting workspace of the editor that + * opened the picker, else the nav workspace. */ + workspace?: string | undefined onClose?: () => void onSelectAndClose?: (selected: { s3: string; storage: string | undefined }) => void } @@ -30,10 +33,13 @@ selectedFileKey = $bindable(undefined), folderOnly = false, regexFilter = undefined, + workspace = undefined, onClose, onSelectAndClose }: Props = $props() + let ws = $derived(workspace ?? $workspaceStore) + let drawer: Drawer | undefined = $state() let s3FilePickerInner: S3FilePickerInner | undefined = $state() @@ -55,8 +61,8 @@ > = $state({}) let secondaryStorageNames = resource( - () => $workspaceStore, - () => SettingService.getSecondaryStorageNames({ workspace: $workspaceStore! }), + () => ws, + () => SettingService.getSecondaryStorageNames({ workspace: ws! }), { lazy: true } ) @@ -105,6 +111,7 @@ bind:uploadModalOpen {folderOnly} {regexFilter} + {workspace} /> {#snippet actions()}
diff --git a/frontend/src/lib/components/S3FilePickerInner.svelte b/frontend/src/lib/components/S3FilePickerInner.svelte index 333857b7eb..9475c0041a 100644 --- a/frontend/src/lib/components/S3FilePickerInner.svelte +++ b/frontend/src/lib/components/S3FilePickerInner.svelte @@ -65,6 +65,8 @@ regexFilter?: RegExp | undefined hideS3SpecificDetails?: boolean rootPath?: string + /** Workspace to browse S3 storage in — defaults to the nav workspace. */ + workspace?: string | undefined workspaceSettingsInitialized?: boolean storage?: string | undefined uploadModalOpen?: boolean @@ -103,6 +105,7 @@ regexFilter = undefined, hideS3SpecificDetails = false, rootPath: initialRootPath = '', + workspace = undefined, workspaceSettingsInitialized = $bindable(true), storage = $bindable(undefined), uploadModalOpen = $bindable(false), @@ -117,6 +120,8 @@ testConnectionRequest = HelpersService.datasetStorageTestConnection }: Props = $props() + let ws = $derived(workspace ?? $workspaceStore) + let rootPath = $state(initialRootPath) let rootPathNestingLevel = $derived(1 * (rootPath.split('/').length - 1)) @@ -183,7 +188,7 @@ async function loadFiles() { fileListLoading = true let availableFiles = await listStoredFilesRequest({ - workspace: $workspaceStore!, + workspace: ws!, maxKeys: maxKeys, // fixed pages of 1000 files for now marker: page == 0 ? undefined : listMarkers[page - 1], prefix: rootPath ?? (filter.trim() != '' ? filter : undefined), @@ -280,7 +285,7 @@ } fileInfoLoading = true let fileMetadataRaw = await loadFileMetadataRequest({ - workspace: $workspaceStore!, + workspace: ws!, fileKey: fileKey, storage: storage }) @@ -300,7 +305,7 @@ async function loadFilePreview(fileKey: string, fileSizeInBytes?: number, fileMimeType?: string) { let filePreviewRaw = await loadFilePreviewRequest({ - workspace: $workspaceStore!, + workspace: ws!, fileKey: fileKey, fileSizeInBytes: fileSizeInBytes, fileMimeType: fileMimeType, @@ -349,7 +354,7 @@ } try { await deleteS3FileRequest({ - workspace: $workspaceStore!, + workspace: ws!, fileKey: fileKey, storage: storage }) @@ -409,7 +414,7 @@ } try { await moveS3FileRequest({ - workspace: $workspaceStore!, + workspace: ws!, srcFileKey: srcFileKey, destFileKey: destFileKey!, storage: storage @@ -457,7 +462,7 @@ fileListLoading = true try { await testConnectionRequest({ - workspace: $workspaceStore!, + workspace: ws!, storage: storage }) workspaceSettingsInitialized = true @@ -716,7 +721,7 @@ {#if filePreview !== undefined && (!hideS3SpecificDetails || !readOnlyMode || allowDelete)}
{#if !hideS3SpecificDetails} - {@const downloadApiPath = `/w/${$workspaceStore}/job_helpers/download_s3_file?file_key=${encodeURIComponent(fileMetadata?.fileKey ?? '')}${storage ? `&storage=${storage}` : ''}`} + {@const downloadApiPath = `/w/${ws}/job_helpers/download_s3_file?file_key=${encodeURIComponent(fileMetadata?.fileKey ?? '')}${storage ? `&storage=${storage}` : ''}`} {@const downloadName = fileMetadata?.fileKey.split('/').pop() ?? 'unnamed_download.file'} {#if shouldDownloadViaClient()} diff --git a/frontend/src/lib/components/S3ObjectPicker.svelte b/frontend/src/lib/components/S3ObjectPicker.svelte index 1c461708cd..bb3e19599e 100644 --- a/frontend/src/lib/components/S3ObjectPicker.svelte +++ b/frontend/src/lib/components/S3ObjectPicker.svelte @@ -14,9 +14,15 @@ interface Props { value: any editor?: SimpleEditor | undefined + /** Workspace to browse/upload S3 objects in; defaults to the nav workspace. */ + workspace?: string | undefined } - let { value = $bindable(), editor = $bindable(undefined) }: Props = $props() + let { + value = $bindable(), + editor = $bindable(undefined), + workspace = undefined + }: Props = $props() const dispatch = createEventDispatcher() @@ -48,6 +54,7 @@ editor?.setCode(rawValue) }} readOnlyMode={false} + {workspace} />
@@ -85,6 +92,7 @@ } }} defaultValue={value?.s3} + {workspace} /> {/if}