github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/cmd/deck/static/tide/tide.ts (about) 1 import {PullRequest, TideData, TidePool} from '../api/tide'; 2 import {tidehistory, tooltip} from '../common/common'; 3 4 declare const tideData: TideData; 5 6 window.onload = (): void => { 7 const infoDiv = document.getElementById("info-div")!; 8 const infoH4 = infoDiv.getElementsByTagName("h4")[0]!; 9 infoH4.addEventListener("click", infoToggle(infoDiv.getElementsByTagName("span")[0]), true); 10 11 redraw(); 12 }; 13 14 function infoToggle(toToggle: HTMLElement): (event: Event) => void { 15 return (event): void => { 16 if (toToggle.className === "hidden") { 17 toToggle.className = ""; 18 (event.target as HTMLElement).textContent = "Merge Requirements: (click to collapse)"; 19 } else { 20 toToggle.className = "hidden"; 21 (event.target as HTMLElement).textContent = "Merge Requirements: (click to expand)"; 22 } 23 }; 24 } 25 26 function redraw(): void { 27 redrawQueries(); 28 redrawPools(); 29 } 30 31 function createLink(href: string, text: string): HTMLAnchorElement { 32 const a = document.createElement("a"); 33 a.href = href; 34 a.appendChild(document.createTextNode(text)); 35 return a; 36 } 37 38 /** 39 * escapeLabel escaped label name that returns a valid name used for css 40 * selector. 41 */ 42 function escapeLabel(label: string): string { 43 if (label === "") { return ""; } 44 const toUnicode = (index: number): string => { 45 const h = label.charCodeAt(index).toString(16).split(''); 46 while (h.length < 6) { h.splice(0, 0, '0'); } 47 48 return `x${ h.join('')}`; 49 }; 50 let result = ""; 51 const alphaNum = /^[0-9a-zA-Z]+$/; 52 53 for (let i = 0; i < label.length; i++) { 54 const c = label.charCodeAt(i); 55 if ((i === 0 && c > 47 && c < 58) || !label[i].match(alphaNum)) { 56 result += toUnicode(i); 57 continue; 58 } 59 result += label[i]; 60 } 61 62 return result; 63 } 64 65 /** 66 * Creates a HTML element for the label given its name 67 */ 68 function createLabelEl(label: string): HTMLElement { 69 const el = document.createElement("span"); 70 const escapedName = escapeLabel(label); 71 el.classList.add("mdl-shadow--2dp", "label", escapedName); 72 el.textContent = label; 73 74 return el; 75 } 76 77 function createStrong(text: string): HTMLElement { 78 const s = document.createElement("strong"); 79 s.appendChild(document.createTextNode(text)); 80 return s; 81 } 82 83 function fillDetail(data: string | string[] | undefined, type: string, connector: string, container: HTMLElement, styleData: (content: string) => Node) { 84 if (!data || (Array.isArray(data) && data.length === 0)) { 85 return; 86 } 87 container.appendChild(createStrong(connector)); 88 container.appendChild(document.createTextNode(`the following ${type}: `)); 89 container.appendChild(document.createElement("br")); 90 91 const ul = document.createElement("ul"); 92 const li = document.createElement("li"); 93 ul.appendChild(li); 94 container.appendChild(ul); 95 96 if (typeof data === 'string') { 97 li.appendChild(document.createTextNode(data)); 98 } else if (Array.isArray(data)) { 99 for (let i = 0; i < data.length; i++) { 100 const v = data[i]; 101 li.appendChild(styleData(v)); 102 if (i + 1 < data.length) { 103 li.appendChild(document.createTextNode(" ")); 104 } 105 } 106 } 107 } 108 109 function redrawQueries(): void { 110 const queries = document.getElementById("queries")!; 111 while (queries.firstChild) { 112 queries.removeChild(queries.firstChild); 113 } 114 115 if (!tideData.Queries) { 116 return; 117 } 118 for (let i = 0; i < tideData.Queries.length; i++) { 119 const query = tideData.Queries[i]; 120 const tideQuery = tideData.TideQueries[i]; 121 122 // create list entry for the query, all details will be within this element 123 const li = document.createElement("li"); 124 125 // GitHub query search link 126 const a = createLink( 127 `/github-link?dest=${encodeURIComponent(`search?utf8=\u2713&q=${ query}`)}`, 128 "GitHub Search Link", 129 ); 130 li.appendChild(a); 131 li.appendChild(document.createTextNode(" - Meaning: Is an open Pull Request")); 132 133 // build the description 134 // all queries should implicitly mean this 135 // add the list of repos, defaulting to an empty array if no repos have been provided. 136 const orgs = tideQuery.orgs || []; 137 const repos = tideQuery.repos || []; 138 const excludedRepos = tideQuery.excludedRepos || []; 139 if (orgs.length > 0) { 140 li.appendChild(document.createTextNode(" in one of the following orgs: ")); 141 const ul = document.createElement("ul"); 142 for (let j = 0; j < orgs.length; j++) { 143 const innerLi = document.createElement("li"); 144 innerLi.appendChild(createLink(`/github-link?dest=${ orgs[j]}`, orgs[j])); 145 if (j + 1 < repos.length) { 146 innerLi.appendChild(document.createTextNode(", ")); 147 } 148 ul.appendChild(innerLi); 149 } 150 li.appendChild(ul); 151 } 152 if (repos.length > 0) { 153 let reposText = " in one of the following repos: "; 154 if (orgs.length > 0) { 155 reposText = ` or ${ reposText}`; 156 } 157 li.appendChild(document.createTextNode(reposText)); 158 const ul = document.createElement("ul"); 159 const innerLi = document.createElement("li"); 160 for (let j = 0; j < repos.length; j++) { 161 innerLi.appendChild(createLink(`/github-link?dest=${ repos[j]}`, repos[j])); 162 if (j + 1 < repos.length) { 163 innerLi.appendChild(document.createTextNode(", ")); 164 } 165 } 166 ul.appendChild(innerLi); 167 li.appendChild(ul); 168 } 169 if (excludedRepos.length > 0) { 170 li.appendChild(document.createTextNode(" but NOT in any of the following excluded repos: ")); 171 const ul = document.createElement("ul"); 172 const innerLi = document.createElement("li"); 173 for (let j = 0; j < excludedRepos.length; j++) { 174 innerLi.appendChild(createLink(`/github-link?dest=${ excludedRepos[j]}`, excludedRepos[j])); 175 if (j + 1 < excludedRepos.length) { 176 innerLi.appendChild(document.createTextNode(", ")); 177 } 178 } 179 ul.appendChild(innerLi); 180 li.appendChild(ul); 181 } 182 // required labels 183 fillDetail(tideQuery.labels, "labels", "with ", li, (data) => createLabelEl(data)); 184 // required to be not present labels 185 fillDetail(tideQuery.missingLabels, "labels", "without ", li, (data) => createLabelEl(data)); 186 // list milestone if existed 187 fillDetail(tideQuery.milestone, "milestone", "with ", li, (data) => document.createTextNode(data)); 188 // list all excluded branches 189 fillDetail(tideQuery.excludedBranches, "branches", "exclude ", li, (data) => document.createTextNode(data)); 190 // list all included branches 191 fillDetail(tideQuery.includedBranches, "branches", "targeting ", li, (data) => document.createTextNode(data)); 192 // GitHub native review required 193 const reviewApprovedRequired = tideQuery.hasOwnProperty("reviewApprovedRequired") && tideQuery.reviewApprovedRequired; 194 if (reviewApprovedRequired) { 195 li.appendChild(document.createTextNode("and must be ")); 196 li.appendChild(createLink( 197 "https://help.github.com/articles/about-pull-request-reviews/", 198 "approved by GitHub review", 199 )); 200 } 201 202 // actually add the entry 203 queries.appendChild(li); 204 } 205 } 206 207 function redrawPools(): void { 208 const pools = document.getElementById("pools")!.getElementsByTagName("tbody")[0]; 209 while (pools.firstChild) { 210 pools.removeChild(pools.firstChild); 211 } 212 213 if (!tideData.Pools) { 214 return; 215 } 216 for (const pool of tideData.Pools) { 217 const r = document.createElement("tr"); 218 219 r.appendChild(createHistoryCell(pool)); 220 r.appendChild(createRepoCell(pool)); 221 r.appendChild(createActionCell(pool)); 222 r.appendChild(createBatchCell(pool)); 223 r.appendChild(createPRCell(pool, pool.SuccessPRs)); 224 r.appendChild(createPRCell(pool, pool.PendingPRs)); 225 r.appendChild(createPRCell(pool, pool.MissingPRs)); 226 227 pools.appendChild(r); 228 } 229 } 230 231 function createHistoryCell(pool: TidePool): HTMLTableDataCellElement { 232 const td = document.createElement("td"); 233 td.classList.add("icon-cell"); 234 td.appendChild(tidehistory.poolIcon(pool.Org, pool.Repo, pool.Branch)); 235 return td; 236 } 237 238 function createRepoCell(pool: TidePool): HTMLTableDataCellElement { 239 const deckLink = `/?repo=${ encodeURIComponent(`${pool.Org}/${pool.Repo}`)}`; 240 const branchLink = `/github-link?dest=${pool.Org}/${pool.Repo}/tree/${pool.Branch}`; 241 const linksTD = document.createElement("td"); 242 linksTD.appendChild(createLink(deckLink, `${pool.Org}/${pool.Repo}`)); 243 linksTD.appendChild(document.createTextNode(" ")); 244 linksTD.appendChild(createLink(branchLink, pool.Branch)); 245 return linksTD; 246 } 247 248 function createActionCell(pool: TidePool): HTMLTableDataCellElement { 249 const targeted = pool.Target && pool.Target.length; 250 const blocked = pool.Blockers && pool.Blockers.length; 251 let action = pool.Action.replace("_", " "); 252 if (targeted || blocked) { 253 action += ": "; 254 } 255 const c = document.createElement("td"); 256 c.appendChild(document.createTextNode(action)); 257 258 if (blocked) { 259 c.classList.add("blocked"); 260 addBlockersToElem(c, pool); 261 } else if (targeted) { 262 addPRsToElem(c, pool, pool.Target); 263 } 264 return c; 265 } 266 267 function createPRCell(pool: TidePool, prs: PullRequest[]): HTMLTableDataCellElement { 268 const c = document.createElement("td"); 269 addPRsToElem(c, pool, prs); 270 return c; 271 } 272 273 function createBatchCell(pool: TidePool): HTMLTableDataCellElement { 274 const td = document.createElement('td'); 275 if (pool.BatchPending) { 276 const numbers = pool.BatchPending.map((p) => String(p.Number)); 277 const batchRef = `${pool.Branch},${numbers.join(',')}`; 278 const encodedRepo = encodeURIComponent(`${pool.Org}/${pool.Repo}`); 279 const href = `/?repo=${encodedRepo}&type=batch&pull=${encodeURIComponent(batchRef)}`; 280 const link = document.createElement('a'); 281 link.href = href; 282 for (let i = 0; i < pool.BatchPending.length; i++) { 283 const pr = pool.BatchPending[i]; 284 const text = document.createElement('span'); 285 text.appendChild(document.createTextNode(`#${ String(pr.Number)}`)); 286 text.id = `pr-${pool.Org}-${pool.Repo}-${pr.Number}-${nextID()}`; 287 if (pr.Title) { 288 const tip = tooltip.forElem(text.id, document.createTextNode(pr.Title)); 289 text.appendChild(tip); 290 } 291 link.appendChild(text); 292 // Add a space after each PR number except the last. 293 if (i + 1 < pool.BatchPending.length) { 294 link.appendChild(document.createTextNode(" ")); 295 } 296 } 297 td.appendChild(link); 298 } 299 return td; 300 } 301 302 // addPRsToElem adds a space separated list of PR numbers that link to the corresponding PR on github. 303 function addPRsToElem(elem: HTMLElement, pool: TidePool, prs?: PullRequest[]): void { 304 if (prs) { 305 for (let i = 0; i < prs.length; i++) { 306 const a = document.createElement("a"); 307 a.href = `/github-link?dest=${pool.Org}/${pool.Repo}/pull/${prs[i].Number}`; 308 a.appendChild(document.createTextNode(`#${ prs[i].Number}`)); 309 a.id = `pr-${pool.Org}-${pool.Repo}-${prs[i].Number}-${nextID()}`; 310 if (prs[i].Title) { 311 const tip = tooltip.forElem(a.id, document.createTextNode(prs[i].Title)); 312 a.appendChild(tip); 313 } 314 elem.appendChild(a); 315 // Add a space after each PR number except the last. 316 if (i + 1 < prs.length) { 317 elem.appendChild(document.createTextNode(" ")); 318 } 319 } 320 } 321 } 322 323 // addBlockersToElem adds a space separated list of Issue numbers that link to the 324 // corresponding Issues on github that are blocking merge. 325 function addBlockersToElem(elem: HTMLElement, pool: TidePool): void { 326 if (!pool.Blockers) { 327 return; 328 } 329 for (let i = 0; i < pool.Blockers.length; i++) { 330 const b = pool.Blockers[i]; 331 const a = document.createElement("a"); 332 a.href = b.URL; 333 a.appendChild(document.createTextNode(`#${ b.Number}`)); 334 a.id = `blocker-${pool.Org}-${pool.Repo}-${b.Number}-${nextID()}`; 335 a.appendChild(tooltip.forElem(a.id, document.createTextNode(b.Title))); 336 337 elem.appendChild(a); 338 // Add a space after each PR number except the last. 339 if (i + 1 < pool.Blockers.length) { 340 elem.appendChild(document.createTextNode(" ")); 341 } 342 } 343 } 344 345 let idCounter = 0; 346 function nextID(): string { 347 idCounter++; 348 return `elemID-${ String(idCounter)}`; 349 }