From 3dc5541ab8bc396f4a2f6a86c720e7aabe2f9154 Mon Sep 17 00:00:00 2001 From: Cole Wagner Date: Wed, 2 Jan 2019 15:27:01 -0800 Subject: [PATCH] Make Deck serve a /tide-history endpoint that exposes a filterable list of historic Tide actions. --- prow/cmd/deck/localdata/.gitignore | 1 + prow/cmd/deck/runlocal | 1 + prow/cmd/deck/static/BUILD.bazel | 32 +- prow/cmd/deck/static/api/tide-history.ts | 19 ++ prow/cmd/deck/static/common/common.ts | 152 +++++++++ prow/cmd/deck/static/prow/prow.ts | 174 ++-------- .../deck/static/tide-history/tide-history.ts | 308 ++++++++++++++++++ prow/cmd/deck/static/tide/tide.ts | 15 +- prow/cmd/deck/template/tide-history.html | 50 +++ 9 files changed, 594 insertions(+), 158 deletions(-) create mode 100644 prow/cmd/deck/static/api/tide-history.ts create mode 100644 prow/cmd/deck/static/common/common.ts create mode 100644 prow/cmd/deck/static/tide-history/tide-history.ts create mode 100644 prow/cmd/deck/template/tide-history.html diff --git a/prow/cmd/deck/localdata/.gitignore b/prow/cmd/deck/localdata/.gitignore index f213e2b8b894..04156012c710 100644 --- a/prow/cmd/deck/localdata/.gitignore +++ b/prow/cmd/deck/localdata/.gitignore @@ -1,5 +1,6 @@ data.js plugin-help.js tide.js +tide-history.js branding.js pr-data.js diff --git a/prow/cmd/deck/runlocal b/prow/cmd/deck/runlocal index 7cf4c379c314..202152b771b7 100755 --- a/prow/cmd/deck/runlocal +++ b/prow/cmd/deck/runlocal @@ -22,6 +22,7 @@ if [[ $1 == "openshift" ]]; then fi curl "$HOST/data.js?var=allBuilds" > data.js curl "$HOST/tide.js?var=tideData" > tide.js +curl "$HOST/tide-history.js?var=tideHistory" > tide-history.js curl "$HOST/plugin-help.js?var=allHelp" > plugin-help.js curl "$HOST/pr-data.js" > pr-data.js bazel run //prow/cmd/deck:deck -- --pregenerated-data=${DIR}/localdata --static-files-location=./prow/cmd/deck/static --template-files-location=./prow/cmd/deck/template --spyglass-files-location=./prow/spyglass/lenses --config-path ${DIR}/../../config.yaml --spyglass \ No newline at end of file diff --git a/prow/cmd/deck/static/BUILD.bazel b/prow/cmd/deck/static/BUILD.bazel index 026aab1700c8..4d410241fb7d 100644 --- a/prow/cmd/deck/static/BUILD.bazel +++ b/prow/cmd/deck/static/BUILD.bazel @@ -8,12 +8,21 @@ ts_library( srcs = glob(["api/**/*.ts"]), ) +ts_library( + name = "common", + srcs = glob(["common/*.ts"]), + deps = [ + ":api", + "@npm//:moment", + ], +) + ts_library( name = "prow", srcs = glob(["prow/*.ts"]) + ["vendor.d.ts"], deps = [ ":api", - "@npm//:moment", + ":common", ], ) @@ -65,6 +74,7 @@ ts_library( srcs = glob(["tide/*.ts"]), deps = [ ":api", + ":common", ], ) @@ -73,6 +83,25 @@ rollup_bundle( entry_point = "prow/cmd/deck/static/tide/tide", deps = [ ":tide", + "@npm//:moment", + ], +) + +ts_library( + name = "tide_history", + srcs = glob(["tide-history/*.ts"]), + deps = [ + ":api", + ":common", + ], +) + +rollup_bundle( + name = "tide_history_bundle", + entry_point = "prow/cmd/deck/static/tide-history/tide-history", + deps = [ + ":tide_history", + "@npm//:moment", ], ) @@ -150,6 +179,7 @@ filegroup( ":spyglass_bundle", ":spyglass_lens_bundle", ":tide_bundle", + ":tide_history_bundle", ], ) diff --git a/prow/cmd/deck/static/api/tide-history.ts b/prow/cmd/deck/static/api/tide-history.ts new file mode 100644 index 000000000000..8d1a95f9cee7 --- /dev/null +++ b/prow/cmd/deck/static/api/tide-history.ts @@ -0,0 +1,19 @@ + +export interface HistoryData { + History: {[key: string]: Record[]}; +} + +export interface Record { + time: string; + action: string; + baseSHA?: string; + target?: PRMeta[]; + err?: string; +} + +export interface PRMeta { + num: number; + author: string; + title: string; + SHA: string; +} diff --git a/prow/cmd/deck/static/common/common.ts b/prow/cmd/deck/static/common/common.ts new file mode 100644 index 000000000000..4c40c0d47571 --- /dev/null +++ b/prow/cmd/deck/static/common/common.ts @@ -0,0 +1,152 @@ +import {JobState} from "../api/prow"; +import moment from "moment"; + +// The cell namespace exposes functions for constructing common table cells. +export namespace cell { + + export function text(text: string): HTMLTableDataCellElement { + const c = document.createElement("td"); + c.appendChild(document.createTextNode(text)); + return c; + }; + + export function time(id: string, time: number): HTMLTableDataCellElement { + const momentTime = moment.unix(time); + const tid = "time-cell-" + id; + const main = document.createElement("div"); + const isADayOld = momentTime.isBefore(moment().startOf('day')); + main.textContent = momentTime.format(isADayOld ? 'MMM DD HH:mm:ss' : 'HH:mm:ss'); + main.id = tid; + + const tooltip = document.createElement("div"); + tooltip.textContent = momentTime.format('MMM DD YYYY, HH:mm:ss [UTC]ZZ'); + tooltip.setAttribute("data-mdl-for", tid); + tooltip.classList.add("mdl-tooltip", "mdl-tooltip--large"); + + const c = document.createElement("td"); + c.appendChild(main); + c.appendChild(tooltip); + + return c; + }; + + export function link(text: string, url: string): HTMLTableDataCellElement { + const c = document.createElement("td"); + const a = document.createElement("a"); + a.href = url; + a.appendChild(document.createTextNode(text)); + c.appendChild(a); + return c; + }; + + export function state(state: JobState): HTMLTableDataCellElement { + const c = document.createElement("td"); + if (!state) { + c.appendChild(document.createTextNode("")); + return c; + } + c.classList.add("icon-cell"); + + let displayState = stateToAdj(state); + displayState = displayState[0].toUpperCase() + displayState.slice(1); + let displayIcon = ""; + switch (state) { + case "triggered": + displayIcon = "schedule"; + break; + case "pending": + displayIcon = "watch_later"; + break; + case "success": + displayIcon = "check_circle"; + break; + case "failure": + displayIcon = "error"; + break; + case "aborted": + displayIcon = "remove_circle"; + break; + case "error": + displayIcon = "warning"; + break; + } + const stateIndicator = document.createElement("i"); + stateIndicator.classList.add("material-icons", "state", state); + stateIndicator.innerText = displayIcon; + c.appendChild(stateIndicator); + c.title = displayState; + + return c; + }; + + function stateToAdj(state: JobState): string { + switch (state) { + case "success": + return "succeeded"; + case "failure": + return "failed"; + default: + return state; + } + }; + + export function commitRevision(repo: string, ref: string, SHA: string): HTMLTableDataCellElement { + const c = document.createElement("td"); + const bl = document.createElement("a"); + bl.href = "https://github.com/" + repo + "/commit/" + SHA; + bl.text = ref + " (" + SHA.slice(0, 7) + ")"; + c.appendChild(bl); + return c; + } + + export function prRevision(repo: string, num: number, author: string, title: string, SHA: string): HTMLTableDataCellElement { + const td = document.createElement("td"); + addPRRevision(td, repo, num, author, title, SHA); + return td; + } + + let idCounter = 0; + function nextID(): String { + idCounter++; + return "tipID-" + String(idCounter); + }; + + export function addPRRevision(elem: Node, repo: string, num: number, author: string, title: string, SHA: string): void { + elem.appendChild(document.createTextNode("#")); + const pl = document.createElement("a"); + pl.href = "https://github.com/" + repo + "/pull/" + num; + pl.text = num.toString(); + if (title) { + pl.id = "pr-" + repo + "-" + num + "-" + nextID(); + const tip = tooltip.forElem(pl.id, document.createTextNode(title)); + pl.appendChild(tip); + } + elem.appendChild(pl); + if (SHA) { + elem.appendChild(document.createTextNode(" (")); + const cl = document.createElement("a"); + cl.href = "https://github.com/" + repo + "/pull/" + num + + '/commits/' + SHA; + cl.text = SHA.slice(0, 7); + elem.appendChild(cl); + elem.appendChild(document.createTextNode(")")); + } + if (author) { + elem.appendChild(document.createTextNode(" by ")) + const al = document.createElement("a"); + al.href = "https://github.com/" + author; + al.text = author; + elem.appendChild(al); + } + } +} + +export namespace tooltip { + export function forElem(elemID: string, tipElem: Node): Node { + const tooltip = document.createElement("div"); + tooltip.appendChild(tipElem); + tooltip.setAttribute("data-mdl-for", elemID); + tooltip.classList.add("mdl-tooltip", "mdl-tooltip--large"); + return tooltip; + } +} \ No newline at end of file diff --git a/prow/cmd/deck/static/prow/prow.ts b/prow/cmd/deck/static/prow/prow.ts index 0ffb6d4e7b34..256ae1c2d0a8 100644 --- a/prow/cmd/deck/static/prow/prow.ts +++ b/prow/cmd/deck/static/prow/prow.ts @@ -1,6 +1,7 @@ import {FuzzySearch} from './fuzzy-search'; import {Job, JobState, JobType} from "../api/prow"; -import moment from "moment"; +import {cell} from "../common/common"; + declare const allBuilds: Job[]; @@ -13,17 +14,6 @@ function getParameterByName(name: string): string | null { return match && decodeURIComponent(match[1].replace(/\+/g, ' ')); } -function updateQueryStringParameter(uri: string, key: string, - value: string): string { - const re = new RegExp("([?&])" + key + "=.*?(&|$)", "i"); - const separator = uri.indexOf('?') !== -1 ? "&" : "?"; - if (uri.match(re)) { - return uri.replace(re, '$1' + key + "=" + value + '$2'); - } else { - return uri + separator + key + "=" + value; - } -} - function shortenBuildRefs(buildRef: string): string { return buildRef && buildRef.replace(/:[0-9a-f]*/g, ''); } @@ -482,7 +472,7 @@ function redraw(fz: FuzzySearch): void { continue; } const r = document.createElement("tr"); - r.appendChild(stateCell(build.state)); + r.appendChild(cell.state(build.state)); if (build.pod_name) { const icon = createIcon("description", "Build log"); icon.href = "log?job=" + build.job + "&id=" + build.build_id; @@ -491,7 +481,7 @@ function redraw(fz: FuzzySearch): void { cell.appendChild(icon); r.appendChild(cell); } else { - r.appendChild(createTextCell("")); + r.appendChild(cell.text("")); } r.appendChild(createRerunCell(modal, rerun_command, build.prow_job)); const key = groupKey(build); @@ -501,26 +491,26 @@ function redraw(fz: FuzzySearch): void { r.className = "changed"; if (build.type === "periodic") { - r.appendChild(createTextCell("")); + r.appendChild(cell.text("")); } else if (build.repo.startsWith("http://") || build.repo.startsWith("https://") ) { - r.appendChild(createLinkCell(build.repo, build.repo, "")); + r.appendChild(cell.link(build.repo, build.repo)); } else { - r.appendChild(createLinkCell(build.repo, "https://github.com/" - + build.repo, "")); + r.appendChild(cell.link(build.repo, "https://github.com/" + + build.repo)); } if (build.type === "presubmit") { - r.appendChild(prRevisionCell(build)); + r.appendChild(cell.prRevision(build.repo, build.number, build.author, "", build.pull_sha)); } else if (build.type === "batch") { r.appendChild(batchRevisionCell(build)); } else if (build.type === "postsubmit") { - r.appendChild(pushRevisionCell(build)); + r.appendChild(cell.commitRevision(build.repo, build.base_ref, build.base_sha)); } else if (build.type === "periodic") { - r.appendChild(createTextCell("")); + r.appendChild(cell.text("")); } } else { // Don't render identical cells for the same PR/commit. - r.appendChild(createTextCell("")); - r.appendChild(createTextCell("")); + r.appendChild(cell.text("")); + r.appendChild(cell.text("")); } if (spyglass) { if (build.state == 'pending') { @@ -530,7 +520,7 @@ function redraw(fz: FuzzySearch): void { } else { const buildIndex = build.url.indexOf('/build/'); if (buildIndex === -1) { - r.appendChild(createTextCell('')); + r.appendChild(cell.text('')); } else { let url = window.location.origin + '/view/gcs/' + build.url.substring(buildIndex + '/build/'.length); @@ -538,16 +528,16 @@ function redraw(fz: FuzzySearch): void { } } } else { - r.appendChild(createTextCell('')); + r.appendChild(cell.text('')); } if (build.url === "") { - r.appendChild(createTextCell(build.job)); + r.appendChild(cell.text(build.job)); } else { - r.appendChild(createLinkCell(build.job, build.url, "")); + r.appendChild(cell.link(build.job, build.url)); } - r.appendChild(createTimeCell(i, parseInt(build.started))); - r.appendChild(createTextCell(build.duration)); + r.appendChild(cell.time(i.toString(), parseInt(build.started))); + r.appendChild(cell.text(build.duration)); builds.appendChild(r); } const jobCount = document.getElementById("job-count")!; @@ -555,53 +545,6 @@ function redraw(fz: FuzzySearch): void { drawJobBar(totalJob, jobCountMap); } -function createSpyglassCell(url: string): HTMLTableDataCellElement { - const icon = createIcon('visibility', 'View in Spyglass'); - icon.href = url; - const cell = document.createElement('td'); - cell.classList.add('icon-cell'); - cell.appendChild(icon); - return cell; -} - -function createTextCell(text: string): HTMLTableDataCellElement { - const c = document.createElement("td"); - c.appendChild(document.createTextNode(text)); - return c; -} - -function createTimeCell(id: number, time: number): HTMLTableDataCellElement { - const momentTime = moment.unix(time); - const tid = "time-cell-" + id; - const main = document.createElement("div"); - const isADayOld = momentTime.isBefore(moment().startOf('day')); - main.textContent = momentTime.format(isADayOld ? 'MMM DD HH:mm:ss' : 'HH:mm:ss'); - main.id = tid; - - const tooltip = document.createElement("div"); - tooltip.textContent = momentTime.format('MMM DD YYYY, HH:mm:ss [UTC]ZZ'); - tooltip.setAttribute("data-mdl-for", tid); - tooltip.classList.add("mdl-tooltip", "mdl-tooltip--large"); - - const c = document.createElement("td"); - c.appendChild(main); - c.appendChild(tooltip); - - return c; -} - -function createLinkCell(text: string, url: string, title: string): HTMLTableDataCellElement { - const c = document.createElement("td"); - const a = document.createElement("a"); - a.href = url; - if (title !== "") { - a.title = title; - } - a.appendChild(document.createTextNode(text)); - c.appendChild(a); - return c; -} - function createRerunCell(modal: HTMLElement, rerunElement: HTMLElement, prowjob: string): HTMLTableDataCellElement { const url = `https://${window.location.hostname}/rerun?prowjob=${prowjob}`; const c = document.createElement("td"); @@ -656,46 +599,6 @@ function copyToClipboardWithToast(text: string): void { toast.MaterialSnackbar.showSnackbar({message: "Copied to clipboard"}); } -function stateCell(state: JobState): HTMLTableDataCellElement { - const c = document.createElement("td"); - if (!state) { - c.appendChild(document.createTextNode("")); - return c; - } - c.classList.add("icon-cell"); - - let displayState = stateToAdj(state); - displayState = displayState[0].toUpperCase() + displayState.slice(1); - let displayIcon = ""; - switch (state) { - case "triggered": - displayIcon = "schedule"; - break; - case "pending": - displayIcon = "watch_later"; - break; - case "success": - displayIcon = "check_circle"; - break; - case "failure": - displayIcon = "error"; - break; - case "aborted": - displayIcon = "remove_circle"; - break; - case "error": - displayIcon = "warning"; - break; - } - const stateIndicator = document.createElement("i"); - stateIndicator.classList.add("material-icons", "state", state); - stateIndicator.innerText = displayIcon; - c.appendChild(stateIndicator); - c.title = displayState; - - return c; -} - function batchRevisionCell(build: Job): HTMLTableDataCellElement { const c = document.createElement("td"); const prRefs = build.refs.split(","); @@ -713,36 +616,6 @@ function batchRevisionCell(build: Job): HTMLTableDataCellElement { return c; } -function pushRevisionCell(build: Job): HTMLTableDataCellElement { - const c = document.createElement("td"); - const bl = document.createElement("a"); - bl.href = "https://github.com/" + build.repo + "/commit/" + build.base_sha; - bl.text = build.base_ref + " (" + build.base_sha.slice(0, 7) + ")"; - c.appendChild(bl); - return c; -} - -function prRevisionCell(build: Job): HTMLTableDataCellElement { - const c = document.createElement("td"); - c.appendChild(document.createTextNode("#")); - const pl = document.createElement("a"); - pl.href = "https://github.com/" + build.repo + "/pull/" + build.number; - pl.text = build.number.toString(); - c.appendChild(pl); - c.appendChild(document.createTextNode(" (")); - const cl = document.createElement("a"); - cl.href = "https://github.com/" + build.repo + "/pull/" + build.number - + '/commits/' + build.pull_sha; - cl.text = build.pull_sha.slice(0, 7); - c.appendChild(cl); - c.appendChild(document.createTextNode(") by ")); - const al = document.createElement("a"); - al.href = "https://github.com/" + build.author; - al.text = build.author; - c.appendChild(al); - return c; -} - function drawJobBar(total: number, jobCountMap: Map): void { const states: JobState[] = ["success", "pending", "triggered", "error", "failure", "aborted", ""]; states.sort((s1, s2) => { @@ -784,6 +657,15 @@ function stateToAdj(state: JobState): string { } } +function createSpyglassCell(url: string): HTMLTableDataCellElement { + const icon = createIcon('visibility', 'View in Spyglass'); + icon.href = url; + const cell = document.createElement('td'); + cell.classList.add('icon-cell'); + cell.appendChild(icon); + return cell; +} + function createIcon(iconString: string, tooltip: string = ""): HTMLAnchorElement { const icon = document.createElement("i"); icon.classList.add("icon-button", "material-icons"); diff --git a/prow/cmd/deck/static/tide-history/tide-history.ts b/prow/cmd/deck/static/tide-history/tide-history.ts new file mode 100644 index 000000000000..6b28b40c3c0f --- /dev/null +++ b/prow/cmd/deck/static/tide-history/tide-history.ts @@ -0,0 +1,308 @@ +import {cell} from "../common/common"; +import {JobState} from "../api/prow"; +import {HistoryData, Record, PRMeta} from "../api/tide-history"; + +declare const tideHistory: HistoryData; + +const recordDisplayLimit = 500; + +interface FilteredRecord extends Record { + // The following are not initially present and are instead populated based on the 'History' map key while filtering. + repo: string; + branch: string; +} + +// http://stackoverflow.com/a/5158301/3694 +function getParameterByName(name: string): string | null { + const match = RegExp('[?&]' + name + '=([^&/]*)').exec( + window.location.search); + return match && decodeURIComponent(match[1].replace(/\+/g, ' ')); +} + +interface Options { + repos: {[key: string]: boolean}; + branchs: {[key: string]: boolean}; // This is intentionally a typo to make pluralization easy. + actions: {[key: string]: boolean}; + states: {[key: string]: boolean}; + authors: {[key: string]: boolean}; + pulls: {[key: string]: boolean}; +} + +function optionsForRepoBranch(repo: string, branch: string): Options { + const opts: Options = { + repos: {}, + branchs: {}, + actions: {}, + states: {}, + authors: {}, + pulls: {}, + }; + + const hist: {[key: string]: Record[]} = typeof tideHistory !== 'undefined' ? tideHistory.History : {}; + const poolKeys = Object.keys(hist); + for (const poolKey of poolKeys) { + const match = RegExp('(.*?):(.*)').exec(poolKey); + if (!match) { + continue; + } + const recRepo = match[1]; + const recBranch = match[2]; + + opts.repos[recRepo] = true; + if (!repo || repo === recRepo) { + opts.branchs[recBranch] = true; + if (!branch || branch == recBranch) { + let recs = hist[poolKey]; + for (const rec of recs) { + opts.actions[rec.action] = true; + opts.states[errorState(rec.err)] = true; + for (const pr of rec.target || []) { + opts.authors[pr.author] = true; + opts.pulls[pr.num] = true; + } + } + } + } + } + + return opts; +} + +function errorState(err?: string): JobState { + return err ? "failure" : "success" +} + +function redrawOptions(opts: Options) { + const repos = Object.keys(opts.repos).sort(); + addOptions(repos, "repo"); + const branchs = Object.keys(opts.branchs).sort(); // English sucks. + addOptions(branchs, "branch"); + const actions = Object.keys(opts.actions).sort(); + addOptions(actions, "action"); + const authors = Object.keys(opts.authors).sort( + (a, b) => a.toLowerCase().localeCompare(b.toLowerCase())); + addOptions(authors, "author"); + const pulls = Object.keys(opts.pulls).sort((a, b) => parseInt(a) - parseInt(b)); + addOptions(pulls, "pull"); + const states = Object.keys(opts.states).sort(); + addOptions(states, "state"); +} + +window.onload = function(): void { + const topNavigator = document.getElementById("top-navigator")!; + let navigatorTimeOut: number | undefined; + const main = document.querySelector("main")! as HTMLElement; + main.onscroll = () => { + topNavigator.classList.add("hidden"); + if (navigatorTimeOut) { + clearTimeout(navigatorTimeOut); + } + navigatorTimeOut = setTimeout(() => { + if (main.scrollTop === 0) { + topNavigator.classList.add("hidden"); + } else if (main.scrollTop > 100) { + topNavigator.classList.remove("hidden"); + } + }, 100); + }; + topNavigator.onclick = () => { + main.scrollTop = 0; + }; + + // Register selection on change functions + const filterBox = document.getElementById("filter-box")!; + const options = filterBox.querySelectorAll("select")!; + options.forEach(opt => { + opt.onchange = () => { + redraw(); + }; + }); + + // set dropdown based on options from query string + redrawOptions(optionsForRepoBranch("", "")); + redraw(); +}; + +function addOptions(options: string[], selectID: string): string | null { + const sel = document.getElementById(selectID)! as HTMLSelectElement; + while (sel.length > 1) { + sel.removeChild(sel.lastChild!); + } + const param = getParameterByName(selectID); + for (let i = 0; i < options.length; i++) { + const o = document.createElement("option"); + o.value = options[i]; + o.text = o.value; + if (param && options[i] === param) { + o.selected = true; + } + sel.appendChild(o); + } + return param; +} + +function equalSelected(sel: string, t: string): boolean { + return sel === "" || sel == t; +} + +function redraw(): void { + const args: string[] = []; + + function getSelection(name: string): string { + const sel = (document.getElementById(name) as HTMLSelectElement).value; + if (sel && opts && !opts[name + 's' as keyof Options][sel]) { + return ""; + } + if (sel !== "") { + args.push(name + "=" + encodeURIComponent(sel)); + } + return sel; + } + + const initialRepoSel = (document.getElementById("repo") as HTMLSelectElement).value; + const initialBranchSel = (document.getElementById("branch") as HTMLSelectElement).value; + + const opts = optionsForRepoBranch(initialRepoSel, initialBranchSel); + const repoSel = getSelection("repo"); + const branchSel = getSelection("branch"); + const pullSel = getSelection("pull"); + const authorSel = getSelection("author"); + const actionSel = getSelection("action"); + const stateSel = getSelection("state"); + + if (window.history && window.history.replaceState !== undefined) { + if (args.length > 0) { + history.replaceState(null, "", "/tide-history?" + args.join('&')); + } else { + history.replaceState(null, "", "/tide-history") + } + } + redrawOptions(opts); + + let filteredRecs: FilteredRecord[] = []; + const hist: {[key: string]: Record[]} = typeof tideHistory !== 'undefined' ? tideHistory.History : {}; + const poolKeys = Object.keys(hist); + for (const poolKey of poolKeys) { + const match = RegExp('(.*?):(.*)').exec(poolKey); + if (!match || match.length != 3) { + return + } + const repo = match[1]; + const branch = match[2]; + + if (!equalSelected(repoSel, repo)) { + continue; + } + if (!equalSelected(branchSel, branch)) { + continue; + } + + const recs = hist[poolKey]; + for (const rec of recs) { + if (!equalSelected(actionSel, rec.action)) { + continue; + } + if (!equalSelected(stateSel, errorState(rec.err))) { + continue; + } + + let anyTargetMatches = false; + for (const pr of rec.target || []) { + if (!equalSelected(pullSel, pr.num.toString())) { + continue; + } + if (!equalSelected(authorSel, pr.author)) { + continue; + } + + anyTargetMatches = true; + break; + } + if (!anyTargetMatches) { + continue; + } + + const filtered = rec; + filtered.repo = repo; + filtered.branch = branch; + filteredRecs.push(filtered); + } + } + // Sort by descending time. + filteredRecs = filteredRecs.sort((a, b) => parseInt(b.time) - parseInt(a.time)); + redrawRecords(filteredRecs); +} + +function redrawRecords(recs: FilteredRecord[]): void { + const records = document.getElementById("records")!.getElementsByTagName( + "tbody")[0]; + while (records.firstChild) { + records.removeChild(records.firstChild); + } + + let lastKey = ''; + const displayCount = Math.min(recs.length, recordDisplayLimit); + for (let i = 0; i < displayCount; i++) { + const rec = recs[i]; + const r = document.createElement("tr"); + + r.appendChild(cell.state(errorState(rec.err))); + const key = `${rec.repo} ${rec.branch} ${rec.baseSHA || ""}`; + if (key !== lastKey) { + // This is a different pool or base branch commit than the previous row. + lastKey = key; + r.className = "changed"; + + r.appendChild(cell.link( + rec.repo + " " + rec.branch, + "https://github.com/" + rec.repo + "/tree/" + rec.branch, + )); + if (rec.baseSHA) { + r.appendChild(cell.link( + rec.baseSHA.slice(0,7), + "https://github.com/" + rec.repo + "/commit/" + rec.baseSHA, + )); + } else { + r.appendChild(cell.text("")); + } + } else { + // Don't render identical cells for the same pool+baseSHA + r.appendChild(cell.text("")); + r.appendChild(cell.text("")); + } + r.appendChild(cell.text(rec.action)); + r.appendChild(targetCell(rec)); + r.appendChild(cell.time(nextID(), parseInt(rec.time))); + r.appendChild(cell.text(rec.err || "")); + records.appendChild(r); + } + const recCount = document.getElementById("record-count")!; + recCount.textContent = `Showing ${displayCount}/${recs.length} records`; +} + +function targetCell(rec: FilteredRecord): HTMLTableDataCellElement { + const target = rec.target || []; + switch (target.length) { + case 0: + return cell.text(""); + case 1: + let pr = target[0]; + return cell.prRevision(rec.repo, pr.num, pr.author, pr.title, pr.SHA); + default: + // Multiple PRs in 'target'. Add them all to the cell, but on separate lines. + let td = document.createElement("td"); + td.style.whiteSpace = "pre"; + for (const pr of target) { + cell.addPRRevision(td, rec.repo, pr.num, pr.author, pr.title, pr.SHA); + td.appendChild(document.createTextNode("\n")); + } + return td; + } +} + + +let idCounter = 0; +function nextID(): string { + idCounter++; + return "histID-" + String(idCounter); +} diff --git a/prow/cmd/deck/static/tide/tide.ts b/prow/cmd/deck/static/tide/tide.ts index 26b33b818b6b..e5f440d3a9c3 100644 --- a/prow/cmd/deck/static/tide/tide.ts +++ b/prow/cmd/deck/static/tide/tide.ts @@ -1,4 +1,5 @@ import {PullRequest, TideData, TidePool} from '../api/tide'; +import {tooltip} from '../common/common'; declare const tideData: TideData; @@ -282,7 +283,7 @@ function createBatchCell(pool: TidePool): HTMLTableDataCellElement { text.appendChild(document.createTextNode("#" + String(pr.Number))); text.id = "pr-" + pool.Org + "-" + pool.Repo + "-" + pr.Number + "-" + nextID(); if (pr.Title) { - const tip = toolTipForElem(text.id, document.createTextNode(pr.Title)); + const tip = tooltip.forElem(text.id, document.createTextNode(pr.Title)); text.appendChild(tip); } link.appendChild(text); @@ -305,7 +306,7 @@ function addPRsToElem(elem: HTMLElement, pool: TidePool, prs?: PullRequest[]): v a.appendChild(document.createTextNode("#" + prs[i].Number)); a.id = "pr-" + pool.Org + "-" + pool.Repo + "-" + prs[i].Number + "-" + nextID(); if (prs[i].Title) { - const tip = toolTipForElem(a.id, document.createTextNode(prs[i].Title)); + const tip = tooltip.forElem(a.id, document.createTextNode(prs[i].Title)); a.appendChild(tip); } elem.appendChild(a); @@ -329,7 +330,7 @@ function addBlockersToElem(elem: HTMLElement, pool: TidePool): void { a.href = b.URL; a.appendChild(document.createTextNode("#" + b.Number)); a.id = "blocker-" + pool.Org + "-" + pool.Repo + "-" + b.Number + "-" + nextID(); - a.appendChild(toolTipForElem(a.id, document.createTextNode(b.Title))); + a.appendChild(tooltip.forElem(a.id, document.createTextNode(b.Title))); elem.appendChild(a); // Add a space after each PR number except the last. @@ -344,11 +345,3 @@ function nextID(): String { idCounter++; return "elemID-" + String(idCounter); } - -function toolTipForElem(elemID: string, tipElem: Node): Node { - const tooltip = document.createElement("div"); - tooltip.appendChild(tipElem); - tooltip.setAttribute("data-mdl-for", elemID); - tooltip.classList.add("mdl-tooltip", "mdl-tooltip--large"); - return tooltip; -} diff --git a/prow/cmd/deck/template/tide-history.html b/prow/cmd/deck/template/tide-history.html new file mode 100644 index 000000000000..6dbe0f091fe6 --- /dev/null +++ b/prow/cmd/deck/template/tide-history.html @@ -0,0 +1,50 @@ +{{define "title"}}Tide History{{end}} + +{{define "scripts"}} + + +{{end}} + +{{define "content"}} + +
+ +
+
+ + + + + + + + + + + + + + +
PoolBase CommitActionTargetTimeError
+
+
+
+{{end}} + +{{template "page" (settings mobileUnfriendly "tide-history" .)}}