| <!DOCTYPE html> |
| <!-- |
| Copyright 2016 The Chromium Authors. All rights reserved. |
| Use of this source code is governed by a BSD-style license that can be |
| found in the LICENSE file. |
| --> |
| |
| <link rel="import" href="/components/core-icon-button/core-icon-button.html"> |
| <link rel="import" href="/components/paper-button/paper-button.html"> |
| <link rel="import" href="/components/paper-progress/paper-progress.html"> |
| |
| <link rel="import" href="/dashboard/elements/autocomplete-box.html"> |
| <link rel="import" href="/dashboard/static/simple_xhr.html"> |
| <link rel="import" href="/dashboard/static/testselection.html"> |
| |
| <polymer-element name="test-picker" attributes="hasChart xsrfToken testSuites"> |
| <template> |
| <style> |
| #container * { |
| margin-right: 3px; |
| } |
| |
| #dnd-btn { |
| padding: 1px 2px; |
| margin: 2px 4px 2px 2px; |
| font-size: 11px; |
| cursor:grabbing; |
| cursor:-moz-grabbing; |
| cursor:-webkit-grabbing; |
| } |
| |
| paper-button { |
| height: 35px; |
| } |
| |
| paper-button:not([disabled]) { |
| color: #4285f4; |
| } |
| |
| paper-button[raised]:not([disabled]) { |
| background: #4285f4; |
| color: #fff; |
| } |
| |
| core-icon { |
| height: 25px; |
| } |
| |
| #suite-description { |
| padding: 10px; |
| } |
| |
| #loading { |
| display: block; |
| position: relative; |
| width: 152px; |
| height: 35px; |
| } |
| |
| paper-progress { |
| display: block; |
| position: absolute; |
| bottom: -1px; |
| height: 1px; |
| width: 100%; |
| } |
| </style> |
| |
| <div id="container" horizontal layout center wrap> |
| <template repeat="{{box, index in selectionModels}}"> |
| <autocomplete-box datalist={{box.datalist}} |
| placeholder="{{box.placeholder}}" |
| disabled={{box.disabled}} |
| multi={{box.multi}} |
| on-dropdownselect="{{onDropdownSelect}}" |
| id="selection-{{index}}"></autocomplete-box> |
| </template> |
| |
| <template if="{{loading}}"> |
| <div id="loading"> |
| <paper-progress indeterminate></paper-progress> |
| </div> |
| </template> |
| |
| <core-icon-button id="dnd-btn" |
| icon="open-with" |
| disabled?="{{!enableAddSeries || !hasChart}}" |
| draggable="{{enableAddSeries && hasChart}}" |
| on-dragstart="{{onSeriesButtonDragStart}}"> |
| Drag me |
| </core-icon-button> |
| |
| <paper-button raised |
| id="add-graph-btn" |
| on-click="{{onAddButtonClicked}}" |
| disabled?="{{!enableAddSeries}}">Add</paper-button> |
| </div> |
| |
| <div id="suite-description"></div> |
| </template> |
| |
| <script> |
| 'use strict'; |
| |
| Polymer('test-picker', { |
| |
| TESTSUITE_LABEL: 'Test suite', |
| BOT_LABEL: 'Bot', |
| SUBTEST_LABEL: 'Subtest', |
| DEPRECATED_TAG: 'no new data', |
| UNMONITORED_TAG: 'unmonitored', |
| |
| ready: function() { |
| this.pageStateLoading = true; |
| this.hasChart = false; |
| this.enableAddSeries = false; |
| this.selectedSuite = null; |
| this.suiteDescription = null; |
| this.selectionModels = [ |
| { |
| placeholder: this.TESTSUITE_LABEL, |
| datalist: this.getSuiteItems() |
| }, |
| { |
| placeholder: this.BOT_LABEL, |
| disabled: true, |
| multi: true |
| } |
| ]; |
| }, |
| |
| testSuitesChanged: function() { |
| this.selectionModels[0].datalist = this.getSuiteItems(); |
| this.getSelectionMenu(0).setDataList(this.selectionModels[0].datalist); |
| }, |
| |
| /** |
| * Gets a list of menu items for test suites. |
| */ |
| getSuiteItems: function() { |
| var suiteMenuItems = []; |
| var self = this; |
| var isUnmonitored = function(suite) { |
| // A suite is considered unmonitored if there are no monitored |
| // subtests. |
| var suiteInfo = self.testSuites[suite]; |
| return !suiteInfo['mon'] || suiteInfo['mon'].length == 0; |
| }; |
| |
| for (var suite in this.testSuites) { |
| var suiteItem = { |
| name: suite |
| }; |
| |
| var tags = []; |
| if (this.testSuites[suite]['dep']) { |
| suiteItem.deprecated = true; |
| tags.push(this.DEPRECATED_TAG); |
| } |
| if (isUnmonitored(suite)) { |
| suiteItem.unmonitored = true; |
| tags.push(this.UNMONITORED_TAG); |
| } |
| suiteItem.tag = tags.join(' '); |
| |
| suiteMenuItems.push(suiteItem); |
| } |
| suiteMenuItems.sort(this.compareTestSuiteItem); |
| return suiteMenuItems; |
| }, |
| |
| /** |
| * Gets a list of bot menu items base on the selected test suite. |
| */ |
| getBotItems: function() { |
| var suite = this.getSelectionMenu(0).value; |
| if (!this.testSuites[suite]) { |
| return []; |
| } |
| var dict = this.testSuites[suite]['mas']; |
| var botMenuItems = []; |
| var masters = Object.keys(dict).sort(); |
| for (var i = 0; i < masters.length; i++) { |
| var groupItem = { |
| name: masters[i], |
| suite: suite, |
| head: true |
| }; |
| botMenuItems.push(groupItem); |
| var bots = dict[masters[i]]; |
| var botNames = Object.keys(dict[masters[i]]).sort(); |
| for (var j = 0; j < botNames.length; j++) { |
| var name = botNames[j]; |
| var botItem = { |
| name: name, |
| group: masters[i], |
| tag: (bots[name] ? this.DEPRECATED_TAG : '') |
| }; |
| botMenuItems.push(botItem); |
| } |
| } |
| return botMenuItems; |
| }, |
| |
| /** |
| * Handles dropdown menu select; updates the subsequent menu accordingly. |
| */ |
| onDropdownSelect: function(event) { |
| var model = event.target.templateInstance.model; |
| var boxIndex = model.index; |
| if (boxIndex == 0) { |
| this.updateTestSuiteDescription(); |
| this.updateBotMenu(); |
| } else if (boxIndex == 1) { |
| this.sendSubtestRequest(); |
| } else { |
| // Update all the next dropdown menus for subtests. |
| this.updateSubtestMenus(boxIndex + 1); |
| } |
| }, |
| |
| updateTestSuiteDescription: function() { |
| // Display the test suite description if there is one. |
| var descriptionElement = this.$['suite-description']; |
| var suite = this.getSelectionMenu(0).value; |
| if (!suite || !this.testSuites[suite]) { |
| descriptionElement.innerHTML = ''; |
| return; |
| } |
| |
| var description = this.testSuites[suite]['des']; |
| if (description) { |
| var descriptionHTML = '<b>' + suite + '</b>: '; |
| descriptionHTML += this.convertMarkdownLinks(description); |
| descriptionElement.innerHTML = descriptionHTML; |
| } else { |
| descriptionElement.innerHTML = ''; |
| } |
| }, |
| |
| /** |
| * Updates bot dropdown menu with bot items. |
| */ |
| updateBotMenu: function() { |
| var menu = this.getSelectionMenu(1); |
| var botItems = this.getBotItems(); |
| menu.setDataList(botItems); |
| menu.disabled = false; |
| this.subtestDict = null; |
| // If there's a selection, send a subtest request. |
| if (menu.value && menu.value.length > 0) { |
| this.sendSubtestRequest(); |
| } else { |
| // Clear all subtest menus. |
| this.selectionModels.splice(2); |
| } |
| this.updateAddButtonState(); |
| }, |
| |
| /** |
| * Sends a request for subtestDict base on selected test suite and bots. |
| */ |
| sendSubtestRequest: function() { |
| if (this.subtestXhr) { |
| this.subtestXhr.abort(); |
| this.subtestXhr = null; |
| } |
| var bots = this.getCheckedBots(); |
| // If no bots are selected, just leave the current subtests. |
| if (bots.length == 0) { |
| return; |
| } |
| var suite = this.getCheckedSuite(); |
| if (!suite) { |
| return; |
| } |
| |
| this.loading = true; |
| |
| var params = { |
| type: 'sub_tests', |
| suite: suite, |
| bots: bots.join(','), |
| xsrf_token: this.xsrfToken |
| }; |
| this.subtestXhr = simple_xhr.send( |
| '/list_tests', |
| params, |
| function(response) { |
| this.loading = false; |
| this.subtestDict = response; |
| // Start at first subtest menu. |
| this.updateSubtestMenus(2); |
| }.bind(this), |
| function(error) { |
| if (error) { |
| console.log('Error from sendSubtestRequest.'); |
| } |
| this.loading = false; |
| }.bind(this) |
| ); |
| }, |
| |
| /** |
| * Updates all subtest menus starting at 'startIndex'. |
| */ |
| updateSubtestMenus: function(startIndex) { |
| var subtestDict = this.getSubtestAtIndex(startIndex); |
| |
| // Update existing subtest menu. |
| for (var i = startIndex; i < this.selectionModels.length; i++) { |
| // Remove the rest of the menu if no subtestDict. |
| if (!subtestDict || Object.keys(subtestDict).length == 0) { |
| this.selectionModels.splice(i); |
| this.updateAddButtonState(); |
| return; |
| } |
| var subtestItems = this.getSubtestItems(subtestDict); |
| var menu = this.getSelectionMenu(i); |
| menu.setDataList(subtestItems); |
| |
| // If there are selected item, update next menu. |
| if (menu.value && menu.value.length > 0) { |
| subtestDict = subtestDict[menu.value]['sub_tests']; |
| } else { |
| subtestDict = null; |
| } |
| } |
| |
| // Check if we still need to add a subtest menu. |
| if (subtestDict && Object.keys(subtestDict).length > 0) { |
| var subtestItems = this.getSubtestItems(subtestDict); |
| this.selectionModels.push({ |
| placeholder: this.SUBTEST_LABEL, |
| datalist: subtestItems |
| }); |
| } |
| |
| this.updateAddButtonState(); |
| }, |
| |
| updateAddButtonState: function() { |
| var selection = this.getCurrentSelection(); |
| this.enableAddSeries = selection != null && selection.isValid(); |
| }, |
| |
| getSubtestAtIndex: function(index) { |
| var subtestDict = this.subtestDict; |
| for (var i = 2; i < index; i++) { |
| var test = this.getSelectionMenu(i).value; |
| if (test in subtestDict) { |
| subtestDict = subtestDict[test]['sub_tests']; |
| } else { |
| return null; |
| } |
| } |
| return subtestDict; |
| }, |
| |
| getSubtestItems: function(subtestDict) { |
| var subtestItems = []; |
| var subtestNames = Object.keys(subtestDict).sort(); |
| for (var i = 0; i < subtestNames.length; i++) { |
| var name = subtestNames[i]; |
| subtestItems.push({ |
| name: name, |
| tag: (subtestDict[name]['deprecated'] ? this.DEPRECATED_TAG : '') |
| }); |
| } |
| return subtestItems; |
| }, |
| |
| getCheckedBots: function() { |
| var bots = []; |
| var botMenu = this.getSelectionMenu(1); |
| var botItems = botMenu.getSelectedItems(); |
| for (var i = 0; i < botItems.length; i++) { |
| bots.push(botItems[i]['group'] + '/' + botItems[i]['name']); |
| } |
| return bots; |
| }, |
| |
| getCheckedSuite: function() { |
| var suiteMenu = this.getSelectionMenu(0); |
| return suiteMenu.value; |
| }, |
| |
| getSelectionMenu: function(index) { |
| return this.$.container.querySelector('#selection-' + index); |
| }, |
| |
| /** |
| * Handler for drag start event for series drag button. |
| */ |
| onSeriesButtonDragStart: function(event, detail, sender) { |
| var selection = this.getCurrentSelection(); |
| var testPathAndSelected = selection.getTestPathAndSelectedSeries(); |
| event.dataTransfer.setData('type', 'seriesdnd'); |
| event.dataTransfer.setData( |
| 'data', JSON.stringify(testPathAndSelected)); |
| event.dataTransfer.effectAllowed = 'copy'; |
| }, |
| |
| /** |
| * Fires add event on 'Add' button clicked. |
| */ |
| onAddButtonClicked: function(event, detail, sender) { |
| this.fire('add'); |
| }, |
| |
| /** |
| * Gets the current selection from the menus. Returns null unless there |
| * are valid test selection. |
| */ |
| getCurrentSelection: function() { |
| // Up to subtest menu. |
| for (var i = 0; i < 3; i++) { |
| var menu = this.getSelectionMenu(i); |
| if (!menu || menu.getSelectedItems().length == 0) { |
| return null; |
| } |
| } |
| |
| var suite = this.getSelectionMenu(0).getSelectedItems()[0]; |
| var selection = new testselection.TestSelection(this.testSuites); |
| var testPathAndSelected = this.addTestPathFromSubtestDict( |
| this.subtestDict, suite.name, 2); |
| var bots = this.getCheckedBots(); |
| for (var i = 0; i < bots.length; i++) { |
| for (var j = 0; j < testPathAndSelected.length; j++) { |
| var fullTestPath = bots[i] + '/' + testPathAndSelected[j][0]; |
| selection.addTestPath(fullTestPath, testPathAndSelected[j][1]); |
| } |
| } |
| |
| return selection; |
| }, |
| |
| /** |
| * This method recursively add test path from a subtestDict. Selected |
| * series are added at the last level of the menu. |
| * @return {Array} List of pair of test path to list of selected series |
| * name. |
| */ |
| addTestPathFromSubtestDict: function(subtestDict, testPath, level) { |
| if (!subtestDict) { |
| return []; |
| } |
| var testPathAndSelected = []; |
| var nextMenu = this.getSelectionMenu(level + 1); |
| // If this is the last menu with selection. |
| var isLastLevel = (level == this.selectionModels.length - 1 || |
| !nextMenu || |
| nextMenu.getSelectedItems().length == 0); |
| |
| var items = this.getSelectionMenu(level).getSelectedItems(); |
| for (var i = 0; i < items.length; i++) { |
| var name = items[i].name; |
| var nextTestPath = testPath + '/' + name; |
| var selectedSeries = []; |
| if (isLastLevel) { |
| if (subtestDict[name]['has_rows']) { |
| selectedSeries.push(name); |
| } |
| // Select important traces by default. |
| var nextSubtestDict = subtestDict[name]['sub_tests']; |
| for (var subName in nextSubtestDict) { |
| if (nextSubtestDict[subName]['has_rows'] && |
| testselection.isImportant(nextTestPath + '/' + subName)) { |
| selectedSeries.push(subName); |
| } |
| } |
| testPathAndSelected.push([nextTestPath, selectedSeries]); |
| } else { |
| var results = this.addTestPathFromSubtestDict( |
| subtestDict[name]['sub_tests'], nextTestPath, level + 1); |
| testPathAndSelected.push.apply(testPathAndSelected, results); |
| } |
| } |
| return testPathAndSelected; |
| }, |
| |
| /** |
| * Sorts by suite items by deprecated, monitored and then by name. |
| * @return {number} negative if suiteA should be before suiteB, |
| * positive if suiteA should be after suiteB, zero if they're equal. |
| */ |
| compareTestSuiteItem: function(suiteA, suiteB) { |
| if (!suiteA.deprecated && suiteB.deprecated) { |
| return -1; |
| } |
| if (!suiteB.deprecated && suiteA.deprecated) { |
| return 1; |
| } |
| if (!suiteA.unmonitored && suiteB.unmonitored) { |
| return -1; |
| } |
| if (!suiteB.unmonitored && suiteA.unmonitored) { |
| return 1; |
| } |
| var nameALower = suiteA.name.toLowerCase(); |
| var nameBLower = suiteB.name.toLowerCase(); |
| if (nameALower > nameBLower) { |
| return 1; |
| } |
| if (nameALower < nameBLower) { |
| return -1; |
| } |
| return 0; |
| }, |
| |
| /** |
| * Converts a link in markdown format to a HTML link (anchor elements). |
| * @param {string} text A link in markdown format. |
| * @return {string} A hyperlink string. |
| */ |
| convertMarkdownLinks: function(text) { |
| return text.replace(/\[(.+?)\]\((.+?)\)/g, '<a href="$2">$1</a>'); |
| } |
| }); |
| </script> |
| </polymer-element> |