| /** |
| * @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(); |
| } |
| }); |