@kestra-io/ui-libs 0.0.222 → 0.0.224

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/{FlowYamlUtils-B2iMnYvW.js → FlowYamlUtils-BnXiRqSX.js} +44 -44
  2. package/dist/{FlowYamlUtils-B2iMnYvW.js.map → FlowYamlUtils-BnXiRqSX.js.map} +1 -1
  3. package/dist/VueFlowUtils-DKrM_RaI.js +4513 -0
  4. package/dist/VueFlowUtils-DKrM_RaI.js.map +1 -0
  5. package/dist/VueFlowUtils-Dh7ybprm.cjs +2 -0
  6. package/dist/VueFlowUtils-Dh7ybprm.cjs.map +1 -0
  7. package/dist/components/nodes/BasicNode.vue.d.ts +1 -0
  8. package/dist/components/nodes/BasicNode.vue.d.ts.map +1 -1
  9. package/dist/components/nodes/CollapsedClusterNode.vue.d.ts +1 -0
  10. package/dist/components/nodes/CollapsedClusterNode.vue.d.ts.map +1 -1
  11. package/dist/components/nodes/EdgeNode.vue.d.ts +1 -0
  12. package/dist/components/nodes/TaskNode.vue.d.ts +1 -0
  13. package/dist/components/nodes/TaskNode.vue.d.ts.map +1 -1
  14. package/dist/components/nodes/TriggerNode.vue.d.ts +1 -0
  15. package/dist/components/nodes/TriggerNode.vue.d.ts.map +1 -1
  16. package/dist/components/topology/Topology.vue.d.ts +15 -4
  17. package/dist/components/topology/Topology.vue.d.ts.map +1 -1
  18. package/dist/components/topology/injectionKeys.d.ts +1 -0
  19. package/dist/components/topology/injectionKeys.d.ts.map +1 -1
  20. package/dist/kestra-flowyamlutils.es.js +6 -6
  21. package/dist/kestra-index.cjs.js +16 -16
  22. package/dist/kestra-index.cjs.js.map +1 -1
  23. package/dist/kestra-index.es.js +3940 -8270
  24. package/dist/kestra-index.es.js.map +1 -1
  25. package/dist/kestra-vueflowutils.cjs.js +2 -0
  26. package/dist/kestra-vueflowutils.cjs.js.map +1 -0
  27. package/dist/kestra-vueflowutils.es.js +33 -0
  28. package/dist/kestra-vueflowutils.es.js.map +1 -0
  29. package/dist/ui-libs.css +1 -1
  30. package/dist/utils/VueFlowUtils.d.ts +102 -49
  31. package/dist/utils/VueFlowUtils.d.ts.map +1 -1
  32. package/dist/utils/constants.d.ts +1 -0
  33. package/dist/utils/constants.d.ts.map +1 -1
  34. package/package.json +9 -1
  35. package/src/components/misc/ExecutionInformations.vue +2 -2
  36. package/src/components/nodes/TaskNode.vue +33 -1
  37. package/src/components/topology/Topology.vue +11 -3
  38. package/src/components/topology/injectionKeys.ts +2 -1
  39. package/src/scss/vue-material-design-icon.scss +26 -0
  40. package/src/utils/VueFlowUtils.test.ts +86 -0
  41. package/src/utils/VueFlowUtils.ts +566 -424
  42. package/src/utils/constants.ts +1 -0
@@ -2,7 +2,7 @@ import {GraphNode, GraphEdge, MarkerType, Position, useVueFlow, Elements} from "
2
2
  import dagre from "dagre";
3
3
  import Utils from "./Utils";
4
4
  import {CLUSTER_PREFIX, NODE_SIZES} from "./constants";
5
- import {flowHaveTasks} from "./FlowYamlUtils";
5
+ import {flowHaveTasks as yamlFlowHaveTask} from "./FlowYamlUtils";
6
6
 
7
7
  const TRIGGERS_NODE_UID = "root.Triggers";
8
8
 
