Refactor to add event listeners and start periodic updates for all trac instances.
[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     var sortedRepositories = Dashboard.sortedRepositories;
42     for (var i = 0; i < sortedRepositories.length; i++) {
43         var trac = sortedRepositories[i].trac;
44         if (trac)
45             trac.addEventListener(Trac.Event.CommitsUpdated, this._newCommitsRecorded, this);
46     }
47 };
48
49 BaseObject.addConstructorFunctions(BuildbotQueueView);
50
51 BuildbotQueueView.UpdateInterval = 45000; // 45 seconds
52 BuildbotQueueView.UpdateSoonTimeout = 1000; // 1 second
53
54 BuildbotQueueView.prototype = {
55     constructor: BuildbotQueueView,
56     __proto__: QueueView.prototype,
57
58     _latestProductiveIteration: function(queue)
59     {
60         if (!queue.iterations.length)
61             return null;
62         if (queue.iterations[0].productive)
63             return queue.iterations[0];
64         return queue.iterations[0].previousProductiveIteration;
65     },
66
67     _appendUnauthorizedLineView: function(queue)
68     {
69         console.assert(queue.buildbot.needsAuthentication);
70         console.assert(!queue.buildbot.isAuthenticated);
71         var javascriptURL = "javascript:buildbots[" + buildbots.indexOf(queue.buildbot) + "].updateQueues(Buildbot.UpdateReason.Reauthenticate)";
72         var status = new StatusLineView("unauthorized", StatusLineView.Status.Unauthorized, "", null, javascriptURL);
73         this.element.appendChild(status.element);
74     },
75
76     _appendPendingRevisionCount: function(queue)
77     {
78         var latestProductiveIteration = this._latestProductiveIteration(queue);
79         if (!latestProductiveIteration)
80             return;
81
82         var totalRevisionsBehind = 0;
83
84         // FIXME: To be 100% correct, we should also filter out changes that are ignored by
85         // the queue, see _should_file_trigger_build in wkbuild.py.
86         var branches = queue.branches;
87         for (var i = 0; i < branches.length; ++i) {
88             var branch = branches[i];
89             var repository = branch.repository;
90             var repositoryName = repository.name;
91             var trac = repository.trac;
92             var latestProductiveRevisionNumber = latestProductiveIteration.revision[repositoryName];
93             if (!latestProductiveRevisionNumber)
94                 continue;
95             if (!trac)
96                 continue;
97             if (!trac.latestRecordedRevisionNumber || trac.oldestRecordedRevisionNumber > latestProductiveRevisionNumber) {
98                 trac.loadMoreHistoricalData();
99                 return;
100             }
101
102             totalRevisionsBehind += trac.commitsOnBranch(branch.name, function(commit) { return commit.revisionNumber > latestProductiveRevisionNumber; }).length;
103         }
104
105         if (!totalRevisionsBehind)
106             return;
107
108         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.
109         messageElement.textContent = totalRevisionsBehind + " " + (totalRevisionsBehind === 1 ? "revision behind" : "revisions behind");
110         var status = new StatusLineView(messageElement, StatusLineView.Status.NoBubble);
111         this.element.appendChild(status.element);
112
113         new PopoverTracker(messageElement, this._presentPopoverForPendingCommits.bind(this), queue);
114     },
115
116     _popoverLinesForCommitRange: function(trac, branch, firstRevisionNumber, lastRevisionNumber)
117     {
118         function lineForCommit(trac, commit)
119         {
120             var result = document.createElement("div");
121             result.className = "pending-commit";
122
123             var linkElement = document.createElement("a");
124             linkElement.className = "revision";
125             linkElement.href = trac.revisionURL(commit.revisionNumber);
126             linkElement.target = "_blank";
127             linkElement.textContent = "r" + commit.revisionNumber;
128             result.appendChild(linkElement);
129
130             var authorElement = document.createElement("span");
131             authorElement.className = "author";
132             authorElement.textContent = commit.author;
133             result.appendChild(authorElement);
134
135             var titleElement = document.createElement("span");
136             titleElement.className = "title";
137             titleElement.innerHTML = commit.title.innerHTML;
138             result.appendChild(titleElement);
139
140             return result;
141         }
142
143         console.assert(trac.oldestRecordedRevisionNumber <= firstRevisionNumber);
144
145         // FIXME: To be 100% correct, we should also filter out changes that are ignored by
146         // the queue, see _should_file_trigger_build in wkbuild.py.
147         var commits = trac.commitsOnBranch(branch.name, function(commit) { return commit.revisionNumber >= firstRevisionNumber && commit.revisionNumber <= lastRevisionNumber; });
148         return commits.map(function(commit) {
149             return lineForCommit(trac, commit);
150         }, this).reverse();
151     },
152
153     _presentPopoverForPendingCommits: function(element, popover, queue)
154     {
155         var latestProductiveIteration = this._latestProductiveIteration(queue);
156         if (!latestProductiveIteration)
157             return false;
158
159         var content = document.createElement("div");
160         content.className = "commit-history-popover";
161
162         var shouldAddDivider = false;
163         var branches = queue.branches;
164         for (var i = 0; i < branches.length; ++i) {
165             var branch = branches[i];
166             var repository = branch.repository;
167             var repositoryName = repository.name;
168             var trac = repository.trac;
169             var latestProductiveRevisionNumber = latestProductiveIteration.revision[repositoryName];
170             if (!latestProductiveRevisionNumber || !trac.latestRecordedRevisionNumber)
171                 continue;
172             var lines = this._popoverLinesForCommitRange(trac, branch, latestProductiveRevisionNumber + 1, trac.latestRecordedRevisionNumber);
173             var length = lines.length;
174             if (length && shouldAddDivider)
175                 this._addDividerToPopover(content);
176             for (var j = 0; j < length; ++j)
177                 content.appendChild(lines[j]);
178             shouldAddDivider = shouldAddDivider || length > 0;
179         }
180
181         var rect = Dashboard.Rect.rectFromClientRect(element.getBoundingClientRect());
182         popover.content = content;
183         popover.present(rect, [Dashboard.RectEdge.MIN_Y, Dashboard.RectEdge.MAX_Y, Dashboard.RectEdge.MAX_X, Dashboard.RectEdge.MIN_X]);
184
185         return true;
186     },
187
188     _presentPopoverForRevisionRange: function(element, popover, context)
189     {
190         var content = document.createElement("div");
191         content.className = "commit-history-popover";
192
193         // FIXME: Nothing guarantees that Trac has historical data for these revisions.
194         var linesForCommits = this._popoverLinesForCommitRange(context.trac, context.branch, context.firstRevision, context.lastRevision);
195
196         var line = document.createElement("div");
197         line.className = "title";
198
199         if (linesForCommits.length) {
200             line.textContent = "commits since previous result";
201             content.appendChild(line);
202             this._addDividerToPopover(content);
203         } else {
204             line.textContent = "no commits to " + context.branch.name + " since previous result";
205             content.appendChild(line);
206         }
207
208         for (var i = 0; i != linesForCommits.length; ++i)
209             content.appendChild(linesForCommits[i]);
210
211         var rect = Dashboard.Rect.rectFromClientRect(element.getBoundingClientRect());
212         popover.content = content;
213         popover.present(rect, [Dashboard.RectEdge.MIN_Y, Dashboard.RectEdge.MAX_Y, Dashboard.RectEdge.MAX_X, Dashboard.RectEdge.MIN_X]);
214
215         return true;
216     },
217
218     _revisionContentWithPopoverForIteration: function(iteration, previousIteration, branch)
219     {
220         var repository = branch.repository;
221         var repositoryName = repository.name;
222         console.assert(iteration.revision[repositoryName]);
223         var content = document.createElement("span");
224         var revision = iteration.revision[repositoryName];
225         if (repository.isSVN)
226             content.textContent = "r" + revision;
227         else if (repository.isGit) {
228             // Truncating for display. Git traditionally uses seven characters for a short hash.
229             content.textContent = revision.substr(0, 7);
230         } else
231             console.assert(false, "Should not get here; " + repository.name + " did not specify a known VCS type.");
232         content.classList.add("revision-number");
233
234         if (previousIteration) {
235             console.assert(previousIteration.revision[repositoryName]);
236             var context = {
237                 trac: repository.trac,
238                 branch: branch,
239                 firstRevision: previousIteration.revision[repositoryName] + 1,
240                 lastRevision: iteration.revision[repositoryName]
241             };
242             if (context.firstRevision <= context.lastRevision)
243                 new PopoverTracker(content, this._presentPopoverForRevisionRange.bind(this), context);
244         }
245
246         return content;
247     },
248
249     _addIterationHeadingToPopover: function(iteration, content, additionalText)
250     {
251         var title = document.createElement("div");
252         title.className = "popover-iteration-heading";
253
254         var queueName = document.createElement("span");
255         queueName.className = "queue-name";
256         queueName.textContent = iteration.queue.id;
257         title.appendChild(queueName);
258
259         var buildbotLink = document.createElement("a");
260         buildbotLink.className = "buildbot-link";
261         buildbotLink.textContent = "build #" + iteration.id;
262         buildbotLink.href = iteration.queue.buildbot.buildPageURLForIteration(iteration);
263         buildbotLink.target = "_blank";
264         title.appendChild(buildbotLink);
265
266         if (additionalText) {
267             var additionalTextElement = document.createElement("span");
268             additionalTextElement.className = "additional-text";
269             additionalTextElement.textContent = additionalText;
270             title.appendChild(additionalTextElement);
271         }
272
273         content.appendChild(title);
274     },
275
276     _addDividerToPopover: function(content)
277     {
278         var divider = document.createElement("div");
279         divider.className = "divider";
280         content.appendChild(divider);
281     },
282
283     revisionContentForIteration: function(iteration, previousDisplayedIteration)
284     {
285         var fragment = document.createDocumentFragment();
286         var shouldAddPlusSign = false;
287         var branches = iteration.queue.branches;
288         for (var i = 0; i < branches.length; ++i) {
289             var branch = branches[i];
290             if (!iteration.revision[branch.repository.name])
291                 continue;
292             var content = this._revisionContentWithPopoverForIteration(iteration, previousDisplayedIteration, branch);
293             if (shouldAddPlusSign)
294                 fragment.appendChild(document.createTextNode(" \uff0b "));
295             fragment.appendChild(content);
296             shouldAddPlusSign = true;
297         }
298         return fragment;
299     },
300
301     appendBuildStyle: function(queues, defaultLabel, appendFunction)
302     {
303         queues.forEach(function(queue) {
304             var releaseLabel = document.createElement("a");
305             releaseLabel.classList.add("queueLabel");
306             releaseLabel.textContent = queue.heading ? queue.heading : defaultLabel;
307             releaseLabel.href = queue.overviewURL;
308             releaseLabel.target = "_blank";
309             this.element.appendChild(releaseLabel);
310
311             appendFunction.call(this, queue);
312         }.bind(this));
313     },
314
315     _updateQueues: function()
316     {
317         this.queues.forEach(function(queue) { queue.update(); });
318     },
319
320     _queueIterationsAdded: function(event)
321     {
322         this.updateSoon();
323
324         event.data.addedIterations.forEach(function(iteration) {
325             iteration.addEventListener(BuildbotIteration.Event.Updated, this._iterationUpdated, this);
326             iteration.addEventListener(BuildbotIteration.Event.UnauthorizedAccess, this._unauthorizedAccess, this);
327         }.bind(this));
328     },
329
330     _iterationUpdated: function(event)
331     {
332         this.updateSoon();
333     },
334     
335     _newCommitsRecorded: function(event)
336     {
337         this.updateSoon();
338     },
339
340     _unauthorizedAccess: function(event)
341     {
342         this.updateSoon();
343     }
344
345 };