/* * Copyright (C) 2011 Google Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF * THE POSSIBILITY OF SUCH DAMAGE. */ var ui = ui || {}; (function () { ui.displayURLForBuilder = function(builderName) { return config.kPlatforms[config.currentPlatform].waterfallURL + '?' + $.param({ 'builder': builderName }); } ui.displayNameForBuilder = function(builderName) { return builderName.replace(/Webkit /, ''); } ui.urlForTest = function(testName) { return 'http://trac.webkit.org/browser/trunk/LayoutTests/' + testName; } ui.urlForFlakinessDashboard = function(opt_testNameList) { var testsParameter = opt_testNameList ? opt_testNameList.join(',') : ''; return 'http://test-results.appspot.com/dashboards/flakiness_dashboard.html#tests=' + encodeURIComponent(testsParameter); } ui.urlForEmbeddedFlakinessDashboard = function(opt_testNameList) { return ui.urlForFlakinessDashboard(opt_testNameList) + '&showChrome=false'; } ui.rolloutReasonForTestNameList = function(testNameList) { return 'Broke:\n' + testNameList.map(function(testName) { return '* ' + testName; }).join('\n'); } ui.onebar = base.extends('div', { init: function() { this.id = 'onebar'; this.innerHTML = '' + '
' + '
' + '
'; this._tabNames = [ 'unexpected', 'expected', 'results', ] this._tabIndexToSavedScrollOffset = {}; this._tabs = $(this).tabs({ disabled: [2], show: function(event, ui) { this._restoreScrollOffset(ui.index); }, }); }, _saveScrollOffset: function() { var tabIndex = this._tabs.tabs('option', 'selected'); this._tabIndexToSavedScrollOffset[tabIndex] = document.body.scrollTop; }, _restoreScrollOffset: function(tabIndex) { document.body.scrollTop = this._tabIndexToSavedScrollOffset[tabIndex] || 0; }, _setupHistoryHandlers: function() { function currentHash() { var hash = window.location.hash; return (!hash || hash == '#') ? '#unexpected' : hash; } var self = this; $('.ui-tabs-nav a').bind('mouseup', function(event) { var href = event.target.getAttribute('href'); var hash = currentHash(); if (href != hash) { self._saveScrollOffset(); window.location = href } }); window.onhashchange = function(event) { var tabName = currentHash().substring(1); self._selectInternal(tabName); }; // When navigating from the browser chrome, we'll // scroll to the #tabname contents. popstate fires before // we scroll, so we can save the scroll offset first. window.onpopstate = function() { self._saveScrollOffset(); }; }, attach: function() { document.body.insertBefore(this, document.body.firstChild); this._setupHistoryHandlers(); }, tabNamed: function(tabName) { if (this._tabNames.indexOf(tabName) == -1) return null; tab = document.getElementById(tabName); // We perform this sanity check below to make sure getElementById // hasn't given us a node in some other unrelated part of the document. // that shouldn't happen normally, but it could happen if an attacker // has somehow sneakily added a node to our document. if (tab.parentNode != this) return null; return tab; }, unexpected: function() { return this.tabNamed('unexpected'); }, expected: function() { return this.tabNamed('expected'); }, results: function() { return this.tabNamed('results'); }, _selectInternal: function(tabName) { var tabIndex = this._tabNames.indexOf(tabName); this._tabs.tabs('enable', tabIndex); this._tabs.tabs('select', tabIndex); }, select: function(tabName) { this._saveScrollOffset(); this._selectInternal(tabName); window.location = '#' + tabName; } }); // FIXME: Loading a module shouldn't set off a timer. The controller should kick this off. setInterval(function() { Array.prototype.forEach.call(document.querySelectorAll("time.relative"), function(time) { time.update && time.update(); }); }, config.kRelativeTimeUpdateFrequency); ui.RelativeTime = base.extends('time', { init: function() { this.className = 'relative'; }, date: function() { return this._date; }, update: function() { this.textContent = this._date ? base.relativizeTime(this._date) : ''; }, setDate: function(date) { this._date = date; this.update(); } }); ui.TreeStatus = base.extends('div', { addStatus: function(name) { var label = document.createElement('div'); label.textContent = " " + name + ' status: '; this.appendChild(label); var statusSpan = document.createElement('span'); statusSpan.textContent = '(Loading...) '; label.appendChild(statusSpan); treestatus.fetchTreeStatus(treestatus.urlByName(name), statusSpan); }, init: function() { this.className = 'treestatus'; this.addStatus('blink'); this.addStatus('chromium'); }, }); ui.StatusArea = base.extends('div', { init: function() { // This is a Singleton. if (ui.StatusArea._instance) return ui.StatusArea._instance; ui.StatusArea._instance = this; this.className = 'status'; document.body.appendChild(this); this._currentId = 0; this._unfinishedIds = {}; this.appendChild(new ui.actions.List([new ui.actions.Close()])); $(this).bind('close', this.close.bind(this)); var processing = document.createElement('progress'); processing.className = 'process-text'; processing.textContent = 'Processing...'; this.appendChild(processing); }, close: function() { this.style.visibility = 'hidden'; Array.prototype.forEach.call(this.querySelectorAll('.status-content'), function(node) { node.parentNode.removeChild(node); }); }, addMessage: function(id, message) { this.style.visibility = 'visible'; $(this).addClass('processing'); var element = document.createElement('div'); $(element).addClass('message').text(message); var content = this.querySelector('#' + id); if (!content) { content = document.createElement('div'); content.id = id; content.className = 'status-content'; this.appendChild(content); } content.appendChild(element); if (element.offsetTop < this.scrollTop || element.offsetTop + element.offsetHeight > this.scrollTop + this.offsetHeight) this.scrollTop = element.offsetTop; }, // FIXME: It's unclear whether this code could live here or in a controller. addFinalMessage: function(id, message) { this.addMessage(id, message); delete this._unfinishedIds[id]; if (!Object.keys(this._unfinishedIds).length) $(this).removeClass('processing'); }, newId: function() { var id = 'status-content-' + ++this._currentId; this._unfinishedIds[id] = 1; return id; } }); ui.revisionDetails = base.extends('span', { init: function() { var theSpan = this; theSpan.appendChild(document.createTextNode('Latest revision processed by every bot: ')); var latestRevision = model.latestRevisionWithNoBuildersInFlight(); var latestRevisions = model.latestRevisionByBuilder(); // Get the list of builders sorted with the most recent one first. var builders = Object.keys(latestRevisions); builders.sort(function (a, b) { return parseInt(latestRevisions[b]) - parseInt(latestRevisions[a])}); var summaryNode = document.createElement('summary'); var summaryLinkNode = base.createLinkNode(trac.changesetURL(latestRevision), latestRevision); summaryNode.appendChild(summaryLinkNode); var revisionsTableNode = document.createElement('table'); builders.forEach(function(builderName) { var trNode = document.createElement('tr'); var tdNode = document.createElement('td'); tdNode.appendChild(base.createLinkNode(ui.displayURLForBuilder(builderName), builderName.replace('WebKit ', ''))); trNode.appendChild(tdNode); var tdNode = document.createElement('td'); tdNode.appendChild(document.createTextNode(latestRevisions[builderName])); trNode.appendChild(tdNode) revisionsTableNode.appendChild(trNode) }); var revisionsNode = document.createElement('details'); revisionsNode.appendChild(summaryNode); revisionsNode.appendChild(revisionsTableNode); theSpan.appendChild(revisionsNode); // This adds a pop-up when we hover over the summary if the details aren't being shown. var revisionsPopUp = $('').appendTo(summaryLinkNode); revisionsPopUp.append($(revisionsTableNode).clone()); $(summaryLinkNode).mouseover(function(ev) { if (!revisionsNode.open) { var tPosX = $(summaryNode).position().left; var tPosY = $(summaryNode).position().top + 16; $(revisionsPopUp).css({'position': 'absolute', 'top': tPosY, 'left': tPosX}); $(revisionsPopUp).addClass('active') } }); $(summaryLinkNode).mouseout(function(ev) { if (!revisionsNode.open) { $(revisionsPopUp).removeClass("active"); } }); var totRevision = model.latestRevision(); theSpan.appendChild(document.createTextNode(', trunk is at ')); theSpan.appendChild(base.createLinkNode(trac.changesetURL(totRevision), totRevision)); checkout.lastBlinkRollRevision(function(revision) { theSpan.appendChild(document.createTextNode(', last roll is to ')); theSpan.appendChild(base.createLinkNode(trac.changesetURL(revision), revision)); }, function() {}); rollbot.fetchCurrentRoll(function(roll) { theSpan.appendChild(document.createTextNode(', current autoroll ')); if (roll) { var linkText = "" + roll.fromRevision + ":" + roll.toRevision; theSpan.appendChild(base.createLinkNode(roll.url, linkText)); if (roll.isStopped) theSpan.appendChild(document.createTextNode(' (STOPPED) ')); } else { theSpan.appendChild(document.createTextNode(' None')); } }); } }); })();