chore(docs): simplify animated layout example (#1747)
* chore(examples): update ts example of layouting Signed-off-by: braks <78412429+bcakmakoglu@users.noreply.github.com> * chore(docs): cleanup animated layout example Signed-off-by: braks <78412429+bcakmakoglu@users.noreply.github.com> --------- Signed-off-by: braks <78412429+bcakmakoglu@users.noreply.github.com>
This commit is contained in:
@@ -1,6 +1,14 @@
|
||||
import { ref, toRef, toValue } from 'vue'
|
||||
import { useVueFlow } from '@vue-flow/core'
|
||||
|
||||
export const ProcessStatus = {
|
||||
ERROR: 'error',
|
||||
SKIPPED: 'skipped',
|
||||
CANCELLED: 'cancelled',
|
||||
FINISHED: 'finished',
|
||||
RUNNING: 'running',
|
||||
}
|
||||
|
||||
/**
|
||||
* Composable to simulate running a process tree.
|
||||
*
|
||||
@@ -8,6 +16,10 @@ import { useVueFlow } from '@vue-flow/core'
|
||||
* When one node finishes, the next one starts.
|
||||
*
|
||||
* When a node has multiple descendants, it will run them in parallel.
|
||||
*
|
||||
* @param options
|
||||
* @param options.graph The graph object containing the nodes and edges.
|
||||
* @param options.cancelOnError Whether to cancel the process if an error occurs.
|
||||
*/
|
||||
export function useRunProcess({ graph: dagreGraph, cancelOnError = true }) {
|
||||
const { updateNodeData, getConnectedEdges } = useVueFlow()
|
||||
@@ -16,24 +28,35 @@ export function useRunProcess({ graph: dagreGraph, cancelOnError = true }) {
|
||||
|
||||
const isRunning = ref(false)
|
||||
|
||||
const executedNodes = new Set()
|
||||
|
||||
/** Map of running tasks with the node ID as the key and the timeout as the value. */
|
||||
const runningTasks = new Map()
|
||||
|
||||
/** Set of node ids of nodes that have been executed */
|
||||
const executedNodes = new Set()
|
||||
|
||||
/** Set of node ids yet to be executed */
|
||||
const upcomingTasks = new Set()
|
||||
|
||||
async function runNode(node, isStart = false) {
|
||||
if (executedNodes.has(node.id)) {
|
||||
/**
|
||||
* Run the process on a node.
|
||||
* It will mark the node as running, simulate an async process, and then mark the node as finished or errored.
|
||||
*
|
||||
* @param nodeId The ID of the node to run.
|
||||
* @param isStart Whether this is a starting node.
|
||||
*/
|
||||
async function runNode(nodeId, isStart = false) {
|
||||
if (executedNodes.has(nodeId)) {
|
||||
return
|
||||
}
|
||||
|
||||
// save the upcoming task in case it gets cancelled before we even start it
|
||||
upcomingTasks.add(node.id)
|
||||
upcomingTasks.add(nodeId)
|
||||
|
||||
const incomers = getConnectedEdges(node.id).filter((connection) => connection.target === node.id)
|
||||
// get all incoming edges to this node
|
||||
const incomers = getConnectedEdges(nodeId).filter((connection) => connection.target === nodeId)
|
||||
|
||||
// wait for edge animations to finish before starting the process
|
||||
await Promise.all(incomers.map((incomer) => until(() => !incomer.data.isAnimating)))
|
||||
await Promise.all(incomers.map((incomer) => until(() => !incomer.data?.isAnimating)))
|
||||
|
||||
// remove the upcoming task since we are about to start it
|
||||
upcomingTasks.clear()
|
||||
@@ -44,9 +67,9 @@ export function useRunProcess({ graph: dagreGraph, cancelOnError = true }) {
|
||||
}
|
||||
|
||||
// mark the node as executed, so it doesn't run again
|
||||
executedNodes.add(node.id)
|
||||
executedNodes.add(nodeId)
|
||||
|
||||
updateNodeData(node.id, { isRunning: true, isFinished: false, hasError: false, isCancelled: false })
|
||||
updateNodeStatus(nodeId, ProcessStatus.RUNNING)
|
||||
|
||||
// simulate an async process with a random timeout between 1-2 seconds
|
||||
const delay = Math.floor(Math.random() * 2000) + 1000
|
||||
@@ -54,49 +77,63 @@ export function useRunProcess({ graph: dagreGraph, cancelOnError = true }) {
|
||||
return new Promise((resolve) => {
|
||||
const timeout = setTimeout(
|
||||
async () => {
|
||||
const children = graph.value.successors(node.id)
|
||||
// get all children of this node
|
||||
const children = graph.value.successors(nodeId) || []
|
||||
|
||||
// randomly decide whether the node will throw an error
|
||||
const willThrowError = Math.random() < 0.15
|
||||
|
||||
// we avoid throwing an error on the starting node
|
||||
if (!isStart && willThrowError) {
|
||||
updateNodeData(node.id, { isRunning: false, hasError: true })
|
||||
updateNodeStatus(nodeId, ProcessStatus.ERROR)
|
||||
|
||||
// if cancelOnError is true, we stop the process and mark all descendants as skipped
|
||||
if (toValue(cancelOnError)) {
|
||||
await skipDescendants(node.id)
|
||||
runningTasks.delete(node.id)
|
||||
await skipDescendants(nodeId)
|
||||
runningTasks.delete(nodeId)
|
||||
|
||||
resolve()
|
||||
resolve(true)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
updateNodeData(node.id, { isRunning: false, isFinished: true })
|
||||
updateNodeStatus(nodeId, ProcessStatus.FINISHED)
|
||||
|
||||
runningTasks.delete(node.id)
|
||||
runningTasks.delete(nodeId)
|
||||
|
||||
if (children.length > 0) {
|
||||
// run the process on the children in parallel
|
||||
await Promise.all(children.map((id) => runNode({ id })))
|
||||
await Promise.all(children.map((child) => runNode(child)))
|
||||
}
|
||||
|
||||
resolve()
|
||||
resolve(true)
|
||||
},
|
||||
// if this is a starting node, we don't want to wait
|
||||
isStart ? 0 : delay,
|
||||
)
|
||||
|
||||
// save the timeout so we can cancel it if needed
|
||||
runningTasks.set(node.id, timeout)
|
||||
runningTasks.set(nodeId, timeout)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a sequence of nodes.
|
||||
* It will start with the nodes that have no predecessors and then run the process on each node in sequence.
|
||||
* If a node has multiple descendants, it will run them in parallel.
|
||||
* If an error occurs, it will stop the process and mark all descendants as skipped.
|
||||
* If cancelOnError is true, it will stop the process if an error occurs.
|
||||
* If the process is stopped, it will mark all running nodes as cancelled.
|
||||
*
|
||||
* @param nodes The nodes to run.
|
||||
*/
|
||||
async function run(nodes) {
|
||||
// if the process is already running, we don't want to start it again
|
||||
if (isRunning.value) {
|
||||
return
|
||||
}
|
||||
|
||||
// reset all nodes to their initial state
|
||||
reset(nodes)
|
||||
|
||||
isRunning.value = true
|
||||
@@ -105,42 +142,57 @@ export function useRunProcess({ graph: dagreGraph, cancelOnError = true }) {
|
||||
const startingNodes = nodes.filter((node) => graph.value.predecessors(node.id)?.length === 0)
|
||||
|
||||
// run the process on all starting nodes in parallel
|
||||
await Promise.all(startingNodes.map((node) => runNode(node, true)))
|
||||
await Promise.all(startingNodes.map((node) => runNode(node.id, true)))
|
||||
|
||||
clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset all nodes to their initial state.
|
||||
*
|
||||
* @param nodes The nodes to reset.
|
||||
*/
|
||||
function reset(nodes) {
|
||||
clear()
|
||||
|
||||
for (const node of nodes) {
|
||||
updateNodeData(node.id, { isRunning: false, isFinished: false, hasError: false, isSkipped: false, isCancelled: false })
|
||||
updateNodeStatus(node.id, null)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Skip all descendants of a node.
|
||||
*
|
||||
* @param nodeId The ID of the node to skip descendants for.
|
||||
*/
|
||||
async function skipDescendants(nodeId) {
|
||||
const children = graph.value.successors(nodeId)
|
||||
const children = graph.value.successors(nodeId) || []
|
||||
|
||||
for (const child of children) {
|
||||
updateNodeData(child, { isRunning: false, isSkipped: true })
|
||||
updateNodeStatus(child, ProcessStatus.SKIPPED)
|
||||
await skipDescendants(child)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the process.
|
||||
*
|
||||
* It will mark all running nodes as cancelled and skip all upcoming tasks.
|
||||
*/
|
||||
async function stop() {
|
||||
isRunning.value = false
|
||||
|
||||
for (const nodeId of upcomingTasks) {
|
||||
clearTimeout(runningTasks.get(nodeId))
|
||||
runningTasks.delete(nodeId)
|
||||
updateNodeData(nodeId, { isRunning: false, isFinished: false, hasError: false, isSkipped: false, isCancelled: true })
|
||||
updateNodeStatus(nodeId, ProcessStatus.CANCELLED)
|
||||
await skipDescendants(nodeId)
|
||||
}
|
||||
|
||||
for (const [nodeId, task] of runningTasks) {
|
||||
clearTimeout(task)
|
||||
runningTasks.delete(nodeId)
|
||||
updateNodeData(nodeId, { isRunning: false, isFinished: false, hasError: false, isSkipped: false, isCancelled: true })
|
||||
updateNodeStatus(nodeId, ProcessStatus.CANCELLED)
|
||||
await skipDescendants(nodeId)
|
||||
}
|
||||
|
||||
@@ -148,12 +200,25 @@ export function useRunProcess({ graph: dagreGraph, cancelOnError = true }) {
|
||||
upcomingTasks.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all running tasks and executed nodes.
|
||||
*/
|
||||
function clear() {
|
||||
isRunning.value = false
|
||||
executedNodes.clear()
|
||||
runningTasks.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the status of a node.
|
||||
*
|
||||
* @param nodeId The ID of the node to update.
|
||||
* @param status The new status of the node.
|
||||
*/
|
||||
function updateNodeStatus(nodeId, status) {
|
||||
updateNodeData(nodeId, { status })
|
||||
}
|
||||
|
||||
return { run, stop, reset, isRunning }
|
||||
}
|
||||
|
||||
@@ -162,7 +227,7 @@ async function until(condition) {
|
||||
const interval = setInterval(() => {
|
||||
if (condition()) {
|
||||
clearInterval(interval)
|
||||
resolve()
|
||||
resolve(true)
|
||||
}
|
||||
}, 100)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user