@kestra-io/ui-libs 0.0.14 → 0.0.16
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 +1 -1
- package/src/assets/icons/SplitCellsHorizontal.vue +1 -1
- package/src/assets/icons/SplitCellsVertical.vue +1 -1
- package/src/components/buttons/AddTaskButton.vue +6 -0
- package/src/components/misc/Duration.vue +1 -1
- package/src/components/misc/ExecutionInformations.vue +2 -2
- package/src/components/misc/Tooltip.vue +1 -1
- package/src/components/nodes/BasicNode.vue +29 -18
- package/src/components/nodes/ClusterNode.vue +31 -4
- package/src/components/nodes/CollapsedClusterNode.vue +18 -8
- package/src/components/nodes/DotNode.vue +3 -3
- package/src/components/nodes/EdgeNode.vue +2 -1
- package/src/components/nodes/TaskNode.vue +49 -13
- package/src/components/nodes/TriggerNode.vue +1 -1
- package/src/components/topology/Topology.vue +59 -55
- package/src/scss/app.scss +7 -2
- package/src/utils/Utils.js +5 -0
- package/src/utils/VueFlowUtils.js +158 -126
- package/src/utils/constants.js +1 -1
package/package.json
CHANGED
|
@@ -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>
|
|
@@ -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>
|
|
@@ -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 :
|
|
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="
|
|
18
|
-
{{
|
|
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:
|
|
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,
|
|
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
|
-
|
|
182
|
-
if(this.data.node.
|
|
183
|
-
return
|
|
181
|
+
cls() {
|
|
182
|
+
if (this.data.node.trigger) {
|
|
183
|
+
return this.data.node.trigger.type;
|
|
184
184
|
}
|
|
185
|
-
|
|
185
|
+
|
|
186
|
+
if (!this.data.node?.task) {
|
|
187
|
+
return undefined;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return this.data.node.task.type;
|
|
186
191
|
},
|
|
187
|
-
|
|
188
|
-
|
|
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}`]">{{
|
|
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(
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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
|
|
11
|
+
import CircleIcon from "vue-material-design-icons/Circle.vue"
|
|
12
12
|
|
|
13
13
|
export default {
|
|
14
14
|
name: "Dot",
|
|
15
|
-
components: {Handle,
|
|
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="
|
|
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="
|
|
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,
|
|
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="
|
|
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="
|
|
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="!
|
|
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="!
|
|
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="!
|
|
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.
|
|
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.
|
|
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,
|
|
@@ -1,28 +1,16 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import {
|
|
3
|
-
|
|
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 {
|
|
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",
|
|
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,21 @@
|
|
|
196
187
|
lastPosition.value = null;
|
|
197
188
|
})
|
|
198
189
|
|
|
190
|
+
const subflowNestedTasks = computed(() => {
|
|
191
|
+
if(!props.flowGraph) {
|
|
192
|
+
return [];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return props.flowGraph.clusters.filter(cluster => cluster.cluster.type.endsWith("SubflowGraphCluster"))
|
|
196
|
+
.flatMap(cluster =>
|
|
197
|
+
cluster.nodes.filter(node => node !== cluster.cluster.taskNode.uid)
|
|
198
|
+
);
|
|
199
|
+
})
|
|
200
|
+
|
|
199
201
|
onNodeDrag((e) => {
|
|
200
202
|
resetNodesStyle();
|
|
201
|
-
getNodes.value.filter(n => n.id !== e.node.id).forEach(n => {
|
|
202
|
-
if (n.type === "trigger" || (n.type === "task" &&
|
|
203
|
+
getNodes.value.filter(n => Utils.afterLastDot(n.id) !== Utils.afterLastDot(e.node.id)).forEach(n => {
|
|
204
|
+
if (n.type === "trigger" || (n.type === "task" && n.id.startsWith(e.node.id + ".")) || subflowNestedTasks.value.includes(n.id)) {
|
|
203
205
|
n.style = {...n.style, opacity: "0.5"}
|
|
204
206
|
} else {
|
|
205
207
|
n.style = {...n.style, opacity: "1"}
|
|
@@ -218,11 +220,15 @@
|
|
|
218
220
|
})
|
|
219
221
|
|
|
220
222
|
const checkIntersections = (intersections, node) => {
|
|
221
|
-
const tasksMeet = intersections.filter(n => n.type === "task").map(n => n.id);
|
|
223
|
+
const tasksMeet = intersections.filter(n => n.type === "task").map(n => Utils.afterLastDot(n.id));
|
|
222
224
|
if (tasksMeet.length > 1) {
|
|
223
225
|
return "toomuchtaskerror";
|
|
224
226
|
}
|
|
225
|
-
|
|
227
|
+
try {
|
|
228
|
+
if (tasksMeet.length === 1 && YamlUtils.isParentChildrenRelation(props.source, Utils.afterLastDot(tasksMeet[0]), Utils.afterLastDot(node.id))) {
|
|
229
|
+
return "parentchildrenerror";
|
|
230
|
+
}
|
|
231
|
+
} catch (e) {
|
|
226
232
|
return "parentchildrenerror";
|
|
227
233
|
}
|
|
228
234
|
if (intersections.filter(n => n.type === "trigger").length > 0) {
|
|
@@ -232,13 +238,12 @@
|
|
|
232
238
|
}
|
|
233
239
|
|
|
234
240
|
const collapseCluster = (clusterUid, regenerate, recursive) => {
|
|
235
|
-
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
collapsed.value = collapsed.value.concat([nodeId])
|
|
241
|
+
const cluster = props.flowGraph.clusters.find(cluster => cluster.cluster.uid.endsWith(clusterUid));
|
|
242
|
+
const nodeId = clusterUid === "root.Triggers" ? "root.Triggers" : clusterUid.replace(CLUSTER_PREFIX, "");
|
|
243
|
+
collapsed.value.add(nodeId)
|
|
239
244
|
|
|
240
245
|
hiddenNodes.value = hiddenNodes.value.concat(cluster.nodes.filter(e => e !== nodeId || recursive));
|
|
241
|
-
if (clusterUid !== "Triggers") {
|
|
246
|
+
if (clusterUid !== "root.Triggers") {
|
|
242
247
|
hiddenNodes.value = hiddenNodes.value.concat([cluster.cluster.uid])
|
|
243
248
|
edgeReplacer.value = {
|
|
244
249
|
...edgeReplacer.value,
|
|
@@ -265,21 +270,21 @@
|
|
|
265
270
|
}
|
|
266
271
|
}
|
|
267
272
|
|
|
268
|
-
const expand = (
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
+
const expand = (expandData) => {
|
|
274
|
+
if (expandData.type === "io.kestra.core.tasks.flows.Flow" && !props.expandedSubflows.includes(expandData.id)) {
|
|
275
|
+
forwardEvent("expand-subflow", [...props.expandedSubflows, expandData.id]);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
edgeReplacer.value = {};
|
|
279
|
+
hiddenNodes.value = [];
|
|
280
|
+
clusterToNode.value = [];
|
|
281
|
+
collapsed.value.delete(expandData.id);
|
|
273
282
|
|
|
274
|
-
collapsed.value.forEach(n => collapseCluster(
|
|
283
|
+
collapsed.value.forEach(n => collapseCluster(n, false, false));
|
|
275
284
|
|
|
276
285
|
generateGraph();
|
|
277
286
|
}
|
|
278
287
|
|
|
279
|
-
const flowables = () => {
|
|
280
|
-
return props.flowGraph && props.flowGraph.flowables ? props.flowGraph.flowables : [];
|
|
281
|
-
}
|
|
282
|
-
|
|
283
288
|
const darkTheme = document.getElementsByTagName("html")[0].className.indexOf("dark") >= 0;
|
|
284
289
|
|
|
285
290
|
</script>
|
|
@@ -311,12 +316,12 @@
|
|
|
311
316
|
<template #node-task="taskProps">
|
|
312
317
|
<TaskNode
|
|
313
318
|
v-bind="taskProps"
|
|
314
|
-
@edit="forwardEvent(
|
|
315
|
-
@delete="forwardEvent(
|
|
319
|
+
@edit="forwardEvent(EVENTS.EDIT, $event)"
|
|
320
|
+
@delete="forwardEvent(EVENTS.DELETE, $event)"
|
|
316
321
|
@expand="expand($event)"
|
|
317
|
-
@open-link="forwardEvent(
|
|
318
|
-
@show-logs="forwardEvent(
|
|
319
|
-
@show-description="forwardEvent(
|
|
322
|
+
@open-link="forwardEvent(EVENTS.OPEN_LINK, $event)"
|
|
323
|
+
@show-logs="forwardEvent(EVENTS.SHOW_LOGS, $event)"
|
|
324
|
+
@show-description="forwardEvent(EVENTS.SHOW_DESCRIPTION, $event)"
|
|
320
325
|
@mouseover="onMouseOver($event)"
|
|
321
326
|
@mouseleave="onMouseLeave()"
|
|
322
327
|
@add-error="forwardEvent('on-add-flowable-error', $event)"
|
|
@@ -328,9 +333,9 @@
|
|
|
328
333
|
v-bind="triggerProps"
|
|
329
334
|
:is-read-only="isReadOnly"
|
|
330
335
|
:is-allowed-edit="isAllowedEdit"
|
|
331
|
-
@delete="forwardEvent(
|
|
332
|
-
@edit="forwardEvent(
|
|
333
|
-
@show-description="forwardEvent(
|
|
336
|
+
@delete="forwardEvent(EVENTS.DELETE, $event)"
|
|
337
|
+
@edit="forwardEvent(EVENTS.EDIT, $event)"
|
|
338
|
+
@show-description="forwardEvent(EVENTS.SHOW_DESCRIPTION, $event)"
|
|
334
339
|
/>
|
|
335
340
|
</template>
|
|
336
341
|
|
|
@@ -345,8 +350,7 @@
|
|
|
345
350
|
<EdgeNode
|
|
346
351
|
v-bind="EdgeProps"
|
|
347
352
|
:yaml-source="source"
|
|
348
|
-
|
|
349
|
-
@add-task="forwardEvent('add-task', $event)"
|
|
353
|
+
@add-task="forwardEvent(EVENTS.ADD_TASK, $event)"
|
|
350
354
|
:is-read-only="isReadOnly"
|
|
351
355
|
:is-allowed-edit="isAllowedEdit"
|
|
352
356
|
/>
|
package/src/scss/app.scss
CHANGED
|
@@ -20,6 +20,10 @@
|
|
|
20
20
|
font-size: 0.66rem;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
.vue-flow__controls-button {
|
|
24
|
+
color: black;
|
|
25
|
+
}
|
|
26
|
+
|
|
23
27
|
.vue-flow__container {
|
|
24
28
|
.top-button-div {
|
|
25
29
|
position: absolute;
|
|
@@ -35,14 +39,15 @@
|
|
|
35
39
|
pointer-events: none !important;
|
|
36
40
|
}
|
|
37
41
|
|
|
42
|
+
|
|
38
43
|
marker[id*='id=marker-custom&type=arrowclosed'] polyline {
|
|
39
44
|
stroke: #9A8EB4;
|
|
40
45
|
fill: #9A8EB4;
|
|
41
46
|
}
|
|
42
47
|
|
|
43
48
|
marker[id*='id=marker-danger&type=arrowclosed'] polyline {
|
|
44
|
-
stroke:
|
|
45
|
-
fill:
|
|
49
|
+
stroke: $red;
|
|
50
|
+
fill: $red;
|
|
46
51
|
}
|
|
47
52
|
|
|
48
53
|
.vue-flow__handle {
|
package/src/utils/Utils.js
CHANGED
|
@@ -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 {
|
|
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,
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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) ?
|
|
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
|
|
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 &&
|
|
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
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
214
|
-
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
213
|
+
if (this.isTriggerNode(node) || this.isCollapsedCluster(node)) {
|
|
214
|
+
return "success";
|
|
215
|
+
}
|
|
233
216
|
|
|
234
|
-
|
|
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
|
|
227
|
+
return "default";
|
|
238
228
|
}
|
|
239
229
|
|
|
240
|
-
static haveAdd(edge,
|
|
241
|
-
|
|
242
|
-
|
|
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
|
-
|
|
248
|
-
|
|
235
|
+
|
|
236
|
+
// edge = clusterRoot -> clusterRootTask
|
|
237
|
+
if (clustersRootTaskUids.includes(edge.target)) {
|
|
238
|
+
return undefined;
|
|
249
239
|
}
|
|
250
|
-
|
|
251
|
-
//
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
260
|
-
|
|
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
|
-
|
|
263
|
-
|
|
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
|
-
|
|
266
|
-
|
|
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
|
-
|
|
269
|
-
|
|
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
|
|
276
|
+
return [Utils.afterLastDot(edge.target), "before"];
|
|
273
277
|
}
|
|
274
278
|
|
|
275
|
-
static getEdgeColor(edge,
|
|
276
|
-
|
|
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
|
-
|
|
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
|
|
297
|
+
static generateGraph(vueFlowId, flowId, namespace, flowGraph, flowSource, hiddenNodes, isHorizontal, edgeReplacer, collapsed, clusterToNode, isReadOnly, isAllowedEdit) {
|
|
286
298
|
const elements = [];
|
|
287
299
|
|
|
288
|
-
const
|
|
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
|
-
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
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
|
-
|
|
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 ===
|
|
358
|
-
height: clusterUid ===
|
|
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:
|
|
383
|
+
color: clusterColor,
|
|
384
|
+
taskNode: cluster.cluster.taskNode
|
|
363
385
|
},
|
|
364
|
-
class: `bg-light-${
|
|
386
|
+
class: `bg-light-${clusterColor}-border rounded p-2`,
|
|
365
387
|
})
|
|
366
388
|
}
|
|
367
389
|
}
|
|
368
390
|
|
|
369
|
-
|
|
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 (
|
|
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
|
|
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,
|
|
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:
|
|
407
|
-
draggable: nodeType === "task"
|
|
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:
|
|
413
|
-
color:
|
|
414
|
-
expandable:
|
|
415
|
-
isReadOnly:
|
|
416
|
-
|
|
417
|
-
|
|
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-${
|
|
431
|
+
class: node.type === "collapsedcluster" ? `bg-light-${color}-border rounded` : "",
|
|
420
432
|
})
|
|
421
433
|
}
|
|
422
434
|
}
|
|
423
|
-
|
|
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:
|
|
432
|
-
|
|
433
|
-
|
|
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,
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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
|
|
453
|
-
|
|
454
|
-
|
|
468
|
+
static isClusterRootOrEnd(node) {
|
|
469
|
+
return ["GraphClusterRoot", "GraphClusterEnd"].some(s => node.type.endsWith(s));
|
|
470
|
+
}
|
|
455
471
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
472
|
+
static computeClusterColor(cluster) {
|
|
473
|
+
if (cluster.uid === CLUSTER_PREFIX + TRIGGERS_NODE_UID) {
|
|
474
|
+
return "success";
|
|
475
|
+
}
|
|
459
476
|
|
|
460
|
-
|
|
461
|
-
|
|
477
|
+
if (cluster.type.endsWith("SubflowGraphCluster")) {
|
|
478
|
+
return "primary";
|
|
462
479
|
}
|
|
463
480
|
|
|
464
|
-
|
|
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
|
}
|
package/src/utils/constants.js
CHANGED