2019-08-21 08:40:05 +00:00
|
|
|
<template>
|
2020-02-14 12:11:15 +00:00
|
|
|
<div class="overflow-x-auto">
|
|
|
|
<svg
|
|
|
|
version="1.1"
|
|
|
|
:width="width"
|
|
|
|
:height="height"
|
|
|
|
class="svg-content"
|
|
|
|
scroll
|
|
|
|
overflow="scroll"
|
|
|
|
>
|
|
|
|
<g v-for="(segment, i) in segments" v-bind:key="segment + i">
|
|
|
|
<line
|
|
|
|
:x1="segment.x1"
|
|
|
|
:y1="segment.y1"
|
|
|
|
:x2="segment.x2"
|
|
|
|
:y2="segment.y2"
|
|
|
|
:stroke-width="segment.strokeWidth"
|
|
|
|
:stroke="segment.stroke"
|
|
|
|
stroke-linecap="round"
|
|
|
|
:class="['stroke-current', segment.stroke]"
|
|
|
|
/>
|
|
|
|
</g>
|
|
|
|
<g v-for="(task, idx) in outTasks" v-bind:key="idx">
|
|
|
|
<foreignObject
|
|
|
|
:x="(taskWidth + taskXSpace) * task.level"
|
|
|
|
:y="(taskHeight + taskYSpace) * task.row"
|
|
|
|
rx="3"
|
|
|
|
ry="3"
|
|
|
|
:width="taskWidth"
|
|
|
|
:height="taskHeight"
|
|
|
|
>
|
|
|
|
<body>
|
|
|
|
<div
|
|
|
|
class="mb-2 border-l-5 rounded-l"
|
|
|
|
:class="taskClass(task)"
|
|
|
|
@mouseover="hoverTask = task"
|
|
|
|
@mouseleave="hoverTask = null"
|
2019-08-21 08:40:05 +00:00
|
|
|
>
|
2020-02-14 12:11:15 +00:00
|
|
|
<router-link
|
|
|
|
tag="a"
|
|
|
|
:to="task.link"
|
|
|
|
class="px-1 flex flex-col border border-l-0 rounded-r"
|
|
|
|
:style="{ height: taskHeight + 'px' }"
|
|
|
|
:title="task.name"
|
|
|
|
>
|
|
|
|
<div class="flex justify-end">
|
|
|
|
<div class="text-right text-xs">{{ task.duration }}</div>
|
|
|
|
</div>
|
|
|
|
<div class="font-bold truncate">{{ task.name }}</div>
|
|
|
|
<div class="flex justify-end">
|
|
|
|
<span
|
|
|
|
v-if="task.waiting_approval"
|
|
|
|
class="bg-gray-200 rounded-full px-2 py-0 text-xs text-center font-semibold"
|
|
|
|
>Waiting Approval</span
|
|
|
|
>
|
|
|
|
</div>
|
|
|
|
</router-link>
|
|
|
|
</div>
|
|
|
|
</body>
|
|
|
|
</foreignObject>
|
|
|
|
</g>
|
|
|
|
</svg>
|
|
|
|
</div>
|
2019-08-21 08:40:05 +00:00
|
|
|
</template>
|
|
|
|
|
|
|
|
<script>
|
|
|
|
import * as moment from "moment";
|
|
|
|
import momentDurationFormatSetup from "moment-duration-format";
|
|
|
|
|
|
|
|
momentDurationFormatSetup(moment);
|
|
|
|
|
|
|
|
export default {
|
|
|
|
name: "tasksgraph",
|
|
|
|
components: {},
|
|
|
|
data() {
|
|
|
|
return {
|
|
|
|
now: moment(),
|
|
|
|
|
|
|
|
graphTasks: [],
|
|
|
|
edges: [],
|
|
|
|
|
|
|
|
taskWidth: 240,
|
|
|
|
taskHeight: 60,
|
|
|
|
taskXSpace: 60,
|
|
|
|
taskYSpace: 20,
|
|
|
|
hoverTask: null,
|
|
|
|
|
|
|
|
height: "400px"
|
|
|
|
};
|
|
|
|
},
|
|
|
|
props: {
|
|
|
|
tasks: Object
|
|
|
|
},
|
|
|
|
computed: {
|
|
|
|
segments: function() {
|
|
|
|
let segments = [];
|
|
|
|
for (let edge of this.edges) {
|
|
|
|
for (let i = 0; i < edge.edgePoints.length - 1; i++) {
|
|
|
|
let strokeWidth = 1;
|
|
|
|
if (this.hoverTask) {
|
|
|
|
if (
|
|
|
|
edge.sourceTask.id == this.hoverTask.id ||
|
|
|
|
edge.targetTask.id == this.hoverTask.id
|
|
|
|
) {
|
|
|
|
strokeWidth = 3;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO(sgotti) set different colors to edges based on source task status???
|
|
|
|
let stroke = "text-dark";
|
|
|
|
segments.push({
|
|
|
|
edge: edge,
|
|
|
|
x1: edge.edgePoints[i].x,
|
|
|
|
y1: edge.edgePoints[i].y,
|
|
|
|
x2: edge.edgePoints[i + 1].x,
|
|
|
|
y2: edge.edgePoints[i + 1].y,
|
|
|
|
strokeWidth: strokeWidth,
|
|
|
|
stroke: stroke
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// sort segments by color (first the ok)
|
|
|
|
return segments;
|
|
|
|
},
|
|
|
|
outTasks() {
|
|
|
|
// just augment this.graphTasks (without recalculating it) with the current duration.
|
|
|
|
for (let task of this.graphTasks) {
|
|
|
|
task.duration = this.duration(task);
|
|
|
|
}
|
|
|
|
|
|
|
|
return this.graphTasks;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
watch: {
|
|
|
|
tasks: function(tasks) {
|
|
|
|
this.update(tasks);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
methods: {
|
|
|
|
duration(task) {
|
|
|
|
let formatString = "h:mm:ss[s]";
|
|
|
|
let start = moment(task.start_time);
|
|
|
|
let end = moment(task.end_time);
|
|
|
|
|
|
|
|
if (task.start_time === null) {
|
|
|
|
return moment.duration(0).format(formatString);
|
|
|
|
}
|
|
|
|
if (task.end_time === null) {
|
|
|
|
return moment.duration(this.now.diff(start)).format(formatString);
|
|
|
|
}
|
|
|
|
return moment.duration(end.diff(start)).format(formatString);
|
|
|
|
},
|
|
|
|
taskClass(task) {
|
|
|
|
if (task.status == "success") return "success";
|
|
|
|
if (task.status == "failed") return "failed";
|
|
|
|
if (task.status == "stopped") return "failed";
|
|
|
|
if (task.status == "running") return "running";
|
2019-09-03 08:21:45 +00:00
|
|
|
if (task.status == "skipped") return "skipped";
|
2019-08-21 08:40:05 +00:00
|
|
|
return "unknown";
|
|
|
|
},
|
|
|
|
update(tasks) {
|
|
|
|
// sort tasks by level
|
|
|
|
let graphTasks = Object.keys(tasks)
|
|
|
|
.sort((a, b) =>
|
|
|
|
tasks[a].level > tasks[b].level
|
|
|
|
? 1
|
|
|
|
: tasks[b].level > tasks[a].level
|
|
|
|
? -1
|
|
|
|
: 0
|
|
|
|
)
|
|
|
|
.map(k => tasks[k]);
|
|
|
|
|
|
|
|
this.graphTasks = graphTasks;
|
|
|
|
|
|
|
|
let maxlevel = 0;
|
|
|
|
for (let task of graphTasks) {
|
|
|
|
if (task.level > maxlevel) {
|
|
|
|
maxlevel = task.level;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let taskChilds = function(tasks, task) {
|
|
|
|
let childs = [];
|
|
|
|
for (let ot of tasks) {
|
|
|
|
for (let depTaskID in ot.depends) {
|
|
|
|
if (task.id == depTaskID) {
|
|
|
|
childs.push(ot);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return childs;
|
|
|
|
};
|
|
|
|
|
|
|
|
let taskMaxChildLevel = function(tasks, task) {
|
|
|
|
let level = task.level;
|
|
|
|
let childs = taskChilds(tasks, task);
|
|
|
|
for (let child of childs) {
|
|
|
|
if (child.level > level) {
|
|
|
|
level = child.level;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return level;
|
|
|
|
};
|
|
|
|
|
|
|
|
let levelTasks = function(tasks, level) {
|
|
|
|
let levelTasks = [];
|
|
|
|
for (let task of tasks) {
|
|
|
|
if (task.level != level) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
levelTasks.push(task);
|
|
|
|
}
|
|
|
|
return levelTasks;
|
|
|
|
};
|
|
|
|
|
2019-08-29 10:08:42 +00:00
|
|
|
let levelsTasks = function(tasks, startLevel) {
|
|
|
|
let levelTasks = [];
|
|
|
|
for (let task of tasks) {
|
|
|
|
if (task.level < startLevel) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
levelTasks.push(task);
|
|
|
|
}
|
|
|
|
return levelTasks;
|
|
|
|
};
|
|
|
|
|
|
|
|
let levelsTasksByRow = function(tasks, startLevel) {
|
|
|
|
return levelsTasks(tasks, startLevel).sort((a, b) =>
|
|
|
|
a.row > b.row ? 1 : b.row > a.row ? -1 : 0
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2019-08-21 08:40:05 +00:00
|
|
|
let levelFreeRow = function(tasks, level) {
|
|
|
|
let rows = [];
|
|
|
|
for (let task of tasks) {
|
|
|
|
if (task.level != level) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if (task.row !== undefined) {
|
|
|
|
rows.push(task.row);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
rows = rows.sort((a, b) => a - b);
|
|
|
|
|
|
|
|
let prevrow = 0;
|
|
|
|
for (let row of rows) {
|
|
|
|
if (row == prevrow) {
|
|
|
|
prevrow++;
|
|
|
|
} else {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return prevrow;
|
|
|
|
};
|
|
|
|
|
|
|
|
let levelsMaxRow = function(tasks, level) {
|
|
|
|
let row = 0;
|
|
|
|
for (let task of tasks) {
|
|
|
|
if (level >= 0 && task.level > level) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if (task.row > row) {
|
|
|
|
row = task.row;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return row;
|
|
|
|
};
|
|
|
|
|
|
|
|
for (let l = maxlevel; l >= 0; l--) {
|
|
|
|
let seenTasks = new Map();
|
|
|
|
|
|
|
|
let row = 0;
|
|
|
|
// if not at the last level fetch parents by their childs
|
|
|
|
if (l < maxlevel) {
|
2019-08-29 10:08:42 +00:00
|
|
|
for (let curTask of levelsTasksByRow(graphTasks, l + 1)) {
|
2019-08-21 08:40:05 +00:00
|
|
|
for (let depTaskID in curTask.depends) {
|
|
|
|
for (let parent of levelTasks(graphTasks, l)) {
|
|
|
|
if (seenTasks.has(parent.id)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if (parent.id == depTaskID) {
|
|
|
|
seenTasks.set(parent.id, true);
|
|
|
|
|
|
|
|
let maxChildLevel = taskMaxChildLevel(graphTasks, parent);
|
|
|
|
if (maxChildLevel > parent.level + 1) {
|
|
|
|
// put parent in a row greater than the max row the next levels until the child level
|
2019-08-29 10:08:42 +00:00
|
|
|
let mrow = levelsMaxRow(graphTasks, maxChildLevel - 1) + 1;
|
2019-08-21 08:40:05 +00:00
|
|
|
parent.row = mrow;
|
|
|
|
row = mrow + 1;
|
|
|
|
} else {
|
|
|
|
parent.row = row;
|
|
|
|
row++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// arrange tasks of this level
|
|
|
|
for (let curTask of levelTasks(graphTasks, l)) {
|
|
|
|
if (seenTasks.has(curTask.id)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
seenTasks.set(curTask.id, true);
|
|
|
|
let crow = levelFreeRow(graphTasks, l);
|
|
|
|
curTask.row = crow;
|
|
|
|
row = crow + 1;
|
|
|
|
|
|
|
|
// group tasks with common parent
|
|
|
|
for (let nextTask of levelTasks(graphTasks, l)) {
|
|
|
|
if (seenTasks.has(nextTask.id)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
let hasCommonParents = false;
|
|
|
|
for (let nextParentID in nextTask.depends) {
|
|
|
|
for (let curParentID in curTask.depends) {
|
|
|
|
if (nextParentID == curParentID) {
|
|
|
|
hasCommonParents = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (hasCommonParents) {
|
|
|
|
seenTasks.set(nextTask.id, true);
|
|
|
|
let crow = levelFreeRow(graphTasks, l);
|
|
|
|
nextTask.row = crow;
|
|
|
|
row = crow + 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let edges = [];
|
|
|
|
|
|
|
|
for (let curTask of graphTasks) {
|
|
|
|
for (let depTaskID in curTask.depends) {
|
|
|
|
for (let pTask of graphTasks) {
|
|
|
|
if (pTask.id == depTaskID) {
|
|
|
|
edges.push({
|
|
|
|
sourceTask: pTask,
|
|
|
|
targetTask: curTask,
|
|
|
|
source: { level: pTask.level, row: pTask.row },
|
|
|
|
target: { level: curTask.level, row: curTask.row }
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
this.edges = edges;
|
|
|
|
|
|
|
|
for (let edge of edges) {
|
|
|
|
edge.edgePoints = [];
|
|
|
|
let taskWidth = this.taskWidth;
|
|
|
|
let taskHeight = this.taskHeight;
|
|
|
|
let taskXSpace = this.taskXSpace;
|
|
|
|
let taskYSpace = this.taskYSpace;
|
|
|
|
|
|
|
|
edge.edgePoints.push({
|
|
|
|
x: (taskWidth + taskXSpace) * edge.source.level + taskWidth,
|
|
|
|
y: (taskHeight + taskYSpace) * edge.source.row + taskHeight / 2
|
|
|
|
});
|
|
|
|
edge.edgePoints.push({
|
|
|
|
x: (taskWidth + taskXSpace) * edge.target.level - taskXSpace / 2,
|
|
|
|
y: (taskHeight + taskYSpace) * edge.source.row + taskHeight / 2
|
|
|
|
});
|
|
|
|
edge.edgePoints.push({
|
|
|
|
x: (taskWidth + taskXSpace) * edge.target.level - taskXSpace / 2,
|
|
|
|
y: (taskHeight + taskYSpace) * edge.target.row + taskHeight / 2
|
|
|
|
});
|
|
|
|
edge.edgePoints.push({
|
|
|
|
x: (taskWidth + taskXSpace) * edge.target.level,
|
|
|
|
y: (taskHeight + taskYSpace) * edge.target.row + taskHeight / 2
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
let width = (maxlevel + 1) * (this.taskWidth + this.taskXSpace);
|
|
|
|
this.width = width + "px";
|
|
|
|
|
|
|
|
let height =
|
|
|
|
(levelsMaxRow(graphTasks, -1) + 1) *
|
|
|
|
(this.taskHeight + this.taskYSpace);
|
|
|
|
this.height = height + "px";
|
|
|
|
}
|
|
|
|
},
|
|
|
|
created() {
|
|
|
|
this.update(this.tasks);
|
|
|
|
|
|
|
|
window.setInterval(() => {
|
|
|
|
this.now = moment();
|
|
|
|
}, 500);
|
|
|
|
}
|
|
|
|
};
|
2020-02-14 12:11:15 +00:00
|
|
|
</script>
|