mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-21 00:02:30 +00:00
* Add note component
* save note size and position
* move add note button up
* nit
* Add markdown support
* wip
* fix add sticky note button
* fix text update
* Add sticky note to saved flow data
* add note color picker
* Introduce node multiselect
* Add group notes
* Adapt layout to group node
* create a note manager class
* clean reactivity
* clean
* improve adaptive layout to group note
* modify layout based on cached text height
* fined grained graph rendering for notes
* separate noteManager into editor and render
* separate noteManager into editor and render
* create a note change observer
* render note node from context
* simplify note state managment
* show note in flow viewer
* clean dirty changes
* clean selection manager
* fix layout check
* improve bg surface select
* Handle z-index for stacked group notes
* clean selection manager
* exclude notes from rect select
* Allow switch between selection modes with keyboard keys
* improve selection box styling
* prevent dragging note when editing
* nit
* Simplify selection using svelte flow built in feature
* handle note selection separately
* Add min size for notes
* improve selection toggle
* improve mode switch
* make size and position optional for group notes
* Improve initial viewport position
* Add context menu for the canevas
* nit
* Add node context menu
* improve note select
* use clickoutside for note deselect
* use pointerdown outside to close context menu
* nit
* fix selection issues
* make edges non selectable
* improve color palette
* fix backend
* fix backend check
* cargo lock restore
* Add toggle to display notes
* fix note selection
* nit
* account for css offset in for loop
* fix multiple selection pannel styling
* clear flow selection when creating note
* Improve placeholder and note default text
* Escape note edit mode when pressing Esc
* Allow note edition in local dev
* clean
* Handle subflow selection
* prevent group note resizing
* nit
* allow notes in flow expand
* Improve multi select panel
* Allow context menu in note mode
* Add event listenner to fix pane click deselect
* prevent zoom in text area in notes
* improve bounding box styling
* Use control for box selection for non mac users
* nit
* clean notes groups
* nit
* use portal for note actions
* handle assets node when computing note layout
* Simplify layout compute for notes
* use smart color choice for notes
* Switch display note when adding a new note
* clean code
* improve group note bound size calculation
* simplify AI tool nodes and asset handling
* nit
* nit
* improve flow centering
* create group note button
* Improve selection of nodes
* Revert "Improve selection of nodes"
This reverts commit d2c40d82b1.
* refert backend changes
* nit
* improve graph selection
* clean
* make backend work except job runs
* fix notSelectable
---------
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
312 lines
9.9 KiB
Svelte
312 lines
9.9 KiB
Svelte
<script lang="ts">
|
|
import FlowModuleWrapper from './FlowModuleWrapper.svelte'
|
|
import { type FlowModule } from '$lib/gen'
|
|
import { getContext } from 'svelte'
|
|
|
|
import type { FlowEditorContext } from '../types'
|
|
import FlowLoop from './FlowLoop.svelte'
|
|
import FlowModuleComponent from './FlowModuleComponent.svelte'
|
|
import FlowBranchAllWrapper from './FlowBranchAllWrapper.svelte'
|
|
import FlowBranchOneWrapper from './FlowBranchOneWrapper.svelte'
|
|
import {
|
|
createInlineScriptModule,
|
|
pickFlow,
|
|
pickScript
|
|
} from '$lib/components/flows/flowStateUtils.svelte'
|
|
import FlowInputs from './FlowInputs.svelte'
|
|
import { Alert } from '$lib/components/common'
|
|
import FlowInputsFlow from './FlowInputsFlow.svelte'
|
|
import FlowBranchesAllWrapper from './FlowBranchesAllWrapper.svelte'
|
|
import FlowBranchesOneWrapper from './FlowBranchesOneWrapper.svelte'
|
|
import FlowWhileLoop from './FlowWhileLoop.svelte'
|
|
import type { TriggerContext } from '$lib/components/triggers'
|
|
import { formatCron } from '$lib/utils'
|
|
import AgentToolWrapper from './AgentToolWrapper.svelte'
|
|
|
|
const { selectionManager, flowStateStore } = getContext<FlowEditorContext>('FlowEditorContext')
|
|
const selectedId = $derived(selectionManager.getSelectedId())
|
|
|
|
const { triggersState, triggersCount } = getContext<TriggerContext>('TriggerContext')
|
|
|
|
let scriptKind: 'script' | 'trigger' | 'approval' = $state('script')
|
|
let scriptTemplate: 'pgsql' | 'mysql' | 'script' | 'docker' | 'powershell' = $state('script')
|
|
|
|
// These pointers are used to easily access previewArgs of parent module, and previous module
|
|
|
|
interface Props {
|
|
flowModule: FlowModule
|
|
noEditor?: boolean
|
|
enableAi?: boolean
|
|
savedModule?: FlowModule | undefined
|
|
// Pointer to parent module, only defined within Branches or Loops.
|
|
parentModule?: FlowModule | undefined
|
|
// Pointer to previous module, for easy access to testing results
|
|
previousModule?: FlowModule | undefined
|
|
forceTestTab?: Record<string, boolean>
|
|
highlightArg?: Record<string, string | undefined>
|
|
isAgentTool?: boolean
|
|
}
|
|
|
|
let {
|
|
flowModule = $bindable(),
|
|
noEditor = false,
|
|
enableAi = false,
|
|
savedModule = undefined,
|
|
parentModule = $bindable(),
|
|
previousModule = undefined,
|
|
forceTestTab,
|
|
highlightArg,
|
|
isAgentTool = false
|
|
}: Props = $props()
|
|
|
|
function initializePrimaryScheduleForTriggerScript(module: FlowModule) {
|
|
const primaryIndex = triggersState.triggers.findIndex((t) => t.isPrimary)
|
|
if (primaryIndex === -1) {
|
|
const primaryCfg = {
|
|
summary: 'Scheduled poll of flow',
|
|
args: {},
|
|
schedule: formatCron('0 */15 * * *'),
|
|
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
enabled: true,
|
|
is_flow: true
|
|
}
|
|
triggersState.addDraftTrigger(triggersCount, 'schedule', undefined, primaryCfg)
|
|
} else if (triggersState.triggers[primaryIndex].draftConfig) {
|
|
//If there is a primary schedule draft update it
|
|
const newCfg = { ...triggersState.triggers[primaryIndex].draftConfig }
|
|
let updated = false
|
|
if (!newCfg.schedule) {
|
|
newCfg.schedule = formatCron('0 */15 * * *')
|
|
updated = true
|
|
}
|
|
if (!newCfg.enabled) {
|
|
newCfg.enabled = true
|
|
updated = true
|
|
}
|
|
if (updated) {
|
|
triggersState.triggers[primaryIndex].draftConfig = newCfg
|
|
}
|
|
}
|
|
|
|
module.stop_after_if = {
|
|
expr: 'result == undefined || Array.isArray(result) && result.length == 0',
|
|
skip_if_stopped: true
|
|
}
|
|
}
|
|
async function createModuleFromScript(
|
|
path: string,
|
|
summary: string,
|
|
kind: string,
|
|
hash: string | undefined
|
|
) {
|
|
const [module, state] = await pickScript(path, summary, flowModule.id, hash)
|
|
|
|
if (kind == 'approval') {
|
|
module.suspend = { required_events: 1, timeout: 1800 }
|
|
}
|
|
|
|
if (kind == 'trigger') {
|
|
initializePrimaryScheduleForTriggerScript(module)
|
|
}
|
|
|
|
flowModule = module
|
|
flowStateStore.val[module.id] = state
|
|
}
|
|
</script>
|
|
|
|
{#if flowModule.id === selectedId}
|
|
{#if flowModule.value.type === 'forloopflow'}
|
|
<FlowLoop {noEditor} bind:mod={flowModule} {parentModule} {previousModule} {enableAi} />
|
|
{:else if flowModule.value.type === 'whileloopflow'}
|
|
<FlowWhileLoop {noEditor} bind:mod={flowModule} {previousModule} {parentModule} />
|
|
{:else if flowModule.value.type === 'branchone'}
|
|
<FlowBranchesOneWrapper {noEditor} {previousModule} {parentModule} bind:flowModule {enableAi} />
|
|
{:else if flowModule.value.type === 'branchall'}
|
|
<FlowBranchesAllWrapper {noEditor} {previousModule} {parentModule} bind:flowModule />
|
|
{:else if flowModule.value.type === 'identity'}
|
|
{#if selectedId == 'failure'}
|
|
<div class="p-4">
|
|
<Alert type="info" title="Error handlers are triggered upon non recovered errors">
|
|
If defined, the error handler will take the error as input.
|
|
</Alert>
|
|
</div>
|
|
{:else if selectedId == 'preprocessor'}
|
|
<div class="p-4">
|
|
<Alert
|
|
type="info"
|
|
title="Preprocessor is called when the flow is triggered by API or email"
|
|
>
|
|
It prepares arguments for the flow. Besides request arguments, the preprocessor receives a
|
|
`wm_trigger` argument with trigger details.
|
|
</Alert>
|
|
</div>
|
|
{/if}
|
|
|
|
{#if flowModule.value.flow}
|
|
<FlowInputsFlow
|
|
on:pick={async ({ detail }) => {
|
|
const { path, summary } = detail
|
|
const [module, state] = await pickFlow(path, summary, flowModule.id)
|
|
|
|
flowModule = module
|
|
flowStateStore.val[module.id] = state
|
|
}}
|
|
/>
|
|
{:else}
|
|
<FlowInputs
|
|
{noEditor}
|
|
summary={flowModule.summary}
|
|
shouldDisableTriggerScripts={parentModule !== undefined ||
|
|
previousModule !== undefined ||
|
|
selectedId == 'failure' ||
|
|
selectedId == 'preprocessor'}
|
|
on:pick={async ({ detail }) => {
|
|
const { path, summary, kind, hash } = detail
|
|
createModuleFromScript(path, summary, kind, hash)
|
|
}}
|
|
on:new={async ({ detail }) => {
|
|
const { language, kind, subkind, summary } = detail
|
|
|
|
const [module, state] = await createInlineScriptModule(
|
|
language,
|
|
kind,
|
|
subkind,
|
|
flowModule.id,
|
|
summary
|
|
)
|
|
scriptKind = kind
|
|
scriptTemplate = subkind
|
|
|
|
if (kind == 'trigger') {
|
|
initializePrimaryScheduleForTriggerScript(module)
|
|
}
|
|
|
|
if (kind == 'approval') {
|
|
module.suspend = { required_events: 1, timeout: 1800 }
|
|
}
|
|
|
|
flowModule = module
|
|
flowStateStore.val[module.id] = state
|
|
}}
|
|
failureModule={selectedId === 'failure'}
|
|
preprocessorModule={selectedId === 'preprocessor'}
|
|
/>
|
|
{/if}
|
|
{:else if flowModule.value.type === 'rawscript' || flowModule.value.type === 'script' || flowModule.value.type === 'flow' || flowModule.value.type === 'aiagent'}
|
|
<FlowModuleComponent
|
|
{noEditor}
|
|
bind:flowModule
|
|
{parentModule}
|
|
{previousModule}
|
|
failureModule={selectedId === 'failure'}
|
|
preprocessorModule={selectedId === 'preprocessor'}
|
|
{scriptKind}
|
|
{scriptTemplate}
|
|
{enableAi}
|
|
{savedModule}
|
|
forceTestTab={forceTestTab?.[flowModule.id]}
|
|
highlightArg={highlightArg?.[flowModule.id]}
|
|
{isAgentTool}
|
|
/>
|
|
{/if}
|
|
{:else if flowModule.value.type === 'forloopflow' || flowModule.value.type == 'whileloopflow'}
|
|
{#each flowModule.value.modules as _, index (index)}
|
|
<FlowModuleWrapper
|
|
{noEditor}
|
|
bind:flowModule={flowModule.value.modules[index]}
|
|
bind:parentModule={flowModule}
|
|
previousModule={flowModule.value.modules[index - 1]}
|
|
savedModule={savedModule?.value.type === 'forloopflow' ||
|
|
savedModule?.value.type === 'whileloopflow'
|
|
? savedModule.value.modules[index]
|
|
: undefined}
|
|
{enableAi}
|
|
{forceTestTab}
|
|
{highlightArg}
|
|
/>
|
|
{/each}
|
|
{:else if flowModule.value.type === 'branchone'}
|
|
{#if selectedId === `${flowModule?.id}-branch-default`}
|
|
<div class="p-2">
|
|
<h3 class="mb-4">Default branch</h3>
|
|
Nothing to configure, this is the default branch if none of the predicates are met.
|
|
</div>
|
|
{:else}
|
|
{#each flowModule.value.default as _, index}
|
|
<FlowModuleWrapper
|
|
{noEditor}
|
|
bind:flowModule={flowModule.value.default[index]}
|
|
bind:parentModule={flowModule}
|
|
previousModule={flowModule.value.default[index - 1]}
|
|
savedModule={savedModule?.value.type === 'branchone'
|
|
? savedModule.value.default[index]
|
|
: undefined}
|
|
{enableAi}
|
|
{forceTestTab}
|
|
{highlightArg}
|
|
/>
|
|
{/each}
|
|
{/if}
|
|
{#each flowModule.value.branches as branch, branchIndex (branchIndex)}
|
|
{#if selectedId === `${flowModule?.id}-branch-${branchIndex}`}
|
|
<FlowBranchOneWrapper
|
|
{noEditor}
|
|
bind:branch={flowModule.value.branches[branchIndex]}
|
|
parentModule={flowModule}
|
|
{previousModule}
|
|
{enableAi}
|
|
/>
|
|
{:else}
|
|
{#each branch.modules as _, index}
|
|
<FlowModuleWrapper
|
|
{noEditor}
|
|
bind:flowModule={flowModule.value.branches[branchIndex].modules[index]}
|
|
bind:parentModule={flowModule}
|
|
previousModule={flowModule.value.branches[branchIndex].modules[index - 1]}
|
|
savedModule={savedModule?.value.type === 'branchone'
|
|
? savedModule.value.branches[branchIndex]?.modules[index]
|
|
: undefined}
|
|
{enableAi}
|
|
{forceTestTab}
|
|
{highlightArg}
|
|
/>
|
|
{/each}
|
|
{/if}
|
|
{/each}
|
|
{:else if flowModule.value.type === 'branchall'}
|
|
{#each flowModule.value.branches as branch, branchIndex (branchIndex)}
|
|
{#if selectedId === `${flowModule?.id}-branch-${branchIndex}`}
|
|
<FlowBranchAllWrapper {noEditor} bind:branch={flowModule.value.branches[branchIndex]} />
|
|
{:else}
|
|
{#each branch.modules as _, index}
|
|
<FlowModuleWrapper
|
|
{noEditor}
|
|
bind:flowModule={flowModule.value.branches[branchIndex].modules[index]}
|
|
bind:parentModule={flowModule}
|
|
previousModule={flowModule.value.branches[branchIndex].modules[index - 1]}
|
|
{enableAi}
|
|
savedModule={savedModule?.value.type === 'branchall'
|
|
? savedModule.value.branches[branchIndex]?.modules[index]
|
|
: undefined}
|
|
{forceTestTab}
|
|
{highlightArg}
|
|
/>
|
|
{/each}
|
|
{/if}
|
|
{/each}
|
|
{:else if flowModule.value.type === 'aiagent'}
|
|
{#each flowModule.value.tools as tool, toolIndex (toolIndex)}
|
|
{#if selectedId === tool.id}
|
|
<AgentToolWrapper
|
|
{noEditor}
|
|
bind:tool={flowModule.value.tools[toolIndex]}
|
|
parentModule={flowModule}
|
|
{previousModule}
|
|
{enableAi}
|
|
{forceTestTab}
|
|
{highlightArg}
|
|
/>
|
|
{/if}
|
|
{/each}
|
|
{/if}
|