text: /**
   * @module alert-manager-sk
   * @description <h2><code>alert-manager-sk</code></h2>
   *
   *   The main application element for am.skia.org.
   *
   */
  import { define } from 'elements-sk/define';
  import 'elements-sk/checkbox-sk';
  import 'elements-sk/error-toast-sk';
  import 'elements-sk/icon/alarm-off-icon-sk';
  import 'elements-sk/icon/comment-icon-sk';
  import 'elements-sk/icon/notifications-icon-sk';
  import 'elements-sk/icon/person-icon-sk';
  import 'elements-sk/spinner-sk';
  import 'elements-sk/styles/buttons';
  import 'elements-sk/tabs-panel-sk';
  import 'elements-sk/tabs-sk';
  import 'elements-sk/toast-sk';
  
  import '../incident-sk';
  import '../bot-chooser-sk';
  import '../email-chooser-sk';
  import '../silence-sk';
  
  import { $$ } from 'common-sk/modules/dom';
  import { errorMessage } from 'elements-sk/errorMessage';
  import { html, render } from 'lit-html';
  import { jsonOrThrow } from 'common-sk/modules/jsonOrThrow';
  import { stateReflector } from 'common-sk/modules/stateReflector';
  import { Login } from '../../../infra-sk/modules/login';
  
  import * as paramset from '../paramset';
  import { displaySilence, expiresIn } from '../am';
  
  // Legal states.
  const START = 'start';
  const INCIDENT = 'incident';
  const EDIT_SILENCE = 'edit_silence';
  const VIEW_STATS = 'view_stats';
  
  const MAX_SILENCES_TO_DISPLAY_IN_TAB = 50;
  
  const BOT_CENTRIC_PARAMS = ['alertname', 'bot'];
  
  function classOfH2(ele, incident) {
    const ret = [];
    if (!incident.active) {
      ret.push('inactive');
    } else if (incident.params.__silence_state === 'silenced') {
      ret.push('silenced');
    } else if (incident.params.assigned_to) {
      ret.push('assigned');
    }
    if (ele._selected && ele._selected.key === incident.key) {
      ret.push('selected');
    }
    return ret.join(' ');
  }
  
  function classOfSilenceH2(ele, silence) {
    const ret = [];
    if (!silence.active) {
      ret.push('inactive');
    }
    if (ele._selected && ele._selected.key === silence.key) {
      ret.push('selected');
    }
    return ret.join(' ');
  }
  
  function editIncident(ele) {
    if (ele._selected) {
      return html`<incident-sk .silences=${ele._silences} .state=${ele._selected}
        ></incident-sk>`;
    }
    return '';
  }
  
  function editSilence(ele) {
    return html`<silence-sk .state=${ele._current_silence} .incidents=${ele._incidents}
      ></silence-sk>`;
  }
  
  function viewStats(ele) {
    return ele._incident_stats.map((i, index) => html`<incident-sk .state=${i} ?minimized params=${index === 0}></incident-sk>`);
  }
  
  function rightHandSide(ele) {
    switch (ele._rhs_state) {
      case START:
        return '';
      case INCIDENT:
        return editIncident(ele);
      case EDIT_SILENCE:
        return editSilence(ele);
      case VIEW_STATS:
        return viewStats(ele);
      default:
        return '';
    }
  }
  
  function hasNotes(o) {
    return (o.notes && o.notes.length > 0) ? '' : 'invisible';
  }
  
  function hasRecentlyExpiredSilence(incident, idsToExpiredRecently) {
    return (idsToExpiredRecently[incident.id]) ? '' : 'invisible';
  }
  
  function displayIncident(incident) {
    const ret = [incident.params.alertname];
    const abbr = incident.params.abbr;
    if (abbr) {
      ret.push(` - ${abbr}`);
    }
    let s = ret.join(' ');
    if (s.length > 33) {
      s = `${s.slice(0, 30)}...`;
    }
    return s;
  }
  
  function trooper(ele) {
    if (ele._trooper === ele._user) {
      return html`<notifications-icon-sk title='You are the trooper, awesome!'></notifications-icon-sk>`;
    }
    return '';
  }
  
  function assignedTo(incident, ele) {
    if (incident.params.assigned_to === ele._user) {
      return html`<person-icon-sk title='This item is assigned to you.'></person-icon-sk>`;
    } if (incident.params.assigned_to) {
      return html`<span class='assigned-circle' title='This item is assigned to ${incident.params.assigned_to}.'>${incident.params.assigned_to[0].toUpperCase()}</span>`;
    }
    return '';
  }
  
  function populateBotsToIncidents(ele, incidents) {
    // Reset bots_to_incidents and populate it from scratch.
    ele._bots_to_incidents = {};
    for (let i = 0; i < incidents.length; i++) {
      const incident = incidents[i];
      if (incident.params && incident.params['bot']) {
        // Only consider active bot incidents that are not assigned or silenced.
        if (!incident.active || incident.params.__silence_state === 'silenced'
            || incident.params.assigned_to) {
          continue;
        }
        const botName = incident.params['bot'];
        if(ele._bots_to_incidents[botName]) {
          ele._bots_to_incidents[botName].push(incident);
        } else {
          ele._bots_to_incidents[botName] = [incident];
        }
      }
    }
  }
  
  function botCentricView(ele, incidents) {
    populateBotsToIncidents(ele, incidents);
    const botsHTML = [];
    for (const botName in ele._bots_to_incidents) {
      botsHTML.push(html`
        <h2 class="bot-centric">
          <span class=noselect>
            <checkbox-sk class=bot-alert-checkbox ?checked=${isBotChecked(ele, ele._bots_to_incidents[botName])} @change=${ele._check_selected} @click=${ele._clickHandler} id=${botName}></checkbox-sk>
            <span class=bot-alert>
              ${botName}
              <span class=bot-incident-list>
                ${incidentListForBot(ele, ele._bots_to_incidents[botName])}
              </span>
            </span>
          </span>
        </h2>
      `)
    }
    return botsHTML;
  }
  
  // Checks to see if all the incidents for the bot are checked.
  function isBotChecked(ele, incidents) {
    for (let i = 0; i < incidents.length; i++) {
      if (!ele._checked.has(incidents[i].key)) {
        return false;
      }
    }
    return true;
  }
  
  function incidentListForBot(ele, incidents) {
    const incidentsHTML = incidents.map((i) => html`<li @click=${() => ele._select(i)}>${i.params.alertname}</li>`);
    return html`<ul class=bot-incident-elem>${incidentsHTML}</ul>`;
  }
  
  function incidentList(ele, incidents, isBotCentricView) {
    if (isBotCentricView) {
      return botCentricView(ele, ele._incidents);
    } else {
      return incidents.map((i) => html`
        <h2 class=${classOfH2(ele, i)} @click=${() => ele._select(i)}>
        <span class=noselect>
          <checkbox-sk ?checked=${ele._checked.has(i.key)} @change=${ele._check_selected} @click=${ele._clickHandler} id=${i.key}></checkbox-sk>
          ${assignedTo(i, ele)}
          ${displayIncident(i)}
        </span>
        <span>
          <alarm-off-icon-sk title='This incident has a recently expired silence' class=${hasRecentlyExpiredSilence(i, ele._incidentsToRecentlyExpired)}></alarm-off-icon-sk>
          <comment-icon-sk title='This incident has notes.' class=${hasNotes(i)}></comment-icon-sk>
        </span>
        </h2>
      `);
    }
  }
  
  function statsList(ele) {
    return ele._stats.map((stat) => html`<h2 @click=${() => ele._statsClick(stat.incident)}>${displayIncident(stat.incident)} <span>${stat.num}</span></h2>`);
  }
  
  function numMatchSilence(ele, s) {
    if (!ele._incidents) {
      return '';
    }
    return ele._incidents.filter(
      (incident) => paramset.match(s.param_set, incident.params) && incident.active,
    ).length;
  }
  
  function clearSelections(ele) {
    return html`<button class=selection ?disabled=${ele._checked.size === 0} @click=${ele._clearSelections}>Clear selections</button>`;
  }
  
  function assignMultiple(ele) {
    return html`<button class=selection ?disabled=${ele._checked.size === 0} @click=${ele._assignMultiple}>Assign ${ele._checked.size} alerts</button>`;
  }
  
  function botCentricBtn(ele) {
    let buttonText;
    if (ele._isBotCentricView) {
      buttonText = 'Switch to Normal view'
    } else {
      buttonText = 'Switch to Bot-centric view'
    }
    return html`<button @click=${ele._flipBotCentricView}>${buttonText}</button>`;
  }
  
  const template = (ele) => html`
  <header>${trooper(ele)}</header>
  <section class=nav>
    <tabs-sk @tab-selected-sk=${ele._tabSwitch} selected=${ele._state.tab}>
      <button>Mine</button>
      <button>Alerts</button>
      <button>Silences</button>
      <button>Stats</button>
    </tabs-sk>
    <tabs-panel-sk>
      <section class=mine>
        <span class=selection-buttons>
          ${assignMultiple(ele)}
          ${clearSelections(ele)}
        </span>
        ${incidentList(ele, ele._incidents.filter((i) => i.active && i.params.__silence_state !== 'silenced' && (ele._user === ele._trooper || (i.params.assigned_to === ele._user) || (i.params.owner === ele._user && !i.params.assigned_to))), false)}
      </section>
      <section class=incidents>
        ${botCentricBtn(ele)}
        <span class=selection-buttons>
          ${assignMultiple(ele)}
          ${clearSelections(ele)}
        </span>
        ${incidentList(ele, ele._incidents, ele._isBotCentricView)}
      </section>
      <section class=silences>
        ${ele._silences.slice(0, MAX_SILENCES_TO_DISPLAY_IN_TAB).map((i) => html`
          <h2 class=${classOfSilenceH2(ele, i)} @click=${() => ele._silenceClick(i)}>
            <span>
              ${displaySilence(i)}
            </span>
            <span>
              <span title='Expires in'>${expiresIn(i)}</span>
              <comment-icon-sk title='This silence has notes.' class=${hasNotes(i)}></comment-icon-sk>
              <span title='The number of active alerts that match this silence.'>${numMatchSilence(ele, i)}</span>
            </span>
          </h2>`)}
      </section>
      <section class=stats>
        ${statsList(ele)}
      </section>
    </tabs-panel-sk>
  </section>
  <section class=edit>
    ${rightHandSide(ele)}
  </section>
  <footer>
    <spinner-sk id=busy></spinner-sk>
    <bot-chooser-sk id=bot-chooser></bot-chooser-sk>
    <email-chooser-sk id=email-chooser></email-chooser-sk>
    <error-toast-sk></error-toast-sk>
  <footer>
  `;
  
  function findParent(ele, tagName) {
    while (ele && (ele.tagName !== tagName)) {
      ele = ele.parentElement;
    }
    return ele;
  }
  
  define('alert-manager-sk', class extends HTMLElement {
    constructor() {
      super();
      this._incidents = []; // All active incidents.
      this._silences = []; // All active silences.
      this._stats = []; // Last requested stats.
      this._stats_range = '1w';
      this._incident_stats = []; // The incidents for a given stat.
      this._rhs_state = START; // One of START, INCIDENT, or EDIT_SILENCE.
      this._selected = null; // The selected incident, i.e. you clicked on the name.
      this._checked = new Set(); // Checked incidents, i.e. you clicked the checkbox.
      this._bots_to_incidents = {}; // Bot names to their incidents. Used in bot-centric view.
      this._isBotCentricView = false;  // Determines if bot-centric view is displayed on incidents tab.
      this._current_silence = null; // A silence under construction.
      // Params to ignore when constructing silences.
      this._ignored = ['__silence_state', 'description', 'id', 'swarming', 'assigned_to',
        'kubernetes_pod_name', 'instance', 'pod_template_hash', 'abbr_owner_regex',
        'controller_revision_hash'];
      this._shift_pressed_during_click = false; // If the shift key was held down during the mouse click.
      this._last_checked_incident = null; // Keeps track of the last checked incident. Used for multi-selecting incidents with shift.
      this._incidents_notified = {}; // Keeps track of all incidents that were notified via desktop notifications.
      this._incidentsToRecentlyExpired = {} // Map of incident IDs to whether their silences recently expired.
      this._user = 'barney@example.org';
      this._trooper = '';
      this._state = {
        tab: 0, // The selected tab.
        alert_id: '', // The selected alert (if any).
      };
      fetch('https://tree-status.skia.org/current-trooper', { mode: 'cors' }).then(jsonOrThrow).then((json) => {
        this._trooper = json.username;
        this._render();
      });
      Login.then((loginstatus) => {
        this._user = loginstatus.Email;
        this._render();
      });
    }
  
    connectedCallback() {
      this._requestDesktopNotificationPermission();
  
      this.addEventListener('save-silence', (e) => this._saveSilence(e.detail.silence));
      this.addEventListener('archive-silence', (e) => this._archiveSilence(e.detail.silence));
      this.addEventListener('reactivate-silence', (e) => this._reactivateSilence(e.detail.silence));
      this.addEventListener('delete-silence', (e) => this._deleteSilence(e.detail.silence));
      this.addEventListener('add-silence-note', (e) => this._addSilenceNote(e));
      this.addEventListener('del-silence-note', (e) => this._delSilenceNote(e));
      this.addEventListener('add-silence-param', (e) => this._addSilenceParam(e.detail.silence));
      this.addEventListener('delete-silence-param', (e) => this._deleteSilenceParam(e.detail.silence));
      this.addEventListener('modify-silence-param', (e) => this._modifySilenceParam(e.detail.silence));
      this.addEventListener('add-note', (e) => this._addNote(e));
      this.addEventListener('del-note', (e) => this._delNote(e));
      this.addEventListener('take', (e) => this._take(e));
      this.addEventListener('bot-chooser', (e) => this._botChooser(e));
      this.addEventListener('assign', (e) => this._assign(e));
      this.addEventListener('assign-to-owner', (e) => this._assignToOwner(e));
  
      this._stateHasChanged = stateReflector(
        () => this._state,
        (state) => {
          this._state = state;
          this._render();
        },
      );
  
      this._render();
      this._busy = $$('#busy', this);
      this._favicon = $$('#favicon');
  
      this._busy.active = true;
      this._poll(true);
    }
  
    _poll(stopSpinner) {
      const incidents = fetch('/_/incidents', {
        credentials: 'include',
      }).then(jsonOrThrow).then((json) => {
        this._incidents = json.incidents;
        // If alert_id is specified and it is in supported rhs_states then display
        // an incident.
        if ((this._rhs_state == START || this._rhs_state == INCIDENT) &&
            this._state.alert_id) {
          for (let i = 0; i < this._incidents.length; i++) {
            if (this._incidents[i].id === this._state.alert_id) {
              this._select(this._incidents[i]);
              break;
            }
          }
        }
        this._incidents = json.incidents;
        this._incidentsToRecentlyExpired = json.ids_to_recently_expired_silences;
      });
  
      const silences = fetch('/_/silences', {
        credentials: 'include',
      }).then(jsonOrThrow).then((json) => {
        this._silences = json;
      });
  
      const emails = fetch('/_/emails', {
        credentials: 'include',
      }).then(jsonOrThrow).then((json) => {
        this._emails = json;
      });
  
      Promise.all([incidents, silences, emails]).then(() => { this._render(); }).catch((msg) => {
        if (msg.resp) {
          msg.resp.text().then(errorMessage);
        } else {
          errorMessage(msg);
        }
      }).finally(() => {
        if (stopSpinner) {
          this._busy.active = false;
        }
        window.setTimeout(() => this._poll(), 10000);
      });
    }
  
    _tabSwitch(e) {
      this._state.tab = e.detail.index;
      // Unset alert_id when switching tabs.
      this._state.alert_id = '';
      this._stateHasChanged();
  
      // If tab is stats then load stats.
      if (e.detail.index === 3) {
        this._getStats();
      }
      // If tab is silences then display empty silence to populate from scratch.
      // This will go away if any existing silence is clicked on.
      if (e.detail.index === 2) {
        fetch('/_/new_silence', {
          credentials: 'include',
        }).then(jsonOrThrow).then((json) => {
          this._selected = null;
          this._current_silence = json;
          this._rhs_state = EDIT_SILENCE;
          this._render();
        }).catch(errorMessage);
      } else {
        this._rhs_state = START;
        this._render();
      }
    }
  
    _clickHandler(e) {
      this._shift_pressed_during_click = e.shiftKey;
      e.stopPropagation();
    }
  
    _silenceClick(silence) {
      this._current_silence = JSON.parse(JSON.stringify(silence));
      this._selected = silence;
      this._rhs_state = EDIT_SILENCE;
      this._render();
    }
  
    _statsClick(incident) {
      this._selected = incident;
      this._incidentStats();
      this._rhs_state = VIEW_STATS;
    }
  
    // Update the paramset for a silence as Incidents are checked and unchecked.
    // TODO(jcgregorio) Remove this once checkbox-sk is fixed.
    _check_selected_impl(key, isChecked) {
      if (isChecked) {
        this._last_checked_incident = key;
        this._checked.add(key);
        this._incidents.forEach((i) => {
          if (i.key === key) {
            paramset.add(this._current_silence.param_set, i.params, this._ignored);
          }
        });
      } else {
        this._last_checked_incident = null;
        this._checked.delete(key);
        this._current_silence.param_set = {};
        this._incidents.forEach((i) => {
          if (this._checked.has(i.key)) {
            paramset.add(this._current_silence.param_set, i.params, this._ignored);
          }
        });
      }
  
      if (this._isBotCentricView) {
        this._make_bot_centric_param_set(this._current_silence.param_set);
      }
      this._rhs_state = EDIT_SILENCE;
      this._render();
    }
  
    // Goes through the paramset and leaves only silence keys that are useful
    // in bot-centric view like 'alertname' and 'bot'.
    _make_bot_centric_param_set(target_paramset) {
      for (const key in target_paramset) {
        if (BOT_CENTRIC_PARAMS.indexOf(key) === -1) {
          delete target_paramset[key];
        }
      }
    }
  
    _check_selected(e) {
      const checkbox = findParent(e.target, 'CHECKBOX-SK');
      const incidents_to_check = [];
      if (this._isBotCentricView && this._bots_to_incidents &&
          this._bots_to_incidents[checkbox.id]) {
        this._bots_to_incidents[checkbox.id].forEach((i) => {
          incidents_to_check.push(i.key);
        });
      } else {
          incidents_to_check.push(checkbox.id);
      }
      const checkSelectedImplFunc = () => {
        incidents_to_check.forEach((id) => {
          this._check_selected_impl(id, checkbox._input.checked);
        });
      };
  
      if (!this._checked.size) {
        // Request a new silence.
        fetch('/_/new_silence', {
          credentials: 'include',
        }).then(jsonOrThrow).then((json) => {
          this._selected = null;
          this._current_silence = json;
          checkSelectedImplFunc();
        }).catch(errorMessage);
      } else if (this._shift_pressed_during_click && this._last_checked_incident) {
        let foundStart = false;
        let foundEnd = false;
        // Find all incidents included in the range during shift click.
        const incidents_included_in_range = [];
  
        // The incidents we go through for shift click selections will be
        // different for bot-centric vs normal view.
        const incidents = this._isBotCentricView
            ? [].concat(...Object.values(this._bots_to_incidents))
            : this._incidents;
  
        incidents.some((i) => {
          if (i.key === this._last_checked_incident ||
                  incidents_to_check.includes(i.key)) {
            if (!foundStart) {
              // This is the 1st time we have entered this block. This means we
              // found the first incident.
              foundStart = true;
            } else {
              // This is the 2nd time we have entered this block. This means we
              // found the last incident.
              foundEnd = true;
            }
          }
          if (foundStart) {
            incidents_included_in_range.push(i.key);
          }
          return foundEnd;
        });
  
        if (foundStart && foundEnd) {
          incidents_included_in_range.forEach((key) => {
            this._check_selected_impl(key, true);
          });
        } else {
          // Could not find start and/or end incident. Only check the last
          // clicked.
          checkSelectedImplFunc();
        }
      } else {
        checkSelectedImplFunc();
      }
    }
  
    _select(incident) {
      this._state.alert_id = incident.id;
      this._stateHasChanged();
  
      this._rhs_state = INCIDENT;
      this._checked = new Set();
      this._selected = incident;
      this._current_silence = null;
      this._render();
    }
  
    _addNote(e) {
      this._doImpl('/_/add_note', e.detail);
    }
  
    _delNote(e) {
      this._doImpl('/_/del_note', e.detail);
    }
  
    _addSilenceParam(silence) {
      // Don't save silences that are just being created when you add a param.
      if (!silence.key) {
        this._current_silence = silence;
        this._render();
        return;
      }
      this._checked = new Set();
      this._doImpl('/_/save_silence', silence, (json) => this._silenceAction(json, false));
    }
  
    _deleteSilenceParam(silence) {
      // Don't save silences that are just being created when you delete a param.
      if (!silence.key) {
        this._current_silence = silence;
        this._render();
        return;
      }
      this._checked = new Set();
      this._doImpl('/_/save_silence', silence, (json) => this._silenceAction(json, false));
    }
  
    _modifySilenceParam(silence) {
      // Don't save silences that are just being created when you modify a param.
      if (!silence.key) {
        this._current_silence = silence;
        this._render();
        return;
      }
      this._checked = new Set();
      this._doImpl('/_/save_silence', silence, (json) => this._silenceAction(json, false));
    }
  
    _saveSilence(silence) {
      this._checked = new Set();
      this._doImpl('/_/save_silence', silence, (json) => this._silenceAction(json, true));
    }
  
    _archiveSilence(silence) {
      this._doImpl('/_/archive_silence', silence, (json) => this._silenceAction(json, true));
    }
  
    _reactivateSilence(silence) {
      this._doImpl('/_/reactivate_silence', silence, (json) => this._silenceAction(json, false));
    }
  
    _deleteSilence(silence) {
      this._doImpl('/_/del_silence', silence, (json) => {
        for (let i = 0; i < this._silences.length; i++) {
          if (this._silences[i].key === json.key) {
            this._silences.splice(i, 1);
            this._rhs_state = START;
            break;
          }
        }
      });
    }
  
    _addSilenceNote(e) {
      this._doImpl('/_/add_silence_note', e.detail, (json) => this._silenceAction(json, false));
    }
  
    _delSilenceNote(e) {
      this._doImpl('/_/del_silence_note', e.detail, (json) => this._silenceAction(json, false));
    }
  
    _botChooser(e) {
      populateBotsToIncidents(this, this._incidents)
      $$('#bot-chooser', this).open(this._bots_to_incidents, this._current_silence.param_set['bot']).then((bot) => {
        if (!bot) {
          return;
        }
        const bot_incidents = this._bots_to_incidents[bot];
        bot_incidents.forEach((i) => {
          const bot_centric_params = {}
          BOT_CENTRIC_PARAMS.forEach((p) => {
            bot_centric_params[p] = i.params[p];
          });
          paramset.add(this._current_silence.param_set, bot_centric_params, this._ignored)
        });
        this._modifySilenceParam(this._current_silence);
      });
    }
  
    _assign(e) {
      const owner = this._selected && this._selected.params.owner;
      $$('#email-chooser', this).open(this._emails, owner).then((email) => {
        const detail = {
          key: e.detail.key,
          email: email,
        };
        this._doImpl('/_/assign', detail);
      });
    }
  
    _flipBotCentricView() {
      this._isBotCentricView = !this._isBotCentricView;
      this._render();
    }
  
    _assignMultiple() {
      const owner = (this._selected && this._selected.params.owner) || '';
      $$('#email-chooser', this).open(this._emails, owner).then((email) => {
        const detail = {
          keys: Array.from(this._checked),
          email: email,
        };
        this._doImpl('/_/assign_multiple', detail, (json) => {
          this._incidents = json;
          this._checked = new Set();
          this._render();
        });
      });
    }
  
    _clearSelections() {
      this._checked = new Set();
      this._render();
    }
  
    _assignToOwner(e) {
      const owner = this._selected && this._selected.params.owner;
      const detail = {
        key: e.detail.key,
        email: owner,
      };
      this._doImpl('/_/assign', detail);
    }
  
    _take(e) {
      this._doImpl('/_/take', e.detail);
      // Do not do desktop notification on takes, it is redundant.
      this._incidents_notified[e.detail.key] = true;
    }
  
    _getStats() {
      const detail = {
        range: this._stats_range,
      };
      this._doImpl('/_/stats', detail, (json) => this._statsAction(json));
    }
  
    _incidentStats() {
      const detail = {
        incident: this._selected,
        range: this._stats_range,
      };
      this._doImpl('/_/incidents_in_range', detail, (json) => this._incidentStatsAction(json));
    }
  
    // Actions to take after updating incident stats.
    _incidentStatsAction(json) {
      this._incident_stats = json;
    }
  
    // Actions to take after updating Stats.
    _statsAction(json) {
      this._stats = json;
    }
  
    // Actions to take after updating an Incident.
    _incidentAction(json) {
      const incidents = this._incidents;
      for (let i = 0; i < incidents.length; i++) {
        if (incidents[i].key === json.key) {
          incidents[i] = json;
          break;
        }
      }
      this._selected = json;
    }
  
    // Actions to take after updating a Silence.
    _silenceAction(json, clear) {
      let found = false;
      this._current_silence = json;
      for (let i = 0; i < this._silences.length; i++) {
        if (this._silences[i].key === json.key) {
          this._silences[i] = json;
          found = true;
          break;
        }
      }
      if (!found) {
        this._silences.push(json);
      }
      if (clear) {
        this._rhs_state = START;
      }
    }
  
    // Common work done for all fetch requests.
    _doImpl(url, detail, action = (json) => this._incidentAction(json)) {
      this._busy.active = true;
      fetch(url, {
        body: JSON.stringify(detail),
        headers: {
          'content-type': 'application/json',
        },
        credentials: 'include',
        method: 'POST',
      }).then(jsonOrThrow).then((json) => {
        action(json);
        this._render();
        this._busy.active = false;
      }).catch((msg) => {
        this._busy.active = false;
        msg.resp.text().then(errorMessage);
      });
    }
  
    // Fix-up all the incidents and silences, including re-sorting them.
    _rationalize() {
      this._incidents.forEach((incident) => {
        const silenced = this._silences.reduce((isSilenced, silence) => isSilenced
                || (silence.active && paramset.match(silence.param_set, incident.params)), false);
        incident.params.__silence_state = silenced ? 'silenced' : 'active';
      });
  
      // Sort the incidents, using the following 'sortby' list as tiebreakers.
      const sortby = ['__silence_state', 'assigned_to', 'alertname', 'abbr', 'id'];
      this._incidents.sort((a, b) => {
        // Sort active before inactive.
        if (a.active !== b.active) {
          return a.active ? -1 : 1;
        }
        // Inactive incidents are then sorted by 'lastseen' timestamp.
        if (!a.active) {
          const delta = b.last_seen - a.last_seen;
          if (delta) {
            return delta;
          }
        }
        for (let i = 0; i < sortby.length; i++) {
          const key = sortby[i];
          const left = a.params[key] || '';
          const right = b.params[key] || '';
          const cmp = left.localeCompare(right);
          if (cmp) {
            return cmp;
          }
        }
        return 0;
      });
      this._silences.sort((a, b) => {
        // Sort active before inactive.
        if (a.active !== b.active) {
          return a.active ? -1 : 1;
        }
        return b.updated - a.updated;
      });
    }
  
    _needsTriaging(incident, isTrooper) {
      if (incident.active
        && (incident.params.__silence_state !== 'silenced')
        && (
          (isTrooper && !incident.params.assigned_to)
          || (incident.params.assigned_to === this._user)
          || (incident.params.owner === this._user
              && !incident.params.assigned_to)
        )
      ) {
        return true;
      }
      return false;
    }
  
    _requestDesktopNotificationPermission() {
      if (Notification && Notification.permission === 'default') {
        Notification.requestPermission((permission) => {
          if (!('permission' in Notification)) {
            Notification.permission = permission;
          }
        });
      }
    }
  
    _sendDesktopNotification(unNotifiedIncidents) {
      if (unNotifiedIncidents.length === 0) {
        // Do nothing.
        return;
      }
      let text = '';
      if (unNotifiedIncidents.length === 1) {
        text = `${unNotifiedIncidents[0].params.alertname}\n\n${unNotifiedIncidents[0].params.description}`;
      } else {
        text = `There are ${unNotifiedIncidents.length} alerts assigned to you`;
      }
      const notification = new Notification('am.skia.org notification', {
        icon: '/static/icon-active.png',
        body: text,
        // 'tag' handles multi-tab scenarios. When multiple tabs are open then
        // only one notification is sent for the same alert.
        tag: `alertManagerNotification${text}`,
      });
      // onclick move focus to the am.skia.org tab and close the notification.
      const that = this;
      notification.onclick = function() {
        window.parent.focus();
        window.focus(); // Supports older browsers.
        that._select(unNotifiedIncidents[0]); // Display the 1st incident.
        this.close();
      };
      setTimeout(notification.close.bind(notification), 10000);
    }
  
    _render() {
      this._rationalize();
      render(template(this), this, { eventContext: this });
      // Update the icon.
      const isTrooper = this._user === this._trooper;
      const numActive = this._incidents.reduce((n, incident) => n += this._needsTriaging(incident, isTrooper) ? 1 : 0, 0);
  
      // Show desktop notifications only if permission was granted and only if
      // silences have been successfully fetched. If silences have not been
      // fetched yet then we might end up notifying on silenced incidents.
      if (Notification.permission === 'granted' && this._silences.length !== 0) {
        const unNotifiedIncidents = this._incidents.filter((i) => !this._incidents_notified[i.key] && this._needsTriaging(i, isTrooper));
        this._sendDesktopNotification(unNotifiedIncidents);
        unNotifiedIncidents.forEach((i) => this._incidents_notified[i.key] = true);
      }
  
      document.title = `${numActive} - AlertManager`;
      if (!this._favicon) {
        return;
      }
      if (numActive > 0) {
        this._favicon.href = '/static/icon-active.png';
      } else {
        this._favicon.href = '/static/icon.png';
      }
    }
  });
  