| /** |
| * @module module/triage-page-sk |
| * @description <h2><code>triage-page-sk</code></h2> |
| * |
| * Allows triaging clusters. |
| * |
| */ |
| import dialogPolyfill from 'dialog-polyfill' |
| import { ElementSk } from '../../../infra-sk/modules/ElementSk' |
| import { equals, deepCopy } from 'common-sk/modules/object' |
| import { errorMessage } from 'elements-sk/errorMessage.js' |
| import { fromObject } from 'common-sk/modules/query' |
| import { html } from 'lit-html' |
| import { jsonOrThrow } from 'common-sk/modules/jsonOrThrow' |
| import { stateReflector } from 'common-sk/modules/stateReflector' |
| |
| import 'elements-sk/spinner-sk' |
| import 'elements-sk/styles/buttons' |
| import 'elements-sk/styles/select' |
| |
| import '../cluster-summary2-sk' |
| import '../commit-detail-sk' |
| import '../day-range-sk' |
| import '../triage-status-sk' |
| |
| const _allFilters = (ele) => ele._all_filter_options.map( |
| (o) => html` |
| <option |
| ?selected=${ele._state.alert_filter === o.value} |
| value=${o.value} |
| title=${o.title} |
| >${o.display} |
| </option>`); |
| |
| const _statusItems = (ele) => ele._currentClusteringStatus.map((item) => html` |
| <table> |
| <tr> |
| <th>Alert</th> |
| <td><a href='/a/?${item.alert.id}'>${item.alert.display_name}</a></td> |
| </tr> |
| <tr> |
| <th>Commit</th> |
| <td><commit-detail-sk .cid=${item.commit}></commit-detail-sk></td> |
| </tr> |
| <tr> |
| <th>Step</th> |
| <td>${item.step}/${item.total}</td> |
| </tr> |
| </table> |
| `); |
| |
| const _headers = (ele) => ele._reg.header.map((item) => { |
| let displayName = item.display_name; |
| if (!item.display_name) { |
| displayName = item.query.slice(0, 10); |
| } |
| // The colspan=2 is important since we will have two columns under each |
| // header, one for high and one for low. |
| return html`<th colspan=2><a href='/a/?${item.id}'>${displayName}</a></th>`; |
| }); |
| |
| const _subHeaders = (ele) => ele._reg.header.map((_, index) => { |
| const ret = []; |
| if (ele._stepDownAt(index)) { |
| ret.push(html`<th>Low</th>`); |
| } |
| if (ele._stepUpAt(index)) { |
| ret.push(html`<th>High</th>`); |
| } |
| // If we have only one of High or Low we stuff in an empty th to match |
| // colspan=2 above. |
| if (ele._notBoth(index)) { |
| ret.push(html`<th></th>`); |
| } |
| return ret; |
| }); |
| |
| function _full_summary(frame, summary) { |
| return { |
| frame: frame, |
| summary: summary, |
| } |
| } |
| |
| const _lowCell = (ele, rowIndex, col, colIndex) => { |
| if (col && col.low) { |
| return html`<triage-status-sk |
| .alert=${ele._alertAt(colIndex)} |
| .cluster_type=low |
| .full_summary=${_full_summary(col.frame, col.low)} |
| .triage=${col.low_status}></triage-status-sk> `; |
| } else { |
| return html`<a |
| title='No clusters found.' |
| href='/g/c/${ele._hashFrom(rowIndex)}?query=${ele._encQueryFrom(colIndex)}'>∅</a> `; |
| } |
| } |
| |
| const _highCell = (ele, rowIndex, col, colIndex) => { |
| if (col && col.high) { |
| return html`<triage-status-sk |
| .alert=${ele._alertAt(colIndex)} |
| .cluster_type=high |
| .full_summary=${_full_summary(col.frame, col.high)} |
| .triage=${col.high_status}></triage-status-sk> `; |
| } else { |
| return html`<a |
| title='No clusters found.' |
| href='/g/c/${ele._hashFrom(rowIndex)}?query=${ele._encQueryFrom(colIndex)}'>∅</a> `; |
| } |
| } |
| |
| const _columns = (ele, row, rowIndex) => row.columns.map((col, colIndex) => { |
| const ret = []; |
| |
| if (ele._stepDownAt(colIndex)) { |
| ret.push(html` |
| <td class=cluster> |
| ${_lowCell(ele, rowIndex, col, colIndex)} |
| </td> |
| `); |
| } |
| |
| if (ele._stepUpAt(colIndex)) { |
| ret.push(html` |
| <td class=cluster> |
| ${_highCell(ele, rowIndex, col, colIndex)} |
| </td> |
| `); |
| } |
| |
| if (ele._notBoth(colIndex)) { |
| ret.push(html`<td></td>`); |
| } |
| return ret; |
| }); |
| |
| const _rows= (ele) => ele._reg.table.map((row, rowIndex) => html` |
| <tr> |
| <td class=fixed> |
| <commit-detail-sk .cid=${row.cid}></commit-detail-sk> |
| </td> |
| ${_columns(ele, row, rowIndex)} |
| </tr>`); |
| |
| const template = (ele) => html` |
| <header> |
| <details> |
| <summary> |
| <h2>Filter</h2> |
| </summary> |
| <h3>Which commits to display.</h3> |
| <select |
| @input=${ele._commitsChange} |
| > |
| <option |
| ?selected=${ele._state.subset==='all'} |
| value=all |
| title='Show results for all commits in the time range.'>All</option> |
| <option |
| ?selected=${ele._state.subset==='flagged'} |
| value=flagged |
| title='Show only the commits with regressions in the given time range regardless of triage status.'>Regressions</option> |
| <option |
| ?selected=${ele._state.subset==='untriaged'} |
| value=untriaged |
| title='Show only commits with untriaged regressions in the given time range.'>Untriaged</option> |
| </select> |
| |
| <h3>Which alerts to display.</h3> |
| |
| <select @input=${ele._filterChange}> |
| ${_allFilters(ele)} |
| </select> |
| </details> |
| <details> |
| <summary> |
| <h2>Range</h2> |
| </summary> |
| <day-range-sk @day-range-change=${ele._rangeChange} begin=${ele._state.begin} end=${ele._state.end}></day-range-sk> |
| </details> |
| <details @toggle=${ele._toggleStatus}> |
| <summary> |
| <h2>Status</h2> |
| </summary> |
| <div> |
| <p>The current work on detecting regressions:</p> |
| <div class=status> |
| ${_statusItems(ele)} |
| </div> |
| </div> |
| </details> |
| </header> |
| <spinner-sk ?active=${ele._triageInProgress || ele._refreshRangeInProgress}></spinner-sk> |
| |
| <dialog> |
| <cluster-summary2-sk |
| @open-keys=${ele._openKeys} |
| @triaged=${ele._triaged} |
| .full_summary=${ele._dialog_state.full_summary} |
| .triage=${ele._dialog_state.triage}> |
| </cluster-summary2-sk> |
| <div class=buttons> |
| <button @click=${ele._close}>Close</button> |
| </div> |
| </dialog> |
| |
| <table @start-triage=${ele._triage_start}> |
| <tr> |
| <th>Commit</th> |
| ${_headers(ele)} |
| </tr> |
| <tr> |
| <th></th> |
| ${_subHeaders(ele)} |
| </tr> |
| ${_rows(ele)} |
| </table> |
| </template> |
| `; |
| |
| window.customElements.define('triage-page-sk', class extends ElementSk { |
| constructor() { |
| super(template); |
| const now = Math.floor(Date.now()/1000); |
| |
| // The state to reflect to the URL, also the body of the POST request |
| // we send to /_/reg/. |
| this._state = { |
| begin: now - 2*7*24*60*60, // 2 weeks. |
| end: now, |
| subset: "untriaged", |
| filter: "ALL", |
| }; |
| |
| this._reg = { |
| header: [], |
| table: [], |
| }; |
| |
| this._all_filter_options = [] |
| |
| this._triageInProgress = false; |
| |
| this._refreshRangeInProgress = false; |
| |
| this._currentClusteringStatus = []; |
| |
| // The ID of the setInterval that is updating _currentClusteringStatus. |
| this._statusIntervalID = 0; |
| |
| this._dialog_state = { |
| full_summary: {}, |
| triage: {}, |
| }; |
| |
| this._lastState = {}; |
| |
| this._firstConnect = false; |
| } |
| |
| connectedCallback() { |
| super.connectedCallback(); |
| if (this._firstConnect) { |
| return; |
| } |
| this._firstConnect = true; |
| |
| this._render(); |
| this._dialog = this.querySelector('triage-page-sk > dialog'); |
| dialogPolyfill.registerDialog(this.querySelector('dialog')); |
| this._stateHasChanged = stateReflector(() => this._state, (state) => { |
| this._state = state; |
| // For backwards compatibility with existing URLs that used 'filter'. |
| if (this._state.filter) { |
| this._state.alert_filter = this._state.filter; |
| } |
| this._render(); |
| this._updateRange(); |
| }); |
| } |
| |
| _commitsChange(e) { |
| this._state.subset = e.target.value; |
| this._updateRange(); |
| this._stateHasChanged(); |
| } |
| |
| _filterChange(e) { |
| this._state.alert_filter = e.target.value; |
| this._updateRange(); |
| this._stateHasChanged(); |
| } |
| |
| _toggleStatus(e) { |
| if (e.target.open) { |
| this._statusIntervalID = window.setInterval(() => this._pollStatus(), 5000); |
| this._pollStatus(); |
| } else { |
| window.clearInterval(this._statusIntervalID); |
| } |
| } |
| |
| _pollStatus() { |
| fetch('/_/reg/current').then(jsonOrThrow).then((json) => { |
| this._currentClusteringStatus = json; |
| this._render(); |
| }).catch(errorMessage); |
| } |
| |
| _triage_start(e) { |
| this._dialog_state = e.detail; |
| this._render(); |
| this._dialog.show(); |
| } |
| |
| _triaged(e) { |
| e.stopPropagation(); |
| const body = Object.assign({}, e.detail); |
| body.alert = this._dialog_state.alert; |
| body.cluster_type = this._dialog_state.cluster_type; |
| this._dialog.close(); |
| this._render(); |
| if (this._triageInProgress) { |
| errorMessage("A triage request is in progress."); |
| return; |
| } |
| this._triageInProgress = true; |
| fetch('/_/triage/', { |
| method: 'POST', |
| body: JSON.stringify(body), |
| headers:{ |
| 'Content-Type': 'application/json' |
| } |
| }).then(jsonOrThrow).then((json) => { |
| this._triageInProgress = false; |
| this._render(); |
| if (json.bug) { |
| // Open the bug reporting page in a new window. |
| window.open(json.bug, '_blank'); |
| } |
| }).catch((msg) => { |
| if (msg) { |
| errorMessage(msg, 10000); |
| } |
| this._triageInProgress = false; |
| this._render(); |
| }); |
| } |
| |
| _close() { |
| this._dialog.close(); |
| } |
| |
| _stepUpAt(index) { |
| const dir = this._reg.header[index].direction; |
| return dir === 'UP' || dir === 'BOTH'; |
| } |
| |
| _stepDownAt(index) { |
| const dir = this._reg.header[index].direction; |
| return dir === 'DOWN' || dir === 'BOTH'; |
| } |
| |
| _notBoth(index) { |
| return this._reg.header[index].direction != 'BOTH'; |
| } |
| |
| _alertAt(index) { |
| return this._reg.header[index]; |
| } |
| |
| _encQueryFrom(colIndex) { |
| return encodeURIComponent(this._reg.header[colIndex].query); |
| } |
| |
| _hashFrom(rowIndex) { |
| return this._reg.table[rowIndex].cid.hash; |
| } |
| |
| _openKeys(e) { |
| const query = { |
| keys: e.detail.shortcut, |
| begin: e.detail.begin, |
| end: e.detail.end, |
| xbaroffset: e.detail.xbar.offset, |
| num_commits: 50, |
| request_type: 1, |
| }; |
| window.open('/e/?' + fromObject(query), '_blank'); |
| } |
| |
| _rangeChange(e) { |
| this._state.begin = Math.floor(e.detail.begin); |
| this._state.end = Math.floor(e.detail.end); |
| this._stateHasChanged(); |
| this._updateRange(); |
| } |
| |
| _updateRange() { |
| if (this._refreshRangeInProgress) { |
| return; |
| } |
| if (equals(this._lastState, this._state)) { |
| return; |
| } |
| this._lastState = deepCopy(this._state); |
| this._refreshRangeInProgress = true; |
| this._render(); |
| fetch('/_/reg/', { |
| method: 'POST', |
| body: JSON.stringify(this._state), |
| headers:{ |
| 'Content-Type': 'application/json' |
| } |
| }).then(jsonOrThrow).then((json) => { |
| this._refreshRangeInProgress = false; |
| this._reg = json; |
| this._calc_all_filter_options(); |
| this._render(); |
| }).catch((msg) => { |
| if (msg) { |
| errorMessage(msg, 10000); |
| } |
| this._refreshRangeInProgress = false; |
| this._render(); |
| }); |
| } |
| |
| _calc_all_filter_options() { |
| const opts = [ |
| { |
| value: 'ALL', |
| title: 'Show all alerts.', |
| display: 'Show all alerts.', |
| }, |
| { |
| value: 'OWNER', |
| title: 'Show only the alerts owned by the logged in user (or all alerts if the user doesn\'t own any alerts).', |
| display: 'Show alerts you own.', |
| }, |
| ]; |
| if (this._reg && this._reg.categories) { |
| this._reg.categories.forEach((cat) => { |
| const displayName = cat || '(default)'; |
| opts.push({ |
| value: `cat:${cat}`, |
| title: `Show only the alerts in the ${displayName} category.`, |
| display: `Category: ${displayName}`, |
| }); |
| }); |
| } |
| this._all_filter_options = opts; |
| } |
| }); |