mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-24 16:04:00 +00:00
* Remove $$props.field usage * Rename slots to ensure no hyphen * _props * _trigger * OnSelectedIteration type correct capitalization * rename _content * Remove afterUpdate * Migrate everything to svelte 5 * array bind * Fix popover * type never * nit fixes * Fixed many trivial errors * onClick * Fix errors * use let: * nit typing * fix: wrap state_referenced_locally vars with untrack() Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Add untrack import * Fix all syntax errors due to untrack migration * Fix undefined errors * Fix more undefined errors * untrack(() => initialOpen) * svelte-ignore * Fix state_descriptors_fixed error in Chart.svelte Use $state.snapshot() to pass plain copies of data/options to Chart.js instead of $state proxies. Chart.js's listenArrayEvents tries to define property descriptors on data arrays, which Svelte 5 proxies reject. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * nit typing * Merge issue * Fix "path is not set" error in resource picker / editor * Fix InputTransformForm error when rerunning some flows * fix npm run check --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
223 lines
6.8 KiB
Svelte
223 lines
6.8 KiB
Svelte
<script lang="ts">
|
|
import { clone, pluralize } from '$lib/utils'
|
|
import { Pyramid } from 'lucide-svelte'
|
|
import { twMerge } from 'tailwind-merge'
|
|
import { Popover } from '../meltComponents'
|
|
import S3FilePicker from '../S3FilePicker.svelte'
|
|
import {
|
|
assetsEq,
|
|
formatAssetAccessType,
|
|
formatAssetKind,
|
|
getAccessType,
|
|
type Asset,
|
|
type AssetWithAltAccessType
|
|
} from './lib'
|
|
import { untrack } from 'svelte'
|
|
import { ResourceService, WorkspaceService } from '$lib/gen'
|
|
import { workspaceStore } from '$lib/stores'
|
|
import Tooltip from '../meltComponents/Tooltip.svelte'
|
|
import Tooltip2 from '../Tooltip.svelte'
|
|
import ResourceEditorDrawer from '../ResourceEditorDrawer.svelte'
|
|
import type { Placement } from '@floating-ui/core'
|
|
import AssetButtons from './AssetButtons.svelte'
|
|
import ToggleButtonGroup from '../common/toggleButton-v2/ToggleButtonGroup.svelte'
|
|
import ToggleButton from '../common/toggleButton-v2/ToggleButton.svelte'
|
|
import { resource } from 'runed'
|
|
|
|
let {
|
|
assets,
|
|
enableChangeAnimation = true,
|
|
size = 'xs',
|
|
noBtnText = false,
|
|
popoverPlacement = 'bottom-end',
|
|
disableLiTooltip = false,
|
|
onHoverLi,
|
|
liSubtitle
|
|
}: {
|
|
assets: AssetWithAltAccessType[]
|
|
enableChangeAnimation?: boolean
|
|
size?: 'xs' | '3xs'
|
|
noBtnText?: boolean
|
|
popoverPlacement?: Placement
|
|
disableLiTooltip?: boolean
|
|
onHoverLi?: (asset: Asset, eventType: 'enter' | 'leave') => void
|
|
liSubtitle?: (asset: Asset) => string
|
|
} = $props()
|
|
|
|
let prevAssets = $state<typeof assets>([])
|
|
let blueBgDiv: HTMLDivElement | undefined = $state()
|
|
|
|
let s3FilePicker: S3FilePicker | undefined = $state()
|
|
let resourceEditorDrawer: ResourceEditorDrawer | undefined = $state()
|
|
let isOpen = $state(false)
|
|
let resourceDataCache: Record<string, string | undefined> = $state({})
|
|
|
|
$effect(() => {
|
|
if (!enableChangeAnimation) {
|
|
if (blueBgDiv) {
|
|
blueBgDiv.classList.remove('animate-fade-out')
|
|
}
|
|
}
|
|
})
|
|
|
|
let datatables = resource([], () =>
|
|
WorkspaceService.listDataTables({ workspace: $workspaceStore ?? '' })
|
|
)
|
|
let ducklakes = resource([], () =>
|
|
WorkspaceService.listDucklakes({ workspace: $workspaceStore ?? '' })
|
|
)
|
|
|
|
$effect(() => {
|
|
assets
|
|
untrack(() => {
|
|
if (assetsEq(assets, prevAssets)) return
|
|
prevAssets = clone(assets)
|
|
|
|
// Replay animation
|
|
if (blueBgDiv && enableChangeAnimation) {
|
|
blueBgDiv.classList.add('animate-fade-out')
|
|
blueBgDiv.style.animation = 'none'
|
|
blueBgDiv.offsetHeight /* trigger reflow */
|
|
blueBgDiv.style.animation = ''
|
|
}
|
|
|
|
for (const asset of assets) {
|
|
if (asset.kind == 'resource') {
|
|
let truncatedPath = asset.path.split('?table=')[0]
|
|
if (truncatedPath in resourceDataCache) continue
|
|
resourceDataCache[truncatedPath] = undefined // avoid fetching multiple times because of async
|
|
ResourceService.getResource({ path: truncatedPath, workspace: $workspaceStore! })
|
|
.then((r) => (resourceDataCache[truncatedPath] = r.resource_type))
|
|
.catch((err) => console.error("Couldn't fetch resource", truncatedPath, err))
|
|
}
|
|
}
|
|
})
|
|
})
|
|
</script>
|
|
|
|
<Popover
|
|
floatingConfig={{ strategy: 'absolute', placement: popoverPlacement }}
|
|
usePointerDownOutside
|
|
closeOnOtherPopoverOpen
|
|
bind:isOpen
|
|
escapeBehavior="ignore"
|
|
>
|
|
{#snippet trigger()}
|
|
<div
|
|
class={twMerge(
|
|
size === '3xs' ? 'h-[1.6rem]' : 'py-1.5',
|
|
'text-xs flex items-center gap-1.5 px-2 rounded-md relative',
|
|
'border',
|
|
'bg-surface hover:bg-surface-hover active:bg-surface',
|
|
'transition-all hover:text-primary backdrop-blur-md cursor-pointer'
|
|
)}
|
|
>
|
|
<div
|
|
bind:this={blueBgDiv}
|
|
class="absolute pointer-events-none bg-slate-300 dark:bg-[#576278] inset-0 rounded-md opacity-0"
|
|
></div>
|
|
<Pyramid size={size === '3xs' ? 13 : 16} class="z-10" />
|
|
<span
|
|
class={twMerge('z-10 font-normal', size === '3xs' ? 'text-3xs mt-[0.08rem]' : 'text-xs')}
|
|
>
|
|
{noBtnText ? assets.length : pluralize(assets.length, 'asset')}
|
|
</span>
|
|
</div>
|
|
{/snippet}
|
|
{#snippet content()}
|
|
<ul class="divide-y rounded-md">
|
|
{#each assets as asset}
|
|
{@const ducklakeNotFound =
|
|
asset.kind === 'ducklake' &&
|
|
ducklakes.current &&
|
|
!ducklakes.current.find((name) => name === asset.path.split('/')[0])}
|
|
{@const datatableNotFound =
|
|
asset.kind === 'datatable' &&
|
|
datatables.current &&
|
|
!datatables.current.find((name) => name === asset.path.split('/')[0])}
|
|
<li
|
|
class="text-sm px-3 h-12 flex gap-3 items-center"
|
|
onmouseenter={() => onHoverLi?.(asset, 'enter')}
|
|
onmouseleave={() => onHoverLi?.(asset, 'leave')}
|
|
>
|
|
<Popover
|
|
contentClasses="py-2 px-4 flex flex-col gap-2"
|
|
disablePopup={!!asset.access_type}
|
|
>
|
|
{#snippet trigger()}
|
|
<div
|
|
class={twMerge(
|
|
'text-xs font-normal border text-primary w-10 p-1 text-center rounded-md',
|
|
!asset.access_type ? 'text-orange-600 !border-orange-600' : '',
|
|
!asset.access_type ? 'hover:bg-surface active:opacity-80' : ''
|
|
)}
|
|
>
|
|
{formatAssetAccessType(getAccessType(asset)) ?? '?'}
|
|
</div>
|
|
{/snippet}
|
|
{#snippet content()}
|
|
{#if !asset.access_type}
|
|
<span class="text-sm text-primary leading-4">
|
|
Could not infer automatically <br />
|
|
<span class="text-xs">Please select manually </span>
|
|
</span>
|
|
<div class="flex items-center gap-2">
|
|
<ToggleButtonGroup
|
|
allowEmpty
|
|
bind:selected={asset.alt_access_type}
|
|
class="max-w-fit"
|
|
>
|
|
{#snippet children({ item })}
|
|
<ToggleButton value="r" label="Read" {item} />
|
|
<ToggleButton value="w" label="Write" {item} />
|
|
<ToggleButton value="rw" label="Read/Write" {item} />
|
|
{/snippet}
|
|
</ToggleButtonGroup>
|
|
|
|
<Tooltip2>
|
|
This is used to determine if the asset should be displayed as an input or an
|
|
output node in the flow editor
|
|
</Tooltip2>
|
|
</div>
|
|
{/if}
|
|
{/snippet}
|
|
</Popover>
|
|
<div class="flex flex-col flex-1">
|
|
<Tooltip class="select-none w-48 truncate" disablePopup={disableLiTooltip}>
|
|
{asset.path}
|
|
{#snippet text()}
|
|
{asset.path}
|
|
{/snippet}
|
|
</Tooltip>
|
|
<span class="text-xs text-primary select-none">
|
|
{liSubtitle?.(asset) ??
|
|
formatAssetKind({
|
|
...asset,
|
|
...(asset.kind === 'resource'
|
|
? {
|
|
metadata: {
|
|
resource_type: resourceDataCache[asset.path.split('?table=')[0]]
|
|
}
|
|
}
|
|
: {})
|
|
})}
|
|
</span>
|
|
</div>
|
|
|
|
<AssetButtons
|
|
onClick={() => (isOpen = false)}
|
|
{asset}
|
|
{resourceDataCache}
|
|
{resourceEditorDrawer}
|
|
{s3FilePicker}
|
|
{ducklakeNotFound}
|
|
{datatableNotFound}
|
|
/>
|
|
</li>
|
|
{/each}
|
|
</ul>
|
|
{/snippet}
|
|
</Popover>
|
|
<S3FilePicker bind:this={s3FilePicker} readOnlyMode />
|
|
<ResourceEditorDrawer bind:this={resourceEditorDrawer} />
|