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);