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