blob: 043394970a6a5d73f1cdacf5c30f0138b233261b [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 dialogPolyfill from 'dialog-polyfill';
import { $$ } from 'common-sk/modules/dom';
import { classMap } from 'lit-html/directives/class-map';
import { define } from 'elements-sk/define';
import { html } from 'lit-html';
import { stateReflector } from 'common-sk/modules/stateReflector';
import { jsonOrThrow } from 'common-sk/modules/jsonOrThrow';
import { ElementSk } from '../../../infra-sk/modules/ElementSk';
import { escapeAndLinkify } from '../../../infra-sk/modules/linkify';
import {
humanReadableQuery, sendBeginTask, sendEndTask, sendFetchError,
} from '../common';
import '../../../infra-sk/modules/confirm-dialog-sk';
import '../edit-ignore-rule-sk';
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';
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} class=create>Create new ignore rule</button>
</div>
<confirm-dialog-sk></confirm-dialog-sk>
<dialog id=edit-ignore-rule-dialog>
<h2>${ele._ruleID ? 'Edit Ignore Rule' : 'Create Ignore Rule'}</h2>
<edit-ignore-rule-sk .paramset=${ele._paramset}></edit-ignore-rule-sk>
<button @click=${() => ele._editIgnoreRuleDialog.close()}>Cancel</button>
<button id=ok class=action @click=${ele._saveIgnoreRule}>
${ele._ruleID ? 'Update' : 'Create'}
</button>
</dialog>
<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 title="Edit this rule."
@click=${() => ele._editIgnoreRule(r)}></mode-edit-icon-sk>
<delete-icon-sk title="Delete this rule."
@click=${() => ele._deleteIgnoreRule(r)}></delete-icon-sk>
</td>
<td class=query><a href=${`/list?include=true&query=${encodeURIComponent(r.query)}`}
>${humanReadableQuery(r.query)}</a></td>
<td>${escapeAndLinkify(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 trimEmail(s) {
return `${s.split('@')[0]}@`;
}
define('ignores-page-sk', class extends ElementSk {
constructor() {
super(template);
this._rules = [];
this._paramset = {};
this._countAllTraces = false;
this._stateChanged = stateReflector(
/* getState */() => ({
// 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;
// This is the dialog element for creating or editing rules.
this._editIgnoreRuleDialog = null;
this._ruleID = '';
}
connectedCallback() {
super.connectedCallback();
this._render();
this._editIgnoreRuleDialog = $$('#edit-ignore-rule-dialog', this);
dialogPolyfill.registerDialog(this._editIgnoreRuleDialog);
}
_deleteIgnoreRule(rule) {
const dialog = $$('confirm-dialog-sk', this);
dialog.open('Are you sure you want to delete '
+ 'this ignore rule?').then(() => {
sendBeginTask(this);
fetch(`/json/v1/ignores/del/${rule.id}`, {
method: 'POST',
}).then(jsonOrThrow).then(() => {
this._fetch();
sendEndTask(this);
}).catch((e) => sendFetchError(this, e, 'deleting ignore'));
});
}
_editIgnoreRule(rule) {
const editor = $$('edit-ignore-rule-sk', this);
editor.reset();
editor.query = rule.query;
editor.note = rule.note;
editor.expires = rule.expires;
this._ruleID = rule.id;
this._render();
this._editIgnoreRuleDialog.showModal();
}
_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,
};
sendBeginTask(this);
sendBeginTask(this);
// We always want the counts of the ignore rules, thus the parameter counts=1.
fetch('/json/v1/ignores?counts=1', extra)
.then(jsonOrThrow)
.then((arr) => {
this._rules = arr || [];
this._render();
sendEndTask(this);
})
.catch((e) => sendFetchError(this, e, 'ignores'));
fetch('/json/v1/paramset', extra)
.then(jsonOrThrow)
.then((paramset) => {
this._paramset = paramset;
this._render();
sendEndTask(this);
})
.catch((e) => sendFetchError(this, e, 'paramset'));
}
_newIgnoreRule() {
const editor = $$('edit-ignore-rule-sk', this);
editor.reset();
this._ruleID = '';
this._render();
this._editIgnoreRuleDialog.showModal();
}
_saveIgnoreRule() {
const editor = $$('edit-ignore-rule-sk', this);
if (editor.verifyFields()) {
const body = {
duration: editor.expires,
filter: editor.query,
note: editor.note,
};
// TODO(kjlubick) remove the / from the json endpoint
let url = '/json/v1/ignores/add/';
if (this._ruleID) {
url = `/json/v1/ignores/save/${this._ruleID}`;
}
sendBeginTask(this);
fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
}).then(jsonOrThrow).then(() => {
this._fetch();
sendEndTask(this);
}).catch((e) => sendFetchError(this, e, 'saving ignore'));
editor.reset();
this._editIgnoreRuleDialog.close();
}
}
_toggleCountAll(e) {
e.preventDefault();
this._countAllTraces = !this._countAllTraces;
this._stateChanged();
this._render();
}
});