| <!-- |
| This in an HTML Import-able file that contains the definition |
| of the following elements: |
| |
| <job-sk> |
| |
| Status information about the task scheduler. |
| |
| To use this file import it: |
| |
| <link href="/res/imp/job-sk.html" rel="import" /> |
| |
| Usage: |
| |
| <job-sk></job-sk> |
| |
| Properties: |
| None. |
| |
| Methods: |
| setJob: Provide job data. |
| |
| Events: |
| None. |
| --> |
| |
| <link rel="import" href="/res/common/imp/human-date-sk.html"> |
| <link rel="import" href="/res/common/imp/timer-sk.html"> |
| <link rel="import" href="/res/imp/bower_components/iron-flex-layout/iron-flex-layout-classes.html"> |
| <link rel="import" href="/res/imp/bower_components/paper-button/paper-button.html"> |
| |
| <dom-module id="job-sk"> |
| <template> |
| <style include="iron-flex iron-flex-alignment"> |
| <style> |
| :host { |
| font-family: sans-serif; |
| } |
| #cancelButton { |
| background-color: #D95F02; |
| color: #fff; |
| font-family: 'Roboto', 'Noto', sans-serif; |
| font-size: 1.0em; |
| } |
| .container { |
| margin: 5px; |
| padding: 10px; |
| border: 1px solid #eeeeee; |
| } |
| .container h2 { |
| font-size: 18px; |
| } |
| .table { |
| border-collapse: collapse; |
| display: table; |
| } |
| .tr { |
| border-bottom: 1px solid #EEEEEE; |
| display: table-row; |
| } |
| .tr:hover { |
| background-color: #F5F5F5; |
| } |
| .tr:hover .tr:hover { |
| background-color: #FFFFFF; |
| } |
| .td,.th { |
| display: table-cell; |
| padding: 10px; |
| } |
| .td { |
| color: #212121; |
| font-size: 0.813em; |
| vertical-align: middle; |
| } |
| .th { |
| color: #767676; |
| font-size: 0.75em; |
| } |
| </style> |
| <timer-sk id="timer" period="[[_reload]]" on-trigger="_loadJob"> |
| <div class="container"> |
| <div class="layout horizontal"> |
| <div class="flex"> |
| <h2>Job Information</h2> |
| </div> |
| <template is="dom-if" if="[[!_job.status]]"> |
| <div> |
| <paper-button raised id="cancelButton" on-tap="_cancelJob">Cancel</paper-button> |
| </div> |
| </template> |
| </div> |
| <div class="table"> |
| <div class="tr"><div class="td">ID</div><div class="td">[[_job.id]]</div></div> |
| <div class="tr"><div class="td">Name</div><div class="td">[[_job.name]]</div></div> |
| <div class="tr"> |
| <div class="td">Status</div> |
| <div class="td" style$="background-color:[[_statusColor]]">[[_statusText]]</div> |
| </div> |
| <div class="tr"><div class="td">Created</div><div class="td"><human-date-sk date="[[_job.created]]"></human-date-sk></div></div> |
| <template is="dom-if" if="[[_job.status]]"> |
| <div class="tr"><div class="td">Finished</div><div class="td"><human-date-sk date="[[_job.finished]]"></human-date-sk></div></div> |
| </template> |
| <div class="tr"><div class="td">Duration</div><div class="td">[[_duration]]</div></div> |
| <div class="tr"> |
| <div class="td">Repo</div> |
| <div class="td"><a href$="[[_job.repo]]" target="_blank">[[_job.repo]]</a></div> |
| </div> |
| <div class="tr"> |
| <div class="td">Revision</div> |
| <div class="td"><a href$="[[_revisionLink]]" target="_blank">[[_job.revision]]</a></div> |
| </div> |
| <template is="dom-if" if="[[_isTryJob]]"> |
| <div class="tr"> |
| <div class="td">Codereview Link</div> |
| <div class="td"><a href$="[[_codereviewLink]]" target="_blank">[[_codereviewLink]]</a></div> |
| </div> |
| <div class="tr"><div class="td">Codereview Server</div><div class="td">[[_job.server]]</div></div> |
| <div class="tr"><div class="td">Issue</div><div class="td">[[_job.issue]]</div></div> |
| <div class="tr"><div class="td">Patchset</div><div class="td">[[_job.patchset]]</div></div> |
| </template> |
| <div class="tr"><div class="td">Manually forced</div><div class="td">[[_job.isForce]]</div></div> |
| </div> |
| </div> |
| |
| <div class="container"> |
| <h2>Tasks</h2> |
| <svg id="tasks_svg"></svg> |
| </div> |
| </template> |
| <script src="/res/imp/bower_components/d3/d3.min.js"></script> |
| <script> |
| (function(){ |
| var jobStatusToTextColor = { |
| "": ["in progress", "rgb(248, 230, 180)"], |
| "SUCCESS": ["succeeded", "rgb(209, 228, 188)"], |
| "FAILURE": ["failed", "rgb(217, 95, 2)"], |
| "MISHAP": ["mishap", "rgb(117, 112, 179)"], |
| "CANCELED": ["canceled", "rgb(117, 112, 179)"], |
| }; |
| |
| var taskStatusToTextColor = { |
| "": ["pending", "rgb(255, 255, 255)"], |
| "RUNNING": ["running", "rgb(248, 230, 180)"], |
| "SUCCESS": ["succeeded", "rgb(209, 228, 188)"], |
| "FAILURE": ["failed", "rgb(217, 95, 2)"], |
| "MISHAP": ["mishap", "rgb(117, 112, 179)"], |
| }; |
| |
| Polymer({ |
| is: "job-sk", |
| |
| properties: { |
| jobId: { |
| type: String, |
| observer: "_loadJob", |
| }, |
| |
| swarmingServer: { |
| type: String, |
| }, |
| |
| _job: { |
| type: Object, |
| }, |
| |
| _codereviewLink: { |
| type: String, |
| computed: "_computeCodereviewLink(_job)", |
| }, |
| _duration: { |
| type: String, |
| computed: "_computeDuration(_job)", |
| }, |
| _isTryJob: { |
| type: Boolean, |
| computed: "_computeIsTryJob(_job)", |
| }, |
| _reload: { |
| type: Number, |
| value: 10, |
| }, |
| _revisionLink: { |
| type: String, |
| computed: "_computeRevisionLink(_job)", |
| }, |
| _statusText: { |
| type: String, |
| computed: "_computeStatusText(_job)", |
| }, |
| _statusColor: { |
| type: String, |
| computed: "_computeStatusColor(_job)", |
| }, |
| }, |
| |
| _loadJob: function() { |
| var url = "/json/job/" + this.jobId; |
| console.log("Loading Job from " + url); |
| sk.get(url).then(JSON.parse).then(function(json) { |
| this.set("_job", json); |
| this._computeTasksGraph(); |
| // If the job is finished, don't reload. |
| if (this._job.status != "") { |
| this.set("_reload", -1); |
| } |
| }.bind(this)).catch(sk.errorMessage); |
| }, |
| |
| _cancelJob: function() { |
| var url = "/json/job/" + this._job.id + "/cancel"; |
| console.log("Canceling Job: " + url); |
| sk.post(url).then(JSON.parse).then(function(json) { |
| this.set("_job", json); |
| this._computeTasksGraph(); |
| }.bind(this)).catch(sk.errorMessage); |
| }, |
| |
| _computeCodereviewLink: function(job) { |
| if (job.server.indexOf("codereview.chromium") != -1) { |
| return job.server + "/" + job.issue + "/#ps" + job.patchset; |
| } else { |
| return job.server + "/c/" + job.issue + "/" + job.patchset; |
| } |
| }, |
| |
| _computeDuration: function(job) { |
| if (!job) { |
| return "???"; |
| } |
| var start = new Date(job.created); |
| var end = new Date(job.finished); |
| if (job.status == "") { |
| end = new Date(); |
| } |
| var duration = (end.getTime() - start.getTime()) / 1000; |
| return sk.human.strDuration(duration); |
| }, |
| |
| _computeIsTryJob: function(job) { |
| return job.server != "" && job.issue != "" && job.patchset != ""; |
| }, |
| |
| _computeRevisionLink: function(job) { |
| // This assumes we use Gitiles, but that's a safe assumption for now. |
| return job.repo + "/+/" + job.revision; |
| }, |
| |
| _computeStatusText: function(job) { |
| if (!job || job.status == undefined || job.status == null) { |
| return "unknown"; |
| } |
| var textColor = jobStatusToTextColor[job.status]; |
| if (!textColor || textColor.length != 2) { |
| return "unknown"; |
| } |
| return textColor[0]; |
| }, |
| |
| _computeStatusColor: function(job) { |
| if (!job || job.status == undefined || job.status == null) { |
| return "rgb(255, 255, 255)"; |
| } |
| var textColor = jobStatusToTextColor[job.status]; |
| if (!textColor || textColor.length != 2) { |
| return "rgb(255, 255, 255)"; |
| } |
| return textColor[1]; |
| }, |
| |
| _computeTasksGraph: function() { |
| var taskData = this._job.tasks; |
| var graph = this._job.dependencies; |
| var taskLinkUrlPrefix = "https://luci-milo.appspot.com/swarming/task/"; |
| if ($$$("login-sk").email && |
| $$$("login-sk").email.endsWith("@google.com")) { |
| taskLinkUrlPrefix = "https://" + this.swarmingServer + "/task?id="; |
| } |
| |
| // Skip drawing the graph if taskData or graph are missing or empty. This |
| // is mainly to prevent errors on the demo page. |
| if (!taskData || !graph || Object.keys(taskData).length == 0 || Object.keys(graph).length == 0) { |
| console.log("Not drawing graph; taskData or graph not ready."); |
| return; |
| } |
| console.log("Drawing tasks graph."); |
| |
| // Compute the "depth" of each task spec. |
| var depth = {}; |
| var cols = []; |
| var visited = {}; |
| |
| var visit = function(current) { |
| visited[current] = true |
| var myDepth = 0; |
| var deps = graph[current] || []; |
| for (var i = 0; i < deps.length; i++) { |
| var dep = deps[i]; |
| // Visit the dep if we haven't yet. Its depth may be zero, so we have |
| // to explicitly use "depth[dep] == undefined" instead of "!depth[dep]" |
| if (depth[dep] == undefined) { |
| visit(dep); |
| } |
| if (depth[dep] >= myDepth) { |
| myDepth = depth[dep] + 1; |
| } |
| } |
| depth[current] = myDepth; |
| if (cols.length == myDepth) { |
| cols.push([]); |
| } else if (myDepth > cols.length) { |
| console.log("_computeTasksGraph skipped a column!"); |
| return; |
| } |
| cols[myDepth].push({ |
| name: current, |
| tasks: taskData[current] || [], |
| }); |
| }; |
| |
| // Visit all of the nodes. |
| for (var key in graph) { |
| if (!visited[key]) { |
| visit(key); |
| } |
| } |
| |
| var fontFamily = "Arial"; |
| var fontSize = 12; |
| var taskSpecMarginX = 20; |
| var taskSpecMarginY = 20; |
| var taskMarginX = 10; |
| var taskMarginY = 10; |
| var textMarginX = 10; |
| var textMarginY = 10; |
| var taskWidth = 30; |
| var taskHeight = 30; |
| var textOffsetX = textMarginX; |
| var textOffsetY = fontSize + textMarginY; |
| var textHeight = fontSize + 2 * textMarginY; |
| var taskSpecHeight = textHeight + taskHeight + taskMarginY; |
| |
| // Compute the task spec block width for each column. |
| var maxTextWidth = 0; |
| var canvas = document.createElement("canvas"); |
| var ctx = canvas.getContext("2d"); |
| ctx.font = fontSize + "px " + fontFamily; |
| var taskSpecWidth = []; |
| for (var col = 0; col < cols.length; col++) { |
| // Get the minimum width of a task spec block needed to fit the entire |
| // task spec name. |
| var maxWidth = 0; |
| for (var i = 0; i < cols[col].length; i++) { |
| var textWidth = ctx.measureText(cols[col][i].name).width + 2 * textMarginX; |
| if (textWidth > maxWidth) { |
| maxWidth = textWidth; |
| } |
| |
| var numTasks = cols[col][i].tasks.length || 1; |
| var tasksWidth = taskMarginX + numTasks * (taskWidth + taskMarginX); |
| if (tasksWidth > maxWidth) { |
| maxWidth = tasksWidth; |
| } |
| } |
| |
| taskSpecWidth.push(maxWidth); |
| } |
| |
| // Lay out the task specs and tasks. |
| var totalWidth = 0; |
| var totalHeight = 0; |
| var taskSpecs = []; |
| var tasks = []; |
| var byName = {}; |
| var curX = taskMarginX; |
| for (var col = 0; col < cols.length; col++) { |
| var curY = taskMarginY; |
| // Add an entry for each task. |
| for (var i = 0; i < cols[col].length; i++) { |
| var taskSpec = cols[col][i]; |
| var entry = { |
| x: curX, |
| y: curY, |
| width: taskSpecWidth[col], |
| height: taskSpecHeight, |
| name: taskSpec.name, |
| numTasks: taskSpec.tasks.length, |
| }; |
| taskSpecs.push(entry); |
| byName[taskSpec.name] = entry; |
| |
| var taskX = curX + taskMarginX; |
| var taskY = curY + textHeight; |
| for (var j = 0; j < taskSpec.tasks.length; j++) { |
| var task = taskSpec.tasks[j]; |
| tasks.push({ |
| x: taskX + j * (taskWidth + taskMarginX), |
| y: taskY, |
| width: taskWidth, |
| height: taskHeight, |
| task: task, |
| }); |
| } |
| curY += taskSpecHeight + taskSpecMarginY; |
| } |
| if (curY > totalHeight) { |
| totalHeight = curY; |
| } |
| curX += taskSpecWidth[col] + taskSpecMarginX; |
| } |
| |
| totalWidth = curX; |
| |
| // Compute the arrows. |
| var arrows = [] |
| for (var name in graph) { |
| var dst = byName[name]; |
| var deps = graph[name]; |
| if (deps) { |
| for (var j = 0; j < deps.length; j++) { |
| var src = byName[deps[j]] |
| if (!src) { |
| console.log("Error: task " + dst.name + " has unknown parent " + deps[j]); |
| } else { |
| arrows.push([src, dst]); |
| } |
| } |
| } |
| } |
| |
| // Draw the graph. |
| svg = d3.select(this.$.tasks_svg) |
| .attr("width", totalWidth) |
| .attr("height", totalHeight); |
| |
| // Draw task specs. |
| var taskSpecRects = svg.selectAll("rect.taskSpec").data(taskSpecs); |
| taskSpecRects.enter() |
| .append("svg:rect") |
| .attr("class", "taskSpec") |
| .attr("rx", "4") |
| .attr("ry", "4") |
| .attr("style", "stroke: black; fill: white;"); |
| taskSpecRects |
| .attr("x", function(data) { return data.x; }) |
| .attr("y", function(data) { return data.y; }) |
| .attr("width", function(data) { return data.width; }) |
| .attr("height", function(data) { return data.height; }); |
| taskSpecRects.exit().remove(); |
| |
| // Draw text. |
| var taskSpecTexts = svg.selectAll("text.taskSpec").data(taskSpecs); |
| taskSpecTexts.enter() |
| .append("svg:text") |
| .attr("class", "taskSpec") |
| .attr("font-family", fontFamily) |
| .attr("font-size", fontSize); |
| taskSpecTexts |
| .attr("x", function(data) { return data.x + textOffsetX; }) |
| .attr("y", function(data) { return data.y + textOffsetY; }) |
| .text(function(data) { return data.name; }); |
| taskSpecTexts.exit().remove(); |
| |
| // Draw tasks. |
| var taskLinks = svg.selectAll("a.task").data(tasks) |
| taskLinks.enter() |
| .append("a") |
| .attr("class", "task") |
| .attr("target", "_blank") |
| .append("svg:rect") |
| .attr("class", "task") |
| .attr("rx", "4") |
| .attr("ry", "4"); |
| taskLinks.attr("href", function(data) { |
| return taskLinkUrlPrefix + data.task.swarmingTaskId; |
| }); |
| taskLinks.exit().remove(); |
| var taskRects = svg.selectAll("rect.task").data(tasks); |
| taskRects |
| .attr("x", function(data) { return data.x; }) |
| .attr("y", function(data) { return data.y; }) |
| .attr("width", function(data) { return data.width; }) |
| .attr("height", function(data) { return data.height; }) |
| .attr("style", function(data) { |
| var color = taskStatusToTextColor[data.task.status][1]; |
| return "stroke: black;" |
| + "fill: " + color + ";"; |
| }); |
| taskRects.exit().remove(); |
| |
| |
| // Draw arrows. |
| var arrowWidth = 4; |
| var arrowHeight = 4; |
| var arrowHeadPath = svg.selectAll("marker.arrowhead").data([0]); |
| arrowHeadPath.enter() |
| .append("svg:marker") |
| .attr("id", "arrowhead") |
| .attr("class", "arrowhead") |
| .attr("viewBox", "0 0 10 10") |
| .attr("refX", "0") |
| .attr("refY", "5") |
| .attr("markerUnits", "strokeWidth") |
| .attr("markerWidth", arrowWidth) |
| .attr("markerHeight", arrowHeight) |
| .attr("orient", "auto") |
| .append("svg:path") |
| .attr("d", "M 0 0 L 10 5 L 0 10 Z"); // Filled triangle path. |
| arrowHeadPath.exit().remove(); |
| |
| var arrowPaths = svg.selectAll("path.arrow").data(arrows); |
| arrowPaths |
| .enter() |
| .append("svg:path") |
| .attr("class", "arrow") |
| .attr("stroke", "black") |
| .attr("stroke-width", "1") |
| .attr("fill", "transparent") |
| .attr("marker-end", "url(#arrowhead)"); |
| arrowPaths |
| .attr("d", function(data) { |
| // Start and end points. |
| var x1 = data[0].x + data[0].width; |
| var y1 = data[0].y + data[0].height / 2; |
| var x2 = data[1].x - arrowWidth; |
| var y2 = data[1].y + data[1].height / 2; |
| // Control points. |
| var cx1 = x1 + taskSpecMarginX - arrowWidth/2; |
| var cy1 = y1; |
| var cx2 = x2 - taskSpecMarginX + arrowWidth/2; |
| var cy2 = y2; |
| return ("M" + x1 + " " + y1 |
| + " C" + cx1 + " " + cy1 |
| + " " + cx2 + " " + cy2 |
| + " " + x2 + " " + y2); |
| }); |
| arrowPaths.exit().remove(); |
| }, |
| }); |
| })(); |
| </script> |
| </dom-module> |