6cabda4455229b9f007adfa9c9987ad082f8fd25
[WebKit-https.git] / Tools / resultsdbpy / resultsdbpy / view / templates / search.html
1 <!--
2  Copyright (C) 2019 Apple Inc. All rights reserved.
3
4  Redistribution and use in source and binary forms, with or without
5  modification, are permitted provided that the following conditions
6  are met:
7  1. Redistributions of source code must retain the above copyright
8     notice, this list of conditions and the following disclaimer.
9  2. Redistributions in binary form must reproduce the above copyright
10     notice, this list of conditions and the following disclaimer in the
11     documentation and/or other materials provided with the distribution.
12
13  THIS SOFTWARE IS PROVIDED BY APPLE INC. "AS IS" AND ANY
14  EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
17  CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
18  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
19  PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
20  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
21  OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
24 -->
25
26 {% extends "base.html" %}
27 {% block head %}
28 <link rel="stylesheet" type="text/css" href="assets/css/search.css">
29 <link rel="stylesheet" type="text/css" href="assets/css/timeline.css">
30 <link rel="stylesheet" type="text/css" href="assets/css/tooltip.css">
31
32 <script type="module">
33 import {CommitBank} from '/assets/js/commit.js';
34 import {deepCompare, ErrorDisplay, queryToParams, paramsToQuery} from '/assets/js/common.js';
35 import {Configuration} from '/assets/js/configuration.js';
36 import {Drawer, BranchSelector, ConfigurationSelectors, LimitSlider} from '/assets/js/drawer.js';
37 import {SearchBar} from '/assets/js/search.js';
38 import {Legend, TimelineFromEndpoint} from '/assets/js/timeline.js';
39 import {ToolTip} from '/assets/js/tooltip.js';
40 import {DOM, REF, EventStream} from '/library/js/Ref.js';
41
42 const DEFAULT_LIMIT = 100;
43 const SUITES = JSON.parse('{{ suites|safe }}');
44
45 class SearchView {
46     constructor() {    
47         this.currentParams = queryToParams(document.URL.split('?')[1]);
48         this.currentLimit = this.currentParams.limit ? parseInt(this.currentParams.limit[this.currentParams.limit.length - 1]) : DEFAULT_LIMIT;
49         delete this.currentParams.limit;
50
51         let state = {children: [], prepending: []};
52         if (this.currentParams.test && (!this.currentParams.suite || this.currentParams.test.length != this.currentParams.suite.length))
53             state = {error: "Query Error", description: 'Must have the same number of suites and tests in query'};
54         else if (this.currentParams.test) {
55             for (let i = 0; i < this.currentParams.test.length; ++i) {
56                 state.children.push({
57                     suite: this.currentParams.suite[i],
58                     test: this.currentParams.test[i],
59                     timeline: new TimelineFromEndpoint(`api/results/${this.currentParams.suite[i]}/${this.currentParams.test[i]}`, this.currentParams.suite[i]),
60                 });
61             }
62         }
63
64         delete this.currentParams.suite;
65         delete this.currentParams.test;
66
67         const self = this;
68         this.ref = REF.createRef({
69             state: state,
70             onStateUpdate: (element, diff, state) => {
71                 if (diff.error) {
72                     DOM.inject(element, ErrorDisplay(diff));
73                     return;
74                 }
75
76                 function renderChild(child) {
77                     let exitRef = REF.createRef({
78                         onElementMount: element => {
79                             element.onclick = () => {
80                                 for (let i = 0; i < self.ref.state.children.length; ++i) {
81                                     if (child.test !== self.ref.state.children[i].test && child.suite !== self.ref.state.children[i].suite)
82                                         continue;
83                                     self.ref.state.children.splice(i, 1);
84                                     break;
85                                 }
86
87                                 const splitURL = document.URL.split('?');
88                                 let params = queryToParams(splitURL[1]);
89                                 if (!params.suite)
90                                     params.suite = [];
91                                 if (!params.test)
92                                     params.test = [];
93                                 for (let i = 0; i < params.suite.length; ++i) {
94                                     // This shouldn't be possible, just keep the current params if it happens.
95                                     if (i >= params.test.length)
96                                         break;
97                                     if (params.suite[i] !== child.suite && params.test[i] !== child.test)
98                                         continue;
99                                     params.suite.splice(i, 1);
100                                     params.test.splice(i, 1);
101                                     break;
102                                 }
103                                 if (params.suite.length === 0)
104                                     delete params.suite;
105                                 if (params.test.length === 0)
106                                     delete params.test;
107
108                                 const queryString = paramsToQuery(params);
109                                 window.history.pushState(queryString, '', splitURL[0] + '?' + queryString);
110
111                                 const section = element.parentElement.parentElement.parentElement.parentElement;
112                                 section.parentElement.removeChild(section);
113                             };
114                         },
115                     });
116
117                     return `<div class="section">
118                             <div class="header">
119                                 <div class="title" style="font-size:var(--smallSize);word-break:break-word;">${child.test} (${child.suite})</div>
120                                 <div class="actions">
121                                 <div class="list">
122                                     <a class="item" ref="${exitRef}">X</a>
123                                 </div>
124                                 </div>
125                             </div>
126                             ${child.timeline}
127                         </div>`;
128                 }
129
130                 if (diff.children)
131                     DOM.inject(element, Legend(() => {
132                         self.ref.state.children.forEach((child) => {
133                             child.timeline.update();
134                         });
135                     }, false) + diff.children.map(renderChild).join(''));
136                 if (diff.prepending) {
137                     diff.prepending.forEach(child => {
138                         DOM.after(element.firstElementChild, renderChild(child));
139                         this.ref.state.children.unshift(child);
140                     });
141                 } 
142             }
143         });
144     }
145     reload() {
146         let params = queryToParams(document.URL.split('?')[1]);
147         let limit = params.limit ? parseInt(params.limit[params.limit.length - 1]) : DEFAULT_LIMIT;
148
149         // Try and determine if the tests we're displaying have changed.
150         if (params.test && (!params.suite || params.test.length != params.suite.length))
151             this.ref.setState({error: "Query Error", description: 'Must have the same number of suites and tests in query'});
152         else if (params.test) {
153             let needReRender = params.test.length != this.ref.state.children.length;
154             let newChildren = [];
155
156             for (let i = 0; i < params.test.length; ++i) {
157                 let didFind = false;
158                 for (let search = 0; search < this.ref.state.children.length; ++search) {
159                     if (this.ref.state.children[search].suite === params.suite[i] && this.ref.state.children[search].test === params.test[i]) {
160                         newChildren.push(this.ref.state.children[search]);
161                         didFind = true;
162                         this.ref.state.children.splice(search, 1);
163                         break;
164                     } else
165                         this.ref.state.children[search].timeline.teardown();
166                     needReRender = true;
167                 }
168                 if (!didFind) {
169                     needReRender = true;
170
171                     newChildren.push({
172                         suite: params.suite[i],
173                         test: params.test[i],
174                         timeline: new TimelineFromEndpoint(`api/results/${params.suite[i]}/${params.test[i]}`),
175                     });
176                 }
177             }
178
179             if (needReRender)
180                 this.ref.setState({children: newChildren});
181             else
182                 this.ref.state.children = newChildren;
183         }
184
185         delete params.limit;
186         delete params.suite;
187         delete params.test;
188
189         if (deepCompare(params, this.currentParams)) {
190             if (limit === this.currentLimit)
191                 return;
192             if (limit < this.currentLimit) {
193                 this.currentLimit = limit;
194                 this.ref.state.children.forEach(child => {
195                     child.timeline.rerender();
196                 });
197                 return;
198             }
199             this.currentLimit = limit;
200         }
201
202         this.ref.state.children.forEach(child => {
203             child.timeline.reload();
204         });
205         this.currentParams = params;
206     }
207     toString() {
208         return `<div ref="${this.ref}"></div>`;
209     }
210     notifiyTimelinesRender() {
211         this.ref.state.children.forEach(child => {
212             child.timeline.notifiyRerender();
213         });
214     }
215 }
216
217 let view = new SearchView();
218 const onLayoutChange = new EventStream();
219 onLayoutChange.action(() => {
220     view.notifiyTimelinesRender();
221 });
222 window.onpopstate = event => {view.reload();};
223 window.onpushstate = event => {view.reload();};
224
225
226 DOM.inject(
227     document.getElementById('app'),
228     `${ToolTip}
229     ${Drawer([
230         LimitSlider(() => {view.reload()}),
231         BranchSelector(() => {
232             CommitBank.reload();
233             view.reload();
234         }),
235         ConfigurationSelectors(() => {view.reload()}),
236     ], () => onLayoutChange.add())}
237
238     <div class="main left under-topbar-with-actions">
239         <div class="content">
240             ${SearchBar(function () {
241                 const splitURL = document.URL.split('?');
242                 let params = queryToParams(splitURL[1]);
243                 if (!params.suite)
244                     params.suite = [];
245                 if (!params.test)
246                     params.test = [];
247
248                 for (let i = 0; i < arguments.length; i++) {
249                     let needToAdd = true;
250                     if (!needToAdd)
251                         continue;
252
253                     let child = {
254                         suite: arguments[i].suite,
255                         test: arguments[i].test,
256                         timeline: new TimelineFromEndpoint(`api/results/${arguments[i].suite}/${arguments[i].test}`),
257                     }
258
259                     view.ref.setState({prepending: [child]});
260                     params.suite.push(child.suite);
261                     params.test.push(child.test);
262                 }
263                 const queryString = paramsToQuery(params);
264                 window.history.pushState(queryString, '', splitURL[0] + '?' + queryString);
265             }, SUITES)}
266             ${view}
267         </div>
268     </div>`
269 );
270
271 </script>
272
273 {% endblock %}
274 {% block content %}
275
276 <div id="app">
277 </div>
278
279 {% endblock %}