blob: 4992a45c5068be3653a8b2e0bd08e4ee5037f82f [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 { html } from 'lit-html';
import { jsonOrThrow } from 'common-sk/modules/jsonOrThrow';
import { stateReflector } from 'common-sk/modules/stateReflector';
import { ElementSk } from '../../../infra-sk/modules/ElementSk';
import '../pagination-sk';
import { sendBeginTask, sendEndTask, sendFetchError } from '../common';
import {
TriageDelta2, TriageLogEntry2, TriageLogResponse2,
} from '../rpc_types';
import { PaginationSkPageChangedEventDetail } from '../pagination-sk/pagination-sk';
export class TriagelogPageSk extends ElementSk {
private static template = (el: TriagelogPageSk) => html`
<table>
<thead>
<tr>
<th>Timestamp</th>
<th>Name</th>
<th># Changes</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
${el.entriesV2.map((entry: TriageLogEntry2) => TriagelogPageSk.logEntryTemplate2(el, entry))}
</tbody>
</table>
<pagination-sk offset=${el.pageOffset}
page_size=${el.pageSize}
total=${el.totalEntries}
@page-changed=${el.pageChanged}>
</pagination-sk>
`;
private static logEntryTemplate2 = (el: TriagelogPageSk, entry: TriageLogEntry2) => html`
<tr>
<td class=timestamp>${TriagelogPageSk.toLocalDate(entry.ts)}</td>
<td class=author>${entry.name}</td>
<td class=num-changes>${entry.details.length}</td>
<td class=actions>
<button @click=${() => el.undoEntry(entry.id)}
class=undo>
Undo
</button>
</td>
</tr>
${entry.details ? TriagelogPageSk.detailsTemplate2(el, entry) : html``}
`;
private static detailsTemplate2 = (el: TriagelogPageSk, entry: TriageLogEntry2) => 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((e) => TriagelogPageSk.detailsEntryTemplate2(el, e))}
<tr class="details details-separator"><td colspan="4"></td></tr>
`;
private static detailsEntryTemplate2 = (el: TriagelogPageSk, delta: TriageDelta2) => {
let detailHref = `/detail?test=${delta.grouping.name}&digest=${delta.digest}`;
if (el.changelistID) {
detailHref += `&changelist_id=${el.changelistID}&crs=${el.crs}`;
}
return html`
<tr class=details>
<td></td>
<td class=test-name title="Grouping ${JSON.stringify(delta.grouping)}">${delta.grouping.name}</td>
<td class=digest>
<a href=${detailHref} target=_blank rel=noopener>
${delta.digest}
</a>
</td>
<td class=label title="${delta.label_before} => ${delta.label_after}">
${delta.label_after}
</td>
</tr>
`;
};
private entriesV2: TriageLogEntry2[] = []; // Log entries fetched from the server.
private pageOffset = 0; // Reflected in the URL.
private pageSize = 0; // Reflected in the URL.
private changelistID = ''; // Reflected in the URL.
private crs = ''; // Code Review System (e.g. 'gerrit', 'github')
private totalEntries = 0; // Total number of entries in the server.
private readonly stateChanged: ()=> void;
private fetchController?: AbortController;
constructor() {
super(TriagelogPageSk.template);
// stateReflector will trigger on DomReady.
this.stateChanged = stateReflector(
/* getState */ () => ({
offset: this.pageOffset,
page_size: this.pageSize,
changelist_id: this.changelistID,
crs: this.crs,
}),
/* 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 as number || 0;
this.pageSize = newState.page_size as number || 20;
this.changelistID = newState.changelist_id as string || '';
this.crs = newState.crs as string || '';
this._render();
this.fetchEntries();
},
);
}
connectedCallback(): void {
super.connectedCallback();
this._render();
}
private pageChanged(e: CustomEvent<PaginationSkPageChangedEventDetail>) {
this.pageOffset = Math.max(0, this.pageOffset + e.detail.delta * this.pageSize);
this.stateChanged();
this._render();
this.fetchEntries();
}
private undoEntry(entryId: string) {
sendBeginTask(this);
this.fetchV2(`/json/v2/triagelog/undo?id=${entryId}`, 'POST')
.then(() => {
this._render();
sendEndTask(this);
})
.catch((e) => sendFetchError(this, e, 'undo'));
}
private fetchEntries(sendBusyDoneEvents = true): Promise<void> {
let url = `/json/v2/triagelog?offset=${this.pageOffset}&size=${this.pageSize}`;
if (this.changelistID) {
url += `&changelist_id=${this.changelistID}&crs=${this.crs}`;
}
if (sendBusyDoneEvents) {
sendBeginTask(this);
}
return this.fetchV2(url, 'GET')
.then(() => {
this._render();
if (sendBusyDoneEvents) {
sendEndTask(this);
}
})
.catch((e) => sendFetchError(this, e, 'triagelog'));
}
private fetchV2(url: string, method: 'GET' | 'POST'): Promise<void> {
// Force only one fetch at a time. Abort any outstanding requests.
if (this.fetchController) {
this.fetchController.abort();
}
this.fetchController = new AbortController();
const options: RequestInit = {
method: method,
signal: this.fetchController.signal,
};
return fetch(url, options)
.then(jsonOrThrow)
.then((response: TriageLogResponse2) => {
this.entriesV2 = response.entries || [];
this.pageOffset = response.offset || 0;
this.pageSize = response.size || 0;
this.totalEntries = response.total || 0;
});
}
private static toLocalDate(timeStampMS: number): string {
return new Date(timeStampMS).toLocaleString();
}
}
define('triagelog-page-sk', TriagelogPageSk);