blob: 86566fd6e680dea3bc2c715d0f99018167f3a365 [file] [log] [blame]
<!-- The <digest-details2-sk> custom element declaration.
Displays the details about a digest.
Attributes:
mode: determines behavior of this element. Supported values:
* list: Element is part of a list view. It only considers the contents
of the 'details' field and expects closes digests to be part of
the 'diff' field within 'details'.
* detail: Element is part of a detail view about an individual digest.
Only the contents of 'details' is considered, but different
UI elements are exposed.
* diff: Element is used to show diff of two digests.
'details' attribute contains information about the left digest.
'right' attribute contains information around right digest
and 'diff' contains diff information. Any 'diff' field within
'details' is ignored.
details - Object, a deserialized instance of search.SRDigest.
right - Object, a deserialized instance of search.SRDigest.
diff - Object, a deserialized instance of search.SRDiffDigest.
commits - Array, a list of commits that 'details' refers to.
embedded - Boolean, a flag that indicates that this is embedded as an
auxiliary view and some field like title should be omitted.
issue - Number, the id of the code review issue that generated this digest
via a tryjob run.
Events:
triage - A triage event is generated when the triage button is pressed. The e.detail
of the event looks like:
{
digest: ["ba636123..."],
status: "positive",
test: "blurs"
}
zoom-clicked - This event is triggered when the user clicks on the 'zoom'
button. The 'detail' in the event is the object expected by the
multi-zoom-sk element.
clear - Clears the current digest shown.
Methods:
triggerTriage(status) - Set the triage status of the digest, triggering the triage event.
getZoomDetail: Return the information used by the multi-zoom-sk element to show a zoomed
view of the images related to this digest.
statusClosest: Returns the status of the closest digest as a string or null if there is
no closest digest.
-->
<link rel="import" href="bower_components/iron-flex-layout/iron-flex-layout-classes.html">
<link rel="import" href="bower_components/paper-button/paper-button.html">
<link rel="import" href="bower_components/paper-checkbox/paper-checkbox.html">
<link rel="import" href="../common/imp/paramset.html">
<link rel="import" href="../common/imp/triage-sk.html">
<link rel="import" href="blame-sk.html">
<link rel="import" href="dots-sk.html">
<link rel="import" href="dot-legend-sk.html">
<link rel="import" href="purge-sk.html">
<link rel="import" href="shared-styles.html">
<dom-module id="digest-details2-sk">
<template>
<style include="iron-flex iron-flex-alignment shared-styles">
circle.status0 {
fill: #000000;
stroke: #000000;
}
dot-legend-sk {
margin-left: 5em;
margin-bottom: 2em;
}
dots-sk {
display: block;
}
.more {
margin-left: 3em;
}
.preview img {
display: block;
max-width: 100%;
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAAAAAA6mKC9AAAAAXNSR0IArs4c6QAAAAJiS0dEAP+Hj8y/AAAACXBIWXMAAA7DAAAOwwHHb6hkAAAAB3RJTUUH3gUBEi4DGRAQYgAAAB1JREFUGNNjfMoAAVJQmokBDdBHgPE/lPFsYN0BABdaAwN6tehMAAAAAElFTkSuQmCC');
}
.hidden * {
display: none;
}
.triageInfo,
.triageInfo > * {
padding: 0.5em;
}
.untriagedImage svg {
margin: auto;
}
.digestDetailImages {
margin-right: 1.5em;
}
.warning {
font-weight: bold;
padding: 1em;
color: #E7298A;
padding-top: 18px;
display: block;
width: 10em;
}
#paramsets {
max-width: 40em;
padding-left: 1em;
padding-right: 1em;
}
.noCompare {
padding-right: 1em;
}
.dotInfo {
display: block;
border: 1px solid #eeeeee;
margin-top: 1em;
margin-bottom: 1em;
margin-left: 1em;
padding: 1em;
}
.leftCol {
margin-right: 3em;
}
.testHeader {
font-size: 16px;
font-weight: bold;
margin-left: 0.5em;
margin-bottom: 0.5em;
white-space: nowrap;
}
#toggleRefButton,
#zoomButton {
height: 45px;
margin: 5px;
}
.metricHead {
padding: 0 1em;
font-weight: bold;
}
.rotateTitle {
height: 28px;
}
.smallbar {
}
</style>
<div class="vertical layout wrap" hidden$="[[_noData(details)]]">
<!-- Triage Controls -->
<div class="horizontal layout triageInfo">
<triage-sk value="{{details.status}}"
id="triageControls"
data-digest$="[[details.digest]]"
data-test$="[[details.test]]">
</triage-sk>
<div class="smallbar horizontal center layout">
<div class="metricHead" hidden$="[[embedded]]">[[details.test]]</div>
<div class="horizontal layout" hidden$="[[!_hasRight(_right)]]">
<div>
<a href$="[[_diffPageUrl(details, _right)]]">Diff Details</a>
</div>
<div><span class="metricHead">Diff Metric:</span> <span>[[_fixedMetric(_diff, metric)]]</span></div>
<div><span class="metricHead">Diff %:</span> <span>[[_fixedPercent(_diff)]]</span></div>
<div><span class="metricHead">Pixels:</span> [[_diff.numDiffPixels]]</div>
<div><span class="metricHead">Max RGBA:</span> [<span>[[_diff.maxRGBADiffs]]</span>]</div>
</div>
<!-- Links to Grid and Cluster. -->
<div hidden$="_hideLinks(details, embedded)"><a href$="[[_cmpUrl(details.test)]]"><iron-icon icon="apps"></iron-icon></a></div>
<div hidden$="_hideLinks(details, embedded)"><a href$="[[_clusterUrl(details.test)]]"><iron-icon icon="radio-button-unchecked"></iron-icon></a></div>
</div>
<paper-button id="zoomButton" raised>Zoom</paper-button>
<paper-button id="toggleRefButton" raised hidden$="[[_hideRefToggle(_refKeys)]]">Toggle Closest</paper-button>
<div class="warning" hidden$="[[!_negIsClosest]]">
Closest Image Is Negative!
</div>
</div>
<div>
<paper-checkbox title="Image" id=leftCheckbox checked></paper-checkbox>
<paper-checkbox title="Closest" id=rightCheckbox checked></paper-checkbox>
<paper-checkbox title="Diff" id=diffCheckbox></paper-checkbox>
<!-- Image title -->
<!-- left -->
<div hidden$="[[_noteq(_rotateImageIndex, 0)]]" class=rotateTitle>
<span hidden$="[[_hideDotsBlame(details)]]">
<svg width="10" height="10" viewBox="-1 -1 2 2">
<circle cx="0" cy="0" r="0.3" class="status0"></circle>
</svg>
</span>
<a href$="[[_detailHref(details.digest)]]" target="_blank">
[[_leftParamTitle]]
</a>
</div>
<!-- right -->
<div hidden$="[[_noteq(_rotateImageIndex, 1)]]" class=rotateTitle>
<a href$="[[_detailHref(_right.digest)]]" target="_blank">
[[_rightParamTitle]]
</a>
<span class="flex self-start testHeader" hidden$="[[!_hasRight(_right)]]">[[_right.digest]]</span>
</div>
<!-- diff -->
<div hidden$="[[_noteq(_rotateImageIndex, 2)]]" class=rotateTitle>
Diff
</div>
</div>
<!-- Images -->
<div>
<!-- left -->
<div class="preview" hidden$="[[_noteq(_rotateImageIndex, 0)]]">
<img src$="[[_digestHref(details.digest)]]">
</div>
<!-- right -->
<div class="preview" hidden$="[[_noteq(_rotateImageIndex, 1)]]">
<img src$="[[_digestHref(_right.digest)]]">
</div>
<!-- diff -->
<div class="preview" hidden$="[[_noteq(_rotateImageIndex, 2)]]">
<img src$="[[_diffImgHref]]">
</div>
</div>
<div class="horizontal layout center" hidden$="[[_hideNegPosFound(_right)]]">
<div class="noCompare">
<strong>No Positive or Negative Digests Found.</strong>
</div>
</div>
<!-- ParamSet -->
<div class="vertical layout" id="paramset">
<paramset-sk id="paramsets"></paramset-sk>
<template is="dom-if" if="[[_eq(mode, 'detail')]]">
<purge-sk digest="[[details.digest]]"></purge-sk>
</template>
</div>
<!-- dots, dot-legend and blame -->
<div class="vertical layout dotInfo" hidden$="[[_hideDotsBlame(details)]]">
<dots-sk id="dots"></dots-sk>
<dot-legend-sk id="dotlegend"
test="{{details.test}}"
digests={{details.traces.digests}}>
</dot-legend-sk>
<blame-sk id="blame"
commits="{{commits}}"
value="{{details.diff.blame}}">
</blame-sk>
</div>
</template>
<script>
Polymer({
is: 'digest-details2-sk',
properties: {
mode: {
type: String,
value: "list",
},
details: {
type: Object,
value: function() { return {}; },
},
right: {
type: Object,
value: null
},
diff: {
type: Object,
value: null
},
commits: {
type: Array,
value: function() { return []; },
},
embedded: {
type: Boolean,
value: false
},
metric: {
type: String,
},
issue: {
type: Number,
value: 0,
},
_negIsClosest: {
type: Boolean,
value: false
},
_right: {
type: Object,
value: null
},
_diff: {
type: Object,
value: null
},
_refKeys: {
type: Array,
value: function() { return []; }
},
_rotateImageIndex: {
type: Number,
value: 0,
}
},
observers: [
'_changedInput(details, commits, right, diff)'
],
ready: function () {
this.listen(this.$.zoomButton, 'click', '_zoomHandler');
this.listen(this.$.toggleRefButton, 'tap', '_toggleRefHandler');
this.listen(this.$.dots, 'hover', '_hoverHandler');
this.listen(this.$.dots, 'mouseleave', '_mouseLeaveHandler');
this.listen(this.$.triageControls, 'change', '_triageChangeHandler');
this.listen(this.$.blame, 'hover-blame', '_blameHoverHandler');
this.rotateIDs = ["leftCheckbox", "rightCheckbox", "diffCheckbox"];
setInterval(() => this._showNext(), 500);
},
_showNext: function() {
let index = this._rotateImageIndex;
for (var i = 0; i < this.rotateIDs.length; i++) {
index = (index + 1) % this.rotateIDs.length;
let id = this.rotateIDs[index];
if (this.querySelector('#' + id).checked) {
this._rotateImageIndex = index;
break;
}
}
},
triggerTriage: function (status) {
this.set('$.triageControls.value', status);
var detail = new gold.TriageQuery(this.$.triageControls.dataset.test,
this.$.triageControls.dataset.digest,
status, this.issue);
this.fire('triage', detail);
},
getZoomDetail: function() {
return {
leftImgUrl: this._digestHref(this.details.digest),
rightImgUrl: this._digestHref(sk.robust_get(this._right, ['digest'])),
middleImgUrl: this._diffImgHref,
llabel: this._leftParamTitle,
rlabel: this._rightParamTitle
};
},
clear: function() {
this.set('details', {});
this.set('right', null);
this.set('diff', null);
},
_triageChangeHandler: function(e) {
// Convert the change event from the triage button into a more detailed triage event.
var detail = new gold.TriageQuery(this.details.test,
this.details.digest,
this.details.status,
this.issue);
this.fire('triage', detail);
},
_zoomHandler: function() {
this.fire('zoom-clicked', this.getZoomDetail());
},
_toggleRefHandler: function() {
this._refIdx = (this._refIdx+1) % this._refKeys.length;
this.details.closestRef = this._refKeys[this._refIdx];
this._setRefDiff();
this._setProperties();
},
_hoverHandler: function(e) {
var id = e.detail;
var params = {};
var traces = this.details.traces.traces;
// Find the matching trace in details.traces.
for (var i=0, len = traces.length; i < len; i++) {
if (traces[i].label == id) {
params = traces[i].params;
break;
}
}
this.$.paramsets.setHighlight(params);
},
_blameHoverHandler: function(ev) {
this.$.dots.highlight(ev.detail.index, ev.detail.b);
},
_mouseLeaveHandler: function() {
this.$.paramsets.clearHighlight();
},
_changedInput: function(details, commits, right, diff) {
// Check if we have the most basic data.
if (!this.details || !this.details.digest) {
return;
}
// Set the data according to the mode.
switch(this.mode) {
case 'list':
this._setRightFromDetails(false);
break;
case 'detail':
this._setRightFromDetails(true);
break;
case 'diff':
this._right = this.right;
this._diff = this.right;
if (this._right) {
this._rightParamTitle = this._abbrev(this._right.digest);
}
break;
default:
console.log("Unknow mode set for digest-detail-sk element.");
return
}
this._setProperties();
},
_setRightFromDetails: function(detailsView) {
this._negIsClosest = false;
this._right = null;
this._diff = null;
this._rightParamTitle = '';
// Set the reference image returned by the server.
var hasRef = this._setRefDiff();
if (hasRef) {
// Get all the reference images that are availalble.
var refKeys = [];
if (this.details.refDiffs) {
for(var k in this.details.refDiffs) {
if (this.details.refDiffs.hasOwnProperty(k)) {
refKeys.push(k);
}
}
}
this._refIdx = refKeys.indexOf(this.details.closestRef);
this.set("_refKeys", refKeys);
}
// Set the blame, dots, dotlegend
this.$.dots.setCommits(this.commits);
this.$.dots.setValue(this.details.traces);
},
statusClosest: function() {
return (this._right) ? ((this._negIsClosest) ? gold.NEGATIVE : gold.POSITIVE) : null;
},
// _setRefDiff sets the reference image and returns true if there is one.
_setRefDiff: function() {
if (this.details.closestRef && this.details.closestRef !== '') {
this._negIsClosest = (this.details.closestRef === gold.REF_NEG);
this._right = this.details.refDiffs[this.details.closestRef];
this._diff = this._right;
this._rightParamTitle = 'Closest ' + this._statusStr(this._negIsClosest);
return true;
}
return false;
},
_digestFromDiff: function(test, refDiff, status) {
return {
test: test,
digest: refDiff.digest,
status: status,
paramset: diff.paramset
};
},
_setProperties: function() {
var paramSets = [];
var paramTitles = [];
this._leftParamTitle = this._abbrev(this.details.digest);
if (this.details.paramset) {
paramSets.push(this.details.paramset);
paramTitles.push(this._leftParamTitle);
}
if (this._right) {
this.set('_diffImgHref', gold.diffImgHref(this._right.digest, this.details.digest));
// TODO(stephana): Fix this on the backend to make sure we never get an empty
// set of parameters. Currently it can occur on occasion.
if (this._right.paramset) {
paramSets.push(this._right.paramset);
paramTitles.push(this._rightParamTitle);
}
}
// TODO(stephana): Remove the forced re-render of _diff by cleaning up how
// the properties are set outside this function.
if (this._diff) {
this.set('_diff', this._diff);
}
if (paramSets.length > 0) {
this.$.paramsets.setParamSets(paramSets, paramTitles);
}
},
_abbrev: function(str) {
if (str.length <= 12) {
return str;
}
return str.substr(0, 12) + '...';
},
_noData: function(details) {
return !(details && details.test && details.digest);
},
_concatOrdered: function(d1, d2) {
},
_hasRight: function(right) {
return (right && right.digest !== '');
},
_digestHref: gold.imgHref,
_fixedPercent: function (refDiff) {
return (refDiff) ? refDiff.pixelDiffPercent.toFixed(2) : '';
},
_fixedMetric: function (refDiff, metric) {
return (refDiff && metric) ? refDiff.diffs[metric].toFixed(2) : '';
},
_diffPageUrl: function (left, right) {
if (!left || !right) {
return "";
}
return '/diff' + gold.diffQuery(left.test, left.digest, right.digest);
},
_detailHref: function (digest) {
if (!digest) {
return '';
}
return '/detail' + gold.detailQuery(this.details.test, digest);
},
_hideNegPosFound: function(closest) {
return closest || (this.mode !== 'list');
},
_statusStr: function(_negIsClosest) {
return _negIsClosest ? 'Negative' : 'Positive';
},
_hideLinks: function(details, isEmbedded) {
return isEmbedded || !details.paramset || !detail.traces;
},
_cmpUrl: function (test) {
if (!test) {
return '';
}
// See gold.defaultSearchState why we need to embed an encoded query
// before encoding the search string.
return 'cmp?' + sk.query.fromObject({query: this._getRefQuery(test)});
},
_hideListFeatures: function(mode) {
return mode != 'list';
},
_hideDotsBlame: function(details) {
return (!details || !details.traces || !details.traces.traces);
},
_clusterUrl: function (test) {
if (!test) {
return '';
}
var q = {
query: this._getRefQuery(test),
head: true,
pos: true,
neg: true,
unt: true,
limit: 200
};
return 'cluster?' + sk.query.fromObject(q);
},
_getRefQuery: function(test) {
var qObj = {name: [test]};
var corpus = this.details && this.details.paramset && this.details.paramset['source_type'];
if (corpus) {
qObj['source_type'] = corpus;
}
return sk.query.fromParamSet(qObj);
},
_eq: function(val, expected) {
return val === expected;
},
_noteq: function(val, expected) {
return val !== expected;
},
_hideRefToggle: function(refKeys) {
return !(refKeys && refKeys.length && refKeys.length > 1);
}
});
</script>
</dom-module>