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 }