use inherited color for link to build log
[web/firmware-selector-openwrt-org.git] / index.js
index 3aa8c9f78423fbec3215ed78044f405683e5a9b5..6264c8a90129df5b31dca6ef329af9301b740327 100644 (file)
--- a/index.js
+++ b/index.js
@@ -1,6 +1,5 @@
 
 var current_model = {};
-var current_language = config.language;
 
 function $(id) {
   return document.getElementById(id);
@@ -14,40 +13,51 @@ function hide(id) {
   $(id).style.display = 'none';
 }
 
+function split(str) {
+  return str.match(/[^\s,]+/g) || [];
+}
+
+function get_model_titles(titles) {
+  return titles.map(e => {
+    if (e.title) {
+      return e.title;
+    } else {
+      return ((e.vendor || '') + ' ' + (e.model || '') + ' ' + (e.variant || '')).trim();
+    }
+  }).join(' / ');
+}
+
 function build_asa_request() {
   if (!current_model || !current_model.id) {
     alert('bad profile');
     return;
   }
 
-  function split(str) {
-    return str.match(/[^\s,]+/g) || [];
-  }
-
-  function get_model_titles(titles) {
-    return titles.map(e => {
-      if (e.title) {
-        return e.title;
-      } else {
-        return ((e.vendor || '') + (e.model || '') + (e.variant || '')).trim();
-      }
-    }).join('/');
+  function showStatus(message, url) {
+    show('buildstatus');
+    var tr = message.startsWith('tr-') ? message : '';
+    if (url) {
+      $('buildstatus').innerHTML = '<a href="' + url + '" class="' + tr + '">' + message + '</a>';
+    } else {
+      $('buildstatus').innerHTML = '<span class="' + tr + '"></span>';
+    }
+    translate();
   }
 
   // hide image view
   updateImages();
 
-  show('loading');
+  show('buildspinner');
+  showStatus('tr-request-image');
 
   var request_data = {
+    'target': current_model.target,
     'profile': current_model.id,
     'packages': split($('packages').value),
     'version': $('versions').value
   }
 
-  console.log('disable request button / show loading spinner')
-
-  fetch(config.asu_url, {
+  fetch(config.asu_url + '/api/build', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(request_data)
@@ -55,48 +65,43 @@ function build_asa_request() {
   .then(response => {
     switch (response.status) {
       case 200:
-        hide('loading');
+        hide('buildspinner');
+        showStatus('tr-build-successful');
 
-        console.log('image found');
         response.json()
         .then(mobj => {
-          console.log(mobj)
+          var download_url = config.asu_url + '/store/' + mobj.bin_dir;
+          showStatus('tr-build-successful', download_url + '/buildlog.txt');
           updateImages(
             mobj.version_number,
             mobj.version_code,
+            mobj.build_at,
             get_model_titles(mobj.titles),
-            mobj.url, mobj, true
+            download_url, mobj, true
           );
         });
         break;
       case 202:
-        // show some spinning animation
-        console.log('check again in 5 seconds');
+        showStatus('tr-check-again');
         setTimeout(_ => { build_asa_request() }, 5000);
         break;
       case 400: // bad request
       case 422: // bad package
       case 500: // build failed
-        hide('loading');
-        console.log('bad request (' + response.status + ')'); // see message
+        hide('buildspinner');
         response.json()
         .then(mobj => {
-          alert(mobj.message)
-        });
+          var message = mobj['message'] || 'tr-build-failed';
+          var url = mobj.buildlog ? (config.asu_url + '/store/' + mobj.bin_dir + '/buildlog.txt') : undefined;
+          showStatus(message, url);
+        })
         break;
     }
   })
-}
-
-function loadFile(url, callback) {
-  var xmlhttp = new XMLHttpRequest();
-  xmlhttp.onreadystatechange = function() {
-    if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
-      callback(JSON.parse(xmlhttp.responseText), url);
-    }
-  };
-  xmlhttp.open('GET', url, true);
-  xmlhttp.send();
+  .catch(err => {
+    hide('buildspinner');
+    showStatus(err);
+  })
 }
 
 function setupSelectList(select, items, onselection) {
@@ -116,23 +121,15 @@ function setupSelectList(select, items, onselection) {
 }
 
 // Change the translation of the entire document
-function applyLanguage(language) {
-  if (language) {
-    current_language = language;
-  }
-
-  var mapping = translations[current_language];
-  if (mapping) {
-    for (var tr in mapping) {
-      Array.from(document.getElementsByClassName(tr))
+function translate() {
+  var mapping = translations[config.language];
+  for (var tr in mapping) {
+    Array.from(document.getElementsByClassName(tr))
       .forEach(e => { e.innerText = mapping[tr]; })
-    }
   }
 }
 
-function setupAutocompleteList(input, items, onselection) {
-  // the setupAutocompleteList function takes two arguments,
-  // the text field element and an array of possible autocompleted values:
+function setupAutocompleteList(input, items, as_list, onbegin, onend) {
   var currentFocus = -1;
 
   // sort numbers and other characters separately
@@ -140,12 +137,20 @@ function setupAutocompleteList(input, items, onselection) {
 
   items.sort(collator.compare);
 
-  // execute a function when someone writes in the text field:
   input.oninput = function(e) {
-    // clear images
-    updateImages();
+    onbegin();
 
+    var offset = 0;
     var value = this.value;
+    var value_list = [];
+
+    if (as_list) {
+      // automcomplete last text item
+      offset = this.value.lastIndexOf(' ') + 1;
+      value = this.value.substr(offset);
+      value_list = split(this.value.substr(0, offset));
+    }
+
     // close any already open lists of autocompleted values
     closeAllLists();
 
@@ -160,7 +165,6 @@ function setupAutocompleteList(input, items, onselection) {
     // append the DIV element as a child of the autocomplete container:
     this.parentNode.appendChild(list);
 
-    // for each item in the array...
     var c = 0;
     for (var i = 0; i < items.length; i += 1) {
       var item = items[i];
@@ -171,6 +175,11 @@ function setupAutocompleteList(input, items, onselection) {
         continue;
       }
 
+      // do not offer a duplicate item
+      if (as_list && value_list.indexOf(item) != -1) {
+        continue;
+      }
+
       c += 1;
       if (c >= 15) {
         var div = document.createElement('DIV');
@@ -186,13 +195,16 @@ function setupAutocompleteList(input, items, onselection) {
           + '<input type="hidden" value="' + item + '">';
 
         div.addEventListener('click', function(e) {
-          // set text field to selected value
-          input.value = this.getElementsByTagName('input')[0].value;
+          // include selected value
+          var selected = this.getElementsByTagName('input')[0].value;
+          if (as_list) {
+            input.value = value_list.join(' ') + ' ' + selected;
+          } else {
+            input.value = selected;
+          }
           // close the list of autocompleted values,
-          // (or any other open lists of autocompleted values:
           closeAllLists();
-          // callback
-          onselection(input.value);
+          onend(input);
         });
 
         list.appendChild(div);
@@ -224,7 +236,12 @@ function setupAutocompleteList(input, items, onselection) {
   };
 
   input.onfocus = function() {
-    onselection(input.value);
+    onend(input);
+  }
+
+  // focus lost
+  input.onblur = function() {
+    onend(input);
   }
 
   function setActive(x) {
@@ -257,7 +274,27 @@ function setupAutocompleteList(input, items, onselection) {
   });
 }
 
-function updateImages(version, code, model, url, mobj, is_custom) {
+// for attended sysupgrade
+function updatePackageList(version, target) {
+  // set available packages
+  fetch(config.asu_url + '/' + config.versions[version] + '/' + target +  '/index.json')
+  .then(response => response.json())
+  .then(all_packages => {
+    setupAutocompleteList($('packages'), all_packages, true, _ => {}, textarea => {
+      textarea.value = split(textarea.value)
+        // make list unique, ignore minus
+        .filter((value, index, self) => {
+          var i = self.indexOf(value.replace(/^\-/, ''));
+          return (i === index) || (i < 0);
+        })
+        // limit to available packages, ignore minus
+        .filter((value, index) => all_packages.indexOf(value.replace(/^\-/, '')) !== -1)
+        .join(' ');
+    });
+  });
+}
+
+function updateImages(version, code, date, model, url, mobj, is_custom) {
   // add download button for image
   function addLink(type, file) {
     var a = document.createElement('A');
@@ -311,7 +348,7 @@ function updateImages(version, code, model, url, mobj, is_custom) {
   Array.from(document.getElementsByClassName('download-help'))
     .forEach(e => e.style.display = 'none');
 
-  if (version && code && model && url && mobj) {
+  if (version && code && date && model && url && mobj) {
     var target = mobj.target;
     var images = mobj.images;
 
@@ -324,13 +361,14 @@ function updateImages(version, code, model, url, mobj, is_custom) {
       switchClass('downloads-title', 'tr-custom-downloads', 'tr-version-downloads');
     }
     // update title translation
-    applyLanguage();
+    translate();
 
     // fill out build info
     $('image-model').innerText = model;
     $('image-target').innerText = target;
     $('image-version').innerText = version;
     $('image-code').innerText = code;
+    $('image-date').innerText = date;
 
     images.sort((a, b) => a.name.localeCompare(b.name));
 
@@ -338,36 +376,76 @@ function updateImages(version, code, model, url, mobj, is_custom) {
       addLink(images[i].type, images[i].name);
     }
 
+    if (config.asu_url) {
+      updatePackageList(version, target);
+    }
+
     show('images');
   } else {
     hide('images');
   }
 }
 
-setupSelectList($('versions'), Object.keys(config.versions), version => {
-  loadFile(config.versions[version], obj => {
-    setupAutocompleteList($('models'), Object.keys(obj['models']), model => {
-      if (model in obj['models']) {
-        var url = obj.url;
-        var code = obj.version_code;
-        var mobj = obj['models'][model];
-        updateImages(version, code, model, url, mobj, false);
-        current_model = mobj;
-      } else {
-        updateImages();
-        current_model = {};
+function init() {
+  var build_date = "unknown"
+  setupSelectList($('versions'), Object.keys(config.versions), version => {
+    var url = config.versions[version];
+    if (config.asu_url) {
+      url = config.asu_url + '/' + url + '/profiles.json';
+    }
+    fetch(url)
+    .then(obj => {
+      build_date = obj.headers.get('last-modified');
+      return obj.json();
+    })
+    .then(obj => {
+      // handle native openwrt json format
+      if ('profiles' in obj) {
+        obj['models'] = {}
+        for (const [key, value] of Object.entries(obj['profiles'])) {
+          obj['models'][get_model_titles(value.titles)] = value
+          obj['models'][get_model_titles(value.titles)]['id'] = key
+        }
       }
-    });
+      return obj
+    })
+    .then(obj => {
+      setupAutocompleteList($('models'), Object.keys(obj['models']), false, updateImages, models => {
+        var model = models.value;
+        if (model in obj['models']) {
+          var url = obj.url || 'unknown';
+          var code = obj.version_code || 'unknown';
+          var mobj = obj['models'][model];
+          updateImages(version, code, build_date, model, url, mobj, false);
+          current_model = mobj;
+        } else {
+          updateImages();
+          current_model = {};
+        }
+      });
 
-    // trigger model update when selected version changes
-    $('models').onfocus();
+      // trigger model update when selected version changes
+      $('models').onfocus();
+    });
   });
-});
 
-if (config.asu_url) {
-  show('custom');
-}
+  if (config.asu_url) {
+    show('custom');
+  }
 
-// hide fields
-updateImages();
-applyLanguage(config.language);
+  // hide fields
+  updateImages();
+
+  var user_lang = (navigator.language || navigator.userLanguage).split('-')[0];
+  if (user_lang in translations) {
+    config.language = user_lang;
+    $('language-selection').value = user_lang;
+  }
+
+  translate();
+
+  $('language-selection').onclick = function() {
+    config.language = this.children[this.selectedIndex].value;
+    translate();
+  }
+}