mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-18 16:02:10 +00:00
fix(frontend): scope raw-app, flow and script editors to the session workspace (#10015)
* fix(frontend): scope raw-app/flow/script editors to the session workspace Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): scope flow and script editor operations to the session workspace Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): scope flow preview, inline-script creation and datatable schema to the session workspace Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * fix(frontend): address Claude review — pass session workspace to preview FlowStatusViewer and align FlowChatManager guards Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * fix(frontend): scope the flow asset explorer's ResourceEditorDrawer to the acting workspace Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: thread acting workspace through flow asset explore controls Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: thread acting workspace through SQL REPL, secret args, helper forms, S3 inputs, saved inputs Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1070,7 +1070,14 @@
|
||||
{/if}
|
||||
</div>
|
||||
{:else if inputCat == 'dynamic'}
|
||||
<DynamicInput name={label} {otherArgs} {helperScript} bind:value format={format ?? ''} />
|
||||
<DynamicInput
|
||||
name={label}
|
||||
{otherArgs}
|
||||
{helperScript}
|
||||
{workspace}
|
||||
bind:value
|
||||
format={format ?? ''}
|
||||
/>
|
||||
{:else if inputCat == 'resource-object' && resourceTypes == undefined}
|
||||
<span class="text-2xs text-primary">Loading resource types...</span>
|
||||
{:else if inputCat == 'resource-object' && (resourceTypes == undefined || (format && format?.split('-').length > 1 && resourceTypes.includes(format?.substring('resource-'.length))))}
|
||||
@@ -1078,6 +1085,7 @@
|
||||
<ObjectResourceInput
|
||||
datatableAsPgResource={label === 'database'}
|
||||
{disabled}
|
||||
{workspace}
|
||||
{defaultValue}
|
||||
selectFirst={!noDefaultOnSelectFirst && required}
|
||||
{disablePortal}
|
||||
@@ -1423,6 +1431,7 @@
|
||||
<ResourcePicker
|
||||
selectFirst={noDefaultOnSelectFirst}
|
||||
{disablePortal}
|
||||
{workspace}
|
||||
bind:value
|
||||
initialValue={defaultValue}
|
||||
resourceType={format && format.split('-').length > 1
|
||||
|
||||
@@ -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)}
|
||||
<Splitpanes horizontal>
|
||||
@@ -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 @@
|
||||
<Pane bind:size={replPanelSize} minSize={REPL_MIN_SIZE} class="relative">
|
||||
<SqlRepl
|
||||
{input}
|
||||
{workspace}
|
||||
onData={(data) => {
|
||||
replResultData = data
|
||||
}}
|
||||
|
||||
@@ -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<string[]>([], 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}
|
||||
<DBManagerContent
|
||||
bind:this={dbManagerContent}
|
||||
input={uriState.effectiveInput}
|
||||
workspace={uriState.workspace}
|
||||
bind:hasReplResult
|
||||
bind:selectedSchemaKey={uriState.selectedSchema}
|
||||
bind:selectedTableKey={uriState.selectedTable}
|
||||
@@ -207,9 +210,9 @@
|
||||
{/key}
|
||||
{/if}
|
||||
{#snippet actions()}
|
||||
{#if uriState.isDatatableInput && uriState.selectedDatatable && $workspaceStore}
|
||||
{#if uriState.isDatatableInput && uriState.selectedDatatable && ws}
|
||||
<DataTableMigrationsButton
|
||||
workspace={$workspaceStore}
|
||||
workspace={ws}
|
||||
datatable={uriState.selectedDatatable}
|
||||
onSchemaChanged={refreshManager}
|
||||
/>
|
||||
@@ -274,7 +277,12 @@
|
||||
</Alert>
|
||||
<div class="flex flex-col gap-2">
|
||||
<span class="text-sm font-medium">Source database</span>
|
||||
<ResourcePicker datatableAsPgResource bind:value={importSource} resourceType="postgresql" />
|
||||
<ResourcePicker
|
||||
datatableAsPgResource
|
||||
bind:value={importSource}
|
||||
resourceType="postgresql"
|
||||
workspace={ws}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<span class="text-sm font-medium">Import mode</span>
|
||||
|
||||
@@ -33,9 +33,17 @@
|
||||
format: string
|
||||
otherArgs?: Record<string, any>
|
||||
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 @@
|
||||
</script>
|
||||
|
||||
{#if helperScript}
|
||||
<JobLoader onlyResult bind:this={resultJobLoader} />
|
||||
<JobLoader onlyResult workspaceOverride={workspace} bind:this={resultJobLoader} />
|
||||
|
||||
<div class="w-full flex-col flex">
|
||||
{#if inputType === 'dynmultiselect'}
|
||||
|
||||
@@ -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()}
|
||||
<div class="flex flex-row gap-x-1 mr-2">
|
||||
@@ -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}
|
||||
<ResourceEditorDrawer bind:this={resourceEditor} on:refresh={resourcePicker.openDrawer} />
|
||||
<VariableEditor bind:this={variableEditor} on:create={variablePicker.openDrawer} />
|
||||
<ResourceEditorDrawer
|
||||
bind:this={resourceEditor}
|
||||
workspace={ws}
|
||||
on:refresh={resourcePicker.openDrawer}
|
||||
/>
|
||||
<VariableEditor bind:this={variableEditor} workspace={ws} on:create={variablePicker.openDrawer} />
|
||||
|
||||
{#if showDucklakePicker}
|
||||
<ItemPicker
|
||||
@@ -842,9 +851,7 @@ JsonNode ${windmillPathToCamelCaseName(path)} = JsonNode.Parse(await client.GetS
|
||||
documentationLink="https://www.windmill.dev/docs/core_concepts/persistent_storage/ducklake"
|
||||
itemName="ducklake"
|
||||
loadItems={async () =>
|
||||
(await WorkspaceService.listDucklakes({ workspace: $workspaceStore ?? 'NO_W' })).map(
|
||||
(path) => ({ path })
|
||||
)}
|
||||
(await WorkspaceService.listDucklakes({ workspace: ws ?? 'NO_W' })).map((path) => ({ path }))}
|
||||
>
|
||||
{#snippet submission()}
|
||||
<div class="flex flex-row gap-x-1 mr-2">
|
||||
@@ -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()}
|
||||
<div class="flex flex-row gap-x-1 mr-2">
|
||||
@@ -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'
|
||||
})}
|
||||
></ItemPicker>
|
||||
|
||||
@@ -220,6 +220,7 @@
|
||||
{kind}
|
||||
size="sm"
|
||||
drawerOffset={4000}
|
||||
workspaceOverride={workspaceId}
|
||||
/>
|
||||
{#if savedPath && path && path !== savedPath}
|
||||
<Alert
|
||||
|
||||
@@ -33,7 +33,8 @@
|
||||
noText = false,
|
||||
buttonVariant = 'default',
|
||||
btnClasses = '',
|
||||
disabled = false
|
||||
disabled = false,
|
||||
workspace = undefined
|
||||
}: {
|
||||
asset: Asset
|
||||
_resourceMetadata?: { resource_type?: string }
|
||||
@@ -44,9 +45,12 @@
|
||||
buttonVariant?: ButtonType.Variant
|
||||
btnClasses?: string
|
||||
disabled?: boolean
|
||||
/** Workspace the explored asset lives in; defaults to the nav workspace. */
|
||||
workspace?: string
|
||||
} = $props()
|
||||
|
||||
let dbManagerDrawer = $derived(globalDbManagerDrawer.val)
|
||||
let ws = $derived(workspace ?? $workspaceStore)
|
||||
const assetUri = $derived(formatAsset(asset))
|
||||
</script>
|
||||
|
||||
@@ -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?.()
|
||||
}}
|
||||
|
||||
@@ -73,7 +73,8 @@
|
||||
runPreview(previewArgs, undefined)
|
||||
}
|
||||
|
||||
const { flowStateStore, pathStore } = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
const { flowStateStore, pathStore, opWorkspace } =
|
||||
getContext<FlowEditorContext>('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}
|
||||
<FlowStatusViewer
|
||||
bind:flowState={flowStateStore.val}
|
||||
workspaceId={opWorkspace?.()}
|
||||
{jobId}
|
||||
onJobsLoaded={({ job: newJob }) => {
|
||||
job = newJob
|
||||
|
||||
@@ -133,8 +133,11 @@
|
||||
fakeInitialPath,
|
||||
customUi,
|
||||
executionCount,
|
||||
devTempScriptRefs
|
||||
devTempScriptRefs,
|
||||
opWorkspace
|
||||
} = $state(getContext<FlowEditorContext>('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 @@
|
||||
<SchemaForm
|
||||
noVariablePicker
|
||||
compact
|
||||
workspace={opWs}
|
||||
schema={flowStore.val.schema}
|
||||
bind:args={previewArgs.val}
|
||||
on:change={() => {
|
||||
@@ -632,7 +638,7 @@
|
||||
<div class="w-full my-6">
|
||||
<FlowExecutionStatus
|
||||
{job}
|
||||
workspaceId={$workspaceStore}
|
||||
workspaceId={opWs}
|
||||
{isOwner}
|
||||
innerModules={job?.flow_status?.modules}
|
||||
{suspendStatus}
|
||||
@@ -665,6 +671,7 @@
|
||||
hideDownloadInGraph={customUi?.downloadLogs === false}
|
||||
wideResults
|
||||
bind:flowState={flowStateStore.val}
|
||||
workspaceId={opWs}
|
||||
{jobId}
|
||||
onDone={async ({ job: completedJob }) => {
|
||||
isRunning = false
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
placement?: 'bottom-start' | 'top-start' | 'bottom-end' | 'top-end'
|
||||
limitPayloadSize?: boolean
|
||||
searchArgs?: Record<string, any> | 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
|
||||
|
||||
@@ -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: {}
|
||||
})
|
||||
|
||||
@@ -39,9 +39,13 @@
|
||||
stepsInputArgs,
|
||||
previewArgs,
|
||||
modulesTestStates,
|
||||
devTempScriptRefs
|
||||
devTempScriptRefs,
|
||||
opWorkspace
|
||||
} = getContext<FlowEditorContext>('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 @@
|
||||
<JobLoader
|
||||
noCode={true}
|
||||
toastError={noEditor}
|
||||
workspaceOverride={opWs}
|
||||
bind:scriptProgress
|
||||
bind:this={jobLoader}
|
||||
bind:isLoading={
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
disabled?: boolean
|
||||
datatableAsPgResource?: boolean
|
||||
onClear?: () => 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 @@
|
||||
<!-- {JSON.stringify({ value })} -->
|
||||
<div class="flex flex-row w-full flex-wrap gap-x-2 gap-y-0.5">
|
||||
{#if format === 'resource-s3_object'}
|
||||
<S3ObjectPicker bind:value />
|
||||
<S3ObjectPicker bind:value {workspace} />
|
||||
{:else if value == undefined || typeof value === 'string'}
|
||||
<ResourcePicker
|
||||
{datatableAsPgResource}
|
||||
@@ -63,6 +66,7 @@
|
||||
{selectFirst}
|
||||
{disablePortal}
|
||||
{onClear}
|
||||
{workspace}
|
||||
bind:value={
|
||||
() => valueToPath(),
|
||||
(v) => {
|
||||
|
||||
@@ -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<boolean> {
|
||||
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}
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -402,6 +402,7 @@
|
||||
{loadingSchema}
|
||||
{resourceToEdit}
|
||||
onLoadResourceType={() => resourceTypeResource.refetch()}
|
||||
workspace={selected}
|
||||
/>
|
||||
{/key}
|
||||
{/if}
|
||||
|
||||
@@ -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}
|
||||
<Label label="Path">
|
||||
<Path
|
||||
disabled={initialPath != '' && !isOwner(initialPath, $userStore, $workspaceStore)}
|
||||
disabled={initialPath != '' && !isOwner(initialPath, $userStore, ws)}
|
||||
bind:path
|
||||
{initialPath}
|
||||
namePlaceholder="resource"
|
||||
kind="resource"
|
||||
workspaceOverride={workspace}
|
||||
/>
|
||||
</Label>
|
||||
</div>
|
||||
@@ -218,7 +225,11 @@
|
||||
{#if resourceToEdit?.resource_type === 'nats' || resourceToEdit?.resource_type === 'kafka'}
|
||||
<TestTriggerConnection kind={resourceToEdit?.resource_type} args={{ connection: args }} />
|
||||
{:else}
|
||||
<TestConnection resourceType={resourceToEdit?.resource_type} {args} />
|
||||
<TestConnection
|
||||
resourceType={resourceToEdit?.resource_type}
|
||||
{args}
|
||||
workspaceOverride={workspace}
|
||||
/>
|
||||
{/if}
|
||||
{#if resource_type === 'git_repository' && $workspaceStore && ($userStore?.is_admin || $userStore?.is_super_admin)}
|
||||
<GitHubAppIntegration
|
||||
@@ -274,6 +285,7 @@
|
||||
schema={resourceSchema}
|
||||
bind:args
|
||||
bind:isValid
|
||||
{workspace}
|
||||
/>
|
||||
{/if}
|
||||
{:else if !can_write}
|
||||
|
||||
@@ -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()}
|
||||
<div class="flex gap-1">
|
||||
|
||||
@@ -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)}
|
||||
<div class="flex gap-2 shrink-0">
|
||||
{#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()}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
<div class="flex flex-col w-full gap-1">
|
||||
@@ -85,6 +92,7 @@
|
||||
}
|
||||
}}
|
||||
defaultValue={value?.s3}
|
||||
{workspace}
|
||||
/>
|
||||
{/if}
|
||||
<Button
|
||||
|
||||
@@ -11,12 +11,13 @@
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
interface Props {
|
||||
runnableId: string | undefined;
|
||||
runnableType: RunnableType | undefined;
|
||||
args: object;
|
||||
disabled?: boolean;
|
||||
small?: boolean | undefined;
|
||||
showTooltip?: boolean | undefined;
|
||||
runnableId: string | undefined
|
||||
runnableType: RunnableType | undefined
|
||||
args: object
|
||||
disabled?: boolean
|
||||
small?: boolean | undefined
|
||||
showTooltip?: boolean | undefined
|
||||
workspace?: string | undefined
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -25,8 +26,11 @@
|
||||
args,
|
||||
disabled = false,
|
||||
small = undefined,
|
||||
showTooltip = undefined
|
||||
}: Props = $props();
|
||||
showTooltip = undefined,
|
||||
workspace = undefined
|
||||
}: Props = $props()
|
||||
|
||||
let ws = $derived(workspace ?? $workspaceStore)
|
||||
|
||||
let savingInputs = $state(false)
|
||||
|
||||
@@ -40,7 +44,7 @@
|
||||
|
||||
try {
|
||||
await InputService.createInput({
|
||||
workspace: $workspaceStore!,
|
||||
workspace: ws!,
|
||||
runnableId,
|
||||
runnableType,
|
||||
requestBody
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
noButton?: boolean
|
||||
jsonView?: boolean
|
||||
limitPayloadSize?: boolean
|
||||
/** Workspace to read/write saved inputs from; defaults to the nav workspace. */
|
||||
workspace?: string
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -30,9 +32,12 @@
|
||||
isValid = false,
|
||||
noButton = false,
|
||||
jsonView = false,
|
||||
limitPayloadSize = false
|
||||
limitPayloadSize = false,
|
||||
workspace = undefined
|
||||
}: Props = $props()
|
||||
|
||||
let ws = $derived(workspace ?? $workspaceStore)
|
||||
|
||||
interface EditableInput extends Input {
|
||||
isEditing?: boolean
|
||||
isSaving?: boolean
|
||||
@@ -58,7 +63,7 @@
|
||||
|
||||
try {
|
||||
await InputService.updateInput({
|
||||
workspace: $workspaceStore!,
|
||||
workspace: ws!,
|
||||
requestBody: {
|
||||
id: input.id,
|
||||
name: input.name,
|
||||
@@ -76,7 +81,7 @@
|
||||
function initLoadInputs() {
|
||||
const loadInputsPageFn = async (page: number, perPage: number) => {
|
||||
const inputs = await InputService.listInputs({
|
||||
workspace: $workspaceStore!,
|
||||
workspace: ws!,
|
||||
runnableId,
|
||||
runnableType,
|
||||
page,
|
||||
@@ -104,7 +109,7 @@
|
||||
|
||||
const deleteInputFn = async (id: string) => {
|
||||
await InputService.deleteInput({
|
||||
workspace: $workspaceStore!,
|
||||
workspace: ws!,
|
||||
input: id
|
||||
})
|
||||
}
|
||||
@@ -119,7 +124,7 @@
|
||||
if (!id) return
|
||||
return await InputService.getArgsFromHistoryOrSavedInput({
|
||||
jobOrInputId: id,
|
||||
workspace: $workspaceStore!,
|
||||
workspace: ws!,
|
||||
input,
|
||||
allowLarge
|
||||
})
|
||||
@@ -198,7 +203,7 @@
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
$workspaceStore &&
|
||||
ws &&
|
||||
runnableId &&
|
||||
runnableType &&
|
||||
(infiniteList && untrack(() => initLoadInputs()), (draft = false))
|
||||
@@ -222,6 +227,7 @@
|
||||
{runnableId}
|
||||
{runnableType}
|
||||
args={previewArgs ?? {}}
|
||||
workspace={ws}
|
||||
disabled={!previewArgs || !runnableId || !isValid || jsonView}
|
||||
on:update={() => {
|
||||
refresh()
|
||||
|
||||
@@ -121,6 +121,8 @@
|
||||
actions: actions_render = undefined
|
||||
}: Props = $props()
|
||||
|
||||
let ws = $derived(workspace ?? $workspaceStore)
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let inputCheck: { [id: string]: boolean } = $state({})
|
||||
@@ -502,7 +504,7 @@
|
||||
documentationLink="https://www.windmill.dev/docs/core_concepts/variables_and_secrets"
|
||||
extraField="path"
|
||||
loadItems={async () =>
|
||||
(await VariableService.listVariable({ workspace: $workspaceStore ?? '' })).map((x) => ({
|
||||
(await VariableService.listVariable({ workspace: ws ?? '' })).map((x) => ({
|
||||
name: x.path,
|
||||
...x
|
||||
}))}
|
||||
@@ -521,5 +523,5 @@
|
||||
{/snippet}
|
||||
</ItemPicker>
|
||||
|
||||
<VariableEditor bind:this={variableEditor} />
|
||||
<VariableEditor bind:this={variableEditor} workspace={ws} />
|
||||
{/if}
|
||||
|
||||
@@ -19,6 +19,9 @@
|
||||
isValid?: boolean
|
||||
jsonView?: boolean
|
||||
children?: import('svelte').Snippet
|
||||
/** Workspace the History / Saved inputs / Captures tabs read and mutate;
|
||||
* defaults to the nav workspace. Session editors pass their acting workspace. */
|
||||
workspace?: string
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -28,7 +31,8 @@
|
||||
previewArgs,
|
||||
isValid = true,
|
||||
jsonView = false,
|
||||
children
|
||||
children,
|
||||
workspace = undefined
|
||||
}: Props = $props()
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
@@ -140,6 +144,7 @@
|
||||
bind:this={historicInputs}
|
||||
{runnableId}
|
||||
{runnableType}
|
||||
{workspace}
|
||||
on:select={(e) => {
|
||||
dispatch('select', { payload: e.detail?.args, type: 'history' })
|
||||
}}
|
||||
@@ -152,6 +157,7 @@
|
||||
{runnableId}
|
||||
{runnableType}
|
||||
{previewArgs}
|
||||
{workspace}
|
||||
bind:this={savedInputsPicker}
|
||||
on:select={(e) => {
|
||||
dispatch('select', { payload: e.detail, type: 'saved' })
|
||||
@@ -171,6 +177,7 @@
|
||||
<div class="h-full">
|
||||
<CaptureTable
|
||||
path={stablePathForCaptures}
|
||||
{workspace}
|
||||
on:select={(e) => {
|
||||
dispatch('select', { payload: e.detail, type: 'captures' })
|
||||
}}
|
||||
|
||||
@@ -1151,6 +1151,7 @@
|
||||
autofocus={false}
|
||||
namePlaceholder="script"
|
||||
kind="script"
|
||||
workspaceOverride={opWorkspace}
|
||||
/>
|
||||
{#if initialPath && script.path && script.path !== initialPath}
|
||||
<Alert
|
||||
@@ -2082,6 +2083,7 @@
|
||||
|
||||
<ScriptEditor
|
||||
{disableAi}
|
||||
workspaceOverride={opWorkspace}
|
||||
sessionOpen={script.path
|
||||
? {
|
||||
target: { kind: 'script', path: script.path },
|
||||
|
||||
@@ -223,6 +223,11 @@
|
||||
// built by the pipeline page from the resolved graph. Absent outside the
|
||||
// pipeline editor — the check still runs, just without suppression.
|
||||
schemaContractContext?: SchemaContractGraphContext
|
||||
// Workspace to scope this editor's calls to. Defaults to the nav
|
||||
// `$workspaceStore`; an AI-session live editor passes the session's
|
||||
// acting workspace (a fork) so tests, captures and toolbar lookups hit
|
||||
// the right workspace instead of the nav one.
|
||||
workspaceOverride?: string
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -266,9 +271,12 @@
|
||||
onTestJob,
|
||||
initialTestPanelCollapsed = false,
|
||||
sessionOpen = undefined,
|
||||
schemaContractContext = undefined
|
||||
schemaContractContext = undefined,
|
||||
workspaceOverride = undefined
|
||||
}: Props = $props()
|
||||
|
||||
let opWs = $derived(workspaceOverride ?? $workspaceStore)
|
||||
|
||||
$effect(() => {
|
||||
onTestStateChange?.(testIsLoading)
|
||||
})
|
||||
@@ -596,7 +604,7 @@
|
||||
let inferAssetsRes = resource([() => lang, () => code, () => code], () => inferAssets(lang, code))
|
||||
let preparedSqlQueries = usePreparedAssetSqlQueries(
|
||||
() => inferAssetsRes.current?.sql_queries,
|
||||
() => $workspaceStore
|
||||
() => opWs
|
||||
)
|
||||
// Asset-parse validity for the editor badge. `undefined` while loading (so
|
||||
// the badge doesn't flicker red); only an explicit parser error counts as
|
||||
@@ -648,7 +656,7 @@
|
||||
let contractCheckSeq = 0
|
||||
watch([() => inferAssetsRes.current, () => schemaContractContext], () => {
|
||||
const res = inferAssetsRes.current
|
||||
const workspace = $workspaceStore
|
||||
const workspace = opWs
|
||||
const seq = ++contractCheckSeq
|
||||
if (!workspace || !res || res.status === 'error') {
|
||||
contractMarkers = []
|
||||
@@ -837,7 +845,7 @@
|
||||
? { _ENTRYPOINT_OVERRIDE: 'preprocessor', ...(args ?? {}) }
|
||||
: (args ?? {})
|
||||
const testSchema = activeModuleTab !== null ? testPanelSchema : schema
|
||||
const testArgs = await processSecretArgs(rawTestArgs, testSchema)
|
||||
const testArgs = await processSecretArgs(rawTestArgs, testSchema, opWs)
|
||||
if (showPsCommonParams) {
|
||||
for (const [k, v] of Object.entries(psCommonParams)) {
|
||||
if (v !== undefined && v !== false && v !== '') {
|
||||
@@ -908,7 +916,7 @@
|
||||
async function loadPastTests(): Promise<void> {
|
||||
pastPreviewsRequest?.cancel()
|
||||
const req = JobService.listCompletedJobs({
|
||||
workspace: $workspaceStore!,
|
||||
workspace: opWs!,
|
||||
jobKinds: 'preview',
|
||||
createdBy: $userStore?.username,
|
||||
scriptPathExact: path,
|
||||
@@ -1210,12 +1218,12 @@
|
||||
dapClient = getDAPClient(dapServerUrl)
|
||||
|
||||
// Fetch contextual variables (WM_WORKSPACE, WM_TOKEN, etc.) from backend
|
||||
const env = await fetchContextualVariables($workspaceStore ?? '')
|
||||
const env = await fetchContextualVariables(opWs ?? '')
|
||||
|
||||
// Sign the debug request (creates audit log entry)
|
||||
let signedPayload
|
||||
try {
|
||||
signedPayload = await signDebugRequest($workspaceStore ?? '', code ?? '', lang ?? 'python3')
|
||||
signedPayload = await signDebugRequest(opWs ?? '', code ?? '', lang ?? 'python3')
|
||||
debugSessionJobId = signedPayload.job_id
|
||||
} catch (signError) {
|
||||
sendUserToast(getDebugErrorMessage(signError), true)
|
||||
@@ -1435,11 +1443,11 @@
|
||||
// what's there" affordance, not a user action. Skipped when a test is
|
||||
// already running so a live job's stream is never clobbered.
|
||||
async function loadLastRunIntoTestPanel(): Promise<void> {
|
||||
if (!path || !$workspaceStore) return
|
||||
if (!path || !opWs) return
|
||||
if (testIsLoading || testJob !== undefined) return
|
||||
try {
|
||||
const jobs = await JobService.listCompletedJobs({
|
||||
workspace: $workspaceStore,
|
||||
workspace: opWs,
|
||||
scriptPathExact: path,
|
||||
hasNullParent: true,
|
||||
perPage: 1,
|
||||
@@ -1471,7 +1479,7 @@
|
||||
|
||||
let token: string | undefined
|
||||
try {
|
||||
token = await signMultiplayerRequest($workspaceStore ?? '')
|
||||
token = await signMultiplayerRequest(opWs ?? '')
|
||||
} catch (e) {
|
||||
console.error('Failed to sign multiplayer request:', e)
|
||||
sendUserToast('Failed to authorize multiplayer session', true)
|
||||
@@ -1486,7 +1494,7 @@
|
||||
|
||||
wsProvider = new WebsocketProvider(
|
||||
buildWsUrl('/ws_mp/'),
|
||||
$workspaceStore + '/' + (path ?? 'no-room-name'),
|
||||
opWs + '/' + (path ?? 'no-room-name'),
|
||||
ydoc,
|
||||
{ connect: false, params: { token } }
|
||||
)
|
||||
@@ -1554,7 +1562,7 @@
|
||||
let url = new URL(window.location.toString().split('#')[0])
|
||||
url.search = ''
|
||||
return (
|
||||
`${url}?collab=1&workspace=${encodeURIComponent($workspaceStore ?? '')}&lang=${encodeURIComponent(lang ?? '')}` +
|
||||
`${url}?collab=1&workspace=${encodeURIComponent(opWs ?? '')}&lang=${encodeURIComponent(lang ?? '')}` +
|
||||
(edit ? '' : `&path=${path}`)
|
||||
)
|
||||
}
|
||||
@@ -1725,6 +1733,7 @@
|
||||
|
||||
<JobLoader
|
||||
noCode={true}
|
||||
workspaceOverride={opWs}
|
||||
bind:scriptProgress
|
||||
bind:this={jobLoader}
|
||||
bind:isLoading={testIsLoading}
|
||||
@@ -1760,6 +1769,7 @@
|
||||
<div class="flex justify-between space-x-2">
|
||||
{#if args}
|
||||
<EditorBar
|
||||
workspace={opWs}
|
||||
scriptPath={edit ? path : undefined}
|
||||
on:toggleCollabMode={() => {
|
||||
if (wsProvider?.shouldConnect) {
|
||||
@@ -1978,7 +1988,7 @@
|
||||
it visually pinned to the top edge without
|
||||
relying on cross-browser overflow behaviour. -->
|
||||
<div class="relative h-full pt-9 flex flex-col">
|
||||
{#if testJob?.id && testJob.type === 'CompletedJob' && $workspaceStore}
|
||||
{#if testJob?.id && testJob.type === 'CompletedJob' && opWs}
|
||||
<!-- Right-side affordances when we're displaying a *completed*
|
||||
job (either the user just ran a test, or the on-mount
|
||||
last-run loader populated the panel). The job-id link
|
||||
@@ -1989,7 +1999,7 @@
|
||||
<div class="absolute top-1 right-2 z-10 flex items-center gap-2">
|
||||
<a
|
||||
class="text-3xs text-blue-600 hover:underline font-mono"
|
||||
href={`${base}/run/${testJob.id}?workspace=${$workspaceStore}`}
|
||||
href={`${base}/run/${testJob.id}?workspace=${opWs}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
title="Open this run"
|
||||
@@ -1997,7 +2007,7 @@
|
||||
{testJob.id.slice(0, 8)}… ↗
|
||||
</a>
|
||||
<DispatchEventsButton
|
||||
workspace={testJob.workspace_id ?? $workspaceStore}
|
||||
workspace={testJob.workspace_id ?? opWs}
|
||||
jobId={testJob.id}
|
||||
/>
|
||||
</div>
|
||||
@@ -2142,6 +2152,7 @@
|
||||
>
|
||||
{#key argsRender}
|
||||
<SchemaForm
|
||||
workspace={opWs}
|
||||
helperScript={{
|
||||
source: 'inline',
|
||||
code,
|
||||
@@ -2211,6 +2222,7 @@
|
||||
{#key argsRender}
|
||||
{#if activeModuleTab !== null}
|
||||
<SchemaForm
|
||||
workspace={opWs}
|
||||
helperScript={{
|
||||
source: 'inline',
|
||||
code: editorCode,
|
||||
@@ -2227,6 +2239,7 @@
|
||||
/>
|
||||
{:else}
|
||||
<SchemaForm
|
||||
workspace={opWs}
|
||||
helperScript={{
|
||||
source: 'inline',
|
||||
code,
|
||||
@@ -2331,6 +2344,7 @@
|
||||
<div class="h-full p-2">
|
||||
<CaptureTable
|
||||
bind:this={captureTable}
|
||||
workspace={opWs}
|
||||
{hasPreprocessor}
|
||||
canHavePreprocessor={canHavePreprocessor(lang)}
|
||||
isFlow={false}
|
||||
@@ -2649,7 +2663,7 @@
|
||||
client={dapClient}
|
||||
currentFrameId={currentDebugFrameId}
|
||||
onClose={() => (showDebugConsole = false)}
|
||||
workspace={$workspaceStore}
|
||||
workspace={opWs}
|
||||
jobId={debugSessionJobId ?? undefined}
|
||||
/>
|
||||
</Pane>
|
||||
|
||||
@@ -21,8 +21,18 @@
|
||||
/** Called after a migration is run via the DDL guard, so the schema view
|
||||
* can be refreshed to reflect the applied change. */
|
||||
onSchemaChange?: () => void
|
||||
/** Workspace the REPL queries and DDL migrations run against; defaults to
|
||||
* the nav workspace. */
|
||||
workspace?: string | undefined
|
||||
}
|
||||
let { input, onData, placeholderTableName, onSchemaChange }: Props = $props()
|
||||
let {
|
||||
input,
|
||||
onData,
|
||||
placeholderTableName,
|
||||
onSchemaChange,
|
||||
workspace = undefined
|
||||
}: Props = $props()
|
||||
let ws = $derived(workspace ?? $workspaceStore)
|
||||
let dbType = $derived(getDbType(input))
|
||||
|
||||
// A datatable REPL targets `datatable://<name>`; surface DDL statements as
|
||||
@@ -47,7 +57,7 @@
|
||||
let runHistory: (StepHistoryData & { code: string; result: Record<string, any>[] })[] = $state([])
|
||||
|
||||
async function run({ doPostgresRowToJsonFix }: { doPostgresRowToJsonFix?: boolean } = {}) {
|
||||
if (isRunning || !$workspaceStore) return
|
||||
if (isRunning || !ws) return
|
||||
const READ_OPS = ['SELECT', 'WITH', 'SHOW', 'EXPLAIN', 'DESCRIBE']
|
||||
|
||||
// On a datatable, intercept DDL statements and offer to make them
|
||||
@@ -91,7 +101,7 @@
|
||||
|
||||
let { job, result } = (await runScriptAndPollResult(
|
||||
{
|
||||
workspace: $workspaceStore,
|
||||
workspace: ws,
|
||||
requestBody: {
|
||||
language: getLanguageByResourceType(dbType),
|
||||
content: transformedCode,
|
||||
@@ -181,6 +191,6 @@
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
|
||||
{#if datatableName && $workspaceStore}
|
||||
<DdlMigrationGuard bind:this={ddlGuard} workspace={$workspaceStore} datatable={datatableName} />
|
||||
{#if datatableName && ws}
|
||||
<DdlMigrationGuard bind:this={ddlGuard} workspace={ws} datatable={datatableName} />
|
||||
{/if}
|
||||
|
||||
@@ -29,6 +29,12 @@
|
||||
wsSpecific: boolean
|
||||
}
|
||||
|
||||
// The "current" workspace this editor defaults New/Edit actions to. Session
|
||||
// editors pass their acting workspace so secrets are created/updated there
|
||||
// rather than in the navigation workspace. Defaults to $workspaceStore.
|
||||
let { workspace = undefined }: { workspace?: string } = $props()
|
||||
let curWs = $derived(workspace ?? $workspaceStore)
|
||||
|
||||
let editPath: string | undefined = $state(undefined)
|
||||
|
||||
// Per-workspace handles are driven by `useMany`. We track the workspace
|
||||
@@ -119,9 +125,7 @@
|
||||
// that case.
|
||||
const selectedDirty = $derived(!!selected && dirtyWorkspaces.includes(selected))
|
||||
const otherDirty = $derived(
|
||||
dirtyWorkspaces.length == 1
|
||||
? dirtyWorkspaces.filter((ws) => ws !== $workspaceStore)
|
||||
: dirtyWorkspaces
|
||||
dirtyWorkspaces.length == 1 ? dirtyWorkspaces.filter((ws) => ws !== curWs) : dirtyWorkspaces
|
||||
)
|
||||
const dirtyValid = $derived(
|
||||
dirtyWorkspaces.every((ws) => {
|
||||
@@ -198,7 +202,7 @@
|
||||
export function initNew(): void {
|
||||
reset()
|
||||
editPath = undefined
|
||||
const ws = $workspaceStore!
|
||||
const ws = curWs!
|
||||
const s: VariableState = {
|
||||
path: '',
|
||||
variable: { value: '', is_secret: true, description: '' },
|
||||
@@ -215,7 +219,7 @@
|
||||
export function editVariable(edit_path: string): void {
|
||||
reset()
|
||||
editPath = edit_path
|
||||
selected = $workspaceStore!
|
||||
selected = curWs!
|
||||
drawer?.openDrawer()
|
||||
}
|
||||
|
||||
@@ -337,18 +341,14 @@
|
||||
{can_write}
|
||||
{edit}
|
||||
onLoadSecret={loadSecret}
|
||||
{workspace}
|
||||
/>
|
||||
{/key}
|
||||
{/if}
|
||||
</div>
|
||||
{#snippet actions()}
|
||||
{#if edit && $workspaceStore}
|
||||
<WsSpecificVersions
|
||||
kind="variable"
|
||||
workspaceId={$workspaceStore}
|
||||
{initialPath}
|
||||
bind:selected
|
||||
/>
|
||||
{#if edit && curWs}
|
||||
<WsSpecificVersions kind="variable" workspaceId={curWs} {initialPath} bind:selected />
|
||||
{/if}
|
||||
<Button
|
||||
on:click={save}
|
||||
|
||||
@@ -32,6 +32,8 @@
|
||||
can_write: boolean
|
||||
edit: boolean
|
||||
onLoadSecret?: () => void
|
||||
/** Workspace the path is validated against; defaults to the nav workspace. */
|
||||
workspace?: string | undefined
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -44,9 +46,12 @@
|
||||
deployTo,
|
||||
can_write,
|
||||
edit,
|
||||
onLoadSecret
|
||||
onLoadSecret,
|
||||
workspace = undefined
|
||||
}: Props = $props()
|
||||
|
||||
let ws = $derived(workspace ?? $workspaceStore)
|
||||
|
||||
const MAX_VARIABLE_LENGTH = 10000
|
||||
|
||||
let editorKind: 'plain' | 'json' | 'yaml' = $state('plain')
|
||||
@@ -60,12 +65,13 @@
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="path" class="text-xs font-semibold text-emphasis">Path</label>
|
||||
<Path
|
||||
disabled={initialPath != '' && !isOwner(initialPath, $userStore, $workspaceStore)}
|
||||
disabled={initialPath != '' && !isOwner(initialPath, $userStore, ws)}
|
||||
bind:error={pathError}
|
||||
bind:path
|
||||
{initialPath}
|
||||
namePlaceholder="variable"
|
||||
kind="variable"
|
||||
workspaceOverride={workspace}
|
||||
/>
|
||||
<LabelsInput bind:labels />
|
||||
</div>
|
||||
|
||||
@@ -41,7 +41,8 @@
|
||||
preserveOnBehalfOf = $bindable(false),
|
||||
labels = $bindable(),
|
||||
rawApp = false,
|
||||
newApp = false
|
||||
newApp = false,
|
||||
operatingWorkspace = undefined
|
||||
}: {
|
||||
policy: any
|
||||
setPublishState: (message?: string) => void
|
||||
@@ -68,8 +69,15 @@
|
||||
* (`/secret_of/...` 404s with no `app` row) and renders a placeholder
|
||||
* instead of the eternally-spinning link. */
|
||||
newApp?: boolean
|
||||
/** Workspace the app is deployed to — the session's acting workspace when
|
||||
* embedded in a session preview, else the navigation `$workspaceStore`.
|
||||
* The secret-URL / custom-path / folder / on-behalf-of lookups must target
|
||||
* it, not `$workspaceStore` (which stays on the nav workspace in a session). */
|
||||
operatingWorkspace?: string
|
||||
} = $props()
|
||||
|
||||
const opWs = $derived(operatingWorkspace ?? $workspaceStore)
|
||||
|
||||
let isDeployer = $derived($userStore?.groups?.includes(WM_DEPLOYERS_GROUP) ?? false)
|
||||
// Admins always pass the backend check. For everyone else, fail closed
|
||||
// while the workspace protection rules are still loading so the toggle
|
||||
@@ -93,7 +101,7 @@
|
||||
|
||||
async function appExists(customPath: string) {
|
||||
return await AppService.customPathExists({
|
||||
workspace: $workspaceStore!,
|
||||
workspace: opWs!,
|
||||
customPath
|
||||
})
|
||||
}
|
||||
@@ -115,7 +123,7 @@
|
||||
let secretUrlHref = $derived(secretUrl ? computeSecretUrl(secretUrl) : undefined)
|
||||
let fullCustomUrl = $derived(
|
||||
`${window.location.origin}${base}/a/${
|
||||
isCloudHosted() || globalWorkspacedRoute ? $workspaceStore + '/' : ''
|
||||
isCloudHosted() || globalWorkspacedRoute ? opWs + '/' : ''
|
||||
}${customPath}`
|
||||
)
|
||||
|
||||
@@ -131,7 +139,7 @@
|
||||
}
|
||||
async function getSecretUrl() {
|
||||
secretUrl = await AppService.getPublicSecretOfApp({
|
||||
workspace: $workspaceStore!,
|
||||
workspace: opWs!,
|
||||
path: appPath
|
||||
})
|
||||
}
|
||||
@@ -229,6 +237,7 @@
|
||||
namePlaceholder="app"
|
||||
kind="app"
|
||||
autofocus={false}
|
||||
workspaceOverride={operatingWorkspace}
|
||||
/>
|
||||
|
||||
<div class="py-2"></div>
|
||||
@@ -247,7 +256,7 @@
|
||||
Because you are either an admin or part of the {WM_DEPLOYERS_GROUP} group, you can select another
|
||||
user to run this app on behalf of. Once deployed the app will be run on behalf of
|
||||
<OnBehalfOfSelector
|
||||
targetWorkspace={$workspaceStore ?? ''}
|
||||
targetWorkspace={opWs ?? ''}
|
||||
targetValue={savedOnBehalfOfEmail}
|
||||
selected={onBehalfOfChoice}
|
||||
onSelect={(choice, details) => {
|
||||
|
||||
@@ -108,7 +108,10 @@ export interface DbManagerUriState {
|
||||
selectedSchema: string | undefined
|
||||
selectedTable: string | undefined
|
||||
readonly open: boolean
|
||||
openDrawer: (nInput: DbInput) => void
|
||||
/** Workspace the drawer's DB operations run against — the acting workspace of
|
||||
* the editor that opened it, else the navigation workspace. */
|
||||
workspace: string | undefined
|
||||
openDrawer: (nInput: DbInput, workspace?: string) => void
|
||||
closeDrawer: () => void
|
||||
}
|
||||
|
||||
@@ -148,7 +151,12 @@ export function useDbManagerUriState(): DbManagerUriState {
|
||||
params.dbm = buildDbm(p)
|
||||
}
|
||||
|
||||
function openDrawer(nInput: DbInput) {
|
||||
// Not URL-persisted: the drawer defaults back to the nav workspace on reload,
|
||||
// which is the correct fallback outside the session that opened it.
|
||||
let workspace = $state<string | undefined>(undefined)
|
||||
|
||||
function openDrawer(nInput: DbInput, ws?: string) {
|
||||
workspace = ws
|
||||
if (nInput.type === 'database') {
|
||||
const isDatatable = nInput.resourcePath.startsWith('datatable://')
|
||||
params.dbm = buildDbm({
|
||||
@@ -203,6 +211,12 @@ export function useDbManagerUriState(): DbManagerUriState {
|
||||
get open() {
|
||||
return !!input
|
||||
},
|
||||
get workspace() {
|
||||
return workspace
|
||||
},
|
||||
set workspace(v: string | undefined) {
|
||||
workspace = v
|
||||
},
|
||||
openDrawer,
|
||||
closeDrawer
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
let s = $state({
|
||||
val: {
|
||||
selectedAsset: undefined,
|
||||
workspace: undefined,
|
||||
s3FilePicker: undefined,
|
||||
resourceEditorDrawer: undefined,
|
||||
resourceMetadataCache: {},
|
||||
@@ -61,7 +62,13 @@
|
||||
} = $props()
|
||||
|
||||
const flowGraphAssetsCtx = getContext<FlowGraphAssetContext | undefined>('FlowGraphAssetContext')
|
||||
const { selectionManager } = getContext<FlowEditorContext>('FlowEditorContext') || {}
|
||||
const { selectionManager, opWorkspace } = getContext<FlowEditorContext>('FlowEditorContext') || {}
|
||||
let opWs = $derived(opWorkspace?.() ?? $workspaceStore)
|
||||
// Expose the acting workspace to the asset explore controls (ExploreAssetButton
|
||||
// reads it from this context; the DB manager / S3 picker act on it).
|
||||
$effect(() => {
|
||||
if (flowGraphAssetsCtx) flowGraphAssetsCtx.val.workspace = opWs
|
||||
})
|
||||
let selectedId = $derived(selectionManager?.getSelectedId())
|
||||
|
||||
let allModules = $derived(getAllModules(modules))
|
||||
@@ -79,7 +86,7 @@
|
||||
let truncatedPath = asset.path.split('?table=')[0]
|
||||
if (truncatedPath in resMetadataCache) continue
|
||||
resMetadataCache[truncatedPath] = undefined // avoid fetching multiple times because of async
|
||||
ResourceService.getResource({ path: truncatedPath, workspace: $workspaceStore! })
|
||||
ResourceService.getResource({ path: truncatedPath, workspace: opWs! })
|
||||
.then((r) => (resMetadataCache[truncatedPath] = { resource_type: r.resource_type }))
|
||||
.catch((err) => console.error("Couldn't fetch resource", truncatedPath, err))
|
||||
}
|
||||
@@ -88,7 +95,7 @@
|
||||
|
||||
// Fetch transitive assets (path scripts and flows)
|
||||
$effect(() => {
|
||||
if (!$workspaceStore || !flowGraphAssetsCtx || !enablePathScriptAndFlowAssets) return
|
||||
if (!opWs || !flowGraphAssetsCtx || !enablePathScriptAndFlowAssets) return
|
||||
let usages: { path: string; kind: AssetUsageKind }[] = []
|
||||
let modIds: string[] = []
|
||||
for (const mod of allModules) {
|
||||
@@ -101,7 +108,7 @@
|
||||
}
|
||||
if (usages.length) {
|
||||
AssetService.listAssetsByUsage({
|
||||
workspace: $workspaceStore,
|
||||
workspace: opWs,
|
||||
requestBody: { usages }
|
||||
}).then((result) => {
|
||||
result.forEach((assets, idx) => {
|
||||
@@ -182,6 +189,6 @@
|
||||
</script>
|
||||
|
||||
{#if flowGraphAssetsCtx}
|
||||
<S3FilePicker bind:this={flowGraphAssetsCtx.val.s3FilePicker} readOnlyMode />
|
||||
<ResourceEditorDrawer bind:this={flowGraphAssetsCtx.val.resourceEditorDrawer} />
|
||||
<S3FilePicker bind:this={flowGraphAssetsCtx.val.s3FilePicker} workspace={opWs} readOnlyMode />
|
||||
<ResourceEditorDrawer bind:this={flowGraphAssetsCtx.val.resourceEditorDrawer} workspace={opWs} />
|
||||
{/if}
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
import { Skeleton } from '$lib/components/common'
|
||||
import Button from '../common/button/Button.svelte'
|
||||
import { ArrowRight, Loader2, Pencil, X } from 'lucide-svelte'
|
||||
import { getContext } from 'svelte'
|
||||
import type { FlowEditorContext } from './types'
|
||||
|
||||
interface Props {
|
||||
path: string
|
||||
@@ -17,6 +19,10 @@
|
||||
}
|
||||
|
||||
let { path, allowFork = false, onHistoryRestore }: Props = $props()
|
||||
|
||||
const flowEditorContext = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
let opWs = $derived(flowEditorContext?.opWorkspace?.() ?? $workspaceStore)
|
||||
|
||||
let loading: boolean = $state(false)
|
||||
|
||||
let versions: FlowVersion[] = $state([])
|
||||
@@ -29,7 +35,7 @@
|
||||
|
||||
async function loadFlow(version: number) {
|
||||
selected = await FlowService.getFlowVersion({
|
||||
workspace: $workspaceStore!,
|
||||
workspace: opWs!,
|
||||
version
|
||||
})
|
||||
}
|
||||
@@ -37,7 +43,7 @@
|
||||
async function loadVersions() {
|
||||
loading = true
|
||||
versions = await FlowService.getFlowHistory({
|
||||
workspace: $workspaceStore!,
|
||||
workspace: opWs!,
|
||||
path: path
|
||||
})
|
||||
loading = false
|
||||
@@ -52,7 +58,7 @@
|
||||
return
|
||||
}
|
||||
await FlowService.updateFlowHistory({
|
||||
workspace: $workspaceStore!,
|
||||
workspace: opWs!,
|
||||
version,
|
||||
requestBody: {
|
||||
deployment_msg: deploymentMsgUpdate!
|
||||
@@ -66,7 +72,7 @@
|
||||
async function restoreVersion(flow: Flow | undefined) {
|
||||
if (!flow) return
|
||||
await FlowService.updateFlow({
|
||||
workspace: $workspaceStore!,
|
||||
workspace: opWs!,
|
||||
requestBody: {
|
||||
...flow,
|
||||
path
|
||||
|
||||
@@ -16,7 +16,8 @@
|
||||
import { ScriptService, type FlowModuleValue, type PathScript } from '$lib/gen'
|
||||
import { hubBaseUrlStore, workspaceStore } from '$lib/stores'
|
||||
import { Flag, Lock, RefreshCw, Unlock } from 'lucide-svelte'
|
||||
import { createEventDispatcher, untrack } from 'svelte'
|
||||
import { createEventDispatcher, getContext, untrack } from 'svelte'
|
||||
import type { FlowEditorContext } from '../types'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { getToolNameError } from '$lib/components/graph/renderers/nodes/AIToolNode.svelte'
|
||||
import { DEFAULT_HUB_BASE_URL, PRIVATE_HUB_MIN_VERSION } from '$lib/hub'
|
||||
@@ -47,6 +48,9 @@
|
||||
|
||||
let latestHash: string | undefined = $state(undefined)
|
||||
|
||||
const flowEditorContext = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
let opWs = $derived(flowEditorContext?.opWorkspace?.() ?? $workspaceStore)
|
||||
|
||||
// Extract version_id from hub path (format: hub/{version_id}/{app}/{summary})
|
||||
let hubVersionId = $derived(
|
||||
flowModuleValue?.type === 'script' && flowModuleValue.path?.startsWith('hub/')
|
||||
@@ -55,7 +59,7 @@
|
||||
)
|
||||
|
||||
function getCachedKey(path: string) {
|
||||
return `${$workspaceStore}-${path}`
|
||||
return `${opWs}-${path}`
|
||||
}
|
||||
function getCachedValues(path: string) {
|
||||
const key = getCachedKey(path)
|
||||
@@ -68,7 +72,7 @@
|
||||
|
||||
async function loadLatestHash(value: PathScript) {
|
||||
let script = await ScriptService.getScriptByPath({
|
||||
workspace: $workspaceStore!,
|
||||
workspace: opWs!,
|
||||
path: value.path
|
||||
})
|
||||
const key = getCachedKey(value.path)
|
||||
|
||||
@@ -7,15 +7,19 @@
|
||||
import { ExternalLink, Loader2 } from 'lucide-svelte'
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
import { emptySchema, type StateStore } from '$lib/utils'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { createEventDispatcher, getContext } from 'svelte'
|
||||
import { fade } from 'svelte/transition'
|
||||
import { initFlow } from '$lib/components/flows/flowStore.svelte'
|
||||
import type { FlowState } from '$lib/components/flows/flowState'
|
||||
import type { FlowEditorContext } from '../types'
|
||||
|
||||
let flowEditorDrawer: Drawer | undefined = $state()
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const flowEditorContext = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
let opWs = $derived(flowEditorContext?.opWorkspace?.() ?? $workspaceStore)
|
||||
|
||||
export async function openDrawer(path: string, cb: () => void): Promise<void> {
|
||||
flowPath = path
|
||||
flow = undefined
|
||||
@@ -25,7 +29,7 @@
|
||||
|
||||
try {
|
||||
const backendFlow = await FlowService.getFlowByPath({
|
||||
workspace: $workspaceStore!,
|
||||
workspace: opWs!,
|
||||
path
|
||||
})
|
||||
|
||||
@@ -86,6 +90,7 @@
|
||||
{flowStore}
|
||||
{flowStateStore}
|
||||
initialPath={flowPath}
|
||||
autosaveWorkspace={opWs}
|
||||
newFlow={false}
|
||||
selectedId="settings-metadata"
|
||||
loading={false}
|
||||
|
||||
@@ -32,7 +32,8 @@
|
||||
|
||||
let { noEditor }: Props = $props()
|
||||
|
||||
const { flowStore } = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
const { flowStore, opWorkspace } = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
let opWs = $derived(opWorkspace?.() ?? $workspaceStore)
|
||||
|
||||
if (!flowStore.val.value.flow_env) {
|
||||
flowStore.val.value.flow_env = {}
|
||||
@@ -244,8 +245,10 @@
|
||||
Flow envs can be referenced in any flow step input using the syntax{' '}
|
||||
<code>flow_env.VARIABLE_NAME</code> or <code>flow_env["VARIABLE_NAME"]</code>. These
|
||||
variables are available in the property picker and can be used in JavaScript expressions and
|
||||
input bindings. String values can link to workspace variables using the <DollarSign size={12}
|
||||
class="inline" /> button. Resource type references workspace resources resolved at runtime.
|
||||
input bindings. String values can link to workspace variables using the <DollarSign
|
||||
size={12}
|
||||
class="inline"
|
||||
/> button. Resource type references workspace resources resolved at runtime.
|
||||
</Alert>
|
||||
|
||||
{#if flowEnvEntries.length === 0}
|
||||
@@ -303,6 +306,7 @@
|
||||
<ResourcePicker
|
||||
bind:value={resourcePaths[entry.key]}
|
||||
disabled={noEditor}
|
||||
workspace={opWs}
|
||||
/>
|
||||
{:else if entry.type === 'json'}
|
||||
<div class="w-full">
|
||||
@@ -320,8 +324,7 @@
|
||||
<input
|
||||
type="text"
|
||||
value={entry.displayValue}
|
||||
oninput={(e) =>
|
||||
updateEnvValue(entry.key, e.currentTarget.value, 'string')}
|
||||
oninput={(e) => updateEnvValue(entry.key, e.currentTarget.value, 'string')}
|
||||
disabled={noEditor}
|
||||
class="input w-full"
|
||||
placeholder="Variable value"
|
||||
@@ -346,8 +349,7 @@
|
||||
Linked to variable <a
|
||||
href="/variables#{entry.value.slice(5)}"
|
||||
target="_blank"
|
||||
class="text-accent underline font-normal"
|
||||
>{entry.value.slice(5)}</a
|
||||
class="text-accent underline font-normal">{entry.value.slice(5)}</a
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -379,7 +381,7 @@
|
||||
itemName="Variable"
|
||||
extraField="path"
|
||||
loadItems={async () =>
|
||||
(await VariableService.listVariable({ workspace: $workspaceStore ?? '' })).map((x) => ({
|
||||
(await VariableService.listVariable({ workspace: opWs ?? '' })).map((x) => ({
|
||||
name: x.path,
|
||||
...x
|
||||
}))}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import JsonInputs from '$lib/components/JsonInputs.svelte'
|
||||
import { convert } from '@redocly/json-to-json-schema'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import EditableSchemaForm from '$lib/components/EditableSchemaForm.svelte'
|
||||
import AddPropertyV2 from '$lib/components/schema/AddPropertyV2.svelte'
|
||||
import FlowInputViewer from '$lib/components/FlowInputViewer.svelte'
|
||||
@@ -74,8 +75,11 @@
|
||||
pathStore,
|
||||
initialPathStore,
|
||||
fakeInitialPath,
|
||||
flowInputEditorState
|
||||
flowInputEditorState,
|
||||
opWorkspace
|
||||
} = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
// Acting workspace when the flow editor runs in an AI session; else the nav workspace.
|
||||
let opWs = $derived(opWorkspace?.() ?? $workspaceStore)
|
||||
|
||||
// Get diffManager from the graph
|
||||
const diffManager = $derived(flowModuleSchemaMap?.getDiffManager())
|
||||
@@ -887,6 +891,7 @@
|
||||
>
|
||||
<HistoricInputs
|
||||
bind:this={historicInputs}
|
||||
workspace={opWs}
|
||||
runnableId={$initialPathStore ?? undefined}
|
||||
runnableType={$pathStore ? 'FlowPath' : undefined}
|
||||
on:select={(e) => {
|
||||
@@ -910,6 +915,7 @@
|
||||
<div class="h-full">
|
||||
<CaptureTable
|
||||
path={$initialPathStore || fakeInitialPath}
|
||||
workspace={opWs}
|
||||
on:select={(e) => {
|
||||
updatePreviewSchemaAndArgs(e.detail ?? undefined)
|
||||
}}
|
||||
@@ -929,6 +935,7 @@
|
||||
title="Saved inputs"
|
||||
>
|
||||
<SavedInputsPicker
|
||||
workspace={opWs}
|
||||
runnableId={$initialPathStore ?? undefined}
|
||||
runnableType={$pathStore ? 'FlowPath' : undefined}
|
||||
on:select={(e) => {
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { emptyString } from '$lib/utils'
|
||||
|
||||
import { createEventDispatcher, untrack } from 'svelte'
|
||||
import { createEventDispatcher, getContext, untrack } from 'svelte'
|
||||
import type { FlowEditorContext } from '../types'
|
||||
import { flip } from 'svelte/animate'
|
||||
import { fade } from 'svelte/transition'
|
||||
interface Props {
|
||||
@@ -15,6 +16,9 @@
|
||||
|
||||
let { children }: Props = $props()
|
||||
|
||||
const flowEditorContext = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
let opWs = $derived(flowEditorContext?.opWorkspace?.() ?? $workspaceStore)
|
||||
|
||||
// export let failureModule: boolean
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@@ -25,10 +29,10 @@
|
||||
let ownerFilter: string | undefined = $state(undefined)
|
||||
|
||||
async function loadFlows() {
|
||||
items = await FlowService.listFlows({ workspace: $workspaceStore!, withoutDescription: true })
|
||||
items = await FlowService.listFlows({ workspace: opWs!, withoutDescription: true })
|
||||
}
|
||||
$effect(() => {
|
||||
$workspaceStore && untrack(() => loadFlows())
|
||||
opWs && untrack(() => loadFlows())
|
||||
})
|
||||
let prefilteredItems = $derived(
|
||||
ownerFilter ? items?.filter((x) => x.path.startsWith(ownerFilter!)) : items
|
||||
|
||||
@@ -94,10 +94,12 @@
|
||||
pathStore,
|
||||
saveDraft,
|
||||
customUi,
|
||||
executionCount
|
||||
executionCount,
|
||||
opWorkspace
|
||||
} = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
|
||||
const selectedId = $derived(selectionManager.getSelectedId())
|
||||
let opWs = $derived(opWorkspace?.() ?? $workspaceStore)
|
||||
|
||||
interface Props {
|
||||
flowModule: FlowModule
|
||||
@@ -398,7 +400,7 @@
|
||||
|
||||
let preparedSqlQueries = usePreparedAssetSqlQueries(
|
||||
() => flowGraphAssetsCtx?.val.sqlQueries[selectedId],
|
||||
() => $workspaceStore
|
||||
() => opWs
|
||||
)
|
||||
|
||||
// Debug mode state
|
||||
@@ -532,16 +534,12 @@
|
||||
resetDAPClient()
|
||||
dapClient = getDAPClient(dapServerUrl)
|
||||
|
||||
const env = await fetchContextualVariables($workspaceStore ?? '')
|
||||
const env = await fetchContextualVariables(opWs ?? '')
|
||||
const code = flowModule.value.content
|
||||
|
||||
let signedPayload
|
||||
try {
|
||||
signedPayload = await signDebugRequest(
|
||||
$workspaceStore ?? '',
|
||||
code ?? '',
|
||||
rawScriptLang ?? 'python3'
|
||||
)
|
||||
signedPayload = await signDebugRequest(opWs ?? '', code ?? '', rawScriptLang ?? 'python3')
|
||||
debugSessionJobId = signedPayload.job_id
|
||||
} catch (signError) {
|
||||
sendUserToast(getDebugErrorMessage(signError), true)
|
||||
@@ -759,14 +757,14 @@
|
||||
on:toggleCache={() => selectAdvanced('cache')}
|
||||
on:toggleStopAfterIf={() => selectAdvanced('early-stop')}
|
||||
on:fork={async () => {
|
||||
const [module, state] = await fork(flowModule)
|
||||
const [module, state] = await fork(flowModule, opWs)
|
||||
flowModule = module
|
||||
flowStateStore.val[module.id] = state
|
||||
}}
|
||||
on:reload={async () => {
|
||||
if (flowModule.value.type == 'script') {
|
||||
if (flowModule.value.hash != undefined) {
|
||||
flowModule.value.hash = await getLatestHashForScript(flowModule.value.path)
|
||||
flowModule.value.hash = await getLatestHashForScript(flowModule.value.path, opWs)
|
||||
}
|
||||
forceReload++
|
||||
await reload(flowModule)
|
||||
@@ -781,7 +779,8 @@
|
||||
flowModule,
|
||||
selectedId,
|
||||
flowStateStore.val[flowModule.id]?.schema,
|
||||
$pathStore
|
||||
$pathStore,
|
||||
opWs
|
||||
)
|
||||
if (flowModule.value.type == 'rawscript') {
|
||||
module.value.input_transforms = flowModule.value.input_transforms
|
||||
@@ -797,6 +796,7 @@
|
||||
<div class="shadow-sm px-1 border-b-1 border-gray-200 dark:border-gray-700">
|
||||
<EditorBar
|
||||
customUi={customUi?.editorBar}
|
||||
workspace={opWs}
|
||||
{validCode}
|
||||
{editor}
|
||||
lang={flowModule.value['language'] ?? 'deno'}
|
||||
@@ -903,7 +903,7 @@
|
||||
},
|
||||
{}
|
||||
)}
|
||||
key={`flow-inline-${$workspaceStore}-${$pathStore}-${flowModule.id}`}
|
||||
key={`flow-inline-${opWs}-${$pathStore}-${flowModule.id}`}
|
||||
moduleId={flowModule.id}
|
||||
preparedAssetsSqlQueries={preparedSqlQueries.current}
|
||||
customTag={flowModule.value.tag}
|
||||
@@ -915,7 +915,7 @@
|
||||
client={dapClient}
|
||||
currentFrameId={currentDebugFrameId}
|
||||
onClose={() => (showDebugConsole = false)}
|
||||
workspace={$workspaceStore}
|
||||
workspace={opWs}
|
||||
jobId={debugSessionJobId ?? undefined}
|
||||
/>
|
||||
</Pane>
|
||||
@@ -966,7 +966,7 @@
|
||||
},
|
||||
{}
|
||||
)}
|
||||
key={`flow-inline-${$workspaceStore}-${$pathStore}-${flowModule.id}`}
|
||||
key={`flow-inline-${opWs}-${$pathStore}-${flowModule.id}`}
|
||||
moduleId={flowModule.id}
|
||||
preparedAssetsSqlQueries={preparedSqlQueries.current}
|
||||
customTag={flowModule.value.tag}
|
||||
|
||||
@@ -20,7 +20,8 @@
|
||||
import { getScriptByPath, scriptLangToEditorLang } from '$lib/scripts'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { Loader2 } from 'lucide-svelte'
|
||||
import { untrack } from 'svelte'
|
||||
import { getContext, untrack } from 'svelte'
|
||||
import type { FlowEditorContext } from '../types'
|
||||
|
||||
interface Props {
|
||||
path: string
|
||||
@@ -44,6 +45,9 @@
|
||||
language = $bindable(undefined)
|
||||
}: Props = $props()
|
||||
|
||||
const flowEditorContext = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
let opWs = $derived(flowEditorContext?.opWorkspace?.() ?? $workspaceStore)
|
||||
|
||||
let code: string | undefined = $state()
|
||||
let previousCode: string | undefined = $state()
|
||||
let lock: string | undefined = $state(undefined)
|
||||
@@ -51,7 +55,7 @@
|
||||
let notFound = $state(false)
|
||||
|
||||
function getCachedKey(path: string, hash: string | undefined) {
|
||||
return `${$workspaceStore}-${path}-${hash ?? ''}`
|
||||
return `${opWs}-${path}-${hash ?? ''}`
|
||||
}
|
||||
function getCachedValues(path: string, hash: string | undefined) {
|
||||
const key = getCachedKey(path, hash)
|
||||
@@ -64,12 +68,15 @@
|
||||
notFound = cachedValues[key]?.notFound ?? false
|
||||
}
|
||||
|
||||
getCachedValues(untrack(() => path), untrack(() => hash))
|
||||
getCachedValues(
|
||||
untrack(() => path),
|
||||
untrack(() => hash)
|
||||
)
|
||||
|
||||
async function loadPreviousCode(previousHash: string) {
|
||||
try {
|
||||
const previousScript = await ScriptService.getScriptByHash({
|
||||
workspace: $workspaceStore!,
|
||||
workspace: opWs!,
|
||||
hash: previousHash
|
||||
})
|
||||
previousCode = previousScript.content
|
||||
@@ -93,8 +100,8 @@
|
||||
const script = path.startsWith('hub/')
|
||||
? await getScriptByPath(path!)
|
||||
: hash
|
||||
? await ScriptService.getScriptByHash({ workspace: $workspaceStore!, hash })
|
||||
: await getScriptByPath(path!)
|
||||
? await ScriptService.getScriptByHash({ workspace: opWs!, hash })
|
||||
: await getScriptByPath(path!, opWs)
|
||||
code = script.content
|
||||
|
||||
language = script.language
|
||||
@@ -133,7 +140,7 @@
|
||||
<div class="text-xs text-primary mb-4">tag: {tag}</div>
|
||||
{/if}
|
||||
{#if notFound}
|
||||
<div class="text-red-400">script not found at {path} in workspace {$workspaceStore}</div>
|
||||
<div class="text-red-400">script not found at {path} in workspace {opWs}</div>
|
||||
{:else if showAllCode}
|
||||
{#if showDiff}
|
||||
{#key (previousCode ?? '') + (code ?? '')}
|
||||
|
||||
@@ -18,7 +18,9 @@
|
||||
import EditableSchemaDrawer from '$lib/components/schema/EditableSchemaDrawer.svelte'
|
||||
import AddProperty from '$lib/components/schema/AddProperty.svelte'
|
||||
|
||||
const { selectionManager, flowStateStore } = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
const { selectionManager, flowStateStore, opWorkspace } =
|
||||
getContext<FlowEditorContext>('FlowEditorContext')
|
||||
let opWs = $derived(opWorkspace?.() ?? $workspaceStore)
|
||||
const result = flowStateStore.val[selectionManager.getSelectedId()]?.previewResult ?? {}
|
||||
let editor: SimpleEditor | undefined = $state(undefined)
|
||||
|
||||
@@ -37,7 +39,7 @@
|
||||
let isSuspendEnabled = $derived(Boolean(flowModule.suspend))
|
||||
|
||||
async function loadGroups(): Promise<void> {
|
||||
allUserGroups = await GroupService.listGroupNames({ workspace: $workspaceStore! })
|
||||
allUserGroups = await GroupService.listGroupNames({ workspace: opWs! })
|
||||
schema.properties['groups'] = {
|
||||
type: 'array',
|
||||
items: {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { run } from 'svelte/legacy';
|
||||
import { run } from 'svelte/legacy'
|
||||
|
||||
import Skeleton from '$lib/components/common/skeleton/Skeleton.svelte'
|
||||
import FlowGraphViewer from '$lib/components/FlowGraphViewer.svelte'
|
||||
@@ -7,16 +7,20 @@
|
||||
import { Triggers } from '$lib/components/triggers/triggers.svelte'
|
||||
import { FlowService, type Flow, type TriggersCount } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { setContext } from 'svelte'
|
||||
import { getContext, setContext } from 'svelte'
|
||||
import type { FlowEditorContext } from '../types'
|
||||
import { writable } from 'svelte/store'
|
||||
|
||||
interface Props {
|
||||
path: string;
|
||||
noSide?: boolean;
|
||||
fillAvailableHeight?: boolean;
|
||||
path: string
|
||||
noSide?: boolean
|
||||
fillAvailableHeight?: boolean
|
||||
}
|
||||
|
||||
let { path, noSide = false, fillAvailableHeight = false }: Props = $props();
|
||||
let { path, noSide = false, fillAvailableHeight = false }: Props = $props()
|
||||
|
||||
const flowEditorContext = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
let opWs = $derived(flowEditorContext?.opWorkspace?.() ?? $workspaceStore)
|
||||
|
||||
let flow: Flow | undefined = $state(undefined)
|
||||
|
||||
@@ -29,15 +33,13 @@
|
||||
})
|
||||
|
||||
async function loadFlow(path: string) {
|
||||
flow = await FlowService.getFlowByPath({ workspace: $workspaceStore!, path })
|
||||
triggersCount.set(
|
||||
await FlowService.getTriggersCountOfFlow({ workspace: $workspaceStore!, path })
|
||||
)
|
||||
flow = await FlowService.getFlowByPath({ workspace: opWs!, path })
|
||||
triggersCount.set(await FlowService.getTriggersCountOfFlow({ workspace: opWs!, path }))
|
||||
}
|
||||
|
||||
run(() => {
|
||||
path && loadFlow(path)
|
||||
});
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col flex-1 h-full overflow-auto">
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
import FlowExecutionStatus from '$lib/components/runs/FlowExecutionStatus.svelte'
|
||||
import type { Job } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { getContext } from 'svelte'
|
||||
import type { FlowEditorContext } from '../types'
|
||||
import FlowCard from '../common/FlowCard.svelte'
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
import type { StateStore } from '$lib/utils'
|
||||
@@ -18,6 +20,9 @@
|
||||
}
|
||||
|
||||
let { job, isOwner, suspendStatus, noEditor, onOpenDetails }: Props = $props()
|
||||
|
||||
const flowEditorContext = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
let opWs = $derived(flowEditorContext?.opWorkspace?.() ?? $workspaceStore)
|
||||
</script>
|
||||
|
||||
<FlowCard {noEditor} title="Flow result">
|
||||
@@ -39,7 +44,7 @@
|
||||
{#if isOwner !== undefined && suspendStatus}
|
||||
<FlowExecutionStatus
|
||||
{job}
|
||||
workspaceId={$workspaceStore}
|
||||
workspaceId={opWs}
|
||||
{isOwner}
|
||||
innerModules={job?.flow_status?.modules}
|
||||
{suspendStatus}
|
||||
|
||||
@@ -147,6 +147,7 @@
|
||||
initialPath={$initialPathStore}
|
||||
namePlaceholder="flow"
|
||||
kind="flow"
|
||||
workspaceOverride={opWorkspace?.()}
|
||||
/>
|
||||
{#if $initialPathStore && $pathStore && $pathStore !== $initialPathStore}
|
||||
<Alert
|
||||
@@ -453,7 +454,7 @@
|
||||
/>
|
||||
{#if flowStore.val.on_behalf_of_email && canPreserve}
|
||||
→ <OnBehalfOfSelector
|
||||
targetWorkspace={$workspaceStore ?? ''}
|
||||
targetWorkspace={opWorkspace?.() ?? $workspaceStore ?? ''}
|
||||
targetValue={$savedOnBehalfOfEmail}
|
||||
selected={onBehalfOfChoice}
|
||||
onSelect={(choice, details) => {
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
import Path from '$lib/components/Path.svelte'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { sameTopDomainOrigin } from '$lib/cookies'
|
||||
import { onDestroy } from 'svelte'
|
||||
import { getContext, onDestroy } from 'svelte'
|
||||
import type { FlowEditorContext } from '../types'
|
||||
|
||||
interface Props {
|
||||
onConnected: (resourcePath: string, resourceName: string) => void
|
||||
@@ -21,6 +22,9 @@
|
||||
|
||||
let { onConnected, onCancel }: Props = $props()
|
||||
|
||||
const flowEditorContext = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
let opWs = $derived(flowEditorContext?.opWorkspace?.() ?? $workspaceStore)
|
||||
|
||||
let serverUrl = $state('')
|
||||
let discoveryResult = $state<DiscoverMcpOauthResponse | null>(null)
|
||||
let selectedScopes = $state<string[]>([])
|
||||
@@ -112,7 +116,7 @@
|
||||
let accountId: number | undefined
|
||||
if (data.expires_in && data.refresh_token) {
|
||||
const accountIdStr = await OauthService.createAccount({
|
||||
workspace: $workspaceStore!,
|
||||
workspace: opWs!,
|
||||
requestBody: {
|
||||
refresh_token: data.refresh_token,
|
||||
expires_in: data.expires_in,
|
||||
@@ -124,7 +128,7 @@
|
||||
}
|
||||
|
||||
await VariableService.createVariable({
|
||||
workspace: $workspaceStore!,
|
||||
workspace: opWs!,
|
||||
requestBody: {
|
||||
path: resourcePath,
|
||||
value: data.access_token,
|
||||
@@ -136,7 +140,7 @@
|
||||
})
|
||||
|
||||
await ResourceService.createResource({
|
||||
workspace: $workspaceStore!,
|
||||
workspace: opWs!,
|
||||
requestBody: {
|
||||
resource_type: 'mcp',
|
||||
path: resourcePath,
|
||||
@@ -228,6 +232,7 @@
|
||||
initialPath=""
|
||||
namePlaceholder={resourceName}
|
||||
kind="resource"
|
||||
workspaceOverride={opWs}
|
||||
/>
|
||||
|
||||
<Button size="sm" onClick={startOAuth} disabled={!resourcePath || pathError !== ''}>
|
||||
|
||||
@@ -26,9 +26,10 @@
|
||||
import { safeSelectItems } from '$lib/components/select/utils.svelte'
|
||||
import ResourcePicker from '$lib/components/ResourcePicker.svelte'
|
||||
import { usePromise } from '$lib/svelte5Utils.svelte'
|
||||
import { untrack } from 'svelte'
|
||||
import { getContext, untrack } from 'svelte'
|
||||
import Alert from '$lib/components/common/alert/Alert.svelte'
|
||||
import McpOAuthConnect from './McpOAuthConnect.svelte'
|
||||
import type { FlowEditorContext } from '../types'
|
||||
|
||||
interface Props {
|
||||
tool: McpTool
|
||||
@@ -36,6 +37,9 @@
|
||||
|
||||
let { tool = $bindable() }: Props = $props()
|
||||
|
||||
const flowEditorContext = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
let opWs = $derived(flowEditorContext?.opWorkspace?.() ?? $workspaceStore)
|
||||
|
||||
let showOAuthForm = $state(false)
|
||||
let refreshCount = $state(0)
|
||||
let resourcePicker: ResourcePicker | undefined = $state()
|
||||
@@ -43,7 +47,7 @@
|
||||
let tools = usePromise(
|
||||
async () =>
|
||||
await loadToolsCached({
|
||||
workspace: $workspaceStore!,
|
||||
workspace: opWs!,
|
||||
path: tool.value.resource_path,
|
||||
refreshCount
|
||||
}),
|
||||
@@ -56,7 +60,7 @@
|
||||
|
||||
$effect(() => {
|
||||
resourcePath
|
||||
$workspaceStore
|
||||
opWs
|
||||
refreshCount
|
||||
untrack(() => {
|
||||
if (resourcePath?.length > 0) {
|
||||
@@ -105,7 +109,12 @@
|
||||
|
||||
<div class="w-full">
|
||||
<Label label="MCP Resource">
|
||||
<ResourcePicker bind:this={resourcePicker} resourceType="mcp" bind:value={tool.value.resource_path} />
|
||||
<ResourcePicker
|
||||
bind:this={resourcePicker}
|
||||
resourceType="mcp"
|
||||
bind:value={tool.value.resource_path}
|
||||
workspace={opWs}
|
||||
/>
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -13,20 +13,24 @@
|
||||
orderedJsonStringify,
|
||||
sendUserToast
|
||||
} from '$lib/utils'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { createEventDispatcher, getContext } from 'svelte'
|
||||
import { fade } from 'svelte/transition'
|
||||
import WorkerTagSelect from '$lib/components/WorkerTagSelect.svelte'
|
||||
import type { FlowEditorContext } from '../types'
|
||||
|
||||
let scriptEditorDrawer: Drawer | undefined = $state()
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const flowEditorContext = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
let opWs = $derived(flowEditorContext?.opWorkspace?.() ?? $workspaceStore)
|
||||
|
||||
export async function openDrawer(hash: string, cb: () => void): Promise<void> {
|
||||
script = undefined
|
||||
closeAnyway = false
|
||||
scriptEditorDrawer?.openDrawer?.()
|
||||
script = await ScriptService.getScriptByHash({
|
||||
workspace: $workspaceStore!,
|
||||
workspace: opWs!,
|
||||
hash
|
||||
})
|
||||
savedScript = structuredClone($state.snapshot(script))
|
||||
@@ -89,7 +93,7 @@
|
||||
}
|
||||
|
||||
await ScriptService.createScript({
|
||||
workspace: $workspaceStore!,
|
||||
workspace: opWs!,
|
||||
requestBody: {
|
||||
...script,
|
||||
language: script.language!,
|
||||
@@ -200,6 +204,7 @@
|
||||
{#if script && displayEditor}
|
||||
{#key script.hash}
|
||||
<ScriptEditor
|
||||
workspaceOverride={opWs}
|
||||
showCaptures={false}
|
||||
on:saveDraft={() => {
|
||||
saveScript()
|
||||
@@ -216,7 +221,10 @@
|
||||
>
|
||||
{#snippet editorBarRight()}
|
||||
<div>
|
||||
<WorkerTagSelect bind:tag={() => script?.tag, (v) => script && (script.tag = v)} />
|
||||
<WorkerTagSelect
|
||||
bind:tag={() => script?.tag, (v) => script && (script.tag = v)}
|
||||
workspaceId={opWs}
|
||||
/>
|
||||
</div>
|
||||
{/snippet}
|
||||
</ScriptEditor>
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
import { createFlowChatManager } from './FlowChatManager.svelte'
|
||||
import FlowConversationsSidebar from './FlowConversationsSidebar.svelte'
|
||||
import FlowChatInterface from './FlowChatInterface.svelte'
|
||||
import { untrack } from 'svelte'
|
||||
import { getContext, untrack } from 'svelte'
|
||||
import type { FlowEditorContext } from '../types'
|
||||
|
||||
interface Props {
|
||||
onRunFlow: (
|
||||
@@ -24,10 +25,13 @@
|
||||
useStreaming = false,
|
||||
path,
|
||||
hideSidebar = false,
|
||||
inputSchema = undefined,
|
||||
inputSchema = undefined
|
||||
}: Props = $props()
|
||||
|
||||
const flowEditorContext = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
|
||||
const manager = createFlowChatManager()
|
||||
manager.operatingWorkspace = () => flowEditorContext?.opWorkspace?.()
|
||||
|
||||
// Initialize manager when component mounts
|
||||
$effect(() => {
|
||||
@@ -52,9 +56,7 @@
|
||||
// Derive additional inputs schema (excluding user_message) for chat mode
|
||||
const additionalInputsSchema = $derived.by(() => {
|
||||
const props = inputSchema?.properties ?? {}
|
||||
const filtered = Object.fromEntries(
|
||||
Object.entries(props).filter(([k]) => k !== 'user_message')
|
||||
)
|
||||
const filtered = Object.fromEntries(Object.entries(props).filter(([k]) => k !== 'user_message'))
|
||||
if (Object.keys(filtered).length === 0) return undefined
|
||||
const required = inputSchema?.required
|
||||
const requiredArray: string[] = Array.isArray(required) ? required : []
|
||||
|
||||
@@ -93,6 +93,7 @@
|
||||
schema={additionalInputsSchema}
|
||||
bind:args={additionalInputsValues}
|
||||
helperScript={dynamicInputHelperScript}
|
||||
workspace={manager.operatingWorkspace?.()}
|
||||
/>
|
||||
{#snippet actions()}
|
||||
<Button onClick={handleModalConfirm} variant="accent">Save</Button>
|
||||
|
||||
@@ -53,6 +53,15 @@ export class FlowChatManager {
|
||||
#useStreaming = $state(false)
|
||||
#path = $state<string | undefined>(undefined)
|
||||
|
||||
// When the flow editor runs as an AI-session live editor, it acts on a workspace
|
||||
// that can differ from the nav store. FlowChat.svelte wires this to
|
||||
// FlowEditorContext.opWorkspace so workspace-scoped calls hit the acting workspace.
|
||||
operatingWorkspace?: () => string | undefined
|
||||
|
||||
#workspace(): string | undefined {
|
||||
return this.operatingWorkspace?.() ?? get(workspaceStore)
|
||||
}
|
||||
|
||||
initialize(
|
||||
onRunFlow: (
|
||||
userMessage: string,
|
||||
@@ -112,7 +121,7 @@ export class FlowChatManager {
|
||||
// Create a new conversation object and add it to the top of the list
|
||||
const newConversation: ConversationWithDraft = {
|
||||
id: newConversationId,
|
||||
workspace_id: get(workspaceStore)!,
|
||||
workspace_id: this.#workspace()!,
|
||||
flow_path: this.#path!,
|
||||
title: 'New chat',
|
||||
created_at: new Date().toISOString(),
|
||||
@@ -160,7 +169,7 @@ export class FlowChatManager {
|
||||
try {
|
||||
this.deletingConversationId = conversationId
|
||||
await FlowConversationsService.deleteFlowConversation({
|
||||
workspace: get(workspaceStore)!,
|
||||
workspace: this.#workspace()!,
|
||||
conversationId
|
||||
})
|
||||
if (this.selectedConversationId === conversationId) {
|
||||
@@ -178,14 +187,14 @@ export class FlowChatManager {
|
||||
}
|
||||
|
||||
async cancelCurrentJob() {
|
||||
if (!get(workspaceStore)) {
|
||||
if (!this.#workspace()) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
if (this.currentJobId) {
|
||||
await JobService.cancelQueuedJob({
|
||||
workspace: get(workspaceStore)!,
|
||||
workspace: this.#workspace()!,
|
||||
id: this.currentJobId,
|
||||
requestBody: {}
|
||||
})
|
||||
@@ -206,11 +215,11 @@ export class FlowChatManager {
|
||||
|
||||
// Only used by InfiniteList
|
||||
private async loadConversations(page: number, perPage: number) {
|
||||
if (!get(workspaceStore) || !this.#path) return []
|
||||
if (!this.#workspace() || !this.#path) return []
|
||||
|
||||
try {
|
||||
const response = await FlowConversationsService.listFlowConversations({
|
||||
workspace: get(workspaceStore)!,
|
||||
workspace: this.#workspace()!,
|
||||
flowPath: this.#path,
|
||||
page: page,
|
||||
perPage: perPage
|
||||
@@ -227,7 +236,7 @@ export class FlowChatManager {
|
||||
// Message loading
|
||||
private async loadMessages(reset: boolean, conversationId?: string) {
|
||||
let conversationIdToUse = conversationId ?? this.selectedConversationId
|
||||
if (!get(workspaceStore) || !conversationIdToUse) return
|
||||
if (!this.#workspace() || !conversationIdToUse) return
|
||||
|
||||
if (reset) {
|
||||
if (this.#conversationsCache[conversationIdToUse]) {
|
||||
@@ -245,7 +254,7 @@ export class FlowChatManager {
|
||||
const previousScrollHeight = this.messagesContainer?.scrollHeight || 0
|
||||
|
||||
const response = await FlowConversationsService.listConversationMessages({
|
||||
workspace: get(workspaceStore)!,
|
||||
workspace: this.#workspace()!,
|
||||
conversationId: conversationIdToUse,
|
||||
page: pageToFetch,
|
||||
perPage: this.#perPage
|
||||
@@ -318,7 +327,7 @@ export class FlowChatManager {
|
||||
// Polling
|
||||
private async pollJobResult(jobId: string) {
|
||||
try {
|
||||
await waitJob(jobId)
|
||||
await waitJob(jobId, this.#workspace())
|
||||
} catch (error) {
|
||||
console.error('Error polling job result:', error)
|
||||
} finally {
|
||||
@@ -338,12 +347,12 @@ export class FlowChatManager {
|
||||
conversationId: string,
|
||||
options?: { isNewConversation?: boolean; removeTempMessages?: boolean }
|
||||
) {
|
||||
if (!get(workspaceStore)) return
|
||||
if (!this.#workspace()) return
|
||||
|
||||
try {
|
||||
const lastSeq = this.getLastPersistedMessageSeq()
|
||||
const response = await FlowConversationsService.listConversationMessages({
|
||||
workspace: get(workspaceStore)!,
|
||||
workspace: this.#workspace()!,
|
||||
conversationId: conversationId,
|
||||
page: 1,
|
||||
perPage: 50,
|
||||
@@ -488,7 +497,7 @@ export class FlowChatManager {
|
||||
}
|
||||
|
||||
// Build the EventSource URL
|
||||
const streamUrl = `/api/w/${get(workspaceStore)}/jobs_u/getupdate_sse/${jobId}`
|
||||
const streamUrl = `/api/w/${this.#workspace()}/jobs_u/getupdate_sse/${jobId}`
|
||||
const url = new URL(streamUrl, window.location.origin)
|
||||
url.searchParams.set('poll_delay_ms', '50')
|
||||
url.searchParams.set('fast', 'true')
|
||||
|
||||
@@ -213,14 +213,17 @@ export async function createFlow(id: string): Promise<[FlowModule, FlowModuleSta
|
||||
}
|
||||
|
||||
export async function fork(
|
||||
flowModule: FlowModule
|
||||
flowModule: FlowModule,
|
||||
// The acting workspace when the flow editor runs in an AI session; else the nav workspace.
|
||||
workspace?: string
|
||||
): Promise<[FlowModule & { value: RawScript }, FlowModuleState]> {
|
||||
if (flowModule.value.type !== 'script') {
|
||||
throw new Error('Can only fork a script module')
|
||||
}
|
||||
const forkedFlowModule = await createInlineScriptModuleFromPath(
|
||||
flowModule.value.path ?? '',
|
||||
flowModule.id
|
||||
flowModule.id,
|
||||
workspace
|
||||
)
|
||||
const flowModuleState = await loadFlowModuleState(forkedFlowModule)
|
||||
return [forkedFlowModule, flowModuleState]
|
||||
@@ -228,9 +231,10 @@ export async function fork(
|
||||
|
||||
async function createInlineScriptModuleFromPath(
|
||||
path: string,
|
||||
id: string
|
||||
id: string,
|
||||
workspace?: string
|
||||
): Promise<FlowModule & { value: RawScript }> {
|
||||
const { content, language } = await getScriptByPath(path)
|
||||
const { content, language } = await getScriptByPath(path, workspace)
|
||||
|
||||
return {
|
||||
id,
|
||||
@@ -255,7 +259,10 @@ export async function createScriptFromInlineScript(
|
||||
flowModule: FlowModule,
|
||||
suffix: string,
|
||||
schema: Schema | undefined,
|
||||
flowPath: string
|
||||
flowPath: string,
|
||||
// The session's acting workspace when the flow editor runs in an AI session;
|
||||
// falls back to the navigation workspace outside a session.
|
||||
workspace?: string
|
||||
): Promise<[FlowModule & { value: PathScript }, FlowModuleState]> {
|
||||
const user = get(userStore)
|
||||
|
||||
@@ -275,10 +282,10 @@ export async function createScriptFromInlineScript(
|
||||
const forkedDescription = wasForked ? `as a fork of ${originalScriptPath}` : ''
|
||||
const description = `This script was edited in place of flow ${flowPath} ${forkedDescription} by ${user?.username}.`
|
||||
|
||||
const availablePath = await findNextAvailablePath(path)
|
||||
const availablePath = await findNextAvailablePath(path, workspace)
|
||||
|
||||
const hash = await ScriptService.createScript({
|
||||
workspace: get(workspaceStore)!,
|
||||
workspace: workspace ?? get(workspaceStore)!,
|
||||
requestBody: {
|
||||
path: availablePath,
|
||||
summary: flowModule.summary ?? '',
|
||||
|
||||
@@ -60,10 +60,7 @@
|
||||
GroupedModulesProxy,
|
||||
type ExtendedOpenFlow
|
||||
} from '$lib/components/graph/groupedModulesProxy.svelte'
|
||||
import {
|
||||
GroupDisplayState,
|
||||
type FlowGroup
|
||||
} from '$lib/components/graph/groupEditor.svelte'
|
||||
import { GroupDisplayState, type FlowGroup } from '$lib/components/graph/groupEditor.svelte'
|
||||
import {
|
||||
type FlowStructureNode,
|
||||
matchStructureNode,
|
||||
@@ -135,9 +132,11 @@
|
||||
flowHasChanged
|
||||
}: Props = $props()
|
||||
|
||||
const { customUi, selectionManager, history, flowStateStore, flowStore, pathStore } =
|
||||
const { customUi, selectionManager, history, flowStateStore, flowStore, pathStore, opWorkspace } =
|
||||
getContext<FlowEditorContext>('FlowEditorContext')
|
||||
|
||||
let opWs = $derived(opWorkspace?.() ?? $workspaceStore)
|
||||
|
||||
const moveManager = new MoveManager()
|
||||
const { triggersCount, triggersState } = getContext<TriggerContext>('TriggerContext')
|
||||
|
||||
@@ -460,7 +459,7 @@
|
||||
}
|
||||
}
|
||||
const previousJobId = await JobService.listCompletedJobs({
|
||||
workspace: $workspaceStore!,
|
||||
workspace: opWs!,
|
||||
scriptPathExact: path,
|
||||
jobKinds: ['preview', 'script', 'flowpreview', 'flow'].join(','),
|
||||
page: 1,
|
||||
@@ -468,7 +467,7 @@
|
||||
})
|
||||
if (previousJobId.length > 0) {
|
||||
const getJobResult = await JobService.getCompletedJobResultMaybe({
|
||||
workspace: $workspaceStore!,
|
||||
workspace: opWs!,
|
||||
id: previousJobId[0].id
|
||||
})
|
||||
if ('result' in getJobResult) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { createEventDispatcher, untrack } from 'svelte'
|
||||
import { createEventDispatcher, getContext, untrack } from 'svelte'
|
||||
import type { FlowEditorContext } from '../types'
|
||||
import { ScriptService } from '$lib/gen'
|
||||
import SearchItems from '$lib/components/SearchItems.svelte'
|
||||
import { Badge, Skeleton } from '$lib/components/common'
|
||||
@@ -21,6 +22,9 @@
|
||||
let items = $state(undefined) as Item[] | undefined
|
||||
|
||||
let filteredItems = $state(undefined) as (Item & { marked?: string })[] | undefined
|
||||
|
||||
const flowEditorContext = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
let opWs = $derived(flowEditorContext?.opWorkspace?.() ?? $workspaceStore)
|
||||
interface Props {
|
||||
kind?: 'script' | 'trigger' | 'approval' | 'failure'
|
||||
isTemplate?: boolean | undefined
|
||||
@@ -39,7 +43,7 @@
|
||||
|
||||
async function loadItems(): Promise<void> {
|
||||
items = await ScriptService.listScripts({
|
||||
workspace: $workspaceStore!,
|
||||
workspace: opWs!,
|
||||
kinds: kind,
|
||||
isTemplate,
|
||||
withoutDescription: true
|
||||
|
||||
@@ -37,7 +37,8 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { createEventDispatcher, untrack } from 'svelte'
|
||||
import { createEventDispatcher, getContext, untrack } from 'svelte'
|
||||
import type { FlowEditorContext } from '../types'
|
||||
import { FlowService, ScriptService } from '$lib/gen'
|
||||
import SearchItems from '$lib/components/SearchItems.svelte'
|
||||
import { Skeleton } from '$lib/components/common'
|
||||
@@ -57,9 +58,11 @@
|
||||
hash?: string
|
||||
}
|
||||
|
||||
const flowEditorContext = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
let opWs = $derived(flowEditorContext?.opWorkspace?.() ?? $workspaceStore)
|
||||
|
||||
let items = usePromise(
|
||||
async () =>
|
||||
await loadItemsCached({ workspace: $workspaceStore!, kind, isTemplate, refreshCount }),
|
||||
async () => await loadItemsCached({ workspace: opWs!, kind, isTemplate, refreshCount }),
|
||||
{ loadInit: false, clearValueOnRefresh: false }
|
||||
)
|
||||
|
||||
|
||||
@@ -39,7 +39,9 @@
|
||||
noHistory = undefined
|
||||
}: Props = $props()
|
||||
|
||||
const { pathStore, flowStateStore } = getContext<FlowEditorContext>('FlowEditorContext') ?? {}
|
||||
const { pathStore, flowStateStore, opWorkspace } =
|
||||
getContext<FlowEditorContext>('FlowEditorContext') ?? {}
|
||||
let opWs = $derived(opWorkspace?.() ?? $workspaceStore)
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let infiniteList: InfiniteList | undefined = $state(undefined)
|
||||
@@ -51,7 +53,7 @@
|
||||
loadInputsPageFn = async (page: number, perPage: number) => {
|
||||
if (staticInputs) return staticInputs
|
||||
const previousJobs = await JobService.listCompletedJobs({
|
||||
workspace: $workspaceStore!,
|
||||
workspace: opWs!,
|
||||
scriptPathExact: path === '' ? $pathStore + '/' + moduleId : path,
|
||||
jobKinds: ['preview', 'script', 'flowpreview', 'flow', 'flowscript'].join(','),
|
||||
page,
|
||||
@@ -77,7 +79,7 @@
|
||||
async function getJobResultAndLogs(jobId: string, noLogs: boolean) {
|
||||
try {
|
||||
const job = await JobService.getJob({
|
||||
workspace: $workspaceStore ?? '',
|
||||
workspace: opWs ?? '',
|
||||
id: jobId ?? '',
|
||||
noLogs
|
||||
})
|
||||
|
||||
@@ -106,6 +106,10 @@ export type FlowEditorContext = {
|
||||
|
||||
export type FlowGraphAssetContext = StateStore<{
|
||||
selectedAsset: Asset | undefined
|
||||
// The workspace asset explore controls (DB manager, S3/volume picker, resource
|
||||
// editor) act on — the flow's acting workspace in an AI session, else the nav
|
||||
// workspace. Set by FlowAssetsHandler.
|
||||
workspace: string | undefined
|
||||
s3FilePicker: S3FilePicker | undefined
|
||||
resourceEditorDrawer: ResourceEditorDrawer | undefined
|
||||
// Maps resource paths to their metadata. undefined is for error
|
||||
|
||||
@@ -185,11 +185,14 @@ export async function runFlowPreview(
|
||||
path: string,
|
||||
restartedFrom: RestartedFrom | undefined,
|
||||
conversationId?: string | undefined,
|
||||
tempScriptRefs?: Record<string, string>
|
||||
tempScriptRefs?: Record<string, string>,
|
||||
// The session's acting workspace when previewing inside an AI-session flow
|
||||
// editor; falls back to the navigation workspace for full-page previews.
|
||||
workspace?: string
|
||||
) {
|
||||
const newFlow = flow
|
||||
return await JobService.runFlowPreview({
|
||||
workspace: get(workspaceStore) ?? '',
|
||||
workspace: workspace ?? get(workspaceStore) ?? '',
|
||||
requestBody: {
|
||||
args,
|
||||
value: newFlow.value,
|
||||
|
||||
@@ -287,6 +287,7 @@
|
||||
noText
|
||||
buttonVariant="accent"
|
||||
s3FilePicker={flowGraphAssetsCtx?.val.s3FilePicker}
|
||||
workspace={flowGraphAssetsCtx?.val.workspace}
|
||||
_resourceMetadata={cachedResourceMetadata}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -10,6 +10,10 @@
|
||||
toSchemaItems
|
||||
} from './datatableUtils.svelte'
|
||||
import { Button } from '../common'
|
||||
import { getRawAppOperatingWorkspace } from './rawAppWorkspace'
|
||||
|
||||
const getOpWs = getRawAppOperatingWorkspace()
|
||||
let opWs = $derived(getOpWs?.() ?? $workspaceStore)
|
||||
|
||||
interface Props {
|
||||
/** Currently selected datatable */
|
||||
@@ -30,8 +34,11 @@
|
||||
}: Props = $props()
|
||||
|
||||
// Load available datatables and schemas using shared utilities
|
||||
const datatables = createDatatablesResource(() => $workspaceStore)
|
||||
const schemas = createSchemasResource(() => datatable)
|
||||
const datatables = createDatatablesResource(() => opWs)
|
||||
const schemas = createSchemasResource(
|
||||
() => datatable,
|
||||
() => opWs
|
||||
)
|
||||
|
||||
const datatableItems = $derived(toDatatableItems(datatables.current))
|
||||
const schemaItems = $derived(toSchemaItems(schemas.current))
|
||||
@@ -49,47 +56,43 @@
|
||||
|
||||
<Popover>
|
||||
{#snippet trigger()}
|
||||
|
||||
<Button
|
||||
title="Configure default datatable & schema"
|
||||
unifiedSize="xs"
|
||||
variant="subtle"
|
||||
nonCaptureEvent
|
||||
btnClasses="px-1"
|
||||
>
|
||||
<Settings size={12} />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
title="Configure default datatable & schema"
|
||||
unifiedSize="xs"
|
||||
variant="subtle"
|
||||
nonCaptureEvent
|
||||
btnClasses="px-1"
|
||||
>
|
||||
<Settings size={12} />
|
||||
</Button>
|
||||
{/snippet}
|
||||
{#snippet content()}
|
||||
|
||||
<div class="flex flex-col gap-3 p-4 min-w-64 max-w-80">
|
||||
<div class="text-xs font-medium text-primary">Default Datatable & Schema</div>
|
||||
<div class="flex flex-col gap-3 p-4 min-w-64 max-w-80">
|
||||
<div class="text-xs font-medium text-primary">Default Datatable & Schema</div>
|
||||
|
||||
<p class="text-2xs text-tertiary leading-relaxed">
|
||||
{description}
|
||||
</p>
|
||||
<p class="text-2xs text-tertiary leading-relaxed">
|
||||
{description}
|
||||
</p>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<span class="text-2xs text-tertiary">Database</span>
|
||||
<Select
|
||||
items={datatableItems}
|
||||
bind:value={() => datatable, (v) => onChange?.(v, schema)}
|
||||
placeholder="Select database"
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<span class="text-2xs text-tertiary">Schema</span>
|
||||
<Select
|
||||
items={schemaItems}
|
||||
bind:value={() => schema ?? '', (v) => onChange?.(datatable, v || undefined)}
|
||||
placeholder="public"
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<span class="text-2xs text-tertiary">Database</span>
|
||||
<Select
|
||||
items={datatableItems}
|
||||
bind:value={() => datatable, (v) => onChange?.(v, schema)}
|
||||
placeholder="Select database"
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<span class="text-2xs text-tertiary">Schema</span>
|
||||
<Select
|
||||
items={schemaItems}
|
||||
bind:value={() => schema ?? '', (v) => onChange?.(datatable, v || undefined)}
|
||||
placeholder="public"
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Popover>
|
||||
|
||||
@@ -12,6 +12,10 @@
|
||||
import DBManagerContent from '../DBManagerContent.svelte'
|
||||
import type { DbInput } from '../dbTypes'
|
||||
import type { SelectedTable } from '../DBManager.svelte'
|
||||
import { getRawAppOperatingWorkspace } from './rawAppWorkspace'
|
||||
|
||||
const getOpWs = getRawAppOperatingWorkspace()
|
||||
let opWs = $derived(getOpWs?.() ?? $workspaceStore)
|
||||
|
||||
interface Props {
|
||||
onAdd?: (ref: DataTableRef) => void
|
||||
@@ -40,11 +44,9 @@
|
||||
|
||||
// Load available datatables from workspace
|
||||
const datatables = resource<string[]>([], async () => {
|
||||
if (!$workspaceStore) return []
|
||||
if (!opWs) return []
|
||||
try {
|
||||
return (await WorkspaceService.listDataTables({ workspace: $workspaceStore })).map(
|
||||
(d) => d.name
|
||||
)
|
||||
return (await WorkspaceService.listDataTables({ workspace: opWs })).map((d) => d.name)
|
||||
} catch (e) {
|
||||
console.error('Failed to load datatables:', e)
|
||||
return []
|
||||
@@ -163,11 +165,12 @@
|
||||
CloseIcon={hasReplResult ? ArrowLeft : undefined}
|
||||
noPadding
|
||||
>
|
||||
{#if dbInput && $workspaceStore}
|
||||
{#if dbInput && opWs}
|
||||
{#key selectedDatatable}
|
||||
<DBManagerContent
|
||||
bind:this={dbManagerContent}
|
||||
input={dbInput}
|
||||
workspace={opWs}
|
||||
bind:hasReplResult
|
||||
bind:selectedSchemaKey
|
||||
bind:selectedTableKey
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
// import { addWmillClient } from './utils'
|
||||
import RawAppBackgroundRunner from './RawAppBackgroundRunner.svelte'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { setRawAppOperatingWorkspace } from './rawAppWorkspace'
|
||||
import { useLocalStorageValue } from '$lib/svelte5Utils.svelte'
|
||||
import {
|
||||
genWmillTs,
|
||||
@@ -175,6 +176,9 @@
|
||||
// embedded in a session preview (autosaveWorkspace), else the navigation
|
||||
// workspace. Deploy/save/background-runner must target it, not $workspaceStore.
|
||||
const opWorkspace = $derived(autosaveWorkspace ?? $workspaceStore)
|
||||
// Expose it to the sidebar sub-components (inline scripts, datatable/shared-UI
|
||||
// drawers, DB selector) so their lookups target the app's workspace too.
|
||||
setRawAppOperatingWorkspace(() => opWorkspace)
|
||||
|
||||
// Convert to object format for child components
|
||||
let dataTableRefsObjects = $derived(data.tables.map(parseDataTableRef))
|
||||
@@ -988,6 +992,7 @@
|
||||
// Clear the cached schema so it gets refreshed with the new table
|
||||
const resourcePath = `datatable://${datatableName}`
|
||||
delete $dbSchemas[resourcePath]
|
||||
delete $dbSchemas[`${opWorkspace}:${resourcePath}`]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -709,6 +709,7 @@
|
||||
{onLatest}
|
||||
{savedApp}
|
||||
rawApp
|
||||
operatingWorkspace={opWorkspace}
|
||||
bind:summary
|
||||
bind:customPath
|
||||
bind:deploymentMsg
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
import { usePreparedAssetSqlQueries } from '$lib/infer.svelte'
|
||||
import AssetsDropdownButton from '../assets/AssetsDropdownButton.svelte'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { getRawAppOperatingWorkspace } from './rawAppWorkspace'
|
||||
import { SvelteSet } from 'svelte/reactivity'
|
||||
import { Pane, Splitpanes } from 'svelte-splitpanes'
|
||||
import { editor as meditor } from 'monaco-editor'
|
||||
@@ -76,6 +77,10 @@
|
||||
onSelectionChange,
|
||||
delete_after_secs = $bindable()
|
||||
}: Props = $props()
|
||||
|
||||
const getOpWs = getRawAppOperatingWorkspace()
|
||||
let opWs = $derived(getOpWs?.() ?? $workspaceStore)
|
||||
|
||||
let diffEditor = $state() as DiffEditor | undefined
|
||||
let validCode = $state(true)
|
||||
|
||||
@@ -145,7 +150,7 @@
|
||||
)
|
||||
let preparedSqlQueries = usePreparedAssetSqlQueries(
|
||||
() => inferAssetsRes.current?.sql_queries,
|
||||
() => $workspaceStore
|
||||
() => opWs
|
||||
)
|
||||
$effect(() => {
|
||||
if (!inlineScript || !inferAssetsRes.current || inferAssetsRes.current.status === 'error')
|
||||
@@ -303,13 +308,13 @@
|
||||
resetDAPClient()
|
||||
dapClient = getDAPClient(dapServerUrl)
|
||||
|
||||
const env = await fetchContextualVariables($workspaceStore ?? '')
|
||||
const env = await fetchContextualVariables(opWs ?? '')
|
||||
const code = inlineScript.content
|
||||
|
||||
let signedPayload
|
||||
try {
|
||||
signedPayload = await signDebugRequest(
|
||||
$workspaceStore ?? '',
|
||||
opWs ?? '',
|
||||
code ?? '',
|
||||
inlineScript.language ?? 'python3'
|
||||
)
|
||||
@@ -636,6 +641,7 @@
|
||||
|
||||
<div class="shadow-sm px-1 border-b-1 border-gray-200 dark:border-gray-700">
|
||||
<EditorBar
|
||||
workspace={opWs}
|
||||
{validCode}
|
||||
{editor}
|
||||
lang={inlineScript.language}
|
||||
@@ -733,7 +739,7 @@
|
||||
client={dapClient}
|
||||
currentFrameId={currentDebugFrameId}
|
||||
onClose={() => (showDebugConsole = false)}
|
||||
workspace={$workspaceStore}
|
||||
workspace={opWs}
|
||||
jobId={debugSessionJobId ?? undefined}
|
||||
/>
|
||||
</Pane>
|
||||
|
||||
@@ -29,6 +29,10 @@
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import { isHubFlowPath } from '$lib/utils'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { getRawAppOperatingWorkspace } from './rawAppWorkspace'
|
||||
|
||||
const getOpWs = getRawAppOperatingWorkspace()
|
||||
let opWs = $derived(getOpWs?.() ?? $workspaceStore)
|
||||
|
||||
type RunnableWithInlineScript = RunnableWithFields & {
|
||||
inlineScript?: InlineScript & { language: ScriptLang }
|
||||
@@ -131,7 +135,7 @@
|
||||
case 'groups':
|
||||
return $userStore?.groups ?? []
|
||||
case 'workspace':
|
||||
return $workspaceStore ?? ''
|
||||
return opWs ?? ''
|
||||
case 'author':
|
||||
return $userStore?.email ?? '' // In editor, author is the current user
|
||||
default:
|
||||
@@ -188,6 +192,7 @@
|
||||
|
||||
<JobLoader
|
||||
noCode={true}
|
||||
workspaceOverride={opWs}
|
||||
bind:scriptProgress
|
||||
bind:this={jobLoader}
|
||||
bind:isLoading={testIsLoading}
|
||||
@@ -299,6 +304,7 @@
|
||||
</div>
|
||||
<SchemaForm
|
||||
on:keydownCmdEnter={testPreview}
|
||||
workspace={opWs}
|
||||
disabledArgs={Object.entries(runnable?.fields ?? {})
|
||||
.filter(([_, v]) => v.type == 'static')
|
||||
.map(([k]) => k)}
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import RawAppInlineScripRunnable, { type Runnable } from './RawAppInlineScriptRunnable.svelte'
|
||||
import { createScriptFromInlineScript } from '../apps/editor/inlineScriptsPanel/utils'
|
||||
import { getRawAppOperatingWorkspace } from './rawAppWorkspace'
|
||||
|
||||
const getOpWs = getRawAppOperatingWorkspace()
|
||||
let opWs = $derived(getOpWs?.() ?? $workspaceStore)
|
||||
|
||||
interface Props {
|
||||
runnables: Record<string, Runnable>
|
||||
@@ -36,12 +40,7 @@
|
||||
<RawAppInlineScripRunnable
|
||||
{appPath}
|
||||
on:createScriptFromInlineScript={(e) => {
|
||||
createScriptFromInlineScript(
|
||||
selectedRunnable ?? '',
|
||||
e.detail,
|
||||
$workspaceStore ?? '',
|
||||
appPath
|
||||
)
|
||||
createScriptFromInlineScript(selectedRunnable ?? '', e.detail, opWs ?? '', appPath)
|
||||
}}
|
||||
on:delete={() => {
|
||||
if (selectedRunnable) {
|
||||
|
||||
@@ -14,6 +14,10 @@
|
||||
import type { InputType } from '../apps/inputType'
|
||||
import Select from '$lib/components/select/Select.svelte'
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import { getRawAppOperatingWorkspace } from './rawAppWorkspace'
|
||||
|
||||
const getOpWs = getRawAppOperatingWorkspace()
|
||||
let opWs = $derived(getOpWs?.() ?? $workspaceStore)
|
||||
|
||||
// Build ctx properties with current user's actual values
|
||||
let ctxProperties = $derived([
|
||||
@@ -31,7 +35,7 @@
|
||||
{
|
||||
value: 'workspace',
|
||||
label: 'Workspace',
|
||||
subtitle: `string — "${$workspaceStore ?? 'unknown'}"`
|
||||
subtitle: `string — "${opWs ?? 'unknown'}"`
|
||||
},
|
||||
{ value: 'author', label: 'Author', subtitle: `string — "${$userStore?.email ?? 'unknown'}"` }
|
||||
])
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
import DrawerContent from '../common/drawer/DrawerContent.svelte'
|
||||
import Editor from '$lib/components/Editor.svelte'
|
||||
import { Pane, Splitpanes } from 'svelte-splitpanes'
|
||||
import { getRawAppOperatingWorkspace } from './rawAppWorkspace'
|
||||
|
||||
const getOpWs = getRawAppOperatingWorkspace()
|
||||
let opWs = $derived(getOpWs?.() ?? $workspaceStore)
|
||||
|
||||
let open = $state(false)
|
||||
let files: Record<string, string> = $state({})
|
||||
@@ -20,10 +24,10 @@
|
||||
}
|
||||
|
||||
async function load() {
|
||||
if (!$workspaceStore) return
|
||||
if (!opWs) return
|
||||
loading = true
|
||||
try {
|
||||
const res = (await WorkspaceService.getSharedUi({ workspace: $workspaceStore })) as any
|
||||
const res = (await WorkspaceService.getSharedUi({ workspace: opWs })) as any
|
||||
files = res.files ?? {}
|
||||
version = res.version ?? 0
|
||||
editedBy = res.edited_by ?? ''
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import type { Policy } from '$lib/gen'
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { getRawAppOperatingWorkspace } from './rawAppWorkspace'
|
||||
import Modal from '$lib/components/common/modal/Modal.svelte'
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
import TextInput from '$lib/components/text_input/TextInput.svelte'
|
||||
@@ -60,8 +61,14 @@
|
||||
let preWhitelistedTables = $state<DataTableRef[]>([])
|
||||
let dataTableDrawer: RawAppDataTableDrawer | undefined = $state()
|
||||
|
||||
const datatables = createDatatablesResource(() => $workspaceStore)
|
||||
const schemas = createSchemasResource(() => selectedDatatable)
|
||||
const getOpWs = getRawAppOperatingWorkspace()
|
||||
let opWs = $derived(getOpWs?.() ?? $workspaceStore)
|
||||
|
||||
const datatables = createDatatablesResource(() => opWs)
|
||||
const schemas = createSchemasResource(
|
||||
() => selectedDatatable,
|
||||
() => opWs
|
||||
)
|
||||
|
||||
const availableDatatables = $derived(datatables.current)
|
||||
const availableSchemas = $derived(schemas.current)
|
||||
@@ -115,11 +122,11 @@
|
||||
async function start(withPrompt: boolean) {
|
||||
const template = templates[selectedTemplateIndex]
|
||||
|
||||
if (schemaMode === 'new' && newSchemaName && selectedDatatable && $workspaceStore) {
|
||||
if (schemaMode === 'new' && newSchemaName && selectedDatatable && opWs) {
|
||||
try {
|
||||
const { dbSchemaOpsWithPreviewScripts } = await import('$lib/components/dbOps')
|
||||
const dbOps = dbSchemaOpsWithPreviewScripts({
|
||||
workspace: $workspaceStore,
|
||||
workspace: opWs,
|
||||
input: {
|
||||
type: 'database',
|
||||
resourceType: 'postgresql',
|
||||
|
||||
@@ -25,22 +25,29 @@ export function createDatatablesResource(getWorkspace: () => string | undefined)
|
||||
* Creates a resource that loads schemas for a given datatable.
|
||||
* The getDatatable getter is used as a reactive dependency - when it changes, schemas are refetched.
|
||||
*/
|
||||
export function createSchemasResource(getDatatable: () => string | undefined) {
|
||||
return resource<string[]>([() => getDatatable() ?? ''], async () => {
|
||||
export function createSchemasResource(
|
||||
getDatatable: () => string | undefined,
|
||||
getWorkspace: () => string | undefined = () => get(workspaceStore)
|
||||
) {
|
||||
return resource<string[]>([() => getDatatable() ?? '', () => getWorkspace() ?? ''], async () => {
|
||||
const datatable = getDatatable()
|
||||
const workspace = get(workspaceStore)
|
||||
const workspace = getWorkspace()
|
||||
if (!datatable || !workspace) return []
|
||||
|
||||
const resourcePath = `datatable://${datatable}`
|
||||
// Key the schema cache by workspace too: a datatable of the same name can
|
||||
// exist in both the nav and the acting workspace, so `datatable://<name>`
|
||||
// alone would let one workspace's schema be reused for the other.
|
||||
const cacheKey = `${workspace}:${resourcePath}`
|
||||
const schemas = get(dbSchemas)
|
||||
let dbSchema = schemas[resourcePath]
|
||||
let dbSchema = schemas[cacheKey]
|
||||
|
||||
if (!dbSchema) {
|
||||
try {
|
||||
schemas[resourcePath] = await getDbSchemas('postgresql', resourcePath, workspace, (msg) =>
|
||||
schemas[cacheKey] = await getDbSchemas('postgresql', resourcePath, workspace, (msg) =>
|
||||
console.error('Schema error:', msg)
|
||||
)
|
||||
dbSchema = get(dbSchemas)[resourcePath]
|
||||
dbSchema = get(dbSchemas)[cacheKey]
|
||||
} catch (e) {
|
||||
console.error(`Failed to load schema for ${datatable}:`, e)
|
||||
return []
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { getContext, setContext } from 'svelte'
|
||||
|
||||
// The workspace a raw-app editor operates on. In a session preview this is the
|
||||
// session's acting workspace, which differs from the navigation `$workspaceStore`
|
||||
// (a session deliberately leaves the nav store on the workspace the top nav
|
||||
// points at). RawAppEditor provides it once; the sidebar sub-components (inline
|
||||
// scripts, datatable/shared-UI drawers, DB selector, …) read it so their lookups
|
||||
// target the workspace the app actually lives in rather than the nav workspace.
|
||||
//
|
||||
// A getter (not a value) so the live `$derived` opWorkspace is read reactively at
|
||||
// each call site. Consumers fall back to `$workspaceStore` when unset — e.g. the
|
||||
// full-page app editor, where the nav workspace IS the operating workspace.
|
||||
|
||||
const KEY = 'RawAppOperatingWorkspace'
|
||||
|
||||
export function setRawAppOperatingWorkspace(get: () => string | undefined): void {
|
||||
setContext(KEY, get)
|
||||
}
|
||||
|
||||
export function getRawAppOperatingWorkspace(): (() => string | undefined) | undefined {
|
||||
return getContext(KEY)
|
||||
}
|
||||
@@ -11,11 +11,14 @@ import { generateRandomString } from '$lib/utils'
|
||||
*/
|
||||
export async function processSecretArgs(
|
||||
args: Record<string, any>,
|
||||
schema: Schema | undefined
|
||||
schema: Schema | undefined,
|
||||
// Workspace the ephemeral secret variable is created in — must match the
|
||||
// workspace the preview job runs in, else $jsonvar: resolves to a missing var.
|
||||
forceWorkspace?: string
|
||||
): Promise<Record<string, any>> {
|
||||
if (!schema?.properties) return args
|
||||
|
||||
const workspace = get(workspaceStore)
|
||||
const workspace = forceWorkspace ?? get(workspaceStore)
|
||||
const user = get(userStore)
|
||||
if (!workspace || !user) return args
|
||||
|
||||
|
||||
@@ -33,6 +33,10 @@
|
||||
limitPayloadSize?: boolean
|
||||
noBorder?: boolean
|
||||
captureActiveIndicator?: boolean | undefined
|
||||
// Workspace to scope capture list/get/delete calls to. Defaults to the nav
|
||||
// `$workspaceStore`; an AI-session live editor passes the session's acting
|
||||
// workspace (a fork) so captures hit the right workspace.
|
||||
workspace?: string
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -47,9 +51,12 @@
|
||||
fullHeight = true,
|
||||
limitPayloadSize = false,
|
||||
noBorder = false,
|
||||
captureActiveIndicator = undefined
|
||||
captureActiveIndicator = undefined,
|
||||
workspace = undefined
|
||||
}: Props = $props()
|
||||
|
||||
let ws = $derived(workspace ?? $workspaceStore)
|
||||
|
||||
let selected: number | undefined = $state(undefined)
|
||||
let testKind: 'preprocessor' | 'main' = $state('main')
|
||||
let isEmpty: boolean = $state(true)
|
||||
@@ -94,7 +101,7 @@
|
||||
function initLoadCaptures(kind: 'preprocessor' | 'main' = testKind) {
|
||||
const loadInputsPageFn = async (page: number, perPage: number) => {
|
||||
const captures = await CaptureService.listCaptures({
|
||||
workspace: $workspaceStore!,
|
||||
workspace: ws!,
|
||||
runnableKind: isFlow ? 'flow' : 'script',
|
||||
path: path ?? '',
|
||||
triggerKind: captureType,
|
||||
@@ -114,7 +121,7 @@
|
||||
payloadData: 'Too big to display here, select to view',
|
||||
getFullCapture: () =>
|
||||
CaptureService.getCapture({
|
||||
workspace: $workspaceStore!,
|
||||
workspace: ws!,
|
||||
id: capture.id
|
||||
})
|
||||
}
|
||||
@@ -148,7 +155,7 @@
|
||||
|
||||
const deleteInputFn = async (id: any) => {
|
||||
await CaptureService.deleteCapture({
|
||||
workspace: $workspaceStore!,
|
||||
workspace: ws!,
|
||||
id
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@ import { get } from 'svelte/store'
|
||||
import { ScriptService } from './gen'
|
||||
import { workspaceStore } from './stores'
|
||||
|
||||
export async function findNextAvailablePath(path: string): Promise<string> {
|
||||
export async function findNextAvailablePath(path: string, workspace?: string): Promise<string> {
|
||||
try {
|
||||
await ScriptService.getScriptByPath({
|
||||
workspace: get(workspaceStore)!,
|
||||
workspace: workspace ?? get(workspaceStore)!,
|
||||
path
|
||||
})
|
||||
|
||||
@@ -17,7 +17,7 @@ export async function findNextAvailablePath(path: string): Promise<string> {
|
||||
|
||||
path = `${path}_${Number(version) + 1}`
|
||||
|
||||
return findNextAvailablePath(path)
|
||||
return findNextAvailablePath(path, workspace)
|
||||
} catch (e) {
|
||||
// Catching an error means the path is available
|
||||
return path
|
||||
|
||||
@@ -189,7 +189,12 @@ export function processLangs(selected: string | undefined, langs: string[]): str
|
||||
|
||||
export const defaultScriptLanguages = Object.fromEntries(scriptLanguagesArray)
|
||||
|
||||
export async function getScriptByPath(path: string): Promise<{
|
||||
export async function getScriptByPath(
|
||||
path: string,
|
||||
// The acting workspace when called from a session live editor; defaults to
|
||||
// the navigation workspace for full-page callers.
|
||||
workspace?: string
|
||||
): Promise<{
|
||||
content: string
|
||||
language: SupportedLanguage
|
||||
schema: any
|
||||
@@ -216,7 +221,7 @@ export async function getScriptByPath(path: string): Promise<{
|
||||
}
|
||||
} else {
|
||||
const script = await ScriptService.getScriptByPath({
|
||||
workspace: get(workspaceStore)!,
|
||||
workspace: workspace ?? get(workspaceStore)!,
|
||||
path: path ?? ''
|
||||
})
|
||||
return {
|
||||
@@ -234,9 +239,9 @@ export async function getScriptByPath(path: string): Promise<{
|
||||
}
|
||||
}
|
||||
|
||||
export async function getLatestHashForScript(path: string): Promise<string> {
|
||||
export async function getLatestHashForScript(path: string, workspace?: string): Promise<string> {
|
||||
const script = await ScriptService.getScriptByPath({
|
||||
workspace: get(workspaceStore)!,
|
||||
workspace: workspace ?? get(workspaceStore)!,
|
||||
path: path ?? ''
|
||||
})
|
||||
return script.hash
|
||||
|
||||
Reference in New Issue
Block a user