Perf - Migrate triage-page-sk away from Polymer.

Bug: skia:9219
Change-Id: If8c8837bc278c744024f9ff1884376df178bb8fb
Reviewed-on: https://skia-review.googlesource.com/c/buildbot/+/233458
Commit-Queue: Joe Gregorio <jcgregorio@google.com>
Reviewed-by: Kevin Lubick <kjlubick@google.com>
diff --git a/perf/elements.html b/perf/elements.html
index 6d5c63e..d1f72c0 100644
--- a/perf/elements.html
+++ b/perf/elements.html
@@ -15,6 +15,5 @@
 
     <link href="/res/common/imp/commit-panel.html" rel="import" />
 
-    <link href="/res/imp/triage-page.html" rel="import" />
     <link href="/res/imp/cluster-lastn-page.html" rel="import" />
 </head>
diff --git a/perf/modules/triage-page-sk/index.js b/perf/modules/triage-page-sk/index.js
new file mode 100644
index 0000000..0904e2e
--- /dev/null
+++ b/perf/modules/triage-page-sk/index.js
@@ -0,0 +1,2 @@
+import './triage-page-sk.js'
+import './triage-page-sk.scss'
diff --git a/perf/modules/triage-page-sk/triage-page-sk-demo.html b/perf/modules/triage-page-sk/triage-page-sk-demo.html
new file mode 100644
index 0000000..5a54a38
--- /dev/null
+++ b/perf/modules/triage-page-sk/triage-page-sk-demo.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <title>triage-page-sk</title>
+  <meta charset="utf-8" />
+  <meta http-equiv="X-UA-Compatible" content="IE=edge">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+</head>
+<body>
+  <h1>triage-page-sk</h1>
+  <triage-page-sk></triage-page-sk>
+
+  <h2>Events</h2>
+  <pre id=events></pre>
+  <script type="text/javascript" charset="utf-8">
+    document.querySelector('triage-page-sk').addEventListener('some-event-name', (e) => {
+      document.querySelector('#events').textContent = JSON.stringify(e.detail, null, '  ');
+    });
+  </script>
+</body>
+</html>
diff --git a/perf/modules/triage-page-sk/triage-page-sk-demo.js b/perf/modules/triage-page-sk/triage-page-sk-demo.js
new file mode 100644
index 0000000..1c51332
--- /dev/null
+++ b/perf/modules/triage-page-sk/triage-page-sk-demo.js
@@ -0,0 +1 @@
+import './index.js'
diff --git a/perf/modules/triage-page-sk/triage-page-sk.js b/perf/modules/triage-page-sk/triage-page-sk.js
new file mode 100644
index 0000000..55b3b6f
--- /dev/null
+++ b/perf/modules/triage-page-sk/triage-page-sk.js
@@ -0,0 +1,451 @@
+/**
+ * @module module/triage-page-sk
+ * @description <h2><code>triage-page-sk</code></h2>
+ *
+ * Allows triaging clusters.
+ *
+ */
+import dialogPolyfill from 'dialog-polyfill'
+import { ElementSk } from '../../../infra-sk/modules/ElementSk'
+import { equals, deepCopy } from 'common-sk/modules/object'
+import { errorMessage } from 'elements-sk/errorMessage.js'
+import { fromObject } from 'common-sk/modules/query'
+import { html } from 'lit-html'
+import { jsonOrThrow } from 'common-sk/modules/jsonOrThrow'
+import { stateReflector } from 'common-sk/modules/stateReflector'
+
+import 'elements-sk/spinner-sk'
+import 'elements-sk/styles/buttons'
+import 'elements-sk/styles/select'
+
+import '../cluster-summary2-sk'
+import '../commit-detail-sk'
+import '../day-range-sk'
+import '../triage-status-sk'
+
+const _allFilters = (ele) => ele._all_filter_options.map(
+  (o) => html`
+    <option
+      ?selected=${ele._state.alert_filter === o.value}
+      value=${o.value}
+      title=${o.title}
+      >${o.display}
+    </option>`);
+
+const _statusItems = (ele) => ele._currentClusteringStatus.map((item) => html`
+  <table>
+    <tr>
+      <th>Alert</th>
+      <td><a href='/a/?${item.alert.id}'>${item.alert.display_name}</a></td>
+    </tr>
+    <tr>
+      <th>Commit</th>
+      <td><commit-detail-sk .cid=${item.commit}></commit-detail-sk></td>
+    </tr>
+    <tr>
+      <th>Step</th>
+      <td>${item.step}/${item.total}</td>
+    </tr>
+  </table>
+`);
+
+const _headers = (ele) => ele._reg.header.map((item) => {
+  let displayName = item.display_name;
+  if (!item.display_name) {
+    displayName = item.query.slice(0, 10);
+  }
+  // The colspan=2 is important since we will have two columns under each
+  // header, one for high and one for low.
+  return html`<th colspan=2><a href='/a/?${item.id}'>${displayName}</a></th>`;
+});
+
+const _subHeaders = (ele) => ele._reg.header.map((_, index) => {
+  const ret = [];
+  if (ele._stepDownAt(index)) {
+    ret.push(html`<th>Low</th>`);
+  }
+  if (ele._stepUpAt(index)) {
+    ret.push(html`<th>High</th>`);
+  }
+  // If we have only one of High or Low we stuff in an empty th to match
+  // colspan=2 above.
+  if (ele._notBoth(index)) {
+    ret.push(html`<th></th>`);
+  }
+  return ret;
+});
+
+function _full_summary(frame, summary) {
+  return {
+    frame: frame,
+    summary: summary,
+  }
+}
+
+const _lowCell = (ele, rowIndex, col, colIndex) => {
+  if (col && col.low) {
+    return html`<triage-status-sk
+                  .alert=${ele._alertAt(colIndex)}
+                  .cluster_type=low
+                  .full_summary=${_full_summary(col.frame, col.low)}
+                  .triage=${col.low_status}></triage-status-sk> `;
+  } else {
+    return html`<a
+                  title='No clusters found.'
+                  href='/g/c/${ele._hashFrom(rowIndex)}?query=${ele._encQueryFrom(colIndex)}'>∅</a> `;
+  }
+}
+
+const _highCell = (ele, rowIndex, col, colIndex) => {
+  if (col && col.high) {
+    return html`<triage-status-sk
+                  .alert=${ele._alertAt(colIndex)}
+                  .cluster_type=high
+                  .full_summary=${_full_summary(col.frame, col.high)}
+                  .triage=${col.high_status}></triage-status-sk> `;
+  } else {
+    return html`<a
+                  title='No clusters found.'
+                  href='/g/c/${ele._hashFrom(rowIndex)}?query=${ele._encQueryFrom(colIndex)}'>∅</a> `;
+  }
+}
+
+const _columns = (ele, row, rowIndex) => row.columns.map((col, colIndex) => {
+  const ret = [];
+
+  if (ele._stepDownAt(colIndex)) {
+    ret.push(html`
+      <td class=cluster>
+        ${_lowCell(ele, rowIndex, col, colIndex)}
+      </td>
+    `);
+  }
+
+  if (ele._stepUpAt(colIndex)) {
+    ret.push(html`
+      <td class=cluster>
+        ${_highCell(ele, rowIndex, col, colIndex)}
+      </td>
+    `);
+  }
+
+  if (ele._notBoth(colIndex)) {
+    ret.push('<td></td>');
+  }
+  return ret;
+});
+
+const _rows= (ele) => ele._reg.table.map((row, rowIndex) => html`
+  <tr>
+    <td class=fixed>
+      <commit-detail-sk .cid=${row.cid}></commit-detail-sk>
+    </td>
+    ${_columns(ele, row, rowIndex)}
+  </tr>`);
+
+const template = (ele) => html`
+  <header>
+    <details>
+      <summary>
+        <h2>Filter</h2>
+      </summary>
+      <h3>Which commits to display.</h3>
+      <select
+        @input=${ele._commitsChange}
+        >
+        <option
+          ?selected=${ele._state.subset==='all'}
+          value=all
+          title='Show results for all commits in the time range.'>All</option>
+        <option
+          ?selected=${ele._state.subset==='flagged'}
+          value=flagged
+          title='Show only the commits with regressions in the given time range regardless of triage status.'>Regressions</option>
+        <option
+          ?selected=${ele._state.subset==='untriaged'}
+          value=untriaged
+          title='Show only commits with untriaged regressions in the given time range.'>Untriaged</option>
+      </select>
+
+      <h3>Which alerts to display.</h3>
+
+      <select @input=${ele._filterChange}>
+        ${_allFilters(ele)}
+      </select>
+    </details>
+    <details>
+      <summary>
+        <h2>Range</h2>
+      </summary>
+      <day-range-sk day-range-change=${ele._rangeChange} begin=${ele._state.begin} end=${ele._state.end}></day-range-sk>
+    </details>
+    <details @toggle=${ele._toggleStatus}>
+      <summary>
+        <h2>Status</h2>
+      </summary>
+      <div>
+        <p>The current work on detecting regressions:</p>
+        <div class=status>
+          ${_statusItems(ele)}
+        </div>
+      </div>
+    </details>
+  </header>
+  <spinner-sk ?active=${ele._triageInProgress || ele._refreshRangeInProgress}></spinner-sk>
+
+  <dialog>
+    <cluster-summary2-sk
+      @open-keys=${ele._openKeys}
+      @triaged=${ele._triaged}
+      .full_summary=${ele._dialog_state.full_summary}
+      .triage=${ele._dialog_state.triage}>
+    </cluster-summary2-sk>
+    <div class=buttons>
+      <button @click=${ele._close}>Close</button>
+    </div>
+  </dialog>
+
+  <table @start-triage=${ele._triage_start}>
+    <tr>
+      <th>Commit</th>
+      ${_headers(ele)}
+    </tr>
+    <tr>
+      <th></th>
+      ${_subHeaders(ele)}
+    </tr>
+    ${_rows(ele)}
+  </table>
+  </template>
+`;
+
+window.customElements.define('triage-page-sk', class extends ElementSk {
+  constructor() {
+    super(template);
+    const now = Math.floor(Date.now()/1000);
+
+    // The state to reflect to the URL, also the body of the POST request
+    // we send to /_/reg/.
+    this._state = {
+      begin: now - 2*7*24*60*60, // 2 weeks.
+      end: now,
+      subset: "untriaged",
+      filter: "ALL",
+    };
+
+    this._reg = {
+      header: [],
+      table: [],
+    };
+
+    this._all_filter_options = []
+
+    this._triageInProgress = false;
+
+    this._refreshRangeInProgress = false;
+
+    this._currentClusteringStatus = [];
+
+    // The ID of the setInterval that is updating _currentClusteringStatus.
+    this._statusIntervalID = 0;
+
+    this._dialog_state = {
+      full_summary: {},
+      triage: {},
+    };
+
+    this._lastState = {};
+
+    this._firstConnect = false;
+  }
+
+  connectedCallback() {
+    super.connectedCallback();
+    if (this._firstConnect) {
+      return;
+    }
+    this._firstConnect = true;
+
+    this._render();
+    this._dialog = this.querySelector('triage-page-sk > dialog');
+    dialogPolyfill.registerDialog(this.querySelector('dialog'));
+    this._stateHasChanged = stateReflector(() => this._state, (state) => {
+      this._state = state;
+      this._render();
+      this._updateRange();
+    });
+  }
+
+  _commitsChange(e) {
+    this._state.subset = e.target.value;
+    this._updateRange();
+    this._stateHasChanged();
+  }
+
+  _filterChange(e) {
+    this._state.alert_filter = e.target.value;
+    this._updateRange();
+    this._stateHasChanged();
+  }
+
+  _toggleStatus(e) {
+    if (e.target.open) {
+      this._statusIntervalID = window.setInterval(() => this._pollStatus(), 5000);
+      this._pollStatus();
+    } else {
+      window.clearInterval(this._statusIntervalID);
+    }
+  }
+
+  _pollStatus() {
+    fetch('/_/reg/current').then(jsonOrThrow).then((json) => {
+      this._currentClusteringStatus = json;
+      this._render();
+    }).catch(errorMessage);
+  }
+
+  _triage_start(e) {
+    this._dialog_state = e.detail;
+    this._render();
+    this._dialog.show();
+  }
+
+  _triaged(e) {
+    e.stopPropagation();
+    const body = Object.assign({}, e.detail);
+    body.alert = this._dialog_state.alert;
+    body.cluster_type = this._dialog_state.cluster_type;
+    this._dialog.close();
+    this._render();
+    if (this._triageInProgress) {
+      errorMessage("A triage request is in progress.");
+      return;
+    }
+    this._triageInProgress = true;
+    fetch('/_/triage/', {
+      method: 'POST',
+      body: JSON.stringify(body),
+      headers:{
+        'Content-Type': 'application/json'
+      }
+    }).then(jsonOrThrow).then((json) => {
+      this._triageInProgress = false;
+      this._render();
+      if (json.bug) {
+        // Open the bug reporting page in a new window.
+        window.open(json.bug, '_blank');
+      }
+    }).catch((msg) => {
+      if (msg) {
+        errorMessage(msg, 10000);
+      }
+      this._triageInProgress = false;
+      this._render();
+    });
+  }
+
+  _close() {
+    this._dialog.close();
+  }
+
+  _stepUpAt(index) {
+    const dir = this._reg.header[index].direction;
+    return  dir === 'UP' || dir === 'BOTH';
+  }
+
+  _stepDownAt(index) {
+    const dir = this._reg.header[index].direction;
+    return  dir === 'DOWN' || dir === 'BOTH';
+  }
+
+  _notBoth(index) {
+    return this._reg.header[index].direction != 'BOTH';
+  }
+
+  _alertAt(index) {
+    return this._reg.header[index];
+  }
+
+  _encQueryFrom(colIndex) {
+    return encodeURIComponent(this._reg.header[colIndex].query);
+  }
+
+  _hashFrom(rowIndex) {
+    return this._reg.table[rowIndex].cid.hash;
+  }
+
+  _openKeys(e) {
+    const query = {
+      keys:       e.detail.shortcut,
+      begin:      e.detail.begin,
+      end:        e.detail.end,
+      xbaroffset: e.detail.xbar.offset,
+      num_commits: 50,
+      request_type: 1,
+    };
+    window.open('/e/?' + fromObject(query), '_blank');
+  }
+
+  _rangeChange(e) {
+    this._state.begin = Math.floor(e.detail.begin);
+    this._state.end = Math.floor(e.detail.end);
+    this._stateHasChanged();
+    this._updateRange();
+  }
+
+  _updateRange() {
+    if (this._refreshRangeInProgress) {
+      return;
+    }
+    if (equals(this._lastState, this._state)) {
+      return;
+    }
+    this._lastState = deepCopy(this._state);
+    this._refreshRangeInProgress = true;
+    this._render();
+    fetch('/_/reg/', {
+      method: 'POST',
+      body: JSON.stringify(this._state),
+      headers:{
+        'Content-Type': 'application/json'
+      }
+    }).then(jsonOrThrow).then((json) => {
+      this._refreshRangeInProgress = false;
+      this._reg = json;
+      this._calc_all_filter_options();
+      this._render();
+    }).catch((msg) => {
+      if (msg) {
+        errorMessage(msg, 10000);
+      }
+      this._refreshRangeInProgress = false;
+      this._render();
+    });
+  }
+
+  _calc_all_filter_options() {
+    const opts = [
+      {
+        value: 'ALL',
+        title: 'Show all alerts.',
+        display: 'Show all alerts.',
+      },
+      {
+        value: 'OWNER',
+        title: 'Show only the alerts owned by the logged in user (or all alerts if the user doesn\'t own any alerts).',
+        display: 'Show alerts you own.',
+      },
+    ];
+    if (this._reg && this._reg.categories) {
+      this._reg.categories.forEach((cat) => {
+        const displayName = cat || '(default)';
+        opts.push({
+          value: `cat:${cat}`,
+          title: `Show only the alerts in the ${displayName} category.`,
+          display: `Category: ${displayName}`,
+        });
+      });
+    }
+    this._all_filter_options = opts;
+  }
+});
diff --git a/perf/modules/triage-page-sk/triage-page-sk.scss b/perf/modules/triage-page-sk/triage-page-sk.scss
new file mode 100644
index 0000000..e8d0b99
--- /dev/null
+++ b/perf/modules/triage-page-sk/triage-page-sk.scss
@@ -0,0 +1,63 @@
+@import '~elements-sk/colors';
+
+triage-page-sk {
+  > dialog {
+    position: fixed;
+    z-index: 2;
+  }
+
+  summary h2 {
+    display: inline-block;
+    margin: 0;
+  }
+
+  header {
+    display: flex;
+  }
+
+  details {
+    display: inline-block;
+    margin: 1em 0.4em;
+    padding: 1em;
+    background: var(--dark-white);
+  }
+
+  details[open] {
+    border: solid 1px var(--light-gray);
+    background: var(--white);
+  }
+
+  details table {
+    margin: 1em;
+  }
+
+  summary {
+    padding: 0 0.6em;
+  }
+
+  summary:focus {
+    outline: dotted thick var(--blue);
+  }
+
+  tr:nth-child(even) {
+    background-color: var(--dark-white);
+  }
+
+  table {
+    border-collapse: collapse;
+    border-spacing: 0;
+  }
+
+  .cluster {
+    text-align: center;
+  }
+
+  th {
+    padding: 0 1em;
+  }
+
+  a {
+    color: var(--blue);
+  }
+
+}
diff --git a/perf/modules/triage-status-sk/triage-status-sk.scss b/perf/modules/triage-status-sk/triage-status-sk.scss
index 4292c91..48b8c32 100644
--- a/perf/modules/triage-status-sk/triage-status-sk.scss
+++ b/perf/modules/triage-status-sk/triage-status-sk.scss
@@ -1,2 +1,5 @@
 triage-status-sk {
+  button {
+    padding: 0;
+  }
 }
