github.com/yrj2011/jx-test-infra@v0.0.0-20190529031832-7a2065ee98eb/prow/cmd/deck/static/pr-script.js (about)

     1  'use strict';
     2  
     3  /**
     4   * A Tide Query helper class that checks whether a pr is covered by the query.
     5   */
     6  class TideQuery {
     7      constructor(query) {
     8          this.repos = query.repos;
     9          this.orgs = query.orgs;
    10          this.labels = query.labels;
    11          this.missingLabels = query.missingLabels;
    12      }
    13  
    14    /**
    15     * Returns true if the pr is covered by the query.
    16     * @param pr
    17     * @returns {boolean}
    18     */
    19    matchPr(pr) {
    20          let isMatched = false;
    21          if (this.repos) {
    22              isMatched |= this.repos.indexOf(pr.Repository.NameWithOwner) !== -1;
    23          } else if (this.orgs) {
    24              isMatched |= this.orgs.indexOf(pr.Repository.Owner.Login) !== -1;
    25          }
    26          return isMatched;
    27      }
    28  
    29    /**
    30     * Returns labels and missing labels of the query.
    31     * @returns {{labels: string[], missingLabels: string[]}}
    32     */
    33    getQuery() {
    34          return {
    35              labels: this.labels,
    36              missingLabels: this.missingLabels
    37          }
    38      }
    39  }
    40  
    41  /**
    42   * Submit the query by redirecting the page with the query and let window.onload
    43   * sends the request.
    44   * @param {Element} input query input element
    45   */
    46  function submitQuery(input) {
    47      const query = getPRQuery(input.value);
    48      input.value = query;
    49      window.location.search = '?query=' + encodeURIComponent(query);
    50  }
    51  
    52  /**
    53   * Creates a XMLHTTP request to /pr-data.js.
    54   * @param {function} fulfillFn
    55   * @param {function} errorHandler
    56   * @return {XMLHttpRequest}
    57   */
    58  function createXMLHTTPRequest(fulfillFn, errorHandler) {
    59      const request = new XMLHttpRequest();
    60      const url = "/pr-data.js";
    61      request.onreadystatechange = () => {
    62          if (request.readyState === 4 && request.status === 200) {
    63              fulfillFn(request);
    64          }
    65      };
    66      request.onerror = () => {
    67          errorHandler();
    68      };
    69      request.withCredentials = true;
    70      request.open("POST", url, true);
    71      request.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
    72  
    73      return request;
    74  }
    75  
    76  /**
    77   * Makes sure the search query is looking for pull requests by dropping all
    78   * is:issue and type:pr tokens and adds is:pr if does not exist.
    79   * @param {string} q
    80   * @return {string}
    81   */
    82  function getPRQuery(q) {
    83      const tokens = q.replace(/\+/g, " ").split(" ");
    84      // Firstly, drop all pr/issue search tokens
    85      let result = tokens.filter(tkn => {
    86          tkn = tkn.trim();
    87          return !(tkn === "is:issue" || tkn === "type:issue" || tkn === "is:pr"
    88              || tkn === "type:pr");
    89      }).join(" ");
    90      // Returns the query with is:pr to the start of the query
    91      result = "is:pr " + result;
    92      return result;
    93  }
    94  
    95  /**
    96   * Redraw the page
    97   * @param {Object} prData
    98   */
    99  function redraw(prData) {
   100      const mainContainer = document.querySelector("#main-container");
   101      while (mainContainer.firstChild) {
   102          mainContainer.removeChild(mainContainer.firstChild);
   103      }
   104      if (prData && prData.Login) {
   105          loadPrStatus(prData);
   106      } else {
   107          forceGithubLogin();
   108      }
   109  }
   110  
   111  /**
   112   * Enables/disables the progress bar.
   113   * @param {boolean} isStarted
   114   */
   115  function loadProgress(isStarted) {
   116      const pg = document.querySelector("#loading-progress");
   117      if (isStarted) {
   118          pg.classList.remove("hidden");
   119      } else {
   120          pg.classList.add("hidden");
   121      }
   122  }
   123  
   124  /**
   125   * Handles the URL query on load event.
   126   */
   127  function onLoadQuery() {
   128      const query = window.location.search.substring(1);
   129      const params = query.split("&");
   130      if (!params[0]) {
   131          return "";
   132      }
   133      const val = params[0].slice("query=".length);
   134      if (val && val !== "") {
   135          return decodeURIComponent(val.replace(/\+/g, ' '));
   136      }
   137      return "";
   138  }
   139  
   140  /**
   141   * Gets cookie by its name.
   142   * @param {string} name
   143   */
   144  function getCookieByName(name) {
   145      if (!document.cookie) {
   146          return "";
   147      }
   148      const cookies = decodeURIComponent(document.cookie).split(";");
   149      for (let i = 0; i < cookies.length; i++) {
   150          const c = cookies[i].trim();
   151          const pref = name + "=";
   152          if (c.indexOf(pref) === 0) {
   153              return c.slice(pref.length);
   154          }
   155      }
   156      return "";
   157  }
   158  
   159  window.onload = () => {
   160      document.querySelectorAll("dialog").forEach((dialog) => {
   161          dialogPolyfill.registerDialog(dialog);
   162          dialog.querySelector('.close').addEventListener('click', () => {
   163              dialog.close();
   164          });
   165      });
   166      // Check URL, if the search is empty, adds search query by default format
   167      // ?is:pr state:open query="author:<user_login>"
   168      if (window.location.search === "") {
   169          const login = getCookieByName("github_login");
   170          const searchQuery = "is:pr state:open " + "author:" + login;
   171          window.location.search = "?query=" + encodeURIComponent(searchQuery);
   172      }
   173      const request = createXMLHTTPRequest((request) => {
   174          const prData = JSON.parse(request.responseText);
   175          redraw(prData);
   176          loadProgress(false);
   177      }, () => {
   178          loadProgress(false);
   179          const mainContainer = document.querySelector("#main-container");
   180              mainContainer.appendChild(createMessage("Something wrongs! We could not fulfill your request"));
   181      });
   182      loadProgress(true);
   183      request.send("query=" + onLoadQuery());
   184  };
   185  
   186  function createSearchCard() {
   187      const searchCard = document.createElement("DIV");
   188      searchCard.id = "search-card";
   189      searchCard.classList.add("pr-card", "mdl-card");
   190  
   191      // Input box
   192      const input = document.createElement("TEXTAREA");
   193      input.id = "search-input";
   194      input.value = decodeURIComponent(window.location.search.slice("query=".length + 1));
   195      input.rows = 1;
   196      input.spellcheck = false;
   197      input.addEventListener("keydown", (event) => {
   198          if (event.keyCode === 13) {
   199              event.preventDefault();
   200              submitQuery(input);
   201          } else {
   202              const el = event.target;
   203              el.style.height  = "auto";
   204              el.style.height = event.target.scrollHeight + "px";
   205          }
   206      });
   207      input.addEventListener("focus", (event) => {
   208          const el = event.target;
   209          el.style.height  = "auto";
   210          el.style.height = event.target.scrollHeight + "px";
   211      });
   212      // Refresh button
   213      const refBtn = createIcon("refresh", "Reload the query", ["search-button"], true);
   214      refBtn.addEventListener("click", () => {
   215          submitQuery(input);
   216      }, true);
   217      const userBtn = createIcon("person", "Show my open pull requests", ["search-button"], true);
   218      userBtn.addEventListener("click", () => {
   219          const login = getCookieByName("github_login");
   220          const searchQuery = "is:pr state:open " + "author:" + login;
   221          window.location.search = "?query=" + encodeURIComponent(searchQuery);
   222      });
   223  
   224      const actionCtn = document.createElement("DIV");
   225      actionCtn.id = "search-action";
   226      actionCtn.appendChild(userBtn);
   227      actionCtn.appendChild(refBtn);
   228  
   229      const inputContainer = document.createElement("DIV");
   230      inputContainer.id = "search-input-ctn";
   231      inputContainer.appendChild(input);
   232      inputContainer.appendChild(actionCtn);
   233  
   234      const title = document.createElement("H6");
   235      title.textContent = "Github search query";
   236      const infoBtn = createIcon("info", "More information about the search query", ["search-info"], true);
   237      const titleCtn = document.createElement("DIV");
   238      titleCtn.appendChild(title);
   239      titleCtn.appendChild(infoBtn);
   240      titleCtn.classList.add("search-title");
   241  
   242      const searchDialog = document.querySelector("#search-dialog");
   243      infoBtn.addEventListener("click", () => {
   244          searchDialog.showModal();
   245      });
   246  
   247      searchCard.appendChild(titleCtn);
   248      searchCard.appendChild(inputContainer);
   249      return searchCard;
   250  }
   251  
   252  /**
   253   * GetFullPRContexts gathers build jobs and pr contexts. It firstly takes
   254   * all pr contexts and only replaces contexts that have existing Prow Jobs. Tide
   255   * context will be omitted from the list.
   256   * @param builds
   257   * @param contexts
   258   * @returns {Array}
   259   */
   260  function getFullPRContext(builds, contexts) {
   261      const contextMap = new Map();
   262      if (contexts) {
   263          for (let context of contexts) {
   264              if (context.Context === "tide") {
   265                  continue;
   266              }
   267              contextMap.set(context.Context, {
   268                  context: context.Context,
   269                  description: context.Description,
   270                  state: context.State.toLowerCase(),
   271                  discrepancy: null,
   272          });
   273        }
   274      }
   275      if (builds) {
   276          for (let build of builds) {
   277              let discrepancy = null;
   278              // If Github context exits, check if mismatch or not.
   279              if (contextMap.has(build.context)) {
   280                  const githubContext = contextMap.get(build.context);
   281                  // TODO (qhuynh96): ProwJob's states and Github contexts states
   282                  // are not equivalent in some states.
   283                  if (githubContext.state !== build.state) {
   284                      discrepancy = "Github context and Prow Job states mismatch";
   285                  }
   286              }
   287              contextMap.set(build.context, {
   288                  context: build.context,
   289                  description: build.description,
   290                  state: build.state,
   291                  url: build.url,
   292                  discrepancy: discrepancy
   293              });
   294          }
   295      }
   296      const fullContexts = [];
   297      for (let value of contextMap.values()) {
   298          fullContexts.push(value);
   299      }
   300      return fullContexts;
   301  }
   302  
   303  /**
   304   * Loads Pr Status
   305   */
   306  function loadPrStatus(prData) {
   307      const tideQueries = [];
   308      for (let query of tideData.TideQueries) {
   309          tideQueries.push(new TideQuery(query));
   310      }
   311  
   312      const container = document.querySelector("#main-container");
   313      container.appendChild(createSearchCard());
   314      if (!prData.PullRequestsWithContexts || prData.PullRequestsWithContexts.length === 0) {
   315          const msg = createMessage("No open PRs found", "");
   316          container.appendChild(msg);
   317          return;
   318      }
   319      for (let prWithContext of prData.PullRequestsWithContexts) {
   320          // There might be multiple runs of jobs for a build.
   321          // allBuilds is sorted with the most recent builds first, so
   322          // we only need to keep the first build for each job name.
   323          let pr = prWithContext.PullRequest;
   324          let seenJobs = {};
   325          let builds = [];
   326          for (let build of allBuilds) {
   327              if (build.type === 'presubmit' &&
   328                  build.repo === pr.Repository.NameWithOwner &&
   329                  build.base_ref === pr.BaseRef.Name &&
   330                  build.number === pr.Number &&
   331                  build.pull_sha === pr.HeadRefOID) {
   332                  if (!seenJobs[build.job]) {  // First (latest) build for job.
   333                      seenJobs[build.job] = true;
   334                      builds.push(build);
   335                  }
   336              }
   337          }
   338          const githubContexts = prWithContext.Contexts;
   339          const contexts = getFullPRContext(builds, githubContexts);
   340          const validQueries = [];
   341          for (let query of tideQueries) {
   342             if (query.matchPr(pr)) {
   343                 validQueries.push(query.getQuery());
   344             }
   345          }
   346          container.appendChild(createPRCard(pr, contexts, validQueries, tideData.Pools));
   347      }
   348  }
   349  
   350  /**
   351   * Creates Pool labels.
   352   * @param pr
   353   * @param tidePool
   354   * @return {Element}
   355   */
   356  function createTidePoolLabel(pr, tidePool) {
   357      if (!tidePool || tidePool.length === 0) {
   358          return null;
   359      }
   360      const poolTypes = [tidePool.Target, tidePool.BatchPending,
   361          tidePool.SuccessPRs, tidePool.PendingPRs, tidePool.MissingPRs];
   362      const inPoolId = poolTypes.findIndex(poolType => {
   363          if (!poolType) {
   364              return false;
   365          }
   366          const index = poolType.findIndex(prInPool => {
   367              return prInPool.Number === pr.Number;
   368          });
   369          return index !== -1;
   370      });
   371      const label = document.createElement("SPAN");
   372      if (inPoolId === -1) {
   373          return null;
   374      }
   375      const labelTitle = ["Merging", "In Batch & Test Pending",
   376          "Test Passing & Merge Pending", "Test Pending",
   377          "Test failed/Missing Labels"];
   378      const labelStyle = ["merging", "batching", "passing", "pending", "failed"];
   379      label.textContent = "In Pool - " + labelTitle[inPoolId];
   380      label.classList.add("title-label", "mdl-shadow--2dp", labelStyle[inPoolId]);
   381  
   382      return label;
   383  }
   384  
   385  /**
   386   * Creates a label for the title. It will prioritise the merge status over the
   387   * job status. Saying that, if the pr has jobs failed and does not meet merge
   388   * requirements, it will show that the PR needs to resolve labels.
   389   * @param isMerge {boolean}
   390   * @param jobStatus {string}
   391   * @param mergeAbility {number}
   392   * @return {Element}
   393   */
   394  function createTitleLabel(isMerge, jobStatus, mergeAbility) {
   395      const label = document.createElement("SPAN");
   396      label.classList.add("title-label");
   397      if (isMerge) {
   398          label.textContent = "Merged";
   399          label.classList.add("merging");
   400      } else if (mergeAbility === -1) {
   401          label.textContent = "Unknown Merge Requirements";
   402          label.classList.add("unknown");
   403      } else if (mergeAbility === 0) {
   404          label.textContent = "Needs to Resolve Labels";
   405          label.classList.add("pending");
   406      } else {
   407          if (jobStatus === "succeeded") {
   408              label.textContent = "Good to be merged";
   409              label.classList.add(jobStatus);
   410          } else {
   411              label.textContent = "Jobs " + jobStatus;
   412              label.classList.add(jobStatus);
   413          }
   414      }
   415  
   416      return label;
   417  }
   418  
   419  /**
   420   * Creates PR Card title.
   421   * @param {Object} pr
   422   * @param {Array<Object>} tidePools
   423   * @param {string} jobStatus
   424   * @param {number} mergeAbility
   425   * @return {Element}
   426   */
   427  function createPRCardTitle(pr, tidePools, jobStatus, mergeAbility) {
   428      const prTitle = document.createElement("DIV");
   429      prTitle.classList.add("mdl-card__title");
   430  
   431      const title = document.createElement("H4");
   432      title.textContent = "#" + pr.Number;
   433      title.classList.add("mdl-card__title-text");
   434  
   435      const subtitle = document.createElement("H5");
   436      subtitle.textContent = pr.Repository.NameWithOwner;
   437      subtitle.classList.add("mdl-card__subtitle-text");
   438  
   439      const link = document.createElement("A");
   440      link.href = "https://github.com/" + pr.Repository.NameWithOwner + "/pull/"
   441          + pr.Number;
   442      link.appendChild(title);
   443  
   444      const prTitleText = document.createElement("DIV");
   445      prTitleText.appendChild(link);
   446      prTitleText.appendChild(subtitle);
   447      prTitleText.classList.add("pr-title-text");
   448      prTitle.appendChild(prTitleText);
   449  
   450      const pool = tidePools.filter(pool => {
   451          const repo = pool.Org + "/" + pool.Repo;
   452          return pr.Repository.NameWithOwner === repo && pr.BaseRef.Name
   453              === pool.Branch;
   454      });
   455      let tidePoolLabel = createTidePoolLabel(pr, pool[0]);
   456      if (!tidePoolLabel) {
   457          tidePoolLabel = createTitleLabel(pr.Merged, jobStatus, mergeAbility);
   458      }
   459      prTitle.appendChild(tidePoolLabel);
   460  
   461      return prTitle;
   462  }
   463  
   464  /**
   465   * Creates a list of contexts.
   466   * @param contexts
   467   * @param itemStyle
   468   * @return {Element}
   469   */
   470  function createContextList(contexts, itemStyle = []) {
   471      const container = document.createElement("UL");
   472      container.classList.add("mdl-list", "job-list");
   473      const getStateIcon = (state) => {
   474          switch (state) {
   475              case "success":
   476                  return "check_circle";
   477              case "failure":
   478                  return "error";
   479              case "pending":
   480                  return "watch_later";
   481              case "triggered":
   482                  return "schedule";
   483              case "aborted":
   484                  return "remove_circle";
   485              case "error":
   486                  return "warning";
   487              default:
   488                  return "";
   489          }
   490      };
   491      const getItemContainer = (context) => {
   492          if (context.url) {
   493              const item = document.createElement("A");
   494              item.href = context.url;
   495              return item;
   496          } else {
   497              return document.createElement("DIV");
   498          }
   499      };
   500      contexts.forEach(context => {
   501          const elCon = document.createElement("LI");
   502          elCon.classList.add("mdl-list__item", "job-list-item", ...itemStyle);
   503          const item = getItemContainer(context);
   504          item.classList.add("mdl-list__item-primary-content");
   505          item.appendChild(createIcon(
   506              getStateIcon(context.state),
   507              "",
   508              ["state", context.state, "mdl-list__item-icon"]));
   509          item.appendChild(document.createTextNode(context.context));
   510          if (context.discrepancy) {
   511              item.appendChild(createIcon(
   512                  "warning",
   513                  context.discrepancy,
   514                  ["state", "context-warning", "mdl-list__item-icon"]));
   515          }
   516          elCon.appendChild(item);
   517          if (context.description) {
   518              const itemDesc = document.createElement("SPAN");
   519              itemDesc.textContent = context.description;
   520              itemDesc.style.color = "grey";
   521              itemDesc.style.fontSize = "14px";
   522              elCon.appendChild(itemDesc);
   523          }
   524          container.appendChild(elCon);
   525      });
   526      return container;
   527  }
   528  
   529  /**
   530   * Creates Job status.
   531   * @param builds
   532   * @return {Element}
   533   */
   534  function createJobStatus(builds) {
   535      const statusContainer = document.createElement("DIV");
   536      statusContainer.classList.add("status-container");
   537      const status = document.createElement("DIV");
   538      const failedJobs = builds.filter(build => {
   539          return build.state === "failure";
   540      });
   541      // Job status indicator
   542      const state = jobStatus(builds);
   543      let statusText = "";
   544      let stateIcon = "";
   545      switch (state) {
   546          case "succeeded":
   547              statusText = "All tests passed";
   548              stateIcon = "check_circle";
   549              break;
   550          case "failed":
   551              statusText = failedJobs.length + " test" + (failedJobs.length === 1 ? "" : "s") + " failed";
   552              stateIcon = "error";
   553              break;
   554          case "unknown":
   555              statusText = "No test found";
   556              break;
   557          default:
   558              statusText = "Tests are running";
   559              stateIcon = "watch_later";
   560      }
   561      const arrowIcon = createIcon("expand_more");
   562      arrowIcon.classList.add("arrow-icon");
   563      if (state === "unknown") {
   564          arrowIcon.classList.add("hidden");
   565          const p = document.createElement("P");
   566          p.textContent = "Test results for this PR are not in our record but you can always find them on PR's GitHub page. Sorry for any inconvenience!";
   567  
   568          status.appendChild(document.createTextNode(statusText));
   569          status.appendChild(createStatusHelp("No test found", [p]));
   570          status.classList.add("no-status");
   571      } else {
   572          status.appendChild(createIcon(stateIcon, "", ["status-icon", state]));
   573          status.appendChild(document.createTextNode(statusText));
   574      }
   575      status.classList.add("status");
   576      statusContainer.appendChild(status);
   577      // Job list
   578      let failedJobsList;
   579      if (failedJobs.length > 0) {
   580          failedJobsList = createContextList(failedJobs);
   581          statusContainer.appendChild(failedJobsList);
   582      }
   583      const jobList = createContextList(builds);
   584      jobList.classList.add("hidden");
   585      status.addEventListener("click", () => {
   586          if (state === "unknown") {
   587              return;
   588          }
   589          if (failedJobsList) {
   590              failedJobsList.classList.add("hidden");
   591          }
   592          jobList.classList.toggle("hidden");
   593          arrowIcon.textContent = arrowIcon.textContent === "expand_more"
   594              ? "expand_less" : "expand_more";
   595      });
   596  
   597      status.appendChild(arrowIcon);
   598      statusContainer.appendChild(jobList);
   599      return statusContainer;
   600  }
   601  
   602  /**
   603   * Creates a merge requirement cell.
   604   * @param labels
   605   * @param notMissingLabel
   606   * @return {Element}
   607   */
   608  function createMergeLabelCell(labels, notMissingLabel = false) {
   609      const cell = document.createElement("TD");
   610      labels.forEach(label => {
   611          const labelEl = document.createElement("SPAN");
   612          const name = label.name.split(" ").join("");
   613          labelEl.classList.add("merge-table-label", "mdl-shadow--2dp", "label",
   614              name);
   615          labelEl.textContent = label.name;
   616          const toDisplay = label.own ^ notMissingLabel;
   617          if (toDisplay) {
   618              cell.appendChild(labelEl);
   619          }
   620      });
   621  
   622      return cell;
   623  }
   624  
   625  function processLabelName(label) {
   626      label = label.split(" ").join("");
   627      if (label.startsWith("area/")) {
   628          return "area";
   629      } else if (label.startsWith("size/")) {
   630          return label.slice(5);
   631      } else {
   632          return label;
   633      }
   634  }
   635  
   636  /**
   637   * Appends labels to a container
   638   * @param {Element} container
   639   * @param {Array<string>} labels
   640   */
   641  function appendLabelsToContainer(container, labels) {
   642      while (container.firstChild) {
   643          container.removeChild(container.firstChild);
   644      }
   645      labels.forEach(label => {
   646          const labelEl = document.createElement("SPAN");
   647  
   648          let labelName = processLabelName(label);
   649          labelEl.classList.add("merge-table-label", "mdl-shadow--2dp", "label", labelName);
   650          labelEl.textContent = label;
   651          container.appendChild(labelEl);
   652      });
   653  }
   654  
   655  /**
   656   * Creates merge requirement table for queries.
   657   * @param prLabels
   658   * @param queries
   659   * @return {Element}
   660   */
   661  function createQueriesTable(prLabels, queries) {
   662      const table = document.createElement("TABLE");
   663      table.classList.add("merge-table");
   664      const thead = document.createElement("THEAD");
   665      const allLabelHeaderRow = document.createElement("TR");
   666      const allLabelHeaderCell = document.createElement("TD");
   667      // Creates all pr labels header.
   668      allLabelHeaderCell.textContent = "PR's Labels";
   669      allLabelHeaderCell.colSpan = 3;
   670      allLabelHeaderRow.appendChild(allLabelHeaderCell);
   671      thead.appendChild(allLabelHeaderRow);
   672  
   673      const allLabelRow = document.createElement("TR");
   674      const allLabelCell = document.createElement("TD");
   675      allLabelCell.colSpan = 3;
   676      appendLabelsToContainer(allLabelCell, prLabels.map(label => {
   677          return label.Label.Name;
   678      }));
   679      allLabelRow.appendChild(allLabelCell);
   680      thead.appendChild(allLabelRow);
   681  
   682      const tableRow = document.createElement("TR");
   683      const col1 = document.createElement("TD");
   684      col1.textContent = "Required Labels (Missing)";
   685      const col2 = document.createElement("TD");
   686      col2.textContent = "Forbidden Labels (Shouldn't have)";
   687      const col3 = document.createElement("TD");
   688  
   689      const body = document.createElement("TBODY");
   690      queries.forEach(query => {
   691          const row = document.createElement("TR");
   692          row.append(createMergeLabelCell(query.labels, true));
   693          row.append(createMergeLabelCell(query.missingLabels));
   694  
   695          const mergeIcon = document.createElement("TD");
   696          mergeIcon.classList.add("merge-table-icon");
   697          const iconButton = createIcon("information", "Clicks to see query details", [], true);
   698          mergeIcon.appendChild(iconButton);
   699          row.appendChild(mergeIcon);
   700  
   701          body.appendChild(row);
   702          const dialog = document.querySelector("#query-dialog");
   703          const allRequired = document.querySelector("#query-all-required");
   704          const allForbidden = document.querySelector("#query-all-forbidden");
   705          iconButton.addEventListener("click", () => {
   706              appendLabelsToContainer(allRequired, query.labels.map(label => {
   707                  return label.name;
   708              }));
   709              appendLabelsToContainer(allForbidden, query.missingLabels.map(label => {
   710                  return label.name;
   711              }));
   712              dialog.showModal();
   713          });
   714      });
   715  
   716      tableRow.appendChild(col1);
   717      tableRow.appendChild(col2);
   718      tableRow.appendChild(col3);
   719      thead.appendChild(tableRow);
   720      table.appendChild(thead);
   721      table.appendChild(body);
   722  
   723      return table;
   724  }
   725  
   726  /**
   727   * Creates the merge requirement status.b
   728   * @param prLabels
   729   * @param queries
   730   * @return {Element}
   731   */
   732  function createMergeStatus(prLabels, queries) {
   733      prLabels = prLabels ? prLabels : [];
   734      const statusContainer = document.createElement("DIV");
   735      statusContainer.classList.add("status-container");
   736      const status = document.createElement("DIV");
   737      const mergeAbility = isAbleToMerge(queries);
   738      if (mergeAbility === 0) {
   739          status.appendChild(createIcon("error", "", ["status-icon", "failed"]));
   740          status.appendChild(document.createTextNode("Does not meet merge requirements"));
   741          // Creates help button
   742          const iconButton = createIcon("help", "", ["help-icon-button"], true);
   743          status.appendChild(iconButton);
   744          // Shows dialog
   745          const dialog = document.querySelector("#merge-help-dialog");
   746          iconButton.addEventListener("click", (event) => {
   747              dialog.showModal();
   748              event.stopPropagation();
   749          });
   750      } else if (mergeAbility === 1) {
   751          status.appendChild(createIcon("check_circle", "", ["status-icon", "succeeded"]));
   752          status.appendChild(document.createTextNode("Meets merge requirements"));
   753      } else {
   754          status.appendChild(document.createTextNode("No Tide query found"));
   755          status.classList.add("no-status");
   756          const p = document.createElement("P");
   757          p.textContent = "This repo may not be configured to use Tide.";
   758          status.appendChild(createStatusHelp("Tide query not found", [p]));
   759      }
   760      const arrowIcon= createIcon("expand_less");
   761      arrowIcon.classList.add("arrow-icon");
   762  
   763      status.classList.add("status");
   764      status.appendChild(arrowIcon);
   765  
   766      const queriesTable = createQueriesTable(prLabels, queries);
   767      if (mergeAbility !== 0) {
   768          queriesTable.classList.add("hidden");
   769          arrowIcon.textContent = "expand_more";
   770      }
   771      status.addEventListener("click", () => {
   772          queriesTable.classList.toggle("hidden");
   773          if (queriesTable.classList.contains("hidden")) {
   774              const offLabels = queriesTable.querySelectorAll(
   775                  ".merge-table-label.off");
   776              offLabels.forEach(offLabel => {
   777                  offLabel.classList.add("hidden");
   778              });
   779          }
   780          arrowIcon.textContent = arrowIcon.textContent === "expand_more"
   781              ? "expand_less" : "expand_more";
   782      });
   783  
   784      statusContainer.appendChild(status);
   785      statusContainer.appendChild(queriesTable);
   786      return statusContainer;
   787  }
   788  
   789  /**
   790   * Creates a help button on the status.
   791   * @param {string} title
   792   * @param {Array<Element>} content
   793   * @return {Element}
   794   */
   795  function createStatusHelp(title, content) {
   796      const dialog = document.querySelector("#status-help-dialog");
   797      const dialogTitle = dialog.querySelector(".mdl-dialog__title");
   798      const dialogContent = dialog.querySelector(".mdl-dialog__content");
   799      const helpIcon = createIcon("help", "", ["help-icon-button"], true);
   800      helpIcon.addEventListener("click", (event) => {
   801          dialogTitle.textContent = title;
   802          while (dialogContent.firstChild) {
   803              dialogContent.removeChild(dialogContent.firstChild);
   804          }
   805          content.forEach(el => {
   806              dialogContent.appendChild(el);
   807          });
   808          dialog.showModal();
   809          event.stopPropagation();
   810      });
   811  
   812      return helpIcon;
   813  }
   814  
   815  function createPRCardBody(pr, builds, queries) {
   816      const cardBody = document.createElement("DIV");
   817      const title = document.createElement("H3");
   818      title.textContent = pr.Title;
   819  
   820      cardBody.classList.add("mdl-card__supporting-text");
   821      cardBody.appendChild(title);
   822      cardBody.appendChild(createJobStatus(builds));
   823      const nodes = pr.Labels && pr.Labels.Nodes ? pr.Labels.Nodes : [];
   824      cardBody.appendChild(createMergeStatus(nodes, queries));
   825  
   826      return cardBody;
   827  }
   828  
   829  /**
   830   * Compare function that prioritizes jobs which are in failure state.
   831   * @param a
   832   * @param b
   833   * @return {number}
   834   */
   835  function compareJobFn(a, b) {
   836      const stateToPrio = [];
   837      stateToPrio["success"] = stateToPrio["expected"] = 3;
   838      stateToPrio["aborted"] = 2;
   839      stateToPrio["pending"] = stateToPrio["triggered"] = 1;
   840      stateToPrio["error"] = stateToPrio["failure"] = 0;
   841  
   842      return stateToPrio[a.state] > stateToPrio[b.state] ? 1
   843          : stateToPrio[a.state] < stateToPrio[b.state] ? -1 : 0;
   844  }
   845  
   846  /**
   847   * Creates a PR card.
   848   * @param {Object} pr
   849   * @param {Array<Object>} builds
   850   * @param {Array<Object>} queries
   851   * @param {Array<Object>} tidePools
   852   * @return {Element}
   853   */
   854  function createPRCard(pr, builds = [], queries = [], tidePools = []) {
   855      builds = builds ? builds : [];
   856      queries = queries ? queries : [];
   857      tidePools = tidePools ? tidePools : [];
   858      const prCard = document.createElement("DIV");
   859      // jobs need to be sorted from high priority (failure, error) to low
   860      // priority (success)
   861      builds.sort(compareJobFn);
   862      const prLabelsSet = new Set();
   863      if (pr.Labels && pr.Labels.Nodes) {
   864          pr.Labels.Nodes.forEach(label => {
   865              prLabelsSet.add(label.Label.Name);
   866          });
   867      }
   868      const processedQuery = [];
   869      queries.forEach(query => {
   870          let score = 0.0;
   871          const labels = [];
   872          const missingLabels = [];
   873          query.labels.sort((a, b) => {
   874              if (a.length === b.length) {
   875                  return 0;
   876              }
   877              return a.length < b.length ? -1 : 1;
   878          });
   879          query.missingLabels.sort((a, b) => {
   880              if (a.length === b.length) {
   881                  return 0;
   882              }
   883              return a.length < b.length ? -1 : 1;
   884          });
   885          query.labels.forEach(label => {
   886              labels.push({name: label, own: prLabelsSet.has(label)});
   887              score += labels[labels.length - 1].own ? 1 : 0;
   888          });
   889          query.missingLabels.forEach(label => {
   890              missingLabels.push({name: label, own: prLabelsSet.has(label)});
   891              score += missingLabels[missingLabels.length - 1].own ? 0 : 1;
   892          });
   893          score = (labels.length + missingLabels.length > 0) ? score
   894              / (labels.length + missingLabels.length) : 1.0;
   895          processedQuery.push(
   896              {score: score, labels: labels, missingLabels: missingLabels});
   897      });
   898      // Sort queries by descending score order.
   899      processedQuery.sort((q1, q2) => {
   900          if (Math.abs(q1.score - q2.score) < Number.EPSILON) {
   901              return 0;
   902          }
   903          return q1.score > q2.score ? -1 : 1;
   904      });
   905      prCard.classList.add("pr-card", "mdl-card");
   906      prCard.appendChild(createPRCardTitle(pr, tidePools, jobStatus(builds), isAbleToMerge(processedQuery)));
   907      prCard.appendChild(createPRCardBody(pr, builds, processedQuery));
   908      return prCard;
   909  }
   910  
   911  /**
   912   * Redirect to initiate github login flow.
   913   */
   914  function forceGithubLogin() {
   915  	window.location.href = window.location.origin + "/github-login";
   916  }
   917  
   918  /**
   919   * Returns the job status based on its state.
   920   * @param builds
   921   * @return {string}
   922   */
   923  function jobStatus(builds) {
   924      if (builds.length === 0) {
   925          return "unknown";
   926      }
   927      switch (builds[0].state) {
   928          case "success":
   929          case "expected":
   930              return "succeeded";
   931          case "failure":
   932          case "error":
   933              return "failed";
   934          default:
   935              return "pending";
   936      }
   937  }
   938  
   939  /**
   940   * Returns -1 if there is no query. 1 if the PR is able to be merged by checking
   941   * the score of the first query in the query list (score === 1), the list has
   942   * been sorted by scores, otherwise 0.
   943   * @param queries
   944   * @return {number}
   945   */
   946  function isAbleToMerge(queries) {
   947      if (queries.length === 0) {
   948          return -1;
   949      }
   950      return Math.abs(queries[0].score - 1.0) < Number.EPSILON ? 1 : 0;
   951  }
   952  
   953  /**
   954   * Returns an icon element.
   955   * @param {string} iconString icon name
   956   * @param {string} tooltip tooltip string
   957   * @param {Array<string>} styles
   958   * @param {boolean} isButton
   959   * @return {Element}
   960   */
   961  function createIcon(iconString, tooltip = "", styles = [], isButton = false) {
   962      const icon = document.createElement("I");
   963      icon.classList.add("icon-button", "material-icons");
   964      icon.textContent = iconString;
   965      if (tooltip !== "") {
   966          icon.title = tooltip;
   967      }
   968      if (!isButton) {
   969          icon.classList.add(...styles);
   970          return icon;
   971      }
   972      const container = document.createElement("BUTTON");
   973      container.appendChild(icon);
   974      container.classList.add("mdl-button", "mdl-js-button", "mdl-button--icon",
   975          ...styles);
   976  
   977      return container;
   978  }
   979  
   980  /**
   981   * Create a simple message with an icon.
   982   * @param msg
   983   * @param icStr
   984   * @return {HTMLElement}
   985   */
   986  function createMessage(msg, icStr) {
   987      const el = document.createElement("H3");
   988      el.textContent = msg;
   989      if (icStr !== "") {
   990          const ic = createIcon(icStr, "", ["message-icon"]);
   991          el.appendChild((ic));
   992      }
   993      const msgContainer = document.createElement("DIV");
   994      msgContainer.appendChild(el);
   995      msgContainer.classList.add("message");
   996  
   997      return msgContainer;
   998  }
   999  
  1000  document.addEventListener("DOMContentLoaded", function(event) {
  1001     configure();
  1002  });
  1003  
  1004  function configure() {
  1005      if (!branding){
  1006          return;
  1007      }
  1008      if (branding.logo !== '') {
  1009          document.getElementById('img').src = branding.logo;
  1010      }
  1011      if (branding.favicon !== '') {
  1012          document.getElementById('favicon').href = branding.favicon;
  1013      }
  1014      if (branding.background_color !== '') {
  1015          document.body.style.background = branding.background_color;
  1016      }
  1017      if (branding.header_color !== '') {
  1018          document.getElementsByTagName('header')[0].style.backgroundColor = branding.header_color;
  1019      }
  1020  }