blob: fc9771d47c11bba3ef0505148e89d6a1cceeca71 [file] [log] [blame]
<!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>