Files
windmill/frontend/src/lib/components/flows/FlowAssetsHandler.svelte
Diego Imbert 2ab5345e61 Assets refactor (#6217)
* Moved logic to FlowAssetsProvider

* Remove assetsMap in flow

* do not parse everything on mount + only check for missing assets fields

* add assets field in backend

* remove fallbackAccessTypes

* better structure and less queries / parsing

* Fix assets not showing when pulling raw_flow from jobs

* flow assets ctx for job run

* Fix transitive assets fetching

* Fix input args asset node

* enablePathScriptAndFlowAssets flag

* edit btn for variable

* untrack refresh

* move parseInputArgsAssets

* Assets tab in runs

* Update FlowStatusViewerInner to svelte 5 + fix asset sync bug

* avoid toast error on bad resource

* fetch res metadata for input arg asset

* Job assets viewer in run page

* r/w selector

* remove indigo badge

* store alt_access_type state in ScriptEditor

* Don't parse assets in flow script editor

* Add alt_access_type in backend

* show Read as selected by default to avoid giving the feeling of having made a decision

* keep alt_access_type when reparsing in flow raw scripts

* Remove variable asset kind, and save assets for scripts

* remove all backend asset parsing

* R/W/RW selector button nits

* fix insert into assets not saving alt access type

* support named arguments in python asset parser

* improve asset usage drawer R/W indicator

* update legacy $res: syntax

* reactivity issue

* remove last variable asset stuff

* sqlx prepare

* tooltip explainer

* deprecated variable asset nit
2025-07-17 22:15:01 +00:00

163 lines
5.2 KiB
Svelte

<script lang="ts" module>
export function initFlowGraphAssetsCtx({
getModules
}: {
getModules: () => FlowModule[]
}): FlowGraphAssetContext {
let s = $state({
val: {
selectedAsset: undefined,
dbManagerDrawer: undefined,
s3FilePicker: undefined,
resourceEditorDrawer: undefined,
resourceMetadataCache: {},
additionalAssetsMap: {},
computeAssetsCount: (asset) => {
return getAllModules(getModules())
.flatMap((m) => getFlowModuleAssets(m, s.val.additionalAssetsMap) ?? [])
.filter((a) => assetEq(asset, a)).length
}
}
} satisfies FlowGraphAssetContext)
return s
}
</script>
<script lang="ts">
import { inferAssets } from '$lib/infer'
import {
assetEq,
getFlowModuleAssets,
type AssetWithAccessType,
type AssetWithAltAccessType
} from '../assets/lib'
import OnChange from '../common/OnChange.svelte'
import { getAllModules } from './flowExplorer'
import { getContext, untrack } from 'svelte'
import type { FlowGraphAssetContext } from './types'
import {
AssetService,
ResourceService,
type AssetUsageKind,
type FlowModule,
type RawScript
} from '$lib/gen'
import { deepEqual } from 'fast-equals'
import { workspaceStore } from '$lib/stores'
import S3FilePicker from '../S3FilePicker.svelte'
import DbManagerDrawer from '../DBManagerDrawer.svelte'
import ResourceEditorDrawer from '../ResourceEditorDrawer.svelte'
let {
modules,
enableParser = false,
enableDbExplore = false,
enablePathScriptAndFlowAssets = false
}: {
modules: FlowModule[]
enableParser?: boolean
enableDbExplore?: boolean
enablePathScriptAndFlowAssets?: boolean
} = $props()
const flowGraphAssetsCtx = getContext<FlowGraphAssetContext | undefined>('FlowGraphAssetContext')
let allModules = $derived(getAllModules(modules))
// Fetch resource metadata for the ExploreAssetButton
const resMetadataCache = $derived(flowGraphAssetsCtx?.val.resourceMetadataCache)
$effect(() => {
if (!resMetadataCache || !enableDbExplore) return
const assets: AssetWithAccessType[] =
allModules.flatMap(
(m) => getFlowModuleAssets(m, flowGraphAssetsCtx?.val.additionalAssetsMap) ?? []
) ?? []
for (const asset of assets) {
if (asset.kind !== 'resource' || asset.path in resMetadataCache) continue
resMetadataCache[asset.path] = undefined // avoid fetching multiple times because of async
ResourceService.getResource({ path: asset.path, workspace: $workspaceStore! })
.then((r) => (resMetadataCache[asset.path] = { resource_type: r.resource_type }))
.catch((err) => {
console.error("Couldn't fetch resource", asset.path, err)
})
}
})
// Fetch transitive assets (path scripts and flows)
$effect(() => {
if (!$workspaceStore || !flowGraphAssetsCtx || !enablePathScriptAndFlowAssets) return
let usages: { path: string; kind: AssetUsageKind }[] = []
let modIds: string[] = []
for (const mod of allModules) {
if (mod.id in flowGraphAssetsCtx.val.additionalAssetsMap) continue
flowGraphAssetsCtx.val.additionalAssetsMap[mod.id] = [] // avoid fetching multiple times because of async
if (mod.value.type === 'flow' || mod.value.type === 'script') {
usages.push({ path: mod.value.path, kind: mod.value.type })
modIds.push(mod.id)
}
}
if (usages.length) {
AssetService.listAssetsByUsage({
workspace: $workspaceStore,
requestBody: { usages }
}).then((result) => {
result.forEach((assets, idx) => {
flowGraphAssetsCtx.val.additionalAssetsMap[modIds[idx]] = assets
})
})
}
})
// Prune all additionalAssetsMap entries from deleted modules
$effect(() => {
if (!flowGraphAssetsCtx) return
const modulesSet = new Set(allModules.map((m) => m.id))
for (const key of Object.keys(flowGraphAssetsCtx.val.additionalAssetsMap)) {
if (!modulesSet.has(key)) {
delete flowGraphAssetsCtx.val.additionalAssetsMap[key]
}
}
})
async function parseAndUpdateRawScriptModule(v: RawScript) {
try {
let parsedAssets: AssetWithAltAccessType[] = await inferAssets(v.language, v.content)
for (const asset of parsedAssets) {
const old = v.assets?.find((a) => assetEq(a, asset))
if (old?.alt_access_type) asset.alt_access_type = old.alt_access_type
}
if (!deepEqual(v.assets, parsedAssets)) v.assets = parsedAssets
} catch (e) {}
}
// Check for raw script modules whose assets were not parsed. Useful for flows created
// before the assets feature was introduced.
$effect(() => {
if (!enableParser) return
untrack(() => {
setTimeout(() => {
for (const mod of allModules) {
if (mod.value.type === 'rawscript' && mod.value.assets === undefined) {
console.log('RawScript module', mod.id, 'without assets field, parsing')
parseAndUpdateRawScriptModule(mod.value)
}
}
}, 500) // ensure modules are loaded
})
})
</script>
{#if enableParser}
{#each allModules as mod (mod.id)}
{#if mod.value.type === 'rawscript'}
{@const v = mod.value}
<OnChange key={v.content} onChange={() => parseAndUpdateRawScriptModule(v)} />
{/if}
{/each}
{/if}
{#if flowGraphAssetsCtx}
<S3FilePicker bind:this={flowGraphAssetsCtx.val.s3FilePicker} readOnlyMode />
<DbManagerDrawer bind:this={flowGraphAssetsCtx.val.dbManagerDrawer} />
<ResourceEditorDrawer bind:this={flowGraphAssetsCtx.val.resourceEditorDrawer} />
{/if}