github.com/shashidharatd/test-infra@v0.0.0-20171006011030-71304e1ca560/triage/interactive.js (about)

     1  "use strict";
     2  
     3  var builds = null;
     4  var clustered = null;         // filtered clusters
     5  var clusteredAll = null;      // all clusters
     6  var options = null;           // user-provided in form or URL
     7  var lastClusterRendered = 0;  // for infinite scrolling
     8  
     9  // Escape special regex characters for putting a literal into a regex.
    10  // http://stackoverflow.com/a/9310752/3694
    11  RegExp.escape = function(text) {
    12    return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
    13  };
    14  
    15  // Load options from form inputs, put them in the URL, and return the options dict.
    16  function readOptions() {
    17    var read = id => {
    18      let el = document.getElementById(id);
    19      if (el.type === "checkbox") return el.checked;
    20      if (el.type === "radio") return el.form[el.name].value;
    21      if (el.type === "select-one") return el.value;
    22      if (el.type === "text") {
    23        if (id.startsWith("filter")) {
    24          if (el.value === "") {
    25            return null;
    26          }
    27          try {
    28            return new RegExp(el.value, "im");
    29          } catch(err) {
    30            console.error("bad regexp", el.value, err);
    31            return new RegExp(RegExp.escape(el.value), "im");
    32          }
    33        } else {
    34          return el.value;
    35        }
    36      }
    37    }
    38  
    39    function readSigs() {
    40      var ret = [];
    41      for (let el of document.getElementById("btn-sig-group").children) {
    42        if (el.classList.contains('active')) {
    43          ret.push(el.textContent);
    44        }
    45      }
    46      return ret;
    47    }
    48  
    49    var opts = {
    50      ci: read('job-ci'),
    51      pr: read('job-pr'),
    52      reText: read('filter-text'),
    53      reJob: read('filter-job'),
    54      reTest: read('filter-test'),
    55      showNormalize: read('show-normalize'),
    56      sort: read('sort'),
    57      sig: readSigs(),
    58    };
    59  
    60    console.log(opts.sig);
    61  
    62    var url = '';
    63    if (!opts.ci) url += '&ci=0';
    64    if (opts.pr) url += '&pr=1';
    65    if (opts.sig.length) url += '&sig=' + opts.sig.join(',');
    66    for (var name of ["text", "job", "test"]) {
    67      var re = opts['re' + name[0].toUpperCase() + name.slice(1)];
    68      if (re) {
    69        var baseRe = re.toString().replace(/im$/, '').replace(/\\\//g, '/').slice(1, -1);
    70        url += '&' + name + '=' + encodeURIComponent(baseRe);
    71      }
    72    }
    73    if (url) {
    74      if (document.location.hash) {
    75        url += document.location.hash;
    76      }
    77      history.replaceState(null, "", "?" + url.slice(1));
    78    } else if (document.location.search) {
    79      history.replaceState(null, "", document.location.pathname + document.location.hash);
    80    }
    81  
    82    return opts;
    83  }
    84  
    85  // Convert querystring parameters into form inputs.
    86  function setOptionsFromURL() {
    87    // http://stackoverflow.com/a/3855394/3694
    88    var qs = (function(a) {
    89      if (a == "") return {};
    90      var b = {};
    91      for (var i = 0; i < a.length; ++i)
    92      {
    93        var p=a[i].split('=', 2);
    94        if (p.length == 1)
    95          b[p[0]] = "";
    96        else
    97          b[p[0]] = decodeURIComponent(p[1].replace(/\+/g, " "));
    98      }
    99      return b;
   100    })(window.location.search.substr(1).split('&'));
   101  
   102    var write = (id, value) => {
   103      if (!value) return;
   104      var el = document.getElementById(id);
   105      if (el.type === "checkbox") el.checked = (value === "1");
   106      else el.value = value;
   107    }
   108  
   109    function writeSigs(sigs) {
   110      for (let sig of (sigs || '').split(',')) {
   111        var el = document.getElementById('btn-sig-' + sig);
   112        if (el) {
   113          el.classList.add('active');
   114        }
   115      }
   116    }
   117  
   118    write('job-ci', qs.ci);
   119    write('job-pr', qs.pr);
   120    write('filter-text', qs.text);
   121    write('filter-job', qs.job);
   122    write('filter-test', qs.test);
   123    writeSigs(qs.sig);
   124  }
   125  
   126  // Render up to `count` clusters, with `start` being the first for consideration.
   127  function renderSubset(start, count) {
   128    var top = document.getElementById('clusters');
   129    var n = 0;
   130    var shown = 0;
   131    for (let c of clustered.data) {
   132      if (n++ < start) continue;
   133      shown += renderCluster(top, c);
   134      lastClusterRendered = n;
   135      if (shown >= count) break;
   136    }
   137  }
   138  
   139  // Clear the page and reinitialize the renderer and filtering. Render a few failures.
   140  function rerender(maxCount) {
   141    if (!clusteredAll) return;
   142  
   143    console.log('rerender!');
   144  
   145    options = readOptions();
   146    clustered = clusteredAll.refilter(options);
   147  
   148    var top = document.getElementById('clusters');
   149    var summary = document.getElementById('summary');
   150    top.removeChildren();
   151    summary.removeChildren();
   152  
   153    var summaryText = `
   154              ${clustered.length} clusters of ${clustered.sum} failures`;
   155  
   156    if (clustered.sumRecent > 0) {
   157      summaryText += ` (${clustered.sumRecent} in last day)`;
   158    }
   159  
   160    summaryText += ` out of ${builds.runCount} builds from ${builds.getStartTime()} to ${builds.getEndTime()}.`
   161  
   162    if (maxCount !== 0) {
   163      summary.innerText = summaryText;
   164  
   165      if (clustered.length > 0) {
   166        let graph = addElement(summary, 'div');
   167        renderGraph(graph, clustered.allBuilds());
   168      }
   169  
   170      renderSubset(0, maxCount || 10);
   171    }
   172  
   173    // draw graphs after the current render cycle, to reduce perceived latency.
   174    setTimeout(drawVisibleGraphs, 0);
   175  }
   176  
   177  function toggle(target) {
   178    if (target.matches('button.toggle')) {
   179      target.classList.toggle("active");
   180      // rerender after repainting the clicked button, to improve responsiveness.
   181      setTimeout(rerender, 0);
   182    } else if (target.matches('span.owner')) {
   183      document.getElementById('btn-sig-' + target.textContent).click();
   184    } else if (target.matches('.clearoptions')) {
   185      document.location = document.location.pathname;
   186    } else {
   187      return false;
   188    }
   189    return true;
   190  }
   191  
   192  // Render just the cluster with the given key.
   193  // Show an error message if no live cluster with that id is found.
   194  function renderOnly(keyId) {
   195    var el = null;
   196    rerender(0);
   197  
   198    var top = document.getElementById('clusters');
   199    top.removeChildren();
   200  
   201    addElement(top, 'h3', null, [createElement('a', {href: ''}, 'View all clusters')]);
   202  
   203    if (!clustered.byId[keyId]) {
   204      var summary = document.getElementById('summary');
   205      summary.innerText = `Cluster ${keyId} not found in the last week of data.`
   206      return;
   207    }
   208  
   209    renderSubset(0, 1);
   210  
   211    // expand the graph for the selected failure.
   212    setTimeout(drawVisibleGraphs, 0);
   213  }
   214  
   215  // When the user scrolls down, render more clusters to provide infinite scrolling.
   216  // This is important to make the first page load fast.
   217  // Also, trigger a debounced lazy graph rendering pass.
   218  function scrollHandler() {
   219    if (!clustered) return;
   220    if (lastClusterRendered < clustered.length) {
   221      var top = document.getElementById('clusters');
   222      if (top.getBoundingClientRect().bottom < 3 * window.innerHeight) {
   223        renderSubset(lastClusterRendered, 10);
   224      }
   225    }
   226    if (drawGraphsTimer) {
   227      clearTimeout(drawGraphsTimer);
   228    }
   229    drawGraphsTimer = setTimeout(drawVisibleGraphs, 50);
   230  }
   231  
   232  var drawGraphsTimer = null;
   233  
   234  function drawVisibleGraphs() {
   235    for (let el of document.querySelectorAll('div.graph')) {
   236      if (el.children.length > 0) {
   237        continue;  // already rendered
   238      }
   239      let rect = el.getBoundingClientRect();
   240      if (0 <= rect.top + kGraphHeight && rect.top - kGraphHeight < window.innerHeight) {
   241        renderGraph(el, clustered.buildsForClusterById(el.dataset.cluster));
   242      }
   243    }
   244  }
   245  
   246  // If someone clicks on an expandable node, expand it!
   247  function clickHandler(evt) {
   248    var target = evt.target;
   249    if (expand(target) || toggle(target)) {
   250      evt.preventDefault();
   251      return true;
   252    }
   253    return false;
   254  }
   255  
   256  // Download a file from GCS and invoke callback with the result.
   257  // extracted/modified from kubernetes/test-infra/gubernator/static/build.js
   258  function get(uri, callback, onprogress) {
   259    if (uri[0] === '/') {
   260      // Matches /bucket/file/path -> [..., "bucket", "file/path"]
   261      var groups = uri.match(/([^/:]+)\/(.*)/);
   262      var bucket = groups[1], path = groups[2];
   263      var url = 'https://www.googleapis.com/storage/v1/b/' + bucket + '/o/' +
   264        encodeURIComponent(path) + '?alt=media';
   265    } else {
   266      var url = uri;
   267    }
   268    var req = new XMLHttpRequest();
   269    req.open('GET', url);
   270    req.onload = function(resp) {
   271      callback(req);
   272    };
   273    req.onprogress = onprogress;
   274    req.send();
   275  }
   276  
   277  function getData(clusterId) {
   278    var url = '/k8s-gubernator/triage/failure_data.json'
   279    if (clusterId) {
   280      url = '/k8s-gubernator/triage/slices/failure_data_' + clusterId.slice(0, 2) + '.json';
   281    }
   282  
   283    var setLoading = t => document.getElementById("loading-progress").innerText = t;
   284    var toMB = b => Math.round(b / 1024 / 1024 * 100) / 100;
   285  
   286    get(url,
   287      req => {
   288        setLoading(`parsing ${toMB(req.response.length)}MB.`);
   289        setTimeout(() => {
   290          var data = JSON.parse(req.response);
   291          builds = new Builds(data.builds);
   292          if (clusterId) {
   293            // rendering just one cluster, filter here.
   294            for (let c of data.clustered) {
   295              if (c.id == clusterId) {
   296                data.clustered = [c];
   297                break;
   298              }
   299            }
   300          }
   301          clusteredAll = new Clusters(data.clustered);
   302          if (clusterId) {
   303            clusteredAll.slice = true;
   304            renderOnly(clusterId);
   305          } else {
   306            rerender();
   307          }
   308        }, 0);
   309      },
   310      evt => {
   311        if (evt.type === "progress") {
   312          setLoading(`downloaded ${toMB(evt.loaded)}MB`);
   313        }
   314      }
   315    );
   316  }
   317  
   318  // One-time initialization of the whole page.
   319  function load() {
   320    setOptionsFromURL();
   321  
   322    var clusterId = null;
   323    if (/^#[a-f0-9]{20}$/.test(window.location.hash)) {
   324      clusterId = window.location.hash.slice(1);
   325      // Hide filtering options, since this page has only a single cluster.
   326      document.getElementById('options').style.display = 'none';
   327    }
   328  
   329    getData(clusterId);
   330  
   331    google.charts.load('current', {'packages': ['corechart', 'line']});
   332    google.charts.setOnLoadCallback(() => { google.charts.loaded = true });
   333  
   334    document.addEventListener('click', clickHandler, false);
   335    document.addEventListener('scroll', scrollHandler);
   336  }