blob: b0cbb9f1dd89162d7603d87871ff27ba67ca7aac [file] [log] [blame]
/**
* @module /silence-sk
* @description <h2><code>silence-sk</code></h2>
*
* @evt add-silence-note Sent when the user adds a note to an silence.
* The detail includes the text of the note and the key of the silence.
*
* <pre>
* detail {
* key: "12312123123",
* text: "blah blah blah",
* }
* </pre>
*
* @evt del-silence-note Sent when the user deletes a note on an silence.
* The detail includes the index of the note and the key of the silence.
*
* <pre>
* detail {
* key: "12312123123",
* index: 0,
* }
* </pre>
*
* @evt save-silence Sent when the user saves a silence.
* The detail is the silence.
*
* <pre>
* detail {
* silence: {...},
* }
* </pre>
*
* @evt archive-silence Sent when the user archives a silence.
* The detail is the silence.
*
* <pre>
* detail {
* silence: {...},
* }
* </pre>
*
* @evt reactivate-silence Sent when the user reactivates a silence.
* The detail is the silence.
*
* <pre>
* detail {
* silence: {...},
* }
* </pre>
*
* @evt delete-silence Sent when the user deletes a silence.
* The detail is the silence.
*
* <pre>
* detail {
* silence: {...},
* }
* </pre>
*
* @evt delete-silence-param Sent when the user deletes a param from a silence.
* The detail is a copy of the silence with the parameter deleted.
*
* <pre>
* detail {
* silence: {...},
* }
* </pre>
*
* @evt modify-silence-param Sent when the user modifies a param from a silence.
* The detail is a copy of the silence with the parameter modified.
*
* <pre>
* detail {
* silence: {...},
* }
* </pre>
*
* @evt add-silence-param Sent when the user add a param to a silence.
* The detail is a copy of the silence with the new parameter added.
*
* <pre>
* detail {
* silence: {...},
* }
* </pre>
*
*/
import { define } from '../../../elements-sk/modules/define';
import '../../../elements-sk/modules/icons/add-box-icon-sk';
import '../../../elements-sk/modules/icons/delete-icon-sk';
import { $$ } from '../../../infra-sk/modules/dom';
import { diffDate } from '../../../infra-sk/modules/human';
import { errorMessage } from '../../../elements-sk/modules/errorMessage';
import { html, render, TemplateResult } from 'lit-html';
import {
abbr, displaySilence, expiresIn, getDurationTillNextDay, displayNotes,
} from '../am';
import * as paramset from '../paramset';
import {
Incident, ParamSet, Note, Silence,
} from '../json';
const BOT_CENTRIC_PARAMS = ['alertname', 'bot'];
export class State {
key: string = '';
param_set: ParamSet = {};
duration: string = '';
created: number = 0;
user: string = '';
notes: Note[] = [];
active: boolean = false;
}
export class SilenceSk extends HTMLElement {
notes: Note[] = [];
private state: State = {
key: '',
param_set: {},
duration: '',
created: 0,
user: '',
notes: [],
active: false,
};
private incidents: Incident[] = [];
private static template = (ele: SilenceSk) => html`
<h2 class=${ele.classOfH2()} @click=${ele.headerClick}>${displaySilence(ele.state.param_set)}</h2>
<div class=body>
<section class=actions>
${ele.actionButtons()}
</section>
<table class=info>
<tr><th>User:</th><td>${ele.state.user}</td></th>
<tr><th>Duration:</th><td><input class="duration" @change=${ele.durationChange} value=${ele.state.duration}></input><button class="param-btns" @click=${ele.tillNextShift}>Till next shift</button></td></th>
<tr><th>Created</th><td title=${new Date(ele.state.created * 1000).toLocaleString()}>${diffDate(ele.state.created * 1000)}</td></tr>
<tr><th>Expires</th><td>${expiresIn(ele.state.active, ele.state.created, ele.state.duration)}</td></tr>
</table>
<table class=params>
${ele.table()}
</table>
<section class=notes>
${displayNotes(ele.state.notes, ele.state.key, 'del-silence-note')}
</section>
<section class=addNote>
${ele.displayAddNote()}
</section>
<section class=matches>
<h1>Matches</h1>
${ele.displayMatches()}
</section>
</div>
`;
connectedCallback(): void {
this._render();
}
/** @prop silence_state A Silence. */
get silence_state(): State { return this.state; }
set silence_state(val: State) {
this.state = val;
this._render();
}
/** @prop silence_incidents The current active incidents. */
get silence_incidents(): Incident[] { return this.incidents; }
set silence_incidents(val: Incident[]) {
this.incidents = val;
this._render();
}
private table(): TemplateResult[] {
const keys = Object.keys(this.state.param_set);
keys.sort();
const botCentricParams = JSON.stringify(keys) === JSON.stringify(BOT_CENTRIC_PARAMS);
const rules = keys.filter((k) => !k.startsWith('__')).map((k) => html`
<tr>
<td>
<delete-icon-sk title='Delete rule.' @click=${() => this.deleteRule(k)}></delete-icon-sk>
</td>
<th>${k}</th>
<td>
<input class=param-val @change=${(e: Event) => this.modifyRule(e, k)} .value=${this.displayParamValue(this.state.param_set[k]!)}></input>
${this.displayAddBots(botCentricParams, k)}
</td>
</tr>`);
rules.push(html`
<tr>
<td>
<add-box-icon-sk title='Add rule.' @click=${() => this.addRule()}></add-box-icon-sk>
</td>
<td>
<input id='add_param_key'></input>
</td>
<td>
<input class=param-val id='add_param_value'></input>
</td>
</tr>
`);
return rules;
}
private displayAddBots(botCentricParams: boolean, key: string): TemplateResult {
if (botCentricParams && key === 'bot') {
return html`<button class="param-btns" @click=${() => this.botsChooser()}>Add bot</button>`;
}
return html``;
}
private displayParamValue(paramValue: string[]): string|string[] {
if (paramValue.length > 1) {
return `${paramValue.join('|')}`;
}
return paramValue;
}
private displayAddNote(): TemplateResult {
if (this.state.key) {
return html`
<textarea rows=2 cols=80 placeholder="Add description for the silence"></textarea>
<button @click=${this.addNote}>Submit</button>
`;
}
return html`<textarea rows=2 cols=80 placeholder="Add description for the silence"></textarea>`;
}
private gotoIncident(incident: Incident): void {
window.location.href = `/?alert_id=${incident.id}&tab=1`;
}
private displayMatches(): TemplateResult[] {
if (!this.incidents) {
return [];
}
return this.incidents.filter(
(incident) => paramset.match(this.state.param_set, incident.params) && incident.active,
).map((incident) => html`<h2 @click=${() => this.gotoIncident(incident)}> ${incident.params.alertname} ${abbr(incident.params.abbr)}</h2>`);
}
private classOfH2(): string {
if (!this.state.active) {
return 'inactive';
}
return '';
}
private actionButtons(): TemplateResult {
if (this.state.active) {
return html`<button @click=${this.save}>Save</button>
<button @click=${this.archive}>Archive</button>`;
}
return html`<button @click=${this.reactivate}>Reactivate</button>
<delete-icon-sk title='Delete silence.' @click=${this.delete}></delete-icon-sk>`;
}
private headerClick(): void {
if (this.hasAttribute('collapsed')) {
this.removeAttribute('collapsed');
} else {
this.setAttribute('collapsed', '');
}
}
private durationChange(e: Event): void {
this.state.duration = (e.target as HTMLInputElement).value;
}
// Populates duration till next Monday 9am.
private tillNextShift(): void {
this.state.duration = getDurationTillNextDay(1, 9);
this._render();
}
private save(): void {
const detail = {
silence: this.state,
};
if (!this.state.key) {
const textarea = $$('textarea', this)! as HTMLInputElement;
if (!textarea.value) {
errorMessage('Please enter a description for the silence');
textarea.focus();
return;
}
detail.silence.notes = [{
text: textarea.value,
ts: Math.floor(new Date().getTime() / 1000),
author: '', // The backend fills in the author.
}];
}
this.dispatchEvent(new CustomEvent('save-silence', { detail: detail, bubbles: true }));
}
private archive(): void {
const detail = {
silence: this.state,
};
this.dispatchEvent(new CustomEvent('archive-silence', { detail: detail, bubbles: true }));
}
private reactivate(): void {
const detail = {
silence: this.state,
};
this.dispatchEvent(new CustomEvent('reactivate-silence', { detail: detail, bubbles: true }));
}
private delete(): void {
const detail = {
silence: this.state,
};
this.dispatchEvent(new CustomEvent('delete-silence', { detail: detail, bubbles: true }));
}
private deleteRule(key: string): void {
const silence = JSON.parse(JSON.stringify(this.state)) as Silence;
delete silence.param_set[key];
const detail = {
silence: silence,
};
this.dispatchEvent(new CustomEvent('delete-silence-param', { detail: detail, bubbles: true }));
}
private modifyRule(e: Event, key: string): void {
const silence = JSON.parse(JSON.stringify(this.state)) as Silence;
silence.param_set[key] = [(e.target as HTMLInputElement).value];
const detail = {
silence: silence,
};
this.dispatchEvent(new CustomEvent('modify-silence-param', { detail: detail, bubbles: true }));
}
private addRule(): void {
const keyInput = $$('#add_param_key', this) as HTMLInputElement;
if (!keyInput.value) {
errorMessage('Please enter a name for the new param');
keyInput.focus();
return;
}
const valueInput = $$('#add_param_value', this) as HTMLInputElement;
if (!valueInput.value) {
errorMessage('Please enter a value for the new param');
valueInput.focus();
return;
}
// Dispatch event adding the new silence param.
const silence = JSON.parse(JSON.stringify(this.state)) as Silence;
silence.param_set[keyInput.value] = [valueInput.value];
const detail = {
silence: silence,
};
this.dispatchEvent(new CustomEvent('add-silence-param', { detail: detail, bubbles: true }));
// Reset the manual param key and value.
keyInput.value = '';
valueInput.value = '';
}
private botsChooser(): void {
this.dispatchEvent(new CustomEvent('bot-chooser', { detail: {}, bubbles: true }));
}
private addNote(): void {
const textarea = $$('textarea', this) as HTMLInputElement;
const detail = {
key: this.state.key,
text: textarea.value,
};
this.dispatchEvent(new CustomEvent('add-silence-note', { detail: detail, bubbles: true }));
textarea.value = '';
}
private _render(): void {
render(SilenceSk.template(this), this, { eventContext: this });
}
}
define('silence-sk', SilenceSk);