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