5faccea95b4823ce61438386477401d21f382198
[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                     element.style.display = 'block';
79             }
80         }
81     });
82
83     const drawerControllerRef = REF.createRef({
84         state: drawerState,
85         onStateUpdate: (element, state) => {
86             if (state) {
87                 element.innerHTML = 'Collapse &gt';
88                 element.style.textAlign = 'center';
89             }
90             else{
91                 element.innerHTML = '&lt';
92                 element.style.textAlign = 'left';
93             }
94         },
95         onElementMount: (element) => {
96             element.onclick = () => {
97                 drawerState = !drawerState;
98                 drawerRef.setState(drawerState);
99                 drawerControllerRef.setState(drawerState);
100             }
101         }
102     });
103
104     return `<div class="sidebar right under-topbar-with-actions unselectable" ref="${drawerRef}">
105             <button class="button desktop-control" ref="${drawerControllerRef}" style="width:96%; margin: 10px 2% 10px 2%;"></button>
106             ${controls.map(control => {
107                 return `<div class="list">
108                         <div class="item">${control}</div>
109                     </div>`;
110                 }).join('')}
111         </div>`;
112 }
113
114 function BranchSelector(callback) {
115     const defaultBranches = new Set(['trunk', 'master']);
116     const defaultBranchKey = [...defaultBranches].sort().join('/');
117     const branchModifier = new QueryModifier('branch');
118
119     let ref = REF.createRef({
120         state: [defaultBranchKey],
121         onElementMount: (element) => {
122             element.onchange = () => {
123                 let branch = element.value;
124                 if (branch === defaultBranchKey)
125                     branchModifier.remove();
126                 else
127                     branchModifier.replace(branch);
128                 callback();
129             }
130         },
131         onStateUpdate: (element, state) => {
132             const branchQuery = branchModifier.current().length ? branchModifier.current()[branchModifier.current().length -1]:null;
133             element.innerHTML = state.map(branch => {
134                 if (!branch)
135                     return '';
136                 if (branch === branchQuery)
137                     return `<option selected value="${branch}">${branch}</option>`;
138                 return `<option value="${branch}">${branch}</option>`;
139             }).join('');
140             element.parentElement.parentElement.parentElement.style.display = state.length > 1 ? null : 'none';
141         },
142     });
143
144     fetch('api/commits/branches').then(response => {
145         response.json().then(json => {
146             let branchNames = new Set();
147             Object.keys(json).forEach(repo => {
148                 json[repo].forEach(branch => {
149                     if (!defaultBranches.has(branch))
150                         branchNames.add(branch);
151                 });
152             });
153             branchNames = [...branchNames];
154             branchNames.sort();
155             branchNames.splice(0, 0, defaultBranchKey);
156             ref.setState(branchNames);
157         });
158     }).catch(error => {
159         // If the load fails, log the error and continue
160         console.error(JSON.stringify(error, null, 4));
161     });
162
163     return `<div class="input">
164             <select required ref="${ref}"></select>
165             <label>Branch</label>
166         </div>`;
167 }
168
169 function LimitSlider(callback, max = 10000, defaultValue = 1000) {
170     const limitModifier = new QueryModifier('limit');
171     const startingValue = limitModifier.current().length ? limitModifier.current()[limitModifier.current().length -1]:defaultValue;
172
173     var numberRef = null;
174     var sliderRef = null;
175
176     numberRef = REF.createRef({
177         state: startingValue,
178         onElementMount: (element) => {
179             element.onchange = () => {
180                 limitModifier.replace(element.value);
181                 sliderRef.setState(parseInt(element.value, 10));
182                 callback();
183             }
184         },
185         onStateUpdate: (element, state) => {element.value = state;}
186     });
187     sliderRef = REF.createRef({
188         state: startingValue,
189         onElementMount: (element) => {
190             element.oninput = () => {
191                 const newLimit = Math.ceil(element.value);
192                 limitModifier.replace(newLimit);
193                 numberRef.setState(newLimit);
194                 callback();
195             }
196         },
197         onStateUpdate: (element, state) => {element.value = state;}
198     });
199     return `<div class="input">
200             <label style="color:var(--boldInverseColor)">Limit:</label>
201             <input type="range" min="0" max="${max}" ref="${sliderRef}" style="background:var(--boldInverseColor)"></input>
202             <input type="number" min="1" ref="${numberRef}" pattern="^[0-9]"></input>
203         </div>`
204 }
205
206 function ConfigurationSelectors(callback) {
207     let configurations = []
208     let configurationsDefinedCallbacks = [];
209     fetch('api/suites').then(response => {
210         response.json().then(json => {
211             json.forEach(pair => {
212                 const config = new Configuration(pair[0]);
213                 configurations.push(config);
214             });
215             configurationsDefinedCallbacks.forEach(callback => {callback();});
216         });
217     }).catch(error => {
218         // If the load fails, log the error and continue
219         console.error(JSON.stringify(error, null, 4));
220     });
221
222     const elements = [
223         {'query': 'platform', 'name': 'Platform'},
224         {'query': 'version_name', 'name': 'Version Name'},
225         {'query': 'style', 'name': 'Style'},
226         {'query': 'model', 'name': 'Model'},
227         {'query': 'architecture', 'name': 'Architecture'},
228         {'query': 'flavor', 'name': 'Flavor'},
229     ];
230     return elements.map(details => {
231         const modifier = new QueryModifier(details.query);
232
233         let ref = REF.createRef({
234             state: configurations,
235             onStateUpdate: (element, state) => {
236                 let candidates = new Set();
237                 state.forEach(configuration => {
238                     if (configuration[details.query] == null)
239                         return;
240                     candidates.add(configuration[details.query]);
241                 });
242                 modifier.current().forEach(param => {
243                     if (param === 'All')
244                         return;
245                     candidates.add(param);
246                 });
247
248                 let options = [...candidates];
249                 options.sort();
250                 options.unshift('All');
251
252                 let switches = {};
253
254                 let isExpanded = false;
255                 let expander = REF.createRef({
256                     onElementMount: (element) => {
257                         element.onclick = () => {
258                             isExpanded = !isExpanded;
259                             element.innerHTML = isExpanded ? '-' : '+';
260
261                             Array.from(element.parentNode.children).forEach(child => {
262                                 if (element == child)
263                                     return;
264                                 child.style.display = isExpanded ? 'block' : 'none';
265                             });
266                         }
267                     }
268                 });
269
270                 DOM.inject(element, `<a class="link-button text medium" ref="${expander}">+</a>
271                     ${details.name} <br>
272                     ${options.map(option => {
273                         let isChecked = false;
274                         if (option === 'All' && modifier.current().length === 0)
275                             isChecked = true;
276                         else if (option !== 'All' && modifier.current().indexOf(option) >= 0)
277                             isChecked = true;
278
279                         let swtch = REF.createRef({
280                             onElementMount: (element) => {
281                                 switches[option] = element;
282                                 element.onchange = () => {
283                                     if (option === 'All') {
284                                         if (!element.checked)
285                                             return;
286                                         Object.keys(switches).forEach(key => {
287                                             if (key === 'All')
288                                                 return;
289                                             switches[key].checked = false;
290                                         });
291                                         modifier.remove();
292                                     } else if (element.checked) {
293                                         switches['All'].checked = false;
294                                         modifier.append(option);
295                                     } else {
296                                         modifier.remove(option);
297                                         if (modifier.current().length === 0)
298                                             switches['All'].checked = true;
299                                     }
300                                     callback();
301                                 };
302                             },
303                         });
304
305                         return `<div class="input" ${isExpanded ? '' : `style="display: none;"`}>
306                                 <label>${option}</label>
307                                 <label class="switch">
308                                     <input type="checkbox"${isChecked ? ' checked': ''} ref="${swtch}">
309                                     <span class="slider"></span>
310                                 </label>
311                             </div>`;
312                     }).join('')}`);
313             },
314         });
315         configurationsDefinedCallbacks.push(() => {
316             ref.setState(configurations);
317         });
318
319         return `<div style="font-size: var(--smallSize);" ref="${ref}"></div>`;
320     }).join('')
321 }
322
323 export {Drawer, BranchSelector, ConfigurationSelectors, LimitSlider};