| /** |
| * @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); |