diff --git a/flowfile_wasm/package-lock.json b/flowfile_wasm/package-lock.json
index 04aedb238..e04233744 100644
--- a/flowfile_wasm/package-lock.json
+++ b/flowfile_wasm/package-lock.json
@@ -1,11 +1,11 @@
{
- "name": "flowfile-wasm",
+ "name": "flowfile-editor",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
- "name": "flowfile-wasm",
+ "name": "flowfile-editor",
"version": "0.1.0",
"dependencies": {
"@ag-grid-community/client-side-row-model": "^31.1.1",
@@ -24,10 +24,7 @@
"@vue-flow/core": "^1.42.1",
"@vue-flow/minimap": "^1.5.2",
"js-yaml": "^4.1.1",
- "pinia": "^2.0.16",
- "vue": "^3.5.13",
- "vue-codemirror": "^6.1.1",
- "vue-router": "^4.4.0"
+ "vue-codemirror": "^6.1.1"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.4.1",
@@ -35,10 +32,22 @@
"@vue/test-utils": "^2.4.6",
"fake-indexeddb": "^6.0.0",
"happy-dom": "^15.11.7",
+ "pinia": "^2.0.16",
"typescript": "~5.6.3",
"vite": "^4.5.3",
"vitest": "^2.1.8",
+ "vue": "^3.5.13",
+ "vue-router": "^4.4.0",
"vue-tsc": "^2.0.19"
+ },
+ "peerDependencies": {
+ "pinia": "^2.0.0",
+ "vue": "^3.3.0"
+ },
+ "peerDependenciesMeta": {
+ "pinia": {
+ "optional": true
+ }
}
},
"node_modules/@ag-grid-community/client-side-row-model": {
@@ -1317,6 +1326,7 @@
"version": "6.6.4",
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
+ "dev": true,
"license": "MIT"
},
"node_modules/@vue/language-core": {
@@ -2250,6 +2260,7 @@
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/pinia/-/pinia-2.3.1.tgz",
"integrity": "sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"@vue/devtools-api": "^6.6.3",
@@ -3872,6 +3883,7 @@
"version": "4.6.4",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz",
"integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"@vue/devtools-api": "^6.6.4"
diff --git a/flowfile_wasm/package.json b/flowfile_wasm/package.json
index 517cda988..709ff137c 100644
--- a/flowfile_wasm/package.json
+++ b/flowfile_wasm/package.json
@@ -1,17 +1,42 @@
{
- "name": "flowfile-wasm",
+ "name": "flowfile-editor",
"version": "0.1.0",
- "description": "Minimal WASM-based data flow designer using Pyodide and Polars",
+ "description": "Embeddable browser-based data flow designer using Pyodide and Polars",
"type": "module",
+ "main": "./dist/flowfile-editor.js",
+ "module": "./dist/flowfile-editor.js",
+ "types": "./dist/lib/index.d.ts",
+ "exports": {
+ ".": {
+ "import": "./dist/flowfile-editor.js",
+ "types": "./dist/lib/index.d.ts"
+ },
+ "./style.css": "./dist/style.css"
+ },
+ "files": [
+ "dist",
+ "README.md"
+ ],
"scripts": {
"dev": "vite",
"build": "vue-tsc --noEmit && vite build",
+ "build:lib": "BUILD_MODE=lib vue-tsc --noEmit && BUILD_MODE=lib vite build",
+ "build:all": "npm run build && npm run build:lib",
"preview": "vite preview",
"test": "vitest",
"test:run": "vitest run",
"test:ui": "vitest --ui",
"test:coverage": "vitest run --coverage"
},
+ "peerDependencies": {
+ "vue": "^3.3.0",
+ "pinia": "^2.0.0"
+ },
+ "peerDependenciesMeta": {
+ "pinia": {
+ "optional": true
+ }
+ },
"dependencies": {
"@ag-grid-community/client-side-row-model": "^31.1.1",
"@ag-grid-community/core": "^31.1.1",
@@ -29,9 +54,6 @@
"@vue-flow/core": "^1.42.1",
"@vue-flow/minimap": "^1.5.2",
"js-yaml": "^4.1.1",
- "pinia": "^2.0.16",
- "vue": "^3.5.13",
- "vue-router": "^4.4.0",
"vue-codemirror": "^6.1.1"
},
"devDependencies": {
@@ -40,9 +62,12 @@
"@vue/test-utils": "^2.4.6",
"fake-indexeddb": "^6.0.0",
"happy-dom": "^15.11.7",
+ "pinia": "^2.0.16",
"typescript": "~5.6.3",
"vite": "^4.5.3",
"vitest": "^2.1.8",
+ "vue": "^3.5.13",
+ "vue-router": "^4.4.0",
"vue-tsc": "^2.0.19"
}
}
diff --git a/flowfile_wasm/src/assets/icons/external_data.svg b/flowfile_wasm/src/assets/icons/external_data.svg
new file mode 100644
index 000000000..cbec864ca
--- /dev/null
+++ b/flowfile_wasm/src/assets/icons/external_data.svg
@@ -0,0 +1,14 @@
+
diff --git a/flowfile_wasm/src/assets/icons/external_output.svg b/flowfile_wasm/src/assets/icons/external_output.svg
new file mode 100644
index 000000000..e299f9352
--- /dev/null
+++ b/flowfile_wasm/src/assets/icons/external_output.svg
@@ -0,0 +1,15 @@
+
diff --git a/flowfile_wasm/src/components/Canvas.vue b/flowfile_wasm/src/components/Canvas.vue
index aa952761d..591469452 100644
--- a/flowfile_wasm/src/components/Canvas.vue
+++ b/flowfile_wasm/src/components/Canvas.vue
@@ -46,21 +46,22 @@
@@ -218,6 +220,9 @@
+
+
+
@@ -229,7 +234,9 @@ import { MiniMap } from '@vue-flow/minimap'
import { Controls } from '@vue-flow/controls'
import { useFlowStore } from '../stores/flow-store'
import { storeToRefs } from 'pinia'
-import type { NodeSettings, FlowEdge, ColumnSchema } from '../types'
+import type { NodeSettings, FlowEdge, ColumnSchema, NodeResult } from '../types'
+import type { ToolbarConfig, NodeCategoryConfig } from '../lib/types'
+import { iconUrls } from '../utils/iconUrls'
// AG Grid imports
import { AgGridVue } from '@ag-grid-community/vue3'
@@ -246,6 +253,7 @@ import FlowNode from './nodes/FlowNode.vue'
import NodeTitle from './nodes/NodeTitle.vue'
import ReadCsvSettings from './nodes/ReadCsvSettings.vue'
import ManualInputSettings from './nodes/ManualInputSettings.vue'
+import ExternalDataSettings from './nodes/ExternalDataSettings.vue'
import FilterSettings from './nodes/FilterSettings.vue'
import SelectSettings from './nodes/SelectSettings.vue'
import GroupBySettings from './nodes/GroupBySettings.vue'
@@ -259,6 +267,7 @@ import CodeGenerator from './CodeGenerator.vue'
import PivotSettings from './nodes/PivotSettings.vue'
import UnpivotSettings from './nodes/UnpivotSettings.vue'
import OutputSettings from './nodes/OutputSettings.vue'
+import ExternalOutputSettings from './nodes/ExternalOutputSettings.vue'
import NodeSettingsWrapper from './nodes/NodeSettingsWrapper.vue'
import { getNodeDescription } from '../config/nodeDescriptions'
import MissingFilesModal from './MissingFilesModal.vue'
@@ -266,6 +275,29 @@ import DemoButton from './DemoButton.vue'
import LayoutControls from './common/LayoutControls.vue'
import { useDemo } from '../composables/useDemo'
+// Props for embeddable configuration
+const props = withDefaults(defineProps<{
+ toolbarConfig?: ToolbarConfig
+ nodeCategoriesConfig?: NodeCategoryConfig[]
+ readonly?: boolean
+}>(), {
+ readonly: false
+})
+
+const emit = defineEmits<{
+ (e: 'execution-complete', results: Map): void
+ (e: 'output', data: { nodeId: number; content: string; fileName: string; mimeType: string }): void
+}>()
+
+// Merge toolbar config with defaults (all visible by default)
+const effectiveToolbar = computed>(() => ({
+ showRun: props.toolbarConfig?.showRun !== false,
+ showSaveLoad: props.toolbarConfig?.showSaveLoad !== false,
+ showClear: props.toolbarConfig?.showClear !== false,
+ showCodeGen: props.toolbarConfig?.showCodeGen !== false,
+ showDemo: props.toolbarConfig?.showDemo ?? true
+}))
+
const flowStore = useFlowStore()
const { nodes: flowNodes, edges: flowEdges, selectedNodeId, nodeResults, isExecuting } = storeToRefs(flowStore)
@@ -287,9 +319,9 @@ const nodeTypes: Record = {
'flow-node': markRaw(FlowNode)
}
-// Get icon URL
+// Get icon URL - uses explicit imports for library build compatibility
function getIconUrl(iconFile: string): string {
- return new URL(`../assets/icons/${iconFile}`, import.meta.url).href
+ return iconUrls[iconFile] || new URL(`../assets/icons/${iconFile}`, import.meta.url).href
}
// Node definition interface
@@ -313,7 +345,8 @@ const nodeCategories = ref([
isOpen: true,
nodes: [
{ type: 'read', name: 'Read CSV', icon: 'input_data.png', inputs: 0, outputs: 1 },
- { type: 'manual_input', name: 'Manual Input', icon: 'manual_input.png', inputs: 0, outputs: 1 }
+ { type: 'manual_input', name: 'Manual Input', icon: 'manual_input.png', inputs: 0, outputs: 1 },
+ { type: 'external_data', name: 'External Data', icon: 'external_data.svg', inputs: 0, outputs: 1 }
]
},
{
@@ -349,7 +382,8 @@ const nodeCategories = ref([
isOpen: true,
nodes: [
{ type: 'explore_data', name: 'Preview', icon: 'view.png', inputs: 1, outputs: 0 },
- { type: 'output', name: 'Write Data', icon: 'output.png', inputs: 1, outputs: 0 }
+ { type: 'output', name: 'Write Data', icon: 'output.png', inputs: 1, outputs: 0 },
+ { type: 'external_output', name: 'External Output', icon: 'external_output.svg', inputs: 1, outputs: 0 }
]
}
])
@@ -546,6 +580,7 @@ function getSettingsComponent(type: string) {
const components: Record = {
read: ReadCsvSettings,
manual_input: ManualInputSettings,
+ external_data: ExternalDataSettings,
filter: FilterSettings,
select: SelectSettings,
group_by: GroupBySettings,
@@ -557,7 +592,8 @@ function getSettingsComponent(type: string) {
explore_data: PreviewSettings,
pivot: PivotSettings,
unpivot: UnpivotSettings,
- output: OutputSettings
+ output: OutputSettings,
+ external_output: ExternalOutputSettings
}
return components[type] || null
}
@@ -669,6 +705,7 @@ function autoSizeColumns() {
// Toolbar handlers
async function handleRunFlow() {
await flowStore.executeFlow()
+ emit('execution-complete', nodeResults.value)
}
function handleSaveFlow() {
@@ -723,10 +760,20 @@ function handleKeyDown(event: KeyboardEvent) {
}
}
-// Register keyboard shortcuts
+// Output callback for embeddable mode
+function handleOutputCallback(data: { nodeId: number; content: string; fileName: string; mimeType: string }) {
+ emit('output', data)
+}
+
+// Register keyboard shortcuts and output callbacks
onMounted(async () => {
window.addEventListener('keydown', handleKeyDown)
+ // Register output callback if the store supports it
+ if (flowStore.onOutput) {
+ flowStore.onOutput(handleOutputCallback)
+ }
+
// Wait for DOM to be fully rendered
await nextTick()
// Calculate toolbar bottom position for panel positioning
@@ -738,6 +785,10 @@ onMounted(async () => {
onUnmounted(() => {
window.removeEventListener('keydown', handleKeyDown)
+ // Unregister output callback
+ if (flowStore.offOutput) {
+ flowStore.offOutput(handleOutputCallback)
+ }
})
@@ -753,6 +804,7 @@ onUnmounted(() => {
flex-direction: column;
height: 100%;
position: relative;
+ overflow: hidden;
background: var(--bg-primary);
}
diff --git a/flowfile_wasm/src/components/DemoButton.vue b/flowfile_wasm/src/components/DemoButton.vue
index 873031685..54ecb0302 100644
--- a/flowfile_wasm/src/components/DemoButton.vue
+++ b/flowfile_wasm/src/components/DemoButton.vue
@@ -7,10 +7,10 @@
title="Dismiss"
aria-label="Dismiss demo button"
>
- close
+
See it in action!
@@ -26,7 +26,7 @@
:disabled="isLoading"
title="Load demo flow"
>
- play_circle
+
{{ isLoading ? 'Loading...' : 'Demo' }}
@@ -128,8 +128,9 @@ function handleDismiss() {
color: var(--color-danger);
}
-.demo-dismiss-btn .material-icons {
- font-size: 16px;
+.demo-dismiss-btn svg {
+ width: 16px;
+ height: 16px;
}
/* Tooltip that appears on hover */
diff --git a/flowfile_wasm/src/components/common/DraggablePanel.vue b/flowfile_wasm/src/components/common/DraggablePanel.vue
index 656c8b50a..716db5141 100644
--- a/flowfile_wasm/src/components/common/DraggablePanel.vue
+++ b/flowfile_wasm/src/components/common/DraggablePanel.vue
@@ -87,6 +87,7 @@ const resizeDirection = ref(null)
let prevViewportWidth = window.innerWidth
let prevViewportHeight = window.innerHeight
let resizeDebounceTimer: ReturnType | null = null
+let containerResizeObserver: ResizeObserver | null = null
// Edge snap threshold in pixels - if panel is within this distance of an edge, consider it "docked"
const EDGE_SNAP_THRESHOLD = 10
@@ -94,10 +95,24 @@ const MIN_PANEL_WIDTH = 200
const MIN_PANEL_HEIGHT = 100
const MIN_VISIBLE_HEADER = 50 // Minimum header visible for dragging
+/**
+ * Get container dimensions. With position: absolute the panel is
+ * positioned relative to .canvas-container instead of the viewport.
+ */
+function getContainerRect(): { width: number; height: number; left: number; top: number } {
+ const parent = panelRef.value?.parentElement
+ if (parent) {
+ const rect = parent.getBoundingClientRect()
+ return { width: rect.width, height: rect.height, left: rect.left, top: rect.top }
+ }
+ return { width: window.innerWidth, height: window.innerHeight, left: 0, top: 0 }
+}
+
// Save current panel state to storage
function saveCurrentState() {
if (!props.panelId) return
+ const container = getContainerRect()
const state: PanelState = {
width: width.value,
height: height.value,
@@ -105,9 +120,9 @@ function saveCurrentState() {
top: top.value,
isMinimized: isMinimized.value,
zIndex: zIndex.value,
- // Save viewport dimensions to detect resize on next load
- savedViewportWidth: window.innerWidth,
- savedViewportHeight: window.innerHeight
+ // Save container dimensions to detect resize on next load
+ savedViewportWidth: container.width,
+ savedViewportHeight: container.height
}
savePanelState(props.panelId, state)
}
@@ -130,8 +145,9 @@ function detectDockedEdges(vw: number, vh: number) {
// Smart resize handler - maintains edge docking and proportional positioning
function handleWindowResizeSmartly() {
- const newVw = window.innerWidth
- const newVh = window.innerHeight
+ const container = getContainerRect()
+ const newVw = container.width
+ const newVh = container.height
// Skip if viewport didn't actually change
if (newVw === prevViewportWidth && newVh === prevViewportHeight) {
@@ -260,8 +276,9 @@ function handleWindowResize() {
// Reset panel to initial position based on initialPosition prop
function resetToInitialPosition() {
- const vh = window.innerHeight
- const vw = window.innerWidth
+ const container = getContainerRect()
+ const vh = container.height
+ const vw = container.width
// Reset to initial dimensions
width.value = props.initialWidth
@@ -304,8 +321,9 @@ function handleLayoutReset() {
// Compute initial position based on prop
onMounted(() => {
- const vh = window.innerHeight
- const vw = window.innerWidth
+ const container = getContainerRect()
+ const vh = container.height
+ const vw = container.width
// Initialize viewport tracking for smart resize
prevViewportWidth = vw
@@ -411,6 +429,15 @@ onMounted(() => {
window.addEventListener('resize', handleWindowResize)
// Listen for layout reset events from LayoutControls
window.addEventListener('layout-reset', handleLayoutReset)
+
+ // Observe container size changes (handles embedded editor resizing)
+ const parent = panelRef.value?.parentElement
+ if (parent) {
+ containerResizeObserver = new ResizeObserver(() => {
+ handleWindowResize()
+ })
+ containerResizeObserver.observe(parent)
+ }
})
// Watch for changes to initialTop and update position (only if no saved state)
@@ -420,12 +447,12 @@ watch(() => props.initialTop, (newTop) => {
return
}
- const vh = window.innerHeight
+ const container = getContainerRect()
top.value = newTop
// Adjust height for left/right panels
if (props.initialPosition === 'left' || props.initialPosition === 'right') {
- height.value = vh - newTop
+ height.value = container.height - newTop
}
})
@@ -473,9 +500,10 @@ function onMove(e: MouseEvent) {
const dx = e.clientX - startX.value
const dy = e.clientY - startY.value
+ const container = getContainerRect()
- left.value = Math.max(0, startLeft.value + dx)
- top.value = Math.max(0, startTop.value + dy)
+ left.value = Math.max(0, Math.min(startLeft.value + dx, container.width - MIN_VISIBLE_HEADER))
+ top.value = Math.max(0, Math.min(startTop.value + dy, container.height - MIN_VISIBLE_HEADER))
}
function stopMove() {
@@ -544,6 +572,11 @@ onUnmounted(() => {
document.removeEventListener('mouseup', stopResize)
window.removeEventListener('resize', handleWindowResize)
window.removeEventListener('layout-reset', handleLayoutReset)
+ // Clean up container resize observer
+ if (containerResizeObserver) {
+ containerResizeObserver.disconnect()
+ containerResizeObserver = null
+ }
// Clear any pending debounce timer
if (resizeDebounceTimer) {
clearTimeout(resizeDebounceTimer)
@@ -558,7 +591,7 @@ onUnmounted(() => {
diff --git a/flowfile_wasm/src/components/nodes/ExternalOutputSettings.vue b/flowfile_wasm/src/components/nodes/ExternalOutputSettings.vue
new file mode 100644
index 000000000..9a6109cf3
--- /dev/null
+++ b/flowfile_wasm/src/components/nodes/ExternalOutputSettings.vue
@@ -0,0 +1,179 @@
+
+
+
External Output
+
+
+ No input connected. Connect an input node first.
+
+
+
+
+
+
+ Identifies this output in the host application's callback.
+
+
+
+
+
+
+
+ {{ col.name }}
+ {{ col.data_type }}
+
+
+
+
+
+
+
Output ready — data sent to host callback
+
+
+
+
Run the flow to produce output
+
+
+
+
+
+
+
+
diff --git a/flowfile_wasm/src/components/nodes/FlowNode.vue b/flowfile_wasm/src/components/nodes/FlowNode.vue
index a1a152bfd..69e636174 100644
--- a/flowfile_wasm/src/components/nodes/FlowNode.vue
+++ b/flowfile_wasm/src/components/nodes/FlowNode.vue
@@ -53,8 +53,8 @@
-
-
+
+