blob: b98bbae03f1e9fb78ff2323702fc8f12947fde75 [file] [log] [blame]
/**
* @module module/ignores-page-sk
* @description <h2><code>ignores-page-sk</code></h2>
*
* Page to view/edit/delete ignore rules.
*/
import * as human from 'common-sk/modules/human';
import { $$ } from 'common-sk/modules/dom';
import { classMap } from 'lit-html/directives/class-map.js';
import { define } from 'elements-sk/define';
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 'elements-sk/checkbox-sk';
import 'elements-sk/icon/delete-icon-sk';
import 'elements-sk/icon/info-outline-icon-sk';
import 'elements-sk/icon/mode-edit-icon-sk';
import 'elements-sk/styles/buttons';
import '../../../infra-sk/modules/confirm-dialog-sk';
const template = (ele) => html`
<div class=controls>
<checkbox-sk label="Only count traces with untriaged digests"
?checked=${!ele._countAllTraces} @click=${ele._toggleCountAll}></checkbox-sk>
<button @click=${ele._newIgnoreRule}>Create new ignore rule</button>
</div>
<confirm-dialog-sk></confirm-dialog-sk>
<table>
<thead>
<tr>
<th colspan=2>Filter</th>
<th>Note</th>
<th> Traces matched <br> exclusive/all
<info-outline-icon-sk class=small-icon
title="'all' is the number of traces that a given ignore rule applies to. 'exclusive' \
is the number of traces which are matched by the given ignore rule and no other ignore rule of the \
rules in this list. If the checkbox is checked to only count traces with untriaged digests, it \
means 'untriaged digests at head', which is typically an indication of a flaky test/config.">
</info-outline-icon-sk>
</th>
<th>Expires in</th>
<th>Created by</th>
<th>Updated by</th>
</tr>
</thead>
<tbody>
${ele._rules.map((r) => ruleTemplate(ele, r))}
</tbody>
</table>`;
const ruleTemplate = (ele, r) => {
const isExpired = Date.parse(r.expires) < Date.now();
return html`
<tr class=${classMap({expired: isExpired})}>
<td class=mutate-icons>
<mode-edit-icon-sk @click=${() => ele._edit(r)}></mode-edit-icon-sk>
<delete-icon-sk @click=${() => ele._delete(r)}></delete-icon-sk>
</td>
<td class=query><a href=${'/list?include=true&query=' + encodeURIComponent(r.query)}
>${humanReadableQuery(r.query)}</a></td>
<td>${r.note || '--'}</td>
<td class=matches title="these counts are recomputed every few minutes">
${ele._countAllTraces ? r.exclusiveCountAll : r.exclusiveCount} /
${ele._countAllTraces ? r.countAll: r.count}
</td>
<td class=${classMap({expired: isExpired})}>
${isExpired ? 'Expired': human.diffDate(r.expires)}
</td>
<td title=${'Originally created by ' + r.name}>${trimEmail(r.name)}</td>
<td title=${'Last updated by ' + r.updatedBy}>
${r.name === r.updatedBy ? '': trimEmail(r.updatedBy)}
</td>
</tr>`;
};
function humanReadableQuery(queryStr) {
if (!queryStr) {
return '';
}
return queryStr.split('&').join('\n')
}
function trimEmail(s) {
return s.split('@')[0] + '@';
}
define('ignores-page-sk', class extends ElementSk {
constructor() {
super(template);
this._rules = [];
this._countAllTraces = false;
this._stateChanged = stateReflector(
/*getState*/() => {
return {
// provide empty values
'count_all': this._countAllTraces,
}
}, /*setState*/(newState) => {
if (!this._connected) {
return;
}
// default values if not specified.
this._countAllTraces = newState.count_all || false;
this._fetch();
this._render();
});
// Allows us to abort fetches if we fetch again.
this._fetchController = null;
}
connectedCallback() {
super.connectedCallback();
this._render();
}
_delete(rule) {
const dialog = $$('confirm-dialog-sk', this);
dialog.open('Are you sure you want to delete '+
'this ignore rule?').then(() => {
this._sendBusy();
fetch('/json/ignores/del/'+rule.id, {
method: 'POST',
}).then(jsonOrThrow).then(() => {
this._fetch();
this._sendDone();
}).catch((e) => this._sendFetchError(e, 'deleting ignore'));
});
}
_edit(rule) {
// TODO(kjlubick)
console.log('edit', rule);
}
_fetch() {
if (this._fetchController) {
// Kill any outstanding requests
this._fetchController.abort();
}
// Make a fresh abort controller for each set of fetches.
// They cannot be re-used once aborted.
this._fetchController = new AbortController();
const extra = {
signal: this._fetchController.signal,
};
this._sendBusy();
return fetch('/json/ignores?counts=1', extra)
.then(jsonOrThrow)
.then((arr) => {
this._rules = arr || [];
this._render();
this._sendDone();
})
.catch((e) => this._sendFetchError(e, 'ignores'));
}
_newIgnoreRule(e) {
// TODO(kjlubick)
console.log('new rule');
}
_sendBusy() {
this.dispatchEvent(new CustomEvent('begin-task', {bubbles: true}));
}
_sendDone() {
this.dispatchEvent(new CustomEvent('end-task', {bubbles: true}));
}
_sendFetchError(e, what) {
this.dispatchEvent(new CustomEvent('fetch-error', {
detail: {
error: e,
loading: what,
}, bubbles: true}));
}
_toggleCountAll(e) {
e.preventDefault();
this._countAllTraces = !this._countAllTraces;
this._stateChanged();
this._render();
}
});