@kestra-io/ui-libs 0.0.15 → 0.0.17

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kestra-io/ui-libs",
3
- "version": "v0.0.15",
3
+ "version": "v0.0.17",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "src",
@@ -20,6 +20,8 @@
20
20
  </template>
21
21
 
22
22
  <style scoped lang="scss">
23
+ @import "../../scss/variables";
24
+
23
25
  .add-task-div {
24
26
  margin: 0.2rem;
25
27
  width: 25px;
@@ -29,5 +31,9 @@
29
31
  html.dark & {
30
32
  background-color: var(--card-bg);
31
33
  }
34
+
35
+ &.text-danger {
36
+ border-color: $red;
37
+ }
32
38
  }
33
39
  </style>
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <tooltip>
2
+ <tooltip :key="lastStep.date">
3
3
  <template #content>
4
4
  <span v-for="(history, index) in filteredHistories" :key="'tt-' + index" class="duration-tt">
5
5
  <span class="square" :class="squareClass(history.state)" />
@@ -10,6 +10,7 @@
10
10
  import moment from "moment";
11
11
  import Duration from "./Duration.vue";
12
12
  import State from "../../utils/state.js";
13
+ import Utils from "../../utils/Utils.js";
13
14
 
14
15
  export default {
15
16
  name: "ExecutionInformations",
@@ -21,7 +22,7 @@
21
22
  return this.execution && this.execution.taskRunList ? this.execution.taskRunList : []
22
23
  },
23
24
  taskRuns() {
24
- return this.taskRunList.filter(t => t.taskId === this.task.id)
25
+ return this.taskRunList.filter(t => t.taskId === Utils.afterLastDot(this.task.id))
25
26
  },
26
27
  state() {
27
28
  if (!this.taskRuns) {
@@ -90,7 +91,6 @@
90
91
  default: null
91
92
  }
92
93
  }
93
-
94
94
  }
95
95
  </script>
96
96
  <style scoped>
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <span ref="tooltip">
2
+ <span ref="tooltip" v-bind="$attrs">
3
3
  <slot name="default" />
4
4
  </span>
5
5
  <span class="d-none" ref="tooltipContent">
@@ -7,24 +7,24 @@
7
7
  >
8
8
  <div v-if="state" class="status-div" :class="[`bg-${stateColor}`]" />
9
9
  <div class="icon rounded">
10
- <TaskIcon :icon="data.icon" :cls="cls" :class="taskIconBg" class="rounded bg-white" theme="light" />
10
+ <TaskIcon :cls="cls" :class="taskIconBg" class="rounded bg-white" theme="light" />
11
11
  </div>
12
12
  <div class="node-content">
13
13
  <div class="d-flex node-title">
14
14
  <div
15
15
  class="text-truncate task-title"
16
16
  >
17
- <tooltip :title="id">
18
- {{ id }}
17
+ <tooltip :title="hoverTooltip">
18
+ {{ trimmedId }}
19
19
  </tooltip>
20
20
  </div>
21
21
  <span
22
22
  class="d-flex"
23
23
  v-if="description"
24
24
  >
25
- <tooltip :title="$t('show description')">
25
+ <tooltip :title="$t('show description')" class="d-flex align-items-center">
26
26
  <InformationOutline
27
- @click="$emit(EVENTS.SHOW_DESCRIPTION, {id: id, description:description})"
27
+ @click="$emit(EVENTS.SHOW_DESCRIPTION, {id: trimmedId, description:description})"
28
28
  class="description-button ms-2"
29
29
  />
30
30
  </tooltip>
@@ -35,10 +35,10 @@
35
35
  <div class="text-white top-button-div">
36
36
  <slot name="badge-button-before" />
37
37
  <span
38
- v-if="link"
38
+ v-if="data.link"
39
39
  class="rounded-button"
40
40
  :class="[`bg-${data.color}`]"
41
- @click="$emit(EVENTS.OPEN_LINK, linkData)"
41
+ @click="$emit(EVENTS.OPEN_LINK, {link: data.link})"
42
42
  >
43
43
  <tooltip :title="$t('open')">
44
44
  <OpenInNew class="button-icon" alt="Open in new tab" />
@@ -66,8 +66,8 @@
66
66
  import ArrowExpand from "vue-material-design-icons/ArrowExpand.vue";
67
67
  import OpenInNew from "vue-material-design-icons/OpenInNew.vue";
68
68
  import Tooltip from "../misc/Tooltip.vue";
69
- import {VueFlowUtils} from "../../index.js";
70
69
  import {mapState} from "vuex";
70
+ import Utils from "../../utils/Utils.js";
71
71
 
72
72
  export default {
73
73
  components: {
@@ -144,9 +144,6 @@
144
144
  expandable() {
145
145
  return this.data?.expandable || false
146
146
  },
147
- link() {
148
- return this.data?.link || false
149
- },
150
147
  description() {
151
148
  const node = this.data.node.task ?? this.data.node.trigger ?? null
152
149
  if (node) {
@@ -154,6 +151,9 @@
154
151
  }
155
152
  return null
156
153
  },
154
+ trimmedId() {
155
+ return Utils.afterLastDot(this.id);
156
+ },
157
157
  taskIconBg() {
158
158
  return !["default", "danger"].includes(this.data.color) ? this.data.color : "";
159
159
  },
@@ -178,16 +178,27 @@
178
178
  [this.$attrs.class]: true
179
179
  }
180
180
  },
181
- linkData() {
182
- if(this.data.node.task) {
183
- return {link: VueFlowUtils.linkDatas(this.data.node.task, this.execution)}
181
+ cls() {
182
+ if (this.data.node.trigger) {
183
+ return this.data.node.trigger.type;
184
184
  }
185
- return null
185
+
186
+ if (!this.data.node?.task) {
187
+ return undefined;
188
+ }
189
+
190
+ return this.data.node.task.type;
186
191
  },
187
- cls() {
188
- return this.data.node?.task ? this.data.node.task.type : this.data.node?.trigger ? this.data.node.trigger.type : null
192
+ hoverTooltip() {
193
+ if (this.data.node.type.endsWith("SubflowGraphTask")) {
194
+ const subflowTask = this.data.node.task;
195
+
196
+ return subflowTask.namespace + " " + subflowTask.flowId;
197
+ }
198
+
199
+ return this.trimmedId;
189
200
  }
190
- },
201
+ }
191
202
  }
192
203
  </script>
193
204
 
@@ -1,12 +1,11 @@
1
1
  <template>
2
- <span class="badge rounded-pill text-color" :class="[`bg-${data.color}`]">{{ id.replace("cluster_", "") }}</span>
2
+ <span class="badge rounded-pill text-color" :class="[`bg-${data.color}`]">{{ clusterName }}</span>
3
3
  <div class="top-button-div text-white d-flex">
4
4
  <span
5
5
  v-if="data.collapsable"
6
6
  class="rounded-button"
7
7
  :class="[`bg-${data.color}`]"
8
8
  @click="collapse()"
9
-
10
9
  >
11
10
  <tooltip :title="$t('collapse')">
12
11
  <ArrowCollapse class="button-icon" alt="Collapse task" />
@@ -17,17 +16,35 @@
17
16
  <script setup>
