mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-07 00:01:49 +00:00
feat: expandable subflows in flows (#4683)
* all * nit right panel flow props * nits
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
import FlowGraphViewerStep from './FlowGraphViewerStep.svelte'
|
||||
import FlowGraphV2 from './graph/FlowGraphV2.svelte'
|
||||
import { dfs } from './flows/dfs'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
|
||||
export let flow: {
|
||||
summary: string
|
||||
@@ -22,6 +23,7 @@
|
||||
export let noGraph = false
|
||||
export let triggerNode = false
|
||||
export let stepDetail: FlowModule | string | undefined = undefined
|
||||
export let workspace: string | undefined = $workspaceStore
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
</script>
|
||||
@@ -37,6 +39,7 @@
|
||||
path={flow?.path}
|
||||
{download}
|
||||
minHeight={400}
|
||||
{workspace}
|
||||
modules={flow?.value?.modules}
|
||||
failureModule={flow?.value?.failure_module}
|
||||
preprocessorModule={flow?.value?.preprocessor_module}
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
import Alert from './common/alert/Alert.svelte'
|
||||
import FlowGraphViewerStep from './FlowGraphViewerStep.svelte'
|
||||
import FlowGraphV2 from './graph/FlowGraphV2.svelte'
|
||||
import { buildPrefix } from './graph/graphBuilder'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@@ -74,6 +75,10 @@
|
||||
|
||||
export let wideResults = false
|
||||
export let hideFlowResult = false
|
||||
export let workspace: string | undefined = $workspaceStore
|
||||
export let prefix: string | undefined = undefined
|
||||
export let subflowParentsGlobalModuleStates: Writable<Record<string, GraphModuleState>>[] = []
|
||||
export let subflowParentsDurationStatuses: Writable<Record<string, DurationStatus>>[] = []
|
||||
|
||||
let jobResults: any[] =
|
||||
flowJobIds?.flowJobs?.map((x, id) => `iter #${id + 1} not loaded by frontend yet`) ?? []
|
||||
@@ -83,6 +88,7 @@
|
||||
|
||||
let localModuleStates: Writable<Record<string, GraphModuleState>> = writable({})
|
||||
let localDurationStatuses: Writable<Record<string, DurationStatus>> = writable({})
|
||||
let expandedSubflows: Record<string, FlowModule[]> = {}
|
||||
|
||||
export let job: Job | undefined = undefined
|
||||
|
||||
@@ -94,6 +100,24 @@
|
||||
// }
|
||||
// }
|
||||
|
||||
function updateModuleStates(
|
||||
moduleState: Writable<Record<string, GraphModuleState>>,
|
||||
key: string,
|
||||
newValue: GraphModuleState,
|
||||
keepType: boolean | undefined
|
||||
) {
|
||||
moduleState.update((x) => {
|
||||
if (keepType && (x[key]?.type == 'Success' || x[key]?.type == 'Failure')) {
|
||||
newValue.type = x[key].type
|
||||
}
|
||||
x[key] = newValue
|
||||
return x
|
||||
})
|
||||
}
|
||||
|
||||
function buildSubflowKey(key: string, prefix: string | undefined) {
|
||||
return prefix ? 'subflow:' + prefix + key : key
|
||||
}
|
||||
function setModuleState(
|
||||
key: string,
|
||||
value: Partial<GraphModuleState>,
|
||||
@@ -102,15 +126,14 @@
|
||||
) {
|
||||
let newValue = { ...($localModuleStates[key] ?? {}), ...value }
|
||||
if (!deepEqual($localModuleStates[key], value) || force) {
|
||||
;[localModuleStates, ...globalModuleStates].forEach((s) => {
|
||||
s.update((x) => {
|
||||
if (keepType && (x[key]?.type == 'Success' || x[key]?.type == 'Failure')) {
|
||||
newValue.type = x[key].type
|
||||
}
|
||||
x[key] = newValue
|
||||
return x
|
||||
})
|
||||
})
|
||||
;[localModuleStates, ...globalModuleStates].forEach((s) =>
|
||||
updateModuleStates(s, key, newValue, keepType)
|
||||
)
|
||||
if (prefix) {
|
||||
subflowParentsGlobalModuleStates.forEach((s) =>
|
||||
updateModuleStates(s, buildSubflowKey(key, prefix), newValue, keepType)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,6 +147,14 @@
|
||||
return x
|
||||
})
|
||||
})
|
||||
if (prefix) {
|
||||
subflowParentsDurationStatuses.forEach((s) => {
|
||||
s.update((x) => {
|
||||
x[buildSubflowKey(key, prefix)].byJob[id] = value
|
||||
return x
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,15 +162,25 @@
|
||||
if ($localDurationStatuses[modId] == undefined) {
|
||||
$localDurationStatuses[modId] = { byJob: {} }
|
||||
}
|
||||
let prefixed = modId
|
||||
globalDurationStatuses.forEach((x) =>
|
||||
x.update((x) => {
|
||||
if (x[prefixed] == undefined) {
|
||||
x[prefixed] = { byJob: {} }
|
||||
if (x[modId] == undefined) {
|
||||
x[modId] = { byJob: {} }
|
||||
}
|
||||
return x
|
||||
})
|
||||
)
|
||||
if (prefix) {
|
||||
subflowParentsDurationStatuses.forEach((x) =>
|
||||
x.update((x) => {
|
||||
let key = buildSubflowKey(modId, prefix)
|
||||
if (x[key] == undefined) {
|
||||
x[key] = { byJob: {} }
|
||||
}
|
||||
return x
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
let innerModules: FlowStatusModule[] = []
|
||||
@@ -368,7 +409,7 @@
|
||||
if (globalRefreshes) {
|
||||
let modId = flowJobIds?.moduleId
|
||||
if (modId) {
|
||||
globalRefreshes[modId] = async (loopJob) => {
|
||||
globalRefreshes[buildSubflowKey(modId, prefix)] = async (loopJob) => {
|
||||
setIteration(loopJob.index, loopJob.job, false, modId ?? '')
|
||||
refresh(true, loopJob)
|
||||
}
|
||||
@@ -631,6 +672,36 @@
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function allModulesForTimeline(
|
||||
modules: FlowModule[],
|
||||
expandedSubflows: Record<string, FlowModule[]>
|
||||
): string[] {
|
||||
const ids = dfs(modules, (x) => x.id)
|
||||
|
||||
function rec(ids: string[], prefix: string | undefined): string[] {
|
||||
return ids.concat(
|
||||
ids.flatMap((id) => {
|
||||
let fms = expandedSubflows[id]
|
||||
let oid = id.split(':').pop()
|
||||
if (!oid) {
|
||||
return []
|
||||
}
|
||||
let nprefix = buildPrefix(prefix, oid)
|
||||
return fms
|
||||
? rec(
|
||||
dfs(fms, (x) =>
|
||||
x.id.startsWith('subflow:') ? x.id : buildSubflowKey(x.id, nprefix)
|
||||
),
|
||||
nprefix
|
||||
)
|
||||
: []
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
return rec(ids, undefined)
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if notAnonynmous}
|
||||
@@ -803,6 +874,9 @@
|
||||
job={storedListJobs[j]}
|
||||
globalModuleStates={[localModuleStates, ...globalModuleStates]}
|
||||
globalDurationStatuses={[localDurationStatuses, ...globalDurationStatuses]}
|
||||
{prefix}
|
||||
{subflowParentsGlobalModuleStates}
|
||||
{subflowParentsDurationStatuses}
|
||||
render={forloop_selected == loopJobId && selected == 'sequence' && render}
|
||||
reducedPolling={flowJobIds?.flowJobs.length && flowJobIds?.flowJobs.length > 20}
|
||||
{workspaceId}
|
||||
@@ -880,6 +954,9 @@
|
||||
{childFlow}
|
||||
globalModuleStates={[localModuleStates, ...globalModuleStates]}
|
||||
globalDurationStatuses={[localDurationStatuses, ...globalDurationStatuses]}
|
||||
{prefix}
|
||||
{subflowParentsGlobalModuleStates}
|
||||
{subflowParentsDurationStatuses}
|
||||
render={failedRetry == retry_selected && render}
|
||||
reducedPolling={false}
|
||||
{workspaceId}
|
||||
@@ -895,6 +972,17 @@
|
||||
bind:refresh={recursiveRefresh[mod.job ?? '']}
|
||||
globalModuleStates={[]}
|
||||
globalDurationStatuses={[]}
|
||||
prefix={buildPrefix(prefix, mod.id ?? '')}
|
||||
subflowParentsGlobalModuleStates={[
|
||||
localModuleStates,
|
||||
...globalModuleStates,
|
||||
...subflowParentsGlobalModuleStates
|
||||
]}
|
||||
subflowParentsDurationStatuses={[
|
||||
localDurationStatuses,
|
||||
...globalDurationStatuses,
|
||||
...subflowParentsDurationStatuses
|
||||
]}
|
||||
render={selected == 'sequence' && render}
|
||||
{workspaceId}
|
||||
jobId={mod.job}
|
||||
@@ -915,6 +1003,9 @@
|
||||
globalDurationStatuses={[localDurationStatuses, ...globalDurationStatuses]}
|
||||
render={selected == 'sequence' && render}
|
||||
{workspaceId}
|
||||
{prefix}
|
||||
{subflowParentsGlobalModuleStates}
|
||||
{subflowParentsDurationStatuses}
|
||||
jobId={mod.job}
|
||||
innerModule={mod.flow_jobs ? job.raw_flow?.modules[i]?.value : undefined}
|
||||
flowJobIds={mod.flow_jobs
|
||||
@@ -973,6 +1064,7 @@
|
||||
minHeight={wrapperHeight}
|
||||
success={jobId != undefined && isSuccess(job?.['success'])}
|
||||
flowModuleStates={$localModuleStates}
|
||||
bind:expandedSubflows
|
||||
on:select={(e) => {
|
||||
if (rightColumnSelect != 'node_definition') {
|
||||
rightColumnSelect = 'node_status'
|
||||
@@ -1004,11 +1096,13 @@
|
||||
selectedForloopIndex: detail.index
|
||||
})
|
||||
globalRefreshes[detail.moduleId]?.({ job: detail.id, index: detail.index })
|
||||
// console.log('selectedIteration', prefix, detail.moduleId, globalRefreshes)
|
||||
}}
|
||||
modules={job.raw_flow?.modules ?? []}
|
||||
failureModule={job.raw_flow?.failure_module}
|
||||
preprocessorModule={job.raw_flow?.preprocessor_module}
|
||||
allowSimplifiedPoll={false}
|
||||
{workspace}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
@@ -1033,7 +1127,10 @@
|
||||
aggregateWaitTime={job?.aggregate_wait_time_ms}
|
||||
flowDone={job?.['success'] != undefined}
|
||||
bind:this={flowTimeline}
|
||||
flowModules={dfs(job.raw_flow?.modules ?? [], (x) => x.id)}
|
||||
flowModules={allModulesForTimeline(
|
||||
job?.raw_flow?.modules ?? [],
|
||||
expandedSubflows ?? {}
|
||||
)}
|
||||
durationStatuses={localDurationStatuses}
|
||||
/>
|
||||
{:else if rightColumnSelect == 'node_status'}
|
||||
|
||||
@@ -178,7 +178,7 @@
|
||||
{/if}
|
||||
|
||||
<div class="px-2 py-2 grid grid-cols-6 w-full">
|
||||
<div class="truncate">{k}</div>
|
||||
<div class="truncate">{k.startsWith('subflow:') ? k.substring(8) : k}</div>
|
||||
<div class="col-span-5 flex min-h-6">
|
||||
{#if min && total}
|
||||
<div class="flex flex-col gap-2 w-full p-2 ml-4">
|
||||
|
||||
@@ -78,6 +78,10 @@
|
||||
newItem={newFlow}
|
||||
isFlow={true}
|
||||
/>
|
||||
{:else if $selectedId.startsWith('subflow:')}
|
||||
<div class="p-4"
|
||||
>Selected step is witin an expanded subflow and is not directly editable in the flow editor</div
|
||||
>
|
||||
{:else}
|
||||
{@const dup = checkDup($flowStore.value.modules)}
|
||||
{#if dup}
|
||||
|
||||
@@ -253,6 +253,10 @@
|
||||
forceReload++
|
||||
await reload(flowModule)
|
||||
}
|
||||
if (flowModule.value.type == 'flow') {
|
||||
forceReload++
|
||||
await reload(flowModule)
|
||||
}
|
||||
}}
|
||||
on:createScriptFromInlineScript={async () => {
|
||||
const [module, state] = await createScriptFromInlineScript(
|
||||
@@ -361,7 +365,9 @@
|
||||
</div>
|
||||
{/if}
|
||||
{:else if flowModule.value.type === 'flow'}
|
||||
<FlowPathViewer path={flowModule.value.path} />
|
||||
{#key forceReload}
|
||||
<FlowPathViewer path={flowModule.value.path} />
|
||||
{/key}
|
||||
{/if}
|
||||
</Pane>
|
||||
<Pane bind:size={editorSettingsPanelSize} minSize={20}>
|
||||
|
||||
@@ -5,10 +5,12 @@
|
||||
import {
|
||||
Bed,
|
||||
Database,
|
||||
ExternalLink,
|
||||
Gauge,
|
||||
GitFork,
|
||||
Pen,
|
||||
PhoneIncoming,
|
||||
RefreshCcw,
|
||||
Repeat,
|
||||
Save,
|
||||
Square,
|
||||
@@ -139,6 +141,32 @@
|
||||
</Button>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if module.value.type === 'flow'}
|
||||
<Button
|
||||
size="xs"
|
||||
color="light"
|
||||
on:click={async () => {
|
||||
if (module.value.type == 'flow') {
|
||||
window.open(`/flows/edit/${module.value.path}`, '_blank', 'noopener,noreferrer')
|
||||
}
|
||||
}}
|
||||
startIcon={{ icon: Pen }}
|
||||
iconOnly={false}
|
||||
>
|
||||
Edit <ExternalLink size={12} />
|
||||
</Button>
|
||||
<Button
|
||||
size="xs"
|
||||
color="light"
|
||||
on:click={async () => {
|
||||
dispatch('reload')
|
||||
}}
|
||||
startIcon={{
|
||||
icon: RefreshCcw
|
||||
}}
|
||||
iconOnly={true}
|
||||
/>
|
||||
{/if}
|
||||
<div class="px-0.5" />
|
||||
{#if module.value.type === 'rawscript'}
|
||||
<FlowModuleWorkerTagSelect bind:tag={module.value.tag} />
|
||||
|
||||
@@ -276,8 +276,13 @@
|
||||
</Popover>
|
||||
|
||||
<div class="flex items-center space-x-2 relative max-w-[25%]" bind:clientWidth={idBadgeWidth}>
|
||||
{#if id && id !== 'preprocessor' && !id.startsWith('failure')}
|
||||
<Badge color="indigo" wrapperClass="max-w-full" baseClass="max-w-full truncate" title={id}>
|
||||
{#if id && id !== 'preprocessor' && !id.startsWith('failure') && !id.startsWith('subflow:')}
|
||||
<Badge
|
||||
color="indigo"
|
||||
wrapperClass="max-w-full"
|
||||
baseClass="max-w-full truncate !px-1"
|
||||
title={id}
|
||||
>
|
||||
<span class="max-w-full text-2xs truncate">{id}</span></Badge
|
||||
>
|
||||
{#if deletable}
|
||||
@@ -290,6 +295,10 @@ hover:border-blue-700 hover:!visible {hover ? '' : '!hidden'}"
|
||||
title="Edit Id"><Pencil size={14} /></button
|
||||
>
|
||||
{/if}
|
||||
{:else if id?.startsWith('subflow:')}
|
||||
<Badge color="blue" wrapperClass="max-w-full" baseClass="!px-1" title={id}>
|
||||
<span class="max-w-full text-2xs truncate">{id.substring('subflow:'.length)}</span></Badge
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
import { getDependentComponents } from '../flowExplorer'
|
||||
import type { FlowCopilotContext } from '$lib/components/copilot/flow'
|
||||
import { fade } from 'svelte/transition'
|
||||
import { copilotInfo, tutorialsToDo } from '$lib/stores'
|
||||
import { copilotInfo, tutorialsToDo, workspaceStore } from '$lib/stores'
|
||||
|
||||
import FlowTutorials from '$lib/components/FlowTutorials.svelte'
|
||||
import { ignoredTutorials } from '$lib/components/tutorials/ignoredTutorials'
|
||||
@@ -44,6 +44,7 @@
|
||||
export let disableSettings = false
|
||||
export let newFlow: boolean = false
|
||||
export let smallErrorHandler = false
|
||||
export let workspace: string | undefined = $workspaceStore
|
||||
|
||||
let flowTutorials: FlowTutorials | undefined = undefined
|
||||
|
||||
@@ -345,6 +346,7 @@
|
||||
preprocessorModule={$flowStore.value?.preprocessor_module}
|
||||
{selectedId}
|
||||
{flowInputsStore}
|
||||
{workspace}
|
||||
on:delete={({ detail }) => {
|
||||
let e = detail.detail
|
||||
dependents = getDependentComponents(e.id, $flowStore)
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
export let modules: FlowModule[]
|
||||
export let moving: string | undefined = undefined
|
||||
export let duration_ms: number | undefined = undefined
|
||||
export let isTrigger: boolean = false
|
||||
|
||||
export let retries: number | undefined = undefined
|
||||
export let flowJobs:
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
<div class="truncate text-2xs text-center"><pre>{preLabel}</pre></div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if id && !hideId}
|
||||
{#if id && !hideId && !id?.startsWith('subflow:')}
|
||||
<div class="flex items-center shrink min-w-0">
|
||||
<Badge color="indigo" wrapperClass="w-full" baseClass="max-w-full" title={id}>
|
||||
<span class="max-w-full text-2xs truncate">{id}</span>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { type FlowModule } from '../../gen'
|
||||
import { FlowService, type FlowModule } from '../../gen'
|
||||
import { NODE, type GraphModuleState } from '.'
|
||||
import { createEventDispatcher, getContext, onDestroy, onMount, setContext } from 'svelte'
|
||||
|
||||
@@ -39,6 +39,8 @@
|
||||
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'
|
||||
|
||||
export let success: boolean | undefined = undefined
|
||||
export let modules: FlowModule[] | undefined = []
|
||||
@@ -65,6 +67,7 @@
|
||||
undefined
|
||||
)
|
||||
export let triggerNode = false
|
||||
export let workspace: string = $workspaceStore ?? 'NO_WORKSPACE'
|
||||
|
||||
let useDataflow: Writable<boolean | undefined> = writable<boolean | undefined>(false)
|
||||
|
||||
@@ -85,6 +88,8 @@
|
||||
|
||||
let simplifiableFlow: SimplifiableFlow | undefined = undefined
|
||||
|
||||
export let expandedSubflows: Record<string, FlowModule[]> = {}
|
||||
|
||||
if (triggerContext && allowSimplifiedPoll) {
|
||||
if (isSimplifiable(modules)) {
|
||||
triggerContext?.simplifiedPoll?.set(true)
|
||||
@@ -201,6 +206,15 @@
|
||||
},
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,7 +236,8 @@
|
||||
$selectedId,
|
||||
moving,
|
||||
simplifiableFlow,
|
||||
triggerNode ? path : undefined
|
||||
triggerNode ? path : undefined,
|
||||
expandedSubflows
|
||||
)
|
||||
|
||||
const nodes = writable<Node[]>([])
|
||||
@@ -230,69 +245,6 @@
|
||||
|
||||
let height = 0
|
||||
|
||||
// function removeInputNode(nodes, edges, id) {
|
||||
// const inputNode = nodes.find((node) => node.id === id)
|
||||
// if (!inputNode) return { nodes, edges }
|
||||
|
||||
// // Find edges connected to the input node
|
||||
// const connectedEdges = edges.filter((edge) => edge.source === id || edge.target === id)
|
||||
|
||||
// // Remove the input node
|
||||
// let updatedNodes = nodes.filter((node) => node.id !== id)
|
||||
|
||||
// // Remove edges connected to the input node
|
||||
// let updatedEdges = edges.filter((edge) => edge.source !== id && edge.target !== id)
|
||||
|
||||
// // Create new edges from the input node's parent to its children
|
||||
// const inputEdges = connectedEdges.filter((edge) => edge.target === id)
|
||||
// const outputEdges = connectedEdges.filter((edge) => edge.source === id)
|
||||
|
||||
// inputEdges.forEach((inputEdge) => {
|
||||
// outputEdges.forEach((outputEdge) => {
|
||||
// const newEdge = {
|
||||
// id: `edge:${inputEdge.source}->${outputEdge.target}`,
|
||||
// source: inputEdge.source,
|
||||
// target: outputEdge.target,
|
||||
// type: 'empty',
|
||||
// data: {
|
||||
// ...outputEdge.data,
|
||||
// sourceId: inputEdge.source,
|
||||
// targetId: outputEdge.target
|
||||
// }
|
||||
// }
|
||||
// updatedEdges.push(newEdge)
|
||||
// })
|
||||
// })
|
||||
|
||||
// // Update parent ids of the nodes
|
||||
// updatedNodes = updatedNodes.map((node) => {
|
||||
// if (node.data && node.data.parentIds && node.data.parentIds.includes(id)) {
|
||||
// const updatedParentIds = node.data.parentIds.filter((parentId) => parentId !== id)
|
||||
// if (inputNode.data && inputNode.data.parentIds) {
|
||||
// updatedParentIds.push(...inputNode.data.parentIds)
|
||||
// }
|
||||
// return {
|
||||
// ...node,
|
||||
// data: {
|
||||
// ...node.data,
|
||||
// parentIds: [...new Set(updatedParentIds)] // Remove duplicates
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// return node
|
||||
// })
|
||||
|
||||
// return { nodes: updatedNodes, edges: updatedEdges }
|
||||
// }
|
||||
|
||||
// function processGraph(graph, simplifiable) {
|
||||
// let newGraph = { nodes: graph.nodes, edges: graph.edges }
|
||||
// newGraph = removeInputNode(newGraph.nodes, newGraph.edges, 'Input')
|
||||
// newGraph = removeInputNode(newGraph.nodes, newGraph.edges, simplifiable.forLoopNode.id)
|
||||
// newGraph = removeInputNode(newGraph.nodes, newGraph.edges, simplifiable.triggerNode.id)
|
||||
// return newGraph
|
||||
// }
|
||||
|
||||
function isSimplifiable(modules: FlowModule[] | undefined): boolean {
|
||||
if (!modules || modules?.length !== 2) {
|
||||
return false
|
||||
@@ -313,7 +265,7 @@
|
||||
|
||||
$nodes = layoutNodes(newGraph.nodes)
|
||||
$edges = newGraph.edges
|
||||
height = Math.max(...$nodes.map((n) => n.position.y + NODE.height + 40), minHeight)
|
||||
height = Math.max(...$nodes.map((n) => n.position.y + NODE.height + 100), minHeight)
|
||||
}
|
||||
|
||||
$: (graph || allowSimplifiedPoll) && updateStores()
|
||||
@@ -330,6 +282,7 @@
|
||||
whileLoopEnd: ForLoopEndNode,
|
||||
branchOneStart: BranchOneStart,
|
||||
branchOneEnd: BranchOneEndNode,
|
||||
subflowBound: SubflowBound,
|
||||
noBranch: NoBranchNode,
|
||||
trigger: TriggersNode
|
||||
} as any
|
||||
|
||||
@@ -15,6 +15,8 @@ export type GraphEventHandlers = {
|
||||
selectedIteration: (detail, moduleId: string) => void
|
||||
changeId: (newId: string) => void
|
||||
simplifyFlow: (detail: boolean) => void
|
||||
expandSubflow: (id: string, path: string) => void
|
||||
minimizeSubflow: (id: string) => void
|
||||
}
|
||||
|
||||
export type SimplifiableFlow = { simplifiedFlow: boolean }
|
||||
@@ -26,6 +28,10 @@ export function isTriggerStep(module: FlowModule | undefined): boolean {
|
||||
module.value.is_trigger === true
|
||||
)
|
||||
}
|
||||
|
||||
export function buildPrefix(prefix: string | undefined, id: string): string {
|
||||
return (prefix ?? '') + id + ':'
|
||||
}
|
||||
export function graphBuilder(
|
||||
modules: FlowModule[] | undefined,
|
||||
extra: Record<string, any>,
|
||||
@@ -37,7 +43,8 @@ export function graphBuilder(
|
||||
selectedId: string | undefined,
|
||||
moving: string | undefined,
|
||||
simplifiableFlow: SimplifiableFlow | undefined,
|
||||
flowPathForTriggerNode: string | undefined
|
||||
flowPathForTriggerNode: string | undefined,
|
||||
expandedSubflows: Record<string, FlowModule[]>
|
||||
// triggerProps?: {
|
||||
// path?: string
|
||||
// flowIsSimplifiable?: boolean
|
||||
@@ -61,6 +68,10 @@ export function graphBuilder(
|
||||
throw new Error(`Duplicated node detected: ${module.id}`)
|
||||
}
|
||||
|
||||
if (module.id.startsWith('subflow:')) {
|
||||
extra.insertable = false
|
||||
}
|
||||
|
||||
nodes.push({
|
||||
id: module.id,
|
||||
data: {
|
||||
@@ -114,6 +125,7 @@ export function graphBuilder(
|
||||
function addEdge(
|
||||
sourceId: string,
|
||||
targetId: string,
|
||||
prefix: string | undefined,
|
||||
options?: {
|
||||
disableInsert?: boolean
|
||||
customId?: string
|
||||
@@ -154,7 +166,7 @@ export function graphBuilder(
|
||||
// If the index is -1, it means that the target module is not in the modules array, so we set it to the length of the array
|
||||
index: index >= 0 ? index : mods?.length ?? 0,
|
||||
...extra,
|
||||
insertable: extra.insertable && !options?.disableInsert
|
||||
insertable: extra.insertable && !options?.disableInsert && prefix == undefined
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -188,11 +200,11 @@ export function graphBuilder(
|
||||
}
|
||||
nodes.push(triggerNode)
|
||||
if (preprocessorModule != null && preprocessorModule != undefined) {
|
||||
addEdge('Trigger', preprocessorModule.id, {
|
||||
addEdge('Trigger', preprocessorModule.id, undefined, {
|
||||
type: 'empty'
|
||||
})
|
||||
} else {
|
||||
addEdge('Trigger', 'Input', {
|
||||
addEdge('Trigger', 'Input', undefined, {
|
||||
type: 'empty'
|
||||
})
|
||||
}
|
||||
@@ -220,16 +232,25 @@ export function graphBuilder(
|
||||
beforeNode: Node,
|
||||
nextNode: Node | undefined,
|
||||
simplifiedTriggerView: boolean,
|
||||
prefix: string | undefined,
|
||||
currentOffset = 0,
|
||||
disableMoveIds: string[] = [],
|
||||
parentIndex?: string,
|
||||
branchChosen?: boolean
|
||||
) {
|
||||
if (prefix != undefined) {
|
||||
modules.forEach((m) => {
|
||||
if (!m['oid']) {
|
||||
m['oid'] = m.id
|
||||
}
|
||||
m.id = 'subflow:' + prefix + m['oid']
|
||||
})
|
||||
}
|
||||
let previousId: string | undefined = undefined
|
||||
|
||||
if (modules.length === 0) {
|
||||
if (nextNode) {
|
||||
addEdge(beforeNode.id, nextNode.id, {
|
||||
addEdge(beforeNode.id, nextNode.id, prefix, {
|
||||
subModules: modules,
|
||||
disableMoveIds
|
||||
})
|
||||
@@ -239,8 +260,8 @@ export function graphBuilder(
|
||||
const localDisableMoveIds = [...disableMoveIds, module.id]
|
||||
|
||||
// Add the edge between the previous node and the current one
|
||||
if (index > 0 && previousId) {
|
||||
addEdge(previousId, module.id, {
|
||||
if (index > 0 && previousId && expandedSubflows[module.id] == undefined) {
|
||||
addEdge(previousId, module.id, prefix, {
|
||||
subModules: modules,
|
||||
disableMoveIds
|
||||
})
|
||||
@@ -284,10 +305,10 @@ export function graphBuilder(
|
||||
|
||||
nodes.push(startNode)
|
||||
|
||||
addEdge(module.id, startNode.id, {
|
||||
addEdge(module.id, startNode.id, prefix, {
|
||||
type: 'empty'
|
||||
})
|
||||
addEdge(startNode.id, endNode.id, {
|
||||
addEdge(startNode.id, endNode.id, prefix, {
|
||||
type: 'empty'
|
||||
})
|
||||
} else {
|
||||
@@ -311,7 +332,7 @@ export function graphBuilder(
|
||||
|
||||
nodes.push(startNode)
|
||||
|
||||
addEdge(module.id, startNode.id, {
|
||||
addEdge(module.id, startNode.id, prefix, {
|
||||
type: 'empty'
|
||||
})
|
||||
|
||||
@@ -320,6 +341,7 @@ export function graphBuilder(
|
||||
startNode,
|
||||
endNode,
|
||||
false,
|
||||
prefix,
|
||||
currentOffset,
|
||||
localDisableMoveIds,
|
||||
parentIndex ? `${parentIndex}-${index}-${branchIndex}` : `${index}-${branchIndex}`
|
||||
@@ -349,11 +371,11 @@ export function graphBuilder(
|
||||
}
|
||||
|
||||
if (!simplifiedTriggerView) {
|
||||
addEdge(module.id, startNode.id, {
|
||||
addEdge(module.id, startNode.id, prefix, {
|
||||
type: 'empty'
|
||||
})
|
||||
} else if (previousId) {
|
||||
addEdge(previousId, startNode.id, {
|
||||
addEdge(previousId, startNode.id, prefix, {
|
||||
type: 'empty'
|
||||
})
|
||||
}
|
||||
@@ -383,6 +405,7 @@ export function graphBuilder(
|
||||
startNode,
|
||||
endNode,
|
||||
false,
|
||||
prefix,
|
||||
currentOffset + 25,
|
||||
localDisableMoveIds,
|
||||
parentIndex
|
||||
@@ -406,7 +429,7 @@ export function graphBuilder(
|
||||
position: { x: -1, y: -1 },
|
||||
type: 'whileLoopStart'
|
||||
}
|
||||
addEdge(module.id, startNode.id, {
|
||||
addEdge(module.id, startNode.id, prefix, {
|
||||
type: 'empty'
|
||||
})
|
||||
|
||||
@@ -427,6 +450,7 @@ export function graphBuilder(
|
||||
startNode,
|
||||
endNode,
|
||||
false,
|
||||
prefix,
|
||||
currentOffset + 25,
|
||||
localDisableMoveIds,
|
||||
parentIndex
|
||||
@@ -465,7 +489,7 @@ export function graphBuilder(
|
||||
|
||||
nodes.push(defaultBranch)
|
||||
|
||||
addEdge(module.id, defaultBranch.id, { type: 'empty' })
|
||||
addEdge(module.id, defaultBranch.id, prefix, { type: 'empty' })
|
||||
|
||||
const branchChosen = extra.flowModuleStates?.[module.id]?.branchChosen
|
||||
processModules(
|
||||
@@ -473,6 +497,7 @@ export function graphBuilder(
|
||||
defaultBranch,
|
||||
endNode,
|
||||
false,
|
||||
prefix,
|
||||
currentOffset,
|
||||
localDisableMoveIds,
|
||||
parentIndex ? `${parentIndex}-${index}` : index.toString(),
|
||||
@@ -500,13 +525,14 @@ export function graphBuilder(
|
||||
|
||||
nodes.push(startNode)
|
||||
|
||||
addEdge(module.id, startNode.id, { type: 'empty' })
|
||||
addEdge(module.id, startNode.id, prefix, { type: 'empty' })
|
||||
|
||||
processModules(
|
||||
branch.modules,
|
||||
startNode,
|
||||
endNode,
|
||||
false,
|
||||
prefix,
|
||||
currentOffset,
|
||||
localDisableMoveIds,
|
||||
parentIndex ? `${parentIndex}-${index}` : index.toString(),
|
||||
@@ -516,13 +542,72 @@ export function graphBuilder(
|
||||
|
||||
previousId = endNode.id
|
||||
} else {
|
||||
addNode(module, currentOffset, 'module', modules)
|
||||
let expanded = expandedSubflows[module.id]
|
||||
if (expanded) {
|
||||
const startId = `${module.id}-subflow-start`
|
||||
const idWithoutPrefix = module.id.startsWith('subflow:')
|
||||
? module.id.substring(8)
|
||||
: module.id
|
||||
const startNode = {
|
||||
id: startId,
|
||||
data: {
|
||||
offset: currentOffset,
|
||||
label: `Start of subflow ${idWithoutPrefix}`,
|
||||
id: startId,
|
||||
subflowId: module.id,
|
||||
modules: modules,
|
||||
eventHandlers: eventHandlers,
|
||||
...extra
|
||||
},
|
||||
position: { x: -1, y: -1 },
|
||||
type: 'subflowBound'
|
||||
}
|
||||
|
||||
previousId = module.id
|
||||
nodes.push(startNode)
|
||||
|
||||
if (previousId) {
|
||||
addEdge(previousId!, startNode.id, prefix, { type: 'empty' })
|
||||
} else {
|
||||
addEdge(beforeNode.id, startNode.id, prefix, { type: 'empty' })
|
||||
}
|
||||
|
||||
const endId = `${module.id}-subflow-end`
|
||||
const endNode = {
|
||||
id: endId,
|
||||
data: {
|
||||
offset: currentOffset,
|
||||
label: `End of subflow ${idWithoutPrefix}`,
|
||||
id: endId,
|
||||
subflowId: module.id,
|
||||
modules: modules,
|
||||
eventHandlers: eventHandlers,
|
||||
...extra
|
||||
},
|
||||
position: { x: -1, y: -1 },
|
||||
type: 'subflowBound'
|
||||
}
|
||||
|
||||
nodes.push(endNode)
|
||||
|
||||
processModules(
|
||||
expanded,
|
||||
startNode,
|
||||
endNode,
|
||||
false,
|
||||
buildPrefix(prefix, module['oid'] ?? module.id),
|
||||
currentOffset,
|
||||
localDisableMoveIds
|
||||
)
|
||||
|
||||
previousId = endNode.id
|
||||
} else {
|
||||
addNode(module, currentOffset, 'module', modules)
|
||||
previousId = module.id
|
||||
}
|
||||
}
|
||||
|
||||
if (index === 0) {
|
||||
addEdge(beforeNode.id, module.id, {
|
||||
if (index === 0 && expandedSubflows[module.id] == undefined) {
|
||||
addEdge(beforeNode.id, module.id, prefix, {
|
||||
subModules: modules,
|
||||
disableMoveIds,
|
||||
disableInsert: simplifiedTriggerView
|
||||
@@ -530,7 +615,7 @@ export function graphBuilder(
|
||||
}
|
||||
|
||||
if (index === modules.length - 1 && previousId && nextNode) {
|
||||
addEdge(previousId, nextNode.id, {
|
||||
addEdge(previousId, nextNode.id, prefix, {
|
||||
subModules: modules,
|
||||
disableMoveIds
|
||||
})
|
||||
@@ -540,9 +625,9 @@ export function graphBuilder(
|
||||
}
|
||||
|
||||
if (simplifiableFlow?.simplifiedFlow === true && triggerNode) {
|
||||
processModules(modules, triggerNode, undefined, true)
|
||||
processModules(modules, triggerNode, undefined, true, undefined)
|
||||
} else {
|
||||
processModules(modules, inputNode, resultNode, false)
|
||||
processModules(modules, inputNode, resultNode, false, undefined)
|
||||
}
|
||||
|
||||
if (failureModule) {
|
||||
@@ -558,14 +643,14 @@ export function graphBuilder(
|
||||
|
||||
Object.entries(toAdd).forEach((x) => {
|
||||
addNode({ ...failureModule, id: x[1] }, 0, 'module')
|
||||
addEdge(x[0], x[1], { type: 'empty' })
|
||||
addEdge(x[0], x[1], undefined, { type: 'empty' })
|
||||
})
|
||||
}
|
||||
|
||||
if (preprocessorModule) {
|
||||
addNode(preprocessorModule, 0, 'module')
|
||||
const id = JSON.parse(JSON.stringify(preprocessorModule.id))
|
||||
addEdge(id, 'Input', { type: 'empty' })
|
||||
addEdge(id, 'Input', undefined, { type: 'empty' })
|
||||
}
|
||||
|
||||
if (failureModule && !extra.flowModuleStates) {
|
||||
@@ -598,7 +683,7 @@ export function graphBuilder(
|
||||
}
|
||||
}
|
||||
|
||||
addEdge(pid, selectedId!, {
|
||||
addEdge(pid, selectedId!, undefined, {
|
||||
customId: `dep-${pid}-${selectedId}-${input}-${index}`,
|
||||
type: 'dataflowedge'
|
||||
})
|
||||
@@ -608,7 +693,7 @@ export function graphBuilder(
|
||||
Object.entries(deps.dependents).forEach((x, i) => {
|
||||
let pid = x[0]
|
||||
|
||||
addEdge(selectedId!, pid, {
|
||||
addEdge(selectedId!, pid, undefined, {
|
||||
customId: `dep-${selectedId}-${pid}-${i}`,
|
||||
type: 'dataflowedge'
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import MapItem from '$lib/components/flows/map/MapItem.svelte'
|
||||
import type { FlowModule, FlowModuleValue } from '$lib/gen'
|
||||
import { GitBranchPlus } from 'lucide-svelte'
|
||||
import { GitBranchPlus, Maximize2 } from 'lucide-svelte'
|
||||
import NodeWrapper from './NodeWrapper.svelte'
|
||||
import type { GraphEventHandlers } from '../../graphBuilder'
|
||||
import type { GraphModuleState } from '../../model'
|
||||
@@ -46,6 +46,19 @@
|
||||
</script>
|
||||
|
||||
<NodeWrapper offset={data.offset} let:darkMode>
|
||||
{#if data.module.value.type == 'flow'}
|
||||
<button
|
||||
title="Unexpand subflow"
|
||||
class="z-50 absolute -top-[10px] right-[25px] rounded-full h-[20px] w-[20px] center-center text-primary bg-surface duration-150 hover:bg-surface-hover"
|
||||
on:click|preventDefault|stopPropagation={() => {
|
||||
if (data.module.value.type == 'flow') {
|
||||
data.eventHandlers.expandSubflow(data.module.id, data.module.value.path)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Maximize2 size={12} />
|
||||
</button>
|
||||
{/if}
|
||||
<MapItem
|
||||
mod={data.module}
|
||||
insertable={data.insertable}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<script lang="ts">
|
||||
import VirtualItem from '$lib/components/flows/map/VirtualItem.svelte'
|
||||
import NodeWrapper from './NodeWrapper.svelte'
|
||||
import { Minimize2 } from 'lucide-svelte'
|
||||
import type { GraphModuleState } from '../../model'
|
||||
import { getStateColor } from '../../util'
|
||||
import type { FlowModule } from '$lib/gen'
|
||||
import type { GraphEventHandlers } from '../../graphBuilder'
|
||||
|
||||
export let data: {
|
||||
label: string
|
||||
preLabel: string | undefined
|
||||
insertable: boolean
|
||||
flowModuleStates: Record<string, GraphModuleState> | undefined
|
||||
subflowId: string
|
||||
id: string
|
||||
modules: FlowModule[]
|
||||
selected: boolean
|
||||
eventHandlers: GraphEventHandlers
|
||||
offset: number
|
||||
}
|
||||
</script>
|
||||
|
||||
<NodeWrapper let:darkMode offset={data.offset}>
|
||||
<VirtualItem
|
||||
label={data.label}
|
||||
preLabel={data.preLabel}
|
||||
selectable
|
||||
selected={data.selected}
|
||||
bgColor={getStateColor(undefined, darkMode)}
|
||||
borderColor={undefined}
|
||||
on:select={() => {
|
||||
data.eventHandlers.select(data.id)
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
title="Unexpand subflow"
|
||||
class="z-50 absolute -top-[10px] right-[25px] rounded-full h-[20px] w-[20px] center-center text-primary bg-surface duration-150 hover:bg-surface-hover"
|
||||
on:click|preventDefault|stopPropagation={() => {
|
||||
data.eventHandlers.minimizeSubflow(data.subflowId)
|
||||
}}
|
||||
>
|
||||
<Minimize2 size={12} />
|
||||
</button>
|
||||
</NodeWrapper>
|
||||
@@ -284,6 +284,7 @@
|
||||
<h2 class="mt-10">Flow details</h2>
|
||||
<div class="border border-gray-700">
|
||||
<FlowGraphV2
|
||||
workspace={job.workspace_id}
|
||||
triggerNode={false}
|
||||
modules={job.raw_flow?.modules}
|
||||
failureModule={job.raw_flow?.failure_module}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import FlowGraphV2 from '$lib/components/graph/FlowGraphV2.svelte'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { decodeState } from '$lib/utils'
|
||||
|
||||
let content = localStorage.getItem('svelvet')
|
||||
@@ -8,7 +9,13 @@
|
||||
: { modules: [], failureModule: undefined, preprocessorModule: undefined }
|
||||
</script>
|
||||
|
||||
<FlowGraphV2 triggerNode={false} {modules} {failureModule} {preprocessorModule} />
|
||||
<FlowGraphV2
|
||||
workspace={$workspaceStore}
|
||||
triggerNode={false}
|
||||
{modules}
|
||||
{failureModule}
|
||||
{preprocessorModule}
|
||||
/>
|
||||
<a
|
||||
download="flow.json"
|
||||
href={'data:text/json;charset=utf-8,' +
|
||||
|
||||
Reference in New Issue
Block a user