| /** |
| * @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 minimized 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/alarm-off-icon-sk'; |
| import 'elements-sk/icon/delete-icon-sk'; |
| import 'elements-sk/icon/thumbs-up-down-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, TemplateResult } from 'lit-html'; |
| import { until } from 'lit-html/directives/until'; |
| import { jsonOrThrow } from 'common-sk/modules/jsonOrThrow'; |
| import { abbr, linkify, displayNotes } from '../am'; |
| import * as paramset from '../paramset'; |
| import { |
| Silence, Incident, Params, RecentIncidentsResponse, Note, |
| } from '../json'; |
| |
| const MAX_MATCHING_SILENCES_TO_DISPLAY = 50; |
| |
| class State { |
| key: string = ''; |
| |
| id: string = ''; |
| |
| params: Params = {}; |
| |
| start: number = 0; |
| |
| last_seen: number = 0; |
| |
| active: boolean = false; |
| |
| notes: Note[] = []; |
| } |
| |
| |
| export class IncidentSk extends HTMLElement { |
| private silences: Silence[] = []; |
| |
| private displaySilencesWithComments: boolean = false; |
| |
| private flaky: boolean = false; |
| |
| private recently_expired_silence: boolean = false; |
| |
| private state: State = { |
| key: '', |
| id: '', |
| params: {}, |
| start: 0, |
| last_seen: 0, |
| active: false, |
| notes: [], |
| }; |
| |
| private static template = (ele: IncidentSk) => html` |
| <h2 class=${ele.classOfH2()}>${ele.state.params.alertname} ${abbr(ele.state.params.abbr)} ${ele.displayRecentlyExpired(ele.recently_expired_silence)} ${ele.displayFlakiness(ele.flaky)}</h2> |
| <section class=detail> |
| ${ele.actionButtons()} |
| <table class=timing> |
| <tr><th>Started</th><td title=${new Date(ele.state.start * 1000).toLocaleString()}>${diffDate(ele.state.start * 1000)}</td></tr> |
| ${ele.lastSeen()} |
| ${ele.duration()} |
| </table> |
| <table class=params> |
| ${ele.table()} |
| </table> |
| ${displayNotes(ele.state.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> |
| ${ele.matchingSilences()} |
| </section> |
| <section class=history> |
| <h3>History</h3> |
| ${until(ele.history(), html`<div class=loading>Loading...</div>`)} |
| </section> |
| </section> |
| `; |
| |
| public deleteNote(e: Event, index: number): void { |
| const detail = { |
| key: this.state.key, |
| index: index, |
| }; |
| this.dispatchEvent(new CustomEvent('del-note', { detail: detail, bubbles: true })); |
| } |
| |
| /** @prop incident_state An Incident. */ |
| get incident_state(): State { return this.state; } |
| |
| set incident_state(val: State) { |
| this.state = val; |
| this._render(); |
| } |
| |
| /** @prop incident_silences The list of active silences. */ |
| get incident_silences(): Silence[] { return this.silences; } |
| |
| set incident_silences(val: Silence[]) { |
| this._render(); |
| this.silences = val; |
| } |
| |
| /** @prop recently_expired_silence Whether silence recently expired. */ |
| get incident_has_recently_expired_silence(): boolean { return this.recently_expired_silence; } |
| |
| set incident_has_recently_expired_silence(val: boolean) { |
| // No need to render again if value is same as old value. |
| if (val !== this.recently_expired_silence) { |
| this.recently_expired_silence = val; |
| this._render(); |
| } |
| } |
| |
| /** @prop flaky Whether this incident has been flaky. */ |
| get incident_flaky(): boolean { return this.flaky; } |
| |
| set incident_flaky(val: boolean) { |
| // No need to render again if value is same as old value. |
| if (val !== this.flaky) { |
| this.flaky = val; |
| this._render(); |
| } |
| } |
| |
| private classOfH2(): string { |
| if (!this.state.active) { |
| return 'inactive'; |
| } |
| if (this.state.params.assigned_to) { |
| return 'assigned'; |
| } |
| return ''; |
| } |
| |
| private table(): TemplateResult[] { |
| const params = this.state.params; |
| const keys = Object.keys(params); |
| keys.sort(); |
| return keys.filter((k) => !k.startsWith('__')).map((k) => html`<tr><th>${k}</th><td>${linkify(params[k])}</td></tr>`); |
| } |
| |
| private actionButtons(): TemplateResult { |
| if (this.state.active) { |
| let assignToOwnerButton = html``; |
| if (this.state.params.owner) { |
| assignToOwnerButton = html`<button @click=${this.assignToOwner}>Assign to Owner</button>`; |
| } |
| return html`<section class=assign> |
| <button @click=${this.take}>Take</button> |
| ${assignToOwnerButton} |
| <button @click=${this.assign}>Assign</button> |
| </section>`; |
| } |
| return html``; |
| } |
| |
| private matchingSilences(): TemplateResult[] { |
| if (this.hasAttribute('minimized')) { |
| return []; |
| } |
| // Filter out silences whose paramsets do not match and |
| // which have no notes if displaySilencesWithComments is true. |
| const filteredSilences = this.silences.filter((silence: Silence) => paramset.match(silence.param_set, this.state.params) |
| && !(this.displaySilencesWithComments && this.doesSilenceHaveNoNotes(silence))); |
| const ret = filteredSilences.slice(0, MAX_MATCHING_SILENCES_TO_DISPLAY).map((silence: Silence) => html`<silence-sk .silence_state=${silence} collapsable collapsed></silence-sk>`); |
| if (!ret.length) { |
| ret.push(html`<div class=nosilences>None</div>`); |
| } |
| return ret; |
| } |
| |
| private doesSilenceHaveNoNotes(silence: Silence): boolean { |
| return !silence.notes || (silence.notes.length === 1 && silence.notes[0].text === ''); |
| } |
| |
| private lastSeen(): TemplateResult { |
| if (this.state.active) { |
| return html``; |
| } |
| return html`<tr><th>Last Seen</th><td title=${new Date(this.state.last_seen * 1000).toLocaleString()}>${diffDate(this.state.last_seen * 1000)}</td></tr>`; |
| } |
| |
| private duration(): TemplateResult { |
| if (this.state.active) { |
| return html``; |
| } |
| return html`<tr><th>Duration</th><td>${strDuration(this.state.last_seen - this.state.start)}</td></tr>`; |
| } |
| |
| private history(): Promise<any> { |
| if (this.hasAttribute('minimized') || this.state.id === '' || this.state.key === '') { |
| return Promise.resolve(); |
| } |
| return fetch(`/_/recent_incidents?id=${this.state.id}&key=${this.state.key}`, { |
| headers: { |
| 'content-type': 'application/json', |
| }, |
| credentials: 'include', |
| method: 'GET', |
| }).then(jsonOrThrow).then((json: RecentIncidentsResponse) => { |
| const incidents = json.incidents || []; |
| this.incident_flaky = json.flaky; |
| this.incident_has_recently_expired_silence = json.recently_expired_silence; |
| return incidents.map((i: Incident) => html`<incident-sk .incident_state=${i} minimized></incident-sk>`); |
| }).catch(errorMessage); |
| } |
| |
| private toggleSilencesWithComments(e: Event): void { |
| // This prevents a double event from happening. |
| e.preventDefault(); |
| this.displaySilencesWithComments = !this.displaySilencesWithComments; |
| this._render(); |
| } |
| |
| private displayRecentlyExpired(recentlyExpiredSilence: boolean): TemplateResult { |
| if (recentlyExpiredSilence) { |
| return html`<alarm-off-icon-sk title='This alert has a recently expired silence'></alarm-off-icon-sk>`; |
| } |
| return html``; |
| } |
| |
| private displayFlakiness(flaky: boolean): TemplateResult { |
| if (flaky) { |
| return html`<thumbs-up-down-icon-sk title='This alert is possibly flaky'></thumbs-up-down-icon-sk>`; |
| } |
| return html``; |
| } |
| |
| private take(): void { |
| const detail = { |
| key: this.state.key, |
| }; |
| this.dispatchEvent(new CustomEvent('take', { detail: detail, bubbles: true })); |
| } |
| |
| private assignToOwner(): void { |
| const detail = { |
| key: this.state.key, |
| }; |
| this.dispatchEvent(new CustomEvent('assign-to-owner', { detail: detail, bubbles: true })); |
| } |
| |
| private assign(): void { |
| const detail = { |
| key: this.state.key, |
| }; |
| this.dispatchEvent(new CustomEvent('assign', { detail: 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-note', { detail: detail, bubbles: true })); |
| textarea.value = ''; |
| } |
| |
| private _render(): void { |
| if (!this.state) { |
| return; |
| } |
| render(IncidentSk.template(this), this, { eventContext: this }); |
| } |
| } |
| |
| define('incident-sk', IncidentSk); |