@@ -19,6 +19,7 @@ interface MinimalNode {
19
19
  uid: string;
20
20
  type: string;
21
21
  task?: {
22
+ id?: string;
22
23
  type: string;
23
24
  namespace: string;
24
25
  flowId: string;
@@ -54,90 +55,90 @@ export interface FlowGraph {
54
55
 
55
56
  type EdgeReplacer = Record<string, string>
56
57
 
57
- export default {
58
- predecessorsEdge(vueFlowId:string, nodeUid: string): GraphEdge[] {
58
+
59
+ export function predecessorsEdge(vueFlowId: string, nodeUid: string): GraphEdge[] {
59
60
  const {getEdges} = useVueFlow(vueFlowId);
60
61
 
61
62
  const nodes = [];
62
63
 
63
64
  for (const edge of getEdges.value) {
64
- if (edge.target === nodeUid) {
65
- nodes.push(edge);
66
- const recursiveEdge = this.predecessorsEdge(vueFlowId, edge.source);
67
- if (recursiveEdge.length > 0) {
68
- nodes.push(...recursiveEdge);
65
+ if (edge.target === nodeUid) {
66
+ nodes.push(edge);
67
+ const recursiveEdge = predecessorsEdge(vueFlowId, edge.source);
68
+ if (recursiveEdge.length > 0) {
69
+ nodes.push(...recursiveEdge);
70
+ }
69
71
  }
70
- }
71
72
  }
72
73
 
73
74
  return nodes;
74
- },
75
+ }
75
76
 
76
- successorsEdge(vueFlowId:string, nodeUid:string):GraphEdge[] {
77
+ export function successorsEdge(vueFlowId: string, nodeUid: string): GraphEdge[] {
77
78
  const {getEdges} = useVueFlow(vueFlowId);
78
79
 
79
80
  const nodes = [];
80
81
 
81
82
  for (const edge of getEdges.value) {
82
- if (edge.source === nodeUid) {
83
- nodes.push(edge);
84
- const recursiveEdge = this.successorsEdge(vueFlowId, edge.target);
85
- if (recursiveEdge.length > 0) {
86
- nodes.push(...recursiveEdge);
83
+ if (edge.source === nodeUid) {
84
+ nodes.push(edge);
85
+ const recursiveEdge = successorsEdge(vueFlowId, edge.target);
86
+ if (recursiveEdge.length > 0) {
87
+ nodes.push(...recursiveEdge);
88
+ }
87
89
  }
88
- }
89
90
  }
90
91
 
91
92
  return nodes;
92
- },
93
+ }
93
94
 
94
- predecessorsNode(vueFlowId:string, nodeUid:string): (GraphEdge | GraphNode)[] {
95
+ export function predecessorsNode(vueFlowId: string, nodeUid: string): (GraphEdge | GraphNode)[] {
95
96
  const {getEdges, findNode} = useVueFlow(vueFlowId);
96
97
 
97
98
  const foundNode = findNode(nodeUid)
98
99
  const nodes: (GraphEdge | GraphNode)[] = foundNode ? [foundNode] : [];
99
100
 
100
101
  for (const edge of getEdges.value) {
101
- if (edge.target === nodeUid) {
102
- nodes.push(edge.sourceNode);
103
- const recursiveEdge = this.predecessorsNode(vueFlowId, edge.source);
104
- if (recursiveEdge.length > 0) {
105
- nodes.push(...recursiveEdge);
102
+ if (edge.target === nodeUid) {
103
+ nodes.push(edge.sourceNode);
104
+ const recursiveEdge = predecessorsNode(vueFlowId, edge.source);
105
+ if (recursiveEdge.length > 0) {
106
+ nodes.push(...recursiveEdge);
107
+ }
106
108
  }
107
- }
108
109
  }
109
110
 
110
111
  return nodes;
111
- },
112
+ }
112
113
 
113
- successorsNode(vueFlowId:string, nodeUid:string) {
114
+ export function successorsNode(vueFlowId: string, nodeUid: string) {
114
115
  const {getEdges, findNode} = useVueFlow(vueFlowId);
115
116
 
116
117
  const nodes = [findNode(nodeUid)];
117
118
 
118
119
  for (const edge of getEdges.value) {
119
- if (edge.source === nodeUid) {
120
- nodes.push(edge.targetNode);
121
- const recursiveEdge = this.successorsNode(vueFlowId, edge.target);
122
- if (recursiveEdge.length > 0) {
123
- nodes.push(...recursiveEdge);
120
+ if (edge.source === nodeUid) {
121
+ nodes.push(edge.targetNode);
122
+ const recursiveEdge = successorsNode(vueFlowId, edge.target);
123
+ if (recursiveEdge.length > 0) {
124
+ nodes.push(...recursiveEdge);
125
+ }
124
126
  }
125
- }
126
127
  }
127
128
 
128
129
  return nodes;
129
- },
130
+ }
130
131
 
131
- linkedElements(vueFlowId:string, nodeUid:string) {
132
+ export function linkedElements(vueFlowId: string, nodeUid: string) {
132
133
  return [
133
- ...this.predecessorsEdge(vueFlowId, nodeUid),
134
- ...this.predecessorsNode(vueFlowId, nodeUid),
135
- ...this.successorsEdge(vueFlowId, nodeUid),
136
- ...this.successorsNode(vueFlowId, nodeUid),
134
+ ...predecessorsEdge(vueFlowId, nodeUid),
135
+ ...predecessorsNode(vueFlowId, nodeUid),
136
+ ...successorsEdge(vueFlowId, nodeUid),
137
+ ...successorsNode(vueFlowId, nodeUid),
137
138
  ];
138
- },
139
+ }
139
140
 
140
- generateDagreGraph(
141
+ export function generateDagreGraph(
141
142
  flowGraph: { nodes: any; clusters: any; edges: any },
142
143
  hiddenNodes: string[],
143
144
  isHorizontal: boolean,
@@ -145,78 +146,78 @@ export default {
145
146
  edgeReplacer: EdgeReplacer,
146
147
  collapsed: Set<string>,
147
148
  clusterToNode: MinimalNode[]
148
- ) {
149
+ ) {
149
150
  const dagreGraph = new dagre.graphlib.Graph({compound: true});
150
151
  dagreGraph.setDefaultEdgeLabel(() => ({}));
151
152
  dagreGraph.setGraph({rankdir: isHorizontal ? "LR" : "TB"});
152
153
 
153
154
  for (const node of flowGraph.nodes) {
154
- if (!hiddenNodes.includes(node.uid)) {
155
- dagreGraph.setNode(node.uid, {
156
- width: this.getNodeWidth(node),
157
- height: this.getNodeHeight(node),
158
- });
159
- }
155
+ if (!hiddenNodes.includes(node.uid)) {
156
+ dagreGraph.setNode(node.uid, {
157
+ width: getNodeWidth(node),
158
+ height: getNodeHeight(node),
159
+ });
160
+ }
160
161
  }
161
162
 
162
163
  for (const cluster of flowGraph.clusters || []) {
163
- const nodeUid = cluster.cluster.uid.replace(CLUSTER_PREFIX, "");
164
- if (
165
- clustersWithoutRootNode.includes(cluster.cluster.uid) &&
166
- collapsed.has(nodeUid)
167
- ) {
168
- const node = {uid: nodeUid, type: "collapsedcluster"};
169
- dagreGraph.setNode(nodeUid, {
170
- width: this.getNodeWidth(node),
171
- height: this.getNodeHeight(node),
172
- });
173
- clusterToNode.push(node);
174
- continue;
175
- }
176
- if (!edgeReplacer[cluster.cluster.uid]) {
177
- dagreGraph.setNode(cluster.cluster.uid, {clusterLabelPos: "top"});
178
-
179
- for (const node of cluster.nodes || []) {
180
- if (!hiddenNodes.includes(node)) {
181
- dagreGraph.setParent(node, cluster.cluster.uid);
182
- }
164
+ const nodeUid = cluster.cluster.uid.replace(CLUSTER_PREFIX, "");
165
+ if (
166
+ clustersWithoutRootNode.includes(cluster.cluster.uid) &&
167
+ collapsed.has(nodeUid)
168
+ ) {
169
+ const node = {uid: nodeUid, type: "collapsedcluster"};
170
+ dagreGraph.setNode(nodeUid, {
171
+ width: getNodeWidth(node),
172
+ height: getNodeHeight(node),
173
+ });
174
+ clusterToNode.push(node);
175
+ continue;
176
+ }
177
+ if (!edgeReplacer[cluster.cluster.uid]) {
178
+ dagreGraph.setNode(cluster.cluster.uid, {clusterLabelPos: "top"});
179
+
180
+ for (const node of cluster.nodes || []) {
181
+ if (!hiddenNodes.includes(node)) {
182
+ dagreGraph.setParent(node, cluster.cluster.uid);
183
+ }
184
+ }
183
185
  }
184
- }
185
- if (cluster.parents) {
186
- const nodeChild = edgeReplacer[cluster.cluster.uid]
187
- ? edgeReplacer[cluster.cluster.uid]
188
- : cluster.cluster.uid;
189
- if (!hiddenNodes.includes(nodeChild)) {
190
- dagreGraph.setParent(
191
- nodeChild,
192
- cluster.parents[cluster.parents.length - 1]
193
- );
186
+ if (cluster.parents) {
187
+ const nodeChild = edgeReplacer[cluster.cluster.uid]
188
+ ? edgeReplacer[cluster.cluster.uid]
189
+ : cluster.cluster.uid;
190
+ if (!hiddenNodes.includes(nodeChild)) {
191
+ dagreGraph.setParent(
192
+ nodeChild,
193
+ cluster.parents[cluster.parents.length - 1]
194
+ );
195
+ }
194
196
  }
195
- }
196
197
  }
197
198
 
198
199
  for (const edge of flowGraph.edges || []) {
199
- const newEdge = this.replaceIfCollapsed(
200
- edge.source,
201
- edge.target,
202
- edgeReplacer,
203
- hiddenNodes
204
- );
205
- if (newEdge) {
206
- dagreGraph.setEdge(newEdge.source, newEdge.target);
207
- }
200
+ const newEdge = replaceIfCollapsed(
201
+ edge.source,
202
+ edge.target,
203
+ edgeReplacer,
204
+ hiddenNodes
205
+ );
206
+ if (newEdge) {
207
+ dagreGraph.setEdge(newEdge.source, newEdge.target);
208
+ }
208
209
  }
209
210
 
210
211
  dagre.layout(dagreGraph);
211
212
  return dagreGraph;
212
- },
213
+ }
213
214
 
214
- getNodePosition(n: {
215
+ export function getNodePosition(n: {
215
216
  x: number;
216
217
  y: number;
217
218
  width: number;
218
219
  height: number;
219
- }, parent?: {
220
+ }, parent?: {
220
221
  x: number;
221
222
  y: number;
222
223
  width: number;
@@ -226,126 +227,126 @@ export default {
226
227
 
227
228
  // bug with parent node,
228
229
  if (parent) {
229
- const parentPosition = this.getNodePosition(parent);
230
- position.x = position.x - parentPosition.x;
231
- position.y = position.y - parentPosition.y;
230
+ const parentPosition = getNodePosition(parent);
231
+ position.x = position.x - parentPosition.x;
232
+ position.y = position.y - parentPosition.y;
232
233
  }
233
234
  return position;
234
- },
235
-
236
- getNodeWidth(node: MinimalNode) {
237
- return this.isTaskNode(node) || this.isTriggerNode(node)
238
- ? NODE_SIZES.TASK_WIDTH
239
- : (this.isCollapsedCluster(node)
240
- ? NODE_SIZES.COLLAPSED_CLUSTER_WIDTH
241
- : NODE_SIZES.DOT_WIDTH);
242
- },
243
-
244
- getNodeHeight(node: MinimalNode) {
245
- return this.isTaskNode(node) || this.isTriggerNode(node)
246
- ? NODE_SIZES.TASK_HEIGHT
247
- : (this.isCollapsedCluster(node)
248
- ? NODE_SIZES.COLLAPSED_CLUSTER_HEIGHT
249
- : NODE_SIZES.DOT_HEIGHT);
250
- },
251
-
252
- isTaskNode(node: MinimalNode) {
235
+ }
236
+
237
+ export function getNodeWidth(node: MinimalNode) {
238
+ return isTaskNode(node) || isTriggerNode(node)
239
+ ? NODE_SIZES.TASK_WIDTH
240
+ : (isCollapsedCluster(node)
241
+ ? NODE_SIZES.COLLAPSED_CLUSTER_WIDTH
242
+ : NODE_SIZES.DOT_WIDTH);
243
+ }
244
+
245
+ export function getNodeHeight(node: MinimalNode) {
246
+ return isTaskNode(node) || isTriggerNode(node)
247
+ ? NODE_SIZES.TASK_HEIGHT
248
+ : (isCollapsedCluster(node)
249
+ ? NODE_SIZES.COLLAPSED_CLUSTER_HEIGHT
250
+ : NODE_SIZES.DOT_HEIGHT);
251
+ }
252
+
253
+ export function isTaskNode(node: MinimalNode) {
253
254
  return ["GraphTask", "SubflowGraphTask$1"].some((t) => node.type.endsWith(t));
254
- },
255
+ }
255
256
 
256
- isTriggerNode(node: MinimalNode) {
257
+ export function isTriggerNode(node: MinimalNode) {
257
258
  return node.type.endsWith("GraphTrigger");
258
- },
259
+ }
259
260
 
260
- isCollapsedCluster(node: MinimalNode) {
261
+ export function isCollapsedCluster(node: MinimalNode) {
261
262
  return node.type === "collapsedcluster";
262
- },
263
+ }
263
264
 
264
- replaceIfCollapsed(source:string, target: string, edgeReplacer: EdgeReplacer, hiddenNodes: string[]) {
265
+ export function replaceIfCollapsed(source: string, target: string, edgeReplacer: EdgeReplacer, hiddenNodes: string[]) {
265
266
  const newSource = edgeReplacer[source] ? edgeReplacer[source] : source;
266
267
  const newTarget = edgeReplacer[target] ? edgeReplacer[target] : target;
267
268
 
268
269
  if (
269
- newSource === newTarget ||
270
- hiddenNodes.includes(newSource) ||
271
- hiddenNodes.includes(newTarget)
270
+ newSource === newTarget ||
271
+ hiddenNodes.includes(newSource) ||
272
+ hiddenNodes.includes(newTarget)
272
273
  ) {
273
- return null;
274
+ return null;
274
275
  }
275
276
  return {target: newTarget, source: newSource};
276
- },
277
+ }
277
278
 
278
- cleanGraph(vueflowId:string) {
279
+ export function cleanGraph(vueflowId: string) {
279
280
  const {
280
- getEdges,
281
- getNodes,
282
- getElements,
283
- removeEdges,
284
- removeNodes,
285
- removeSelectedElements,
281
+ getEdges,
282
+ getNodes,
283
+ getElements,
284
+ removeEdges,
285
+ removeNodes,
286
+ removeSelectedElements,
286
287
  } = useVueFlow(vueflowId);
287
288
  removeEdges(getEdges.value);
288
289
  removeNodes(getNodes.value);
289
290
  removeSelectedElements(getElements.value);
290
- },
291
+ }
291
292
 
292
- flowHaveTasks(source:string) {
293
- return source ? flowHaveTasks(source) : false;
294
- },
293
+ export function flowHaveTasks(source: string) {
294
+ return source ? yamlFlowHaveTask(source) : false;
295
+ }
295
296
 
296
- nodeColor(node:MinimalNode, collapsed: Set<string>) {
297
+ export function nodeColor(node: MinimalNode, collapsed: Set<string>) {
297
298
  if (node.uid === TRIGGERS_NODE_UID) {
298
- return "success";
299
+ return "success";
299
300
  }
300
301
 
301
- if (this.isTriggerNode(node) || this.isCollapsedCluster(node)) {
302
- return "success";
302
+ if (isTriggerNode(node) || isCollapsedCluster(node)) {
303
+ return "success";
303
304
  }
304
305
 
305
306
  if (node.type.endsWith("SubflowGraphTask")) {
306
- return "primary";
307
+ return "primary";
307
308
  }
308
309
 
309
310
  if (node.branchType == BranchType.ERROR) {
310
- return "danger";
311
+ return "danger";
311
312
  }
312
313
 
313
314
  if (node.branchType == BranchType.FINALLY) {
314
- return "warning";
315
+ return "warning";
315
316
  }
316
317
 
317
318
  if (collapsed.has(node.uid)) {
318
- return "blue";
319
+ return "blue";
319
320
  }
320
321
 
321
322
  return "default";
322
- },
323
+ }
323
324
 
324
- haveAdd(edge:GraphEdge,
325
- nodeByUid:Record<string, MinimalNode>,
326
- clustersRootTaskUids:string[],
327
- readOnlyUidPrefixes:string[]) {
325
+ export function haveAdd(edge: GraphEdge,
326
+ nodeByUid: Record<string, MinimalNode>,
327
+ clustersRootTaskUids: string[],
328
+ readOnlyUidPrefixes: string[]) {
328
329
  // prevent subflow edit (edge = subflowNode -> subflowNode)
329
330
  if (
330
- readOnlyUidPrefixes.some(
331
- (prefix) =>
332
- edge.source.startsWith(prefix) && edge.target.startsWith(prefix)
333
- )
331
+ readOnlyUidPrefixes.some(
332
+ (prefix) =>
333
+ edge.source.startsWith(prefix) && edge.target.startsWith(prefix)
334
+ )
334
335
  ) {
335
- return undefined;
336
+ return undefined;
336
337
  }
337
338
 
338
339
  // edge = clusterRoot -> clusterRootTask
339
340
  if (clustersRootTaskUids.includes(edge.target)) {
340
- return undefined;
341
+ return undefined;
341
342
  }
342
343
 
343
344
  // edge = Triggers cluster -> something || edge = something -> Triggers cluster
344
345
  if (
345
- edge.source.startsWith(TRIGGERS_NODE_UID) ||
346
- edge.target.startsWith(TRIGGERS_NODE_UID)
346
+ edge.source.startsWith(TRIGGERS_NODE_UID) ||
347
+ edge.target.startsWith(TRIGGERS_NODE_UID)
347
348
  ) {
348
- return undefined;
349
+ return undefined;
349
350
  }
350
351
 
351
352
  const dotSplitTarget = edge.target.split(".");
@@ -356,60 +357,60 @@ export default {
356
357
  // edge = task of parallel -> end of parallel, we only add + symbol right after the parallel cluster root task node
357
358
  const targetNode = nodeByUid[edge.target];
358
359
  if (
359
- targetNode.type.endsWith("GraphClusterEnd") &&
360
- nodeByUid[targetNodeClusterUid]?.task?.type?.endsWith("Parallel")
360
+ targetNode.type.endsWith("GraphClusterEnd") &&
361
+ nodeByUid[targetNodeClusterUid]?.task?.type?.endsWith("Parallel")
361
362
  ) {
362
- return undefined;
363
+ return undefined;
363
364
  }
364
365
 
365
366
  // edge = something -> clusterRoot ==> we insert before the cluster
366
367
  // clusterUid = clusterTraversalPrefix.{rootTaskUid}
367
368
  // clusterRoot.uid = clusterUid.someUid = clusterTraversalPrefix.{rootTaskUid}.someUid
368
369
  if (targetNode.type.endsWith("GraphClusterRoot")) {
369
- return [clusterRootTaskId, "before"];
370
+ return [clusterRootTaskId, "before"];
370
371
  }
371
372
 
372
373
  const sourceIsEndOfCluster =
373
- nodeByUid[edge.source].type.endsWith("GraphClusterEnd");
374
+ nodeByUid[edge.source].type.endsWith("GraphClusterEnd");
374
375
  // edge = clusterTask -> clusterEnd ==> we insert after the previous task
375
376
  if (!sourceIsEndOfCluster && targetNode.type.endsWith("GraphClusterEnd")) {
376
- return [Utils.afterLastDot(edge.source), "after"];
377
+ return [Utils.afterLastDot(edge.source), "after"];
377
378
  }
378
379
 
379
380
  // edge = cluster1End -> something ==> we insert after cluster1
380
381
  if (sourceIsEndOfCluster) {
381
- const dotSplitSource = edge.source.split(".");
382
- return [dotSplitSource[dotSplitSource.length - 2], "after"];
382
+ const dotSplitSource = edge.source.split(".");
383
+ return [dotSplitSource[dotSplitSource.length - 2], "after"];
383
384
  }
384
385
 
385
386
  return [Utils.afterLastDot(edge.target), "before"];
386
- },
387
+ }
387
388
 
388
- getEdgeColor(edge: GraphEdge, nodeByUid: Record<string, MinimalNode>, clusterByNodeUid: Record<string, Cluster>) {
389
+ export function getEdgeColor(edge: GraphEdge, nodeByUid: Record<string, MinimalNode>, clusterByNodeUid: Record<string, Cluster>) {
389
390
  const findRootBranchType = (nodeId: string): BranchType | null => {
390
- const uidParts = nodeId.split(".");
391
- for (let i = 1; i <= uidParts.length; i++) {
392
- const parentUid = uidParts.slice(0, i).join(".");
393
- const branchType = clusterByNodeUid[parentUid]?.branchType;
394
- if (branchType) return branchType;
395
- }
396
- return nodeByUid[nodeId]?.branchType ?? null;
391
+ const uidParts = nodeId.split(".");
392
+ for (let i = 1; i <= uidParts.length; i++) {
393
+ const parentUid = uidParts.slice(0, i).join(".");
394
+ const branchType = clusterByNodeUid[parentUid]?.branchType;
395
+ if (branchType) return branchType;
396
+ }
397
+ return nodeByUid[nodeId]?.branchType ?? null;
397
398
  };
398
399
 
399
400
  const sourceBranchType = findRootBranchType(edge.source);
400
401
  const targetBranchType = findRootBranchType(edge.target);
401
402
 
402
- return [sourceBranchType, targetBranchType].includes(BranchType.ERROR) ? "danger"
403
- : [sourceBranchType, targetBranchType].includes(BranchType.FINALLY) ? "warning"
404
- : null;
405
- },
403
+ return [sourceBranchType, targetBranchType].includes(BranchType.ERROR) ? "danger"
404
+ : [sourceBranchType, targetBranchType].includes(BranchType.FINALLY) ? "warning"
405
+ : null;
406
+ }
406
407
 
407
- generateGraph(
408
+ export function generateGraph(
408
409
  _vueFlowId: string,
409
- flowId:string | undefined,
410
- namespace:string| undefined,
410
+ flowId: string | undefined,
411
+ namespace: string | undefined,
411
412
  flowGraph: FlowGraph | undefined,
412
- flowSource:string | undefined,
413
+ flowSource: string | undefined,
413
414
  hiddenNodes: string[],
414
415
  isHorizontal: boolean,
415
416
  edgeReplacer: EdgeReplacer,
@@ -418,300 +419,441 @@ export default {
418
419
  isReadOnly: boolean,
419
420
  isAllowedEdit: boolean,
420
421
  enableSubflowInteraction: boolean
421
- ):Elements | undefined{
422
- const elements:Elements = [];
422
+ ): Elements | undefined {
423
+ const elements: Elements = [];
423
424
 
424
425
  const clustersWithoutRootNode = [CLUSTER_PREFIX + TRIGGERS_NODE_UID];
425
426
 
426
- if (!flowGraph || (flowSource && !this.flowHaveTasks(flowSource))) {
427
- elements.push({
428
- id: "start",
429
- type: "dot",
430
- position: {x: 0, y: 0},
431
- style: {
432
- width: "5px",
433
- height: "5px",
434
- },
435
- sourcePosition: isHorizontal ? Position.Right : Position.Bottom,
436
- targetPosition: isHorizontal ? Position.Left : Position.Top,
437
- parentNode: undefined,
438
- draggable: false,
439
- });
440
- elements.push({
441
- id: "end",
442
- type: "dot",
443
- position: isHorizontal ? {x: 50, y: 0} : {x: 0, y: 50},
444
- style: {
445
- width: "5px",
446
- height: "5px",
447
- },
448
- sourcePosition: isHorizontal ? Position.Right : Position.Bottom,
449
- targetPosition: isHorizontal ? Position.Left : Position.Top,
450
- parentNode: undefined,
451
- draggable: false,
452
- });
453
- elements.push({
454
- id: "start|end",
455
- source: "start",
456
- target: "end",
457
- type: "edge",
458
- data: {
459
- edge: {
460
- relation: {
461
- relationType: "SEQUENTIAL",
427
+ if (!flowGraph || (flowSource && !flowHaveTasks(flowSource))) {
428
+ elements.push({
429
+ id: "start",
430
+ type: "dot",
431
+ position: {x: 0, y: 0},
432
+ style: {
433
+ width: "5px",
434
+ height: "5px",
435
+ },
436
+ sourcePosition: isHorizontal ? Position.Right : Position.Bottom,
437
+ targetPosition: isHorizontal ? Position.Left : Position.Top,
438
+ parentNode: undefined,
439
+ draggable: false,
440
+ });
441
+ elements.push({
442
+ id: "end",
443
+ type: "dot",
444
+ position: isHorizontal ? {x: 50, y: 0} : {x: 0, y: 50},
445
+ style: {
446
+ width: "5px",
447
+ height: "5px",
462
448
  },
463
- },
464
- isFlowable: false,
465
- initTask: true,
466
- color: "primary",
467
- },
468
- });
469
-
470
- return;
471
- }
472
-
473
- const dagreGraph = this.generateDagreGraph(
474
- flowGraph,
475
- hiddenNodes,
476
- isHorizontal,
477
- clustersWithoutRootNode,
478
- edgeReplacer,
479
- collapsed,
480
- clusterToNode
449
+ sourcePosition: isHorizontal ? Position.Right : Position.Bottom,
450
+ targetPosition: isHorizontal ? Position.Left : Position.Top,
451
+ parentNode: undefined,
452
+ draggable: false,
453
+ });
454
+ elements.push({
455
+ id: "start|end",
456
+ source: "start",
457
+ target: "end",
458
+ type: "edge",
459
+ data: {
460
+ edge: {
461
+ relation: {
462
+ relationType: "SEQUENTIAL",
463
+ },
464
+ },
465
+ isFlowable: false,
466
+ initTask: true,
467
+ color: "primary",
468
+ },
469
+ });
470
+
471
+ return;
472
+ }
473
+
474
+ const dagreGraph = generateDagreGraph(
475
+ flowGraph,
476
+ hiddenNodes,
477
+ isHorizontal,
478
+ clustersWithoutRootNode,
479
+ edgeReplacer,
480
+ collapsed,
481
+ clusterToNode
481
482
  );
482
483
 
483
- const clusterByNodeUid:Record<string, Cluster> = {};
484
+ const clusterByNodeUid: Record<string, Cluster> = {};
484
485
  const clusters = flowGraph.clusters || [];
485
486
  const rawClusters = clusters.map((c) => c.cluster);
486
487
  const readOnlyUidPrefixes = rawClusters
487
- .filter((c) => c.type.endsWith("SubflowGraphCluster"))
488
- .map((c) => c.taskNode.uid);
488
+ .filter((c) => c.type.endsWith("SubflowGraphCluster"))
489
+ .map((c) => c.taskNode.uid);
489
490
 
490
491
  const nodeByUid = Object.fromEntries(
491
- flowGraph.nodes.concat(clusterToNode).map((node) => [node.uid, node])
492
+ flowGraph.nodes.concat(clusterToNode).map((node) => [node.uid, node])
492
493
  );
493
494
  for (const cluster of clusters) {
494
- if (
495
- !edgeReplacer[cluster.cluster.uid] &&
496
- !collapsed.has(cluster.cluster.uid)
497
- ) {
498
495
  if (
499
- cluster.cluster.taskNode?.task?.type ===
500
- "io.kestra.core.tasks.flows.Dag"
496
+ !edgeReplacer[cluster.cluster.uid] &&
497
+ !collapsed.has(cluster.cluster.uid)
501
498
  ) {
502
- readOnlyUidPrefixes.push(cluster.cluster.taskNode.uid);
499
+ if (
500
+ cluster.cluster.taskNode?.task?.type ===
501
+ "io.kestra.core.tasks.flows.Dag"
502
+ ) {
503
+ readOnlyUidPrefixes.push(cluster.cluster.taskNode.uid);
504
+ }
505
+
506
+ for (const nodeUid of cluster.nodes) {
507
+ clusterByNodeUid[nodeUid] = cluster.cluster;
508
+ }
509
+
510
+ const clusterUid = cluster.cluster.uid;
511
+ const dagreNode = dagreGraph.node(clusterUid);
512
+ const parentNode = cluster.parents
513
+ ? cluster.parents[cluster.parents.length - 1]
514
+ : undefined;
515
+
516
+ const clusterColor = computeClusterColor(cluster.cluster);
517
+
518
+ elements.push({
519
+ id: clusterUid,
520
+ type: "cluster",
521
+ parentNode: parentNode,
522
+ position: getNodePosition(
523
+ dagreNode,
524
+ parentNode ? dagreGraph.node(parentNode) : undefined
525
+ ),
526
+ style: {
527
+ width:
528
+ clusterUid === TRIGGERS_NODE_UID && isHorizontal
529
+ ? NODE_SIZES.TRIGGER_CLUSTER_WIDTH + "px"
530
+ : dagreNode.width + "px",
531
+ height:
532
+ clusterUid === TRIGGERS_NODE_UID && !isHorizontal
533
+ ? NODE_SIZES.TRIGGER_CLUSTER_HEIGHT + "px"
534
+ : dagreNode.height + "px",
535
+ },
536
+ data: {
537
+ collapsable: true,
538
+ color: clusterColor,
539
+ taskNode: cluster.cluster.taskNode,
540
+ unused: cluster.cluster.taskNode
541
+ ? nodeByUid[cluster.cluster.taskNode.uid].unused
542
+ : false,
543
+ },
544
+ class: `ks-topology-${clusterColor}-border rounded p-2`,
545
+ } as any);
503
546
  }
504
-
505
- for (const nodeUid of cluster.nodes) {
506
- clusterByNodeUid[nodeUid] = cluster.cluster;
507
- }
508
-
509
- const clusterUid = cluster.cluster.uid;
510
- const dagreNode = dagreGraph.node(clusterUid);
511
- const parentNode = cluster.parents
512
- ? cluster.parents[cluster.parents.length - 1]
513
- : undefined;
514
-
515
- const clusterColor = this.computeClusterColor(cluster.cluster);
516
-
517
- elements.push({
518
- id: clusterUid,
519
- type: "cluster",
520
- parentNode: parentNode,
521
- position: this.getNodePosition(
522
- dagreNode,
523
- parentNode ? dagreGraph.node(parentNode) : undefined
524
- ),
525
- style: {
526
- width:
527
- clusterUid === TRIGGERS_NODE_UID && isHorizontal
528
- ? NODE_SIZES.TRIGGER_CLUSTER_WIDTH + "px"
529
- : dagreNode.width + "px",
530
- height:
531
- clusterUid === TRIGGERS_NODE_UID && !isHorizontal
532
- ? NODE_SIZES.TRIGGER_CLUSTER_HEIGHT + "px"
533
- : dagreNode.height + "px",
534
- },
535
- data: {
536
- collapsable: true,
537
- color: clusterColor,
538
- taskNode: cluster.cluster.taskNode,
539
- unused: cluster.cluster.taskNode
540
- ? nodeByUid[cluster.cluster.taskNode.uid].unused
541
- : false,
542
- },
543
- class: `ks-topology-${clusterColor}-border rounded p-2`,
544
- } as any);
545
- }
546
547
  }
547
548
 
548
549
  for (const node of flowGraph.nodes.concat(clusterToNode)) {
549
- if (!hiddenNodes.includes(node.uid)) {
550
- const dagreNode = dagreGraph.node(node.uid);
551
- let nodeType = "task";
552
- if (this.isClusterRootOrEnd(node)) {
553
- nodeType = "dot";
554
- } else if (node.type.includes("GraphTrigger")) {
555
- nodeType = "trigger";
556
- } else if (node.type === "collapsedcluster") {
557
- nodeType = "collapsedcluster";
550
+ if (!hiddenNodes.includes(node.uid)) {
551
+ const dagreNode = dagreGraph.node(node.uid);
552
+ let nodeType = "task";
553
+ if (isClusterRootOrEnd(node)) {
554
+ nodeType = "dot";
555
+ } else if (node.type.includes("GraphTrigger")) {
556
+ nodeType = "trigger";
557
+ } else if (node.type === "collapsedcluster") {
558
+ nodeType = "collapsedcluster";
559
+ }
560
+
561
+ const color = nodeColor(node, collapsed);
562
+ // If task type includes '$', it's an inner class so it's probably an internal class not supposed to be editable
563
+ // In such case, only the root task will be editable
564
+ const isReadOnlyTask =
565
+ isReadOnly ||
566
+ node.task?.type?.includes("$") ||
567
+ readOnlyUidPrefixes.some((prefix) =>
568
+ node.uid.startsWith(prefix + ".")
569
+ );
570
+ elements.push({
571
+ id: node.uid,
572
+ type: nodeType,
573
+ position: getNodePosition(
574
+ dagreNode,
575
+ clusterByNodeUid[node.uid]
576
+ ? dagreGraph.node(clusterByNodeUid[node.uid].uid)
577
+ : undefined
578
+ ),
579
+ style: {
580
+ width: getNodeWidth(node) + "px",
581
+ height: getNodeHeight(node) + "px",
582
+ },
583
+ sourcePosition: isHorizontal ? Position.Right : Position.Bottom,
584
+ targetPosition: isHorizontal ? Position.Left : Position.Top,
585
+ parentNode: clusterByNodeUid[node.uid]
586
+ ? clusterByNodeUid[node.uid].uid
587
+ : undefined,
588
+ draggable: nodeType === "task" ? !isReadOnlyTask : false,
589
+ data: {
590
+ node: node,
591
+ parent: clusterByNodeUid[node.uid] ? clusterByNodeUid[node.uid] : undefined,
592
+ namespace:
593
+ clusterByNodeUid[node.uid]?.taskNode?.task?.namespace ??
594
+ namespace,
595
+ flowId:
596
+ clusterByNodeUid[node.uid]?.taskNode?.task?.flowId ?? flowId,
597
+ isFlowable:
598
+ clusterByNodeUid[node.uid]?.uid === CLUSTER_PREFIX + node.uid &&
599
+ !node.type.endsWith("SubflowGraphTask"),
600
+ color: color,
601
+ expandable: isExpandableTask(
602
+ node,
603
+ clusterByNodeUid,
604
+ edgeReplacer,
605
+ enableSubflowInteraction
606
+ ),
607
+ isReadOnly: isReadOnlyTask,
608
+ iconComponent: isCollapsedCluster(node)
609
+ ? "lightning-bolt"
610
+ : null,
611
+ executionId: node.executionId,
612
+ unused: node.unused,
613
+ },
614
+ class:
615
+ node.type === "collapsedcluster"
616
+ ? `ks-topology-${color}-border rounded`
617
+ : "",
618
+ });
558
619
  }
559
-
560
- const color = this.nodeColor(node, collapsed);
561
- // If task type includes '$', it's an inner class so it's probably an internal class not supposed to be editable
562
- // In such case, only the root task will be editable
563
- const isReadOnlyTask =
564
- isReadOnly ||
565
- node.task?.type?.includes("$") ||
566
- readOnlyUidPrefixes.some((prefix) =>
567
- node.uid.startsWith(prefix + ".")
568
- );
569
- elements.push({
570
- id: node.uid,
571
- type: nodeType,
572
- position: this.getNodePosition(
573
- dagreNode,
574
- clusterByNodeUid[node.uid]
575
- ? dagreGraph.node(clusterByNodeUid[node.uid].uid)
576
- : undefined
577
- ),
578
- style: {
579
- width: this.getNodeWidth(node) + "px",
580
- height: this.getNodeHeight(node) + "px",
581
- },
582
- sourcePosition: isHorizontal ? Position.Right : Position.Bottom,
583
- targetPosition: isHorizontal ? Position.Left : Position.Top,
584
- parentNode: clusterByNodeUid[node.uid]
585
- ? clusterByNodeUid[node.uid].uid
586
- : undefined,
587
- draggable: nodeType === "task" ? !isReadOnlyTask : false,
588
- data: {
589
- node: node,
590
- parent: clusterByNodeUid[node.uid] ? clusterByNodeUid[node.uid] : undefined,
591
- namespace:
592
- clusterByNodeUid[node.uid]?.taskNode?.task?.namespace ??
593
- namespace,
594
- flowId:
595
- clusterByNodeUid[node.uid]?.taskNode?.task?.flowId ?? flowId,
596
- isFlowable:
597
- clusterByNodeUid[node.uid]?.uid === CLUSTER_PREFIX + node.uid &&
598
- !node.type.endsWith("SubflowGraphTask"),
599
- color: color,
600
- expandable: this.isExpandableTask(
601
- node,
602
- clusterByNodeUid,
603
- edgeReplacer,
604
- enableSubflowInteraction
605
- ),
606
- isReadOnly: isReadOnlyTask,
607
- iconComponent: this.isCollapsedCluster(node)
608
- ? "lightning-bolt"
609
- : null,
610
- executionId: node.executionId,
611
- unused: node.unused,
612
- },
613
- class:
614
- node.type === "collapsedcluster"
615
- ? `ks-topology-${color}-border rounded`
616
- : "",
617
- });
618
- }
619
620
  }
620
621
 
621
622
  const clusterRootTaskNodeUids = rawClusters
622
- .filter((c) => c.taskNode)
623
- .map((c) => c.taskNode.uid);
623
+ .filter((c) => c.taskNode)
624
+ .map((c) => c.taskNode.uid);
624
625
  const edges = flowGraph.edges ?? [];
625
626
 
626
627
  for (const edge of edges) {
627
- const newEdge = this.replaceIfCollapsed(
628
- edge.source,
629
- edge.target,
630
- edgeReplacer,
631
- hiddenNodes
632
- );
633
- if (newEdge) {
634
- const edgeColor = this.getEdgeColor(edge, nodeByUid, clusterByNodeUid);
635
- elements.push({
636
- id: newEdge.source + "|" + newEdge.target,
637
- source: newEdge.source,
638
- target: newEdge.target,
639
- type: "edge",
640
- markerEnd: this.isClusterRootOrEnd(nodeByUid[newEdge.target])
641
- ? ""
642
- : {
643
- id: "marker-" + (nodeByUid[newEdge.target].branchType ? nodeByUid[newEdge.target].branchType?.toLocaleLowerCase() : "custom"),
644
- type: MarkerType.ArrowClosed,
645
- color: edgeColor ? `var(--ks-border-${edgeColor})` : "var(--ks-topology-edge-color)"
646
- },
647
- data: {
648
- haveAdd:
649
- !isReadOnly &&
650
- isAllowedEdit &&
651
- this.haveAdd(
652
- edge,
653
- nodeByUid,
654
- clusterRootTaskNodeUids,
655
- readOnlyUidPrefixes
656
- ),
657
- haveDashArray:
658
- nodeByUid[edge.source].type.endsWith("GraphTrigger") ||
659
- nodeByUid[edge.target].type.endsWith("GraphTrigger") ||
660
- edge.source.startsWith(TRIGGERS_NODE_UID),
661
- color: edgeColor,
662
- unused: (edge as any).unused,
663
- },
664
- style: {
665
- zIndex: 10,
666
- },
667
- });
668
- }
628
+ const newEdge = replaceIfCollapsed(
629
+ edge.source,
630
+ edge.target,
631
+ edgeReplacer,
632
+ hiddenNodes
633
+ );
634
+ if (newEdge) {
635
+ const edgeColor = getEdgeColor(edge, nodeByUid, clusterByNodeUid);
636
+ elements.push({
637
+ id: newEdge.source + "|" + newEdge.target,
638
+ source: newEdge.source,
639
+ target: newEdge.target,
640
+ type: "edge",
641
+ markerEnd: isClusterRootOrEnd(nodeByUid[newEdge.target])
642
+ ? ""
643
+ : {
644
+ id: "marker-" + (nodeByUid[newEdge.target].branchType ? nodeByUid[newEdge.target].branchType?.toLocaleLowerCase() : "custom"),
645
+ type: MarkerType.ArrowClosed,
646
+ color: edgeColor ? `var(--ks-border-${edgeColor})` : "var(--ks-topology-edge-color)"
647
+ },
648
+ data: {
649
+ haveAdd:
650
+ !isReadOnly &&
651
+ isAllowedEdit &&
652
+ haveAdd(
653
+ edge,
654
+ nodeByUid,
655
+ clusterRootTaskNodeUids,
656
+ readOnlyUidPrefixes
657
+ ),
658
+ haveDashArray:
659
+ nodeByUid[edge.source].type.endsWith("GraphTrigger") ||
660
+ nodeByUid[edge.target].type.endsWith("GraphTrigger") ||
661
+ edge.source.startsWith(TRIGGERS_NODE_UID),
662
+ color: edgeColor,
663
+ unused: (edge as any).unused,
664
+ },
665
+ style: {
666
+ zIndex: 10,
667
+ },
668
+ });
669
+ }
669
670
  }
670
671
 
671
672
  return elements;
672
- },
673
+ }
673
674
 
674
- isClusterRootOrEnd(node:MinimalNode) {
675
+ export function isClusterRootOrEnd(node: MinimalNode) {
675
676
  return ["GraphClusterRoot", "GraphClusterFinally", "GraphClusterAfterExecution", "GraphClusterEnd"].some((s) =>
676
- node.type.endsWith(s)
677
+ node.type.endsWith(s)
677
678
  );
678
- },
679
+ }
679
680
 
680
- computeClusterColor(cluster:Cluster) {
681
+ export function computeClusterColor(cluster: Cluster) {
681
682
  if (cluster.uid === CLUSTER_PREFIX + TRIGGERS_NODE_UID) {
682
- return "success";
683
+ return "success";
683
684
  }
684
685
 
685
686
  if (cluster.type.endsWith("SubflowGraphCluster")) {
686
- return "primary";
687
+ return "primary";
687
688
  }
688
689
 
689
690
  if (cluster.branchType === BranchType.ERROR) {
690
- return "danger";
691
+ return "danger";
691
692
  }
692
693
 
693
694
  return "blue";
694
- },
695
+ }
695
696
 
696
- isExpandableTask(
697
- node:MinimalNode,
697
+ export function isExpandableTask(
698
+ node: MinimalNode,
698
699
  clusterByNodeUid: Record<string, Cluster>,
699
700
  edgeReplacer: EdgeReplacer,
700
701
  enableSubflowInteraction?: boolean
701
- ) {
702
+ ) {
702
703
  if (Object.values(edgeReplacer).includes(node.uid)) {
703
- return true;
704
+ return true;
704
705
  }
705
706
 
706
- if (this.isCollapsedCluster(node)) {
707
- return true;
707
+ if (isCollapsedCluster(node)) {
708
+ return true;
708
709
  }
709
710
 
710
711
  return (
711
- node.type.endsWith("SubflowGraphTask") &&
712
- clusterByNodeUid[node.uid]?.uid?.replace(CLUSTER_PREFIX, "") !==
712
+ node.type.endsWith("SubflowGraphTask") &&
713
+ clusterByNodeUid[node.uid]?.uid?.replace(CLUSTER_PREFIX, "") !==
713
714
  node.uid &&
714
- enableSubflowInteraction
715
+ enableSubflowInteraction
715
716
  );
716
- },
717
- };
717
+ }
718
+
719
+ /**
720
+ * Get nodes that have no incoming edges, i.e., root nodes of the graph.
721
+ */
722
+ export function getRootNodes(graph: FlowGraph) {
723
+ const nodeUIDs = graph.nodes.map((node) => node.uid);
724
+ const rootUIDs = nodeUIDs.filter((uid) => {
725
+ return !graph.edges.some((edge) => edge.target === uid);
726
+ });
727
+ return graph.nodes.filter((node) => rootUIDs.includes(node.uid));
728
+ }
729
+
730
+ /**
731
+ * Get the edges connected as the source to a specific node. (outward facing arrows)
732
+ * @param graph The flow graph.
733
+ * @param nodeUid The UID of the node.
734
+ * @returns An array of edges connected to the node.
735
+ */
736
+ export function getTargetNodesEdges(graph: FlowGraph, nodeUid?: string) {
737
+ if (!nodeUid) {
738
+ return undefined;
739
+ }
740
+ return graph.edges.filter((edge) => edge.source === nodeUid && edge.target);
741
+ }
742
+
743
+ /**
744
+ * Follow the graph from a specific node to find the next task nodes.
745
+ * This function traverses the graph until it finds a node that is not a cluster.
746
+ * @param graph
747
+ * @param initialNode The initial node to start the search from.
748
+ * @returns An array of the next task nodes found.
749
+ */
750
+ export function getNextTaskNodes(graph: FlowGraph, initialNode: MinimalNode) {
751
+ let edges: GraphEdge[], nextTaskNodes: MinimalNode[], nodeUIDs: string[] = [initialNode.uid];
752
+ // loop until we find a node that is not a cluster
753
+ do {
754
+ // find all the edges that are connected to this task
755
+ edges = nodeUIDs.flatMap((uid) => getTargetNodesEdges(graph, uid)).filter(Boolean) as GraphEdge[];
756
+ // if there are no edges, return undefined
757
+ if (edges.length === 0) {
758
+ return [];
759
+ }
760
+ nodeUIDs = edges.map((edge) => edge.target);
761
+ nextTaskNodes = graph.nodes.filter((node) => nodeUIDs.includes(node.uid) && node.task);
762
+ } while (!nextTaskNodes.length);
763
+
764
+ return nextTaskNodes
765
+ }
766
+
767
+ /**
768
+ * Check if the tasks in the current graph are identical to the previous graph until the specified task.
769
+ * @param previousGraph The graph from the previous execution.
770
+ * @param currentGraph The graph from the current execution.
771
+ * @param taskId The ID of the task to check.
772
+ * @returns True if all tasks are identical, false otherwise.
773
+ */
774
+ export function areTasksIdenticalInGraphUntilTask(previousGraph: FlowGraph, currentGraph: FlowGraph, taskId?: string) {
775
+ if (!taskId) {
776
+ return false;
777
+ }
778
+
779
+ let previousRootTaskNodes = getRootNodes(previousGraph);
780
+ let currentRootTaskNodes = getRootNodes(currentGraph);
781
+
782
+ // if the root nodes are not the same, we cannot compare
783
+ if (previousRootTaskNodes.length !== currentRootTaskNodes.length) {
784
+ return false;
785
+ }
786
+
787
+ // avoid infinite loop
788
+ let failIndex = 120
789
+
790
+ // walk the graph until we find the taskId in the current root task nodes
791
+ // or until we run out of nodes to compare
792
+ do {
793
+ currentRootTaskNodes = currentRootTaskNodes.flatMap((node) => getNextTaskNodes(currentGraph, node));
794
+
795
+ // stop if we find the taskId in the current root task nodes
796
+ if (currentRootTaskNodes.some((node: any) => node.task.id === taskId)) {
797
+ return true;
798
+ }
799
+
800
+ previousRootTaskNodes = previousRootTaskNodes.flatMap((node) => getNextTaskNodes(previousGraph, node));
801
+
802
+ if (previousRootTaskNodes.length !== currentRootTaskNodes.length) {
803
+ return false;
804
+ }
805
+
806
+ for (const currentTaskNode of currentRootTaskNodes) {
807
+ const prevTaskNode = previousRootTaskNodes.find((taskNode) => taskNode.task?.id === currentTaskNode.task?.id);
808
+ const prevTaskValue = prevTaskNode?.task as Record<string, any> ?? {};
809
+ const currentTaskValue = currentTaskNode.task as Record<string, any> ?? {};
810
+
811
+ // if any member of the task is different, tasks are different
812
+ if (!prevTaskNode
813
+ || Object.keys(prevTaskValue).length !== Object.keys(currentTaskValue).length
814
+ ){
815
+ return false;
816
+ }
817
+ for (const key in currentTaskNode.task) {
818
+ if (prevTaskValue[key] !== currentTaskValue[key]) {
819
+ return false;
820
+ }
821
+ }
822
+ }
823
+ } while (previousRootTaskNodes.length && currentRootTaskNodes.length && failIndex-- > 0);
824
+
825
+ if (failIndex <= 0) {
826
+ console.warn("areTasksIdenticalInGraphUntilTask: Infinite loop detected, stopping comparison.");
827
+ return false;
828
+ }
829
+
830
+ return true;
831
+ }
832
+
833
+ /**
834
+ * @deprecated prefer using VueFlowUtils directly for tree shaking
835
+ */
836
+ export default {
837
+ isClusterRootOrEnd,
838
+ computeClusterColor,
839
+ isExpandableTask,
840
+ generateGraph,
841
+ generateDagreGraph,
842
+ getNodePosition,
843
+ getNodeWidth,
844
+ getNodeHeight,
845
+ isTaskNode,
846
+ isTriggerNode,
847
+ isCollapsedCluster,
848
+ replaceIfCollapsed,
849
+ cleanGraph,
850
+ flowHaveTasks,
851
+ nodeColor,
852
+ haveAdd,
853
+ getEdgeColor,
854
+ predecessorsEdge,
855
+ successorsEdge,
856
+ predecessorsNode,
857
+ successorsNode,
858
+ linkedElements,
859
+ }