| <!-- |
| 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 {SearchBar} from '/assets/js/search.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 SearchView { |
| 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; |
| |
| let state = {children: [], prepending: []}; |
| if (this.currentParams.test && (!this.currentParams.suite || this.currentParams.test.length != this.currentParams.suite.length)) |
| state = {error: "Query Error", description: 'Must have the same number of suites and tests in query'}; |
| else if (this.currentParams.test) { |
| for (let i = 0; i < this.currentParams.test.length; ++i) { |
| state.children.push({ |
| suite: this.currentParams.suite[i], |
| test: this.currentParams.test[i], |
| timeline: new TimelineFromEndpoint(`api/results/${this.currentParams.suite[i]}/${this.currentParams.test[i]}`, this.currentParams.suite[i], this.currentParams.test[i]), |
| }); |
| } |
| } |
| |
| delete this.currentParams.suite; |
| delete this.currentParams.test; |
| |
| const self = this; |
| this.ref = REF.createRef({ |
| state: state, |
| onStateUpdate: (element, diff, state) => { |
| if (diff.error) { |
| DOM.inject(element, ErrorDisplay(diff)); |
| return; |
| } |
| |
| function renderChild(child) { |
| let exitRef = REF.createRef({ |
| onElementMount: element => { |
| element.onclick = () => { |
| for (let i = 0; i < self.ref.state.children.length; ++i) { |
| if (child.test !== self.ref.state.children[i].test && child.suite !== self.ref.state.children[i].suite) |
| continue; |
| self.ref.state.children.splice(i, 1); |
| break; |
| } |
| |
| const splitURL = document.URL.split('?'); |
| let params = queryToParams(splitURL[1]); |
| if (!params.suite) |
| params.suite = []; |
| if (!params.test) |
| params.test = []; |
| for (let i = 0; i < params.suite.length; ++i) { |
| // This shouldn't be possible, just keep the current params if it happens. |
| if (i >= params.test.length) |
| break; |
| if (params.suite[i] !== child.suite && params.test[i] !== child.test) |
| continue; |
| params.suite.splice(i, 1); |
| params.test.splice(i, 1); |
| break; |
| } |
| if (params.suite.length === 0) |
| delete params.suite; |
| if (params.test.length === 0) |
| delete params.test; |
| |
| const queryString = paramsToQuery(params); |
| window.history.pushState(queryString, '', splitURL[0] + '?' + queryString); |
| |
| const section = element.parentElement.parentElement.parentElement.parentElement; |
| section.parentElement.removeChild(section); |
| }; |
| }, |
| }); |
| |
| return `<div class="section"> |
| <div class="header"> |
| <div class="title" style="font-size:var(--smallSize);word-break:break-word;">${child.test} (${child.suite})</div> |
| <div class="actions"> |
| <div class="list"> |
| <a class="item link-button" ref="${exitRef}">X</a> |
| </div> |
| </div> |
| </div> |
| ${child.timeline} |
| </div>`; |
| } |
| |
| if (diff.children) |
| DOM.inject(element, diff.children.map(renderChild).join('')); |
| if (diff.prepending) { |
| diff.prepending.forEach(child => { |
| if (element.firstElementChild) |
| DOM.after(element.firstElementChild, renderChild(child)); |
| else |
| DOM.inject(element, renderChild(child)); |
| this.ref.state.children.unshift(child); |
| }); |
| } |
| } |
| }); |
| } |
| reload() { |
| let params = queryToParams(document.URL.split('?')[1]); |
| let limit = params.limit ? parseInt(params.limit[params.limit.length - 1]) : DEFAULT_LIMIT; |
| |
| // Try and determine if the tests we're displaying have changed. |
| if (params.test && (!params.suite || params.test.length != params.suite.length)) |
| this.ref.setState({error: "Query Error", description: 'Must have the same number of suites and tests in query'}); |
| else if (params.test) { |
| let needReRender = params.test.length != this.ref.state.children.length; |
| let newChildren = []; |
| |
| for (let i = 0; i < params.test.length; ++i) { |
| let didFind = false; |
| for (let search = 0; search < this.ref.state.children.length; ++search) { |
| if (this.ref.state.children[search].suite === params.suite[i] && this.ref.state.children[search].test === params.test[i]) { |
| newChildren.push(this.ref.state.children[search]); |
| didFind = true; |
| this.ref.state.children.splice(search, 1); |
| break; |
| } else |
| this.ref.state.children[search].timeline.teardown(); |
| needReRender = true; |
| } |
| if (!didFind) { |
| needReRender = true; |
| |
| newChildren.push({ |
| suite: params.suite[i], |
| test: params.test[i], |
| timeline: new TimelineFromEndpoint(`api/results/${params.suite[i]}/${params.test[i]}`, params.suite[i], params.test[i]), |
| }); |
| } |
| } |
| |
| if (needReRender) |
| this.ref.setState({children: newChildren}); |
| else |
| this.ref.state.children = newChildren; |
| } |
| |
| delete params.limit; |
| delete params.suite; |
| delete params.test; |
| |
| if (deepCompare(params, this.currentParams)) { |
| if (limit === this.currentLimit) |
| return; |
| if (limit < this.currentLimit) { |
| this.currentLimit = limit; |
| this.ref.state.children.forEach(child => { |
| child.timeline.rerender(); |
| }); |
| return; |
| } |
| this.currentLimit = limit; |
| } |
| |
| this.ref.state.children.forEach(child => { |
| child.timeline.reload(); |
| }); |
| this.currentParams = params; |
| } |
| toString() { |
| return `<div ref="${this.ref}"></div>`; |
| } |
| notifyTimelinesRender() { |
| this.ref.state.children.forEach(child => { |
| child.timeline.notifyRerender(); |
| }); |
| } |
| } |
| |
| let view = new SearchView(); |
| const onLayoutChange = new EventStream(); |
| onLayoutChange.action(() => { |
| view.notifyTimelinesRender(); |
| }); |
| window.onpopstate = event => {view.reload();}; |
| window.onpushstate = event => {view.reload();}; |
| |
| let viewport = null; |
| DOM.inject( |
| document.getElementById('app'), |
| `${ToolTip} |
| ${InvestigateDrawer} |
| ${Drawer([ |
| Legend(() => { |
| view.ref.state.children.forEach((child) => { |
| child.timeline.update(); |
| }); |
| }, false), |
| LimitSlider(() => {view.reload()}), |
| BranchSelector(() => { |
| CommitBank.reload(); |
| view.reload(); |
| }), |
| ConfigurationSelectors(() => {view.reload()}), |
| ], () => onLayoutChange.add())} |
| |
| <div class="main right-sidebar under-topbar-with-actions"> |
| <div class="content" ref=${REF.createRef({ |
| onElementMount: (element) => { |
| view.ref.state.children.forEach((child) => { |
| child.timeline.viewport = element; |
| }); |
| viewport = element; |
| } |
| })}> |
| ${SearchBar(function () { |
| const splitURL = document.URL.split('?'); |
| let params = queryToParams(splitURL[1]); |
| if (!params.suite) |
| params.suite = []; |
| if (!params.test) |
| params.test = []; |
| |
| for (let i = 0; i < arguments.length; i++) { |
| let needToAdd = true; |
| if (!needToAdd) |
| continue; |
| |
| let child = { |
| suite: arguments[i].suite, |
| test: arguments[i].test, |
| timeline: new TimelineFromEndpoint(`api/results/${arguments[i].suite}/${arguments[i].test}`, arguments[i].suite, arguments[i].test, viewport), |
| } |
| |
| view.ref.setState({prepending: [child]}); |
| params.suite.push(child.suite); |
| params.test.push(child.test); |
| } |
| const queryString = paramsToQuery(params); |
| window.history.pushState(queryString, '', splitURL[0] + '?' + queryString); |
| }, SUITES)} |
| ${view} |
| </div> |
| </div>` |
| ); |
| |
| </script> |
| |
| {% endblock %} |
| {% block content %} |
| |
| <div id="app"> |
| </div> |
| |
| {% endblock %} |