18
17
  import ArrowCollapse from "vue-material-design-icons/ArrowCollapse.vue"
19
18
  import {EVENTS} from "../../utils/constants.js";
19
+ import {Position} from "@vue-flow/core";
20
20
 
21
- const props = defineProps(["data", "sourcePosition", "targetPosition", "label", "id"]);
21
+ const props = defineProps({
22
+ "data": {
23
+ type: Object,
24
+ default: undefined
25
+ },
26
+ "sourcePosition": {
27
+ type: Position,
28
+ default: undefined
29
+ },
30
+ "targetPosition": {
31
+ type: Position,
32
+ default: undefined
33
+ },
34
+ "id": {
35
+ type: String,
36
+ default: undefined
37
+ }
38
+ });
22
39
  const emit = defineEmits([EVENTS.COLLAPSE])
23
40
 
24
41
  const collapse = () => {
25
42
  emit(EVENTS.COLLAPSE, props.id)
26
43
  };
27
-
28
44
  </script>
29
45
  <script>
30
46
  import Tooltip from "../misc/Tooltip.vue";
47
+ import Utils from "../../utils/Utils.js";
31
48
 
32
49
  export default {
33
50
  inheritAttrs: false,
@@ -37,6 +54,16 @@
37
54
  tooltips: [],
38
55
  }
39
56
  },
57
+ computed: {
58
+ clusterName() {
59
+ const taskNode = this.data.taskNode;
60
+ if (taskNode?.type?.endsWith("SubflowGraphTask")) {
61
+ return taskNode.task.namespace + " " + taskNode.task.flowId;
62
+ }
63
+
64
+ return Utils.afterLastDot(this.id);
65
+ }
66
+ }
40
67
  }
41
68
  </script>
42
69
  <style scoped lang="scss">
@@ -1,34 +1,42 @@
1
1
  <template>
2
- <Handle type="source" :position="sourcePosition"/>
3
- <div class="collapsed-cluster-node">
2
+ <Handle type="source" :position="sourcePosition" />
3
+ <div class="collapsed-cluster-node d-flex">
4
4
  <span class="node-text">
5
5
  <component v-if="data.iconComponent" :is="data.iconComponent" :class="`text-${data.color} me-2`" />
6
- {{ id }}
6
+ {{ Utils.afterLastDot(id) }}
7
7
  </span>
8
8
  <div class="text-white top-button-div">
9
- <slot name="badge-button-before"/>
9
+ <slot name="badge-button-before" />
10
10
  <span
11
11
  v-if="expandable"
12
12
  class="rounded-button"
13
13
  :class="[`bg-${data.color}`]"
14
- @click="$emit(EVENTS.EXPAND, id)"
14
+ @click="$emit(EVENTS.EXPAND, {id})"
15
15
  >
16
- <ArrowExpand class="button-icon" alt="Expand task"/>
16
+ <tooltip :title="$t('expand')">
17
+ <ArrowExpand class="button-icon" alt="Expand task" />
18
+ </tooltip>
17
19
  </span>
18
- <slot name="badge-button-after"/>
20
+ <slot name="badge-button-after" />
19
21
  </div>
20
22
  </div>
21
- <Handle type="target" :position="targetPosition"/>
23
+ <Handle type="target" :position="targetPosition" />
22
24
  </template>
23
25
 
26
+ <script setup>
27
+ import Utils from "../../utils/Utils.js";
28
+ </script>
29
+
24
30
  <script>
25
31
  import {EVENTS} from "../../utils/constants.js";
26
32
  import ArrowExpand from "vue-material-design-icons/ArrowExpand.vue";
27
33
  import Webhook from "vue-material-design-icons/Webhook.vue";
28
34
  import {Handle} from "@vue-flow/core";
35
+ import Tooltip from "../misc/Tooltip.vue";
29
36
 
