// Copyright (C) 2013 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: // // * Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // * 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. // * Neither the name of Google Inc. nor the names of its // contributors may be used to endorse or promote products derived from // this software without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND 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 THE COPYRIGHT // OWNER OR 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 history = history || {}; (function() { history.DEFAULT_CROSS_DASHBOARD_STATE_VALUES = { group: null, showAllRuns: false, testType: 'layout-tests', useTestData: false, } history.validateParameter = function(state, key, value, validateFn) { if (validateFn()) { state[key] = value; return true; } else { console.log(key + ' value is not valid: ' + value); return false; } } history.isTreeMap = function() { return string.endsWith(window.location.pathname, 'treemap.html'); } // TODO(jparent): Make private once callers move here. history.queryHashAsMap = function() { var hash = window.location.hash; var paramsList = hash ? hash.substring(1).split('&') : []; var paramsMap = {}; var invalidKeys = []; for (var i = 0; i < paramsList.length; i++) { var thisParam = paramsList[i].split('='); if (thisParam.length != 2) { console.log('Invalid query parameter: ' + paramsList[i]); continue; } paramsMap[thisParam[0]] = decodeURIComponent(thisParam[1]); } // FIXME: remove support for mapping from the master parameter to the group // one once the waterfall starts to pass in the builder name instead. if (paramsMap.master) { var errors = new ui.Errors(); if (paramsMap.master == 'TryServer') 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.'); else if (!builders.masters[paramsMap.master]) errors.addError('ERROR: Unknown master name: ' + paramsMap.master); if (errors.hasErrors()) { errors.show(); window.location.hash = window.location.hash.replace('master=' + paramsMap.master, ''); } else { var groupIndex = paramsMap.master == 'ChromiumWebkit' ? 1 : 0; paramsMap.group = builders.masters[paramsMap.master].groups[groupIndex]; window.location.hash = window.location.hash.replace('master=' + paramsMap.master, 'group=' + encodeURIComponent(paramsMap.group)); delete paramsMap.master; } } // FIXME: Find a better way to do this. For layout-tests, we want the default group to be // the ToT blink group. For other test types, we want it to be the Deps group. if (!paramsMap.group && (!paramsMap.testType || paramsMap.testType == 'layout-tests')) paramsMap.group = builders.groupNamesForTestType('layout-tests')[1]; return paramsMap; } history._diffStates = function(oldState, newState) { // If there is no old state, everything in the current state is new. if (!oldState) return newState; var changedParams = {}; for (curKey in newState) { var oldVal = oldState[curKey]; var newVal = newState[curKey]; // Add new keys or changed values. if (!oldVal || oldVal != newVal) changedParams[curKey] = newVal; } return changedParams; } history._fillMissingValues = function(to, from) { for (var state in from) { if (!(state in to)) to[state] = from[state]; } } history.History = function(configuration) { this.crossDashboardState = {}; this.dashboardSpecificState = {}; if (configuration) { this._defaultDashboardSpecificStateValues = configuration.defaultStateValues; this._handleValidHashParameter = configuration.handleValidHashParameter; this._handleQueryParameterChange = configuration.handleQueryParameterChange || function(historyInstance, params) { return true; }; this._dashboardSpecificInvalidatingParameters = configuration.invalidatingHashParameters; this._generatePage = configuration.generatePage; } } history.reloadRequiringParameters = ['showAllRuns', 'group', 'testType']; var CROSS_DB_INVALIDATING_PARAMETERS = { 'testType': 'group' }; history.History.prototype = { initialize: function() { window.onhashchange = this._handleLocationChange.bind(this); this._handleLocationChange(); }, isLayoutTestResults: function() { return this.crossDashboardState.testType == 'layout-tests'; }, isGPUTestResults: function() { return this.crossDashboardState.testType == 'gpu_tests'; }, parseCrossDashboardParameters: function() { this.crossDashboardState = {}; var parameters = history.queryHashAsMap(); for (parameterName in history.DEFAULT_CROSS_DASHBOARD_STATE_VALUES) this.parseParameter(parameters, parameterName); history._fillMissingValues(this.crossDashboardState, history.DEFAULT_CROSS_DASHBOARD_STATE_VALUES); }, _parseDashboardSpecificParameters: function() { this.dashboardSpecificState = {}; var parameters = history.queryHashAsMap(); for (parameterName in this._defaultDashboardSpecificStateValues) this.parseParameter(parameters, parameterName); }, // TODO(jparent): Make private once callers move here. parseParameters: function() { var oldCrossDashboardState = this.crossDashboardState; var oldDashboardSpecificState = this.dashboardSpecificState; this.parseCrossDashboardParameters(); // Some parameters require loading different JSON files when the value changes. Do a reload. if (Object.keys(oldCrossDashboardState).length) { for (var key in this.crossDashboardState) { if (oldCrossDashboardState[key] != this.crossDashboardState[key] && history.reloadRequiringParameters.indexOf(key) != -1) { window.location.reload(); return false; } } } this._parseDashboardSpecificParameters(); var dashboardSpecificDiffState = history._diffStates(oldDashboardSpecificState, this.dashboardSpecificState); history._fillMissingValues(this.dashboardSpecificState, this._defaultDashboardSpecificStateValues); // FIXME: dashboard_base shouldn't know anything about specific dashboard specific keys. if (dashboardSpecificDiffState.builder) delete this.dashboardSpecificState.tests; if (this.dashboardSpecificState.tests) delete this.dashboardSpecificState.builder; var shouldGeneratePage = true; if (Object.keys(dashboardSpecificDiffState).length) shouldGeneratePage = this._handleQueryParameterChange(this, dashboardSpecificDiffState); return shouldGeneratePage; }, // TODO(jparent): Make private once callers move here. parseParameter: function(parameters, key) { if (!(key in parameters)) return; var value = parameters[key]; if (!this._handleValidHashParameterWrapper(key, value)) console.log("Invalid query parameter: " + key + '=' + value); }, // Takes a key and a value and sets the this.dashboardSpecificState[key] = value iff key is // a valid hash parameter and the value is a valid value for that key. Handles // cross-dashboard parameters then falls back to calling // handleValidHashParameter for dashboard-specific parameters. // // @return {boolean} Whether the key what inserted into the this.dashboardSpecificState. _handleValidHashParameterWrapper: function(key, value) { switch(key) { case 'testType': history.validateParameter(this.crossDashboardState, key, value, function() { return builders.testTypes.indexOf(value) != -1; }); return true; case 'group': history.validateParameter(this.crossDashboardState, key, value, function() { return builders.getAllGroupNames().indexOf(value) != -1; }); return true; case 'useTestData': case 'showAllRuns': this.crossDashboardState[key] = value == 'true'; return true; default: return this._handleValidHashParameter(this, key, value); } }, queryParameterValue: function(parameter) { return this.dashboardSpecificState[parameter] || this.crossDashboardState[parameter]; }, // Sets the page state. Takes varargs of key, value pairs. setQueryParameter: function(var_args) { var queryParamsAsState = {}; for (var i = 0; i < arguments.length; i += 2) { var key = arguments[i]; queryParamsAsState[key] = arguments[i + 1]; } this.invalidateQueryParameters(queryParamsAsState); var newState = this._combinedDashboardState(); for (var key in queryParamsAsState) { newState[key] = queryParamsAsState[key]; } // Note: We use window.location.hash rather that window.location.replace // because of bugs in Chrome where extra entries were getting created // when back button was pressed and full page navigation was occuring. // FIXME: file those bugs. window.location.hash = this._permaLinkURLHash(newState); }, toggleQueryParameter: function(param) { this.setQueryParameter(param, !this.queryParameterValue(param)); }, invalidateQueryParameters: function(queryParamsAsState) { for (var key in queryParamsAsState) { if (key in CROSS_DB_INVALIDATING_PARAMETERS) delete this.crossDashboardState[CROSS_DB_INVALIDATING_PARAMETERS[key]]; if (this._dashboardSpecificInvalidatingParameters && key in this._dashboardSpecificInvalidatingParameters) delete this.dashboardSpecificState[this._dashboardSpecificInvalidatingParameters[key]]; } }, _joinParameters: function(stateObject) { var state = []; for (var key in stateObject) { var value = stateObject[key]; if (value != this._defaultValue(key)) state.push(key + '=' + encodeURIComponent(value)); } return state.join('&'); }, _permaLinkURLHash: function(opt_state) { var state = opt_state || this._combinedDashboardState(); return '#' + this._joinParameters(state); }, _combinedDashboardState: function() { var combinedState = Object.create(this.dashboardSpecificState); for (var key in this.crossDashboardState) combinedState[key] = this.crossDashboardState[key]; return combinedState; }, _defaultValue: function(key) { if (key in this._defaultDashboardSpecificStateValues) return this._defaultDashboardSpecificStateValues[key]; return history.DEFAULT_CROSS_DASHBOARD_STATE_VALUES[key]; }, _handleLocationChange: function() { if (this.parseParameters()) this._generatePage(this); } } })();