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

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