diff --git a/perf/modules/triage2-sk/triage2-sk.scss b/perf/modules/triage2-sk/triage2-sk.scss
index adc8fe7..cfbeae8 100644
--- a/perf/modules/triage2-sk/triage2-sk.scss
+++ b/perf/modules/triage2-sk/triage2-sk.scss
@@ -30,6 +30,10 @@
   .untriaged[raised]:hover {
     background: #ddd;
   }
+
+  button {
+    padding: 0;
+  }
 }
 
 .darkmode triage2-sk {
diff --git a/perf/pages/main.js b/perf/pages/main.js
index a91579e..6bc88b5 100644
--- a/perf/pages/main.js
+++ b/perf/pages/main.js
@@ -26,5 +26,6 @@
 import '../modules/perf-scaffold-sk'
 import '../modules/triage-status-sk'
 import '../modules/triage2-sk'
+import '../modules/triage-page-sk'
 import '../modules/tricon2-sk'
 import '../modules/word-cloud-sk'
diff --git a/perf/templates/triage.html b/perf/templates/triage.html
index 5ae3aaa..624fa98 100644
--- a/perf/templates/triage.html
+++ b/perf/templates/triage.html
@@ -6,7 +6,46 @@
       this.sk = this.sk || {};
       this.sk.perf = {{.context}};
     </script>
-    {{template "header.html" .}}
+    <!-- Can't use header.html here until everything is ported away from Polymer. -->
+    <meta charset="utf-8">
+    <meta name="theme-color" content="#1f78b4">
+    <link rel="shortcut icon" href="/res/img/favicon.ico" />
+    <link rel="manifest" href="/res/manifest.json">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <script src="/res/js/core.js" type="text/javascript" charset="utf-8"></script>
+    <style type="text/css" media="screen">
+      body {
+        margin: 0;
+        padding: 0;
+      }
+
+      .main * {
+        font-size: 12px;
+      }
+
+      .main h2 {
+        font-size: 16px !important;
+      }
+
+      .main h3 {
+        font-size: 14px !important;
+      }
+
+      .main button {
+        text-transform: none !important;
+      }
+    </style>
+    <script>
+      // Check that service workers are registered
+      if ('serviceWorker' in navigator) {
+        window.addEventListener('load', () => {
+          navigator.serviceWorker.register('/service-worker.js');
+        });
+      }
+    </script>
+    <link href="/dist/main-bundle.css" rel="stylesheet">
+    <script type="text/javascript" src="/dist/main-bundle.js"></script>
   </head>
   <body>
     <perf-scaffold-sk>