mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-26 00:03:59 +00:00
update
This commit is contained in:
Generated
+2
-48
@@ -844,7 +844,6 @@
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.0.tgz",
|
||||
"integrity": "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -856,7 +855,6 @@
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.0.tgz",
|
||||
"integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -867,7 +865,6 @@
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz",
|
||||
"integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -1357,7 +1354,6 @@
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz",
|
||||
"integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -1514,7 +1510,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1531,7 +1526,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1548,7 +1542,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1565,7 +1558,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1582,7 +1574,6 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1599,7 +1590,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1616,7 +1606,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1633,7 +1622,6 @@
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1650,7 +1638,6 @@
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1667,7 +1654,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1684,7 +1670,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1701,7 +1686,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1718,7 +1702,6 @@
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -1735,7 +1718,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1752,7 +1734,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2058,7 +2039,6 @@
|
||||
"version": "0.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
|
||||
"integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -6834,7 +6814,7 @@
|
||||
"version": "1.21.7",
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
|
||||
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"jiti": "bin/jiti.js"
|
||||
@@ -7333,7 +7313,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -7354,7 +7333,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -7375,7 +7353,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -7396,7 +7373,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -7417,7 +7393,6 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -7438,7 +7413,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -7459,7 +7433,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -7480,7 +7453,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -7501,7 +7473,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -7522,7 +7493,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -7543,7 +7513,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -12112,21 +12081,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/svelte-check/node_modules/picomatch": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/svelte-eslint-parser": {
|
||||
"version": "0.43.0",
|
||||
"resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.43.0.tgz",
|
||||
@@ -12857,7 +12811,7 @@
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
|
||||
@@ -70,6 +70,15 @@
|
||||
path: string
|
||||
unsaved?: boolean
|
||||
}) => Promise<string | undefined>
|
||||
// Page-supplied dispatch for the per-runnable-node action menu.
|
||||
// Drafts are discarded immediately by the page; persisted scripts
|
||||
// are routed through the details pane's archive/delete confirmation
|
||||
// flow. Without this callback, the node menu button is hidden.
|
||||
onRunnableMenuRemove?: (info: {
|
||||
runnable_kind: 'script' | 'flow'
|
||||
path: string
|
||||
unsaved?: boolean
|
||||
}) => void
|
||||
}
|
||||
let {
|
||||
graph,
|
||||
@@ -80,7 +89,8 @@
|
||||
pathPrefix = '',
|
||||
defaultPathSuffix = '',
|
||||
defaultScheduleCron = '',
|
||||
onRunProducer
|
||||
onRunProducer,
|
||||
onRunnableMenuRemove
|
||||
}: Props = $props()
|
||||
|
||||
const ADD_NODE_ID = '__add__'
|
||||
@@ -178,7 +188,15 @@
|
||||
in_pipeline: r.in_pipeline ?? false,
|
||||
partition_kind: r.partition_kind,
|
||||
freshness: r.freshness,
|
||||
unsaved: r.unsaved ?? false
|
||||
unsaved: r.unsaved ?? false,
|
||||
onRequestRemove: onRunnableMenuRemove
|
||||
? () =>
|
||||
onRunnableMenuRemove({
|
||||
runnable_kind: r.usage_kind,
|
||||
path: r.path,
|
||||
unsaved: r.unsaved ?? false
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -85,6 +85,12 @@
|
||||
// true on success; false on collision/validation failure so the
|
||||
// popover can keep itself open and surface the error inline.
|
||||
onDraftPathChange?: (oldPath: string, newPath: string) => boolean | string
|
||||
// Bumped by the parent (e.g. from the runnable-node action menu) to
|
||||
// auto-open the archive/delete confirmation modal for the currently
|
||||
// loaded persisted script. No-ops while the pane is showing a draft
|
||||
// or is still loading. Counter pattern (not a boolean) so successive
|
||||
// triggers re-fire even if the modal was just closed.
|
||||
requestRemoveSignal?: number
|
||||
}
|
||||
let {
|
||||
selection,
|
||||
@@ -100,7 +106,8 @@
|
||||
runsRefreshKey,
|
||||
runsPendingJobId,
|
||||
pathPrefix = '',
|
||||
onDraftPathChange
|
||||
onDraftPathChange,
|
||||
requestRemoveSignal
|
||||
}: Props = $props()
|
||||
|
||||
// Bumped when the runs panel reports a watched job has reached a
|
||||
@@ -147,6 +154,21 @@
|
||||
let removing = $state(false)
|
||||
let canHardDelete = $derived(!!($userStore?.is_admin || $userStore?.is_super_admin))
|
||||
|
||||
// React to the parent's remove-signal counter and pop the same modal
|
||||
// the in-pane trash button uses. Skipped for drafts (the parent calls
|
||||
// `onDiscard` directly there) and while the script hasn't loaded.
|
||||
// Counter pattern (not boolean) so successive triggers re-fire even if
|
||||
// the modal was just closed without acting.
|
||||
let lastRemoveSignal = $state<number | undefined>(undefined)
|
||||
$effect(() => {
|
||||
if (requestRemoveSignal === undefined) return
|
||||
if (requestRemoveSignal === lastRemoveSignal) return
|
||||
lastRemoveSignal = requestRemoveSignal
|
||||
if (isDraft) return
|
||||
if (!script || !script.hash) return
|
||||
removeOpen = true
|
||||
})
|
||||
|
||||
async function archive() {
|
||||
if (!script || !script.hash) return
|
||||
removing = true
|
||||
@@ -527,7 +549,7 @@
|
||||
Failed to load: {scriptRes.error.message}
|
||||
</div>
|
||||
{:else if script}
|
||||
{#key (script.hash ?? 'draft') + script.language}
|
||||
{#key (script.hash ?? `draft:${script.path}`) + script.language}
|
||||
<ScriptEditor
|
||||
showCaptures={false}
|
||||
noSyncFromGithub
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { Handle, Position } from '@xyflow/svelte'
|
||||
import { Code2, GitBranch, Layers, Timer } from 'lucide-svelte'
|
||||
import { Code2, EllipsisVertical, GitBranch, Layers, Timer, Trash2 } from 'lucide-svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { preventDefault, stopPropagation } from 'svelte/legacy'
|
||||
import type { GraphUsageKind } from './types'
|
||||
import { NODE } from '$lib/components/graph/util'
|
||||
import DropdownV2 from '$lib/components/DropdownV2.svelte'
|
||||
import type { Item } from '$lib/utils'
|
||||
|
||||
interface Props {
|
||||
data: {
|
||||
@@ -15,6 +18,11 @@
|
||||
// True for nodes synthesized from local drafts (script not yet
|
||||
// persisted). Same convention as `unsaved` on triggers/edges.
|
||||
unsaved?: boolean
|
||||
// Wired by the canvas. When set, the node renders an
|
||||
// EllipsisVertical hover-button that opens a small action menu —
|
||||
// "Discard" for drafts, "Delete…" (which the page maps to its
|
||||
// archive/delete confirmation flow) for persisted scripts.
|
||||
onRequestRemove?: () => void
|
||||
}
|
||||
}
|
||||
let { data }: Props = $props()
|
||||
@@ -30,9 +38,26 @@
|
||||
? `${data.path} (pipeline member)`
|
||||
: data.path
|
||||
)
|
||||
|
||||
let hover = $state(false)
|
||||
let menuOpen = $state(false)
|
||||
|
||||
let menuItems: Item[] = $derived(
|
||||
data.onRequestRemove
|
||||
? [
|
||||
{
|
||||
displayName: data.unsaved ? 'Discard' : 'Delete…',
|
||||
icon: Trash2,
|
||||
type: 'delete' as const,
|
||||
action: () => data.onRequestRemove?.()
|
||||
}
|
||||
]
|
||||
: []
|
||||
)
|
||||
</script>
|
||||
|
||||
<div class="relative">
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div class="relative" onmouseenter={() => (hover = true)} onmouseleave={() => (hover = false)}>
|
||||
<!--
|
||||
Three visual states that compose:
|
||||
- persisted, non-pipeline: solid 1px gray border, surface-tertiary fill
|
||||
@@ -78,6 +103,40 @@
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if menuItems.length > 0}
|
||||
<!--
|
||||
Hover-revealed action menu, top-right of the node. Mirrors the
|
||||
FlowModuleSchemaItem pattern: position the trigger button just
|
||||
outside the node frame, only render it on hover (or while the
|
||||
menu is open) so the canvas isn't visually cluttered when idle.
|
||||
The pointerdown stop+preventDefault keeps xyflow from selecting
|
||||
the node when the user is reaching for the menu.
|
||||
-->
|
||||
<div class="absolute -top-2 -right-2 h-7 p-1 min-w-7" style="will-change: transform;">
|
||||
<DropdownV2
|
||||
items={menuItems}
|
||||
placement="bottom-end"
|
||||
bind:open={menuOpen}
|
||||
fixedHeight={false}
|
||||
usePointerDownOutside
|
||||
>
|
||||
{#snippet buttonReplacement()}
|
||||
<button
|
||||
class={twMerge(
|
||||
'center-center p-1 text-secondary shadow-sm bg-surface duration-0 hover:bg-surface-tertiary',
|
||||
hover || menuOpen ? 'block' : '!hidden',
|
||||
'shadow-md rounded-md'
|
||||
)}
|
||||
onpointerdown={stopPropagation(preventDefault(() => {}))}
|
||||
title="Actions"
|
||||
>
|
||||
<EllipsisVertical size={12} />
|
||||
</button>
|
||||
{/snippet}
|
||||
</DropdownV2>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<Handle type="target" position={Position.Top} isConnectable={false} />
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { ScriptLang, AssetKind } from '$lib/gen'
|
||||
import { random_adj } from '$lib/components/random_positive_adjetive'
|
||||
|
||||
// What kind of asset the new script will produce. Drives the auto-generated
|
||||
// output annotation, the random output path scheme, and the body skeleton.
|
||||
@@ -82,42 +83,70 @@ export function compatibleOutputKinds(lang: ScriptLang): PipelineOutputKind[] {
|
||||
return LANG_COMPATIBILITY[lang] ?? ['none']
|
||||
}
|
||||
|
||||
// Generates the random suffix portion of an auto-named output asset. Same
|
||||
// 7-char Crockford-ish alphabet we use for script paths so the slugs read
|
||||
// consistently.
|
||||
function randomSlug(len = 7): string {
|
||||
// Short uniqueness backstop appended to the human-readable name, so that two
|
||||
// drafts created in the same session don't collide on `<adj>_<noun>`. 4 chars
|
||||
// of base36 → ~1.7M variants, more than enough alongside the adj×noun prefix.
|
||||
function shortSlug(len = 4): string {
|
||||
const a = 'abcdefghijklmnopqrstuvwxyz0123456789'
|
||||
let out = ''
|
||||
for (let i = 0; i < len; i++) out += a[Math.floor(Math.random() * a.length)]
|
||||
return out
|
||||
}
|
||||
|
||||
// Asset-shaped noun pools. Mirrors the `random_adj() + '_' + namePlaceholder`
|
||||
// convention used by Path.svelte for scripts/flows/apps, but the noun is
|
||||
// chosen to read like the thing the asset *is*: a table for datatable /
|
||||
// ducklake, a dataset / file payload for S3 outputs.
|
||||
const TABLE_NOUNS = [
|
||||
'table',
|
||||
'dataset',
|
||||
'records',
|
||||
'events',
|
||||
'rows',
|
||||
'entries',
|
||||
'metrics',
|
||||
'snapshot'
|
||||
]
|
||||
const DATASET_NOUNS = ['dataset', 'snapshot', 'extract', 'export', 'records', 'batch']
|
||||
const FILE_NOUNS = ['payload', 'output', 'report', 'extract', 'snapshot', 'dump']
|
||||
|
||||
function pick(arr: string[]): string {
|
||||
return arr[Math.floor(Math.random() * arr.length)]
|
||||
}
|
||||
|
||||
// Fully-qualified asset URI for a freshly-created output, given the picked
|
||||
// kind. The path is auto-generated to land under `pipelines/<folder>/` for
|
||||
// s3 / parquet, and uses the more conventional `main/<name>` form for
|
||||
// datatable / ducklake (matches what the schema/preview UI expects). The
|
||||
// name suffix is randomized to avoid collisions when the user adds multiple
|
||||
// downstream scripts in one session.
|
||||
// kind. Names follow `<adjective>_<noun>_<slug>` to read like real assets
|
||||
// (e.g. `nimble_dataset_a3kp`) instead of opaque hashes — matches the
|
||||
// `random_adj()_*` naming we already use for scripts/flows/apps. Layout:
|
||||
// - datatable / ducklake → `main/<adj>_<noun>_<slug>` (table refs)
|
||||
// - s3 parquet / object → `pipelines/<folder>/<adj>_<noun>_<slug>.<ext>`
|
||||
export function autoOutputAsset(
|
||||
kind: PipelineOutputKind,
|
||||
folder: string,
|
||||
language?: ScriptLang
|
||||
): { kind: AssetKind; path: string } | undefined {
|
||||
const slug = randomSlug()
|
||||
const slug = shortSlug()
|
||||
const adj = random_adj()
|
||||
switch (kind) {
|
||||
case 'datatable':
|
||||
return { kind: 'datatable', path: `main/${folder}_${slug}` }
|
||||
return { kind: 'datatable', path: `main/${adj}_${pick(TABLE_NOUNS)}_${slug}` }
|
||||
case 'ducklake':
|
||||
return { kind: 'ducklake', path: `main/${folder}_${slug}` }
|
||||
return { kind: 'ducklake', path: `main/${adj}_${pick(TABLE_NOUNS)}_${slug}` }
|
||||
case 's3_parquet':
|
||||
return { kind: 's3object', path: `pipelines/${folder}/out_${slug}.parquet` }
|
||||
return {
|
||||
kind: 's3object',
|
||||
path: `pipelines/${folder}/${adj}_${pick(DATASET_NOUNS)}_${slug}.parquet`
|
||||
}
|
||||
case 's3_object': {
|
||||
// duckdb's natural output for a generic blob is CSV (one COPY TO
|
||||
// statement). TS/Python templates serialize JSON, so default to
|
||||
// .json for those — keeps the body and the file extension in
|
||||
// sync without the user having to rename anything.
|
||||
const ext = language === 'duckdb' ? 'csv' : 'json'
|
||||
return { kind: 's3object', path: `pipelines/${folder}/out_${slug}.${ext}` }
|
||||
return {
|
||||
kind: 's3object',
|
||||
path: `pipelines/${folder}/${adj}_${pick(FILE_NOUNS)}_${slug}.${ext}`
|
||||
}
|
||||
}
|
||||
case 'none':
|
||||
return undefined
|
||||
@@ -167,6 +196,19 @@ function qualifiedDatatableRef(p: string, defaultSchema = 'public'): string {
|
||||
return `${parsed.schema ?? defaultSchema}.${parsed.table || 'table_name'}`
|
||||
}
|
||||
|
||||
// Catalog-relative table reference. Emits `<schema>.<table>` only when the
|
||||
// asset path explicitly carries a schema (`main/myschema.mytable`); otherwise
|
||||
// returns the bare `<table>`. Used inside DuckDB attached-catalog references
|
||||
// (`pg.<ref>`, `lake.<ref>`) where the asset parser maps a 2-part name like
|
||||
// `pg.foo` straight to `datatable://main/foo` — adding a redundant `public.`
|
||||
// would shift the parsed asset to `datatable://main/public.foo` and silently
|
||||
// desync from the `// Output: ...` annotation.
|
||||
function catalogTableRef(p: string): string {
|
||||
const parsed = parseDatatablePath(p)
|
||||
const table = parsed.table || 'table_name'
|
||||
return parsed.schema ? `${parsed.schema}.${table}` : table
|
||||
}
|
||||
|
||||
// Comment prefix per language; mirrors what the parser accepts.
|
||||
function commentPrefix(lang: ScriptLang): string {
|
||||
switch (lang) {
|
||||
@@ -401,31 +443,48 @@ function bodyDuckdb(ctx: TemplateContext): string {
|
||||
if (output) lines.push(`-- Output: ${assetUri(output)}`)
|
||||
lines.push('')
|
||||
|
||||
// Resolve the catalog db name to ATTACH. Output's db wins when both sides
|
||||
// share a kind (the typical pipeline shape: read from one table, write to
|
||||
// another in the same database). We emit at most one ATTACH per catalog
|
||||
// kind so the generated script stays readable.
|
||||
const datatableDb = (() => {
|
||||
if (output?.kind === 'datatable') return parseDatatablePath(output.path).db
|
||||
if (input?.kind === 'datatable') return parseDatatablePath(input.path).db
|
||||
return undefined
|
||||
})()
|
||||
const ducklakeDb = (() => {
|
||||
if (outputKind === 'ducklake' && output?.kind === 'ducklake')
|
||||
return parseDatatablePath(output.path).db
|
||||
if (input?.kind === 'ducklake') return parseDatatablePath(input.path).db
|
||||
return undefined
|
||||
})()
|
||||
if (datatableDb) {
|
||||
lines.push(`ATTACH 'datatable://${datatableDb}' AS pg;`)
|
||||
}
|
||||
if (ducklakeDb) {
|
||||
lines.push(`ATTACH 'ducklake://${ducklakeDb}' AS lake;`)
|
||||
}
|
||||
if (datatableDb || ducklakeDb) lines.push('')
|
||||
|
||||
const inSql = (() => {
|
||||
if (!input) return null
|
||||
switch (input.kind) {
|
||||
case 's3object':
|
||||
return `read_parquet('s3://${input.path}')`
|
||||
case 'datatable':
|
||||
// `pg` is the attached Postgres catalog; we schema-qualify
|
||||
// the table reference so the SELECT doesn't rely on the
|
||||
// catalog's default schema setting.
|
||||
return `pg.${qualifiedDatatableRef(input.path)}`
|
||||
case 'ducklake': {
|
||||
return null // attach + read shown below
|
||||
}
|
||||
// `pg` is the attached Postgres catalog (see ATTACH above).
|
||||
// Use a 2-part `pg.<table>` ref so the asset parser maps it
|
||||
// back to `datatable://<db>/<table>` — matching the
|
||||
// `// Upstream` annotation. Schema is only emitted if the
|
||||
// asset path explicitly includes one (`main/myschema.mytable`).
|
||||
return `pg.${catalogTableRef(input.path)}`
|
||||
case 'ducklake':
|
||||
return `lake.${catalogTableRef(input.path)}`
|
||||
default:
|
||||
return null
|
||||
}
|
||||
})()
|
||||
|
||||
if (input?.kind === 'ducklake') {
|
||||
lines.push(
|
||||
`-- DuckLake catalog is auto-attached as 'lake' inside windmill duckdb scripts.`,
|
||||
`-- Reference upstream tables as lake.<table_name>.`
|
||||
)
|
||||
}
|
||||
|
||||
switch (outputKind) {
|
||||
case 's3_parquet':
|
||||
if (output) {
|
||||
@@ -451,23 +510,23 @@ function bodyDuckdb(ctx: TemplateContext): string {
|
||||
break
|
||||
case 'ducklake':
|
||||
if (output) {
|
||||
const tableName = output.path.split('/').slice(1).join('_') || 'table_name'
|
||||
const ref = `lake.${catalogTableRef(output.path)}`
|
||||
lines.push(
|
||||
`-- Write into the auto-attached DuckLake catalog.`,
|
||||
`CREATE TABLE IF NOT EXISTS lake.${tableName} AS`,
|
||||
`CREATE TABLE IF NOT EXISTS ${ref} AS`,
|
||||
`SELECT * FROM ${inSql ?? '(SELECT 1 AS placeholder)'};`
|
||||
)
|
||||
}
|
||||
break
|
||||
case 'datatable':
|
||||
if (output) {
|
||||
// Schema-qualified `pg.<schema>.<table>` (rather than
|
||||
// `pg.<table>`) so the CREATE TABLE doesn't silently
|
||||
// land in whatever default schema the attached catalog
|
||||
// resolves to.
|
||||
const ref = `pg.${qualifiedDatatableRef(output.path)}`
|
||||
// 2-part `pg.<table>` so the asset parser resolves the
|
||||
// CREATE TABLE write to `datatable://<db>/<table>` —
|
||||
// matching the `// Output` annotation. Postgres'
|
||||
// search_path defaults to `public`, so the DDL still
|
||||
// lands in the right schema. We only schema-qualify
|
||||
// when the asset path itself carries an explicit schema.
|
||||
const ref = `pg.${catalogTableRef(output.path)}`
|
||||
lines.push(
|
||||
`-- DataTable is exposed as the 'pg' attached database in duckdb scripts.`,
|
||||
`CREATE TABLE IF NOT EXISTS ${ref} AS`,
|
||||
`SELECT * FROM ${inSql ?? '(SELECT 1 AS placeholder)'};`
|
||||
)
|
||||
|
||||
@@ -643,6 +643,11 @@
|
||||
// The most recently dispatched job id — surfaces to AssetRunsPanel so
|
||||
// the new run auto-selects without an extra click.
|
||||
let runsPendingJobId = $state<string | undefined>(undefined)
|
||||
// Counter bumped from the runnable-node action menu to ask the pane to
|
||||
// open its archive/delete confirmation modal for the loaded script.
|
||||
// Counter (vs boolean) so successive triggers re-fire even if the user
|
||||
// dismissed the previous modal without acting.
|
||||
let requestRemoveSignal = $state(0)
|
||||
|
||||
// Producers (write/rw edges) for the currently-selected asset, derived
|
||||
// from `graphWithDraft.edges`. Threaded into the details pane so the
|
||||
@@ -913,6 +918,22 @@
|
||||
}}
|
||||
onAddPipelineScript={(language, scriptPath, source, outputKind) =>
|
||||
openMaterializerDraft(language, scriptPath, [source], outputKind)}
|
||||
onRunnableMenuRemove={(info) => {
|
||||
// Drafts: drop the local entry immediately — the
|
||||
// existing onDiscard pathway already handles this
|
||||
// without a confirm modal. Persisted: select the
|
||||
// script (so the pane loads it), then bump the
|
||||
// remove-signal counter so the pane opens its
|
||||
// archive/delete modal.
|
||||
if (info.unsaved) {
|
||||
discardDraft(info.path)
|
||||
return
|
||||
}
|
||||
if (info.runnable_kind !== 'script') return
|
||||
activeDraftPath = undefined
|
||||
selection = { kind: 'runnable', runnable_kind: 'script', path: info.path }
|
||||
requestRemoveSignal++
|
||||
}}
|
||||
onRunProducer={async (producer) => {
|
||||
// Saved scripts go through runScriptByPath; drafts
|
||||
// have no DB row yet, so dispatch to runScriptPreview
|
||||
@@ -959,6 +980,7 @@
|
||||
selectionProducers={activeDraft ? [] : selectionProducers}
|
||||
{runsRefreshKey}
|
||||
{runsPendingJobId}
|
||||
{requestRemoveSignal}
|
||||
draftScript={activeDraft?.script}
|
||||
{pathPrefix}
|
||||
onDraftPathChange={renameDraft}
|
||||
|
||||
Reference in New Issue
Block a user