blob: dce63ad26694074ddd5db075ce60f2f7e419ed3b [file] [log] [blame]
/**
* @module incident-sk
* @description <h2><code>incident-sk</code></h2>
*
* <p>
* Displays a single Incident.
* </p>
*
* @attr minimized {boolean} If not set then the incident is displayed in expanded
* mode, otherwise it is displayed in compact mode.
*
* @attr params {boolean} If set then the incident params are displayed, only
* applicable if minimzed is true.
*
* @evt add-note Sent when the user adds a note to an incident.
* The detail includes the text of the note and the key of the incident.
*
* <pre>
* detail {
* key: "12312123123",
* text: "blah blah blah",
* }
* </pre>
*
* @evt del-note Sent when the user deletes a note on an incident.
* The detail includes the index of the note and the key of the incident.
*
* <pre>
* detail {
* key: "12312123123",
* index: 0,
* }
* </pre>
*
* @evt take Sent when the user wants the incident assigned to themselves.
* The detail includes the key of the incident.
*
* <pre>
* detail {
* key: "12312123123",
* }
* </pre>
*
* @evt assign Sent when the user want to assign the incident to someone else.
* The detail includes the key of the incident.
*
* <pre>
* detail {
* key: "12312123123",
* }
* </pre>
*
*/
import { define } from 'elements-sk/define';
import 'elements-sk/icon/delete-icon-sk';
import '../silence-sk';
import { $$ } from 'common-sk/modules/dom';
import { diffDate, strDuration } from 'common-sk/modules/human';
import { errorMessage } from 'elements-sk/errorMessage';
import { html, render } from 'lit-html';
import { until } from 'lit-html/directives/until';
import { jsonOrThrow } from 'common-sk/modules/jsonOrThrow';
import { abbr, linkify, notes } from '../am';
import * as paramset from '../paramset';
const MAX_MATCHING_SILENCES_TO_DISPLAY = 50;
function classOfH2(ele) {
if (!ele._state.active) {
return 'inactive';
}
if (ele._state.params.assigned_to) {
return 'assigned';
}
return '';
}
function table(o) {
const keys = Object.keys(o);
keys.sort();
return keys.filter((k) => !k.startsWith('__')).map((k) => html`<tr><th>${k}</th><td>${linkify(o[k])}</td></tr>`);
}
function actionButtons(ele) {
if (ele._state.active) {
let assignToOwnerButton = '';
if (ele._state.params.owner) {
assignToOwnerButton = html`<button @click=${ele._assignToOwner}>Assign to Owner</button>`;
}
return html`<section class=assign>
<button @click=${ele._take}>Take</button>
${assignToOwnerButton}
<button @click=${ele._assign}>Assign</button>
</section>`;
}
return html``;
}
function matchingSilences(ele) {
if (ele.hasAttribute('minimized')) {
return '';
}
// Filter out silences whose paramsets do not match and
// which have no notes if displaySilencesWithComments is true.
const filteredSilences = ele._silences.filter((silence) => paramset.match(silence.param_set, ele._state.params)
&& !(ele._displaySilencesWithComments && doesSilenceHaveNoNotes(silence)));
const ret = filteredSilences.slice(0, MAX_MATCHING_SILENCES_TO_DISPLAY).map((silence) => html`<silence-sk .state=${silence} collapsable collapsed></silence-sk>`);
if (!ret.length) {
ret.push(html`<div class=nosilences>None</div>`);
}
return ret;
}
function doesSilenceHaveNoNotes(silence) {
return !silence.notes || (silence.notes.length === 1 && silence.notes[0].text === '');
}
function lastSeen(ele) {
if (ele._state.active) {
return '';
}
return html`<tr><th>Last Seen</th><td title=${new Date(ele._state.last_seen * 1000).toLocaleString()}>${diffDate(ele._state.last_seen * 1000)}</td></tr>`;
}
function duration(ele) {
if (ele._state.active) {
return '';
}
return html`<tr><th>Duration</th><td>${strDuration(ele._state.last_seen - ele._state.start)}</td></tr>`;
}
function history(ele) {
if (ele.hasAttribute('minimized')) {
return '';
}
return fetch(`/_/recent_incidents?id=${ele._state.id}&key=${ele._state.key}`, {
headers: {
'content-type': 'application/json',
},
credentials: 'include',
method: 'GET',
}).then(jsonOrThrow).then((json) => {
json = json || {};
const incidents = json.incidents || [];
ele.flaky = json.flaky || false;
return incidents.map((i) => html`<incident-sk .state=${i} minimized></incident-sk>`);
}).catch(errorMessage);
}
const template = (ele) => html`
<h2 class=${classOfH2(ele)}>${ele._state.params.alertname} ${abbr(ele._state)} ${ele._displayFlakiness(ele._flaky)}</h2>
<section class=detail>
${actionButtons(ele)}
<table class=timing>
<tr><th>Started</th><td title=${new Date(ele._state.start * 1000).toLocaleString()}>${diffDate(ele._state.start * 1000)}</td></tr>
${lastSeen(ele)}
${duration(ele)}
</table>
<table class=params>
${table(ele._state.params)}
</table>
${notes(ele)}
<section class=addNote>
<textarea rows=2 cols=80></textarea>
<button @click=${ele._addNote}>Submit</button>
</section>
<section class=matchingSilences>
<span class=matchingSilencesHeaders>
<h3>Matching Silences</h3>
<checkbox-sk ?checked=${ele._displaySilencesWithComments} @click=${ele._toggleSilencesWithComments} label="Show only silences with comments">
</checkbox-sk>
</span>
${matchingSilences(ele)}
</section>
<section class=history>
<h3>History</h3>
${until(history(ele), html`<div class=loading>Loading...</div>`)}
</section>
</section>
`;
define('incident-sk', class extends HTMLElement {
constructor() {
super();
this._silences = [];
this._displaySilencesWithComments = false;
this._flaky = false;
}
/** @prop state {Object} An Incident. */
get state() { return this._state; }
set state(val) {
this._state = val;
this._render();
}
/** @prop silences {string} The list of active silences. */
get silences() { return this._silences; }
set silences(val) {
this._render();
this._silences = val;
}
/** @prop flaky {bool} Whether this incident has been flaky. */
get flaky() { return this._flaky; }
set flaky(val) {
// No need to render again if value is same as old value.
if (val !== this._flaky) {
this._flaky = val;
this._render();
}
}
_toggleSilencesWithComments(e) {
// This prevents a double event from happening.
e.preventDefault();
this._displaySilencesWithComments = !this._displaySilencesWithComments;
this._render();
}
_displayFlakiness(flaky) {
if (flaky) {
return html`<span class='flaky' title='This alert is possibly flaky'>[Possibly Flaky]</span>`;
}
return '';
}
_take() {
const detail = {
key: this._state.key,
};
this.dispatchEvent(new CustomEvent('take', { detail: detail, bubbles: true }));
}
_assignToOwner() {
const detail = {
key: this._state.key,
};
this.dispatchEvent(new CustomEvent('assign-to-owner', { detail: detail, bubbles: true }));
}
_assign() {
const detail = {
key: this._state.key,
};
this.dispatchEvent(new CustomEvent('assign', { detail: detail, bubbles: true }));
}
_deleteNote(e, index) {
const detail = {
key: this._state.key,
index: index,
};
this.dispatchEvent(new CustomEvent('del-note', { detail: detail, bubbles: true }));
}
_addNote() {
const textarea = $$('textarea', this);
const detail = {
key: this._state.key,
text: textarea.value,
};
this.dispatchEvent(new CustomEvent('add-note', { detail: detail, bubbles: true }));
textarea.value = '';
}
_render() {
if (!this._state) {
return;
}
render(template(this), this, { eventContext: this });
}
});