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 }