4b20ba45344f82f12789fbf1a4634b8a409a14f4
[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 Drawer(controls = []) {
29     const COLLAPSED = false;
30     const EXTENDED = true;
31     var drawerState = COLLAPSED;
32
33     const drawerRef = REF.createRef({
34         state: drawerState,
35         onStateUpdate: (element, state) => {
36             if (state)
37                 element.classList.add("display");
38             else
39                 element.classList.remove("display");
40         }
41     });
42     const drawerControllerRef = REF.createRef({
43         state: drawerState,
44         onStateUpdate: (element, state) => {
45             if (state) {
46                 element.classList.remove("collapsed");
47                 element.classList.add("extended");
48             } else {
49                 element.classList.remove("extended");
50                 element.classList.add("collapsed");
51             }
52         },
53         onElementMount: (element) => {
54             element.onclick = () => {
55                 drawerState = !drawerState;
56                 drawerRef.setState(drawerState);
57                 drawerControllerRef.setState(drawerState);
58             }
59         }
60     });
61
62     return `<div class="drawer left under-topbar-with-actions" ref="${drawerRef}">
63             ${controls.map(control => {
64                 return `<div class="list">
65                         <div class="item">${control}</div>
66                     </div>`;
67                 }).join('')}
68         </div>
69         <button class="drawer-control collapsed" ref="${drawerControllerRef}"><div></div></button>`;
70 }
71
72 function BranchSelector(callback) {
73     const defaultBranches = new Set(['trunk', 'master']);
74     const defaultBranchKey = [...defaultBranches].sort().join('/');
75     const branchModifier = new QueryModifier('branch');
76
77     let ref = REF.createRef({
78         state: [defaultBranchKey],
79         onElementMount: (element) => {
80             element.onchange = () => {
81                 let branch = element.value;
82                 if (branch === defaultBranchKey)
83                     branchModifier.remove();
84                 else
85                     branchModifier.replace(branch);
86                 callback();
87             }
88         },
89         onStateUpdate: (element, state) => {
90             const branchQuery = branchModifier.current().length ? branchModifier.current()[branchModifier.current().length -1]:null;
91             element.innerHTML = state.map(branch => {
92                 if (!branch)
93                     return '';
94                 if (branch === branchQuery)
95                     return `<option selected value="${branch}">${branch}</option>`;
96                 return `<option value="${branch}">${branch}</option>`;
97             }).join('');
98         },
99     });
100
101     fetch('api/commits/branches').then(response => {
102         response.json().then(json => {
103             let branchNames = new Set();
104             Object.keys(json).forEach(repo => {
105                 json[repo].forEach(branch => {
106                     if (!defaultBranches.has(branch))
107                         branchNames.add(branch);
108                 });
109             });
110             branchNames = [...branchNames];
111             branchNames.sort();
112             branchNames.splice(0, 0, defaultBranchKey);
113             ref.setState(branchNames);
114         });
115     }).catch(error => {
116         // If the load fails, log the error and continue
117         console.error(JSON.stringify(error, null, 4));
118     });
119
120     return `<div class="input">
121             <select required ref="${ref}"></select>
122             <label>Branch</label>
123         </div>`;
124 }
125
126 function LimitSlider(callback, max = 1000, defaultValue = 100) {
127     const maxRange = 1000;
128     const scale = (Math.log(max) - Math.log(1)) / maxRange;
129     const limitModifier = new QueryModifier('limit');
130     const startingValue = limitModifier.current().length ? limitModifier.current()[limitModifier.current().length -1]:defaultValue;
131
132     var numberRef = null;
133     var sliderRef = null;
134
135     numberRef = REF.createRef({
136         state: startingValue,
137         onElementMount: (element) => {
138             element.onchange = () => {
139                 limitModifier.replace(element.value);
140                 sliderRef.setState(parseInt(element.value, 10));
141                 callback();
142             }
143         },
144         onStateUpdate: (element, state) => {element.value = state;}
145     });
146     sliderRef = REF.createRef({
147         state: startingValue,
148         onElementMount: (element) => {
149             element.oninput = () => {
150                 const newLimit = Math.ceil(Math.exp(Math.log(1) + scale * element.value));
151                 limitModifier.replace(newLimit);
152                 numberRef.setState(newLimit);
153                 callback();
154             }
155         },
156         onStateUpdate: (element, state) => {element.value = (Math.log(state) - Math.log(1)) / scale;}
157     });
158     return `<div class="input">
159             <label style="color:var(--boldInverseColor)">Limit:</label>
160             <input type="range" min="0" max="${maxRange}" ref="${sliderRef}" style="background:var(--boldInverseColor)"></input>
161             <input type="number" min="1" ref="${numberRef}" pattern="^[0-9]"></input>
162         </div>`
163 }
164
165 function ConfigurationSelectors(callback) {
166     let configurations = []
167     let configurationsDefinedCallbacks = [];
168     fetch('api/suites').then(response => {
169         response.json().then(json => {
170             json.forEach(pair => {
171                 const config = new Configuration(pair[0]);
172                 configurations.push(config);
173             });
174             configurationsDefinedCallbacks.forEach(callback => {callback();});
175         });
176     }).catch(error => {
177         // If the load fails, log the error and continue
178         console.error(JSON.stringify(error, null, 4));
179     });
180
181     const elements = [
182         {'query': 'platform', 'name': 'Platform'},
183         {'query': 'version_name', 'name': 'Version Name'},
184         {'query': 'style', 'name': 'Style'},
185         {'query': 'model', 'name': 'Model'},
186         {'query': 'architecture', 'name': 'Architecture'},
187         {'query': 'flavor', 'name': 'Flavor'},
188     ];
189     return elements.map(details => {
190         const modifier = new QueryModifier(details.query);
191
192         let ref = REF.createRef({
193             state: configurations,
194             onStateUpdate: (element, state) => {
195                 let candidates = new Set();
196                 state.forEach(configuration => {
197                     if (configuration[details.query] == null)
198                         return;
199                     candidates.add(configuration[details.query]);
200                 });
201                 modifier.current().forEach(param => {
202                     if (param === 'All')
203                         return;
204                     candidates.add(param);
205                 });
206
207                 let options = [...candidates];
208                 options.sort();
209                 options.unshift('All');
210
211                 let switches = {};
212
213                 let isExpanded = false;
214                 let expander = REF.createRef({
215                     onElementMount: (element) => {
216                         element.onclick = () => {
217                             isExpanded = !isExpanded;
218                             element.innerHTML = isExpanded ? '-' : '+';
219
220                             Array.from(element.parentNode.children).forEach(child => {
221                                 if (element == child)
222                                     return;
223                                 child.style.display = isExpanded ? 'block' : 'none';
224                             });
225                         }
226                     }
227                 });
228
229                 DOM.inject(element, `<a style="cursor: pointer;" class="text medium" ref="${expander}">+</a>
230                     ${details.name} <br>
231                     ${options.map(option => {
232                         let isChecked = false;
233                         if (option === 'All' && modifier.current().length === 0)
234                             isChecked = true;
235                         else if (option !== 'All' && modifier.current().indexOf(option) >= 0)
236                             isChecked = true;
237
238                         let swtch = REF.createRef({
239                             onElementMount: (element) => {
240                                 switches[option] = element;
241                                 element.onchange = () => {
242                                     if (option === 'All') {
243                                         if (!element.checked)
244                                             return;
245                                         Object.keys(switches).forEach(key => {
246                                             if (key === 'All')
247                                                 return;
248                                             switches[key].checked = false;
249                                         });
250                                         modifier.remove();
251                                     } else if (element.checked) {
252                                         switches['All'].checked = false;
253                                         modifier.append(option);
254                                     } else {
255                                         modifier.remove(option);
256                                         if (modifier.current().length === 0)
257                                             switches['All'].checked = true;
258                                     }
259                                     callback();
260                                 };
261                             },
262                         });
263
264                         return `<div class="input" ${isExpanded ? '' : `style="display: none;"`}>
265                                 <label>${option}</label>
266                                 <label class="switch">
267                                     <input type="checkbox"${isChecked ? ' checked': ''} ref="${swtch}">
268                                     <span class="slider"></span>
269                                 </label>
270                             </div>`;
271                     }).join('')}`);
272             },
273         });
274         configurationsDefinedCallbacks.push(() => {
275             ref.setState(configurations);
276         });
277
278         return `<div ref="${ref}"></div>`;
279     }).join('')
280 }
281
282 export {Drawer, BranchSelector, ConfigurationSelectors, LimitSlider};