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 30var history = history || {}; 31 32(function() { 33 34history.DEFAULT_CROSS_DASHBOARD_STATE_VALUES = { 35 group: null, 36 showAllRuns: false, 37 testType: 'layout-tests', 38 useTestData: false, 39} 40 41history.validateParameter = function(state, key, value, validateFn) 42{ 43 if (validateFn()) { 44 state[key] = value; 45 return true; 46 } else { 47 console.log(key + ' value is not valid: ' + value); 48 return false; 49 } 50} 51 52history.isTreeMap = function() 53{ 54 return string.endsWith(window.location.pathname, 'treemap.html'); 55} 56 57// TODO(jparent): Make private once callers move here. 58history.queryHashAsMap = function() 59{ 60 var hash = window.location.hash; 61 var paramsList = hash ? hash.substring(1).split('&') : []; 62 var paramsMap = {}; 63 var invalidKeys = []; 64 for (var i = 0; i < paramsList.length; i++) { 65 var thisParam = paramsList[i].split('='); 66 if (thisParam.length != 2) { 67 console.log('Invalid query parameter: ' + paramsList[i]); 68 continue; 69 } 70 71 paramsMap[thisParam[0]] = decodeURIComponent(thisParam[1]); 72 } 73 74 // FIXME: remove support for mapping from the master parameter to the group 75 // one once the waterfall starts to pass in the builder name instead. 76 if (paramsMap.master) { 77 var errors = new ui.Errors(); 78 if (paramsMap.master == 'TryServer') 79 errors.addError('ERROR: You got here from the trybot waterfall. The try bots do not record data in the flakiness dashboard. Showing results for the regular waterfall.'); 80 else if (!builders.masters[paramsMap.master]) 81 errors.addError('ERROR: Unknown master name: ' + paramsMap.master); 82 83 if (errors.hasErrors()) { 84 errors.show(); 85 window.location.hash = window.location.hash.replace('master=' + paramsMap.master, ''); 86 } else { 87 var groupIndex = paramsMap.master == 'ChromiumWebkit' ? 1 : 0; 88 paramsMap.group = builders.masters[paramsMap.master].groups[groupIndex]; 89 window.location.hash = window.location.hash.replace('master=' + paramsMap.master, 'group=' + encodeURIComponent(paramsMap.group)); 90 delete paramsMap.master; 91 } 92 } 93 94 // FIXME: Find a better way to do this. For layout-tests, we want the default group to be 95 // the ToT blink group. For other test types, we want it to be the Deps group. 96 if (!paramsMap.group && (!paramsMap.testType || paramsMap.testType == 'layout-tests')) 97 paramsMap.group = builders.groupNamesForTestType('layout-tests')[1]; 98 99 return paramsMap; 100} 101 102history._diffStates = function(oldState, newState) 103{ 104 // If there is no old state, everything in the current state is new. 105 if (!oldState) 106 return newState; 107 108 var changedParams = {}; 109 for (curKey in newState) { 110 var oldVal = oldState[curKey]; 111 var newVal = newState[curKey]; 112 // Add new keys or changed values. 113 if (!oldVal || oldVal != newVal) 114 changedParams[curKey] = newVal; 115 } 116 return changedParams; 117} 118 119history._fillMissingValues = function(to, from) 120{ 121 for (var state in from) { 122 if (!(state in to)) 123 to[state] = from[state]; 124 } 125} 126 127history.History = function(configuration) 128{ 129 this.crossDashboardState = {}; 130 this.dashboardSpecificState = {}; 131 132 if (configuration) { 133 this._defaultDashboardSpecificStateValues = configuration.defaultStateValues; 134 this._handleValidHashParameter = configuration.handleValidHashParameter; 135 this._handleQueryParameterChange = configuration.handleQueryParameterChange || function(historyInstance, params) { return true; }; 136 this._dashboardSpecificInvalidatingParameters = configuration.invalidatingHashParameters; 137 this._generatePage = configuration.generatePage; 138 } 139} 140 141history.reloadRequiringParameters = ['showAllRuns', 'group', 'testType']; 142 143var CROSS_DB_INVALIDATING_PARAMETERS = { 144 'testType': 'group' 145}; 146 147history.History.prototype = { 148 initialize: function() 149 { 150 window.onhashchange = this._handleLocationChange.bind(this); 151 this._handleLocationChange(); 152 }, 153 isLayoutTestResults: function() 154 { 155 return this.crossDashboardState.testType == 'layout-tests'; 156 }, 157 isGPUTestResults: function() 158 { 159 return this.crossDashboardState.testType == 'gpu_tests'; 160 }, 161 parseCrossDashboardParameters: function() 162 { 163 this.crossDashboardState = {}; 164 var parameters = history.queryHashAsMap(); 165 for (parameterName in history.DEFAULT_CROSS_DASHBOARD_STATE_VALUES) 166 this.parseParameter(parameters, parameterName); 167 168 history._fillMissingValues(this.crossDashboardState, history.DEFAULT_CROSS_DASHBOARD_STATE_VALUES); 169 }, 170 _parseDashboardSpecificParameters: function() 171 { 172 this.dashboardSpecificState = {}; 173 var parameters = history.queryHashAsMap(); 174 for (parameterName in this._defaultDashboardSpecificStateValues) 175 this.parseParameter(parameters, parameterName); 176 }, 177 // TODO(jparent): Make private once callers move here. 178 parseParameters: function() 179 { 180 var oldCrossDashboardState = this.crossDashboardState; 181 var oldDashboardSpecificState = this.dashboardSpecificState; 182 183 this.parseCrossDashboardParameters(); 184 185 // Some parameters require loading different JSON files when the value changes. Do a reload. 186 if (Object.keys(oldCrossDashboardState).length) { 187 for (var key in this.crossDashboardState) { 188 if (oldCrossDashboardState[key] != this.crossDashboardState[key] && history.reloadRequiringParameters.indexOf(key) != -1) { 189 window.location.reload(); 190 return false; 191 } 192 } 193 } 194 195 this._parseDashboardSpecificParameters(); 196 var dashboardSpecificDiffState = history._diffStates(oldDashboardSpecificState, this.dashboardSpecificState); 197 198 history._fillMissingValues(this.dashboardSpecificState, this._defaultDashboardSpecificStateValues); 199 200 // FIXME: dashboard_base shouldn't know anything about specific dashboard specific keys. 201 if (dashboardSpecificDiffState.builder) 202 delete this.dashboardSpecificState.tests; 203 if (this.dashboardSpecificState.tests) 204 delete this.dashboardSpecificState.builder; 205 206 var shouldGeneratePage = true; 207 if (Object.keys(dashboardSpecificDiffState).length) 208 shouldGeneratePage = this._handleQueryParameterChange(this, dashboardSpecificDiffState); 209 return shouldGeneratePage; 210 }, 211 // TODO(jparent): Make private once callers move here. 212 parseParameter: function(parameters, key) 213 { 214 if (!(key in parameters)) 215 return; 216 var value = parameters[key]; 217 if (!this._handleValidHashParameterWrapper(key, value)) 218 console.log("Invalid query parameter: " + key + '=' + value); 219 }, 220 // Takes a key and a value and sets the this.dashboardSpecificState[key] = value iff key is 221 // a valid hash parameter and the value is a valid value for that key. Handles 222 // cross-dashboard parameters then falls back to calling 223 // handleValidHashParameter for dashboard-specific parameters. 224 // 225 // @return {boolean} Whether the key what inserted into the this.dashboardSpecificState. 226 _handleValidHashParameterWrapper: function(key, value) 227 { 228 switch(key) { 229 case 'testType': 230 history.validateParameter(this.crossDashboardState, key, value, 231 function() { return builders.testTypes.indexOf(value) != -1; }); 232 return true; 233 234 case 'group': 235 history.validateParameter(this.crossDashboardState, key, value, 236 function() { 237 return builders.getAllGroupNames().indexOf(value) != -1; 238 }); 239 return true; 240 241 case 'useTestData': 242 case 'showAllRuns': 243 this.crossDashboardState[key] = value == 'true'; 244 return true; 245 246 default: 247 return this._handleValidHashParameter(this, key, value); 248 } 249 }, 250 queryParameterValue: function(parameter) 251 { 252 return this.dashboardSpecificState[parameter] || this.crossDashboardState[parameter]; 253 }, 254 // Sets the page state. Takes varargs of key, value pairs. 255 setQueryParameter: function(var_args) 256 { 257 var queryParamsAsState = {}; 258 for (var i = 0; i < arguments.length; i += 2) { 259 var key = arguments[i]; 260 queryParamsAsState[key] = arguments[i + 1]; 261 } 262 263 this.invalidateQueryParameters(queryParamsAsState); 264 265 var newState = this._combinedDashboardState(); 266 for (var key in queryParamsAsState) { 267 newState[key] = queryParamsAsState[key]; 268 } 269 270 // Note: We use window.location.hash rather that window.location.replace 271 // because of bugs in Chrome where extra entries were getting created 272 // when back button was pressed and full page navigation was occuring. 273 // FIXME: file those bugs. 274 window.location.hash = this._permaLinkURLHash(newState); 275 }, 276 toggleQueryParameter: function(param) 277 { 278 this.setQueryParameter(param, !this.queryParameterValue(param)); 279 }, 280 invalidateQueryParameters: function(queryParamsAsState) 281 { 282 for (var key in queryParamsAsState) { 283 if (key in CROSS_DB_INVALIDATING_PARAMETERS) 284 delete this.crossDashboardState[CROSS_DB_INVALIDATING_PARAMETERS[key]]; 285 if (this._dashboardSpecificInvalidatingParameters && key in this._dashboardSpecificInvalidatingParameters) 286 delete this.dashboardSpecificState[this._dashboardSpecificInvalidatingParameters[key]]; 287 } 288 }, 289 _joinParameters: function(stateObject) 290 { 291 var state = []; 292 for (var key in stateObject) { 293 var value = stateObject[key]; 294 if (value != this._defaultValue(key)) 295 state.push(key + '=' + encodeURIComponent(value)); 296 } 297 return state.join('&'); 298 }, 299 _permaLinkURLHash: function(opt_state) 300 { 301 var state = opt_state || this._combinedDashboardState(); 302 return '#' + this._joinParameters(state); 303 }, 304 _combinedDashboardState: function() 305 { 306 var combinedState = Object.create(this.dashboardSpecificState); 307 for (var key in this.crossDashboardState) 308 combinedState[key] = this.crossDashboardState[key]; 309 return combinedState; 310 }, 311 _defaultValue: function(key) 312 { 313 if (key in this._defaultDashboardSpecificStateValues) 314 return this._defaultDashboardSpecificStateValues[key]; 315 return history.DEFAULT_CROSS_DASHBOARD_STATE_VALUES[key]; 316 }, 317 _handleLocationChange: function() 318 { 319 if (this.parseParameters()) 320 this._generatePage(this); 321 } 322 323} 324 325})(); 326