1/*
2 * Copyright (C) 2011 Google 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
26var ui = ui || {};
27
28(function () {
29
30ui.displayURLForBuilder = function(builderName)
31{
32    return config.kPlatforms[config.currentPlatform].waterfallURL + '?' + $.param({
33        'builder': builderName
34    });
35}
36
37ui.displayNameForBuilder = function(builderName)
38{
39    return builderName.replace(/Webkit /, '');
40}
41
42ui.urlForTest = function(testName)
43{
44    return 'http://trac.webkit.org/browser/trunk/LayoutTests/' + testName;
45}
46
47ui.urlForFlakinessDashboard = function(opt_testNameList)
48{
49    var testsParameter = opt_testNameList ? opt_testNameList.join(',') : '';
50    return 'http://test-results.appspot.com/dashboards/flakiness_dashboard.html#tests=' + encodeURIComponent(testsParameter);
51}
52
53ui.urlForEmbeddedFlakinessDashboard = function(opt_testNameList)
54{
55    return ui.urlForFlakinessDashboard(opt_testNameList) + '&showChrome=false';
56}
57
58ui.rolloutReasonForTestNameList = function(testNameList)
59{
60    return 'Broke:\n' + testNameList.map(function(testName) {
61        return '* ' + testName;
62    }).join('\n');
63}
64
65ui.onebar = base.extends('div', {
66    init: function()
67    {
68        this.id = 'onebar';
69        this.innerHTML =
70            '<ul>' +
71                '<li><a href="#unexpected">Unexpected Failures</a></li>' +
72                '<li><a href="#expected">Expected Failures</a></li>' +
73                '<li><a href="#results">Results</a></li>' +
74                '<li><a href="#perf">perf</a></li>' +
75            '</ul>' +
76            '<div id="unexpected"></div>' +
77            '<div id="expected"></div>' +
78            '<div id="results"></div>' +
79            '<div id="perf"></div>';
80        this._tabNames = [
81            'unexpected',
82            'expected',
83            'results',
84            'perf',
85        ]
86
87        this._tabIndexToSavedScrollOffset = {};
88        this._tabs = $(this).tabs({
89            disabled: [2],
90            show: function(event, ui) { this._restoreScrollOffset(ui.index); },
91        });
92    },
93    _saveScrollOffset: function() {
94        var tabIndex = this._tabs.tabs('option', 'selected');
95        this._tabIndexToSavedScrollOffset[tabIndex] = document.body.scrollTop;
96    },
97    _restoreScrollOffset: function(tabIndex)
98    {
99        document.body.scrollTop = this._tabIndexToSavedScrollOffset[tabIndex] || 0;
100    },
101    _setupHistoryHandlers: function()
102    {
103        function currentHash() {
104            var hash = window.location.hash;
105            return (!hash || hash == '#') ? '#unexpected' : hash;
106        }
107
108        var self = this;
109        $('.ui-tabs-nav a').bind('mouseup', function(event) {
110            var href = event.target.getAttribute('href');
111            var hash = currentHash();
112            if (href != hash) {
113                self._saveScrollOffset();
114                window.location = href
115            }
116        });
117
118        window.onhashchange = function(event) {
119            var tabName = currentHash().substring(1);
120            self._selectInternal(tabName);
121        };
122
123        // When navigating from the browser chrome, we'll
124        // scroll to the #tabname contents. popstate fires before
125        // we scroll, so we can save the scroll offset first.
126        window.onpopstate = function() {
127            self._saveScrollOffset();
128        };
129    },
130    attach: function()
131    {
132        document.body.insertBefore(this, document.body.firstChild);
133        this._setupHistoryHandlers();
134    },
135    tabNamed: function(tabName)
136    {
137        if (this._tabNames.indexOf(tabName) == -1)
138            return null;
139        tab = document.getElementById(tabName);
140        // We perform this sanity check below to make sure getElementById
141        // hasn't given us a node in some other unrelated part of the document.
142        // that shouldn't happen normally, but it could happen if an attacker
143        // has somehow sneakily added a node to our document.
144        if (tab.parentNode != this)
145            return null;
146        return tab;
147    },
148    unexpected: function()
149    {
150        return this.tabNamed('unexpected');
151    },
152    expected: function()
153    {
154        return this.tabNamed('expected');
155    },
156    results: function()
157    {
158        return this.tabNamed('results');
159    },
160    perf: function()
161    {
162        return this.tabNamed('perf');
163    },
164    _selectInternal: function(tabName) {
165        var tabIndex = this._tabNames.indexOf(tabName);
166        this._tabs.tabs('enable', tabIndex);
167        this._tabs.tabs('select', tabIndex);
168    },
169    select: function(tabName)
170    {
171        this._saveScrollOffset();
172        this._selectInternal(tabName);
173        window.location = '#' + tabName;
174    }
175});
176
177// FIXME: Loading a module shouldn't set off a timer.  The controller should kick this off.
178setInterval(function() {
179    Array.prototype.forEach.call(document.querySelectorAll("time.relative"), function(time) {
180        time.update && time.update();
181    });
182}, config.kRelativeTimeUpdateFrequency);
183
184ui.RelativeTime = base.extends('time', {
185    init: function()
186    {
187        this.className = 'relative';
188    },
189    date: function()
190    {
191        return this._date;
192    },
193    update: function()
194    {
195        this.textContent = this._date ? base.relativizeTime(this._date) : '';
196    },
197    setDate: function(date)
198    {
199        this._date = date;
200        this.update();
201    }
202});
203
204ui.StatusArea = base.extends('div',  {
205    init: function()
206    {
207        // This is a Singleton.
208        if (ui.StatusArea._instance)
209            return ui.StatusArea._instance;
210        ui.StatusArea._instance = this;
211
212        this.className = 'status';
213        document.body.appendChild(this);
214        this._currentId = 0;
215        this._unfinishedIds = {};
216
217        this.appendChild(new ui.actions.List([new ui.actions.Close()]));
218        $(this).bind('close', this.close.bind(this));
219
220        var processing = document.createElement('progress');
221        processing.className = 'process-text';
222        processing.textContent = 'Processing...';
223        this.appendChild(processing);
224    },
225    close: function()
226    {
227        this.style.visibility = 'hidden';
228        Array.prototype.forEach.call(this.querySelectorAll('.status-content'), function(node) {
229            node.parentNode.removeChild(node);
230        });
231    },
232    addMessage: function(id, message)
233    {
234        this.style.visibility = 'visible';
235        $(this).addClass('processing');
236
237        var element = document.createElement('div');
238        $(element).addClass('message').text(message);
239
240        var content = this.querySelector('#' + id);
241        if (!content) {
242            content = document.createElement('div');
243            content.id = id;
244            content.className = 'status-content';
245            this.appendChild(content);
246        }
247
248        content.appendChild(element);
249        if (element.offsetTop < this.scrollTop || element.offsetTop + element.offsetHeight > this.scrollTop + this.offsetHeight)
250            this.scrollTop = element.offsetTop;
251    },
252    // FIXME: It's unclear whether this code could live here or in a controller.
253    addFinalMessage: function(id, message)
254    {
255        this.addMessage(id, message);
256
257        delete this._unfinishedIds[id];
258        if (!Object.keys(this._unfinishedIds).length)
259            $(this).removeClass('processing');
260    },
261    newId: function() {
262        var id = 'status-content-' + ++this._currentId;
263        this._unfinishedIds[id] = 1;
264        return id;
265    }
266});
267
268ui.revisionDetails = base.extends('span', {
269    init: function() {
270        var theSpan = this;
271        theSpan.appendChild(document.createTextNode('Latest revision processed by every bot: '));
272
273        var latestRevision = model.latestRevisionWithNoBuildersInFlight();
274        var latestRevisions = model.latestRevisionByBuilder();
275
276        // Get the list of builders sorted with the most recent one first.
277        var builders = Object.keys(latestRevisions);
278        builders.sort(function (a, b) { return parseInt(latestRevisions[b]) - parseInt(latestRevisions[a])});
279
280        var summaryNode = document.createElement('summary');
281        var summaryLinkNode = base.createLinkNode(trac.changesetURL(latestRevision), latestRevision);
282        summaryNode.appendChild(summaryLinkNode);
283
284        var revisionsTableNode = document.createElement('table');
285        builders.forEach(function(builderName) {
286            var trNode = document.createElement('tr');
287
288            var tdNode = document.createElement('td');
289            tdNode.appendChild(base.createLinkNode(ui.displayURLForBuilder(builderName), builderName.replace('WebKit ', '')));
290            trNode.appendChild(tdNode);
291
292            var tdNode = document.createElement('td');
293            tdNode.appendChild(document.createTextNode(latestRevisions[builderName]));
294            trNode.appendChild(tdNode)
295
296            revisionsTableNode.appendChild(trNode)
297        });
298
299        var revisionsNode = document.createElement('details');
300        revisionsNode.appendChild(summaryNode);
301        revisionsNode.appendChild(revisionsTableNode);
302        theSpan.appendChild(revisionsNode);
303
304        // This adds a pop-up when we hover over the summary if the details aren't being shown.
305        var revisionsPopUp = $('<span id="revisionPopUp">').appendTo(summaryLinkNode);
306        revisionsPopUp.append($(revisionsTableNode).clone());
307        $(summaryLinkNode).mouseover(function(ev) {
308            if (!revisionsNode.open) {
309                var tPosX = $(summaryNode).position().left;
310                var tPosY = $(summaryNode).position().top + 16;
311                $(revisionsPopUp).css({'position': 'absolute', 'top': tPosY, 'left': tPosX});
312                $(revisionsPopUp).addClass('active')
313            }
314        });
315        $(summaryLinkNode).mouseout(function(ev) {
316            if (!revisionsNode.open) {
317                $(revisionsPopUp).removeClass("active");
318            }
319        });
320
321        var totRevision = model.latestRevision();
322        theSpan.appendChild(document.createTextNode(', trunk is at '));
323        theSpan.appendChild(base.createLinkNode(trac.changesetURL(totRevision), totRevision));
324
325        checkout.lastBlinkRollRevision(function(revision) {
326            theSpan.appendChild(document.createTextNode(', last roll is to '));
327            theSpan.appendChild(base.createLinkNode(trac.changesetURL(revision), revision));
328        }, function() {});
329
330        rollbot.fetchCurrentRoll(function(roll) {
331            theSpan.appendChild(document.createTextNode(', current autoroll '));
332            if (roll) {
333                var linkText = "" + roll.fromRevision + ":" + roll.toRevision;
334                theSpan.appendChild(base.createLinkNode(roll.url, linkText));
335                if (roll.isStopped)
336                    theSpan.appendChild(document.createTextNode(' (STOPPED) '));
337            } else {
338                theSpan.appendChild(document.createTextNode(' None'));
339            }
340        });
341    }
342});
343
344})();
345