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 26 var ui = ui || {}; 27 28 (function () { 29 30 ui.displayURLForBuilder = function(builderName) 31 { 32 return config.waterfallURL + '?' + $.param({ 33 'builder': builderName 34 }); 35 } 36 37 ui.kUseNewWindowForLinksSetting = 'gardenomatic.use-new-window-for-links'; 38 39 ui.displayNameForBuilder = function(builderName) 40 { 41 return builderName.replace(/Webkit /, ''); 42 } 43 44 ui.urlForTest = function(testName) 45 { 46 return 'http://trac.webkit.org/browser/trunk/LayoutTests/' + testName; 47 } 48 49 ui.urlForCrbug = function(bugID) 50 { 51 return 'http://crbug.com/' + bugID; 52 } 53 54 ui.urlForFlakinessDashboard = function(opt_testNameList) 55 { 56 var testsParameter = opt_testNameList ? opt_testNameList.join(',') : ''; 57 return 'http://test-results.appspot.com/dashboards/flakiness_dashboard.html#tests=' + encodeURIComponent(testsParameter); 58 } 59 60 ui.urlForEmbeddedFlakinessDashboard = function(opt_testNameList) 61 { 62 return ui.urlForFlakinessDashboard(opt_testNameList) + '&showChrome=false'; 63 } 64 65 ui.rolloutReasonForTestNameList = function(testNameList) 66 { 67 return 'Broke:\n' + testNameList.map(function(testName) { 68 return '* ' + testName; 69 }).join('\n'); 70 } 71 72 ui.setTargetForLink = function(anchor) 73 { 74 if (anchor.href.indexOf('#') === 0) 75 return; 76 if (ui.useNewWindowForLinks) 77 anchor.target = '_blank'; 78 else 79 anchor.removeAttribute('target'); 80 } 81 82 ui.setUseNewWindowForLinks = function(enabled) 83 { 84 ui.useNewWindowForLinks = enabled; 85 if (enabled) 86 localStorage[ui.kUseNewWindowForLinksSetting] = 'true'; 87 else 88 delete localStorage[ui.kUseNewWindowForLinksSetting]; 89 90 $('a').each(function() { 91 ui.setTargetForLink(this); 92 }); 93 } 94 ui.setUseNewWindowForLinks(!!localStorage[ui.kUseNewWindowForLinksSetting]); 95 96 ui.createLinkNode = function(url, textContent) 97 { 98 var link = document.createElement('a'); 99 link.href = url; 100 ui.setTargetForLink(link); 101 link.appendChild(document.createTextNode(textContent)); 102 return link; 103 } 104 105 ui.onebar = base.extends('div', { 106 init: function() 107 { 108 this.id = 'onebar'; 109 this.innerHTML = 110 '<ul>' + 111 '<li><a href="#unexpected">Unexpected Failures</a></li>' + 112 '<li><a href="#expected">Expected Failures</a></li>' + 113 '<li><a href="#results">Results</a></li>' + 114 '</ul>' + 115 '<div id="link-handling"><input type="checkbox" id="new-window-for-links"><label for="new-window-for-links">Open links in new window</label></div>' + 116 '<div id="unexpected"></div>' + 117 '<div id="expected"></div>' + 118 '<div id="results"></div>'; 119 this._tabNames = [ 120 'unexpected', 121 'expected', 122 'results', 123 ] 124 125 this._tabIndexToSavedScrollOffset = {}; 126 this._tabs = $(this).tabs({ 127 disabled: [2], 128 show: function(event, ui) { this._restoreScrollOffset(ui.index); }, 129 select: function(event, ui) { 130 this._saveScrollOffset(); 131 window.location.hash = ui.tab.hash; 132 }.bind(this) 133 }); 134 }, 135 _saveScrollOffset: function() { 136 var tabIndex = this._tabs.tabs('option', 'selected'); 137 this._tabIndexToSavedScrollOffset[tabIndex] = document.body.scrollTop; 138 }, 139 _restoreScrollOffset: function(tabIndex) 140 { 141 document.body.scrollTop = this._tabIndexToSavedScrollOffset[tabIndex] || 0; 142 }, 143 _setupHistoryHandlers: function() 144 { 145 function currentHash() { 146 var hash = window.location.hash; 147 return (!hash || hash == '#') ? '#unexpected' : hash; 148 } 149 150 var self = this; 151 window.onhashchange = function(event) { 152 var tabName = currentHash().substring(1); 153 self._selectInternal(tabName); 154 }; 155 156 // When navigating from the browser chrome, we'll 157 // scroll to the #tabname contents. popstate fires before 158 // we scroll, so we can save the scroll offset first. 159 window.onpopstate = function() { 160 self._saveScrollOffset(); 161 }; 162 }, 163 _setupLinkSettingHandler: function() 164 { 165 $('#new-window-for-links').attr('checked', ui.useNewWindowForLinks); 166 $('#new-window-for-links').change(function(event) { 167 ui.setUseNewWindowForLinks(this.checked); 168 }); 169 }, 170 attach: function() 171 { 172 document.body.insertBefore(this, document.body.firstChild); 173 this._setupLinkSettingHandler(); 174 this._setupHistoryHandlers(); 175 }, 176 tabNamed: function(tabName) 177 { 178 if (this._tabNames.indexOf(tabName) == -1) 179 return null; 180 tab = document.getElementById(tabName); 181 // We perform this sanity check below to make sure getElementById 182 // hasn't given us a node in some other unrelated part of the document. 183 // that shouldn't happen normally, but it could happen if an attacker 184 // has somehow sneakily added a node to our document. 185 if (tab.parentNode != this) 186 return null; 187 return tab; 188 }, 189 unexpected: function() 190 { 191 return this.tabNamed('unexpected'); 192 }, 193 expected: function() 194 { 195 return this.tabNamed('expected'); 196 }, 197 results: function() 198 { 199 return this.tabNamed('results'); 200 }, 201 _selectInternal: function(tabName) { 202 var tabIndex = this._tabNames.indexOf(tabName); 203 this._tabs.tabs('enable', tabIndex); 204 this._tabs.tabs('select', tabIndex); 205 }, 206 select: function(tabName) 207 { 208 this._saveScrollOffset(); 209 this._selectInternal(tabName); 210 window.location = '#' + tabName; 211 } 212 }); 213 214 ui.TreeStatus = base.extends('div', { 215 addStatus: function(name) 216 { 217 var label = document.createElement('div'); 218 label.textContent = " " + name + ' status: '; 219 this.appendChild(label); 220 var statusSpan = document.createElement('span'); 221 statusSpan.textContent = '(Loading...) '; 222 label.appendChild(statusSpan); 223 treestatus.fetchTreeStatus(treestatus.urlByName(name), statusSpan); 224 }, 225 init: function() 226 { 227 this.className = 'treestatus'; 228 this.addStatus('blink'); 229 this.addStatus('chromium'); 230 }, 231 }); 232 233 ui.StatusArea = base.extends('div', { 234 init: function() 235 { 236 // This is a Singleton. 237 if (ui.StatusArea._instance) 238 return ui.StatusArea._instance; 239 ui.StatusArea._instance = this; 240 241 var kMinimumStatusAreaHeightPx = 60; 242 var dragger = document.createElement('div'); 243 var initialY; 244 var initialHeight; 245 dragger.className = 'dragger'; 246 $(dragger).mousedown(function(e) { 247 initialY = e.pageY; 248 initialHeight = $(this).height(); 249 $(document.body).addClass('status-resizing'); 250 }.bind(this)); 251 $(document.body).mouseup(function(e) { 252 initialY = 0; 253 initialHeight = 0; 254 $(document.body).removeClass('status-resizing'); 255 }); 256 $(document.body).mousemove(function(e) { 257 if (initialY) { 258 var newHeight = initialHeight + initialY - e.pageY; 259 if (newHeight >= kMinimumStatusAreaHeightPx) 260 $(this).height(newHeight); 261 e.preventDefault(); 262 } 263 }.bind(this)); 264 this.appendChild(dragger); 265 266 this.contents = document.createElement('div'); 267 this.contents.className = 'contents'; 268 this.appendChild(this.contents); 269 270 this.className = 'status'; 271 document.body.appendChild(this); 272 this._currentId = 0; 273 this._unfinishedIds = {}; 274 275 this.appendChild(new ui.actions.List([new ui.actions.Close()])); 276 $(this).bind('close', this.close.bind(this)); 277 278 var processing = document.createElement('progress'); 279 processing.className = 'process-text'; 280 processing.textContent = 'Processing...'; 281 this.appendChild(processing); 282 }, 283 close: function() 284 { 285 this.style.visibility = 'hidden'; 286 Array.prototype.forEach.call(this.querySelectorAll('.status-content'), function(node) { 287 node.parentNode.removeChild(node); 288 }); 289 }, 290 addMessage: function(id, message) 291 { 292 this.style.visibility = 'visible'; 293 $(this).addClass('processing'); 294 295 var element = document.createElement('div'); 296 $(element).addClass('message').text(message); 297 298 var content = this.querySelector('#' + id); 299 if (!content) { 300 content = document.createElement('div'); 301 content.id = id; 302 content.className = 'status-content'; 303 this.contents.appendChild(content); 304 } 305 306 content.appendChild(element); 307 if (element.offsetTop < this.scrollTop || element.offsetTop + element.offsetHeight > this.scrollTop + this.offsetHeight) 308 this.scrollTop = element.offsetTop; 309 }, 310 // FIXME: It's unclear whether this code could live here or in a controller. 311 addFinalMessage: function(id, message) 312 { 313 this.addMessage(id, message); 314 315 delete this._unfinishedIds[id]; 316 if (!Object.keys(this._unfinishedIds).length) 317 $(this).removeClass('processing'); 318 }, 319 newId: function() { 320 var id = 'status-content-' + ++this._currentId; 321 this._unfinishedIds[id] = 1; 322 return id; 323 } 324 }); 325 326 ui.revisionDetails = base.extends('span', { 327 // We only support 2 levels of visual escalation levels: warning and critical. 328 warnRollRevisionSpanThreshold: 45, 329 criticalRollRevisionSpanThreshold: 80, 330 classNameForUrgencyLevel: function(rollRevisionSpan) { 331 if (rollRevisionSpan < this.criticalRollRevisionSpanThreshold) 332 return "warning"; 333 return "critical"; 334 }, 335 updateUI: function(totRevision) { 336 this.appendChild(document.createElement("br")); 337 this.appendChild(document.createTextNode('Last roll is to ')); 338 this.appendChild(ui.createLinkNode(trac.changesetURL(this.lastRolledRevision), this.lastRolledRevision)); 339 var rollRevisionSpan = totRevision - this.lastRolledRevision; 340 // Don't clutter the UI if we haven't run behind. 341 if (rollRevisionSpan > this.warnRollRevisionSpanThreshold) { 342 var wrapper = document.createElement("span"); 343 wrapper.className = this.classNameForUrgencyLevel(rollRevisionSpan); 344 wrapper.appendChild(document.createTextNode("(" + rollRevisionSpan + " revisions behind)")); 345 this.appendChild(wrapper); 346 } 347 this.appendChild(document.createTextNode(', current autoroll ')); 348 if (this.roll) { 349 var linkText = "" + this.roll.fromRevision + ":" + this.roll.toRevision; 350 this.appendChild(ui.createLinkNode(this.roll.url, linkText)); 351 if (this.roll.isStopped) 352 this.appendChild(document.createTextNode(' (STOPPED) ')); 353 } else { 354 this.appendChild(document.createTextNode(' None')); 355 } 356 }, 357 init: function() { 358 var theSpan = this; 359 theSpan.appendChild(document.createTextNode('Latest revision processed by every bot: ')); 360 361 var latestRevision = model.latestRevisionWithNoBuildersInFlight(); 362 var latestRevisions = model.latestRevisionByBuilder(); 363 364 // Get the list of builders sorted with the most recent one first. 365 var builders = Object.keys(latestRevisions); 366 builders.sort(function (a, b) { return parseInt(latestRevisions[b]) - parseInt(latestRevisions[a]);}); 367 368 var summaryNode = document.createElement('summary'); 369 var summaryLinkNode = ui.createLinkNode(trac.changesetURL(latestRevision), latestRevision); 370 summaryNode.appendChild(summaryLinkNode); 371 372 var revisionsTableNode = document.createElement('table'); 373 builders.forEach(function(builderName) { 374 var trNode = document.createElement('tr'); 375 376 var tdNode = document.createElement('td'); 377 tdNode.appendChild(ui.createLinkNode(ui.displayURLForBuilder(builderName), builderName.replace('WebKit ', ''))); 378 trNode.appendChild(tdNode); 379 380 var tdNode = document.createElement('td'); 381 tdNode.appendChild(document.createTextNode(latestRevisions[builderName])); 382 trNode.appendChild(tdNode); 383 384 revisionsTableNode.appendChild(trNode); 385 }); 386 387 var revisionsNode = document.createElement('details'); 388 revisionsNode.appendChild(summaryNode); 389 revisionsNode.appendChild(revisionsTableNode); 390 theSpan.appendChild(revisionsNode); 391 392 // This adds a pop-up when we hover over the summary if the details aren't being shown. 393 var revisionsPopUp = $('<span id="revisionPopUp">').appendTo(summaryLinkNode); 394 revisionsPopUp.append($(revisionsTableNode).clone()); 395 $(summaryLinkNode).mouseover(function(ev) { 396 if (!revisionsNode.open) { 397 var tPosX = $(summaryNode).position().left; 398 var tPosY = $(summaryNode).position().top + 16; 399 $(revisionsPopUp).css({'position': 'absolute', 'top': tPosY, 'left': tPosX}); 400 $(revisionsPopUp).addClass('active'); 401 } 402 }); 403 $(summaryLinkNode).mouseout(function(ev) { 404 if (!revisionsNode.open) { 405 $(revisionsPopUp).removeClass("active"); 406 } 407 }); 408 409 var totRevision = model.latestRevision(); 410 theSpan.appendChild(document.createTextNode(', trunk is at ')); 411 theSpan.appendChild(ui.createLinkNode(trac.changesetURL(totRevision), totRevision)); 412 413 Promise.all([checkout.lastBlinkRollRevision(), rollbot.fetchCurrentRoll()]).then(function(results) { 414 theSpan.lastRolledRevision = results[0]; 415 theSpan.roll = results[1]; 416 theSpan.updateUI(totRevision); 417 }); 418 } 419 }); 420 421 })(); 422