results.webkit.org: Move legend into sidebar
[WebKit-https.git] / Tools / resultsdbpy / resultsdbpy / view / static / js / drawer.js
1 // Copyright (C) 2019 Apple Inc. All rights reserved.
2 //
3 // Redistribution and use in source and binary forms, with or without
4 // modification, are permitted provided that the following conditions
5 // are met:
6 // 1. Redistributions of source code must retain the above copyright
7 //    notice, this list of conditions and the following disclaimer.
8 // 2. Redistributions in binary form must reproduce the above copyright
9 //    notice, this list of conditions and the following disclaimer in the
10 //    documentation and/or other materials provided with the distribution.
11 //
12 // THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS "AS IS"
13 // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
14 // THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
15 // PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
16 // BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
17 // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
18 // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
19 // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
20 // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
21 // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
22 // THE POSSIBILITY OF SUCH DAMAGE.
23
24 import {DOM, REF} from '/library/js/Ref.js';
25 import {QueryModifier} from '/assets/js/common.js';
26 import {Configuration} from '/assets/js/configuration.js'
27
28 function setEnableRecursive(element, state) {
29     element.disabled = !state;
30     if (!state)
31         element.classList.add("disabled");
32     else
33         element.classList.remove("disabled");
34
35     for (let node of element.children)
36         setEnableRecursive(node, state);
37 }
38
39 function Drawer(controls = [], onCollapseChange) {
40     const HIDDEN = false;
41     const VISIBLE = true;
42     let drawerState = VISIBLE;
43     let main = null;
44
45     const sidebarControl = document.getElementsByClassName('mobile-sidebar-control')[0];
46     sidebarControl.classList.add('display');
47
48     const drawerRef = REF.createRef({
49         state: drawerState,
50         onStateUpdate: (element, state) => {
51             if (state) {
52                 element.classList.remove("hidden");
53                 if (main)
54                     main.classList.remove("hidden");
55             } else {
56                 element.classList.add("hidden");
57                 if (main)
58                     main.classList.add("hidden");
59             }
60
61             for (let node of element.children) {
62                 if (node.classList.contains("list"))
63                     setEnableRecursive(node, state);
64             }
65             
66             if (onCollapseChange)
67                 onCollapseChange();
68         },
69         onElementMount: (element) => {
70             let candidates = document.getElementsByClassName("main");
71             if (candidates.length)
72                 main = candidates[0];
73
74             sidebarControl.onclick = () => {
75                 if (element.style.display)
76                     element.style.display = null;
77                 else {
78                     for (let node of element.children) {
79                         if (node.classList.contains("list"))
80                             setEnableRecursive(node, true);
81                     }
82                     element.style.display = 'block';
83                 }
84             }
85         }
86     });
87
88     const drawerControllerRef = REF.createRef({
89         state: drawerState,
90         onStateUpdate: (element, state) => {
91             if (state) {
92                 element.innerHTML = 'Collapse &gt';
93                 element.style.textAlign = 'center';
94             }
95             else{
96                 element.innerHTML = '&lt';
97                 element.style.textAlign = 'left';
98             }
99         },
100         onElementMount: (element) => {
101             element.onclick = () => {
102                 drawerState = !drawerState;
103                 drawerRef.setState(drawerState);
104                 drawerControllerRef.setState(drawerState);
105             }
106         }
107     });
108
109     return `<div class="sidebar right under-topbar-with-actions unselectable" ref="${drawerRef}">
110             <button class="button desktop-control" ref="${drawerControllerRef}" style="width:96%; margin: 10px 2% 10px 2%;"></button>
111             ${controls.map(control => {
112                 return `<div class="list">
113                         <div class="item">${control}</div>
114                     </div>`;
115                 }).join('')}
116         </div>`;
117 }
118
119 function BranchSelector(callback) {
120     const defaultBranches = new Set(['trunk', 'master']);
121     const defaultBranchKey = [...defaultBranches].sort().join('/');
122     const branchModifier = new QueryModifier('branch');
123
124     let ref = REF.createRef({
125         state: [defaultBranchKey],
126         onElementMount: (element) => {
127             element.onchange = () => {
128                 let branch = element.value;
129                 if (branch === defaultBranchKey)
130                     branchModifier.remove();
131                 else
132                     branchModifier.replace(branch);
133                 callback();
134             }
135         },
136         onStateUpdate: (element, state) => {
137             const branchQuery = branchModifier.current().length ? branchModifier.current()[branchModifier.current().length -1]:null;
138             element.innerHTML = state.map(branch => {
139                 if (!branch)
140                     return '';
141                 if (branch === branchQuery)
142                     return `<option selected value="${branch}">${branch}</option>`;
143                 return `<option value="${branch}">${branch}</option>`;
144             }).join('');
145             element.parentElement.parentElement.parentElement.style.display = state.length > 1 ? null : 'none';
146         },
147     });
148
149     fetch('api/commits/branches').then(response => {
150         response.json().then(json => {
151             let branchNames = new Set();
152             Object.keys(json).forEach(repo => {
153                 json[repo].forEach(branch => {
154                     if (!defaultBranches.has(branch))
155                         branchNames.add(branch);
156                 });
157             });
158             branchNames = [...branchNames];
159             branchNames.sort();
160             branchNames.splice(0, 0, defaultBranchKey);
161             ref.setState(branchNames);
162         });
163     }).catch(error => {
164         // If the load fails, log the error and continue
165         console.error(JSON.stringify(error, null, 4));
166     });
167
168     return `<div class="input">
169             <select required ref="${ref}"></select>
170             <label>Branch</label>
171         </div>`;
172 }
173
174 function LimitSlider(callback, max = 10000, defaultValue = 1000) {
175     const limitModifier = new QueryModifier('limit');
176     const startingValue = limitModifier.current().length ? limitModifier.current()[limitModifier.current().length -1]:defaultValue;
177
178     var numberRef = null;
179     var sliderRef = null;
180
181     numberRef = REF.createRef({
182         state: startingValue,
183         onElementMount: (element) => {
184             element.onchange = () => {
185                 limitModifier.replace(element.value);
186                 sliderRef.setState(parseInt(element.value, 10));
187                 callback();
188             }
189         },
190         onStateUpdate: (element, state) => {element.value = state;}
191     });
192     sliderRef = REF.createRef({
193         state: startingValue,
194         onElementMount: (element) => {
195             element.oninput = () => {
196                 const newLimit = Math.ceil(element.value);
197                 limitModifier.replace(newLimit);
198                 numberRef.setState(newLimit);
199                 callback();
200             }
201         },
202         onStateUpdate: (element, state) => {element.value = state;}
203     });
204     return `<div class="input">
205             <label style="color:var(--boldInverseColor)">Limit:</label>
206             <input type="range" min="0" max="${max}" ref="${sliderRef}" style="background:var(--boldInverseColor)"></input>
207             <input type="number" min="1" ref="${numberRef}" pattern="^[0-9]"></input>
208         </div>`
209 }
210
211 function ConfigurationSelectors(callback) {
212     let configurations = []
213     let configurationsDefinedCallbacks = [];
214     fetch('api/suites').then(response => {
215         response.json().then(json => {
216             json.forEach(pair => {
217                 const config = new Configuration(pair[0]);
218                 configurations.push(config);
219             });
220             configurationsDefinedCallbacks.forEach(callback => {callback();});
221         });
222     }).catch(error => {
223         // If the load fails, log the error and continue
224         console.error(JSON.stringify(error, null, 4));
225     });
226
227     const elements = [
228         {'query': 'platform', 'name': 'Platform'},
229         {'query': 'version_name', 'name': 'Version Name'},
230         {'query': 'style', 'name': 'Style'},
231         {'query': 'model', 'name': 'Model'},
232         {'query': 'architecture', 'name': 'Architecture'},
233         {'query': 'flavor', 'name': 'Flavor'},
234     ];
235     return elements.map(details => {
236         const modifier = new QueryModifier(details.query);
237
238         let ref = REF.createRef({
239             state: configurations,
240             onStateUpdate: (element, state) => {
241                 let candidates = new Set();
242                 state.forEach(configuration => {
243                     if (configuration[details.query] == null)
244                         return;
245                     candidates.add(configuration[details.query]);
246                 });
247                 modifier.current().forEach(param => {
248                     if (param === 'All')
249                         return;
250                     candidates.add(param);
251                 });
252
253                 let options = [...candidates];
254                 options.sort();
255                 options.unshift('All');
256
257                 let switches = {};
258
259                 let isExpanded = false;
260                 let expander = REF.createRef({
261                     onElementMount: (element) => {
262                         element.onclick = () => {
263                             isExpanded = !isExpanded;
264                             element.innerHTML = isExpanded ? '-' : '+';
265
266                             Array.from(element.parentNode.children).forEach(child => {
267                                 if (element == child)
268                                     return;
269                                 child.style.display = isExpanded ? 'block' : 'none';
270                             });
271                         }
272                     }
273                 });
274
275                 DOM.inject(element, `<a class="link-button text medium" ref="${expander}">+</a>
276                     ${details.name} <br>
277                     ${options.map(option => {
278                         let isChecked = false;
279                         if (option === 'All' && modifier.current().length === 0)
280                             isChecked = true;
281                         else if (option !== 'All' && modifier.current().indexOf(option) >= 0)
282                             isChecked = true;
283
284                         let swtch = REF.createRef({
285                             onElementMount: (element) => {
286                                 switches[option] = element;
287                                 element.onchange = () => {
288                                     if (option === 'All') {
289                                         if (!element.checked)
290                                             return;
291                                         Object.keys(switches).forEach(key => {
292                                             if (key === 'All')
293                                                 return;
294                                             switches[key].checked = false;
295                                         });
296                                         modifier.remove();
297                                     } else if (element.checked) {
298                                         switches['All'].checked = false;
299                                         modifier.append(option);
300                                     } else {
301                                         modifier.remove(option);
302                                         if (modifier.current().length === 0)
303                                             switches['All'].checked = true;
304                                     }
305                                     callback();
306                                 };
307                             },
308                         });
309
310                         return `<div class="input" ${isExpanded ? '' : `style="display: none;"`}>
311                                 <label>${option}</label>
312                                 <label class="switch">
313                                     <input type="checkbox"${isChecked ? ' checked': ''} ref="${swtch}">
314                                     <span class="slider"></span>
315                                 </label>
316                             </div>`;
317                     }).join('')}`);
318             },
319         });
320         configurationsDefinedCallbacks.push(() => {
321             ref.setState(configurations);
322         });
323
324         return `<div style="font-size: var(--smallSize);" ref="${ref}"></div>`;
325     }).join('')
326 }
327
328 export {Drawer, BranchSelector, ConfigurationSelectors, LimitSlider};