github.com/web-platform-tests/wpt.fyi@v0.0.0-20240530210107-70cf978996f1/webapp/components/wpt-runs.js (about)

     1  /**
     2   * Copyright 2018 The WPT Dashboard Project. All rights reserved.
     3   * Use of this source code is governed by a BSD-style license that can be
     4   * found in the LICENSE file.
     5   */
     6  
     7  import '../node_modules/@polymer/polymer/lib/elements/dom-if.js';
     8  import '../node_modules/@polymer/iron-collapse/iron-collapse.js';
     9  import '../node_modules/@polymer/iron-scroll-threshold/iron-scroll-threshold.js';
    10  import '../node_modules/@polymer/paper-button/paper-button.js';
    11  import '../node_modules/@polymer/paper-toast/paper-toast.js';
    12  import '../node_modules/@polymer/paper-progress/paper-progress.js';
    13  import '../node_modules/@polymer/paper-spinner/paper-spinner-lite.js';
    14  import '../node_modules/@polymer/paper-styles/color.js';
    15  import '../node_modules/@polymer/polymer/lib/elements/dom-if.js';
    16  import '../node_modules/@polymer/polymer/lib/elements/dom-repeat.js';
    17  import '../node_modules/@polymer/polymer/polymer-element.js';
    18  import { html } from '../node_modules/@polymer/polymer/polymer-element.js';
    19  import './info-banner.js';
    20  import { LoadingState } from './loading-state.js';
    21  import { CommitTypes } from './product-info.js';
    22  import { SelfNavigation } from './self-navigator.js';
    23  import './test-run.js';
    24  import './test-runs-query-builder.js';
    25  import { TestRunsUIBase } from './test-runs.js';
    26  import { WPTFlags } from './wpt-flags.js';
    27  import { Pluralizer } from './pluralize.js';
    28  
    29  class WPTRuns extends Pluralizer(WPTFlags(SelfNavigation(LoadingState(TestRunsUIBase)))) {
    30    static get template() {
    31      return html`
    32      <style>
    33        a {
    34          text-decoration: none;
    35          color: #0d5de6;
    36          font-family: monospace;
    37        }
    38        table {
    39          width: 100%;
    40          border-collapse: separate;
    41          margin-bottom: 2em;
    42        }
    43        td {
    44          padding: 0 0.5em;
    45          margin: 2px;
    46        }
    47        td[no-padding] {
    48          padding: 0;
    49          margin: 0;
    50        }
    51        td[day-boundary] {
    52          border-top: 1px solid var(--paper-blue-100);
    53        }
    54        .time {
    55          color: var(--paper-grey-300);
    56        }
    57        .missing {
    58          background-color: var(--paper-grey-100);
    59        }
    60        .runs {
    61          text-align: center;
    62        }
    63        .runs a {
    64          display: inline-block;
    65        }
    66        .runs.present {
    67          background-color: var(--paper-blue-100);
    68        }
    69        .loading {
    70          display: flex;
    71          flex-direction: column;
    72          align-items: center;
    73        }
    74        test-runs-query-builder {
    75          display: block;
    76          margin-bottom: 32px;
    77        }
    78        .github {
    79          display: flex;
    80          align-content: center;
    81          align-items: center;
    82        }
    83        .github img {
    84          margin-right: 8px;
    85          height: 24px;
    86          width: 24px;
    87        }
    88        test-run {
    89          display: inline-block;
    90          cursor: pointer;
    91        }
    92        test-run[selected] {
    93          padding: 4px;
    94          background: var(--paper-blue-700);
    95          border-radius: 50%;
    96        }
    97        paper-toast {
    98          min-width: 320px;
    99        }
   100        paper-toast div {
   101          display: flex;
   102          align-items: center;
   103        }
   104        paper-toast span {
   105          flex-grow: 1;
   106        }
   107        paper-toast paper-button {
   108          display: inline-block;
   109          flex-grow: 0;
   110          flex-shrink: 0;
   111        }
   112        paper-progress {
   113          --paper-progress-active-color: var(--paper-light-blue-500);
   114          --paper-progress-secondary-color: var(--paper-light-blue-100);
   115          width: 100%;
   116        }
   117  
   118        @media (max-width: 1200px) {
   119          table tr td:first-child::after {
   120            content: "";
   121            display: inline-block;
   122            vertical-align: top;
   123            min-height: 30px;
   124          }
   125        }
   126      </style>
   127  
   128      <paper-toast id="selected-toast" duration="0">
   129        <div style="display: flex;">
   130          <span>[[selectedRuns.length]] [[runPlural]] selected</span>
   131          <paper-button onclick="[[showRuns]]">View [[runPlural]]</paper-button>
   132          <template is="dom-if" if="[[twoRunsSelected]]">
   133            <paper-button onclick="[[showDiff]]">View diff</paper-button>
   134          </template>
   135        </div>
   136      </paper-toast>
   137  
   138      <template is="dom-if" if="[[resultsRangeMessage]]">
   139        <info-banner>
   140          [[resultsRangeMessage]]
   141          <paper-button onclick="[[toggleBuilder]]" slot="small">Edit</paper-button>
   142        </info-banner>
   143      </template>
   144  
   145      <template is="dom-if" if="[[queryBuilder]]">
   146        <iron-collapse opened="[[editingQuery]]">
   147          <test-runs-query-builder product-specs="[[productSpecs]]"
   148                                   labels="[[labels]]"
   149                                   master="[[master]]"
   150                                   shas="[[shas]]"
   151                                   aligned="[[aligned]]"
   152                                   on-submit="[[submitQuery]]"
   153                                   from="[[from]]"
   154                                   to="[[to]]"
   155                                   diff="[[diff]]"
   156                                   show-time-range>
   157          </test-runs-query-builder>
   158        </iron-collapse>
   159      </template>
   160  
   161      <template is="dom-if" if="[[loadingFailed]]">
   162        <info-banner type="error">
   163          Failed to load test runs.
   164        </info-banner>
   165      </template>
   166  
   167      <template is="dom-if" if="[[noResults]]">
   168        <info-banner type="info">
   169          No results.
   170        </info-banner>
   171      </template>
   172  
   173      <template is="dom-if" if="[[testRuns.length]]">
   174        <table>
   175          <thead>
   176            <tr>
   177              <th width="120">SHA</th>
   178              <template is="dom-repeat" items="{{ browsers }}" as="browser">
   179                <th width="[[computeThWidth(browsers)]]">[[displayName(browser)]]</th>
   180              </template>
   181            </tr>
   182          </thead>
   183          <tbody>
   184          <template is="dom-repeat" items="{{ testRunsBySHA }}" as="results">
   185            <tr>
   186              <td>
   187                <a class="github" href="{{ revisionLink(results) }}">
   188                  <template is="dom-if" if="[[results.commitType]]">
   189                    <img src="/static/[[results.commitType]].svg">
   190                    {{ githubRevision(results.sha) }}
   191                  </template>
   192                  <template is="dom-if" if="[[!results.commitType]]">
   193                    [[ results.sha ]]
   194                  </template>
   195                </a>
   196              </td>
   197              <template is="dom-repeat" items="{{ browsers }}" as="browser">
   198                <td class\$="runs [[ runClass(results.runs, browser) ]]">
   199                  <template is="dom-repeat" items="[[runList(results.runs, browser)]]" as="run">
   200                    <test-run onclick="[[selectRun]]"
   201                              data-run-id$="[[run.id]]"
   202                              test-run="[[run]]"
   203                              small
   204                              overlap
   205                              show-platform
   206                              show-source></test-run>
   207                  </template>
   208                </td>
   209              </template>
   210              <td day-boundary\$="{{results.day_boundary}}">
   211                <template is="dom-if" if="[[results.day_boundary]]">
   212                  {{ computeDateDisplay(results) }}
   213                </template>
   214                <span class="time">
   215                  {{ computeTimeDisplay(results) }}
   216                </span>
   217              </td>
   218            </tr>
   219          </template>
   220            <tr>
   221              <td colspan="999" no-padding>
   222                <paper-progress indeterminate hidden="[[!isLoading]]"></paper-progress>
   223              </td>
   224            </tr>
   225          </tbody>
   226        </table>
   227  
   228        <iron-scroll-threshold lower-threshold="0" on-lower-threshold="loadNextPage" id="threshold" scroll-target="document">
   229        </iron-scroll-threshold>
   230      </template>
   231  
   232      <div class="loading">
   233        <paper-spinner-lite active="[[isLoadingFirstRuns]]" class="blue"></paper-spinner-lite>
   234      </div>
   235  `;
   236    }
   237  
   238    static get is() {
   239      return 'wpt-runs';
   240    }
   241  
   242    static get properties() {
   243      return {
   244        // Array({ sha, Array({ platform, run, sum }))
   245        testRunsBySHA: {
   246          type: Array
   247        },
   248        browsers: {
   249          type: Array
   250        },
   251        displayedNodes: {
   252          type: Array,
   253          value: []
   254        },
   255        loadingFailed: {
   256          type: Boolean,
   257          value: false,
   258        },
   259        editingQuery: Boolean,
   260        toggleBuilder: Function,
   261        submitQuery: Function,
   262        selectedRuns: {
   263          type: Array,
   264          value: [],
   265        },
   266        runPlural: {
   267          type: String,
   268          computed: 'computeRunPlural(selectedRuns)',
   269        },
   270        twoRunsSelected: {
   271          type: Boolean,
   272          computed: 'computeTwoRunsSelected(selectedRuns)',
   273        },
   274        isLoadingFirstRuns: {
   275          type: Boolean,
   276          computed: 'computeIsLoadingFirstRuns(isLoading)',
   277        }
   278      };
   279    }
   280  
   281    constructor() {
   282      super();
   283      this.onLoadingComplete = () => {
   284        this.loadingFailed = !this.testRunsBySHA;
   285        this.noResults = !this.loadingFailed && !this.testRunsBySHA.length;
   286      };
   287      this.toggleBuilder = () => {
   288        this.editingQuery = !this.editingQuery;
   289      };
   290      this.submitQuery = this.handleSubmitQuery.bind(this);
   291      this.loadNextPage = this.handleLoadNextPage.bind(this);
   292      this.selectRun = this.handleSelectRun.bind(this);
   293      this.showRuns = () => this._showRuns(false);
   294      this.showDiff = () => this._showRuns(true);
   295    }
   296  
   297    async ready() {
   298      super.ready();
   299      this.load(this.loadRuns().then(() => this.resetScrollThreshold()));
   300      this._createMethodObserver('testRunsLoaded(testRuns, testRuns.*)');
   301    }
   302  
   303    resetScrollThreshold() {
   304      const threshold = this.shadowRoot.querySelector('iron-scroll-threshold');
   305      threshold && threshold.clearTriggers();
   306    }
   307  
   308    computeIsLoadingFirstRuns(isLoading) {
   309      return isLoading && !(this.testRuns && this.testRuns.length);
   310    }
   311  
   312    computeDateDisplay(results) {
   313      if (!results || !results.date) {
   314        return;
   315      }
   316      const date = results.date;
   317      const opts = {
   318        month: 'short',
   319        day: 'numeric',
   320      };
   321      if (results.year_boundary
   322        && date.getYear() !== new Date().getYear()) {
   323        opts.year = 'numeric';
   324      }
   325      return date && date.toLocaleDateString(navigator.language, opts);
   326    }
   327  
   328    computeTimeDisplay(results) {
   329      if (!results || !results.date) {
   330        return;
   331      }
   332      const date = results.date;
   333      return date && date.toLocaleTimeString(navigator.language, {
   334        hour: 'numeric',
   335        minute: '2-digit',
   336        hour12: false,
   337      });
   338    }
   339  
   340    testRunsLoaded(testRuns) {
   341      let browsers = new Set();
   342      // Group the runs by their revision/SHA
   343      let shaToRunsMap = testRuns.reduce((accum, results) => {
   344        browsers.add(results.browser_name);
   345        if (!accum[results.revision]) {
   346          accum[results.revision] = {};
   347        }
   348        if (!accum[results.revision][results.browser_name]) {
   349          accum[results.revision][results.browser_name] = [];
   350        }
   351        accum[results.revision][results.browser_name].push(results);
   352        return accum;
   353      }, {});
   354  
   355      // We flatten into an array of objects so Polymer can deal with them.
   356      const firstRunDate = runs => {
   357        return Object.values(runs)
   358          .reduce((oldest, runs) => {
   359            for (const time of runs.map(r => new Date(r.time_start))) {
   360              if (time < oldest) {
   361                oldest = time;
   362              }
   363            }
   364            return oldest;
   365          }, new Date()); // Existing runs should be historical...
   366      };
   367      const flattened = Object.entries(shaToRunsMap)
   368        .map(([sha, runs]) => ({
   369          sha,
   370          runs,
   371          firstRunDate: firstRunDate(runs),
   372          commitType: this.commitType(runs),
   373        }))
   374        .sort((a, b) => b.firstRunDate.getTime() - a.firstRunDate.getTime());
   375  
   376      // Append time (day) metadata.
   377      if (flattened.length > 1) {
   378        let previous = new Date(8640000000000000); // Max date.
   379        for (let i = 0; i < flattened.length; i++) {
   380          let current = flattened[i].firstRunDate;
   381          flattened[i].date = current;
   382          if (previous.getDate() !== current.getDate()) {
   383            flattened[i].day_boundary = true;
   384          }
   385          if (previous.getYear() !== current.getYear()) {
   386            flattened[i].year_boundary = true;
   387          }
   388          previous = current;
   389        }
   390      }
   391      this.testRunsBySHA = flattened;
   392      this.browsers = Array.from(browsers).sort();
   393    }
   394  
   395    runClass(testRuns, browser) {
   396      let testRun = testRuns[browser];
   397      if (!testRun) {
   398        return 'missing';
   399      }
   400      return 'present';
   401    }
   402  
   403    runList(testRuns, browser) {
   404      return testRuns[browser] || [];
   405    }
   406  
   407    runLink(run) {
   408      let link = new URL('/results', window.location);
   409      link.searchParams.set('sha', run.revision);
   410      for (const label of ['experimental', 'stable']) {
   411        if (run.labels && run.labels.includes(label)) {
   412          link.searchParams.append('label', label);
   413        }
   414      }
   415      return link.toString();
   416    }
   417  
   418    revisionLink(results) {
   419      const url = new URL('/results', window.location);
   420      url.search = this.query;
   421      url.searchParams.set('sha', results.sha);
   422      url.searchParams.set('max-count', 1);
   423      url.searchParams.delete('from');
   424      return url;
   425    }
   426  
   427    computeThWidth(browsers) {
   428      return `${100 / (browsers.length + 2)}%`;
   429    }
   430  
   431    handleSubmitQuery() {
   432      const queryBefore = this.query;
   433      const builder = this.shadowRoot.querySelector('test-runs-query-builder');
   434      this.editingQuery = false;
   435      this.nextPageToken = null;
   436      this.updateQueryParams(builder.queryParams);
   437      if (queryBefore === this.query) {
   438        return;
   439      }
   440      // Trigger a virtual navigation.
   441      this.navigateToLocation(window.location);
   442      this.setProperties({
   443        browsers: [],
   444        testRuns: [],
   445      });
   446      this.load(this.loadRuns());
   447    }
   448  
   449    handleLoadNextPage() {
   450      this.load(this.loadMoreRuns().then(runs => {
   451        runs && runs.length && this.resetScrollThreshold();
   452      }));
   453    }
   454  
   455    githubRevision(sha) {
   456      return sha.substr(0, 7);
   457    }
   458  
   459    commitType(runsByBrowser) {
   460      if (!this.githubCommitLinks) {
   461        return;
   462      }
   463      const types = CommitTypes;
   464      for (const runs of Object.values(runsByBrowser)) {
   465        for (const r of runs) {
   466          const label = r.labels && r.labels.find(l => types.has(l));
   467          if (label) {
   468            return label;
   469          }
   470        }
   471      }
   472    }
   473  
   474    _showRuns(diff) {
   475      const url = new URL('/results', window.location);
   476      for (const id of this.selectedRuns) {
   477        url.searchParams.append('run_id', id);
   478      }
   479      if (diff) {
   480        url.searchParams.set('diff', true);
   481      }
   482      window.location = url;
   483    }
   484  
   485    handleSelectRun(e) {
   486      const id = e.target.getAttribute('data-run-id');
   487      if (this.selectedRuns.find(r => r === id)) {
   488        this.selectedRuns = this.selectedRuns.filter(r => r !== id);
   489        e.target.removeAttribute('selected');
   490      } else {
   491        this.selectedRuns = [...this.selectedRuns, id];
   492        e.target.setAttribute('selected', 'selected');
   493      }
   494      const toast = this.shadowRoot.querySelector('#selected-toast');
   495      if (this.selectedRuns.length) {
   496        toast.show();
   497      } else {
   498        toast.hide();
   499      }
   500    }
   501  
   502    computeRunPlural(selectedRuns) {
   503      return this.pluralize('run', selectedRuns.length);
   504    }
   505  
   506    computeTwoRunsSelected(selectedRuns) {
   507      return selectedRuns.length === 2;
   508    }
   509  }
   510  
   511  window.customElements.define(WPTRuns.is, WPTRuns);