github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/cmd/deck/static/tide/tide.ts (about)

     1  import {PullRequest, TideData, TidePool} from '../api/tide';
     2  import {tidehistory, tooltip} from '../common/common';
     3  
     4  declare const tideData: TideData;
     5  
     6  window.onload = (): void => {
     7    const infoDiv = document.getElementById("info-div")!;
     8    const infoH4 = infoDiv.getElementsByTagName("h4")[0]!;
     9    infoH4.addEventListener("click", infoToggle(infoDiv.getElementsByTagName("span")[0]), true);
    10  
    11    redraw();
    12  };
    13  
    14  function infoToggle(toToggle: HTMLElement): (event: Event) => void {
    15    return (event): void => {
    16      if (toToggle.className === "hidden") {
    17        toToggle.className = "";
    18        (event.target as HTMLElement).textContent = "Merge Requirements: (click to collapse)";
    19      } else {
    20        toToggle.className = "hidden";
    21        (event.target as HTMLElement).textContent = "Merge Requirements: (click to expand)";
    22      }
    23    };
    24  }
    25  
    26  function redraw(): void {
    27    redrawQueries();
    28    redrawPools();
    29  }
    30  
    31  function createLink(href: string, text: string): HTMLAnchorElement {
    32    const a = document.createElement("a");
    33    a.href = href;
    34    a.appendChild(document.createTextNode(text));
    35    return a;
    36  }
    37  
    38  /**
    39   * escapeLabel escaped label name that returns a valid name used for css
    40   * selector.
    41   */
    42  function escapeLabel(label: string): string {
    43    if (label === "") { return ""; }
    44    const toUnicode = (index: number): string => {
    45      const h = label.charCodeAt(index).toString(16).split('');
    46      while (h.length < 6) { h.splice(0, 0, '0'); }
    47  
    48      return `x${  h.join('')}`;
    49    };
    50    let result = "";
    51    const alphaNum = /^[0-9a-zA-Z]+$/;
    52  
    53    for (let i = 0; i < label.length; i++) {
    54      const c = label.charCodeAt(i);
    55      if ((i === 0 && c > 47 && c < 58) || !label[i].match(alphaNum)) {
    56        result += toUnicode(i);
    57        continue;
    58      }
    59      result += label[i];
    60    }
    61  
    62    return result;
    63  }
    64  
    65  /**
    66   * Creates a HTML element for the label given its name
    67   */
    68  function createLabelEl(label: string): HTMLElement {
    69    const el = document.createElement("span");
    70    const escapedName = escapeLabel(label);
    71    el.classList.add("mdl-shadow--2dp", "label", escapedName);
    72    el.textContent = label;
    73  
    74    return el;
    75  }
    76  
    77  function createStrong(text: string): HTMLElement {
    78    const s = document.createElement("strong");
    79    s.appendChild(document.createTextNode(text));
    80    return s;
    81  }
    82  
    83  function fillDetail(data: string | string[] | undefined, type: string, connector: string, container: HTMLElement, styleData: (content: string) => Node) {
    84    if (!data || (Array.isArray(data) && data.length === 0)) {
    85      return;
    86    }
    87    container.appendChild(createStrong(connector));
    88    container.appendChild(document.createTextNode(`the following ${type}: `));
    89    container.appendChild(document.createElement("br"));
    90  
    91    const ul = document.createElement("ul");
    92    const li = document.createElement("li");
    93    ul.appendChild(li);
    94    container.appendChild(ul);
    95  
    96    if (typeof data === 'string') {
    97      li.appendChild(document.createTextNode(data));
    98    } else  if (Array.isArray(data)) {
    99      for (let i = 0; i < data.length; i++) {
   100        const v = data[i];
   101        li.appendChild(styleData(v));
   102        if (i + 1 < data.length) {
   103          li.appendChild(document.createTextNode(" "));
   104        }
   105      }
   106    }
   107  }
   108  
   109  function redrawQueries(): void {
   110    const queries = document.getElementById("queries")!;
   111    while (queries.firstChild) {
   112      queries.removeChild(queries.firstChild);
   113    }
   114  
   115    if (!tideData.Queries) {
   116      return;
   117    }
   118    for (let i = 0; i < tideData.Queries.length; i++) {
   119      const query = tideData.Queries[i];
   120      const tideQuery = tideData.TideQueries[i];
   121  
   122      // create list entry for the query, all details will be within this element
   123      const li = document.createElement("li");
   124  
   125      // GitHub query search link
   126      const a = createLink(
   127        `/github-link?dest=${encodeURIComponent(`search?utf8=\u2713&q=${  query}`)}`,
   128        "GitHub Search Link",
   129      );
   130      li.appendChild(a);
   131      li.appendChild(document.createTextNode(" - Meaning: Is an open Pull Request"));
   132  
   133      // build the description
   134      // all queries should implicitly mean this
   135      // add the list of repos, defaulting to an empty array if no repos have been provided.
   136      const orgs = tideQuery.orgs || [];
   137      const repos = tideQuery.repos || [];
   138      const excludedRepos = tideQuery.excludedRepos || [];
   139      if (orgs.length > 0) {
   140        li.appendChild(document.createTextNode(" in one of the following orgs: "));
   141        const ul = document.createElement("ul");
   142        for (let j = 0; j < orgs.length; j++) {
   143          const innerLi = document.createElement("li");
   144          innerLi.appendChild(createLink(`/github-link?dest=${  orgs[j]}`, orgs[j]));
   145          if (j + 1 < repos.length) {
   146            innerLi.appendChild(document.createTextNode(", "));
   147          }
   148          ul.appendChild(innerLi);
   149        }
   150        li.appendChild(ul);
   151      }
   152      if (repos.length > 0) {
   153        let reposText = " in one of the following repos: ";
   154        if (orgs.length > 0) {
   155          reposText = ` or ${  reposText}`;
   156        }
   157        li.appendChild(document.createTextNode(reposText));
   158        const ul = document.createElement("ul");
   159        const innerLi = document.createElement("li");
   160        for (let j = 0; j < repos.length; j++) {
   161          innerLi.appendChild(createLink(`/github-link?dest=${  repos[j]}`, repos[j]));
   162          if (j + 1 < repos.length) {
   163            innerLi.appendChild(document.createTextNode(", "));
   164          }
   165        }
   166        ul.appendChild(innerLi);
   167        li.appendChild(ul);
   168      }
   169      if (excludedRepos.length > 0) {
   170        li.appendChild(document.createTextNode(" but NOT in any of the following excluded repos: "));
   171        const ul = document.createElement("ul");
   172        const innerLi = document.createElement("li");
   173        for (let j = 0; j < excludedRepos.length; j++) {
   174          innerLi.appendChild(createLink(`/github-link?dest=${  excludedRepos[j]}`, excludedRepos[j]));
   175          if (j + 1 < excludedRepos.length) {
   176            innerLi.appendChild(document.createTextNode(", "));
   177          }
   178        }
   179        ul.appendChild(innerLi);
   180        li.appendChild(ul);
   181      }
   182      // required labels
   183      fillDetail(tideQuery.labels, "labels", "with ", li, (data) => createLabelEl(data));
   184      // required to be not present labels
   185      fillDetail(tideQuery.missingLabels, "labels", "without ", li, (data) => createLabelEl(data));
   186      // list milestone if existed
   187      fillDetail(tideQuery.milestone, "milestone", "with ", li, (data) => document.createTextNode(data));
   188      // list all excluded branches
   189      fillDetail(tideQuery.excludedBranches, "branches", "exclude ", li, (data) => document.createTextNode(data));
   190      // list all included branches
   191      fillDetail(tideQuery.includedBranches, "branches", "targeting ", li, (data) => document.createTextNode(data));
   192      // GitHub native review required
   193      const reviewApprovedRequired = tideQuery.hasOwnProperty("reviewApprovedRequired") && tideQuery.reviewApprovedRequired;
   194      if (reviewApprovedRequired) {
   195        li.appendChild(document.createTextNode("and must be "));
   196        li.appendChild(createLink(
   197          "https://help.github.com/articles/about-pull-request-reviews/",
   198          "approved by GitHub review",
   199        ));
   200      }
   201  
   202      // actually add the entry
   203      queries.appendChild(li);
   204    }
   205  }
   206  
   207  function redrawPools(): void {
   208    const pools = document.getElementById("pools")!.getElementsByTagName("tbody")[0];
   209    while (pools.firstChild) {
   210      pools.removeChild(pools.firstChild);
   211    }
   212  
   213    if (!tideData.Pools) {
   214      return;
   215    }
   216    for (const pool of tideData.Pools) {
   217      const r = document.createElement("tr");
   218  
   219      r.appendChild(createHistoryCell(pool));
   220      r.appendChild(createRepoCell(pool));
   221      r.appendChild(createActionCell(pool));
   222      r.appendChild(createBatchCell(pool));
   223      r.appendChild(createPRCell(pool, pool.SuccessPRs));
   224      r.appendChild(createPRCell(pool, pool.PendingPRs));
   225      r.appendChild(createPRCell(pool, pool.MissingPRs));
   226  
   227      pools.appendChild(r);
   228    }
   229  }
   230  
   231  function createHistoryCell(pool: TidePool): HTMLTableDataCellElement {
   232    const td = document.createElement("td");
   233    td.classList.add("icon-cell");
   234    td.appendChild(tidehistory.poolIcon(pool.Org, pool.Repo, pool.Branch));
   235    return td;
   236  }
   237  
   238  function createRepoCell(pool: TidePool): HTMLTableDataCellElement {
   239    const deckLink = `/?repo=${  encodeURIComponent(`${pool.Org}/${pool.Repo}`)}`;
   240    const branchLink = `/github-link?dest=${pool.Org}/${pool.Repo}/tree/${pool.Branch}`;
   241    const linksTD = document.createElement("td");
   242    linksTD.appendChild(createLink(deckLink, `${pool.Org}/${pool.Repo}`));
   243    linksTD.appendChild(document.createTextNode(" "));
   244    linksTD.appendChild(createLink(branchLink, pool.Branch));
   245    return linksTD;
   246  }
   247  
   248  function createActionCell(pool: TidePool): HTMLTableDataCellElement {
   249    const targeted = pool.Target && pool.Target.length;
   250    const blocked = pool.Blockers && pool.Blockers.length;
   251    let action = pool.Action.replace("_", " ");
   252    if (targeted || blocked) {
   253      action += ": ";
   254    }
   255    const c = document.createElement("td");
   256    c.appendChild(document.createTextNode(action));
   257  
   258    if (blocked) {
   259      c.classList.add("blocked");
   260      addBlockersToElem(c, pool);
   261    } else if (targeted) {
   262      addPRsToElem(c, pool, pool.Target);
   263    }
   264    return c;
   265  }
   266  
   267  function createPRCell(pool: TidePool, prs: PullRequest[]): HTMLTableDataCellElement {
   268    const c = document.createElement("td");
   269    addPRsToElem(c, pool, prs);
   270    return c;
   271  }
   272  
   273  function createBatchCell(pool: TidePool): HTMLTableDataCellElement {
   274    const td = document.createElement('td');
   275    if (pool.BatchPending) {
   276      const numbers = pool.BatchPending.map((p) => String(p.Number));
   277      const batchRef = `${pool.Branch},${numbers.join(',')}`;
   278      const encodedRepo = encodeURIComponent(`${pool.Org}/${pool.Repo}`);
   279      const href = `/?repo=${encodedRepo}&type=batch&pull=${encodeURIComponent(batchRef)}`;
   280      const link = document.createElement('a');
   281      link.href = href;
   282      for (let i = 0; i < pool.BatchPending.length; i++) {
   283        const pr = pool.BatchPending[i];
   284        const text = document.createElement('span');
   285        text.appendChild(document.createTextNode(`#${  String(pr.Number)}`));
   286        text.id = `pr-${pool.Org}-${pool.Repo}-${pr.Number}-${nextID()}`;
   287        if (pr.Title) {
   288          const tip = tooltip.forElem(text.id, document.createTextNode(pr.Title));
   289          text.appendChild(tip);
   290        }
   291        link.appendChild(text);
   292        // Add a space after each PR number except the last.
   293        if (i + 1 < pool.BatchPending.length) {
   294          link.appendChild(document.createTextNode(" "));
   295        }
   296      }
   297      td.appendChild(link);
   298    }
   299    return td;
   300  }
   301  
   302  // addPRsToElem adds a space separated list of PR numbers that link to the corresponding PR on github.
   303  function addPRsToElem(elem: HTMLElement, pool: TidePool, prs?: PullRequest[]): void {
   304    if (prs) {
   305      for (let i = 0; i < prs.length; i++) {
   306        const a = document.createElement("a");
   307        a.href = `/github-link?dest=${pool.Org}/${pool.Repo}/pull/${prs[i].Number}`;
   308        a.appendChild(document.createTextNode(`#${  prs[i].Number}`));
   309        a.id = `pr-${pool.Org}-${pool.Repo}-${prs[i].Number}-${nextID()}`;
   310        if (prs[i].Title) {
   311          const tip = tooltip.forElem(a.id, document.createTextNode(prs[i].Title));
   312          a.appendChild(tip);
   313        }
   314        elem.appendChild(a);
   315        // Add a space after each PR number except the last.
   316        if (i + 1 < prs.length) {
   317          elem.appendChild(document.createTextNode(" "));
   318        }
   319      }
   320    }
   321  }
   322  
   323  // addBlockersToElem adds a space separated list of Issue numbers that link to the
   324  // corresponding Issues on github that are blocking merge.
   325  function addBlockersToElem(elem: HTMLElement, pool: TidePool): void {
   326    if (!pool.Blockers) {
   327      return;
   328    }
   329    for (let i = 0; i < pool.Blockers.length; i++) {
   330      const b = pool.Blockers[i];
   331      const a = document.createElement("a");
   332      a.href = b.URL;
   333      a.appendChild(document.createTextNode(`#${  b.Number}`));
   334      a.id = `blocker-${pool.Org}-${pool.Repo}-${b.Number}-${nextID()}`;
   335      a.appendChild(tooltip.forElem(a.id, document.createTextNode(b.Title)));
   336  
   337      elem.appendChild(a);
   338      // Add a space after each PR number except the last.
   339      if (i + 1 < pool.Blockers.length) {
   340        elem.appendChild(document.createTextNode(" "));
   341      }
   342    }
   343  }
   344  
   345  let idCounter = 0;
   346  function nextID(): string {
   347    idCounter++;
   348    return `elemID-${  String(idCounter)}`;
   349  }