30
37
  export default {
31
38
  components: {
39
+ Tooltip,
32
40
  Handle,
33
41
  ArrowExpand,
34
42
  Webhook
@@ -87,6 +95,8 @@
87
95
  color: black;
88
96
  font-size: 0.90rem;
89
97
  display: flex;
98
+ align-items: center;
99
+
90
100
  html.dark & {
91
101
  color: white;
92
102
  }
@@ -1,18 +1,18 @@
1
1
  <template>
2
2
  <Handle type="source" class="custom-handle" :position="sourcePosition" />
3
3
  <div class="dot">
4
- <Circle class="circle" alt="circle" />
4
+ <CircleIcon :class="{'text-danger': data.node.error}" class="circle" alt="circle" />
5
5
  </div>
6
6
  <Handle type="target" class="custom-handle" :position="targetPosition" />
7
7
  </template>
8
8
 
9
9
  <script>
10
10
  import {Handle} from "@vue-flow/core";
11
- import Circle from "vue-material-design-icons/Circle.vue"
11
+ import CircleIcon from "vue-material-design-icons/Circle.vue"
12
12
 
13
13
  export default {
14
14
  name: "Dot",
15
- components: {Handle, Circle},
15
+ components: {Handle, CircleIcon},
16
16
  inheritAttrs: false,
17
17
  props: {
18
18
  data: {
@@ -72,9 +72,10 @@
72
72
  >
73
73
  <tooltip :title="$t('add task')">
74
74
  <AddTaskButton
75
- v-if="!data.disabled && data.haveAdd != undefined"
75
+ v-if="data.haveAdd"
76
76
  :add-task="true"
77
77
  @click="$emit(EVENTS.ADD_TASK, data.haveAdd)"
78
+ :class="{'text-danger': data.color}"
78
79
  />
79
80
  </tooltip>
80
81
 
@@ -2,31 +2,31 @@
2
2
  <Handle type="source" :position="sourcePosition" />
3
3
  <basic-node
4
4
  :id="id"
5
- :data="data"
5
+ :data="dataWithLink"
6
6
  :state="state"
7
7
  :class="classes"
8
8
  @show-description="forwardEvent(EVENTS.SHOW_DESCRIPTION, $event)"
9
- @expand="forwardEvent(EVENTS.EXPAND, id)"
9
+ @expand="forwardEvent(EVENTS.EXPAND, expandData)"
10
10
  @open-link="forwardEvent(EVENTS.OPEN_LINK, $event)"
11
11
  @mouseover="forwardEvent(EVENTS.MOUSE_OVER, $event)"
12
12
  @mouseleave="forwardEvent(EVENTS.MOUSE_LEAVE)"
13
13
  >
14
14
  <template #content>
15
- <execution-informations v-if="execution" :execution="execution" :task="data.node.task" :color="color" />
15
+ <execution-informations v-if="taskExecution" :execution="taskExecution" :task="data.node.task" :color="color" />
16
16
  </template>
17
17
  <template #badge-button-before>
18
18
  <span
19
- v-if="execution"
19
+ v-if="taskExecution"
20
20
  class="rounded-button"
21
21
  :class="[`bg-${color}`]"
22
- @click="$emit(EVENTS.SHOW_LOGS, {id, taskRuns})"
22
+ @click="$emit(EVENTS.SHOW_LOGS, {id: taskId, execution: taskExecution, taskRuns})"
23
23
  >
24
24
  <tooltip :title="$t('show task logs')">
25
25
  <TextBoxSearch class="button-icon" alt="Show logs" />
26
26
  </tooltip>
27
27
  </span>
28
28
  <span
29
- v-if="!execution && !data.isReadOnly && data.isFlowable"
29
+ v-if="!taskExecution && !data.isReadOnly && data.isFlowable"
30
30
  class="rounded-button"
31
31
  :class="[`bg-${color}`]"
32
32
  @click="$emit(EVENTS.ADD_ERROR, {task: data.node.task})"
@@ -36,7 +36,7 @@
36
36
  </tooltip>
37
37
  </span>
38
38
  <span
39
- v-if="!execution && !data.isReadOnly"
39
+ v-if="!taskExecution && !data.isReadOnly"
40
40
  class="rounded-button"
41
41
  :class="[`bg-${color}`]"
42
42
  @click="$emit(EVENTS.EDIT, {task: data.node.task, section: SECTIONS.TASKS})"
@@ -46,10 +46,10 @@
46
46
  </tooltip>
47
47
  </span>
48
48
  <span
49
- v-if="!execution && !data.isReadOnly"
49
+ v-if="!taskExecution && !data.isReadOnly"
50
50
  class="rounded-button"
51
51
  :class="[`bg-${color}`]"
52
- @click="$emit(EVENTS.DELETE, {id, section: SECTIONS.TASKS})"
52
+ @click="$emit(EVENTS.DELETE, {id: taskId, section: SECTIONS.TASKS})"
53
53
  >
54
54
  <tooltip :title="$t('delete')">
55
55
  <Delete class="button-icon" alt="Delete task" />
@@ -71,8 +71,9 @@
71
71
  import Delete from "vue-material-design-icons/Delete.vue";
72
72
  import TextBoxSearch from "vue-material-design-icons/TextBoxSearch.vue";
73
73
  import AlertOutline from "vue-material-design-icons/AlertOutline.vue"
74
- import {mapState} from "vuex";
74
+ import {mapGetters, mapState} from "vuex";
75
75
  import Tooltip from "../misc/Tooltip.vue"
76
+ import Utils from "../../utils/Utils.js";
76
77
 
77
78
  export default {
78
79
  name: "Task",
@@ -88,6 +89,7 @@
88
89
  inheritAttrs: false,
89
90
  computed: {
90
91
  ...mapState("execution", ["execution"]),
92
+ ...mapGetters("execution", ["subflowsExecutions"]),
91
93
  SECTIONS() {
92
94
  return SECTIONS
93
95
  },
@@ -97,11 +99,23 @@
97
99
  color() {
98
100
  return this.data.color ?? "primary"
99
101
  },
102
+ taskId() {
103
+ return Utils.afterLastDot(this.id);
104
+ },
100
105
  taskRunList() {
101
- return this.execution && this.execution.taskRunList ? this.execution.taskRunList : []
106
+ return this.taskExecution && this.taskExecution.taskRunList ? this.taskExecution.taskRunList : []
107
+ },
108
+ taskExecution() {
109
+ const executionId = this.data.executionId;
110
+ if(executionId) {
111
+ return executionId === this.execution?.id ? this.execution
112
+ : Object.values(this.subflowsExecutions).filter(execution => execution.id === this.data.executionId)?.[0];
113
+ }
114
+
115
+ return undefined;
102
116
  },
103
117
  taskRuns() {
104
- return this.taskRunList.filter(t => t.taskId === this.data.node.task.id)
118
+ return this.taskRunList.filter(t => t.taskId === Utils.afterLastDot(this.data.node.task.id))
105
119
  },
106
120
  state() {
107
121
  if (!this.taskRuns) {
@@ -138,9 +152,31 @@
138
152
  },
139
153
  classes() {
140
154
  return {
141
- "execution-no-taskrun": this.execution && this.taskRuns && this.taskRuns.length === 0,
155
+ "execution-no-taskrun": this.taskExecution && this.taskRuns && this.taskRuns.length === 0,
156
+ }
157
+ },
158
+ expandData() {
159
+ return {
160
+ id: this.id,
161
+ type: this.data.node.task.type
142
162
  }
143
163
  },
164
+ dataWithLink() {
165
+ if(this.data.node.type.endsWith("SubflowGraphTask")){
166
+ return {
167
+ ...this.data,
168
+ link: {
169
+ namespace: this.data.node.task.namespace,
170
+ id: this.data.node.task.flowId,
171
+ executionId: this.taskExecution?.taskRunList
172
+ .filter(taskRun => taskRun.taskId === this.data.node.task.id && taskRun.outputs?.executionId)
173
+ ?.[0]?.outputs?.executionId
174
+ }
175
+ }
176
+ }
177
+
178
+ return this.data;
179
+ }
144
180
  },
145
181
  emits: [
146
182
  EVENTS.EXPAND,
@@ -5,7 +5,7 @@
5
5
  :data="data"
6
6
  :color="color"
7
7
  @show-description="forwardEvent(EVENTS.SHOW_DESCRIPTION, $event)"
8
- @expand="forwardEvent(EVENTS.EXPAND, id)"
8
+ @expand="forwardEvent(EVENTS.EXPAND, {id})"
9
9
  >
10
10
  <template #badge-button-before>
11
11
  <span
@@ -1,28 +1,16 @@
1
1
  <script setup>
2
- import {
3
- ref,
4
- watch,
5
- nextTick,
6
- onMounted
7
- } from "vue";
8
- import {
9
- ClusterNode,
10
- DotNode,
11
- TaskNode,
12
- TriggerNode,
13
- CollapsedClusterNode,
14
- EdgeNode,
15
- } from "../index.js";
2
+ import {computed, nextTick, onMounted, ref, watch} from "vue";
3
+ import {ClusterNode, CollapsedClusterNode, DotNode, EdgeNode, TaskNode, TriggerNode,} from "../index.js";
16
4
  import {useVueFlow, VueFlow} from "@vue-flow/core";
17
5
  import {ControlButton, Controls} from "@vue-flow/controls";
18
6
  import SplitCellsVertical from "../../assets/icons/SplitCellsVertical.vue";
19
7
  import SplitCellsHorizontal from "../../assets/icons/SplitCellsHorizontal.vue";
20
8
  import {cssVariable} from "../../utils/global.js";
21
9
  import {VueFlowUtils, YamlUtils} from "../../index.js";
22
- import Utils from "../../utils/Utils.js";
23
10
  import VueflowUtils from "../../utils/VueFlowUtils.js";
24
- import {CLUSTER_UID_SEPARATOR, EVENTS} from "../../utils/constants.js";
11
+ import {CLUSTER_PREFIX, EVENTS} from "../../utils/constants.js";
25
12
  import {Background} from "@vue-flow/background";
13
+ import Utils from "../../utils/Utils.js";
26
14
 
27
15
  const props = defineProps({
28
16
  id: {
@@ -41,10 +29,6 @@
41
29
  type: Boolean,
42
30
  default: false,
43
31
  },
44
- flowables: {
45
- type: Array,
46
- default: () => [],
47
- },
48
32
  source: {
49
33
  type: String,
50
34
  default: undefined,
@@ -68,6 +52,10 @@
68
52
  required: false,
69
53
  default: undefined
70
54
  },
55
+ expandedSubflows: {
56
+ type: Array,
57
+ default: () => []
58
+ }
71
59
  });
72
60
 
73
61
  const dragging = ref(false);
@@ -75,7 +63,7 @@
75
63
  const {getNodes, onNodeDrag, onNodeDragStart, onNodeDragStop, fitView, setElements} = useVueFlow({id: props.id});
76
64
  const edgeReplacer = ref({});
77
65
  const hiddenNodes = ref([]);
78
- const collapsed = ref([]);
66
+ const collapsed = ref(new Set());
79
67
  const clusterToNode = ref([])
80
68
 
81
69
  const emit = defineEmits(
@@ -90,11 +78,12 @@
90
78
  "toggle-orientation",
91
79
  "loading",
92
80
  "swapped-task",
93
- "message"
81
+ "message",
82
+ "expand-subflow"
94
83
  ]
95
84
  )
96
85
 
97
- onMounted( () => {
86
+ onMounted(() => {
98
87
  generateGraph();
99
88
  })
100
89
 
@@ -123,8 +112,7 @@
123
112
  collapsed.value,
124
113
  clusterToNode.value,
125
114
  props.isReadOnly,
126
- props.isAllowedEdit,
127
- flowables()
115
+ props.isAllowedEdit
128
116
  );
129
117
  setElements(elements);
130
118
  fitView();
@@ -176,7 +164,10 @@
176
164
  const taskNode2 = e.intersections.find(n => n.type === "task");
177
165
  if (taskNode2) {
178
166
  try {
179
- emit("swapped-task", YamlUtils.swapTasks(props.source, taskNode1.id, taskNode2.id))
167
+ emit("swapped-task", {
168
+ newSource: YamlUtils.swapTasks(props.source, Utils.afterLastDot(taskNode1.id), Utils.afterLastDot(taskNode2.id)),
169
+ swappedTasks: [taskNode1.id, taskNode2.id]
170
+ })
180
171
  } catch (e) {
181
172
  emit("message", {
182
173
  variant: "error",
@@ -196,10 +187,19 @@
196
187
  lastPosition.value = null;
197
188
  })
198
189
 
190
+ const subflowPrefixes = computed(() => {
191
+ if(!props.flowGraph) {
192
+ return [];
193
+ }
194
+
195
+ return props.flowGraph.clusters.filter(cluster => cluster.cluster.type.endsWith("SubflowGraphCluster"))
196
+ .map(cluster => cluster.cluster.taskNode.uid + ".");
197
+ })
198
+
199
199
  onNodeDrag((e) => {
200
200
  resetNodesStyle();
201
201
  getNodes.value.filter(n => n.id !== e.node.id).forEach(n => {
202
- if (n.type === "trigger" || (n.type === "task" && YamlUtils.isParentChildrenRelation(props.source, n.id, e.node.id))) {
202
+ if (n.type === "trigger" || (n.type === "task" && (n.id.startsWith(e.node.id + ".") || e.node.id.startsWith(n.id + "."))) || subflowPrefixes.value.some(subflowPrefix => n.id.startsWith(subflowPrefix))) {
203
203
  n.style = {...n.style, opacity: "0.5"}
204
204
  } else {
205
205
  n.style = {...n.style, opacity: "1"}
@@ -218,11 +218,15 @@
218
218
  })
219
219
 
220
220
  const checkIntersections = (intersections, node) => {
221
- const tasksMeet = intersections.filter(n => n.type === "task").map(n => n.id);
221
+ const tasksMeet = intersections.filter(n => n.type === "task").map(n => Utils.afterLastDot(n.id));
222
222
  if (tasksMeet.length > 1) {
223
223
  return "toomuchtaskerror";
224
224
  }
225
- if (tasksMeet.length === 1 && YamlUtils.isParentChildrenRelation(props.source, tasksMeet[0], node.id)) {
225
+ try {
226
+ if (tasksMeet.length === 1 && YamlUtils.isParentChildrenRelation(props.source, Utils.afterLastDot(tasksMeet[0]), Utils.afterLastDot(node.id))) {
227
+ return "parentchildrenerror";
228
+ }
229
+ } catch (e) {
226
230
  return "parentchildrenerror";
227
231
  }
228
232
  if (intersections.filter(n => n.type === "trigger").length > 0) {
@@ -232,13 +236,12 @@
232
236
  }
233
237
 
234
238
  const collapseCluster = (clusterUid, regenerate, recursive) => {
235
-
236
- const cluster = props.flowGraph.clusters.find(cluster => cluster.cluster.uid === clusterUid)
237
- const nodeId = clusterUid === "Triggers" ? "Triggers" : Utils.splitFirst(clusterUid, CLUSTER_UID_SEPARATOR);
238
- collapsed.value = collapsed.value.concat([nodeId])
239
+ const cluster = props.flowGraph.clusters.find(cluster => cluster.cluster.uid.endsWith(clusterUid));
240
+ const nodeId = clusterUid === "root.Triggers" ? "root.Triggers" : clusterUid.replace(CLUSTER_PREFIX, "");
241
+ collapsed.value.add(nodeId)
239
242
 
240
243
  hiddenNodes.value = hiddenNodes.value.concat(cluster.nodes.filter(e => e !== nodeId || recursive));
241
- if (clusterUid !== "Triggers") {
244
+ if (clusterUid !== "root.Triggers") {
242
245
  hiddenNodes.value = hiddenNodes.value.concat([cluster.cluster.uid])
243
246
  edgeReplacer.value = {
244
247
  ...edgeReplacer.value,
@@ -265,21 +268,21 @@
265
268
  }
266
269
  }
267
270
 
268
- const expand = (taskId) => {
269
- edgeReplacer.value = {}
270
- hiddenNodes.value = []
271
- clusterToNode.value = []
272
- collapsed.value = collapsed.value.filter(n => n != taskId)
271
+ const expand = (expandData) => {
272
+ if (expandData.type === "io.kestra.core.tasks.flows.Flow" && !props.expandedSubflows.includes(expandData.id)) {
273
+ forwardEvent("expand-subflow", [...props.expandedSubflows, expandData.id]);
274
+ return;
275
+ }
276
+ edgeReplacer.value = {};
277
+ hiddenNodes.value = [];
278
+ clusterToNode.value = [];
279
+ collapsed.value.delete(expandData.id);
273
280
 
274
- collapsed.value.forEach(n => collapseCluster(CLUSTER_UID_SEPARATOR + n, false, false))
281
+ collapsed.value.forEach(n => collapseCluster(n, false, false));
275
282
 
276
283
  generateGraph();
277
284
  }
278
285
 
279
- const flowables = () => {
280
- return props.flowGraph && props.flowGraph.flowables ? props.flowGraph.flowables : [];
281
- }
282
-
283
286
  const darkTheme = document.getElementsByTagName("html")[0].className.indexOf("dark") >= 0;
284
287
 
285
288
  </script>
@@ -311,12 +314,12 @@
311
314
  <template #node-task="taskProps">
312
315
  <TaskNode
313
316
  v-bind="taskProps"
314
- @edit="forwardEvent('edit', $event)"
315
- @delete="forwardEvent('delete', $event)"
317
+ @edit="forwardEvent(EVENTS.EDIT, $event)"
318
+ @delete="forwardEvent(EVENTS.DELETE, $event)"
316
319
  @expand="expand($event)"
317
- @open-link="forwardEvent('open-link', $event)"
318
- @show-logs="forwardEvent('show-logs', $event)"
319
- @show-description="forwardEvent('show-description', $event)"
320
+ @open-link="forwardEvent(EVENTS.OPEN_LINK, $event)"
321
+ @show-logs="forwardEvent(EVENTS.SHOW_LOGS, $event)"
322
+ @show-description="forwardEvent(EVENTS.SHOW_DESCRIPTION, $event)"
320
323
  @mouseover="onMouseOver($event)"
321
324
  @mouseleave="onMouseLeave()"
322
325
  @add-error="forwardEvent('on-add-flowable-error', $event)"
@@ -328,9 +331,9 @@
328
331
  v-bind="triggerProps"
329
332
  :is-read-only="isReadOnly"
330
333
  :is-allowed-edit="isAllowedEdit"
331
- @delete="forwardEvent('delete', $event)"
332
- @edit="forwardEvent('edit', $event)"
333
- @show-description="forwardEvent('show-description', $event)"
334
+ @delete="forwardEvent(EVENTS.DELETE, $event)"
335
+ @edit="forwardEvent(EVENTS.EDIT, $event)"
336
+ @show-description="forwardEvent(EVENTS.SHOW_DESCRIPTION, $event)"
334
337
  />
335
338
  </template>
336
339
 
@@ -345,8 +348,7 @@
345
348
  <EdgeNode
346
349
  v-bind="EdgeProps"
347
350
  :yaml-source="source"
348
- :flowables-ids="flowables"
349
- @add-task="forwardEvent('add-task', $event)"
351
+ @add-task="forwardEvent(EVENTS.ADD_TASK, $event)"
350
352
  :is-read-only="isReadOnly"
351
353
  :is-allowed-edit="isAllowedEdit"
352
354
  />
package/src/scss/app.scss CHANGED
@@ -46,8 +46,8 @@
46
46
  }
47
47
 
48
48
  marker[id*='id=marker-danger&type=arrowclosed'] polyline {
49
- stroke: #9A8EB4;
50
- fill: #9A8EB4;
49
+ stroke: $red;
50
+ fill: $red;
51
51
  }
52
52
 
53
53
  .vue-flow__handle {
@@ -1,4 +1,5 @@
1
1
  import humanizeDuration from "humanize-duration";
2
+ import moment from "moment";
2
3
 
3
4
  const humanizeDurationLanguages = {
4
5
  "en" : {
@@ -44,4 +45,8 @@ export default class Utils {
44
45
 
45
46
  return humanizeDuration(value * 1000, options).replace(/\.([0-9])s$/i, ".$10s")
46
47
  }
48
+
49
+ static afterLastDot(str) {
50
+ return str.split(".").pop();
51
+ }
47
52
  }
@@ -2,8 +2,9 @@ import {MarkerType, Position, useVueFlow} from "@vue-flow/core"
2
2
  import dagre from "dagre";
3
3
  import {YamlUtils} from "../index.js";
4
4
  import Utils from "./Utils.js";
5
- import {CLUSTER_UID_SEPARATOR, NODE_SIZES} from "./constants.js";
5
+ import {CLUSTER_PREFIX, NODE_SIZES} from "./constants.js";
6
6
 
7
+ const TRIGGERS_NODE_UID = "root.Triggers";
7
8
  export default class VueFlowUtils {
8
9
 
9
10
  static predecessorsEdge(vueFlowId, nodeUid) {
@@ -87,7 +88,7 @@ export default class VueFlowUtils {
87
88
  ])
88
89
  }
89
90
 
90
- static generateDagreGraph(flowGraph, hiddenNodes, isHorizontal, clusterCollapseToNode, edgeReplacer, collapsed, clusterToNode) {
91
+ static generateDagreGraph(flowGraph, hiddenNodes, isHorizontal, clustersWithoutRootNode, edgeReplacer, collapsed, clusterToNode) {
91
92
  const dagreGraph = new dagre.graphlib.Graph({compound: true})
92
93
  dagreGraph.setDefaultEdgeLabel(() => ({}))
93
94
  dagreGraph.setGraph({rankdir: isHorizontal ? "LR" : "TB"})
@@ -102,9 +103,10 @@ export default class VueFlowUtils {
102
103
  }
103
104
 
104
105
  for (let cluster of (flowGraph.clusters || [])) {
105
- if (clusterCollapseToNode.includes(cluster.cluster.uid) && collapsed.includes(cluster.cluster.uid)) {
106
- const node = {uid: cluster.cluster.uid, type: "collapsedcluster"};
107
- dagreGraph.setNode(cluster.cluster.uid, {
106
+ const nodeUid = cluster.cluster.uid.replace(CLUSTER_PREFIX, "");
107
+ if (clustersWithoutRootNode.includes(cluster.cluster.uid) && collapsed.has(nodeUid)) {
108
+ const node = {uid: nodeUid, type: "collapsedcluster"};
109
+ dagreGraph.setNode(nodeUid, {
108
110
  width: this.getNodeWidth(node),
109
111
  height: this.getNodeHeight(node)
110
112
  });
@@ -156,15 +158,15 @@ export default class VueFlowUtils {
156
158
  }
157
159
 
158
160
  static getNodeHeight(node) {
159
- return this.isTaskNode(node) || this.isTriggerNode(node) ? NODE_SIZES.TASK_HEIGHT : this.isCollapsedCluster(node) ? NODE_SIZES.COLLAPSED_CLUSTER_HEIGHT : NODE_SIZES.DOT_HEIGHT;
161
+ return this.isTaskNode(node) || this.isTriggerNode(node) ? NODE_SIZES.TASK_HEIGHT : this.isCollapsedCluster(node) ? NODE_SIZES.COLLAPSED_CLUSTER_HEIGHT : NODE_SIZES.DOT_HEIGHT;
160
162
  }
161
163
 
162
164
  static isTaskNode(node) {
163
- return node.task !== undefined && (node.type === "io.kestra.core.models.hierarchies.GraphTask")
165
+ return node.task !== undefined && ["GraphTask", "SubflowGraphTask"].some(t => node.type.endsWith(t));
164
166
  }
165
167
 
166
168
  static isTriggerNode(node) {
167
- return node.trigger !== undefined && (node.type === "io.kestra.core.models.hierarchies.GraphTrigger");
169
+ return node.trigger !== undefined && node.type.endsWith("GraphTrigger");
168
170
  }
169
171
 
170
172
  static isCollapsedCluster(node) {
@@ -199,98 +201,107 @@ export default class VueFlowUtils {
199
201
  return source ? YamlUtils.flowHaveTasks(source) : false;
200
202
  }
201
203
 
202
- static linkDatas(task, execution) {
203
- const data = {id: task.flowId, namespace: task.namespace}
204
- if (execution) {
205
- const taskrun = execution.taskRunList.find(r => r.taskId === task.id && r.outputs.executionId)
206
- if (taskrun) {
207
- data.executionId = taskrun?.outputs.executionId
208
- }
204
+ static nodeColor(node, collapsed) {
205
+ if (node.uid === TRIGGERS_NODE_UID) {
206
+ return "success";
209
207
  }
210
- return data
211
- }
212
208
 
213
- static nodeColor(node, collapsed, flowSource) {
214
- if (this.isTaskNode(node)) {
215
- if (YamlUtils.isTaskError(flowSource, node.task.id)) {
216
- return "danger"
217
- }
218
- if (collapsed.includes(node.uid)) {
219
- return "blue";
220
- }
221
- if (node.task.type === "io.kestra.core.tasks.flows.Flow") {
222
- return "primary"
223
- }
224
- } else if (this.isTriggerNode(node) || this.isCollapsedCluster(node)) {
225
- return "success";
209
+ if (node.type === "dot") {
210
+ return null;
226
211
  }
227
- return "default"
228
- }
229
212
 
230
- static getClusterTaskIdWithEndNodeUid (nodeUid, flowGraph) {
231
- const cluster = flowGraph.clusters.find(cluster => cluster.end === nodeUid);
232
- if (cluster) {
213
+ if (this.isTriggerNode(node) || this.isCollapsedCluster(node)) {
214
+ return "success";
215
+ }
233
216
 
234
- return Utils.splitFirst(cluster.cluster.uid, CLUSTER_UID_SEPARATOR);
217
+ if (node.type.endsWith("SubflowGraphTask")) {
218
+ return "primary";
219
+ }
220
+ if (node.error) {
221
+ return "danger";
222
+ }
223
+ if (collapsed.has(node.uid)) {
224
+ return "blue";
235
225
  }
236
226
 
237
- return undefined;
227
+ return "default";
238
228
  }
239
229
 
240
- static haveAdd(edge, flowSource, flowGraph, flowables) {
241
- if (edge.target === YamlUtils.getFirstTask(flowSource)) {
242
- return [YamlUtils.getNextTaskId(edge.target, flowSource, flowGraph), "before"];
243
- }
244
- if (YamlUtils.isTaskParallel(edge.target, flowSource) || YamlUtils.isTrigger(flowSource, edge.target) || YamlUtils.isTrigger(flowSource, edge.source)) {
230
+ static haveAdd(edge, nodeByUid, clustersRootTaskUids, readOnlyUidPrefixes) {
231
+ // prevent subflow edit (edge = subflowNode -> subflowNode)
232
+ if(readOnlyUidPrefixes.some(prefix => edge.source.startsWith(prefix) && edge.target.startsWith(prefix))) {
245
233
  return undefined;
246
234
  }
247
- if (YamlUtils.extractTask(flowSource, edge.source) && YamlUtils.extractTask(flowSource, edge.target)) {
248
- return [edge.source, "after"];
235
+
236
+ // edge = clusterRoot -> clusterRootTask
237
+ if (clustersRootTaskUids.includes(edge.target)) {
238
+ return undefined;
249
239
  }
250
- // Check if edge is an ending flowable
251
- // If true, enable add button to add a task
252
- // under the flowable task
253
- if (edge.source.endsWith("_end") && edge.target.endsWith("_end")) {
254
- // Cluster uid contains the flowable task id
255
- // So we look for the cluster having this end edge
256
- // to return his flowable id
257
- return [this.getClusterTaskIdWithEndNodeUid(edge.source, flowGraph), "after"];
240
+
241
+ // edge = Triggers cluster -> something || edge = something -> Triggers cluster
242
+ if(edge.source.startsWith(TRIGGERS_NODE_UID) || edge.target.startsWith(TRIGGERS_NODE_UID)) {
243
+ return undefined;
258
244
  }
259
- if (flowables.includes(edge.source)) {
260
- return [YamlUtils.getNextTaskId(edge.target, flowSource, flowGraph), "before"];
245
+
246
+ const dotSplitTarget = edge.target.split(".");
247
+ dotSplitTarget.pop();
248
+ const targetNodeClusterUid = dotSplitTarget.join(".");
249
+ const clusterRootTaskId = Utils.afterLastDot(targetNodeClusterUid);
250
+
251
+ // edge = task of parallel -> end of parallel, we only add + symbol right after the parallel cluster root task node
252
+ const targetNode = nodeByUid[edge.target];
253
+ if(targetNode.type.endsWith("GraphClusterEnd") && nodeByUid[targetNodeClusterUid]?.task?.type?.endsWith("Parallel")) {
254
+ return undefined;
261
255
  }
262
- if (YamlUtils.extractTask(flowSource, edge.source) && edge.target.endsWith("_end")) {
263
- return [edge.source, "after"];
256
+
257
+ // edge = something -> clusterRoot ==> we insert before the cluster
258
+ // clusterUid = clusterTraversalPrefix.{rootTaskUid}
259
+ // clusterRoot.uid = clusterUid.someUid = clusterTraversalPrefix.{rootTaskUid}.someUid
260
+ if(targetNode.type.endsWith("GraphClusterRoot")) {
261
+ return [clusterRootTaskId, "before"];
264
262
  }
265
- if (YamlUtils.extractTask(flowSource, edge.source) && edge.target.endsWith("_start")) {
266
- return [edge.source, "after"];
263
+
264
+ const sourceIsEndOfCluster = nodeByUid[edge.source].type.endsWith("GraphClusterEnd");
265
+ // edge = clusterTask -> clusterEnd ==> we insert after the previous task
266
+ if(!sourceIsEndOfCluster && targetNode.type.endsWith("GraphClusterEnd")) {
267
+ return [Utils.afterLastDot(edge.source), "after"]
267
268
  }
268
- if (YamlUtils.extractTask(flowSource, edge.target) && edge.source.endsWith("_end")) {
269
- return [edge.target, "before"];
269
+
270
+ // edge = cluster1End -> something ==> we insert after cluster1
271
+ if(sourceIsEndOfCluster) {
272
+ const dotSplitSource = edge.source.split(".");
273
+ return [dotSplitSource[dotSplitSource.length - 2], "after"];
270
274
  }
271
275
 
272
- return undefined;
276
+ return [Utils.afterLastDot(edge.target), "before"];
273
277
  }
274
278
 
275
- static getEdgeColor(edge, flowSource) {
276
- if (YamlUtils.isTaskError(flowSource, edge.source) || YamlUtils.isTaskError(flowSource, edge.target)) {
279
+ static getEdgeColor(edge, nodeByUid) {
280
+ const sourceNode = nodeByUid[edge.source];
281
+ const targetNode = nodeByUid[edge.target];
282
+ if (sourceNode.error && targetNode.error) {
277
283
  return "danger"
278
284
  }
279
- if (this.isClusterError(flowSource, edge.source) || this.isClusterError(flowSource, edge.target)) {
285
+
286
+ if (sourceNode.type.endsWith("GraphClusterRoot") && targetNode.error) {
287
+ return "danger"
288
+ }
289
+
290
+ if (sourceNode.error && targetNode.type.endsWith("GraphClusterEnd")) {
280
291
  return "danger"
281
292
  }
293
+
282
294
  return null;
283
295
  }
284
296
 
285
- static generateGraph(vueFlowId, flowId, namespace, flowGraph, flowSource, hiddenNodes, isHorizontal, edgeReplacer, collapsed, clusterToNode, isReadOnly, isAllowedEdit, flowables) {
297
+ static generateGraph(vueFlowId, flowId, namespace, flowGraph, flowSource, hiddenNodes, isHorizontal, edgeReplacer, collapsed, clusterToNode, isReadOnly, isAllowedEdit) {
286
298
  const elements = [];
287
299
 
288
- const clusterCollapseToNode = ["Triggers"];
300
+ const clustersWithoutRootNode = [CLUSTER_PREFIX + TRIGGERS_NODE_UID];
289
301
 
290
302
  if (!flowGraph || !this.flowHaveTasks(flowSource)) {
291
303
  elements.push({
292
304
  id: "start",
293
- label: "",
294
305
  type: "dot",
295
306
  position: {x: 0, y: 0},
296
307
  style: {
@@ -304,7 +315,6 @@ export default class VueFlowUtils {
304
315
  })
305
316
  elements.push({
306
317
  id: "end",
307
- label: "",
308
318
  type: "dot",
309
319
  position: isHorizontal ? {x: 50, y: 0} : {x: 0, y: 50},
310
320
  style: {
@@ -335,92 +345,98 @@ export default class VueFlowUtils {
335
345
 
336
346
  return;
337
347
  }
338
- const dagreGraph = this.generateDagreGraph(flowGraph, hiddenNodes, isHorizontal, clusterCollapseToNode, edgeReplacer, collapsed, clusterToNode);
339
- const clusters = {};
340
- for (let cluster of (flowGraph.clusters || [])) {
341
- if (!edgeReplacer[cluster.cluster.uid] && !collapsed.includes(cluster.cluster.uid)) {
348
+
349
+ const dagreGraph = this.generateDagreGraph(flowGraph, hiddenNodes, isHorizontal, clustersWithoutRootNode, edgeReplacer, collapsed, clusterToNode);
350
+
351
+ const clusterByNodeUid = {};
352
+
353
+ const clusters = flowGraph.clusters || [];
354
+ const rawClusters = clusters.map(c => c.cluster);
355
+ const readOnlyUidPrefixes = rawClusters.filter(c => c.type.endsWith("SubflowGraphCluster")).map(c => c.taskNode.uid);
356
+ for (let cluster of clusters) {
357
+ if (!edgeReplacer[cluster.cluster.uid] && !collapsed.has(cluster.cluster.uid)) {
358
+ if (cluster.cluster.taskNode?.task?.type === "io.kestra.core.tasks.flows.Dag") {
359
+ readOnlyUidPrefixes.push(cluster.cluster.taskNode.uid);
360
+ }
361
+
342
362
  for (let nodeUid of cluster.nodes) {
343
- clusters[nodeUid] = cluster.cluster;
363
+ clusterByNodeUid[nodeUid] = cluster.cluster;
344
364
  }
345
365
 
346
366
  const clusterUid = cluster.cluster.uid;
347
- const isClusterError = YamlUtils.isTaskError(flowSource, Utils.splitFirst(clusterUid, CLUSTER_UID_SEPARATOR))
348
367
  const dagreNode = dagreGraph.node(clusterUid)
349
368
  const parentNode = cluster.parents ? cluster.parents[cluster.parents.length - 1] : undefined;
350
369
 
370
+ const clusterColor = this.computeClusterColor(cluster.cluster);
371
+
351
372
  elements.push({
352
373
  id: clusterUid,
353
374
  type: "cluster",
354
375
  parentNode: parentNode,
355
376
  position: this.getNodePosition(dagreNode, parentNode ? dagreGraph.node(parentNode) : undefined),
356
377
  style: {
357
- width: clusterUid === "Triggers" && isHorizontal ? NODE_SIZES.TRIGGER_CLUSTER_WIDTH + "px" : dagreNode.width + "px",
358
- height: clusterUid === "Triggers" && !isHorizontal ? NODE_SIZES.TRIGGER_CLUSTER_HEIGHT + "px" : dagreNode.height + "px"
378
+ width: clusterUid === TRIGGERS_NODE_UID && isHorizontal ? NODE_SIZES.TRIGGER_CLUSTER_WIDTH + "px" : dagreNode.width + "px",
379
+ height: clusterUid === TRIGGERS_NODE_UID && !isHorizontal ? NODE_SIZES.TRIGGER_CLUSTER_HEIGHT + "px" : dagreNode.height + "px"
359
380
  },
360
381
  data: {
361
382
  collapsable: true,
362
- color: clusterUid === "Triggers" ? "success" : isClusterError ? "danger" : "blue"
383
+ color: clusterColor,
384
+ taskNode: cluster.cluster.taskNode
363
385
  },
364
- class: `bg-light-${clusterUid === "Triggers" ? "success" : isClusterError ? "danger" : "blue"}-border rounded p-2`,
386
+ class: `bg-light-${clusterColor}-border rounded p-2`,
365
387
  })
366
388
  }
367
389
  }
368
390
 
369
- let disabledLowCode = [];
391
+ const nodeByUid = {};
370
392
  for (const node of flowGraph.nodes.concat(clusterToNode)) {
393
+ nodeByUid[node.uid] = node;
394
+
371
395
  if (!hiddenNodes.includes(node.uid)) {
372
396
  const dagreNode = dagreGraph.node(node.uid);
373
397
  let nodeType = "task";
374
- if (node.type.includes("GraphClusterEnd")) {
375
- nodeType = "dot";
376
- } else if (clusters[node.uid] === undefined && node.type.includes("GraphClusterRoot")) {
377
- nodeType = "dot";
378
- } else if (node.type.includes("GraphClusterRoot")) {
398
+ if (this.isClusterRootOrEnd(node)) {
379
399
  nodeType = "dot";
380
400
  } else if (node.type.includes("GraphTrigger")) {
381
401
  nodeType = "trigger";
382
402
  } else if (node.type === "collapsedcluster") {
383
403
  nodeType = "collapsedcluster";
384
404
  }
385
- // Disable interaction for Dag task
386
- // because our low code editor can not handle it for now
387
- if (this.isTaskNode(node) && node.task.type === "io.kestra.core.tasks.flows.Dag") {
388
- disabledLowCode.push(node.task.id);
389
- YamlUtils.getChildrenTasks(flowSource, node.task.id).forEach(child => {
390
- disabledLowCode.push(child);
391
- })
392
- }
393
405
 
394
- const taskId = node?.task?.id;
406
+ const color = this.nodeColor(node, collapsed, flowSource);
407
+ const isReadOnlyTask = isReadOnly || readOnlyUidPrefixes.some(prefix => node.uid.startsWith(prefix + "."));
395
408
  elements.push({
396
409
  id: node.uid,
397
- label: this.isTaskNode(node) ? taskId : "",
398
410
  type: nodeType,
399
- position: this.getNodePosition(dagreNode, clusters[node.uid] ? dagreGraph.node(clusters[node.uid].uid) : undefined),
411
+ position: this.getNodePosition(dagreNode, clusterByNodeUid[node.uid] ? dagreGraph.node(clusterByNodeUid[node.uid].uid) : undefined),
400
412
  style: {
401
413
  width: this.getNodeWidth(node) + "px",
402
414
  height: this.getNodeHeight(node) + "px"
403
415
  },
404
416
  sourcePosition: isHorizontal ? Position.Right : Position.Bottom,
405
417
  targetPosition: isHorizontal ? Position.Left : Position.Top,
406
- parentNode: clusters[node.uid] ? clusters[node.uid].uid : undefined,
407
- draggable: nodeType === "task" && !isReadOnly && this.isTaskNode(node) ? !disabledLowCode.includes(taskId) : false,
418
+ parentNode: clusterByNodeUid[node.uid] ? clusterByNodeUid[node.uid].uid : undefined,
419
+ draggable: nodeType === "task" ? !isReadOnlyTask : false,
408
420
  data: {
409
421
  node: node,
410
- namespace: namespace,
411
- flowId: flowId,
412
- isFlowable: this.isTaskNode(node) ? flowables.includes(taskId) : false,
413
- color: nodeType != "dot" ? this.nodeColor(node, collapsed, flowSource) : null,
414
- expandable: taskId ? edgeReplacer[CLUSTER_UID_SEPARATOR + taskId] !== undefined : this.isCollapsedCluster(node),
415
- isReadOnly: isReadOnly,
416
- link: node.task?.type === "io.kestra.core.tasks.flows.Flow" ? this.linkDatas(node.task) : false,
417
- iconComponent: this.isCollapsedCluster(node) ? "webhook" : null
422
+ namespace: clusterByNodeUid[node.uid]?.taskNode?.task?.namespace ?? namespace,
423
+ flowId: clusterByNodeUid[node.uid]?.taskNode?.task?.flowId ?? flowId,
424
+ isFlowable: clusterByNodeUid[node.uid]?.uid === CLUSTER_PREFIX + node.uid && !node.type.endsWith("SubflowGraphTask"),
425
+ color: color,
426
+ expandable: this.isExpandableTask(node, clusterByNodeUid, edgeReplacer),
427
+ isReadOnly: isReadOnlyTask,
428
+ iconComponent: this.isCollapsedCluster(node) ? "webhook" : null,
429
+ executionId: node.executionId
418
430
  },
419
- class: node.type === "collapsedcluster" ? `bg-light-${node.uid === "Triggers" ? "success" : YamlUtils.isTaskError(flowSource, taskId) ? "danger" : "blue"}-border rounded` : "",
431
+ class: node.type === "collapsedcluster" ? `bg-light-${color}-border rounded` : "",
420
432
  })
421
433
  }
422
434
  }
423
- for (const edge of (flowGraph.edges || [])) {
435
+
436
+ const clusterRootTaskNodeUids = rawClusters.filter(c => c.taskNode).map(c => c.taskNode.uid);
437
+ const edges = flowGraph.edges ?? [];
438
+
439
+ for (const edge of edges) {
424
440
  const newEdge = this.replaceIfCollapsed(edge.source, edge.target, edgeReplacer, hiddenNodes);
425
441
  if (newEdge) {
426
442
  elements.push({
@@ -428,17 +444,17 @@ export default class VueFlowUtils {
428
444
  source: newEdge.source,
429
445
  target: newEdge.target,
430
446
  type: "edge",
431
- markerEnd: YamlUtils.extractTask(flowSource, newEdge.target) ? {
432
- id: YamlUtils.isTaskError(flowSource, newEdge.target) ? "marker-danger" : "marker-custom",
433
- type: MarkerType.ArrowClosed,
434
- } : "",
447
+ markerEnd: this.isClusterRootOrEnd(nodeByUid[newEdge.target]) ? ""
448
+ : {
449
+ id: nodeByUid[newEdge.target].error ? "marker-danger" : "marker-custom",
450
+ type: MarkerType.ArrowClosed,
451
+ },
435
452
  data: {
436
- haveAdd: this.haveAdd(edge, flowSource, flowGraph, flowables),
437
- isFlowable: flowables.includes(edge.source) || flowables.includes(edge.target),
438
- haveDashArray: YamlUtils.isTrigger(flowSource, edge.source) || YamlUtils.isTrigger(flowSource, edge.target),
439
- nextTaskId: YamlUtils.getNextTaskId(edge.target, flowSource, flowGraph),
440
- disabled: disabledLowCode.includes(edge.source) || isReadOnly || !isAllowedEdit,
441
- color: this.getEdgeColor(edge, flowSource)
453
+ haveAdd: !isReadOnly && isAllowedEdit && this.haveAdd(edge, nodeByUid, clusterRootTaskNodeUids, readOnlyUidPrefixes),
454
+ haveDashArray: nodeByUid[edge.source].type.endsWith("GraphTrigger")
455
+ || nodeByUid[edge.target].type.endsWith("GraphTrigger")
456
+ || edge.source.startsWith(TRIGGERS_NODE_UID),
457
+ color: this.getEdgeColor(edge, nodeByUid)
442
458
  },
443
459
  style: {
444
460
  zIndex: 10,
@@ -449,19 +465,35 @@ export default class VueFlowUtils {
449
465
  return elements;
450
466
  }
451
467
 
452
- static isClusterError(flowSource, clusterUid) {
453
- if(clusterUid.startsWith(CLUSTER_UID_SEPARATOR)) {
454
- if(clusterUid.endsWith("_end")) {
468
+ static isClusterRootOrEnd(node) {
469
+ return ["GraphClusterRoot", "GraphClusterEnd"].some(s => node.type.endsWith(s));
470
+ }
455
471
 
456
- return YamlUtils.isTaskError(flowSource, Utils.splitFirst(clusterUid.substring(0, clusterUid.length - 4), CLUSTER_UID_SEPARATOR));
457
- }
458
- if(clusterUid.endsWith("_start")) {
472
+ static computeClusterColor(cluster) {
473
+ if (cluster.uid === CLUSTER_PREFIX + TRIGGERS_NODE_UID) {
474
+ return "success";
475
+ }
459
476
 
460
- return YamlUtils.isTaskError(flowSource, Utils.splitFirst(clusterUid.substring(0, clusterUid.length - 6), CLUSTER_UID_SEPARATOR));
461
- }
477
+ if (cluster.type.endsWith("SubflowGraphCluster")) {
478
+ return "primary";
462
479
  }
463
480
 
464
- return false
481
+ if (cluster.error) {
482
+ return "danger";
483
+ }
484
+
485
+ return "blue";
465
486
  }
466
487
 
488
+ static isExpandableTask(node, clusterByNodeUid, edgeReplacer) {
489
+ if (Object.values(edgeReplacer).includes(node.uid)) {
490
+ return true;
491
+ }
492
+
493
+ if (this.isCollapsedCluster(node)) {
494
+ return true;
495
+ }
496
+
497
+ return node.type.endsWith("SubflowGraphTask") && clusterByNodeUid[node.uid]?.uid?.replace(CLUSTER_PREFIX, "") !== node.uid;
498
+ }
467
499
  }
@@ -18,7 +18,7 @@ export const EVENTS = {
18
18
  "EXPAND_DEPENDENCIES": "expandDependencies",
19
19
  }
20
20
 
21
- export const CLUSTER_UID_SEPARATOR = "cluster_";
21
+ export const CLUSTER_PREFIX = "cluster_";
22
22
 
23
23
  export const NODE_SIZES = {
24
24
  TASK_WIDTH: 184,