848eb1b013b3c5c830c85a8819b0e6ba202e03fc
[WebKit-https.git] / Tools / BuildSlaveSupport / build.webkit.org-config / public_html / dashboard / Scripts / BuildbotQueueView.js
1 /*
2  * Copyright (C) 2013 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 BuildbotQueueView = function(queues)
27 {
28     QueueView.call(this);
29
30     this.queues = queues || [];
31
32     this.queues.forEach(function(queue) {
33         if (this.platform && this.platform != queue.platform)
34             throw "A buildbot view may not contain queues for multiple platforms."
35         else
36             this.platform = queue.platform;
37         queue.addEventListener(BuildbotQueue.Event.IterationsAdded, this._queueIterationsAdded, this);
38         queue.addEventListener(BuildbotQueue.Event.UnauthorizedAccess, this._unauthorizedAccess, this);
39     }.bind(this));
40
41     Dashboard.Repository.OpenSource.trac.addEventListener(Trac.Event.CommitsUpdated, this._newCommitsRecorded, this);
42     if (typeof Dashboard.Repository.Internal.trac != "undefined")
43         Dashboard.Repository.Internal.trac.addEventListener(Trac.Event.CommitsUpdated, this._newCommitsRecorded, this);
44 };
45
46 BaseObject.addConstructorFunctions(BuildbotQueueView);
47
48 BuildbotQueueView.UpdateInterval = 45000; // 45 seconds
49 BuildbotQueueView.UpdateSoonTimeout = 1000; // 1 second
50
51 BuildbotQueueView.prototype = {
52     constructor: BuildbotQueueView,
53     __proto__: QueueView.prototype,
54
55     _latestProductiveIteration: function(queue)
56     {
57         if (!queue.iterations.length)
58             return null;
59         if (queue.iterations[0].productive)
60             return queue.iterations[0];
61         return queue.iterations[0].previousProductiveIteration;
62     },
63
64     _appendUnauthorizedLineView: function(queue)
65     {
66         console.assert(queue.buildbot.needsAuthentication);
67         console.assert(!queue.buildbot.isAuthenticated);
68         var javascriptURL = "javascript:buildbots[" + buildbots.indexOf(queue.buildbot) + "].updateQueues(Buildbot.UpdateReason.Reauthenticate)";
69         var status = new StatusLineView("unauthorized", StatusLineView.Status.Unauthorized, "", null, javascriptURL);
70         this.element.appendChild(status.element);
71     },
72
73     _appendPendingRevisionCount: function(queue)
74     {
75         var latestProductiveIteration = this._latestProductiveIteration(queue);
76         if (!latestProductiveIteration)
77             return;
78
79         var totalRevisionsBehind = 0;
80
81         // FIXME: To be 100% correct, we should also filter out changes that are ignored by
82         // the queue, see _should_file_trigger_build in wkbuild.py.
83         var branches = queue.branches;
84         for (var i = 0; i < branches.length; ++i) {
85             var branch = branches[i];
86             var repository = branch.repository;
87             var repositoryName = repository.name;
88             var trac = repository.trac;
89             var latestProductiveRevisionNumber = latestProductiveIteration.revision[repositoryName];
90             if (!latestProductiveRevisionNumber)
91                 continue;
92             if (!trac.latestRecordedRevisionNumber || trac.oldestRecordedRevisionNumber > latestProductiveRevisionNumber) {
93                 trac.loadMoreHistoricalData();
94                 return;
95             }
96
97             totalRevisionsBehind += trac.commitsOnBranch(branch.name, function(commit) { return commit.revisionNumber > latestProductiveRevisionNumber; }).length;
98         }
99
100         if (!totalRevisionsBehind)
101             return;
102
103         var messageElement = document.createElement("span"); // We can't just pass text to StatusLineView here, because we need an element that perfectly fits the text for popover positioning.
104         messageElement.textContent = totalRevisionsBehind + " " + (totalRevisionsBehind === 1 ? "revision behind" : "revisions behind");
105         var status = new StatusLineView(messageElement, StatusLineView.Status.NoBubble);
106         this.element.appendChild(status.element);
107
108         new PopoverTracker(messageElement, this._presentPopoverForPendingCommits.bind(this), queue);
109     },
110
111     _popoverLinesForCommitRange: function(trac, branch, firstRevisionNumber, lastRevisionNumber)
112     {
113         function lineForCommit(trac, commit)
114         {
115             var result = document.createElement("div");
116             result.className = "pending-commit";
117
118             var linkElement = document.createElement("a");
119             linkElement.className = "revision";
120             linkElement.href = trac.revisionURL(commit.revisionNumber);
121             linkElement.target = "_blank";
122             linkElement.textContent = "r" + commit.revisionNumber;
123             result.appendChild(linkElement);
124
125             var authorElement = document.createElement("span");
126             authorElement.className = "author";
127             authorElement.textContent = commit.author;
128             result.appendChild(authorElement);
129
130             var titleElement = document.createElement("span");
131             titleElement.className = "title";
132             titleElement.innerHTML = commit.title.innerHTML;
133             result.appendChild(titleElement);
134
135             return result;
136         }
137
138         console.assert(trac.oldestRecordedRevisionNumber <= firstRevisionNumber);
139
140         // FIXME: To be 100% correct, we should also filter out changes that are ignored by
141         // the queue, see _should_file_trigger_build in wkbuild.py.
142         var commits = trac.commitsOnBranch(branch, function(commit) { return commit.revisionNumber >= firstRevisionNumber && commit.revisionNumber <= lastRevisionNumber; });
143         return commits.map(function(commit) {
144             return lineForCommit(trac, commit);
145         }, this).reverse();
146     },
147
148     _presentPopoverForPendingCommits: function(element, popover, queue)
149     {
150         var latestProductiveIteration = this._latestProductiveIteration(queue);
151         if (!latestProductiveIteration)
152             return false;
153
154         var content = document.createElement("div");
155         content.className = "commit-history-popover";
156
157         var shouldAddDivider = false;
158         var branches = queue.branches;
159         for (var i = 0; i < branches.length; ++i) {
160             var branch = branches[i];
161             var repository = branch.repository;
162             var repositoryName = repository.name;
163             var trac = repository.trac;
164             var latestProductiveRevisionNumber = latestProductiveIteration.revision[repositoryName];
165             if (!latestProductiveRevisionNumber || !trac.latestRecordedRevisionNumber)
166                 continue;
167             var lines = this._popoverLinesForCommitRange(trac, branch.name, latestProductiveRevisionNumber + 1, trac.latestRecordedRevisionNumber);
168             var length = lines.length;
169             if (length && shouldAddDivider)
170                 this._addDividerToPopover(content);
171             for (var j = 0; j < length; ++j)
172                 content.appendChild(lines[j]);
173             shouldAddDivider = shouldAddDivider || length > 0;
174         }
175
176         var rect = Dashboard.Rect.rectFromClientRect(element.getBoundingClientRect());
177         popover.content = content;
178         popover.present(rect, [Dashboard.RectEdge.MIN_Y, Dashboard.RectEdge.MAX_Y, Dashboard.RectEdge.MAX_X, Dashboard.RectEdge.MIN_X]);
179
180         return true;
181     },
182
183     _presentPopoverForRevisionRange: function(element, popover, context)
184     {
185         var content = document.createElement("div");
186         content.className = "commit-history-popover";
187
188         // FIXME: Nothing guarantees that Trac has historical data for these revisions.
189         var linesForCommits = this._popoverLinesForCommitRange(context.trac, context.branch, context.firstRevision, context.lastRevision);
190
191         var line = document.createElement("div");
192         line.className = "title";
193
194         if (linesForCommits.length) {
195             line.textContent = "commits since previous result";
196             content.appendChild(line);
197             this._addDividerToPopover(content);
198         } else {
199             line.textContent = "no commits to " + context.branch + " since previous result";
200             content.appendChild(line);
201         }
202
203         for (var i = 0; i != linesForCommits.length; ++i)
204             content.appendChild(linesForCommits[i]);
205
206         var rect = Dashboard.Rect.rectFromClientRect(element.getBoundingClientRect());
207         popover.content = content;
208         popover.present(rect, [Dashboard.RectEdge.MIN_Y, Dashboard.RectEdge.MAX_Y, Dashboard.RectEdge.MAX_X, Dashboard.RectEdge.MIN_X]);
209
210         return true;
211     },
212
213     _revisionContentWithPopoverForIteration: function(iteration, previousIteration, branch)
214     {
215         var repository = branch.repository;
216         var repositoryName = repository.name;
217         console.assert(iteration.revision[repositoryName]);
218         var content = document.createElement("span");
219         var revision = iteration.revision[repositoryName];
220         if (repository.isSVN)
221             content.textContent = "r" + revision;
222         else if (repository.isGit) {
223             // Truncating for display. Git traditionally uses seven characters for a short hash.
224             content.textContent = revision.substr(0, 7);
225         } else
226             console.assert(false, "Should not get here; " + repository.name + " did not specify a known VCS type.");
227         content.classList.add("revision-number");
228
229         if (previousIteration) {
230             console.assert(previousIteration.revision[repositoryName]);
231             var context = {
232                 trac: repository.trac,
233                 branch: branch.name,
234                 firstRevision: previousIteration.revision[repositoryName] + 1,
235                 lastRevision: iteration.revision[repositoryName]
236             };
237             if (context.firstRevision <= context.lastRevision)
238                 new PopoverTracker(content, this._presentPopoverForRevisionRange.bind(this), context);
239         }
240
241         return content;
242     },
243
244     _addIterationHeadingToPopover: function(iteration, content, additionalText)
245     {
246         var title = document.createElement("div");
247         title.className = "popover-iteration-heading";
248
249         var queueName = document.createElement("span");
250         queueName.className = "queue-name";
251         queueName.textContent = iteration.queue.id;
252         title.appendChild(queueName);
253
254         var buildbotLink = document.createElement("a");
255         buildbotLink.className = "buildbot-link";
256         buildbotLink.textContent = "build #" + iteration.id;
257         buildbotLink.href = iteration.queue.buildbot.buildPageURLForIteration(iteration);
258         buildbotLink.target = "_blank";
259         title.appendChild(buildbotLink);
260
261         if (additionalText) {
262             var additionalTextElement = document.createElement("span");
263             additionalTextElement.className = "additional-text";
264             additionalTextElement.textContent = additionalText;
265             title.appendChild(additionalTextElement);
266         }
267
268         content.appendChild(title);
269     },
270
271     _addDividerToPopover: function(content)
272     {
273         var divider = document.createElement("div");
274         divider.className = "divider";
275         content.appendChild(divider);
276     },
277
278     revisionContentForIteration: function(iteration, previousDisplayedIteration)
279     {
280         var fragment = document.createDocumentFragment();
281         var shouldAddPlusSign = false;
282         var branches = iteration.queue.branches;
283         for (var i = 0; i < branches.length; ++i) {
284             var branch = branches[i];
285             if (!iteration.revision[branch.repository.name])
286                 continue;
287             var content = this._revisionContentWithPopoverForIteration(iteration, previousDisplayedIteration, branch);
288             if (shouldAddPlusSign)
289                 fragment.appendChild(document.createTextNode(" \uff0b "));
290             fragment.appendChild(content);
291             shouldAddPlusSign = true;
292         }
293         console.assert(fragment.childNodes.length);
294         return fragment;
295     },
296
297     appendBuildStyle: function(queues, defaultLabel, appendFunction)
298     {
299         queues.forEach(function(queue) {
300             var releaseLabel = document.createElement("a");
301             releaseLabel.classList.add("queueLabel");
302             releaseLabel.textContent = queue.heading ? queue.heading : defaultLabel;
303             releaseLabel.href = queue.overviewURL;
304             releaseLabel.target = "_blank";
305             this.element.appendChild(releaseLabel);
306
307             appendFunction.call(this, queue);
308         }.bind(this));
309     },
310
311     _updateQueues: function()
312     {
313         this.queues.forEach(function(queue) { queue.update(); });
314     },
315
316     _queueIterationsAdded: function(event)
317     {
318         this.updateSoon();
319
320         event.data.addedIterations.forEach(function(iteration) {
321             iteration.addEventListener(BuildbotIteration.Event.Updated, this._iterationUpdated, this);
322             iteration.addEventListener(BuildbotIteration.Event.UnauthorizedAccess, this._unauthorizedAccess, this);
323         }.bind(this));
324     },
325
326     _iterationUpdated: function(event)
327     {
328         this.updateSoon();
329     },
330     
331     _newCommitsRecorded: function(event)
332     {
333         this.updateSoon();
334     },
335
336     _unauthorizedAccess: function(event)
337     {
338         this.updateSoon();
339     }
340
341 };