diff --git a/frontend/src/lib/components/assets/AssetGraph/AddDataSourceMenu.svelte b/frontend/src/lib/components/assets/AssetGraph/AddDataSourceMenu.svelte new file mode 100644 index 0000000000..31f1600073 --- /dev/null +++ b/frontend/src/lib/components/assets/AssetGraph/AddDataSourceMenu.svelte @@ -0,0 +1,145 @@ + + + + diff --git a/frontend/src/lib/components/assets/AssetGraph/AddNode.svelte b/frontend/src/lib/components/assets/AssetGraph/AddNode.svelte index a3f32a995b..57a31e34d8 100644 --- a/frontend/src/lib/components/assets/AssetGraph/AddNode.svelte +++ b/frontend/src/lib/components/assets/AssetGraph/AddNode.svelte @@ -1,160 +1,36 @@
- - {#snippet trigger()} - - - {/snippet} - + + {#snippet trigger()} + + + {/snippet} +
diff --git a/frontend/src/lib/components/assets/AssetGraph/AssetGraphCanvas.svelte b/frontend/src/lib/components/assets/AssetGraph/AssetGraphCanvas.svelte index b79856d864..9a48e7092d 100644 --- a/frontend/src/lib/components/assets/AssetGraph/AssetGraphCanvas.svelte +++ b/frontend/src/lib/components/assets/AssetGraph/AssetGraphCanvas.svelte @@ -14,17 +14,30 @@ import RunnableNode from './RunnableNode.svelte' import TriggerNode, { type TriggerNodeKind } from './TriggerNode.svelte' import AddNode from './AddNode.svelte' + import AddDataSourceMenu from './AddDataSourceMenu.svelte' import DataTestNode from './DataTestNode.svelte' import AssetGraphEdge from './AssetGraphEdge.svelte' import PanToNode from './PanToNode.svelte' import InitialFitView from './InitialFitView.svelte' import { layoutAssetGraph, PIPELINE_NODE_EXTRA_ROW } from './assetGraphLayout' - import { assetsOnlyView, type AssetUpstream } from './assetsOnlyView' + import { + assetsOnlyView, + upstreamDeletion, + type AssetUpstream, + type AssetUpstreamDelete + } from './assetsOnlyView' import { assetsOnlyNodeWidth } from './assetNodeWidth' import { formatAssetKind, formatShortAssetPath } from '$lib/components/assets/lib' import { TRIGGER_NODE_STYLE } from './TriggerNode.svelte' import { describeSchedule } from '$lib/utils/describeCron' import Toggle from '$lib/components/Toggle.svelte' + import { fly } from 'svelte/transition' + import { Plus } from 'lucide-svelte' + import { + getContextMenuContainerClass, + CONTEXT_MENU_ITEM_BASE_CLASS, + CONTEXT_MENU_ITEM_HOVER_CLASS + } from '$lib/components/common/contextmenu/contextMenuStyles' import { computeMutedReadKeys, dbtAssociations } from './resolveGraph' import { buildDownstreamMap } from './graphTraversal' import { buildLineageDownstreamMap } from './boundedCascade' @@ -200,6 +213,9 @@ /** Offer the "Assets only" switch, which folds scripts and triggers into the * assets they produce. */ assetsOnlyToggle?: boolean + /** Deletes what produces an asset in the assets-only view: its script, and its + * trigger when set. The canvas only offers what no other asset shares. */ + onDeleteAssetUpstream?: (target: AssetUpstreamDelete) => void } let { graph, @@ -233,11 +249,30 @@ recomputedAssetIds, assetRunStatus, scrollZoom = true, - assetsOnlyToggle = false + assetsOnlyToggle = false, + onDeleteAssetUpstream }: Props = $props() let assetsOnly = $state(false) + // Right-click on the empty canvas: a menu at the pointer whose entry opens the + // add-data-source menu there too. + let paneMenu = $state<{ x: number; y: number } | undefined>(undefined) + let addMenuAt = $state<{ x: number; y: number }>({ x: 0, y: 0 }) + let addMenuSignal = $state(0) + function handleContextMenu(e: MouseEvent) { + const t = e.target as HTMLElement | null + // A node's own actions menu opens on right-click; the browser's never does, + // on nodes without one either. + if (t?.closest('.svelte-flow__node')) { + e.preventDefault() + return + } + if (!onAddPipelineScript || boundPick || !t?.closest('.svelte-flow__pane')) return + e.preventDefault() + paneMenu = { x: e.clientX, y: e.clientY } + } + // `${kind}:${path}` ids for the hovered / pinned runs (both script and flow // variants, since the run row's kind isn't known here). const runIds = (paths: string[] | undefined): Set => @@ -859,6 +894,7 @@ timezone: info.timezone, summary: info.summary, runnable_path: info.runnable_path, + runnable_paths: info.runnable_paths, // data_upload nodes go green once a file is staged for their // target script (see readyDataUploadPaths / page dataUploadArgs). ready: info.runnable_path @@ -994,6 +1030,10 @@ path: r.path }) } + const toDelete = + onDeleteAssetUpstream && u && !u.multiple && r + ? upstreamDeletion(u, r, v.upstream) + : undefined const asset = { kind: n.data.asset_kind, path: n.data.path } // Chips this estimate does not model keep the regular width. const width = @@ -1015,7 +1055,20 @@ scriptChip: 'runnableId' in upstream }) : undefined - return { ...n, data: { ...n.data, upstream, width } } + return { + ...n, + data: { + ...n.data, + upstream, + width, + ...(toDelete + ? { + deleteCount: toDelete.trigger ? 2 : 1, + onDeleteUpstream: () => onDeleteAssetUpstream?.(toDelete) + } + : {}) + } + } }) const edges: BuiltEdge[] = v.edges.map((e) => ({ ...e, kind: e.kind as BuiltEdge['kind'] })) if (nodes.some((n) => n.id === ADD_NODE_ID)) { @@ -1507,7 +1560,12 @@ -
+
+ { + if (paneMenu && !(e.target as HTMLElement | null)?.closest('[data-pane-menu]')) + paneMenu = undefined + }} + onkeydown={(e) => { + if (e.key === 'Escape') paneMenu = undefined + }} +/> + +{#if paneMenu} + +{/if} + +{#if onAddPipelineScript} + +{/if} +