blob: b9a34a02927d61eb1a45d320393ee85b51d8599a [file] [log] [blame]
/**
* @module modules/triagelog-page-sk
* @description <h2><code>triagelog-page-sk</code></h2>
*
* Allows the user to page through the diff triage logs, and optionally undo
* labels applied to triaged diffs.
*/
import { define } from 'elements-sk/define'
import 'elements-sk/checkbox-sk'
import { ElementSk } from '../../../infra-sk/modules/ElementSk'
import { html } from 'lit-html'
import { jsonOrThrow } from 'common-sk/modules/jsonOrThrow';
import { stateReflector } from 'common-sk/modules/stateReflector';
import '../pagination-sk'
const template = (el) => html`
<table>
<thead>
<tr>
<th>Timestamp</th>
<th>Name</th>
<th># Changes</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
${el._entries.map((entry) => logEntryTemplate(el, entry))}
</tbody>
</table>
<pagination-sk offset=${el._pageOffset}
page_size=${el._pageSize}
total=${el._totalEntries}
@page-changed=${el._pageChanged}>
</pagination-sk>
`;
const logEntryTemplate = (el, entry) => html`
<tr>
<td class=timestamp>${el._toLocalDate(entry.ts)}</td>
<td class=author>${entry.name}</td>
<td class=num-changes>${entry.changeCount}</td>
<td class=actions>
<button @click=${() => el._undoEntry(entry.id)}
class=undo>
Undo
</button>
</td>
</tr>
${entry.details ? detailsTemplate(entry) : html``}
`;
const detailsTemplate = (entry) => html`
<tr class=details>
<td></td>
<td><strong>Test name</strong></td>
<td><strong>Digest</strong></td>
<td><strong>Label</strong></td>
</tr>
${entry.details.map(detailsEntryTemplate)}
<tr class="details details-separator"><td colspan="4"></td></tr>
`;
const detailsEntryTemplate = (detailsEntry) => html`
<tr class=details>
<td></td>
<td class=test-name>${detailsEntry.test_name}</td>
<td class=digest>
<a href="/detail?test=${detailsEntry.test_name}&digest=${detailsEntry.digest}"
target="_blank"
rel="noopener">
${detailsEntry.digest}
</a>
</td>
<td class=label>${detailsEntry.label}</td>
</tr>
`;
define('triagelog-page-sk', class extends ElementSk {
constructor() {
super(template);
this._entries = []; // Log entries fetched from the server.
this._pageOffset = 0; // Reflected in the URL.
this._pageSize = 0; // Reflected in the URL.
this._issue = 0; // Reflected in the URL.
this._totalEntries = 0; // Total number of entries in the server.
this._urlParamsLoaded = false;
// stateReflector will trigger on DomReady.
this._stateChanged = stateReflector(
/* getState */ () => this._getState(),
/* setState */ (newState) => {
// The stateReflector's lingering popstate event handler will continue
// to call this function on e.g. browser back button clicks long after
// this custom element is detached from the DOM.
if (!this._connected) {
return;
}
this._pageOffset = newState.offset || 0;
this._pageSize = newState.page_size || 20;
this._issue = newState.issue || 0;
this._render();
this._fetchEntries();
});
}
_getState() {
return {
'offset': this._pageOffset,
'page_size': this._pageSize,
'issue': this._issue,
};
}
connectedCallback() {
super.connectedCallback();
this._render();
}
_pageChanged(e) {
this._pageOffset =
Math.max(0, this._pageOffset + e.detail.delta * this._pageSize);
this._stateChanged();
this._render();
this._fetchEntries();
}
_undoEntry(entryId) {
this._sendBusy();
this._fetch(`/json/triagelog/undo?id=${entryId}`, 'POST')
// The undo RPC returns the first page of results with details hidden.
// But we always show details, so we need to make another request to
// fetch the triage log with details from /json/triagelog.
// TODO(lovisolo): Rethink this after we delete the old triage log page.
.then(() => this._fetchEntries(/* sendBusyDoneEvents= */ false))
.then(() => this._sendDone())
.catch((e) => this._sendFetchError(e));
}
_fetchEntries(sendBusyDoneEvents = true) {
let url =
`/json/triagelog?details=true&offset=${this._pageOffset}` +
`&size=${this._pageSize}`;
if (this._issue) {
url += `&issue=${this._issue}`;
}
if (sendBusyDoneEvents) {
this._sendBusy();
}
return this._fetch(url, 'GET')
.then(() => {
this._render();
if (sendBusyDoneEvents) {
this._sendDone();
}
})
.catch((e) => this._sendFetchError(e));
}
// Both /json/triagelog and /json/triagelog/undo RPCs return the same kind of
// response, which is a page with triage log entries. Therefore this method is
// called by both _fetchEntries and _undoEntry to carry out their
// corresponding RPCs and handle the server response.
_fetch(url, method) {
// Force only one fetch at a time. Abort any outstanding requests.
if (this._fetchController) {
this._fetchController.abort();
}
this._fetchController = new AbortController();
const options = {
method: method,
signal: this._fetchController.signal
};
return fetch(url, options)
.then(jsonOrThrow)
.then((json) => {
this._entries = json.data || [];
this._pageOffset = json.pagination.offset || 0;
this._pageSize = json.pagination.size || 0;
this._totalEntries = json.pagination.total || 0;
});
}
_toLocalDate(timeStampMS) {
return new Date(timeStampMS).toLocaleString();
}
_sendBusy() {
this.dispatchEvent(new CustomEvent('begin-task', {bubbles: true}));
}
_sendDone() {
this.dispatchEvent(new CustomEvent('end-task', {bubbles: true}));
}
_sendFetchError(error) {
this.dispatchEvent(new CustomEvent('fetch-error', {
detail: {
error: error,
loading: 'triagelog',
}, bubbles: true
}));
}
});