show asu build log for build success
[web/firmware-selector-openwrt-org.git] / index.js
1
2 var current_model = {};
3
4 function $(id) {
5 return document.getElementById(id);
6 }
7
8 function show(id) {
9 $(id).style.display = 'block';
10 }
11
12 function hide(id) {
13 $(id).style.display = 'none';
14 }
15
16 function split(str) {
17 return str.match(/[^\s,]+/g) || [];
18 }
19
20 function get_model_titles(titles) {
21 return titles.map(e => {
22 if (e.title) {
23 return e.title;
24 } else {
25 return ((e.vendor || '') + ' ' + (e.model || '') + ' ' + (e.variant || '')).trim();
26 }
27 }).join(' / ');
28 }
29
30 function build_asa_request() {
31 if (!current_model || !current_model.id) {
32 alert('bad profile');
33 return;
34 }
35
36 function showStatus(message, url) {
37 show('buildstatus');
38 var tr = message.startsWith('tr-') ? message : '';
39 if (url) {
40 $('buildstatus').innerHTML = '<a href="' + url + '" class="' + tr + '">' + message + '</a>';
41 } else {
42 $('buildstatus').innerHTML = '<span class="' + tr + '"></span>';
43 }
44 translate();
45 }
46
47 // hide image view
48 updateImages();
49
50 show('buildspinner');
51 showStatus('tr-request-image');
52
53 var request_data = {
54 'target': current_model.target,
55 'profile': current_model.id,
56 'packages': split($('packages').value),
57 'version': $('versions').value
58 }
59
60 fetch(config.asu_url + '/api/build', {
61 method: 'POST',
62 headers: { 'Content-Type': 'application/json' },
63 body: JSON.stringify(request_data)
64 })
65 .then(response => {
66 switch (response.status) {
67 case 200:
68 hide('buildspinner');
69 showStatus('tr-build-successful');
70
71 response.json()
72 .then(mobj => {
73 var download_url = config.asu_url + '/store/' + mobj.bin_dir;
74 showStatus('tr-build-successful', download_url + '/buildlog.txt');
75 updateImages(
76 mobj.version_number,
77 mobj.version_code,
78 mobj.build_at,
79 get_model_titles(mobj.titles),
80 download_url, mobj, true
81 );
82 });
83 break;
84 case 202:
85 showStatus('tr-check-again');
86 setTimeout(_ => { build_asa_request() }, 5000);
87 break;
88 case 400: // bad request
89 case 422: // bad package
90 case 500: // build failed
91 hide('buildspinner');
92 response.json()
93 .then(mobj => {
94 var message = mobj['message'] || 'tr-build-failed';
95 var url = mobj.buildlog ? (config.asu_url + '/store/' + mobj.bin_dir + '/buildlog.txt') : undefined;
96 showStatus(message, url);
97 })
98 break;
99 }
100 })
101 .catch(err => {
102 hide('buildspinner');
103 showStatus(err);
104 })
105 }
106
107 function setupSelectList(select, items, onselection) {
108 for (var i = 0; i < items.length; i += 1) {
109 var option = document.createElement('OPTION');
110 option.innerHTML = items[i];
111 select.appendChild(option);
112 }
113
114 select.addEventListener('change', e => {
115 onselection(items[select.selectedIndex]);
116 });
117
118 if (select.selectedIndex >= 0) {
119 onselection(items[select.selectedIndex]);
120 }
121 }
122
123 // Change the translation of the entire document
124 function translate() {
125 var mapping = translations[config.language];
126 for (var tr in mapping) {
127 Array.from(document.getElementsByClassName(tr))
128 .forEach(e => { e.innerText = mapping[tr]; })
129 }
130 }
131
132 function setupAutocompleteList(input, items, as_list, onbegin, onend) {
133 var currentFocus = -1;
134
135 // sort numbers and other characters separately
136 var collator = new Intl.Collator(undefined, {numeric: true, sensitivity: 'base'});
137
138 items.sort(collator.compare);
139
140 input.oninput = function(e) {
141 onbegin();
142
143 var offset = 0;
144 var value = this.value;
145 var value_list = [];
146
147 if (as_list) {
148 // automcomplete last text item
149 offset = this.value.lastIndexOf(' ') + 1;
150 value = this.value.substr(offset);
151 value_list = split(this.value.substr(0, offset));
152 }
153
154 // close any already open lists of autocompleted values
155 closeAllLists();
156
157 if (!value) {
158 return false;
159 }
160
161 // create a DIV element that will contain the items (values):
162 var list = document.createElement('DIV');
163 list.setAttribute('id', this.id + '-autocomplete-list');
164 list.setAttribute('class', 'autocomplete-items');
165 // append the DIV element as a child of the autocomplete container:
166 this.parentNode.appendChild(list);
167
168 var c = 0;
169 for (var i = 0; i < items.length; i += 1) {
170 var item = items[i];
171
172 // match
173 var j = item.toUpperCase().indexOf(value.toUpperCase());
174 if (j < 0) {
175 continue;
176 }
177
178 // do not offer a duplicate item
179 if (as_list && value_list.indexOf(item) != -1) {
180 continue;
181 }
182
183 c += 1;
184 if (c >= 15) {
185 var div = document.createElement('DIV');
186 div.innerHTML = '...';
187 list.appendChild(div);
188 break;
189 } else {
190 var div = document.createElement('DIV');
191 // make the matching letters bold:
192 div.innerHTML = item.substr(0, j)
193 + '<strong>' + item.substr(j, value.length) + '</strong>'
194 + item.substr(j + value.length)
195 + '<input type="hidden" value="' + item + '">';
196
197 div.addEventListener('click', function(e) {
198 // include selected value
199 var selected = this.getElementsByTagName('input')[0].value;
200 if (as_list) {
201 input.value = value_list.join(' ') + ' ' + selected;
202 } else {
203 input.value = selected;
204 }
205 // close the list of autocompleted values,
206 closeAllLists();
207 onend(input);
208 });
209
210 list.appendChild(div);
211 }
212 }
213 };
214
215 input.onkeydown = function(e) {
216 var x = document.getElementById(this.id + '-autocomplete-list');
217 if (x) x = x.getElementsByTagName('div');
218 if (e.keyCode == 40) {
219 // key down
220 currentFocus += 1;
221 // and and make the current item more visible:
222 setActive(x);
223 } else if (e.keyCode == 38) {
224 // key up
225 currentFocus -= 1;
226 // and and make the current item more visible:
227 setActive(x);
228 } else if (e.keyCode == 13) {
229 // If the ENTER key is pressed, prevent the form from being submitted,
230 e.preventDefault();
231 if (currentFocus > -1) {
232 // and simulate a click on the 'active' item:
233 if (x) x[currentFocus].click();
234 }
235 }
236 };
237
238 input.onfocus = function() {
239 onend(input);
240 }
241
242 // focus lost
243 input.onblur = function() {
244 onend(input);
245 }
246
247 function setActive(x) {
248 // a function to classify an item as 'active':
249 if (!x) return false;
250 // start by removing the 'active' class on all items:
251 for (var i = 0; i < x.length; i++) {
252 x[i].classList.remove('autocomplete-active');
253 }
254 if (currentFocus >= x.length) currentFocus = 0;
255 if (currentFocus < 0) currentFocus = (x.length - 1);
256 // add class 'autocomplete-active':
257 x[currentFocus].classList.add('autocomplete-active');
258 }
259
260 function closeAllLists(elmnt) {
261 // close all autocomplete lists in the document,
262 // except the one passed as an argument:
263 var x = document.getElementsByClassName('autocomplete-items');
264 for (var i = 0; i < x.length; i++) {
265 if (elmnt != x[i] && elmnt != input) {
266 x[i].parentNode.removeChild(x[i]);
267 }
268 }
269 }
270
271 // execute a function when someone clicks in the document:
272 document.addEventListener('click', e => {
273 closeAllLists(e.target);
274 });
275 }
276
277 // for attended sysupgrade
278 function updatePackageList(version, target) {
279 // set available packages
280 fetch(config.asu_url + '/' + config.versions[version] + '/' + target + '/index.json')
281 .then(response => response.json())
282 .then(all_packages => {
283 setupAutocompleteList($('packages'), all_packages, true, _ => {}, textarea => {
284 textarea.value = split(textarea.value)
285 // make list unique, ignore minus
286 .filter((value, index, self) => {
287 var i = self.indexOf(value.replace(/^\-/, ''));
288 return (i === index) || (i < 0);
289 })
290 // limit to available packages, ignore minus
291 .filter((value, index) => all_packages.indexOf(value.replace(/^\-/, '')) !== -1)
292 .join(' ');
293 });
294 });
295 }
296
297 function updateImages(version, code, date, model, url, mobj, is_custom) {
298 // add download button for image
299 function addLink(type, file) {
300 var a = document.createElement('A');
301 a.classList.add('download-link');
302 a.href = url
303 .replace('{target}', mobj.target)
304 .replace('{version}', version)
305 + '/' + file;
306 var span = document.createElement('SPAN');
307 span.appendChild(document.createTextNode(''));
308 a.appendChild(span);
309 a.appendChild(document.createTextNode(type.toUpperCase()));
310
311 if (config.showHelp) {
312 a.onmouseover = function() {
313 // hide all help texts
314 Array.from(document.getElementsByClassName('download-help'))
315 .forEach(e => e.style.display = 'none');
316 var lc = type.toLowerCase();
317 if (lc.includes('sysupgrade')) {
318 show('sysupgrade-help');
319 } else if (lc.includes('factory') || lc == 'trx' || lc == 'chk') {
320 show('factory-help');
321 } else if (lc.includes('kernel') || lc.includes('zimage') || lc.includes('uimage')) {
322 show('kernel-help');
323 } else if (lc.includes('root')) {
324 show('rootfs-help');
325 } else if (lc.includes('sdcard')) {
326 show('sdcard-help');
327 } else if (lc.includes('tftp')) {
328 show('tftp-help');
329 } else {
330 show('other-help');
331 }
332 };
333 }
334
335 $('download-links').appendChild(a);
336 }
337
338 function switchClass(id, from_class, to_class) {
339 $(id).classList.remove(from_class);
340 $(id).classList.add(to_class);
341 }
342
343 // remove all download links
344 Array.from(document.getElementsByClassName('download-link'))
345 .forEach(e => e.remove());
346
347 // hide all help texts
348 Array.from(document.getElementsByClassName('download-help'))
349 .forEach(e => e.style.display = 'none');
350
351 if (version && code && date && model && url && mobj) {
352 var target = mobj.target;
353 var images = mobj.images;
354
355 // change between "version" and "custom" title
356 if (is_custom) {
357 switchClass('images-title', 'tr-version-build', 'tr-custom-build');
358 switchClass('downloads-title', 'tr-version-downloads', 'tr-custom-downloads');
359 } else {
360 switchClass('images-title', 'tr-custom-build', 'tr-version-build');
361 switchClass('downloads-title', 'tr-custom-downloads', 'tr-version-downloads');
362 }
363 // update title translation
364 translate();
365
366 // fill out build info
367 $('image-model').innerText = model;
368 $('image-target').innerText = target;
369 $('image-version').innerText = version;
370 $('image-code').innerText = code;
371 $('image-date').innerText = date;
372
373 images.sort((a, b) => a.name.localeCompare(b.name));
374
375 for (var i in images) {
376 addLink(images[i].type, images[i].name);
377 }
378
379 if (config.asu_url) {
380 updatePackageList(version, target);
381 }
382
383 show('images');
384 } else {
385 hide('images');
386 }
387 }
388
389 function init() {
390 var build_date = "unknown"
391 setupSelectList($('versions'), Object.keys(config.versions), version => {
392 var url = config.versions[version];
393 if (config.asu_url) {
394 url = config.asu_url + '/' + url + '/profiles.json';
395 }
396 fetch(url)
397 .then(obj => {
398 build_date = obj.headers.get('last-modified');
399 return obj.json();
400 })
401 .then(obj => {
402 // handle native openwrt json format
403 if ('profiles' in obj) {
404 obj['models'] = {}
405 for (const [key, value] of Object.entries(obj['profiles'])) {
406 obj['models'][get_model_titles(value.titles)] = value
407 obj['models'][get_model_titles(value.titles)]['id'] = key
408 }
409 }
410 return obj
411 })
412 .then(obj => {
413 setupAutocompleteList($('models'), Object.keys(obj['models']), false, updateImages, models => {
414 var model = models.value;
415 if (model in obj['models']) {
416 var url = obj.url || 'unknown';
417 var code = obj.version_code || 'unknown';
418 var mobj = obj['models'][model];
419 updateImages(version, code, build_date, model, url, mobj, false);
420 current_model = mobj;
421 } else {
422 updateImages();
423 current_model = {};
424 }
425 });
426
427 // trigger model update when selected version changes
428 $('models').onfocus();
429 });
430 });
431
432 if (config.asu_url) {
433 show('custom');
434 }
435
436 // hide fields
437 updateImages();
438
439 var user_lang = (navigator.language || navigator.userLanguage).split('-')[0];
440 if (user_lang in translations) {
441 config.language = user_lang;
442 $('language-selection').value = user_lang;
443 }
444
445 translate();
446
447 $('language-selection').onclick = function() {
448 config.language = this.children[this.selectedIndex].value;
449 translate();
450 }
451 }