blob: 32c254f545d141e0c48c8de2610002598c7f24c5 [file] [log] [blame]
/**
* @module module/bulk-triage-sk
* @description <h2><code>bulk-triage-sk</code></h2>
*
* An element (meant for use in a dialog) which facilitates triaging multiple digests
* at once. It supports two modes - all the digests on this page of results or all
* digests that match the search results.
*
* @evt bulk_triage_cancelled - if the cancel button is clicked.
* @evt bulk_triage_invoked - Sent just before the triage RPC is hit.
* @evt bulk_triage_finished - Sent if the triage RPC returns success.
*/
import { define } from 'elements-sk/define';
import { html } from 'lit-html';
import { ElementSk } from '../../../infra-sk/modules/ElementSk';
import 'elements-sk/checkbox-sk';
import 'elements-sk/icon/cancel-icon-sk';
import 'elements-sk/icon/check-circle-icon-sk';
import 'elements-sk/icon/help-icon-sk';
import 'elements-sk/icon/view-agenda-icon-sk';
import 'elements-sk/styles/buttons';
import { sendBeginTask, sendEndTask, sendFetchError } from '../common';
import { Label, TriageRequest, TriageRequestData } from '../rpc_types';
/**
* The label to apply to the selected digests via the bulk triage dialog, or 'closest' to apply the
* label of the closest triaged reference digest.
*/
export type BulkTriageLabel = Label | 'closest';
export class BulkTriageSk extends ElementSk {
private static template = (el: BulkTriageSk) => html`
<h2>Bulk Triage</h2>
<p>Assign the status to all images on this page at once.</p>
${el.changeListID ? html`<p class=cl>This affects Changelist ${el.changeListID}.</p>` : ''}
<div class=status>
<button class="positive ${el.value === 'positive' ? 'selected' : ''}"
@click=${() => el._setDesiredLabel('positive')}>
<check-circle-icon-sk></check-circle-icon-sk>
</button>
<button class="negative ${el.value === 'negative' ? 'selected' : ''}"
@click=${() => el._setDesiredLabel('negative')}>
<cancel-icon-sk></cancel-icon-sk>
</button>
<button class="untriaged ${el.value === 'untriaged' ? 'selected' : ''}"
@click=${() => el._setDesiredLabel('untriaged')}>
<help-icon-sk></help-icon-sk>
</button>
<button class="closest ${el.value === 'closest' ? 'selected' : ''}"
@click=${() => el._setDesiredLabel('closest')}>
<view-agenda-icon-sk></view-agenda-icon-sk>
</button>
</div>
<div>
<checkbox-sk @change=${el._toggleAll} label="Triage all ${el._allDigestCount} digests"
title='Choose whether to triage just the digests on this page or all that match the query'
?checked=${el._triageAll} class=triage_all></checkbox-sk>
</div>
<div class=controls>
<button @click=${el._cancel} class=cancel>
Cancel (do nothing)
</button>
<button @click=${el._triage} class="action triage">
Triage ${el._triageAll ? el._allDigestCount : el._pageDigestCount} digests as ${el._value}
</button>
</div>
`;
private _changeListID = '';
private _crs = '';
private _value: BulkTriageLabel = 'closest';
private _triageAll = false;
private _pageDigests: TriageRequestData = {};
private _pageDigestCount = 0;
private _allDigests: TriageRequestData = {};
private _allDigestCount = 0;
constructor() {
super(BulkTriageSk.template);
}
connectedCallback() {
super.connectedCallback();
this._render();
}
/**
* The label to apply ("positive", "negative", "untriaged"), or "closest" to apply the label of
* of the closest triaged reference digest in each case.
*/
get value() {
return this._value;
}
set value(newValue) {
if (!['positive', 'negative', 'untriaged', 'closest'].includes(newValue)) {
throw new RangeError(`Invalid bulk-triage-sk value: "${newValue}".`);
}
this._value = newValue;
this._render();
}
/**
* The ID of the changelist to which these expectations should belong, or the empty string if
* none.
*/
get changeListID() {
return this._changeListID;
}
set changeListID(newValue) {
this._changeListID = newValue;
this._render();
}
/**
* The Code Review System (e.g. "gerrit") associated with the provided changelist ID, or the empty
* string if none.
*/
get crs() {
return this._crs;
}
set crs(c) {
this._crs = c;
this._render();
}
// Notes:
//
// Currently the /json/v1/search endpoint returns a SearchResponse struct where the
// bulk_triage_data field is populated with the empty string instead of a valid expectations.Label
// to indicate that a digest does not have a closest triaged reference digest:
// https://github.com/google/skia-buildbot/blob/89e3a329bda8f377e24fd0d36dabd715f70bad38/golden/go/search/search.go#L225.
//
// An empty string is technically not a valid expectations.Label (nor the corresponding Label type
// in rpc_types.ts) because the only allowed values are "positive", "negative" or "untriaged".
//
// The legacy, Polymer-based search-page-sk passes the contents of bulk_triage_data as-is to this
// component as the allDigests argument to a call to the setDigests() method defined below:
// https://github.com/google/skia-buildbot/blob/89e3a329bda8f377e24fd0d36dabd715f70bad38/golden/frontend/res/imp/search-page-sk.html#L346
//
// The legacy search page also builds the pageDigests argument to the setDigests() method using
// the empty string as a Label in the same exact way as in the bulk_triage_data field:
// https://github.com/google/skia-buildbot/blob/89e3a329bda8f377e24fd0d36dabd715f70bad38/golden/frontend/res/imp/search-page-sk.html#L432
//
// To preserve backwards-compatibility with the legacy search page, this component turns a blind
// eye to said invalid Label values, and passes the labels as-is to the /json/v1/triage endpoint,
// which ignores any digests for which the expectations.Label is set to the empty string:
// https://github.com/google/skia-buildbot/blob/89e3a329bda8f377e24fd0d36dabd715f70bad38/golden/go/web/web.go#L970
//
// This is messy because neither the Golang nor the TypeScript types involved in the above RPCs
// capture the possibility of empty labels. In other words, the types do not correctly describe
// the actual data, which can be a source of confusion and potential bugs in the future.
//
// Once we delete the legacy search page, we can clean things up by making the following changes:
//
// 1. Change the search RPC to use "untriaged" instead of the empty string to indicate that a
// digest does not have a closest triaged reference digest.
//
// 2. Change bulk-triage-sk to exclude any such digests from the /json/v1/triage RPC when
// triaging by "closest".
//
// 3. Delete any code in the /json/v1/triage endpoint that handles empty labels.
//
// TODO(lovisolo): Execute the above plan after the legacy search page is deleted.
/**
* Deprecated. Use the currentPageDigests and allDigests property setters instead.
*
* TODO(lovisolo): Delete after the legacy search-page-sk is removed.
*/
setDigests(pageDigests: TriageRequestData, allDigests: TriageRequestData) {
this.currentPageDigests = pageDigests;
this.allDigests = allDigests;
}
/**
* The digests in the current page of search results, mapped to the labels of their closest
* triaged reference digests.
*
* The labels will be applied when using the "closest" bulk triage option.
*/
get currentPageDigests() { return this._pageDigests; }
set currentPageDigests(digests: TriageRequestData) {
this._pageDigests = digests;
this._pageDigestCount = this._countDigests(digests);
this._render();
}
/**
* All the digests matching the current search (not just the ones in the current page of search
* results), mapped to the labels of their closest triaged reference digests.
*
* The labels will be applied when using the "closest" bulk triage option.
*/
get allDigests() { return this._allDigests; }
set allDigests(digests: TriageRequestData) {
this._allDigests = digests;
this._allDigestCount = this._countDigests(digests);
this._render();
}
private _countDigests(testDigestLabelMap: TriageRequestData) {
let count = 0;
for (const testName of Object.keys(testDigestLabelMap)) {
count += Object.keys(testDigestLabelMap[testName]).length;
}
return count;
}
private _setDesiredLabel(newValue: BulkTriageLabel) {
this.value = newValue;
}
private _cancel() {
this.dispatchEvent(new CustomEvent('bulk_triage_cancelled', { bubbles: true }));
}
/**
* This creates an object that can be sent to the triage RPC on the Gold server. The labels
* will be set to match the current value. See frontend.TriageRequest for more.
*/
private _getTriageStatuses() {
let baseDigests = this._pageDigests;
if (this._triageAll) {
baseDigests = this._allDigests;
}
if (this.value === 'closest') {
return baseDigests;
}
const copyWithSameValue: TriageRequestData = {};
for (const testName of Object.keys(baseDigests)) {
copyWithSameValue[testName] = {};
for (const digest of Object.keys(baseDigests[testName])) {
copyWithSameValue[testName][digest] = this.value;
}
}
return copyWithSameValue;
}
private _triage() {
const triageRequest: TriageRequest = {
testDigestStatus: this._getTriageStatuses(),
changelist_id: this.changeListID,
crs: this.crs,
}
sendBeginTask(this);
this.dispatchEvent(new CustomEvent('bulk_triage_invoked', { bubbles: true }));
fetch('/json/v1/triage', {
method: 'POST',
body: JSON.stringify(triageRequest)
}).then(() => {
// Even if we get back a non-200 code, we want to say we finished.
this.dispatchEvent(new CustomEvent('bulk_triage_finished', { bubbles: true }));
sendEndTask(this);
}).catch((e) => sendFetchError(this, e, 'bulk triaging'));
}
private _toggleAll(e: Event) {
e.preventDefault();
this._triageAll = !this._triageAll;
this._render();
}
};
define('bulk-triage-sk', BulkTriageSk);