blob: 7250dd186ca271e9e74b851f2c65f4aeb2cb6c18 [file] [log] [blame]
/**
* @module module/ignores-page-sk
* @description <h2><code>ignores-page-sk</code></h2>
*
* Page to view/edit/delete ignore rules.
*/
import { classMap } from 'lit-html/directives/class-map';
import { html } from 'lit-html';
import * as human from '../../../infra-sk/modules/human';
import { define } from '../../../elements-sk/modules/define';
import { stateReflector } from '../../../infra-sk/modules/stateReflector';
import { jsonOrThrow } from '../../../infra-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 {
IgnoreRule,
IgnoreRuleBody,
IgnoresResponse,
ParamSet,
} from '../rpc_types';
import { EditIgnoreRuleSk } from '../edit-ignore-rule-sk/edit-ignore-rule-sk';
import { ConfirmDialogSk } from '../../../infra-sk/modules/confirm-dialog-sk/confirm-dialog-sk';
import '../../../infra-sk/modules/confirm-dialog-sk';
import '../edit-ignore-rule-sk';
import '../../../elements-sk/modules/checkbox-sk';
import '../../../elements-sk/modules/icons/delete-icon-sk';
import '../../../elements-sk/modules/icons/info-outline-icon-sk';
import '../../../elements-sk/modules/icons/mode-edit-icon-sk';
function trimEmail(s: string) {
return `${s.split('@')[0]}@`;
}
export class IgnoresPageSk extends ElementSk {
private static template = (ele: IgnoresPageSk) => 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) => IgnoresPageSk.ruleTemplate(ele, r))}
</tbody>
</table>
`;
private static ruleTemplate = (ele: IgnoresPageSk, r: IgnoreRule) => {
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>
`;
};
private rules: IgnoreRule[] = [];
private paramset: ParamSet = {};
private countAllTraces = false;
private ruleID = '';
private editIgnoreRuleDialog?: HTMLDialogElement; // Dialog for creating or editing rules.
private editIgnoreRuleSk?: EditIgnoreRuleSk;
private confirmDialogSk?: ConfirmDialogSk;
private readonly stateChanged: () => void;
private fetchController?: AbortController; // Allows us to abort fetches if we fetch again.
constructor() {
super(IgnoresPageSk.template);
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 as boolean) || false;
this.fetch();
this._render();
}
);
}
connectedCallback(): void {
super.connectedCallback();
this._render();
this.editIgnoreRuleDialog = this.querySelector<HTMLDialogElement>(
'#edit-ignore-rule-dialog'
)!;
this.editIgnoreRuleSk = this.querySelector<EditIgnoreRuleSk>(
'edit-ignore-rule-sk'
)!;
this.confirmDialogSk =
this.querySelector<ConfirmDialogSk>('confirm-dialog-sk')!;
}
private deleteIgnoreRule(rule: IgnoreRule) {
this.confirmDialogSk!.open(
// eslint-disable-next-line no-useless-concat
'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'));
});
}
private editIgnoreRule(rule: IgnoreRule) {
this.editIgnoreRuleSk!.reset();
this.editIgnoreRuleSk!.query = rule.query;
this.editIgnoreRuleSk!.note = rule.note;
this.editIgnoreRuleSk!.expires = rule.expires;
this.ruleID = rule.id;
this._render();
this.editIgnoreRuleDialog!.showModal();
}
private 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);
const url = '/json/v2/ignores';
fetch(url, extra)
.then(jsonOrThrow)
.then((response: IgnoresResponse) => {
this.rules = response.rules || [];
this._render();
sendEndTask(this);
})
.catch((e) => sendFetchError(this, e, 'ignores'));
const paramsUrl = '/json/v2/paramset';
fetch(paramsUrl, extra)
.then(jsonOrThrow)
.then((paramset: ParamSet) => {
this.paramset = paramset;
this._render();
sendEndTask(this);
})
.catch((e) => sendFetchError(this, e, 'paramset'));
}
private newIgnoreRule() {
this.editIgnoreRuleSk!.reset();
this.ruleID = '';
this._render();
this.editIgnoreRuleDialog!.showModal();
}
private saveIgnoreRule() {
if (this.editIgnoreRuleSk!.verifyFields()) {
const body: IgnoreRuleBody = {
duration: this.editIgnoreRuleSk!.expires,
filter: this.editIgnoreRuleSk!.query,
note: this.editIgnoreRuleSk!.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'));
this.editIgnoreRuleSk!.reset();
this.editIgnoreRuleDialog!.close();
}
}
private toggleCountAll(e: Event) {
e.preventDefault();
this.countAllTraces = !this.countAllTraces;
this.stateChanged();
this._render();
}
}
define('ignores-page-sk', IgnoresPageSk);