github.com/web-platform-tests/wpt.fyi@v0.0.0-20240530210107-70cf978996f1/webapp/components/test-runs-query-builder.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/iron-icons/iron-icons.js';
     8  import '../node_modules/@polymer/paper-button/paper-button.js';
     9  import '../node_modules/@polymer/paper-checkbox/paper-checkbox.js';
    10  import '../node_modules/@polymer/paper-input/paper-input.js';
    11  import '../node_modules/@polymer/paper-item/paper-item.js';
    12  import '../node_modules/@polymer/polymer/lib/elements/dom-if.js';
    13  import '../node_modules/@polymer/polymer/lib/elements/dom-repeat.js';
    14  import { html } from '../node_modules/@polymer/polymer/polymer-element.js';
    15  import { PolymerElement } from '../node_modules/@polymer/polymer/polymer-element.js';
    16  import '../node_modules/@vaadin/vaadin-date-picker/vaadin-date-picker-light.js';
    17  import '../node_modules/@vaadin/vaadin-date-picker/vaadin-date-picker.js';
    18  import './info-banner.js';
    19  import './product-builder.js';
    20  import { AllBrowserNames, SemanticLabels} from './product-info.js';
    21  import { TestRunsUIQuery } from './test-runs-query.js';
    22  import { WPTFlags } from './wpt-flags.js';
    23  
    24  
    25  /**
    26   * Base class for re-use of results-fetching behaviour, between
    27   * multi-item (wpt-results) and single-test (test-file-results) views.
    28   */
    29  class TestRunsQueryBuilder extends WPTFlags(TestRunsUIQuery(PolymerElement)) {
    30    static get template() {
    31      return html`
    32      <style>
    33        #add-button {
    34          background-color: var(--paper-blue-500);
    35          color: white;
    36        }
    37        #clear-button {
    38          background-color: var(--paper-red-500);
    39          color: white;
    40        }
    41        #submit-button {
    42          background-color: var(--paper-green-500);
    43          color: white;
    44        }
    45        product-builder {
    46          max-width: 180px;
    47          display: inline-block;
    48        }
    49        vaadin-date-picker-light + vaadin-date-picker-light {
    50          margin-left: 16px;
    51        }
    52      </style>
    53  
    54      <h3>
    55        Products
    56      </h3>
    57      <template is="dom-if" if="[[debug]]">
    58        [[query]]
    59      </template>
    60      <div>
    61        <template is="dom-repeat" items="[[products]]" as="p" index-as="i">
    62          <product-builder browser-name="{{p.browser_name}}"
    63                           browser-version="{{p.browser_version}}"
    64                           labels="{{p.labels}}"
    65                           debug="[[debug]]"
    66                           on-product-changed="[[productChanged(i)]]"
    67                           on-delete="[[productDeleted(i)]]">
    68          </product-builder>
    69        </template>
    70        <template is="dom-if" if="[[!products.length]]">
    71          <info-banner>
    72            <iron-icon icon="info"></iron-icon> No products selected. The default products will be used.
    73          </info-banner>
    74        </template>
    75      </div>
    76      <template is="dom-if" if="[[showTimeRange]]">
    77        <paper-item>
    78          <vaadin-date-picker-light attr-for-value="value" value="[[fromISO]]">
    79            <paper-input label="From" value="{{fromISO}}"></paper-input>
    80          </vaadin-date-picker-light>
    81          <vaadin-date-picker-light attr-for-value="value" value="[[toISO]]">
    82            <paper-input label="To" value="{{toISO}}"></paper-input>
    83          </vaadin-date-picker-light>
    84        </paper-item>
    85      </template>
    86      <paper-item>
    87        <paper-checkbox id="aligned-checkbox" checked="{{aligned}}">Aligned runs only</paper-checkbox>
    88      </paper-item>
    89      <paper-item>
    90        <paper-checkbox checked="{{diff}}" disabled="{{!canShowDiff}}">Show diff</paper-checkbox>
    91      </paper-item>
    92      <paper-item>
    93        <paper-checkbox id="master-checkbox" checked="{{master}}">Only master branch</paper-checkbox>
    94      </paper-item>
    95      <paper-item>
    96        <paper-input label="Labels" always-float-label placeholder="e.g. stable,buildbot" value="{{ labelsString::input }}">
    97        </paper-input>
    98      </paper-item>
    99      <template is="dom-if" if="[[queryBuilderSHA]]">
   100        <paper-item>
   101          <paper-input-container always-float-label>
   102            <label slot="label">SHA</label>
   103            <input name="os_version" placeholder="(Latest)" list="shas-datalist" value="{{ _sha::input }}" slot="input">
   104            <datalist id="shas-datalist"></datalist>
   105          </paper-input-container>
   106        </paper-item>
   107      </template>
   108      <br>
   109      <paper-button raised id="add-button" onclick="[[addProduct]]">
   110        <iron-icon icon="add"></iron-icon> Add product
   111      </paper-button>
   112      <paper-button raised id="clear-button" onclick="[[clearAll]]">
   113        <iron-icon icon="delete"></iron-icon> Clear all
   114      </paper-button>
   115      <paper-button raised id="submit-button" onclick="[[submit]]">
   116        <iron-icon icon="done"></iron-icon> Submit
   117      </paper-button>
   118  `;
   119    }
   120  
   121    static get is() {
   122      return 'test-runs-query-builder';
   123    }
   124  
   125    static get properties() {
   126      return {
   127        debug: {
   128          type: Boolean,
   129          value: false,
   130        },
   131        onSubmit: Function,
   132        labelsString: {
   133          type: String,
   134          observer: 'labelsStringUpdated',
   135        },
   136        showTimeRange: Boolean,
   137        shasURL: {
   138          type: String,
   139          computed: 'computeSHAsURL(query)',
   140          observer: 'shasURLUpdated',
   141        },
   142        _sha: {
   143          type: String,
   144          observer: 'shaUpdated'
   145        },
   146        matchingSHAs: {
   147          type: Array,
   148        },
   149        shasAutocomplete: {
   150          type: Array,
   151          observer: 'shasAutocompleteUpdated'
   152        },
   153        canShowDiff: {
   154          type: Boolean,
   155          computed: 'computeCanShowDiff(productSpecs)',
   156        },
   157        fromISO: {
   158          type: String,
   159          observer: 'fromISOChanged',
   160        },
   161        toISO: {
   162          type: String,
   163          observer: 'toISOChanged',
   164        },
   165      };
   166    }
   167  
   168    constructor() {
   169      super();
   170      this.productDeleted = i => () => {
   171        this.handleDeleteProduct(i);
   172      };
   173      this.productChanged = i => {
   174        return product => {
   175          this.handleProductChanged(i, product);
   176        };
   177      };
   178      this.addProduct = () => {
   179        this.handleAddProduct();
   180      };
   181      this.clearAll = this.handleClearAll.bind(this);
   182      this.submit = this.handleSubmit.bind(this);
   183      this._createMethodObserver('labelsUpdated(labels, labels.*)');
   184      this._createMethodObserver('shasUpdated(_sha, matchingSHAs)');
   185    }
   186  
   187    ready() {
   188      super.ready();
   189      if (this.from) {
   190        this.fromISO = this.from.toISOString().substring(0, 10);
   191      }
   192      if (this.to) {
   193        this.toISO = this.to.toISOString().substring(0, 10);
   194      }
   195    }
   196  
   197    computeCanShowDiff(productSpecs) {
   198      return productSpecs && productSpecs.length === 2;
   199    }
   200  
   201    handleDeleteProduct(i) {
   202      this.splice('products', i, 1);
   203    }
   204  
   205    handleProductChanged(i, product) {
   206      this.set(`products.${i}`, product);
   207    }
   208  
   209    handleSubmit() {
   210      // Handle the edge-case that the user typed a label for channel or source, etc.
   211      const productBuilders = this.shadowRoot.querySelectorAll('product-builder');
   212      for (const semantic of SemanticLabels) {
   213        for (const label of semantic.values) {
   214          if (this.labels.includes(label)) {
   215            this.labels = this.labels.filter(l => l !== label);
   216            for (const p of productBuilders) {
   217              p[semantic.property] = label;
   218            }
   219          }
   220        }
   221      }
   222      this.onSubmit && this.onSubmit();
   223    }
   224  
   225    // Respond to query changes by computing a new shas URL.
   226    computeSHAsURL(query) {
   227      const url = new URL('/api/shas', window.location);
   228      url.search = query || '';
   229      url.searchParams.delete('sha');
   230      return url;
   231    }
   232  
   233    // Respond to shas URL changing by fetching the shas
   234    shasURLUpdated(url) {
   235      fetch(url).then(r => r.json()).then(s => {
   236        this.matchingSHAs = s;
   237      });
   238    }
   239  
   240    // Respond to newly fetched shas, or user input, by filtering the autocomplete list.
   241    shasUpdated(sha, matchingSHAs) {
   242      if (!matchingSHAs || !matchingSHAs.length || !this.queryBuilderSHA) {
   243        return;
   244      }
   245      if (sha) {
   246        matchingSHAs = matchingSHAs.filter(s => s.startsWith(sha));
   247      }
   248      matchingSHAs = matchingSHAs.slice(0, 10);
   249      // Check actually different from current.
   250      const current = new Set(this.shasAutocomplete || []);
   251      if (current.size === matchingSHAs.length && !matchingSHAs.find(v => !current.has(v))) {
   252        return;
   253      }
   254      this.shasAutocomplete = matchingSHAs;
   255    }
   256  
   257    shaUpdated(sha) {
   258      this.shas = this.computeIsLatest(sha) ? [] : [sha];
   259    }
   260  
   261    shasAutocompleteUpdated(shasAutocomplete) {
   262      const datalist = this.shadowRoot.querySelector('datalist');
   263      datalist.innerHTML = '';
   264      for (const sha of shasAutocomplete) {
   265        const option = document.createElement('option');
   266        option.setAttribute('value', sha);
   267        datalist.appendChild(option);
   268      }
   269    }
   270  
   271    labelsUpdated(labels) {
   272      let joined = labels && labels.length && labels.join(', ')
   273        || null;
   274      if (joined !== this.labelsString) {
   275        this.labelsString = joined;
   276      }
   277    }
   278  
   279    labelsStringUpdated(labelsString) {
   280      const labels = (labelsString || '')
   281        .split(',').map(i => i.trim()).filter(i => i);
   282      if (labels.join(',') !== this.labels.join(',')) {
   283        this.labels = labels;
   284      }
   285    }
   286  
   287    handleAddProduct() {
   288      // TODO(lukebjerring): Make a smart(er) suggestion.
   289      let next = { browser_name: 'chrome' };
   290      for (const d of AllBrowserNames) {
   291        if (this.products.find(p => p.browser_name === d)) {
   292          continue;
   293        }
   294        next.browser_name = d;
   295        break;
   296      }
   297      this.splice('products', this.products.length, 0, next);
   298    }
   299  
   300    clearQuery() {
   301      super.clearQuery();
   302      this.diff = undefined;
   303    }
   304  
   305    handleClearAll() {
   306      this.clearQuery();
   307      this.set('products', []);
   308    }
   309  
   310    fromISOChanged(from) {
   311      from = new Date(from);
   312      if (isFinite(from)) {
   313        this.from = from;
   314      }
   315    }
   316  
   317    toISOChanged(to) {
   318      to = new Date(to);
   319      if (isFinite(to)) {
   320        this.to = to;
   321      }
   322    }
   323  }
   324  
   325  window.customElements.define(TestRunsQueryBuilder.is, TestRunsQueryBuilder);