Skip to content

Commit 7bd2996

Browse files
committed
docs: add run process to layouting example
1 parent 0ae7502 commit 7bd2996

File tree

6 files changed

+213
-24
lines changed

6 files changed

+213
-24
lines changed

docs/examples/index.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { IntersectionApp, IntersectionCSS } from './intersection'
1818
import { SnapToHandleApp, SnappableConnectionLine } from './connection-radius'
1919
import { NodeResizerApp, ResizableNode } from './node-resizer'
2020
import { ToolbarApp, ToolbarNode } from './node-toolbar'
21-
import { LayoutApp, LayoutElements } from './layout'
21+
import { LayoutApp, LayoutElements, LayoutNode, LayoutScript } from './layout'
2222

2323
export const exampleImports = {
2424
basic: {
@@ -127,6 +127,8 @@ export const exampleImports = {
127127
layout: {
128128
'App.vue': LayoutApp,
129129
'initial-elements.js': LayoutElements,
130+
'ProcessNode.vue': LayoutNode,
131+
'useRunProcess.js': LayoutScript,
130132
'additionalImports': {
131133
dagre: 'https://cdn.skypack.dev/pin/[email protected]/mode=imports,min/optimized/dagre.js',
132134
},

docs/examples/layout/App.vue

+23-9
Original file line numberDiff line numberDiff line change
@@ -3,40 +3,48 @@ import dagre from 'dagre'
33
import { nextTick, ref } from 'vue'
44
import { Panel, Position, VueFlow, useVueFlow } from '@vue-flow/core'
55
import { Background } from '@vue-flow/background'
6+
import ProcessNode from './ProcessNode.vue'
67
78
import { initialEdges, initialNodes } from './initial-elements.js'
9+
import { useRunProcess } from './useRunProcess'
810
911
const nodes = ref(initialNodes)
1012
1113
const edges = ref(initialEdges)
1214
15+
const dagreGraph = ref(new dagre.graphlib.Graph())
16+
17+
dagreGraph.value.setDefaultEdgeLabel(() => ({}))
18+
19+
const { run } = useRunProcess()
20+
1321
const { findNode, fitView } = useVueFlow()
1422
1523
function handleLayout(direction) {
1624
// we create a new graph instance, in case some nodes/edges were removed, otherwise dagre would act as if they were still there
17-
const dagreGraph = new dagre.graphlib.Graph()
25+
dagreGraph.value = new dagre.graphlib.Graph()
1826
19-
dagreGraph.setDefaultEdgeLabel(() => ({}))
27+
dagreGraph.value.setDefaultEdgeLabel(() => ({}))
2028
2129
const isHorizontal = direction === 'LR'
22-
dagreGraph.setGraph({ rankdir: direction })
30+
dagreGraph.value.setGraph({ rankdir: direction })
2331
2432
for (const node of nodes.value) {
2533
// if you need width+height of nodes for your layout, you can use the dimensions property of the internal node (`GraphNode` type)
2634
const graphNode = findNode(node.id)
2735
28-
dagreGraph.setNode(node.id, { width: graphNode.dimensions.width || 150, height: graphNode.dimensions.height || 50 })
36+
dagreGraph.value.setNode(node.id, { width: graphNode.dimensions.width || 150, height: graphNode.dimensions.height || 50 })
2937
}
3038
3139
for (const edge of edges.value) {
32-
dagreGraph.setEdge(edge.source, edge.target)
40+
dagreGraph.value.setEdge(edge.source, edge.target)
3341
}
3442
35-
dagre.layout(dagreGraph)
43+
dagre.layout(dagreGraph.value)
3644
3745
// set nodes with updated positions
3846
nodes.value = nodes.value.map((node) => {
39-
const nodeWithPosition = dagreGraph.node(node.id)
47+
const nodeWithPosition = dagreGraph.value.node(node.id)
4048
4149
return {
4250
...node,
@@ -55,11 +63,17 @@ function handleLayout(direction) {
5563
<template>
5664
<div class="layoutflow">
5765
<VueFlow :nodes="nodes" :edges="edges" @nodes-initialized="handleLayout('TB')">
66+
<template #node-process="props">
67+
<ProcessNode v-bind="props" />
68+
</template>
69+
5870
<Background />
5971

6072
<Panel style="display: flex; gap: 1rem" position="top-right">
61-
<button @click="handleLayout('TB')">vertical layout</button>
62-
<button @click="handleLayout('LR')">horizontal layout</button>
73+
<button @click="handleLayout('TB')">vertical</button>
74+
<button @click="handleLayout('LR')">horizontal</button>
75+
76+
<button @click="run(nodes, dagreGraph)">Run</button>
6377
</Panel>
6478
</VueFlow>
6579
</div>

docs/examples/layout/ProcessNode.vue

+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<script setup>
2+
import { toRef } from 'vue'
3+
import { Handle } from '@vue-flow/core'
4+
5+
const props = defineProps({
6+
data: {
7+
type: Object,
8+
required: true,
9+
},
10+
sourcePosition: {
11+
type: String,
12+
},
13+
targetPosition: {
14+
type: String,
15+
},
16+
})
17+
18+
const bgColor = toRef(() => {
19+
if (props.data.hasError) {
20+
return '#f87171'
21+
}
22+
23+
if (props.data.isFinished) {
24+
return '#10b981'
25+
}
26+
27+
if (props.data.isRunning || props.data.isSkipped) {
28+
// pick me a lighter color please
29+
return '#6b7280'
30+
}
31+
32+
return '#1a192b'
33+
})
34+
</script>
35+
36+
<template>
37+
<div class="process-node" :style="{ backgroundColor: bgColor }">
38+
<Handle type="target" :position="targetPosition" />
39+
<Handle type="source" :position="sourcePosition" />
40+
41+
<div style="display: flex; align-items: center; gap: 8px">
42+
<div v-if="data.isRunning" class="spinner" />
43+
<span v-else-if="data.hasError">&#x274C;</span>
44+
<span v-else-if="data.isSkipped">&#x1F6A7;</span>
45+
<span v-else>&#x1F4E6;</span>
46+
</div>
47+
</div>
48+
</template>
49+
50+
<style scoped>
51+
.process-node {
52+
padding: 10px;
53+
color: white;
54+
border: 1px solid #1a192b;
55+
border-radius: 99px;
56+
font-size: 10px;
57+
width: 15px;
58+
height: 15px;
59+
display: flex;
60+
align-items: center;
61+
justify-content: center;
62+
}
63+
64+
.spinner {
65+
border: 2px solid #f3f3f3;
66+
border-top: 2px solid #3498db;
67+
border-radius: 50%;
68+
width: 8px;
69+
height: 8px;
70+
animation: spin 1s linear infinite;
71+
}
72+
73+
@keyframes spin {
74+
0% {
75+
transform: rotate(0deg);
76+
}
77+
100% {
78+
transform: rotate(360deg);
79+
}
80+
}
81+
</style>

docs/examples/layout/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
export { default as LayoutApp } from './App.vue?raw'
22
export { default as LayoutElements } from './initial-elements.js?raw'
3+
export { default as LayoutNode } from './ProcessNode.vue?raw'
4+
export { default as LayoutScript } from './useRunProcess.js?raw'

docs/examples/layout/initial-elements.js

+16-14
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,63 @@
11
const position = { x: 0, y: 0 }
2+
const type = 'process'
23

34
export const initialNodes = [
45
{
56
id: '1',
6-
type: 'input',
77
label: 'Start',
88
position,
9+
type,
910
},
1011
{
1112
id: '2',
12-
label: 'Process 1',
1313
position,
14+
type,
1415
},
1516
{
1617
id: '2a',
17-
label: 'Process 2a',
1818
position,
19+
type,
1920
},
2021
{
2122
id: '2b',
22-
type: 'output',
23-
label: 'Process 2b',
2423
position,
24+
type,
2525
},
2626
{
2727
id: '2c',
28-
label: 'Process 2c',
2928
position,
29+
type,
3030
},
3131
{
3232
id: '2d',
33-
label: 'Process 2d',
3433
position,
34+
type,
3535
},
3636
{
3737
id: '3',
38-
label: 'Process 3',
3938
position,
39+
type,
4040
},
4141
{
4242
id: '4',
43-
type: 'input',
44-
label: 'Start',
4543
position,
44+
type,
4645
},
4746
{
4847
id: '5',
49-
label: 'Process 5',
5048
position,
49+
type,
5150
},
5251
{
5352
id: '6',
54-
type: 'output',
55-
label: 'Process 6',
5653
position,
54+
type,
55+
},
56+
{
57+
id: '7',
58+
position,
59+
type,
5760
},
58-
{ id: '7', label: 'Process 7', position },
5961
]
6062

6163
export const initialEdges = [

docs/examples/layout/useRunProcess.js

+88
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { ref } from 'vue'
2+
import { useVueFlow } from '@vue-flow/core'
3+
4+
/**
5+
* Composable to simulate running a process tree.
6+
*
7+
* It loops through each node, pretends to run an async process, and updates the node's data indicating whether the process has finished.
8+
* When one node finishes, the next one starts.
9+
*
10+
* When a node has multiple descendants, it will run them in parallel.
11+
*/
12+
export function useRunProcess() {
13+
const { updateNodeData } = useVueFlow()
14+
15+
const running = ref(false)
16+
const executedNodes = new Set()
17+
18+
async function runNode(node, dagreGraph) {
19+
if (executedNodes.has(node.id)) {
20+
return
21+
}
22+
23+
executedNodes.add(node.id)
24+
25+
updateNodeData(node.id, { isRunning: true, isFinished: false, hasError: false })
26+
27+
// Simulate an async process with a random timeout between 1 and 3 seconds
28+
const delay = Math.floor(Math.random() * 2000) + 1000
29+
await new Promise((resolve) => setTimeout(resolve, delay))
30+
31+
const children = dagreGraph.successors(node.id)
32+
33+
// Randomly decide whether the node will throw an error
34+
const willThrowError = Math.random() < 0.15
35+
36+
if (willThrowError) {
37+
updateNodeData(node.id, { isRunning: false, hasError: true })
38+
39+
await skipDescendants(node.id, dagreGraph)
40+
return
41+
}
42+
43+
updateNodeData(node.id, { isRunning: false, isFinished: true })
44+
45+
// Run the process on the children in parallel
46+
await Promise.all(
47+
children.map((id) => {
48+
return runNode({ id }, dagreGraph)
49+
}),
50+
)
51+
}
52+
53+
async function run(nodes, dagreGraph) {
54+
if (running.value) {
55+
return
56+
}
57+
58+
reset(nodes)
59+
60+
running.value = true
61+
62+
// Get all starting nodes (nodes with no predecessors)
63+
const startingNodes = nodes.filter((node) => dagreGraph.predecessors(node.id)?.length === 0)
64+
65+
// Run the process on all starting nodes in parallel
66+
await Promise.all(startingNodes.map((node) => runNode(node, dagreGraph)))
67+
68+
running.value = false
69+
executedNodes.clear()
70+
}
71+
72+
function reset(nodes) {
73+
for (const node of nodes) {
74+
updateNodeData(node.id, { isRunning: false, isFinished: false, hasError: false, isSkipped: false })
75+
}
76+
}
77+
78+
async function skipDescendants(nodeId, dagreGraph) {
79+
const children = dagreGraph.successors(nodeId)
80+
81+
for (const child of children) {
82+
updateNodeData(child, { isRunning: false, isSkipped: true })
83+
await skipDescendants(child, dagreGraph)
84+
}
85+
}
86+
87+
return { run, running }
88+
}

0 commit comments

Comments
 (0)