blob: b2930300e4607708b5fdd1d3cd80a460b10132e2 [file] [log] [blame]
<!--
The <search-page-sk> custom element declaration.
Shows the results of a search request.
It sends the query string as a JSON request to the
search endpoint ('/json/search') and renders the result.
It assumes to the be part of a client site routed system
of views and therefore offers the 'pageSelected' and 'pageUnselected'
functions. These need to be called whenever the page goes
in and out of view.
Attributes:
None
Methods:
pageSelected(ctx) - Called by the router when the view becomes visible.
ctx is the context provided in the route dispatch of page.js.
pageDeselected - Called by the router when the view is no longer visible.
Events:
None
Mailboxes:
None
-->
<link rel="import" href="bower_components/polymer/polymer.html">
<link rel="import" href="bower_components/iron-flex-layout/iron-flex-layout-classes.html">
<link rel="import" href="bower_components/iron-icons/iron-icons.html">
<link rel="import" href="bower_components/paper-dialog/paper-dialog.html">
<link rel="import" href="bower_components/paper-button/paper-button.html">
<link rel="import" href="bower_components/paper-menu-button/paper-menu-button.html">
<link rel="import" href="bower_components/paper-toggle-button/paper-toggle-button.html">
<link rel="import" href="activity-sk.html">
<link rel=import href="detail-list-sk.html">
<dom-module id="search-page-sk">
<template>
<style include="iron-flex iron-flex-alignment"></style>
<style include="shared-styles">
digest-details-sk {
display: block;
box-shadow: 3px 3px 6px 1px rgba(133,133,133,1);
margin-bottom: 1em;
padding: 0.5em;
}
digest-details-sk[data-focus] {
box-shadow: 3px 3px 6px 5px #FF7F00;
}
#missing {
padding-top: 2em;
}
.searchPageWrapper {
padding: 5px;
}
paper-button {
min-width: 2em;
margin: 5px;
background: #eee;
}
.actionMenu {
max-width: 10em;
}
.actionButton {
color: green;
}
.searchResponse {
padding: 1em 1em 0 1em;
margin: 1em 1em 1em 0;
border: 1px solid #555555;
}
.searchStats {
padding-bottom: 1em;
}
#searchControls {
flex-grow: 1;
}
</style>
<div class="searchPageWrapper layout vertical">
<activity-sk id="activityBar" busy="{{_hideAll}}"></activity-sk>
<div class="layout horizontal">
<search-controls-sk id="searchControls" beta></search-controls-sk>
<paper-menu-button close-on-activate no-animations class="actionMenu">
<paper-button class="dropdown-trigger actionButton" raised>Actions</paper-button>
<paper-menu class="dropdown-content">
<paper-item id="actionTestView">Switch To Test View</paper-item>
<paper-item id="actionBulkTriage">Bulk Triage</paper-item>
<paper-item id="actionExport">Export</paper-item>
<paper-item id="actionLegacySearch">Legacy Search</paper-item>
<paper-item id="actionHelpDialog">Help</paper-item>
</paper-menu>
</paper-menu-button>
</div>
<div class="searchResponse" hidden$="[[_noResult(_allData)]]">
<div class="searchStats">
Displaying: [[_allData.digests.length]] of [[_allData.size]] images starting at [[_allData.offset]]<br>
UniqueTests Visible: [[_uniqueTests.length]] <br>
UniqueTests Total: [[_uniqueTestsAll.length]]<br>
</div>
<changelist-controls-sk id="changeListControls"
ps_order=[[_state.patchsets]] include_master=[[_state.master]]></changelist-controls-sk>
</div>
<div hidden$="[[_hideAll]]">
<div id="missing" hidden$="[[_nonEmptyResult(data)]]">
No digests match your query.
</div>
<div hidden$="[[_emptyResult(data)]]">
<detail-list-sk id="detailList">
<template is="dom-repeat" items="[[data.digests]]">
<digest-details-sk
id$="[[_entryId(item)]]"
mode="list"
details="[[item]]"
commits="[[data.commits]]"
metric="[[_state.metric]]"
issue="[[_changeListID]]">
</digest-details-sk>
</template>
</detail-list-sk>
</div>
</div>
</div>
<dialog id="bulkDialog">
<bulk-triage-sk id="bulkTriage"></bulk-triage-sk>
</dialog>
</template>
<script>
Polymer({
is: "search-page-sk",
behaviors: [gold.PageStateBehavior],
properties: {
data: {
type: Object,
value: function() { return {}; }
},
_changeListID: {
type: String,
value: "",
},
},
ready: function() {
this.listen(this.$.searchControls, 'search-controls-sk-change', '_handleStateChange');
this.listen(this.$.searchControls, 'query-dialog-open', '_disableDigestListKeybindings');
this.listen(this.$.searchControls, 'query-dialog-close', '_enableDigestListKeybindings');
this.$.changeListControls.addEventListener('cl-control-change', (e) => {
this._state.master = e.detail.include_master;
this._state.patchsets = e.detail.ps_order;
this._redirectToState(this._state);
});
this.listen(this.$.actionTestView, 'tap', '_handleSwitchToByTest');
this.listen(this.$.actionBulkTriage, 'tap', '_showBulkTriageDialog');
this.listen(this.$.bulkTriage, 'bulk_triage_cancelled', '_hideBulkTriageDialog');
this.listen(this.$.bulkTriage, 'bulk_triage_invoked', '_startBulkTriage');
this.listen(this.$.bulkTriage, 'bulk_triage_finished', '_endBulkTriage');
this.listen(this.$.actionExport, 'tap', '_handleExport');
this.listen(this.$.actionLegacySearch, 'tap', '_handleLegacySearchButton');
this.listen(this.$.actionHelpDialog, 'tap', '_openHelpDialog');
this._setDefaultState(gold.defaultSearchState, false);
},
pageSelected: function(ctx) {
this.set('data', {});
this.set('_allData', {});
this._enableDigestListKeybindings();
// Initialize the state and set values of the controls.
this._initState(ctx, this._getDefaultStateWithCorpus());
// Extract corpus from query and remove from it.
const queryParamSet = sk.query.toParamSet(this._state.query);
const corpus = queryParamSet.source_type[0];
delete queryParamSet["source_type"];
// Set search-control-sk's search criteria.
const searchCriteria = {
corpus: corpus,
leftHandTraceFilter: queryParamSet,
rightHandTraceFilter: sk.query.toParamSet(this._state.rquery),
includePositiveDigests: this._state.pos,
includeNegativeDigests: this._state.neg,
includeUntriagedDigests: this._state.unt,
includeDigestsNotAtHead: !this._state.head,
includeIgnoredDigests: this._state.include,
minRGBADelta: this._state.frgbamin,
maxRGBADelta: this._state.frgbamax,
mustHaveReferenceImage: this._state.fref,
sortOrder: this._state.sort === 'asc' ? 'ascending' : 'descending'
};
this.$.searchControls.searchCriteria = searchCriteria;
this.$.changeListControls.ps_order = +this._state.patchsets;
this.$.changeListControls.include_master = this._state.master;
// load the commits, parameters and data.
this._loadCommits();
this._loadParamset();
this._load();
},
pageDeselected: function() {
this._disableDigestListKeybindings();
},
_disableDigestListKeybindings: function() {
this.$.detailList.endUse();
},
_enableDigestListKeybindings: function() {
this.$.detailList.startUse();
},
_handleStateChange: function(ev) {
const searchCriteria = ev.detail;
// Make a copy of the current state.
const newState = JSON.parse(JSON.stringify(this._state));
// Set query fields.
const queryParamSet = searchCriteria.leftHandTraceFilter;
queryParamSet.source_type = [searchCriteria.corpus];
newState.query = sk.query.fromParamSet(queryParamSet);
newState.rquery = sk.query.fromParamSet(searchCriteria.rightHandTraceFilter);
// Set remaining fields.
newState.pos = searchCriteria.includePositiveDigests;
newState.neg = searchCriteria.includeNegativeDigests;
newState.unt = searchCriteria.includeUntriagedDigests;
newState.head = !searchCriteria.includeDigestsNotAtHead;
newState.include = searchCriteria.includeIgnoredDigests;
newState.frgbamin = searchCriteria.minRGBADelta;
newState.frgbamax = searchCriteria.maxRGBADelta;
newState.fref = searchCriteria.mustHaveReferenceImage;
newState.sort = searchCriteria.sortOrder === 'ascending' ? 'asc' : 'desc';
// Reload the page with the new state in the query parameters.
this._redirectToState(newState);
},
_handleStateUpdate: function(ev) {
// Update the state and the URL and then distribute the new state to
// the control elements.
this._replaceState(ev.detail);
this.$.searchControls.setState(this._state);
},
_handleSwitchToByTest: function() {
this._redirectToState({}, "/list");
},
_openHelpDialog: function(ev) {
this.$.detailList.openHelpDialog();
},
_handleExport: function(ev) {
var url = this._getRedirectURL({}, "/json/export");
window.open(url, "_blank");
},
_handleLegacySearchButton: function(ev) {
this._redirectToState({}, "/legacysearch");
},
// _statusClosest returns label based on whether the digest is closest to
// a positive or negative image.
_statusClosest:function(digest) {
if (digest.closestRef === gold.REF_NEG) {
return gold.NEGATIVE;
}
return gold.POSITIVE;
},
// _getBulkTriageCount returns the number of digests that will be triaged
// in a bulk triage operation.
_getBulkTriageCount: function(digests, bulkStatus) {
if (bulkStatus !== '=') {
return digests.length;
}
// If we compare to the closest we only show the number of digests
// that actually have a reference image.
var count = 0;
for(var i=0; i < digests.length; i++) {
if (digests[i].closestRef !== '') {
count++;
}
}
return count;
},
_showBulkTriageDialog: function() {
this.$.bulkDialog.showModal();
},
_hideBulkTriageDialog: function() {
this.$.bulkDialog.close();
},
_startBulkTriage: function() {
this.$.activityBar.startSpinner("Triaging ...");
},
_endBulkTriage: function() {
this.$.bulkDialog.close();
this._load();
},
_load: function() {
this.$.activityBar.startSpinner("Loading ...");
var q = window.location.search;
// Maybe if we detect that issue is set, do the query to something like
// /json/changelist/gerrit/1786867 for the changelist data.
sk.get("/json/search" + q).then(JSON.parse).then(function (json) {
// Split out the digests that are being displayed and keep a copy
// of all digests that matched the query.
const displayData = sk.object.shallowCopy(json);
this.set('_allData', json);
this.set('_uniqueTestsAll', this._getUniqueTests(json.digests));
this.set('_uniqueTests', this._getUniqueTests(displayData.digests));
this.set('data', displayData);
const allBulkTriageData = displayData.bulk_triage_data;
const pageBulkTriageData = getPageBulkTriageData(displayData.digests);
this.$.bulkTriage.setDigests(pageBulkTriageData, allBulkTriageData);
this.$.activityBar.stopSpinner();
}.bind(this)).catch(function(e) {
this.$.activityBar.stopSpinner();
sk.errorMessage(e);
}.bind(this));
if (this._state.issue) {
sk.get(`/json/changelist/${this._state.crs}/${this._state.issue}`).then(JSON.parse).then((json) => {
this.$.changeListControls.setSummary(json);
this.set('_changeListID', json.cl.id);
});
this.$.bulkTriage.changeListID = this._state.issue;
}
},
_getUniqueTests: function(digests) {
var unique = {};
for(var i=0; i < digests.length; i++) {
unique[digests[i].test] = true;
}
return Object.keys(unique);
},
_loadCommits: function() {
sk.get("/json/commits").then(JSON.parse).then(function (json) {
this._commits = json;
}.bind(this)).catch(sk.errorMessage);
},
_loadParamset: function() {
let url = '/json/paramset';
if (this._state.issue) {
url += '?changelist_id='+this._state.issue;
}
sk.get(url).then(JSON.parse).then(function (json) {
this._paramSet = json;
// Extract corpora from response.
this.$.searchControls.corpora = json.source_type;
// Delete source_type and set the paramSet.
const paramSetCopy = JSON.parse(JSON.stringify(json));
const corpora = paramSetCopy.source_type;
delete paramSetCopy["source_type"];
this.$.searchControls.paramSet = paramSetCopy;
}.bind(this)).catch(sk.errorMessage);
},
_entryId: function(item) {
// Make sure it's a valid selector.
return (item.test + '_' + item.digest).replace(/[^_A-Za-z0-9]/g, '_');
},
// Returns true if there was a query and result is not empty.
_nonEmptyResult: function(data) {
return !data.digests || data.digests.length > 0;
},
// Returns true if there was a query and the result is empty.
_emptyResult: function(data) {
return !data.digests || data.digests.length === 0;
},
_noResult: function(data) {
return !data.digests;
},
_noMore: function(allData) {
return (allData.digests && allData.digests.length <= allData.size);
}
});
// The return value here is a map of test name to digest to the label string of
// the closest reference image. If there is no closest reference image, the label string
// will be empty string (it still needs to be included for a bulk triage positive/negative).
function getPageBulkTriageData(searchResults) {
const testDigestClosest = {};
for (const result of searchResults) {
let byTest = testDigestClosest[result.test];
if (!byTest) {
byTest = {};
testDigestClosest[result.test] = byTest;
}
let valueToSet = '';
if (result.closestRef === 'pos') {
valueToSet = 'positive';
} else if (result.closestRef === 'neg') {
valueToSet = 'negative';
}
byTest[result.digest] = valueToSet;
}
return testDigestClosest;
}
</script>
</dom-module>