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  }