| <!-- |
| Copyright (C) 2019 Apple Inc. All rights reserved. |
| |
| Redistribution and use in source and binary forms, with or without |
| modification, are permitted provided that the following conditions |
| are met: |
| 1. Redistributions of source code must retain the above copyright |
| notice, this list of conditions and the following disclaimer. |
| 2. Redistributions in binary form must reproduce the above copyright |
| notice, this list of conditions and the following disclaimer in the |
| documentation and/or other materials provided with the distribution. |
| |
| THIS SOFTWARE IS PROVIDED BY APPLE INC. "AS IS" AND ANY |
| EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
| IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR |
| CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
| EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
| PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
| PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY |
| OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| --> |
| |
| {% extends "base.html" %} |
| {% block head %} |
| <link rel="stylesheet" type="text/css" href="assets/css/search.css"> |
| <link rel="stylesheet" type="text/css" href="assets/css/timeline.css"> |
| <link rel="stylesheet" type="text/css" href="assets/css/tooltip.css"> |
| |
| <script type="module"> |
| import {CommitBank} from '/assets/js/commit.js'; |
| import {deepCompare, ErrorDisplay, queryToParams, paramsToQuery} from '/assets/js/common.js'; |
| import {Configuration} from '/assets/js/configuration.js'; |
| import {Drawer, BranchSelector, ConfigurationSelectors, LimitSlider} from '/assets/js/drawer.js'; |
| import {InvestigateDrawer} from '/assets/js/investigate.js'; |
| import {Legend, TimelineFromEndpoint} from '/assets/js/timeline.js'; |
| import {ToolTip} from '/assets/js/tooltip.js'; |
| import {DOM, REF, EventStream} from '/library/js/Ref.js'; |
| |
| const DEFAULT_LIMIT = 100; |
| const SUITES = JSON.parse('{{ suites|safe }}'); |
| |
| class MainView { |
| constructor() { |
| this.currentParams = queryToParams(document.URL.split('?')[1]); |
| this.currentLimit = this.currentParams.limit ? parseInt(this.currentParams.limit[this.currentParams.limit.length - 1]) : DEFAULT_LIMIT; |
| delete this.currentParams.limit; |
| |
| this.ref = REF.createRef({ |
| state: SUITES, |
| onStateUpdate: (element, state) => { |
| if (!state) |
| element.innerHTML = this.placeholder(); |
| else if (state.error) |
| element.innerHTML = ErrorDisplay(state); |
| else if (state.length === 0) |
| element.innerHTML = ErrorDisplay({ |
| error: 'Not Found', |
| description: 'No suites matching the specified criteria', |
| }); |
| else |
| DOM.inject(element, this.render(state)); |
| } |
| }); |
| this.latestDispatch = Date.now(); |
| this.children = {}; |
| |
| SUITES.forEach((suite) => { |
| this.children[suite] = new TimelineFromEndpoint('api/results/' + suite, suite); |
| }); |
| } |
| unpackSuites(suitesJson) { |
| if (suitesJson.error) |
| return suitesJson; |
| |
| let suites = new Set(); |
| suitesJson.forEach((pair) => { |
| pair[1].forEach((suite) => { |
| suites.add(suite); |
| }); |
| }); |
| |
| let sortedSuites = [...suites].sort(); |
| Object.keys(this.children).forEach(key => {this.children[key].teardown();}); |
| this.children = {}; |
| sortedSuites.forEach((suite) => { |
| this.children[suite] = new TimelineFromEndpoint('api/results/' + suite, suite); |
| }); |
| return sortedSuites; |
| } |
| reload() { |
| let params = queryToParams(document.URL.split('?')[1]); |
| let limit = params.limit ? parseInt(params.limit[params.limit.length - 1]) : DEFAULT_LIMIT; |
| delete params.limit; |
| |
| if (deepCompare(params, this.currentParams)) { |
| if (limit === this.currentLimit) |
| return; |
| |
| for (let suite in this.children) { |
| if (limit > this.currentLimit) |
| this.children[suite].reload(); |
| else |
| this.children[suite].rerender(); |
| } |
| this.currentLimit = limit; |
| return; |
| } |
| |
| let myDispatch = Date.now(); |
| this.currentParams = params; |
| this.currentLimit = limit; |
| this.latestDispatch = Math.max(this.latestDispatch, myDispatch); |
| this.ref.setState(null); |
| |
| let self = this; |
| let suiteParams = {}; |
| Configuration.members().forEach(member => { |
| if (params[member]) |
| suiteParams[member] = params[member]; |
| }); |
| if (params.branch) |
| suiteParams.branch = params.branch; |
| let query = paramsToQuery(suiteParams); |
| fetch(query ? 'api/suites?' + query: 'api/suites').then(response => { |
| response.json().then(json => { |
| if (myDispatch === this.latestDispatch) |
| self.ref.setState(this.unpackSuites(json)); |
| }); |
| }).catch(error => { |
| if (myDispatch === self.latestDispatch) |
| self.ref.setState({error: "Connection Error", description: error}); |
| }); |
| } |
| placeholder() { |
| return `<div class="placeholder dimmer"> |
| <div class="loader"> |
| <div class="spinner"></div> |
| </div> |
| </div>`; |
| } |
| toString() { |
| return `<div ref="${this.ref}"></div>`; |
| } |
| render(suites) { |
| let children = this.children; |
| return suites.map(suite => { |
| return `<div class="section"> |
| <div class="header"> |
| <div class="title">${suite}</div> |
| </div> |
| ${children[suite]} |
| </div><br>`; |
| }).join(''); |
| } |
| notifyTimelinesRender() { |
| for (let suite in this.children) { |
| this.children[suite].notifyRerender(); |
| } |
| } |
| } |
| |
| let view = new MainView(); |
| const onLayoutChange = new EventStream(); |
| onLayoutChange.action(() => { |
| view.notifyTimelinesRender(); |
| }); |
| |
| DOM.inject(document.getElementById('app'), `${ToolTip} |
| ${InvestigateDrawer} |
| ${Drawer([ |
| Legend(() => { |
| for (let suite in view.children) { |
| view.children[suite].update(); |
| } |
| }, true, true), |
| LimitSlider(() => {view.reload()}), |
| BranchSelector(() => { |
| CommitBank.reload(); |
| view.reload(); |
| }), |
| ConfigurationSelectors(() => {view.reload()}), |
| ], () => onLayoutChange.add())} |
| <div class="main right-sidebar under-topbar-with-actions"> |
| <br> |
| <div class="content" ref=${REF.createRef({ |
| onElementMount: (element) => { |
| for (let suite in view.children) { |
| view.children[suite].viewport = element; |
| } |
| } |
| })}> |
| ${view} |
| </div> |
| </div>`); |
| |
| </script> |
| |
| {% endblock %} |
| {% block content %} |
| |
| <div id="app"> |
| </div> |
| |
| {% endblock %} |