github.com/yrj2011/jx-test-infra@v0.0.0-20190529031832-7a2065ee98eb/prow/cmd/deck/static/script.js (about) 1 "use strict"; 2 3 function getParameterByName(name) { // http://stackoverflow.com/a/5158301/3694 4 var match = RegExp('[?&]' + name + '=([^&/]*)').exec( 5 window.location.search); 6 return match && decodeURIComponent(match[1].replace(/\+/g, ' ')); 7 } 8 9 function updateQueryStringParameter(uri, key, value) { 10 var re = new RegExp("([?&])" + key + "=.*?(&|$)", "i"); 11 var separator = uri.indexOf('?') !== -1 ? "&" : "?"; 12 if (uri.match(re)) { 13 return uri.replace(re, '$1' + key + "=" + value + '$2'); 14 } else { 15 return uri + separator + key + "=" + value; 16 } 17 } 18 19 function shortenBuildRefs(buildRef) { 20 return buildRef && buildRef.replace(/:[0-9a-f]*/g, ''); 21 } 22 23 function optionsForRepo(repo) { 24 var opts = { 25 types: {}, 26 repos: {}, 27 jobs: {}, 28 authors: {}, 29 pulls: {}, 30 batches: {}, 31 states: {}, 32 }; 33 34 for (var i = 0; i < allBuilds.length; i++) { 35 var build = allBuilds[i]; 36 opts.types[build.type] = true; 37 opts.repos[build.repo] = true; 38 if (!repo || repo === build.repo) { 39 opts.jobs[build.job] = true; 40 opts.states[build.state] = true; 41 if (build.type === "presubmit") { 42 opts.authors[build.author] = true; 43 opts.pulls[build.number] = true; 44 } else if (build.type === "batch") { 45 opts.batches[shortenBuildRefs(build.refs)] = true; 46 } 47 } 48 } 49 50 return opts; 51 } 52 53 function redrawOptions(fz, opts) { 54 var ts = Object.keys(opts.types).sort(); 55 var selectedType = addOptions(ts, "type"); 56 var rs = Object.keys(opts.repos).filter(function (r) { 57 return r !== "/"; 58 }).sort(); 59 addOptions(rs, "repo"); 60 var js = Object.keys(opts.jobs).sort(); 61 var jobInput = document.getElementById("job-input"); 62 var jobList = document.getElementById("job-list"); 63 addOptionFuzzySearch(fz, js, "job", jobList, jobInput); 64 var as = Object.keys(opts.authors).sort(function (a, b) { 65 return a.toLowerCase().localeCompare(b.toLowerCase()); 66 }); 67 addOptions(as, "author"); 68 if (selectedType === "batch") { 69 opts.pulls = opts.batches; 70 } 71 if (selectedType !== "periodic" && selectedType !== "postsubmit") { 72 var ps = Object.keys(opts.pulls).sort(function (a, b) { 73 return parseInt(a) - parseInt(b); 74 }); 75 addOptions(ps, "pull"); 76 } else { 77 addOptions([], "pull"); 78 } 79 var ss = Object.keys(opts.states).sort(); 80 addOptions(ss, "state"); 81 }; 82 83 function adjustScroll(el) { 84 var parent = el.parentElement; 85 var parentRect = parent.getBoundingClientRect(); 86 var elRect = el.getBoundingClientRect(); 87 88 if (elRect.top < parentRect.top) { 89 parent.scrollTop -= elRect.height; 90 } else if (elRect.top + elRect.height >= parentRect.top 91 + parentRect.height) { 92 parent.scrollTop += elRect.height; 93 } 94 } 95 96 function handleDownKey() { 97 var activeSearches = 98 document.getElementsByClassName("active-fuzzy-search"); 99 if (activeSearches !== null && activeSearches.length !== 1) { 100 return; 101 } 102 var activeSearch = activeSearches[0]; 103 if (activeSearch.tagName !== "UL" || 104 activeSearch.childElementCount === 0) { 105 return; 106 } 107 108 var selectedJobs = activeSearch.getElementsByClassName("job-selected"); 109 if (selectedJobs.length > 1) { 110 return; 111 } 112 if (selectedJobs.length === 0) { 113 // If no job selected, selecte the first one that visible in the list. 114 var jobs = Array.from(activeSearch.children) 115 .filter(function (elChild) { 116 var childRect = elChild.getBoundingClientRect(); 117 var listRect = activeSearch.getBoundingClientRect(); 118 return childRect.top >= listRect.top && 119 (childRect.top < listRect.top + listRect.height); 120 }); 121 if (jobs.length === 0) { 122 return; 123 } 124 jobs[0].classList.add("job-selected"); 125 return; 126 } 127 var selectedJob = selectedJobs[0]; 128 var nextSibling = selectedJob.nextElementSibling; 129 if (!nextSibling) { 130 return; 131 } 132 133 selectedJob.classList.remove("job-selected"); 134 nextSibling.classList.add("job-selected"); 135 adjustScroll(nextSibling); 136 } 137 138 function handleUpKey() { 139 var activeSearches = 140 document.getElementsByClassName("active-fuzzy-search"); 141 if (activeSearches && activeSearches.length !== 1) { 142 return; 143 } 144 var activeSearch = activeSearches[0]; 145 if (activeSearch.tagName !== "UL" || 146 activeSearch.childElementCount === 0) { 147 return; 148 } 149 150 var selectedJobs = activeSearch.getElementsByClassName("job-selected"); 151 if (selectedJobs.length !== 1) { 152 return; 153 } 154 155 var selectedJob = selectedJobs[0]; 156 var previousSibling = selectedJob.previousElementSibling; 157 if (!previousSibling) { 158 return; 159 } 160 161 selectedJob.classList.remove("job-selected"); 162 previousSibling.classList.add("job-selected"); 163 adjustScroll(previousSibling); 164 } 165 166 window.onload = function () { 167 var topNavigator = document.querySelector("#top-navigator"); 168 var navigatorTimeOut; 169 var main = document.querySelector("main"); 170 main.onscroll = () => { 171 topNavigator.classList.add("hidden"); 172 if (navigatorTimeOut) { 173 clearTimeout(navigatorTimeOut); 174 } 175 navigatorTimeOut = setTimeout(() => { 176 if (main.scrollTop === 0) { 177 topNavigator.classList.add("hidden"); 178 } else if (main.scrollTop > 100) { 179 topNavigator.classList.remove("hidden"); 180 } 181 }, 100); 182 }; 183 topNavigator.onclick = () => { 184 main.scrollTop = 0; 185 }; 186 187 document.addEventListener("keydown", function (event) { 188 if (event.keyCode === 40) { 189 handleDownKey(); 190 } else if (event.keyCode === 38) { 191 handleUpKey(); 192 } 193 }); 194 // Register selection on change functions 195 var filterBox = document.querySelector("#filter-box"); 196 var options = filterBox.querySelectorAll("select"); 197 options.forEach(opt => { 198 opt.onchange = () => { 199 redraw(fz); 200 }; 201 }); 202 // Attach job status bar on click 203 var stateFilter = document.querySelector("#state"); 204 document.querySelectorAll(".job-bar-state").forEach(jb => { 205 var state = jb.id.slice("job-bar-".length); 206 if (state === "unknown") { 207 return; 208 } 209 jb.addEventListener("click", () => { 210 stateFilter.value = state; 211 stateFilter.onchange(); 212 }); 213 }); 214 // set dropdown based on options from query string 215 var opts = optionsForRepo(""); 216 var fz = initFuzzySearch( 217 "job", 218 "job-input", 219 "job-list", 220 Object.keys(opts["jobs"]).sort()); 221 redrawOptions(fz, opts); 222 redraw(fz); 223 }; 224 225 document.addEventListener("DOMContentLoaded", function (event) { 226 configure(); 227 }); 228 229 function configure() { 230 if (!branding) { 231 return; 232 } 233 if (branding.logo) { 234 document.getElementById('img').src = branding.logo; 235 } 236 if (branding.favicon) { 237 document.getElementById('favicon').href = branding.favicon; 238 } 239 if (branding.background_color) { 240 document.body.style.background = branding.background_color; 241 } 242 if (branding.header_color) { 243 document.getElementsByTagName( 244 'header')[0].style.backgroundColor = branding.header_color; 245 } 246 } 247 248 function displayFuzzySearchResult(el, inputContainer) { 249 el.classList.add("active-fuzzy-search"); 250 el.style.top = inputContainer.height - 1 + "px"; 251 el.style.width = inputContainer.width + "px"; 252 el.style.height = 200 + "px"; 253 el.style.zIndex = "9999" 254 } 255 256 function fuzzySearch(fz, id, list, input) { 257 var inputValue = input.value.trim(); 258 addOptionFuzzySearch(fz, fz.search(inputValue), id, list, input, true); 259 } 260 261 function validToken(token) { 262 // 0-9 263 if (token >= 48 && token <= 57) { 264 return true; 265 } 266 // a-z 267 if (token >= 65 && token <= 90) { 268 return true; 269 } 270 // - and backspace 271 return token === 189 || token === 8; 272 } 273 274 function handleEnterKeyDown(fz, list, input) { 275 var selectedJobs = list.getElementsByClassName("job-selected"); 276 if (selectedJobs && selectedJobs.length === 1) { 277 input.value = selectedJobs[0].innerHTML; 278 } 279 // TODO(@qhuynh96): according to discussion in https://github.com/kubernetes/test-infra/pull/7165, the 280 // fuzzy search should respect user input no matter it is in the list or not. User may 281 // experience being redirected back to default view if the search input is invalid. 282 input.blur(); 283 list.classList.remove("active-fuzzy-search"); 284 redraw(fz); 285 } 286 287 function registerFuzzySearchHandler(fz, id, list, input) { 288 input.addEventListener("keydown", function (event) { 289 if (event.keyCode === 13) { 290 handleEnterKeyDown(fz, list, input); 291 } else if (validToken(event.keyCode)) { 292 // Delay 1 frame that the input character is recorded before getting 293 // input value 294 setTimeout(function () { 295 fuzzySearch(fz, id, list, input); 296 }, 32); 297 } 298 }); 299 } 300 301 function initFuzzySearch(id, inputId, listId, data) { 302 var fz = new FuzzySearch(data); 303 var el = document.getElementById(id); 304 var input = document.getElementById(inputId); 305 var list = document.getElementById(listId); 306 307 list.classList.remove("active-fuzzy-search"); 308 input.addEventListener("focus", function () { 309 fuzzySearch(fz, id, list, input); 310 displayFuzzySearchResult(list, el.getBoundingClientRect()); 311 }); 312 input.addEventListener("blur", function () { 313 list.classList.remove("active-fuzzy-search"); 314 }); 315 316 registerFuzzySearchHandler(fz, id, list, input); 317 return fz; 318 } 319 320 function registerJobResultEventHandler(fz, li, input) { 321 li.addEventListener("mousedown", function (event) { 322 input.value = event.currentTarget.innerHTML; 323 redraw(fz); 324 }); 325 li.addEventListener("mouseover", function (event) { 326 var selectedJobs = document.getElementsByClassName("job-selected"); 327 if (!selectedJobs) { 328 return; 329 } 330 331 for (var i = 0; i < selectedJobs.length; i++) { 332 selectedJobs[i].classList.remove("job-selected"); 333 } 334 event.currentTarget.classList.add("job-selected"); 335 }); 336 li.addEventListener("mouseout", function (event) { 337 event.currentTarget.classList.remove("job-selected"); 338 }); 339 } 340 341 function addOptionFuzzySearch(fz, data, id, list, input, stopAutoFill) { 342 if (!stopAutoFill) { 343 input.value = getParameterByName(id); 344 } 345 while (list.firstChild) { 346 list.removeChild(list.firstChild); 347 } 348 list.scrollTop = 0; 349 for (var i = 0; i < data.length; i++) { 350 var li = document.createElement("li"); 351 li.innerHTML = data[i]; 352 registerJobResultEventHandler(fz, li, input); 353 list.appendChild(li); 354 } 355 } 356 357 function addOptions(s, p) { 358 var sel = document.getElementById(p); 359 while (sel.length > 1) { 360 sel.removeChild(sel.lastChild); 361 } 362 var param = getParameterByName(p); 363 for (var i = 0; i < s.length; i++) { 364 var o = document.createElement("option"); 365 o.text = s[i]; 366 if (param && s[i] === param) { 367 o.selected = true; 368 } 369 sel.appendChild(o); 370 } 371 return param; 372 } 373 374 function selectionText(sel, t) { 375 return sel.selectedIndex == 0 ? "" : sel.options[sel.selectedIndex].text; 376 } 377 378 function equalSelected(sel, t) { 379 return sel === "" || sel == t; 380 } 381 382 function groupKey(build) { 383 return build.repo + " " + build.number + " " + build.refs; 384 } 385 386 function redraw(fz) { 387 var modal = document.getElementById('rerun'); 388 var rerun_command = document.getElementById('rerun-content'); 389 window.onclick = function (event) { 390 if (event.target == modal) { 391 modal.style.display = "none"; 392 } 393 }; 394 var builds = document.getElementById("builds").getElementsByTagName( 395 "tbody")[0]; 396 while (builds.firstChild) { 397 builds.removeChild(builds.firstChild); 398 } 399 400 var args = []; 401 402 function getSelection(name) { 403 var sel = selectionText(document.getElementById(name)); 404 if (sel && opts && !opts[name + 's'][sel]) { 405 return ""; 406 } 407 if (sel !== "") { 408 args.push(name + "=" + encodeURIComponent(sel)); 409 } 410 return sel; 411 } 412 413 function getSelectionFuzzySearch(id, inputId) { 414 var input = document.getElementById(inputId); 415 var inputText = input.value; 416 if (inputText !== "" && opts && !opts[id + 's'][inputText]) { 417 return ""; 418 } 419 if (inputText !== "") { 420 args.push(id + "=" + encodeURIComponent( 421 inputText)); 422 } 423 424 return inputText; 425 } 426 427 var opts = null; 428 var repoSel = getSelection("repo"); 429 opts = optionsForRepo(repoSel); 430 431 var typeSel = getSelection("type"); 432 if (typeSel === "batch") { 433 opts.pulls = opts.batches; 434 } 435 var pullSel = getSelection("pull"); 436 var authorSel = getSelection("author"); 437 var jobSel = getSelectionFuzzySearch("job", "job-input"); 438 var stateSel = getSelection("state"); 439 440 if (window.history && window.history.replaceState !== undefined) { 441 if (args.length > 0) { 442 history.replaceState(null, "", "/?" + args.join('&')); 443 } else { 444 history.replaceState(null, "", "/") 445 } 446 } 447 fz.setDict(Object.keys(opts.jobs)); 448 redrawOptions(fz, opts); 449 450 var lastKey = ''; 451 const jobCountMap = new Map(); 452 let totalJob = 0; 453 for (var i = 0; i < allBuilds.length; i++) { 454 var build = allBuilds[i]; 455 if (!equalSelected(typeSel, build.type)) { 456 continue; 457 } 458 if (!equalSelected(repoSel, build.repo)) { 459 continue; 460 } 461 if (!equalSelected(stateSel, build.state)) { 462 continue; 463 } 464 if (!equalSelected(jobSel, build.job)) { 465 continue; 466 } 467 if (build.type === "presubmit") { 468 if (!equalSelected(pullSel, build.number)) { 469 continue; 470 } 471 if (!equalSelected(authorSel, build.author)) { 472 continue; 473 } 474 } else if (build.type === "batch" && !authorSel) { 475 if (!equalSelected(pullSel, shortenBuildRefs(build.refs))) { 476 continue; 477 } 478 } else if (pullSel || authorSel) { 479 continue; 480 } 481 482 if (!jobCountMap.has(build.state)) { 483 jobCountMap.set(build.state, 0); 484 } 485 totalJob ++; 486 jobCountMap.set(build.state, jobCountMap.get(build.state) + 1); 487 if (totalJob > 499) { 488 continue; 489 } 490 var r = document.createElement("tr"); 491 r.appendChild(stateCell(build.state)); 492 if (build.pod_name) { 493 const icon = createIcon("description", "Build log"); 494 icon.href = "log?job=" + build.job + "&id=" + build.build_id; 495 const cell = document.createElement("TD"); 496 cell.classList.add("icon-cell"); 497 cell.appendChild(icon); 498 r.appendChild(cell); 499 } else { 500 r.appendChild(createTextCell("")); 501 } 502 r.appendChild(createRerunCell(modal, rerun_command, build.prow_job)); 503 var key = groupKey(build); 504 if (key !== lastKey) { 505 // This is a different PR or commit than the previous row. 506 lastKey = key; 507 r.className = "changed"; 508 509 if (build.type === "periodic") { 510 r.appendChild(createTextCell("")); 511 } else if (build.repo.startsWith("http://") || build.repo.startsWith("https://") ) { 512 r.appendChild(createLinkCell(build.repo, build.repo, "")); 513 } else { 514 r.appendChild(createLinkCell(build.repo, "https://github.com/" 515 + build.repo, "")); 516 } 517 if (build.type === "presubmit") { 518 r.appendChild(prRevisionCell(build)); 519 } else if (build.type === "batch") { 520 r.appendChild(batchRevisionCell(build)); 521 } else if (build.type === "postsubmit") { 522 r.appendChild(pushRevisionCell(build)); 523 } else if (build.type === "periodic") { 524 r.appendChild(createTextCell("")); 525 } 526 } else { 527 // Don't render identical cells for the same PR/commit. 528 r.appendChild(createTextCell("")); 529 r.appendChild(createTextCell("")); 530 } 531 if (build.url === "") { 532 r.appendChild(createTextCell(build.job)); 533 } else { 534 r.appendChild(createLinkCell(build.job, build.url, "")); 535 } 536 r.appendChild(createTimeCell(i, parseInt(build.started))); 537 r.appendChild(createTextCell(build.duration)); 538 builds.appendChild(r); 539 } 540 const jobCount = document.getElementById("job-count"); 541 jobCount.textContent = "Showing " + Math.min(totalJob, 500) + "/" + totalJob + " jobs"; 542 drawJobBar(totalJob, jobCountMap); 543 } 544 545 function createTextCell(text) { 546 var c = document.createElement("td"); 547 c.appendChild(document.createTextNode(text)); 548 return c; 549 } 550 551 function createTimeCell(id, time) { 552 var momentTime = moment.unix(time); 553 var tid = "time-cell-" + id; 554 var main = document.createElement("div"); 555 var isADayOld = momentTime.isBefore(moment().startOf('day')); 556 main.textContent = momentTime.format(isADayOld ? 'MMM DD HH:mm:ss' : 'HH:mm:ss'); 557 main.id = tid; 558 559 var tooltip = document.createElement("div"); 560 tooltip.textContent = momentTime.format('MMM DD YYYY, HH:mm:ss [UTC]ZZ'); 561 tooltip.setAttribute("data-mdl-for", tid); 562 tooltip.classList.add("mdl-tooltip", "mdl-tooltip--large"); 563 564 var c = document.createElement("td"); 565 c.appendChild(main); 566 c.appendChild(tooltip); 567 568 return c; 569 } 570 571 function createLinkCell(text, url, title) { 572 const c = document.createElement("td"); 573 const a = document.createElement("a"); 574 a.href = url; 575 if (title !== "") { 576 a.title = title; 577 } 578 a.appendChild(document.createTextNode(text)); 579 c.appendChild(a); 580 return c; 581 } 582 583 function createRerunCell(modal, rerun_command, prowjob) { 584 const url = "https://" + window.location.hostname + "/rerun?prowjob=" 585 + prowjob; 586 const c = document.createElement("td"); 587 const icon = createIcon("refresh", "Show instructions for rerunning this job"); 588 icon.onclick = function () { 589 modal.style.display = "block"; 590 const rerun_html = "kubectl create -f \"<a href='" + url + "'>" 591 + url + "</a>\" " 592 + "<a class='mdl-button mdl-js-button mdl-button--icon' onclick=\""+ 593 "copyToClipboardWithToast('kubectl create -f " + url + "')\">" 594 + "<i class='material-icons state triggered' style='color: gray'>file_copy</i></a>"; 595 rerun_command.innerHTML = rerun_html; 596 }; 597 c.appendChild(icon); 598 c.classList.add("icon-cell"); 599 return c; 600 } 601 602 // copyToClipboard is from https://stackoverflow.com/a/33928558 603 // Copies a string to the clipboard. Must be called from within an 604 // event handler such as click. May return false if it failed, but 605 // this is not always possible. Browser support for Chrome 43+, 606 // Firefox 42+, Safari 10+, Edge and IE 10+. 607 // IE: The clipboard feature may be disabled by an administrator. By 608 // default a prompt is shown the first time the clipboard is 609 // used (per session). 610 function copyToClipboard(text) { 611 if (window.clipboardData && window.clipboardData.setData) { 612 // IE specific code path to prevent textarea being shown while dialog is visible. 613 return clipboardData.setData("Text", text); 614 615 } else if (document.queryCommandSupported && document.queryCommandSupported("copy")) { 616 var textarea = document.createElement("textarea"); 617 textarea.textContent = text; 618 textarea.style.position = "fixed"; // Prevent scrolling to bottom of page in MS Edge. 619 document.body.appendChild(textarea); 620 textarea.select(); 621 try { 622 return document.execCommand("copy"); // Security exception may be thrown by some browsers. 623 } catch (ex) { 624 console.warn("Copy to clipboard failed.", ex); 625 return false; 626 } finally { 627 document.body.removeChild(textarea); 628 } 629 } 630 } 631 632 function copyToClipboardWithToast(text) { 633 copyToClipboard(text); 634 const toast = document.body.querySelector("#toast"); 635 toast.MaterialSnackbar.showSnackbar({message: "Copied to clipboard"}); 636 } 637 638 function stateCell(state) { 639 const c = document.createElement("td"); 640 if (!state || state === "") { 641 c.appendChild(document.createTextNode("")); 642 return c; 643 } 644 c.classList.add("icon-cell"); 645 646 let displayState = stateToAdj(state); 647 displayState = displayState[0].toUpperCase() + displayState.slice(1); 648 let displayIcon = ""; 649 switch (state) { 650 case "triggered": 651 displayIcon = "schedule"; 652 break; 653 case "pending": 654 displayIcon = "watch_later"; 655 break; 656 case "success": 657 displayIcon = "check_circle"; 658 break; 659 case "failure": 660 displayIcon = "error"; 661 break; 662 case "aborted": 663 displayIcon = "remove_circle"; 664 break; 665 case "error": 666 displayIcon = "warning"; 667 break; 668 } 669 const stateIndicator = document.createElement("I"); 670 stateIndicator.classList.add("material-icons", "state", state); 671 stateIndicator.innerText = displayIcon; 672 c.appendChild(stateIndicator); 673 c.title = displayState; 674 675 return c; 676 } 677 678 function batchRevisionCell(build) { 679 var c = document.createElement("td"); 680 var pr_refs = build.refs.split(","); 681 for (var i = 1; i < pr_refs.length; i++) { 682 if (i != 1) { 683 c.appendChild(document.createTextNode(", ")); 684 } 685 var pr = pr_refs[i].split(":")[0]; 686 var l = document.createElement("a"); 687 l.href = "https://github.com/" + build.repo + "/pull/" + pr; 688 l.text = pr; 689 c.appendChild(document.createTextNode("#")); 690 c.appendChild(l); 691 } 692 return c; 693 } 694 695 function pushRevisionCell(build) { 696 var c = document.createElement("td"); 697 var bl = document.createElement("a"); 698 bl.href = "https://github.com/" + build.repo + "/commit/" + build.base_sha; 699 bl.text = build.base_ref + " (" + build.base_sha.slice(0, 7) + ")"; 700 c.appendChild(bl); 701 return c; 702 } 703 704 function prRevisionCell(build) { 705 var c = document.createElement("td"); 706 c.appendChild(document.createTextNode("#")); 707 var pl = document.createElement("a"); 708 pl.href = "https://github.com/" + build.repo + "/pull/" + build.number; 709 pl.text = build.number; 710 c.appendChild(pl); 711 c.appendChild(document.createTextNode(" (")); 712 var cl = document.createElement("a"); 713 cl.href = "https://github.com/" + build.repo + "/pull/" + build.number 714 + '/commits/' + build.pull_sha; 715 cl.text = build.pull_sha.slice(0, 7); 716 c.appendChild(cl); 717 c.appendChild(document.createTextNode(") by ")); 718 var al = document.createElement("a"); 719 al.href = "https://github.com/" + build.author; 720 al.text = build.author; 721 c.appendChild(al); 722 return c; 723 } 724 725 function drawJobBar(total, jobCountMap) { 726 const states = ["success", "pending", "triggered", "error", "failure", "aborted", ""]; 727 states.sort((s1, s2) => { 728 return jobCountMap.get(s1) - jobCountMap.get(s2); 729 }); 730 states.forEach((state, index) => { 731 const count = jobCountMap.get(state); 732 // If state is undefined or empty, treats it as unkown state. 733 if (!state || state === "") { 734 state = "unknown"; 735 } 736 const id = "job-bar-" + state; 737 const el = document.getElementById(id); 738 const tt = document.getElementById(state + "-tooltip"); 739 if (!count || count === 0 || total === 0) { 740 el.textContent = ""; 741 tt.textContent = ""; 742 el.style.width = "0"; 743 } else { 744 el.textContent = count; 745 tt.textContent = count + " " + stateToAdj(state) + " jobs"; 746 if (index === states.size - 1) { 747 el.style.width = "auto"; 748 } else { 749 el.style.width = Math.max((count / total * 100), 1) + "%"; 750 } 751 } 752 }); 753 } 754 755 function stateToAdj(state) { 756 switch (state) { 757 case "success": 758 return "succeeded"; 759 case "failure": 760 return "failed"; 761 default: 762 return state; 763 } 764 } 765 766 /** 767 * Returns an icon element. 768 * @param {string} iconString icon name 769 * @param {string} tooltip tooltip string 770 * @return {Element} 771 */ 772 function createIcon(iconString, tooltip = "") { 773 const icon = document.createElement("I"); 774 icon.classList.add(...["icon-button", "material-icons"]); 775 icon.innerHTML = iconString; 776 if (tooltip !== "") { 777 icon.title = tooltip; 778 } 779 780 const container = document.createElement("A"); 781 container.appendChild(icon); 782 container.classList.add(...["mdl-button", "mdl-js-button", "mdl-button--icon"]); 783 784 return container; 785 }