UI improvements to Flakiness Dashboard.
[WebKit-https.git] / Tools / TestResultServer / static-dashboards / ui.js
1 // Copyright (C) 2013 Google Inc. All rights reserved.
2 //
3 // Redistribution and use in source and binary forms, with or without
4 // modification, are permitted provided that the following conditions are
5 // met:
6 //
7 //         * Redistributions of source code must retain the above copyright
8 // notice, this list of conditions and the following disclaimer.
9 //         * Redistributions in binary form must reproduce the above
10 // copyright notice, this list of conditions and the following disclaimer
11 // in the documentation and/or other materials provided with the
12 // distribution.
13 //         * Neither the name of Google Inc. nor the names of its
14 // contributors may be used to endorse or promote products derived from
15 // this software without specific prior written permission.
16 //
17 // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18 // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
19 // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
20 // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
21 // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
22 // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
23 // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
24 // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
25 // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
29 var ui = ui || {};
30
31 (function() {
32
33 ui.popup = {};
34
35 ui.popup.hide = function()
36 {
37     var popup = $('popup');
38     if (popup) {
39         popup.parentNode.removeChild(popup);
40         document.removeEventListener('mousedown', ui.popup._handleMouseDown, false);
41     }
42 }
43
44 ui.popup.show = function(target, html)
45 {
46     var popup = $('popup');
47     if (!popup) {
48         popup = document.createElement('div');
49         popup.id = 'popup';
50         document.body.appendChild(popup);
51         document.addEventListener('mousedown', ui.popup._handleMouseDown, false);
52     }
53
54     // Set html first so that we can get accurate size metrics on the popup.
55     popup.innerHTML = html;
56
57     var targetRect = target.getBoundingClientRect();
58
59     var x = Math.min(targetRect.left - 10, document.documentElement.clientWidth - popup.offsetWidth);
60     x = Math.max(0, x);
61     popup.style.left = x + document.body.scrollLeft + 'px';
62
63     var y = targetRect.top + targetRect.height;
64     if (y + popup.offsetHeight > document.documentElement.clientHeight)
65         y = targetRect.top - popup.offsetHeight;
66     y = Math.max(0, y);
67     popup.style.top = y + document.body.scrollTop + 'px';
68 }
69
70 ui.popup._handleMouseDown = function(e) {
71     // Clear the open popup, unless the click was inside the popup.
72     var popup = $('popup');
73     if (popup && e.target != popup && !(popup.compareDocumentPosition(e.target) & 16)) 
74         ui.popup.hide();    
75 }
76
77 ui.html = {};
78
79 ui.html.checkbox = function(queryParameter, label, isChecked, opt_extraJavaScript)
80 {
81     var js = opt_extraJavaScript || '';
82     return '<label style="padding-left: 2em">' +
83         '<input type="checkbox" onchange="g_history.toggleQueryParameter(\'' + queryParameter + '\');' + js + '" ' +
84             (isChecked ? 'checked' : '') + '>' + label +
85         '</label> ';
86 }
87
88 ui.html.select = function(label, queryParameter, options)
89 {
90     var html = '<label style="padding-left: 2em">' + label + ': ' +
91         '<select onchange="g_history.setQueryParameter(\'' + queryParameter + '\', this[this.selectedIndex].value)">';
92
93     for (var i = 0; i < options.length; i++) {
94         var value = options[i];
95         html += '<option value="' + value + '" ' +
96             (g_history.queryParameterValue(queryParameter) == value ? 'selected' : '') +
97             '>' + value + '</option>'
98     }
99     html += '</select></label> ';
100     return html;
101 }
102
103 // Returns the HTML for the select element to switch to different testTypes.
104 ui.html.testTypeSwitcher = function(opt_noBuilderMenu, opt_extraHtml, opt_includeNoneBuilder)
105 {
106     var html = '<div style="border-bottom:1px dashed">';
107     html += '' +
108         ui.html._dashboardLink('Stats', 'aggregate_results.html') +
109         ui.html._dashboardLink('Timeline', 'timeline_explorer.html') +
110         ui.html._dashboardLink('Results', 'flakiness_dashboard.html') +
111         ui.html._dashboardLink('Treemap', 'treemap.html');
112
113     if (!opt_noBuilderMenu) {
114         var buildersForMenu = Object.keys(currentBuilders());
115         if (opt_includeNoneBuilder)
116             buildersForMenu.unshift('--------------');
117         html += ui.html.select('Builder', 'builder', buildersForMenu);
118     }
119
120     if (!history.isTreeMap())
121         html += ui.html.checkbox('showAllRuns', 'Show all runs', g_history.crossDashboardState.showAllRuns);
122
123     if (opt_extraHtml)
124         html += opt_extraHtml;
125     return html + '</div>';
126 }
127
128 ui.html._loadDashboard = function(fileName)
129 {
130     var pathName = window.location.pathname;
131     pathName = pathName.substring(0, pathName.lastIndexOf('/') + 1);
132     window.location = pathName + fileName + window.location.hash;
133 }
134
135 ui.html._topLink = function(html, onClick, isSelected)
136 {
137     var cssText = isSelected ? 'font-weight: bold;' : 'color:blue;text-decoration:underline;cursor:pointer;';
138     cssText += 'margin: 0 5px;';
139     return '<span style="' + cssText + '" onclick="' + onClick + '">' + html + '</span>';
140 }
141
142 ui.html._dashboardLink = function(html, fileName)
143 {
144     var pathName = window.location.pathname;
145     var currentFileName = pathName.substring(pathName.lastIndexOf('/') + 1);
146     var isSelected = currentFileName == fileName;
147     var onClick = 'ui.html._loadDashboard(\'' + fileName + '\')';
148     return ui.html._topLink(html, onClick, isSelected);
149 }
150
151 ui.html._revisionLink = function(results, index, key, singleUrlTemplate, rangeUrlTemplate)
152 {
153     var currentRevision = parseInt(results[key][index], 10);
154     var previousRevision = parseInt(results[key][index + 1], 10);
155
156     function singleUrl()
157     {
158         return singleUrlTemplate.replace('<rev>', currentRevision);
159     }
160
161     function rangeUrl()
162     {
163         return rangeUrlTemplate.replace('<rev1>', currentRevision).replace('<rev2>', previousRevision + 1);
164     }
165
166     if (currentRevision == previousRevision)
167         return 'At <a href="' + singleUrl() + '">r' + currentRevision    + '</a>';
168     else if (currentRevision - previousRevision == 1)
169         return '<a href="' + singleUrl() + '">r' + currentRevision    + '</a>';
170     else
171         return '<a href="' + rangeUrl() + '">r' + (previousRevision + 1) + ' to r' + currentRevision + '</a>';
172 }
173
174 ui.html.webKitRevisionLink = function(results, index)
175 {
176     return ui.html._revisionLink(
177         results,
178         index,
179         WEBKIT_REVISIONS_KEY,
180         'https://trac.webkit.org/changeset/<rev>',
181         'https://trac.webkit.org/log/trunk/?rev=<rev1>&stop_rev=<rev2>&limit=100&verbose=on');
182 }
183
184
185 ui.Errors = function() {
186     this._messages = '';
187     // Element to display the errors within.
188     this._containerElement = null;
189 }
190
191 ui.Errors.prototype = {
192     show: function()
193     {
194         if (!this._containerElement) {
195             this._containerElement = document.createElement('H2');
196             this._containerElement.style.color = 'red';
197             this._containerElement.id = 'errors';
198             document.body.appendChild(this._containerElement);
199         }
200
201         this._containerElement.innerHTML = this._messages;
202     },
203     // Record a new error message.
204     addError: function(message)
205     {
206         this._messages += message + '<br>';
207     }
208 }
209
210 })();