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 }