results.webkit.org: Move legend into sidebar
[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, diff.children.map(renderChild).join(''));
132                 if (diff.prepending) {
133                     diff.prepending.forEach(child => {
134                         DOM.after(element.firstElementChild, renderChild(child));
135                         this.ref.state.children.unshift(child);
136                     });
137                 } 
138             }
139         });
140     }
141     reload() {
142         let params = queryToParams(document.URL.split('?')[1]);
143         let limit = params.limit ? parseInt(params.limit[params.limit.length - 1]) : DEFAULT_LIMIT;
144
145         // Try and determine if the tests we're displaying have changed.
146         if (params.test && (!params.suite || params.test.length != params.suite.length))
147             this.ref.setState({error: "Query Error", description: 'Must have the same number of suites and tests in query'});
148         else if (params.test) {
149             let needReRender = params.test.length != this.ref.state.children.length;
150             let newChildren = [];
151
152             for (let i = 0; i < params.test.length; ++i) {
153                 let didFind = false;
154                 for (let search = 0; search < this.ref.state.children.length; ++search) {
155                     if (this.ref.state.children[search].suite === params.suite[i] && this.ref.state.children[search].test === params.test[i]) {
156                         newChildren.push(this.ref.state.children[search]);
157                         didFind = true;
158                         this.ref.state.children.splice(search, 1);
159                         break;
160                     } else
161                         this.ref.state.children[search].timeline.teardown();
162                     needReRender = true;
163                 }
164                 if (!didFind) {
165                     needReRender = true;
166
167                     newChildren.push({
168                         suite: params.suite[i],
169                         test: params.test[i],
170                         timeline: new TimelineFromEndpoint(`api/results/${params.suite[i]}/${params.test[i]}`),
171                     });
172                 }
173             }
174
175             if (needReRender)
176                 this.ref.setState({children: newChildren});
177             else
178                 this.ref.state.children = newChildren;
179         }
180
181         delete params.limit;
182         delete params.suite;
183         delete params.test;
184
185         if (deepCompare(params, this.currentParams)) {
186             if (limit === this.currentLimit)
187                 return;
188             if (limit < this.currentLimit) {
189                 this.currentLimit = limit;
190                 this.ref.state.children.forEach(child => {
191                     child.timeline.rerender();
192                 });
193                 return;
194             }
195             this.currentLimit = limit;
196         }
197
198         this.ref.state.children.forEach(child => {
199             child.timeline.reload();
200         });
201         this.currentParams = params;
202     }
203     toString() {
204         return `<div ref="${this.ref}"></div>`;
205     }
206     notifiyTimelinesRender() {
207         this.ref.state.children.forEach(child => {
208             child.timeline.notifiyRerender();
209         });
210     }
211 }
212
213 let view = new SearchView();
214 const onLayoutChange = new EventStream();
215 onLayoutChange.action(() => {
216     view.notifiyTimelinesRender();
217 });
218 window.onpopstate = event => {view.reload();};
219 window.onpushstate = event => {view.reload();};
220
221
222 DOM.inject(
223     document.getElementById('app'),
224     `${ToolTip}
225     ${Drawer([
226         Legend(() => {
227             view.children.forEach((child) => {
228                 child.timeline.update();
229             });
230         }, false),
231         LimitSlider(() => {view.reload()}),
232         BranchSelector(() => {
233             CommitBank.reload();
234             view.reload();
235         }),
236         ConfigurationSelectors(() => {view.reload()}),
237     ], () => onLayoutChange.add())}
238
239     <div class="main left under-topbar-with-actions">
240         <div class="content">
241             ${SearchBar(function () {
242                 const splitURL = document.URL.split('?');
243                 let params = queryToParams(splitURL[1]);
244                 if (!params.suite)
245                     params.suite = [];
246                 if (!params.test)
247                     params.test = [];
248
249                 for (let i = 0; i < arguments.length; i++) {
250                     let needToAdd = true;
251                     if (!needToAdd)
252                         continue;
253
254                     let child = {
255                         suite: arguments[i].suite,
256                         test: arguments[i].test,
257                         timeline: new TimelineFromEndpoint(`api/results/${arguments[i].suite}/${arguments[i].test}`),
258                     }
259
260                     view.ref.setState({prepending: [child]});
261                     params.suite.push(child.suite);
262                     params.test.push(child.test);
263                 }
264                 const queryString = paramsToQuery(params);
265                 window.history.pushState(queryString, '', splitURL[0] + '?' + queryString);
266             }, SUITES)}
267             ${view}
268         </div>
269     </div>`
270 );
271
272 </script>
273
274 {% endblock %}
275 {% block content %}
276
277 <div id="app">
278 </div>
279
280 {% endblock %}