mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-22 08:02:19 +00:00
0caa533fbd
* Refactor 1
* claude tmp1
* fixes1
* support for insert and update
* Fix returning
* docs: add nuanced mutex selection guidance to Rust backend skill (#7737)
Add "Mutex Selection in Async Code" section explaining when to use
std::sync::Mutex vs tokio::sync::Mutex based on official Tokio docs.
std::sync::Mutex is preferred for data protection as it's faster;
tokio::sync::Mutex only needed when holding locks across .await points.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* fix(cli): revert findCodebase change that broke ../shared codebases (#7740)
* fix(cli): revert findCodebase relative_path check that broke ../shared codebases
The previous change added a check to ensure script paths start with the
codebase's relative_path. However, this broke cases where relative_path
uses parent directory references (e.g., "../shared") because:
1. path.join normalizes paths, so "/project/../shared/f/script.ts" becomes
"/shared/f/script.ts"
2. FSFSElement strips the cwd prefix, resulting in "f/script.ts"
3. The check "f/script.ts".startsWith("../shared/") failed
The original behavior was correct - relative_path indicates where to find
codebase files, while includes/excludes patterns match against the normalized
paths that get passed during sync.
Fixes regression reported in #7729 comments.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* test(cli): add preview test for codebase with imports
Tests that codebase bundling correctly includes imported modules,
which is the key functionality needed for ../shared codebases.
The test creates a helper module and a main script that imports
from it, then verifies the bundled script executes correctly.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* chore(main): release 1.621.2 (#7735)
* chore(main): release 1.621.2
* Apply automatic changes
---------
Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
* feat: workspace dedicated workers (#7741)
* feat: workspace dedicated workers
* ref
* chore: update ee-repo-ref to a18ac31062ac092cb9a5fc87629e217d97f4911d
This commit updates the EE repository reference after PR #398 was merged in windmill-ee-private.
Previous ee-repo-ref: 98cfe3fef764d9d815d326d5056c734a03689d33
New ee-repo-ref: a18ac31062ac092cb9a5fc87629e217d97f4911d
Automated by sync-ee-ref workflow.
* fix(frontend): workspace script in flow steps
---------
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
* fix: remove uuid-ossp extension requirement for RDS compatibility
The uuid-ossp extension was created in the first migration but never
actually used - the codebase uses gen_random_uuid() which is built-in
to PostgreSQL 13+. This allows Windmill to run on AWS RDS where
application users may not have CREATE SCHEMA privileges.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix: indexer build error (#7744)
* fix: indexer build error
* prepare sqlx
* Remove changes from Cargo.toml
* refactor: remove seed parameter from AI chat completions (#7745)
* better claude
* refactor: remove seed parameter from AI chat completions
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* fix: visibility bug on deployment UI (issue when renaming items) + add tracking of folders and resource types (#7739)
* fix: Raw apps deployment UI (and merge UI)
* Add folders and resource tpyes to merge UI
* claude first pass on adding the new arg for h_deploy_metadata
* Add missing argument to handle_deployment_metadata in all its calls
* Add support for folders and resource types in merge UI
* Update eereporef for CI
* Update ee repo
* Add migration to reset cached diff with potential artifacts
* fix type in frontend
* Preapare sqlx
* Remove unused import and logs
* update ee-repo
* Update eerepo
* chore: update ee-repo-ref to aca38475afd2cafaf63f4bbffc65be9437d57d86
This commit updates the EE repository reference after PR #397 was merged in windmill-ee-private.
Previous ee-repo-ref: 19c64cf8c61d83f45047b37660054b29658cd403
New ee-repo-ref: aca38475afd2cafaf63f4bbffc65be9437d57d86
Automated by sync-ee-ref workflow.
* Make integration test for workspace comparisons
* Update SQLx metadata
---------
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
* fix: require AGENT_TOKEN and BASE_INTERNAL_URL for agent mode
- Add AgentConfig struct to validate required env vars on startup
- Change build_agent_http_client to require explicit token and URL
- Remove DEFAULT_BASE_INTERNAL_URL fallback (no more silent localhost:8000)
- Exit immediately if agent cannot connect to server on initial load
- Update integration tests to use dynamic port for BASE_INTERNAL_URL
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix: forward teams error to client (#7746)
* fix: forward teams error to client
* chore: update ee-repo-ref to 9a3d71f2c6a41ed4d17111a8c05d8e1d4933898d
This commit updates the EE repository reference after PR #400 was merged in windmill-ee-private.
Previous ee-repo-ref: 25d35a8de1cd70e281dc876e51cd30402580b5c0
New ee-repo-ref: 9a3d71f2c6a41ed4d17111a8c05d8e1d4933898d
Automated by sync-ee-ref workflow.
* fix
* fix
* fix
* al
* sqlx
* sqlx
* all
* all
---------
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
* feat: add token usage tracking to AI agent output (#7738)
* feat: add token usage tracking to AI agent output
Add TokenUsage struct to track input/output/cache tokens from AI providers.
Currently implemented for Bedrock provider, with infrastructure in place
for other providers. Usage is included in the AI agent result alongside
output and messages when available.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* feat: add token usage extraction for Anthropic provider
Extract usage from message_delta SSE event and convert to TokenUsage.
Includes input_tokens, output_tokens, cache_read_input_tokens, and
cache_creation_input_tokens (mapped to cache_write_input_tokens).
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* feat: add token usage extraction for Google AI/Gemini provider
Extract usage from usageMetadata in Gemini SSE events and convert to TokenUsage.
Maps promptTokenCount -> input_tokens, candidatesTokenCount -> output_tokens,
totalTokenCount -> total_tokens.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* feat: add token usage extraction for OpenAI Responses API provider
Extract usage from response.completed SSE event and convert to TokenUsage.
Maps input_tokens, output_tokens, and total_tokens directly.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* feat: add token usage extraction for Azure OpenAI / Chat Completions API
Add stream_options.include_usage to request and parse usage from final
SSE chunk for providers using the standard OpenAI Chat Completions API
(Azure OpenAI, Mistral, DeepSeek, Groq, TogetherAI, CustomAI).
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix: cleanup token usage tracking - remove unused Image usage field and accumulate across iterations
- Remove unused `usage` field from ParsedResponse::Image variant
- Add TokenUsage::accumulate() method to sum usage across agent iterations
- Accumulate input/output/total/cache tokens instead of replacing with last iteration
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: remove verbose debug logging from AI providers
Remove tracing::info!("[debug] ...") statements that were too verbose
for production. These logged raw events on every streaming event.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: add retry mechanism for OpenAI-compatible providers without stream_options support
Some OpenAI-compatible providers don't support the stream_options parameter
for usage tracking. This adds a retry mechanism that:
- First attempts the request with stream_options.include_usage
- If it fails with 400 and error mentions stream_options/include_usage,
automatically retries without the parameter
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: remove unnecessary text parsing overhead in image response handlers
Revert debugging changes that read response as text before parsing JSON.
Using response.json() directly is more efficient.
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor: centralize TokenUsage conversion with constructor methods
Add new(), from_input_output(), and with_cache() constructors to TokenUsage
to eliminate duplicate conversion logic across providers. Also fixes potential
truncation in Bedrock cache token conversion by using i32::try_from with
fallback to i32::MAX.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* refactor: simplify Anthropic usage extraction and add Default derive
- Use idiomatic `if let` pattern instead of `is_some()` check for usage extraction
- Add Default derive to OpenAIChatUsage for consistency with other usage structs
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix: use saturating_add to prevent overflow in token accumulation
In long-running agents with many iterations, token counts could
potentially overflow. Using saturating_add ensures values cap at
i32::MAX instead of wrapping around.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* better claude
* nit
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* chore(main): release 1.622.0 (#7742)
* chore(main): release 1.622.0
* Apply automatic changes
---------
Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
* fix npm check
* fix: add schema compatibility layer for MCP clients like n8n (#7747)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* nit ui nextcloud triggers (#7749)
* feat: add PocketID OAuth provider support (#7318)
* feat(oauth): add Pocket-ID OAuth provider component
- Implements PocketIdSetting.svelte following Keycloak pattern
- Configures OIDC endpoints for Pocket-ID (/authorize, /api/oidc/*)
- Supports standard OIDC scopes (openid, profile, email)
- Uses passkey-only authentication via Pocket-ID
Refs #5678
* feat(oauth): register Pocket-ID in SSO provider list
- Import PocketIdSetting component
- Add Pocket-ID to provider list in SSO tab
- Update exclusion filter to prevent duplicate custom entries
Refs #5678
* fix(oauth): add missing PocketID icon and fix component integration
- Create PocketIdIcon.svelte component with user profile icon
- Register pocket-id in APP_TO_ICON_COMPONENT mapping
- Fix PocketIdSetting to use IconedResourceType pattern matching other OAuth providers
This resolves the issue where PocketID toggle was not appearing in SSO settings.
Refs #5678
* refactor: migrate PocketIdSetting to Svelte 5 runes syntax
- Use $props() with $bindable() for reactive prop binding
- Use $state() for local reactive state
- Use $derived() for computed values
- Use $effect() for reactive side effects
- Replace on:change with onchange event handler
- Pre-populate base URL from existing config when editing
- Clean up bracket notation to dot notation for value properties
Addresses reviewer feedback
* fix: rename pocket-id to pocketid for naming convention compliance
Change identifier from 'pocket-id' to 'pocketid' to match Windmill's naming convention.
No OAuth provider uses hyphens - all custom SSO providers (keycloak, authentik, authelia,
kanidm, zitadel) use no separator.
Changes:
- AuthSettings.svelte: oauths['pocket-id'] → oauths['pocketid'] (2 locations)
- PocketIdSetting.svelte: name={'pocket-id'} → name={'pocketid'}
- icons/index.ts: 'pocket-id': PocketIdIcon → pocketid: PocketIdIcon
Note: PocketID does not need oauth_connect.json entry as it's a custom SSO provider
with user-configured endpoints, similar to Keycloak/Authentik.
Addresses reviewer feedback
* fix: use TextInput component for consistency
---------
Co-authored-by: hugocasa <hugo@casademont.ch>
* fix: preserve script envs field during sync push
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* nit frontend fix
* chore(main): release 1.623.0 (#7748)
* chore(main): release 1.623.0
* Apply automatic changes
---------
Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
* fix: resolve infinite effect loop in PocketIdSetting component (#7753)
* fix: prevent retention cleanup from deleting jobs of active flows (#7755)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* chore(main): release 1.623.1 (#7754)
* chore(main): release 1.623.1
* Apply automatic changes
---------
Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
* feat: default to quickjs on ce for flow eval (#7756)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* feat: runtime assets (#7656)
* Runtime assets
* Nits
* Revert "Nits"
This reverts commit 3031a2ddd1.
* detection_kinds
* don't delete runtime assets
* Show latest executions
* conditional unique idx
* nit status
* refactor
* nit refactor
* prepare sql
* Detect assets in complex JSON input objects
* false positive prevent
* nit
* redundant idx
* Update frontend/src/lib/components/assets/AssetsUsageDrawer.svelte
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
* Update backend/migrations/20260122134517_runtime_assets.up.sql
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
* runtime assets are inserted in a loop
* nit
* nit fix
* Don't use lazy static
* fix compilation
* nits
* missing on conflict do nothing
* add index
* Fix max n logic
* created at
* nits
* remove pagination
* sqlx prepare
* Only detect resource assets in input
* get_runtime_asset_sender()
* use global get_runtime_asset_sender to avoid prop drilling
* nit refactor : register_runtime_asset
* get job_id from token
* job as a usage kind
* fixes
* ee
* nit refactor
* merge access types when same job uses same asset multiple times
* Refactor to support wmill s3 API
* nit
* parse_wmill_sdk_sql_assets refactor
* Detect datatable and ducklake usage
* nit order by
* Join with v2_job
* better UI
* add sequential id for cursor pagination
* useInfiniteQuery
* useScrollToBottom
* sql index
* claude code stash
* migration fixes
* Infinite scroll UI
* nit
* style nit
* runtime asset created at
* Asset filters
* fix usage kind filter
* also check runnable_path for jobs when filtering
* better filters
* avoid flickering
* debounced filters
* nit
* tooltips
* fix: update AssetUsage type to match new ListAssetsResponse structure
The ListAssetsResponse changed from an array to an object with an 'assets' property.
Updated the type extraction accordingly.
Co-authored-by: Diego Imbert <diegoimbert@users.noreply.github.com>
* sqlx prepare
* Delete .claude/hooks/.symlink-manifest
* unnecessary dep
* nit refactor
* nit comment
* nit naming
* CI fix attempt 1
* ee ref
* nit remove alerts
* nit
* chore: update ee-repo-ref to 138a4f5f868f3bded5bb7cb77b222b532c07e4af
This commit updates the EE repository reference after PR #395 was merged in windmill-ee-private.
Previous ee-repo-ref: 7d3a21d53066726e97dfea9f117373299bc9318c
New ee-repo-ref: 138a4f5f868f3bded5bb7cb77b222b532c07e4af
Automated by sync-ee-ref workflow.
---------
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Diego Imbert <diegoimbert@users.noreply.github.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
* fix: remove $schema field from Google AI output schema requests (#7765)
* fix: remove $schema field from Google AI output schema requests
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* test: add $schema field to all output schema integration tests
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix: remove $schema field from Google AI tool parameter schemas
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* test: add workspace script tool test for AI agents
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* fix: Fix app multiselect not refreshing result when creating element (#7766)
* Fix returning
* asset columns are saved and displayed in the assets page
* runtime assets column detectionz
* frontend nits
* update regex parsers
* UI nits
* Display asset columns in flow graph
* Column hint directly in asset node
* nit bg
* sqlx prepare
* ee repo ref
* chore: update ee-repo-ref to 66a68df97e8c65c498b28f302a365ab8687cad9e
This commit updates the EE repository reference after PR #402 was merged in windmill-ee-private.
Previous ee-repo-ref: 0a32bc104cbaec9664a4d7cb1565823722c875a1
New ee-repo-ref: 66a68df97e8c65c498b28f302a365ab8687cad9e
Automated by sync-ee-ref workflow.
---------
Co-authored-by: centdix <40307056+centdix@users.noreply.github.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
Co-authored-by: hugocasa <hugo@casademont.ch>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
Co-authored-by: wendrul <53628737+wendrul@users.noreply.github.com>
Co-authored-by: Alexander Petric <alpetric@users.noreply.github.com>
Co-authored-by: Devdatta Talele <50290838+devdattatalele@users.noreply.github.com>
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Diego Imbert <diegoimbert@users.noreply.github.com>
1093 lines
31 KiB
Svelte
1093 lines
31 KiB
Svelte
<script lang="ts">
|
|
import { FlowService, type FlowModule, type FlowNote, type Job, type OpenFlow } from '../../gen'
|
|
import { AI_OR_ASSET_NODE_TYPES, NODE, type GraphModuleState } from '.'
|
|
import { getContext, onDestroy, onMount, tick, untrack, type Snippet } from 'svelte'
|
|
import { createFlowDiffManager } from '../flows/flowDiffManager.svelte'
|
|
|
|
import { get, writable, type Writable } from 'svelte/store'
|
|
import '@xyflow/svelte/dist/base.css'
|
|
import {
|
|
SvelteFlow,
|
|
type Node,
|
|
type Edge,
|
|
ConnectionLineType,
|
|
Controls,
|
|
ControlButton,
|
|
SvelteFlowProvider,
|
|
type Viewport,
|
|
SelectionMode
|
|
} from '@xyflow/svelte'
|
|
import {
|
|
graphBuilder,
|
|
isTriggerStep,
|
|
topologicalSort,
|
|
type InlineScript,
|
|
type InsertKind,
|
|
type NodeLayout,
|
|
type onSelectedIteration,
|
|
type SimplifiableFlow
|
|
} from './graphBuilder.svelte'
|
|
import ModuleNode from './renderers/nodes/ModuleNode.svelte'
|
|
import InputNode from './renderers/nodes/InputNode.svelte'
|
|
import BranchAllStart from './renderers/nodes/BranchAllStart.svelte'
|
|
import BranchAllEndNode from './renderers/nodes/BranchAllEndNode.svelte'
|
|
import ForLoopEndNode from './renderers/nodes/ForLoopEndNode.svelte'
|
|
import ForLoopStartNode from './renderers/nodes/ForLoopStartNode.svelte'
|
|
import ResultNode from './renderers/nodes/ResultNode.svelte'
|
|
import BaseEdge from './renderers/edges/BaseEdge.svelte'
|
|
import EmptyEdge from './renderers/edges/EmptyEdge.svelte'
|
|
import { sugiyama, dagStratify, coordCenter, decrossTwoLayer, decrossOpt } from 'd3-dag'
|
|
import { Expand, MousePointer, Hand } from 'lucide-svelte'
|
|
import Toggle from '../Toggle.svelte'
|
|
import DataflowEdge from './renderers/edges/DataflowEdge.svelte'
|
|
import { encodeState, readFieldsRecursively, getModifierKey, isMac } from '$lib/utils'
|
|
import BranchOneStart from './renderers/nodes/BranchOneStart.svelte'
|
|
import NoBranchNode from './renderers/nodes/NoBranchNode.svelte'
|
|
import HiddenBaseEdge from './renderers/edges/HiddenBaseEdge.svelte'
|
|
import TriggersNode from './renderers/nodes/TriggersNode.svelte'
|
|
import { Alert, Drawer } from '../common'
|
|
import Button from '../common/button/Button.svelte'
|
|
import FlowYamlEditor from '../flows/header/FlowYamlEditor.svelte'
|
|
import BranchOneEndNode from './renderers/nodes/branchOneEndNode.svelte'
|
|
import type { TriggerContext } from '../triggers'
|
|
import { workspaceStore } from '$lib/stores'
|
|
import SubflowBound from './renderers/nodes/SubflowBound.svelte'
|
|
import DiffDrawer from '../DiffDrawer.svelte'
|
|
import ViewportResizer from './ViewportResizer.svelte'
|
|
import ViewportSynchronizer from './ViewportSynchronizer.svelte'
|
|
import AssetNode, { computeAssetNodes } from './renderers/nodes/AssetNode.svelte'
|
|
import AssetsOverflowedNode from './renderers/nodes/AssetsOverflowedNode.svelte'
|
|
import type { FlowGraphAssetContext } from '../flows/types'
|
|
import AiToolNode, { computeAIToolNodes } from './renderers/nodes/AIToolNode.svelte'
|
|
import NewAiToolNode from './renderers/nodes/NewAIToolNode.svelte'
|
|
import NoteNode from './renderers/nodes/NoteNode.svelte'
|
|
import NoteTool from './NoteTool.svelte'
|
|
import SelectionBoundingBox from './SelectionBoundingBox.svelte'
|
|
import SelectionTool from './SelectionTool.svelte'
|
|
import PaneContextMenu from './PaneContextMenu.svelte'
|
|
import { SelectionManager } from './selectionUtils.svelte'
|
|
import { ChangeTracker } from '$lib/svelte5Utils.svelte'
|
|
import { NoteManager } from './noteManager.svelte'
|
|
import type { ModulesTestStates } from '../modulesTest.svelte'
|
|
import { deepEqual } from 'fast-equals'
|
|
import type { AssetWithAltAccessType } from '../assets/lib'
|
|
import type { ModuleActionInfo } from '$lib/components/flows/flowDiff'
|
|
import { setGraphContext } from './graphContext'
|
|
import { computeNoteNodes } from './noteUtils.svelte'
|
|
import { Tooltip } from '../meltComponents'
|
|
import { getNoteEditorContext } from './noteEditor.svelte'
|
|
|
|
let useDataflow: Writable<boolean | undefined> = writable<boolean | undefined>(false)
|
|
let showAssets: Writable<boolean | undefined> = writable<boolean | undefined>(true)
|
|
let showNotes = $state(true)
|
|
|
|
const triggerContext = getContext<TriggerContext>('TriggerContext')
|
|
|
|
// Create diffManager instance for this FlowGraphV2
|
|
const diffManager = createFlowDiffManager()
|
|
|
|
let fullWidth = 0
|
|
let width = $state(0)
|
|
|
|
let simplifiableFlow: SimplifiableFlow | undefined = $state(undefined)
|
|
|
|
interface Props {
|
|
success?: boolean | undefined
|
|
modules?: FlowModule[] | undefined
|
|
failureModule?: FlowModule | undefined
|
|
preprocessorModule?: FlowModule | undefined
|
|
minHeight?: number
|
|
maxHeight?: number | undefined
|
|
notSelectable?: boolean
|
|
flowModuleStates?: Record<string, GraphModuleState> | undefined
|
|
testModuleStates?: ModulesTestStates
|
|
moduleActions?: Record<string, ModuleActionInfo>
|
|
selectionManager?: SelectionManager
|
|
path?: string | undefined
|
|
newFlow?: boolean
|
|
insertable?: boolean
|
|
earlyStop?: boolean
|
|
cache?: boolean
|
|
scroll?: boolean
|
|
moving?: string | undefined
|
|
// Download: display a top level button to open the graph in a new tab
|
|
download?: boolean
|
|
fullSize?: boolean
|
|
disableAi?: boolean
|
|
triggerNode?: boolean
|
|
workspace?: string
|
|
editMode?: boolean
|
|
allowSimplifiedPoll?: boolean
|
|
expandedSubflows?: Record<string, FlowModule[]>
|
|
isOwner?: boolean
|
|
isRunning?: boolean
|
|
individualStepTests?: boolean
|
|
flowJob?: Job | undefined
|
|
showJobStatus?: boolean
|
|
suspendStatus?: Record<string, { job: Job; nb: number }>
|
|
noteMode?: boolean
|
|
notes?: FlowNote[]
|
|
chatInputEnabled?: boolean
|
|
multiSelectEnabled?: boolean
|
|
onDelete?: (id: string) => void
|
|
onInsert?: (detail: {
|
|
sourceId?: string
|
|
targetId?: string
|
|
branch?: { rootId: string; branch: number }
|
|
index: number
|
|
detail: string
|
|
isPreprocessor?: boolean
|
|
agentId?: string
|
|
inlineScript?: InlineScript
|
|
script?: { path: string; summary: string; hash: string | undefined }
|
|
flow?: { path: string; summary: string }
|
|
kind: InsertKind
|
|
}) => Promise<void>
|
|
onNewBranch?: (id: string) => Promise<void>
|
|
onSelect?: (id: string | FlowModule) => void
|
|
onDeleteBranch?: (detail: { id: string; index: number }) => Promise<void>
|
|
onChangeId?: (detail: { id: string; newId: string; deps: Record<string, string[]> }) => void
|
|
onMove?: (id: string) => void
|
|
onUpdateMock?: (detail: { mock: FlowModule['mock']; id: string }) => void
|
|
onTestUpTo?: ((id: string) => void) | undefined
|
|
onSelectedIteration?: onSelectedIteration
|
|
onEditInput?: (moduleId: string, key: string) => void
|
|
onTestFlow?: () => void
|
|
onCancelTestFlow?: () => void
|
|
onOpenPreview?: () => void
|
|
onHideJobStatus?: () => void
|
|
flowHasChanged?: boolean
|
|
exitNoteMode?: () => void
|
|
onNotePositionUpdate?: (noteId: string, position: { x: number; y: number }) => void
|
|
// Viewport synchronization props (for diff viewer)
|
|
sharedViewport?: Viewport
|
|
onViewportChange?: (viewport: Viewport, isUserInitiated: boolean) => void
|
|
leftHeader?: Snippet
|
|
// Diff mode props
|
|
diffBeforeFlow?: OpenFlow
|
|
currentInputSchema?: Record<string, any>
|
|
markRemovedAsShadowed?: boolean
|
|
}
|
|
|
|
let {
|
|
onInsert = undefined,
|
|
onDelete = undefined,
|
|
onMove = undefined,
|
|
onDeleteBranch = undefined,
|
|
onNewBranch = undefined,
|
|
onSelect = undefined,
|
|
onChangeId = undefined,
|
|
onUpdateMock = undefined,
|
|
onSelectedIteration = undefined,
|
|
success = undefined,
|
|
modules = [],
|
|
failureModule = undefined,
|
|
preprocessorModule = undefined,
|
|
minHeight = 0,
|
|
maxHeight = undefined,
|
|
notSelectable = false,
|
|
flowModuleStates = undefined,
|
|
testModuleStates = undefined,
|
|
moduleActions = undefined,
|
|
selectionManager: selectionManagerProp = undefined,
|
|
path = undefined,
|
|
newFlow = false,
|
|
insertable = false,
|
|
earlyStop = false,
|
|
cache = false,
|
|
scroll = false,
|
|
moving = undefined,
|
|
download = false,
|
|
fullSize = false,
|
|
disableAi = false,
|
|
triggerNode = false,
|
|
workspace = $workspaceStore ?? 'NO_WORKSPACE',
|
|
editMode = false,
|
|
allowSimplifiedPoll = true,
|
|
expandedSubflows = $bindable({}),
|
|
onTestUpTo = undefined,
|
|
onEditInput = undefined,
|
|
isOwner = false,
|
|
onTestFlow = undefined,
|
|
isRunning = false,
|
|
onCancelTestFlow = undefined,
|
|
onOpenPreview = undefined,
|
|
onHideJobStatus = undefined,
|
|
individualStepTests = false,
|
|
flowJob = undefined,
|
|
showJobStatus = false,
|
|
suspendStatus = {},
|
|
flowHasChanged = false,
|
|
noteMode = false,
|
|
notes = undefined,
|
|
exitNoteMode = undefined,
|
|
onNotePositionUpdate = undefined,
|
|
chatInputEnabled = false,
|
|
sharedViewport = undefined,
|
|
onViewportChange = undefined,
|
|
leftHeader = undefined,
|
|
diffBeforeFlow = undefined,
|
|
currentInputSchema = undefined,
|
|
markRemovedAsShadowed = false,
|
|
multiSelectEnabled = false
|
|
}: Props = $props()
|
|
|
|
// Initialize note manager with fine-grained reactivity
|
|
const noteManager = new NoteManager(
|
|
() => notes ?? [],
|
|
(newNodes) => {
|
|
nodes = newNodes
|
|
},
|
|
() => nodes
|
|
)
|
|
|
|
// Runtime text height tracking for notes (not stored in FlowNote)
|
|
let noteTextHeights = $state<Record<string, number>>({})
|
|
|
|
// Reference to pane context menu component
|
|
let paneContextMenu: PaneContextMenu | undefined = $state(undefined)
|
|
let flowContainer: HTMLDivElement | undefined = $state(undefined)
|
|
|
|
// Selection manager - create one if not provided
|
|
let selectionManager = selectionManagerProp || new SelectionManager()
|
|
const selectedId = $derived(selectionManager.getSelectedId())
|
|
|
|
const noteEditorContext = getNoteEditorContext()
|
|
|
|
// Function to calculate extra gap needed for notes below the lowest flow nodes
|
|
function calculateNoteGap(notes: FlowNote[] | undefined): number {
|
|
if (!notes || notes.length === 0) {
|
|
return 0
|
|
}
|
|
let maxNoteBelowGap = 0
|
|
|
|
notes.forEach((note) => {
|
|
if (note.position?.y && note.position.y < 0) {
|
|
maxNoteBelowGap = Math.max(maxNoteBelowGap, -note.position.y)
|
|
}
|
|
})
|
|
|
|
return maxNoteBelowGap
|
|
}
|
|
|
|
// Calculate note gap based on current nodes and notes
|
|
const topPadding = editMode ? 100 : 24
|
|
const yOffset = calculateNoteGap(notes) + topPadding
|
|
|
|
setGraphContext({
|
|
selectionManager: selectionManager,
|
|
useDataflow,
|
|
showAssets,
|
|
noteManager,
|
|
clearFlowSelection,
|
|
yOffset,
|
|
diffManager
|
|
} as any)
|
|
|
|
if (triggerContext && allowSimplifiedPoll) {
|
|
if (isSimplifiable(modules)) {
|
|
triggerContext?.simplifiedPoll?.set(true)
|
|
}
|
|
triggerContext?.simplifiedPoll.subscribe((value) => {
|
|
computeSimplifiableFlow(modules ?? [], value ?? false)
|
|
})
|
|
}
|
|
|
|
function computeSimplifiableFlow(modules: FlowModule[], simplifiedFlow: boolean) {
|
|
const isSimplif = isSimplifiable(modules)
|
|
simplifiableFlow = isSimplif ? { simplifiedFlow } : undefined
|
|
}
|
|
|
|
onDestroy(() => {
|
|
if (isSimplifiable(modules)) {
|
|
triggerContext?.simplifiedPoll?.set(undefined)
|
|
}
|
|
diffManager.setDiffDrawer(undefined)
|
|
})
|
|
|
|
function onModulesChange(modules: FlowModule[]) {
|
|
computeSimplifiableFlow(
|
|
modules,
|
|
triggerContext?.simplifiedPoll ? (get(triggerContext.simplifiedPoll) ?? false) : false
|
|
)
|
|
}
|
|
|
|
type NodeDep = {
|
|
id: string
|
|
parentIds?: string[]
|
|
offset?: number
|
|
data?: { assets?: AssetWithAltAccessType[] }
|
|
}
|
|
type NodePos = { position: { x: number; y: number } }
|
|
let lastNodes: [NodeDep[], (NodeDep & NodePos)[]] | undefined = undefined
|
|
|
|
function layoutNodes(nodes: NodeDep[]): (NodeDep & NodePos)[] {
|
|
let lastResult = lastNodes?.[1]
|
|
if (lastResult && deepEqual(nodes, lastNodes?.[0])) {
|
|
console.debug('layoutNodes', 'same nodes')
|
|
return lastResult
|
|
}
|
|
console.debug('layoutNodes', nodes.length)
|
|
let seenId: string[] = []
|
|
for (const n of nodes) {
|
|
if (seenId.includes(n.id)) {
|
|
n.id = n.id + '_dup'
|
|
}
|
|
seenId.push(n.id)
|
|
}
|
|
|
|
let nodeWidths: Record<string, number> = {}
|
|
const nodes2: (NodeDep & NodePos)[] = nodes.map((n) => {
|
|
return { ...n, position: { x: 0, y: 0 } }
|
|
})
|
|
for (const n of topologicalSort(nodes)) {
|
|
const endId = n.id + '-end'
|
|
|
|
if (nodeWidths[endId] != undefined) {
|
|
nodeWidths[n.id] = Math.max(nodeWidths[n.id] ?? 0, nodeWidths[endId])
|
|
}
|
|
if (n.parentIds && n.parentIds?.length == 1) {
|
|
const parent = n.parentIds[0]
|
|
const nodeWidth = nodeWidths[n.id] ?? 1
|
|
nodeWidths[parent] = (nodeWidths[parent] ?? 0) + nodeWidth
|
|
}
|
|
}
|
|
|
|
const dag = dagStratify().id(({ id }: NodeDep & NodePos) => id)(nodes2)
|
|
|
|
let boxSize: any
|
|
try {
|
|
const layout = sugiyama()
|
|
.decross(nodes.length > 20 ? decrossTwoLayer() : decrossOpt())
|
|
.coord(coordCenter())
|
|
.nodeSize((d) => {
|
|
return [
|
|
(nodeWidths[d?.data?.['id'] ?? ''] ?? 1) * (NODE.width + NODE.gap.horizontal * 1),
|
|
NODE.height + NODE.gap.vertical
|
|
] as readonly [number, number]
|
|
})
|
|
boxSize = layout(dag as any)
|
|
} catch {
|
|
const layout = sugiyama()
|
|
.decross(decrossTwoLayer())
|
|
.coord(coordCenter())
|
|
.nodeSize(() => [NODE.width + NODE.gap.horizontal, NODE.height + NODE.gap.vertical])
|
|
boxSize = layout(dag as any)
|
|
}
|
|
|
|
const newNodes = dag.descendants().map((des) => ({
|
|
id: des.data.id,
|
|
position: {
|
|
x: des.x
|
|
? // @ts-ignore
|
|
(des.data.offset ?? 0) +
|
|
// @ts-ignore
|
|
des.x +
|
|
(fullSize ? fullWidth : width) / 2 -
|
|
boxSize.width / 2 -
|
|
NODE.width / 2 -
|
|
(width - fullWidth) / 2
|
|
: 0,
|
|
y: des.y || 0
|
|
}
|
|
}))
|
|
|
|
lastNodes = [nodes, newNodes]
|
|
return newNodes
|
|
}
|
|
|
|
let eventHandler = {
|
|
deleteBranch: (detail, label) => {
|
|
selectionManager.selectId(label)
|
|
onDeleteBranch?.(detail)
|
|
},
|
|
insert: (detail) => {
|
|
onInsert?.(detail)
|
|
},
|
|
select: (modId) => {
|
|
// AI tools are not selectable by the flow. Selection has to be refactored to be simplier.
|
|
if (nodes.find((n) => n.data?.moduleId === modId)?.type === 'aiTool' || modId === 'Trigger') {
|
|
selectionManager.selectId(modId)
|
|
}
|
|
if (!notSelectable) {
|
|
onSelect?.(modId)
|
|
}
|
|
},
|
|
changeId: (detail) => {
|
|
onChangeId?.(detail)
|
|
},
|
|
delete: (detail) => {
|
|
onDelete?.(detail.id)
|
|
},
|
|
newBranch: (id) => {
|
|
onNewBranch?.(id)
|
|
},
|
|
move: (detail) => {
|
|
onMove?.(detail.id)
|
|
},
|
|
selectedIteration: (detail) => {
|
|
onSelectedIteration?.(detail)
|
|
},
|
|
simplifyFlow: (detail) => {
|
|
triggerContext?.simplifiedPoll.set(detail)
|
|
},
|
|
expandSubflow: async (id: string, path: string) => {
|
|
const flow = await FlowService.getFlowByPath({ workspace: workspace, path })
|
|
expandedSubflows[id] = flow.value.modules
|
|
expandedSubflows = expandedSubflows
|
|
},
|
|
minimizeSubflow: (id: string) => {
|
|
delete expandedSubflows[id]
|
|
expandedSubflows = expandedSubflows
|
|
},
|
|
updateMock: (detail) => {
|
|
onUpdateMock?.(detail)
|
|
},
|
|
testUpTo: (id: string) => {
|
|
onTestUpTo?.(id)
|
|
},
|
|
editInput: (moduleId: string, key: string) => {
|
|
onEditInput?.(moduleId, key)
|
|
},
|
|
testFlow: () => {
|
|
onTestFlow?.()
|
|
},
|
|
cancelTestFlow: () => {
|
|
onCancelTestFlow?.()
|
|
},
|
|
openPreview: () => {
|
|
onOpenPreview?.()
|
|
},
|
|
hideJobStatus: () => {
|
|
onHideJobStatus?.()
|
|
}
|
|
}
|
|
|
|
// Validation: error if both diffBeforeFlow and moduleActions are provided
|
|
$effect(() => {
|
|
if (diffBeforeFlow && moduleActions) {
|
|
throw new Error('Cannot provide both diffBeforeFlow and moduleActions props to FlowGraphV2')
|
|
}
|
|
})
|
|
|
|
// Sync props to diffManager
|
|
$effect(() => {
|
|
const currentFlowValue = {
|
|
modules: modules,
|
|
failure_module: failureModule,
|
|
preprocessor_module: preprocessorModule
|
|
}
|
|
diffManager.setCurrentFlow(currentFlowValue)
|
|
diffManager.setCurrentInputSchema(currentInputSchema)
|
|
|
|
// Handle diff mode setup
|
|
if (diffBeforeFlow) {
|
|
diffManager.setEditMode(editMode)
|
|
diffManager.setBeforeFlow(diffBeforeFlow)
|
|
diffManager.setMarkRemovedAsShadowed(markRemovedAsShadowed)
|
|
} else if (moduleActions) {
|
|
// Display-only mode: just set the module actions
|
|
diffManager.setModuleActions(moduleActions)
|
|
}
|
|
})
|
|
|
|
// Use diffManager state for rendering
|
|
let effectiveModuleActions = $derived(diffManager.moduleActions)
|
|
|
|
// Use merged flow when in diff mode (includes removed modules), otherwise use raw modules
|
|
let effectiveModules = $derived(diffManager.mergedFlow?.modules ?? modules)
|
|
|
|
let effectiveFailureModule = $derived(diffManager.mergedFlow?.failure_module ?? failureModule)
|
|
|
|
let effectivePreprocessorModule = $derived(
|
|
diffManager.mergedFlow?.preprocessor_module ?? preprocessorModule
|
|
)
|
|
|
|
let canUseDiffDrawer = $derived(diffBeforeFlow || moduleActions || editMode)
|
|
|
|
// Initialize moduleTracker with effectiveModules
|
|
let moduleTracker = $state(new ChangeTracker<FlowModule[]>([]))
|
|
|
|
let nodes = $state.raw<Node[]>([])
|
|
let edges = $state.raw<Edge[]>([])
|
|
|
|
let height = $state(0)
|
|
|
|
// Derived nodes with yOffset applied to all nodes uniformly and selectable flag set to false if notSelectable is true
|
|
const nodesWithOffset = $derived.by(() => {
|
|
return nodes.map((node) => {
|
|
if (node.type && !AI_OR_ASSET_NODE_TYPES.includes(node.type)) {
|
|
return {
|
|
...node,
|
|
position: { ...node.position, y: node.position.y + yOffset },
|
|
selectable: notSelectable ? false : node.selectable
|
|
}
|
|
}
|
|
return {
|
|
...node,
|
|
selectable: notSelectable ? false : node.selectable
|
|
}
|
|
})
|
|
})
|
|
|
|
// Note feature state
|
|
|
|
function isSimplifiable(modules: FlowModule[] | undefined): boolean {
|
|
if (!modules || modules?.length !== 2) {
|
|
return false
|
|
}
|
|
if (isTriggerStep(modules?.[0])) {
|
|
let secondValue = modules?.[1].value
|
|
return secondValue.type == 'forloopflow'
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// Clear SvelteFlow's internal selection by creating new nodes array
|
|
function clearFlowSelection() {
|
|
nodes = nodes.map((node) => {
|
|
if (node.selected) {
|
|
return { ...node, selected: false }
|
|
}
|
|
return node
|
|
})
|
|
}
|
|
|
|
// Keyboard event handling
|
|
function handleKeyDown(event: KeyboardEvent) {
|
|
selectionManager.handleKeyDown(event)
|
|
noteManager.handleKeyDown(event)
|
|
if (event.key === 'Escape') {
|
|
if (noteMode) {
|
|
exitNoteMode?.()
|
|
}
|
|
}
|
|
}
|
|
|
|
async function updateStores() {
|
|
if (graph.error) {
|
|
return
|
|
}
|
|
|
|
// console.log('compute')
|
|
|
|
let layoutedNodes = layoutNodes(
|
|
Object.values(graph.nodes).map((n) => ({
|
|
id: n.id,
|
|
parentIds: n.parentIds,
|
|
offset: n.data.offset ?? 0,
|
|
data: { assets: (n.data as any).assets }
|
|
}))
|
|
)
|
|
let newNodes: (Node & NodeLayout)[] = layoutedNodes.map((n) => ({ ...n, ...graph.nodes[n.id] }))
|
|
|
|
let assetNodesResult = $showAssets
|
|
? computeAssetNodes(
|
|
newNodes.map((n) => ({
|
|
data: { assets: n.data?.assets as AssetWithAltAccessType[] },
|
|
id: n.id,
|
|
position: n.position
|
|
}))
|
|
)
|
|
: undefined
|
|
if (assetNodesResult) {
|
|
newNodes = newNodes.map((n) => ({
|
|
...n,
|
|
position: assetNodesResult.newNodePositions[n.id]
|
|
}))
|
|
}
|
|
let aiToolNodesResult = computeAIToolNodes(newNodes, eventHandler, insertable, flowModuleStates)
|
|
let nodesAfterAITools = newNodes.map((n) => ({
|
|
...n,
|
|
position: aiToolNodesResult.newNodePositions[n.id]
|
|
}))
|
|
|
|
let finalNodes = [
|
|
...nodesAfterAITools,
|
|
...(assetNodesResult?.newAssetNodes ?? []),
|
|
...aiToolNodesResult.toolNodes
|
|
]
|
|
|
|
// Compute note nodes and positions
|
|
let noteNodesResult = showNotes
|
|
? computeNoteNodes(
|
|
finalNodes.map((n) => ({
|
|
id: n.id,
|
|
position: n.position,
|
|
parentIds: n.parentIds,
|
|
offset: n.data?.offset ?? 0,
|
|
data: { assets: (n.data as any)?.assets },
|
|
type: n.type
|
|
})),
|
|
notes ?? [],
|
|
noteTextHeights,
|
|
(noteId: string, height: number) => {
|
|
noteTextHeights[noteId] = height
|
|
noteManager.render()
|
|
},
|
|
editMode,
|
|
noteEditorContext
|
|
)
|
|
: undefined
|
|
|
|
// Apply note positioning to nodes if notes are enabled
|
|
if (noteNodesResult) {
|
|
finalNodes = finalNodes.map((n) => ({
|
|
...n,
|
|
position: noteNodesResult.newNodePositions[n.id] || n.position
|
|
}))
|
|
}
|
|
|
|
// update nodes
|
|
nodes = [...finalNodes, ...(noteNodesResult?.noteNodes ?? [])]
|
|
|
|
edges = [
|
|
...(assetNodesResult?.newAssetEdges ?? []),
|
|
...aiToolNodesResult.toolEdges,
|
|
...graph.edges
|
|
]
|
|
|
|
await tick()
|
|
updateHeight()
|
|
}
|
|
|
|
function updateHeight() {
|
|
if (nodes.length === 0) {
|
|
height = minHeight
|
|
} else {
|
|
const minY = Math.min(...nodes.map((n) => n.position.y))
|
|
const maxBottom = Math.max(...nodes.map((n) => n.position.y + NODE.height + 100))
|
|
height = Math.max(maxBottom - minY, minHeight)
|
|
}
|
|
}
|
|
|
|
$effect(() => {
|
|
minHeight
|
|
untrack(() => updateHeight())
|
|
})
|
|
|
|
const nodeTypes = {
|
|
input2: InputNode,
|
|
module: ModuleNode,
|
|
branchAllStart: BranchAllStart,
|
|
branchAllEnd: BranchAllEndNode,
|
|
forLoopEnd: ForLoopEndNode,
|
|
forLoopStart: ForLoopStartNode,
|
|
result: ResultNode,
|
|
whileLoopStart: ForLoopStartNode,
|
|
whileLoopEnd: ForLoopEndNode,
|
|
branchOneStart: BranchOneStart,
|
|
branchOneEnd: BranchOneEndNode,
|
|
subflowBound: SubflowBound,
|
|
noBranch: NoBranchNode,
|
|
trigger: TriggersNode,
|
|
asset: AssetNode,
|
|
assetsOverflowed: AssetsOverflowedNode,
|
|
aiTool: AiToolNode,
|
|
newAiTool: NewAiToolNode,
|
|
note: NoteNode
|
|
} as any
|
|
|
|
const edgeTypes = {
|
|
edge: BaseEdge,
|
|
empty: EmptyEdge,
|
|
dataflowedge: DataflowEdge,
|
|
hiddenedge: HiddenBaseEdge
|
|
} as any
|
|
|
|
const proOptions = { hideAttribution: true }
|
|
|
|
// onMount(() => {
|
|
// centerViewport(width)
|
|
// })
|
|
let yamlEditorDrawer: Drawer | undefined = $state(undefined)
|
|
let diffDrawer: DiffDrawer | undefined = $state(undefined)
|
|
|
|
const flowGraphAssetsCtx = getContext<FlowGraphAssetContext | undefined>('FlowGraphAssetContext')
|
|
|
|
$effect(() => {
|
|
allowSimplifiedPoll && modules && untrack(() => onModulesChange(modules ?? []))
|
|
})
|
|
$effect(() => {
|
|
readFieldsRecursively(effectiveModules)
|
|
untrack(() => moduleTracker.track($state.snapshot(effectiveModules)))
|
|
})
|
|
|
|
// Wire up the diff drawer to the diffManager
|
|
$effect(() => {
|
|
diffManager.setDiffDrawer(diffDrawer)
|
|
})
|
|
|
|
let graph = $derived.by(() => {
|
|
moduleTracker.counter
|
|
effectiveModuleActions
|
|
return graphBuilder(
|
|
untrack(() => effectiveModules),
|
|
{
|
|
disableAi,
|
|
insertable,
|
|
flowModuleStates: untrack(() => flowModuleStates),
|
|
testModuleStates: untrack(() => testModuleStates),
|
|
moduleActions: untrack(() => effectiveModuleActions),
|
|
selectedId: untrack(() => selectedId),
|
|
path,
|
|
newFlow,
|
|
cache,
|
|
earlyStop,
|
|
editMode,
|
|
isOwner,
|
|
isRunning,
|
|
individualStepTests,
|
|
flowJob,
|
|
showJobStatus,
|
|
suspendStatus,
|
|
flowHasChanged,
|
|
chatInputEnabled,
|
|
additionalAssetsMap: flowGraphAssetsCtx?.val.additionalAssetsMap
|
|
},
|
|
untrack(() => effectiveFailureModule),
|
|
effectivePreprocessorModule,
|
|
eventHandler,
|
|
success,
|
|
$useDataflow,
|
|
untrack(() => selectedId),
|
|
moving,
|
|
simplifiableFlow,
|
|
triggerNode ? path : undefined,
|
|
expandedSubflows
|
|
)
|
|
})
|
|
let hideAssetsToggle = $derived(
|
|
$showAssets && Object.values(nodes).every((n) => n.type !== 'asset')
|
|
)
|
|
let hideNotesToggle = $derived(!notes || notes.length === 0)
|
|
|
|
$effect(() => {
|
|
;[graph, allowSimplifiedPoll, $showAssets, showNotes, noteManager.renderCount]
|
|
untrack(async () => {
|
|
await updateStores()
|
|
})
|
|
})
|
|
|
|
// Add global keyboard event listener for selection controls
|
|
onMount(() => {
|
|
function globalKeyDownHandler(event: KeyboardEvent) {
|
|
handleKeyDown(event)
|
|
}
|
|
|
|
document.addEventListener('keydown', globalKeyDownHandler)
|
|
|
|
return () => {
|
|
document.removeEventListener('keydown', globalKeyDownHandler)
|
|
}
|
|
})
|
|
|
|
// DOM event handling for pane clicks in rect-select mode
|
|
$effect(() => {
|
|
// Only add manual handling when in rect-select mode
|
|
if (selectionManager.mode !== 'rect-select') {
|
|
return
|
|
}
|
|
|
|
function paneClickHandler(event: Event) {
|
|
// Find the pane within our specific flow container
|
|
const pane = flowContainer?.querySelector('.svelte-flow__pane')
|
|
if (!pane || !event.target || !pane.contains(event.target as Element)) {
|
|
return
|
|
}
|
|
|
|
// Don't trigger if clicking on nodes or UI elements
|
|
const target = event.target as Element
|
|
if (
|
|
target.closest('.svelte-flow__node') ||
|
|
target.closest('button') ||
|
|
target.closest('[role="button"]') ||
|
|
target.closest('.svelte-flow__controls')
|
|
) {
|
|
return
|
|
}
|
|
|
|
// Trigger the same logic as onpaneclick
|
|
document.dispatchEvent(new Event('focus'))
|
|
selectionManager.clearSelection()
|
|
}
|
|
|
|
const pane = flowContainer?.querySelector('.svelte-flow__pane')
|
|
if (pane) {
|
|
pane.addEventListener('click', paneClickHandler)
|
|
}
|
|
|
|
return () => {
|
|
const pane = flowContainer?.querySelector('.svelte-flow__pane')
|
|
if (pane) {
|
|
pane.removeEventListener('click', paneClickHandler)
|
|
}
|
|
}
|
|
})
|
|
|
|
let showDataflow = $derived(
|
|
selectedId !== undefined &&
|
|
selectedId !== null &&
|
|
!selectedId?.startsWith('constants') &&
|
|
!selectedId?.startsWith('settings') &&
|
|
selectedId !== 'failure' &&
|
|
selectedId !== 'preprocessor' &&
|
|
selectedId !== 'Result' &&
|
|
selectedId !== 'Trigger'
|
|
)
|
|
let debouncedWidth: number | undefined = $state(undefined)
|
|
let timeout: number | undefined = $state(undefined)
|
|
$effect(() => {
|
|
if (!debouncedWidth) {
|
|
return
|
|
}
|
|
if (untrack(() => width) == undefined) {
|
|
width = debouncedWidth
|
|
return
|
|
}
|
|
if (untrack(() => timeout)) {
|
|
clearTimeout(untrack(() => timeout))
|
|
}
|
|
timeout = setTimeout(() => {
|
|
if (debouncedWidth && untrack(() => width) != debouncedWidth) {
|
|
width = debouncedWidth
|
|
}
|
|
}, 10)
|
|
})
|
|
|
|
let viewportResizer: ViewportResizer | undefined = $state(undefined)
|
|
let viewportSynchronizer: ViewportSynchronizer | undefined = $state(undefined)
|
|
|
|
export function isNodeVisible(nodeId: string): boolean {
|
|
return viewportResizer?.isNodeVisible(nodeId) ?? false
|
|
}
|
|
|
|
export function zoomIn() {
|
|
viewportSynchronizer?.zoomIn()
|
|
}
|
|
|
|
export function zoomOut() {
|
|
viewportSynchronizer?.zoomOut()
|
|
}
|
|
|
|
export function getDiffManager() {
|
|
return diffManager
|
|
}
|
|
|
|
export function enableNotes() {
|
|
if (!showNotes) {
|
|
showNotes = true
|
|
}
|
|
}
|
|
|
|
const modifierKey = isMac() ? 'Meta' : 'Control'
|
|
</script>
|
|
|
|
{#if insertable}
|
|
<FlowYamlEditor bind:drawer={yamlEditorDrawer} />
|
|
{/if}
|
|
{#if canUseDiffDrawer}
|
|
<DiffDrawer bind:this={diffDrawer} />
|
|
{/if}
|
|
<div
|
|
style={`height: ${height}px; max-height: ${maxHeight}px;`}
|
|
class="overflow-clip relative"
|
|
bind:clientWidth={debouncedWidth}
|
|
bind:this={flowContainer}
|
|
>
|
|
{#if graph?.error}
|
|
<div class="center-center p-2">
|
|
<Alert title="Error parsing the flow" type="error" class="max-w-1/2">
|
|
{graph.error}
|
|
|
|
<Button
|
|
color="red"
|
|
size="xs"
|
|
btnClasses="mt-2 w-min"
|
|
on:click={() => yamlEditorDrawer?.openDrawer()}>Open YAML editor</Button
|
|
>
|
|
</Alert>
|
|
</div>
|
|
{:else}
|
|
<SvelteFlowProvider>
|
|
<ViewportResizer {height} {width} {nodes} bind:this={viewportResizer} />
|
|
{#if sharedViewport && onViewportChange}
|
|
<ViewportSynchronizer
|
|
{sharedViewport}
|
|
onLocalChange={onViewportChange}
|
|
bind:this={viewportSynchronizer}
|
|
/>
|
|
{/if}
|
|
<PaneContextMenu {editMode} bind:this={paneContextMenu} />
|
|
<SvelteFlow
|
|
onpaneclick={() => {
|
|
document.dispatchEvent(new Event('focus'))
|
|
selectionManager.clearSelection()
|
|
}}
|
|
onpanecontextmenu={({ event }) => {
|
|
paneContextMenu?.onPaneContextMenu(event)
|
|
}}
|
|
onnodedragstop={(event) => {
|
|
const node = event.targetNode
|
|
if (node && node.type === 'note') {
|
|
const positionWithOffset = {
|
|
x: node.position.x,
|
|
y: node.position.y - yOffset
|
|
}
|
|
onNotePositionUpdate?.(node.id, positionWithOffset)
|
|
}
|
|
}}
|
|
onmove={(event, viewport) => {
|
|
viewportSynchronizer?.handleLocalViewportChange(event, viewport)
|
|
}}
|
|
nodes={nodesWithOffset}
|
|
{edges}
|
|
{edgeTypes}
|
|
{nodeTypes}
|
|
{height}
|
|
{width}
|
|
minZoom={0.2}
|
|
maxZoom={1.6}
|
|
connectionLineType={ConnectionLineType.SmoothStep}
|
|
defaultEdgeOptions={{ type: 'smoothstep' }}
|
|
preventScrolling={scroll}
|
|
selectionOnDrag={selectionManager.mode === 'rect-select'}
|
|
elementsSelectable={true}
|
|
selectionMode={SelectionMode.Partial}
|
|
selectionKey={selectionManager.mode === 'rect-select' || !editMode ? null : modifierKey}
|
|
panActivationKey={selectionManager.mode === 'rect-select' ? modifierKey : null}
|
|
panOnDrag={selectionManager.mode === 'rect-select' ? [1] : true}
|
|
zoomOnDoubleClick={false}
|
|
elevateNodesOnSelect={false}
|
|
{proOptions}
|
|
multiSelectionKey={'Shift'}
|
|
nodesDraggable={false}
|
|
--background-color={false}
|
|
>
|
|
<div class="absolute inset-0 !bg-surface-secondary h-full" id="flow-graph-v2"></div>
|
|
|
|
{#if noteMode}
|
|
<NoteTool {exitNoteMode} {yOffset} />
|
|
{/if}
|
|
|
|
{#if multiSelectEnabled}
|
|
<SelectionBoundingBox
|
|
selectedNodes={selectionManager.selectedIds}
|
|
allNodes={nodesWithOffset as (Node & { type: string })[]}
|
|
/>
|
|
{/if}
|
|
|
|
<!-- SelectionTool for handling selection changes and filtering -->
|
|
<SelectionTool {selectionManager} clearGraphSelection={clearFlowSelection} />
|
|
|
|
{#if leftHeader}
|
|
<div class="absolute top-2 left-2 z-10">
|
|
{@render leftHeader()}
|
|
</div>
|
|
{:else}
|
|
<Controls position="top-right" orientation="horizontal" showLock={false}>
|
|
{#if multiSelectEnabled}
|
|
<div class="flex items-center gap-2">
|
|
<Tooltip>
|
|
<ControlButton
|
|
onclick={() => {
|
|
selectionManager.mode =
|
|
selectionManager.mode === 'normal' ? 'rect-select' : 'normal'
|
|
}}
|
|
>
|
|
{#if selectionManager.mode === 'rect-select'}
|
|
<MousePointer size="14" />
|
|
{:else}
|
|
<Hand size="14" />
|
|
{/if}
|
|
</ControlButton>
|
|
{#snippet text()}
|
|
<div class="flex flex-col gap-2">
|
|
<div class="flex items-center gap-2">
|
|
<Hand size="14" />
|
|
<span class="text-secondary"
|
|
><strong class="text-primary">Grab</strong>: Click and drag to pan. Hold
|
|
<kbd class="text-primary text-lg">{getModifierKey()}</kbd> to box select.</span
|
|
>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<MousePointer size="14" />
|
|
<span class="text-secondary"
|
|
><strong class="text-primary">Select</strong> Click and drag to box
|
|
select. Hold
|
|
<kbd class="text-primary text-lg">{getModifierKey()}</kbd> to pan.</span
|
|
>
|
|
</div>
|
|
</div>
|
|
{/snippet}
|
|
</Tooltip>
|
|
</div>
|
|
{/if}
|
|
{#if download}
|
|
<ControlButton
|
|
onclick={() => {
|
|
try {
|
|
localStorage.setItem(
|
|
'svelvet',
|
|
encodeState({ modules, failureModule, preprocessorModule, notes })
|
|
)
|
|
} catch (e) {
|
|
console.error('error interacting with local storage', e)
|
|
}
|
|
window.open('/view_graph', '_blank')
|
|
}}
|
|
class="!bg-surface"
|
|
>
|
|
<Expand size="14" />
|
|
</ControlButton>
|
|
{/if}
|
|
</Controls>
|
|
|
|
<Controls
|
|
position="top-left"
|
|
orientation="vertical"
|
|
showLock={false}
|
|
showZoom={false}
|
|
showFitView={false}
|
|
class="!shadow-none gap-3"
|
|
style={leftHeader ? 'margin-top: 40px;' : ''}
|
|
>
|
|
{#if !hideAssetsToggle}
|
|
<Toggle bind:checked={$showAssets} size="xs" options={{ right: 'Assets' }} />
|
|
{/if}
|
|
{#if !hideNotesToggle}
|
|
<Toggle bind:checked={showNotes} size="xs" options={{ right: 'Notes' }} />
|
|
{/if}
|
|
{#if showDataflow}
|
|
<Toggle bind:checked={$useDataflow} size="xs" options={{ right: 'Dataflow' }} />
|
|
{/if}
|
|
</Controls>
|
|
{/if}
|
|
</SvelteFlow>
|
|
</SvelteFlowProvider>
|
|
{/if}
|
|
</div>
|
|
|
|
<style lang="postcss">
|
|
:global(.svelte-flow__handle) {
|
|
opacity: 0;
|
|
}
|
|
|
|
:global(.svelte-flow__controls-button) {
|
|
@apply bg-surface border-0;
|
|
}
|
|
:global(.svelte-flow__controls-button:hover) {
|
|
@apply bg-surface-hover;
|
|
}
|
|
|
|
:global(.svelte-flow__edgelabel-renderer) {
|
|
@apply z-50;
|
|
}
|
|
|
|
:global(.svelte-flow__selection) {
|
|
display: none;
|
|
pointer-events: none;
|
|
}
|
|
</style>
|