Home | History | Annotate | Download | only in static-dashboards
      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 
     30 var history = history || {};
     31 
     32 (function() {
     33 
     34 history.DEFAULT_CROSS_DASHBOARD_STATE_VALUES = {
     35     group: null,
     36     showAllRuns: false,
     37     testType: 'layout-tests',
     38     useTestData: false,
     39 }
     40 
     41 history.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 
     52 history.isTreeMap = function()
     53 {
     54     return string.endsWith(window.location.pathname, 'treemap.html');
     55 }
     56 
     57 // TODO(jparent): Make private once callers move here.
     58 history.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 
    102 history._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 
    119 history._fillMissingValues = function(to, from)
    120 {
    121     for (var state in from) {
    122         if (!(state in to))
    123             to[state] = from[state];
    124     }
    125 }
    126 
    127 history.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 
    141 history.reloadRequiringParameters = ['showAllRuns', 'group', 'testType'];
    142 
    143 var CROSS_DB_INVALIDATING_PARAMETERS = {
    144     'testType': 'group'
    145 };
    146 
    147 history.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