github.com/munnerz/test-infra@v0.0.0-20190108210205-ce3d181dc989/prow/cmd/deck/static/tide-history/tide-history.ts (about)

     1  import {cell} from "../common/common";
     2  import {JobState} from "../api/prow";
     3  import {HistoryData, Record, PRMeta} from "../api/tide-history";
     4  import moment from "moment";
     5  
     6  declare const tideHistory: HistoryData;
     7  
     8  const recordDisplayLimit = 500;
     9  
    10  interface FilteredRecord extends Record {
    11    // The following are not initially present and are instead populated based on the 'History' map key while filtering.
    12    repo: string;
    13    branch: string;
    14  }
    15  
    16  // http://stackoverflow.com/a/5158301/3694
    17  function getParameterByName(name: string): string | null {
    18    const match = RegExp('[?&]' + name + '=([^&/]*)').exec(
    19      window.location.search);
    20    return match && decodeURIComponent(match[1].replace(/\+/g, ' '));
    21  }
    22  
    23  interface Options {
    24    repos: {[key: string]: boolean};
    25    branchs: {[key: string]: boolean};  // This is intentionally a typo to make pluralization easy.
    26    actions: {[key: string]: boolean};
    27    states: {[key: string]: boolean};
    28    authors: {[key: string]: boolean};
    29    pulls: {[key: string]: boolean};
    30  }
    31  
    32  function optionsForRepoBranch(repo: string, branch: string): Options {
    33    const opts: Options = {
    34      repos: {},
    35      branchs: {},
    36      actions: {},
    37      states: {},
    38      authors: {},
    39      pulls: {},
    40    };
    41  
    42    const hist: {[key: string]: Record[]} = typeof tideHistory !== 'undefined' ? tideHistory.History : {};
    43    const poolKeys = Object.keys(hist);
    44    for (const poolKey of poolKeys) {
    45      const match = RegExp('(.*?):(.*)').exec(poolKey);
    46      if (!match) {
    47        continue;
    48      }
    49      const recRepo = match[1];
    50      const recBranch = match[2];
    51  
    52      opts.repos[recRepo] = true;
    53      if (!repo || repo === recRepo) {
    54        opts.branchs[recBranch] = true;
    55        if (!branch || branch == recBranch) {
    56          let recs = hist[poolKey];
    57          for (const rec of recs) {
    58            opts.actions[rec.action] = true;
    59            opts.states[errorState(rec.err)] = true;
    60            for (const pr of rec.target || []) {
    61              opts.authors[pr.author] = true;
    62              opts.pulls[pr.num] = true;
    63            }
    64          }
    65        }
    66      }
    67    }
    68  
    69    return opts;
    70  }
    71  
    72  function errorState(err?: string): JobState {
    73    return err ? "failure" : "success"
    74  }
    75  
    76  function redrawOptions(opts: Options) {
    77    const repos = Object.keys(opts.repos).sort();
    78    addOptions(repos, "repo");
    79    const branchs = Object.keys(opts.branchs).sort(); // English sucks.
    80    addOptions(branchs, "branch");
    81    const actions = Object.keys(opts.actions).sort();
    82    addOptions(actions, "action");
    83    const authors = Object.keys(opts.authors).sort(
    84      (a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
    85    addOptions(authors, "author");
    86    const pulls = Object.keys(opts.pulls).sort((a, b) => parseInt(a) - parseInt(b));
    87    addOptions(pulls, "pull");
    88    const states = Object.keys(opts.states).sort();
    89    addOptions(states, "state");
    90  }
    91  
    92  window.onload = function(): void {
    93    const topNavigator = document.getElementById("top-navigator")!;
    94    let navigatorTimeOut: number | undefined;
    95    const main = document.querySelector("main")! as HTMLElement;
    96    main.onscroll = () => {
    97      topNavigator.classList.add("hidden");
    98      if (navigatorTimeOut) {
    99        clearTimeout(navigatorTimeOut);
   100      }
   101      navigatorTimeOut = setTimeout(() => {
   102        if (main.scrollTop === 0) {
   103          topNavigator.classList.add("hidden");
   104        } else if (main.scrollTop > 100) {
   105          topNavigator.classList.remove("hidden");
   106        }
   107      }, 100);
   108    };
   109    topNavigator.onclick = () => {
   110      main.scrollTop = 0;
   111    };
   112  
   113    // Register selection on change functions
   114    const filterBox = document.getElementById("filter-box")!;
   115    const options = filterBox.querySelectorAll("select")!;
   116    options.forEach(opt => {
   117        opt.onchange = () => {
   118            redraw();
   119        };
   120    });
   121  
   122    // set dropdown based on options from query string
   123    redrawOptions(optionsForRepoBranch("", ""));
   124    redraw();
   125  };
   126  
   127  function addOptions(options: string[], selectID: string): string | null {
   128    const sel = document.getElementById(selectID)! as HTMLSelectElement;
   129    while (sel.length > 1) {
   130      sel.removeChild(sel.lastChild!);
   131    }
   132    const param = getParameterByName(selectID);
   133    for (let i = 0; i < options.length; i++) {
   134      const o = document.createElement("option");
   135      o.value = options[i];
   136      o.text = o.value;
   137      if (param && options[i] === param) {
   138        o.selected = true;
   139      }
   140      sel.appendChild(o);
   141    }
   142    return param;
   143  }
   144  
   145  function equalSelected(sel: string, t: string): boolean {
   146    return sel === "" || sel == t;
   147  }
   148  
   149  function redraw(): void {
   150    const args: string[] = [];
   151  
   152    function getSelection(name: string): string {
   153      const sel = (document.getElementById(name) as HTMLSelectElement).value;
   154      if (sel && opts && !opts[name + 's' as keyof Options][sel]) {
   155        return "";
   156      }
   157      if (sel !== "") {
   158        args.push(name + "=" + encodeURIComponent(sel));
   159      }
   160      return sel;
   161    }
   162  
   163    const initialRepoSel = (document.getElementById("repo") as HTMLSelectElement).value;
   164    const initialBranchSel = (document.getElementById("branch") as HTMLSelectElement).value;
   165  
   166    const opts = optionsForRepoBranch(initialRepoSel, initialBranchSel);
   167    const repoSel = getSelection("repo");
   168    const branchSel = getSelection("branch");
   169    const pullSel = getSelection("pull");
   170    const authorSel = getSelection("author");
   171    const actionSel = getSelection("action");
   172    const stateSel = getSelection("state");
   173  
   174    if (window.history && window.history.replaceState !== undefined) {
   175      if (args.length > 0) {
   176        history.replaceState(null, "", "/tide-history?" + args.join('&'));
   177      } else {
   178        history.replaceState(null, "", "/tide-history")
   179      }
   180    }
   181    redrawOptions(opts);
   182  
   183    let filteredRecs: FilteredRecord[] = [];
   184    const hist: {[key: string]: Record[]} = typeof tideHistory !== 'undefined' ? tideHistory.History : {};
   185    const poolKeys = Object.keys(hist);
   186    for (const poolKey of poolKeys) {
   187      const match = RegExp('(.*?):(.*)').exec(poolKey);
   188      if (!match || match.length != 3) {
   189        return
   190      }
   191      const repo = match[1];
   192      const branch = match[2];
   193  
   194      if (!equalSelected(repoSel, repo)) {
   195        continue;
   196      }
   197      if (!equalSelected(branchSel, branch)) {
   198        continue;
   199      }
   200  
   201      const recs = hist[poolKey];
   202      for (const rec of recs) {
   203        if (!equalSelected(actionSel, rec.action)) {
   204          continue;
   205        }
   206        if (!equalSelected(stateSel, errorState(rec.err))) {
   207          continue;
   208        }
   209  
   210        let anyTargetMatches = false;
   211        for (const pr of rec.target || []) {
   212          if (!equalSelected(pullSel, pr.num.toString())) {
   213            continue;
   214          }
   215          if (!equalSelected(authorSel, pr.author)) {
   216            continue;
   217          }
   218  
   219          anyTargetMatches = true;
   220          break;
   221        }
   222        if (!anyTargetMatches) {
   223          continue;
   224        }
   225  
   226        const filtered = <FilteredRecord>rec;
   227        filtered.repo = repo;
   228        filtered.branch = branch;
   229        filteredRecs.push(filtered);
   230      }
   231    }
   232    // Sort by descending time.
   233    filteredRecs = filteredRecs.sort((a, b) => moment(b.time).unix() - moment(a.time).unix());
   234    redrawRecords(filteredRecs);
   235  }
   236  
   237  function redrawRecords(recs: FilteredRecord[]): void {
   238    const records = document.getElementById("records")!.getElementsByTagName(
   239      "tbody")[0];
   240    while (records.firstChild) {
   241      records.removeChild(records.firstChild);
   242    }
   243  
   244    let lastKey = '';
   245    const displayCount = Math.min(recs.length, recordDisplayLimit);
   246    for (let i = 0; i < displayCount; i++) {
   247      const rec = recs[i];
   248      const r = document.createElement("tr");
   249  
   250      r.appendChild(cell.state(errorState(rec.err)));
   251      const key = `${rec.repo} ${rec.branch} ${rec.baseSHA || ""}`;
   252      if (key !== lastKey) {
   253        // This is a different pool or base branch commit than the previous row.
   254        lastKey = key;
   255        r.className = "changed";
   256  
   257        r.appendChild(cell.link(
   258          rec.repo + " " + rec.branch,
   259          "https://github.com/" + rec.repo + "/tree/" + rec.branch,
   260        ));
   261        if (rec.baseSHA) {
   262            r.appendChild(cell.link(
   263              rec.baseSHA.slice(0,7),
   264              "https://github.com/" + rec.repo + "/commit/" + rec.baseSHA,
   265            ));
   266        } else {
   267            r.appendChild(cell.text(""));
   268        }
   269      } else {
   270        // Don't render identical cells for the same pool+baseSHA
   271        r.appendChild(cell.text(""));
   272        r.appendChild(cell.text(""));
   273      }
   274      r.appendChild(cell.text(rec.action));
   275      r.appendChild(targetCell(rec));
   276      r.appendChild(cell.time(nextID(), moment(rec.time)));
   277      r.appendChild(cell.text(rec.err || ""));
   278      records.appendChild(r);
   279    }
   280    const recCount = document.getElementById("record-count")!;
   281    recCount.textContent = `Showing ${displayCount}/${recs.length} records`;
   282  }
   283  
   284  function targetCell(rec: FilteredRecord): HTMLTableDataCellElement {
   285    const target = rec.target || [];
   286    switch (target.length) {
   287      case 0:
   288        return cell.text("");
   289      case 1:
   290        let pr = target[0];
   291        return cell.prRevision(rec.repo, pr.num, pr.author, pr.title, pr.SHA);
   292      default:
   293        // Multiple PRs in 'target'. Add them all to the cell, but on separate lines.
   294        let td = document.createElement("td");
   295        td.style.whiteSpace = "pre";
   296        for (const pr of target) {
   297          cell.addPRRevision(td, rec.repo, pr.num, pr.author, pr.title, pr.SHA);
   298          td.appendChild(document.createTextNode("\n"));
   299        }
   300        return td;
   301     }
   302  }
   303  
   304  
   305  let idCounter = 0;
   306  function nextID(): string {
   307    idCounter++;
   308    return "histID-" + String(idCounter);
   309  }