github.com/yrj2011/jx-test-infra@v0.0.0-20190529031832-7a2065ee98eb/prow/cmd/deck/static/pr-script.js (about) 1 'use strict'; 2 3 /** 4 * A Tide Query helper class that checks whether a pr is covered by the query. 5 */ 6 class TideQuery { 7 constructor(query) { 8 this.repos = query.repos; 9 this.orgs = query.orgs; 10 this.labels = query.labels; 11 this.missingLabels = query.missingLabels; 12 } 13 14 /** 15 * Returns true if the pr is covered by the query. 16 * @param pr 17 * @returns {boolean} 18 */ 19 matchPr(pr) { 20 let isMatched = false; 21 if (this.repos) { 22 isMatched |= this.repos.indexOf(pr.Repository.NameWithOwner) !== -1; 23 } else if (this.orgs) { 24 isMatched |= this.orgs.indexOf(pr.Repository.Owner.Login) !== -1; 25 } 26 return isMatched; 27 } 28 29 /** 30 * Returns labels and missing labels of the query. 31 * @returns {{labels: string[], missingLabels: string[]}} 32 */ 33 getQuery() { 34 return { 35 labels: this.labels, 36 missingLabels: this.missingLabels 37 } 38 } 39 } 40 41 /** 42 * Submit the query by redirecting the page with the query and let window.onload 43 * sends the request. 44 * @param {Element} input query input element 45 */ 46 function submitQuery(input) { 47 const query = getPRQuery(input.value); 48 input.value = query; 49 window.location.search = '?query=' + encodeURIComponent(query); 50 } 51 52 /** 53 * Creates a XMLHTTP request to /pr-data.js. 54 * @param {function} fulfillFn 55 * @param {function} errorHandler 56 * @return {XMLHttpRequest} 57 */ 58 function createXMLHTTPRequest(fulfillFn, errorHandler) { 59 const request = new XMLHttpRequest(); 60 const url = "/pr-data.js"; 61 request.onreadystatechange = () => { 62 if (request.readyState === 4 && request.status === 200) { 63 fulfillFn(request); 64 } 65 }; 66 request.onerror = () => { 67 errorHandler(); 68 }; 69 request.withCredentials = true; 70 request.open("POST", url, true); 71 request.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); 72 73 return request; 74 } 75 76 /** 77 * Makes sure the search query is looking for pull requests by dropping all 78 * is:issue and type:pr tokens and adds is:pr if does not exist. 79 * @param {string} q 80 * @return {string} 81 */ 82 function getPRQuery(q) { 83 const tokens = q.replace(/\+/g, " ").split(" "); 84 // Firstly, drop all pr/issue search tokens 85 let result = tokens.filter(tkn => { 86 tkn = tkn.trim(); 87 return !(tkn === "is:issue" || tkn === "type:issue" || tkn === "is:pr" 88 || tkn === "type:pr"); 89 }).join(" "); 90 // Returns the query with is:pr to the start of the query 91 result = "is:pr " + result; 92 return result; 93 } 94 95 /** 96 * Redraw the page 97 * @param {Object} prData 98 */ 99 function redraw(prData) { 100 const mainContainer = document.querySelector("#main-container"); 101 while (mainContainer.firstChild) { 102 mainContainer.removeChild(mainContainer.firstChild); 103 } 104 if (prData && prData.Login) { 105 loadPrStatus(prData); 106 } else { 107 forceGithubLogin(); 108 } 109 } 110 111 /** 112 * Enables/disables the progress bar. 113 * @param {boolean} isStarted 114 */ 115 function loadProgress(isStarted) { 116 const pg = document.querySelector("#loading-progress"); 117 if (isStarted) { 118 pg.classList.remove("hidden"); 119 } else { 120 pg.classList.add("hidden"); 121 } 122 } 123 124 /** 125 * Handles the URL query on load event. 126 */ 127 function onLoadQuery() { 128 const query = window.location.search.substring(1); 129 const params = query.split("&"); 130 if (!params[0]) { 131 return ""; 132 } 133 const val = params[0].slice("query=".length); 134 if (val && val !== "") { 135 return decodeURIComponent(val.replace(/\+/g, ' ')); 136 } 137 return ""; 138 } 139 140 /** 141 * Gets cookie by its name. 142 * @param {string} name 143 */ 144 function getCookieByName(name) { 145 if (!document.cookie) { 146 return ""; 147 } 148 const cookies = decodeURIComponent(document.cookie).split(";"); 149 for (let i = 0; i < cookies.length; i++) { 150 const c = cookies[i].trim(); 151 const pref = name + "="; 152 if (c.indexOf(pref) === 0) { 153 return c.slice(pref.length); 154 } 155 } 156 return ""; 157 } 158 159 window.onload = () => { 160 document.querySelectorAll("dialog").forEach((dialog) => { 161 dialogPolyfill.registerDialog(dialog); 162 dialog.querySelector('.close').addEventListener('click', () => { 163 dialog.close(); 164 }); 165 }); 166 // Check URL, if the search is empty, adds search query by default format 167 // ?is:pr state:open query="author:<user_login>" 168 if (window.location.search === "") { 169 const login = getCookieByName("github_login"); 170 const searchQuery = "is:pr state:open " + "author:" + login; 171 window.location.search = "?query=" + encodeURIComponent(searchQuery); 172 } 173 const request = createXMLHTTPRequest((request) => { 174 const prData = JSON.parse(request.responseText); 175 redraw(prData); 176 loadProgress(false); 177 }, () => { 178 loadProgress(false); 179 const mainContainer = document.querySelector("#main-container"); 180 mainContainer.appendChild(createMessage("Something wrongs! We could not fulfill your request")); 181 }); 182 loadProgress(true); 183 request.send("query=" + onLoadQuery()); 184 }; 185 186 function createSearchCard() { 187 const searchCard = document.createElement("DIV"); 188 searchCard.id = "search-card"; 189 searchCard.classList.add("pr-card", "mdl-card"); 190 191 // Input box 192 const input = document.createElement("TEXTAREA"); 193 input.id = "search-input"; 194 input.value = decodeURIComponent(window.location.search.slice("query=".length + 1)); 195 input.rows = 1; 196 input.spellcheck = false; 197 input.addEventListener("keydown", (event) => { 198 if (event.keyCode === 13) { 199 event.preventDefault(); 200 submitQuery(input); 201 } else { 202 const el = event.target; 203 el.style.height = "auto"; 204 el.style.height = event.target.scrollHeight + "px"; 205 } 206 }); 207 input.addEventListener("focus", (event) => { 208 const el = event.target; 209 el.style.height = "auto"; 210 el.style.height = event.target.scrollHeight + "px"; 211 }); 212 // Refresh button 213 const refBtn = createIcon("refresh", "Reload the query", ["search-button"], true); 214 refBtn.addEventListener("click", () => { 215 submitQuery(input); 216 }, true); 217 const userBtn = createIcon("person", "Show my open pull requests", ["search-button"], true); 218 userBtn.addEventListener("click", () => { 219 const login = getCookieByName("github_login"); 220 const searchQuery = "is:pr state:open " + "author:" + login; 221 window.location.search = "?query=" + encodeURIComponent(searchQuery); 222 }); 223 224 const actionCtn = document.createElement("DIV"); 225 actionCtn.id = "search-action"; 226 actionCtn.appendChild(userBtn); 227 actionCtn.appendChild(refBtn); 228 229 const inputContainer = document.createElement("DIV"); 230 inputContainer.id = "search-input-ctn"; 231 inputContainer.appendChild(input); 232 inputContainer.appendChild(actionCtn); 233 234 const title = document.createElement("H6"); 235 title.textContent = "Github search query"; 236 const infoBtn = createIcon("info", "More information about the search query", ["search-info"], true); 237 const titleCtn = document.createElement("DIV"); 238 titleCtn.appendChild(title); 239 titleCtn.appendChild(infoBtn); 240 titleCtn.classList.add("search-title"); 241 242 const searchDialog = document.querySelector("#search-dialog"); 243 infoBtn.addEventListener("click", () => { 244 searchDialog.showModal(); 245 }); 246 247 searchCard.appendChild(titleCtn); 248 searchCard.appendChild(inputContainer); 249 return searchCard; 250 } 251 252 /** 253 * GetFullPRContexts gathers build jobs and pr contexts. It firstly takes 254 * all pr contexts and only replaces contexts that have existing Prow Jobs. Tide 255 * context will be omitted from the list. 256 * @param builds 257 * @param contexts 258 * @returns {Array} 259 */ 260 function getFullPRContext(builds, contexts) { 261 const contextMap = new Map(); 262 if (contexts) { 263 for (let context of contexts) { 264 if (context.Context === "tide") { 265 continue; 266 } 267 contextMap.set(context.Context, { 268 context: context.Context, 269 description: context.Description, 270 state: context.State.toLowerCase(), 271 discrepancy: null, 272 }); 273 } 274 } 275 if (builds) { 276 for (let build of builds) { 277 let discrepancy = null; 278 // If Github context exits, check if mismatch or not. 279 if (contextMap.has(build.context)) { 280 const githubContext = contextMap.get(build.context); 281 // TODO (qhuynh96): ProwJob's states and Github contexts states 282 // are not equivalent in some states. 283 if (githubContext.state !== build.state) { 284 discrepancy = "Github context and Prow Job states mismatch"; 285 } 286 } 287 contextMap.set(build.context, { 288 context: build.context, 289 description: build.description, 290 state: build.state, 291 url: build.url, 292 discrepancy: discrepancy 293 }); 294 } 295 } 296 const fullContexts = []; 297 for (let value of contextMap.values()) { 298 fullContexts.push(value); 299 } 300 return fullContexts; 301 } 302 303 /** 304 * Loads Pr Status 305 */ 306 function loadPrStatus(prData) { 307 const tideQueries = []; 308 for (let query of tideData.TideQueries) { 309 tideQueries.push(new TideQuery(query)); 310 } 311 312 const container = document.querySelector("#main-container"); 313 container.appendChild(createSearchCard()); 314 if (!prData.PullRequestsWithContexts || prData.PullRequestsWithContexts.length === 0) { 315 const msg = createMessage("No open PRs found", ""); 316 container.appendChild(msg); 317 return; 318 } 319 for (let prWithContext of prData.PullRequestsWithContexts) { 320 // There might be multiple runs of jobs for a build. 321 // allBuilds is sorted with the most recent builds first, so 322 // we only need to keep the first build for each job name. 323 let pr = prWithContext.PullRequest; 324 let seenJobs = {}; 325 let builds = []; 326 for (let build of allBuilds) { 327 if (build.type === 'presubmit' && 328 build.repo === pr.Repository.NameWithOwner && 329 build.base_ref === pr.BaseRef.Name && 330 build.number === pr.Number && 331 build.pull_sha === pr.HeadRefOID) { 332 if (!seenJobs[build.job]) { // First (latest) build for job. 333 seenJobs[build.job] = true; 334 builds.push(build); 335 } 336 } 337 } 338 const githubContexts = prWithContext.Contexts; 339 const contexts = getFullPRContext(builds, githubContexts); 340 const validQueries = []; 341 for (let query of tideQueries) { 342 if (query.matchPr(pr)) { 343 validQueries.push(query.getQuery()); 344 } 345 } 346 container.appendChild(createPRCard(pr, contexts, validQueries, tideData.Pools)); 347 } 348 } 349 350 /** 351 * Creates Pool labels. 352 * @param pr 353 * @param tidePool 354 * @return {Element} 355 */ 356 function createTidePoolLabel(pr, tidePool) { 357 if (!tidePool || tidePool.length === 0) { 358 return null; 359 } 360 const poolTypes = [tidePool.Target, tidePool.BatchPending, 361 tidePool.SuccessPRs, tidePool.PendingPRs, tidePool.MissingPRs]; 362 const inPoolId = poolTypes.findIndex(poolType => { 363 if (!poolType) { 364 return false; 365 } 366 const index = poolType.findIndex(prInPool => { 367 return prInPool.Number === pr.Number; 368 }); 369 return index !== -1; 370 }); 371 const label = document.createElement("SPAN"); 372 if (inPoolId === -1) { 373 return null; 374 } 375 const labelTitle = ["Merging", "In Batch & Test Pending", 376 "Test Passing & Merge Pending", "Test Pending", 377 "Test failed/Missing Labels"]; 378 const labelStyle = ["merging", "batching", "passing", "pending", "failed"]; 379 label.textContent = "In Pool - " + labelTitle[inPoolId]; 380 label.classList.add("title-label", "mdl-shadow--2dp", labelStyle[inPoolId]); 381 382 return label; 383 } 384 385 /** 386 * Creates a label for the title. It will prioritise the merge status over the 387 * job status. Saying that, if the pr has jobs failed and does not meet merge 388 * requirements, it will show that the PR needs to resolve labels. 389 * @param isMerge {boolean} 390 * @param jobStatus {string} 391 * @param mergeAbility {number} 392 * @return {Element} 393 */ 394 function createTitleLabel(isMerge, jobStatus, mergeAbility) { 395 const label = document.createElement("SPAN"); 396 label.classList.add("title-label"); 397 if (isMerge) { 398 label.textContent = "Merged"; 399 label.classList.add("merging"); 400 } else if (mergeAbility === -1) { 401 label.textContent = "Unknown Merge Requirements"; 402 label.classList.add("unknown"); 403 } else if (mergeAbility === 0) { 404 label.textContent = "Needs to Resolve Labels"; 405 label.classList.add("pending"); 406 } else { 407 if (jobStatus === "succeeded") { 408 label.textContent = "Good to be merged"; 409 label.classList.add(jobStatus); 410 } else { 411 label.textContent = "Jobs " + jobStatus; 412 label.classList.add(jobStatus); 413 } 414 } 415 416 return label; 417 } 418 419 /** 420 * Creates PR Card title. 421 * @param {Object} pr 422 * @param {Array<Object>} tidePools 423 * @param {string} jobStatus 424 * @param {number} mergeAbility 425 * @return {Element} 426 */ 427 function createPRCardTitle(pr, tidePools, jobStatus, mergeAbility) { 428 const prTitle = document.createElement("DIV"); 429 prTitle.classList.add("mdl-card__title"); 430 431 const title = document.createElement("H4"); 432 title.textContent = "#" + pr.Number; 433 title.classList.add("mdl-card__title-text"); 434 435 const subtitle = document.createElement("H5"); 436 subtitle.textContent = pr.Repository.NameWithOwner; 437 subtitle.classList.add("mdl-card__subtitle-text"); 438 439 const link = document.createElement("A"); 440 link.href = "https://github.com/" + pr.Repository.NameWithOwner + "/pull/" 441 + pr.Number; 442 link.appendChild(title); 443 444 const prTitleText = document.createElement("DIV"); 445 prTitleText.appendChild(link); 446 prTitleText.appendChild(subtitle); 447 prTitleText.classList.add("pr-title-text"); 448 prTitle.appendChild(prTitleText); 449 450 const pool = tidePools.filter(pool => { 451 const repo = pool.Org + "/" + pool.Repo; 452 return pr.Repository.NameWithOwner === repo && pr.BaseRef.Name 453 === pool.Branch; 454 }); 455 let tidePoolLabel = createTidePoolLabel(pr, pool[0]); 456 if (!tidePoolLabel) { 457 tidePoolLabel = createTitleLabel(pr.Merged, jobStatus, mergeAbility); 458 } 459 prTitle.appendChild(tidePoolLabel); 460 461 return prTitle; 462 } 463 464 /** 465 * Creates a list of contexts. 466 * @param contexts 467 * @param itemStyle 468 * @return {Element} 469 */ 470 function createContextList(contexts, itemStyle = []) { 471 const container = document.createElement("UL"); 472 container.classList.add("mdl-list", "job-list"); 473 const getStateIcon = (state) => { 474 switch (state) { 475 case "success": 476 return "check_circle"; 477 case "failure": 478 return "error"; 479 case "pending": 480 return "watch_later"; 481 case "triggered": 482 return "schedule"; 483 case "aborted": 484 return "remove_circle"; 485 case "error": 486 return "warning"; 487 default: 488 return ""; 489 } 490 }; 491 const getItemContainer = (context) => { 492 if (context.url) { 493 const item = document.createElement("A"); 494 item.href = context.url; 495 return item; 496 } else { 497 return document.createElement("DIV"); 498 } 499 }; 500 contexts.forEach(context => { 501 const elCon = document.createElement("LI"); 502 elCon.classList.add("mdl-list__item", "job-list-item", ...itemStyle); 503 const item = getItemContainer(context); 504 item.classList.add("mdl-list__item-primary-content"); 505 item.appendChild(createIcon( 506 getStateIcon(context.state), 507 "", 508 ["state", context.state, "mdl-list__item-icon"])); 509 item.appendChild(document.createTextNode(context.context)); 510 if (context.discrepancy) { 511 item.appendChild(createIcon( 512 "warning", 513 context.discrepancy, 514 ["state", "context-warning", "mdl-list__item-icon"])); 515 } 516 elCon.appendChild(item); 517 if (context.description) { 518 const itemDesc = document.createElement("SPAN"); 519 itemDesc.textContent = context.description; 520 itemDesc.style.color = "grey"; 521 itemDesc.style.fontSize = "14px"; 522 elCon.appendChild(itemDesc); 523 } 524 container.appendChild(elCon); 525 }); 526 return container; 527 } 528 529 /** 530 * Creates Job status. 531 * @param builds 532 * @return {Element} 533 */ 534 function createJobStatus(builds) { 535 const statusContainer = document.createElement("DIV"); 536 statusContainer.classList.add("status-container"); 537 const status = document.createElement("DIV"); 538 const failedJobs = builds.filter(build => { 539 return build.state === "failure"; 540 }); 541 // Job status indicator 542 const state = jobStatus(builds); 543 let statusText = ""; 544 let stateIcon = ""; 545 switch (state) { 546 case "succeeded": 547 statusText = "All tests passed"; 548 stateIcon = "check_circle"; 549 break; 550 case "failed": 551 statusText = failedJobs.length + " test" + (failedJobs.length === 1 ? "" : "s") + " failed"; 552 stateIcon = "error"; 553 break; 554 case "unknown": 555 statusText = "No test found"; 556 break; 557 default: 558 statusText = "Tests are running"; 559 stateIcon = "watch_later"; 560 } 561 const arrowIcon = createIcon("expand_more"); 562 arrowIcon.classList.add("arrow-icon"); 563 if (state === "unknown") { 564 arrowIcon.classList.add("hidden"); 565 const p = document.createElement("P"); 566 p.textContent = "Test results for this PR are not in our record but you can always find them on PR's GitHub page. Sorry for any inconvenience!"; 567 568 status.appendChild(document.createTextNode(statusText)); 569 status.appendChild(createStatusHelp("No test found", [p])); 570 status.classList.add("no-status"); 571 } else { 572 status.appendChild(createIcon(stateIcon, "", ["status-icon", state])); 573 status.appendChild(document.createTextNode(statusText)); 574 } 575 status.classList.add("status"); 576 statusContainer.appendChild(status); 577 // Job list 578 let failedJobsList; 579 if (failedJobs.length > 0) { 580 failedJobsList = createContextList(failedJobs); 581 statusContainer.appendChild(failedJobsList); 582 } 583 const jobList = createContextList(builds); 584 jobList.classList.add("hidden"); 585 status.addEventListener("click", () => { 586 if (state === "unknown") { 587 return; 588 } 589 if (failedJobsList) { 590 failedJobsList.classList.add("hidden"); 591 } 592 jobList.classList.toggle("hidden"); 593 arrowIcon.textContent = arrowIcon.textContent === "expand_more" 594 ? "expand_less" : "expand_more"; 595 }); 596 597 status.appendChild(arrowIcon); 598 statusContainer.appendChild(jobList); 599 return statusContainer; 600 } 601 602 /** 603 * Creates a merge requirement cell. 604 * @param labels 605 * @param notMissingLabel 606 * @return {Element} 607 */ 608 function createMergeLabelCell(labels, notMissingLabel = false) { 609 const cell = document.createElement("TD"); 610 labels.forEach(label => { 611 const labelEl = document.createElement("SPAN"); 612 const name = label.name.split(" ").join(""); 613 labelEl.classList.add("merge-table-label", "mdl-shadow--2dp", "label", 614 name); 615 labelEl.textContent = label.name; 616 const toDisplay = label.own ^ notMissingLabel; 617 if (toDisplay) { 618 cell.appendChild(labelEl); 619 } 620 }); 621 622 return cell; 623 } 624 625 function processLabelName(label) { 626 label = label.split(" ").join(""); 627 if (label.startsWith("area/")) { 628 return "area"; 629 } else if (label.startsWith("size/")) { 630 return label.slice(5); 631 } else { 632 return label; 633 } 634 } 635 636 /** 637 * Appends labels to a container 638 * @param {Element} container 639 * @param {Array<string>} labels 640 */ 641 function appendLabelsToContainer(container, labels) { 642 while (container.firstChild) { 643 container.removeChild(container.firstChild); 644 } 645 labels.forEach(label => { 646 const labelEl = document.createElement("SPAN"); 647 648 let labelName = processLabelName(label); 649 labelEl.classList.add("merge-table-label", "mdl-shadow--2dp", "label", labelName); 650 labelEl.textContent = label; 651 container.appendChild(labelEl); 652 }); 653 } 654 655 /** 656 * Creates merge requirement table for queries. 657 * @param prLabels 658 * @param queries 659 * @return {Element} 660 */ 661 function createQueriesTable(prLabels, queries) { 662 const table = document.createElement("TABLE"); 663 table.classList.add("merge-table"); 664 const thead = document.createElement("THEAD"); 665 const allLabelHeaderRow = document.createElement("TR"); 666 const allLabelHeaderCell = document.createElement("TD"); 667 // Creates all pr labels header. 668 allLabelHeaderCell.textContent = "PR's Labels"; 669 allLabelHeaderCell.colSpan = 3; 670 allLabelHeaderRow.appendChild(allLabelHeaderCell); 671 thead.appendChild(allLabelHeaderRow); 672 673 const allLabelRow = document.createElement("TR"); 674 const allLabelCell = document.createElement("TD"); 675 allLabelCell.colSpan = 3; 676 appendLabelsToContainer(allLabelCell, prLabels.map(label => { 677 return label.Label.Name; 678 })); 679 allLabelRow.appendChild(allLabelCell); 680 thead.appendChild(allLabelRow); 681 682 const tableRow = document.createElement("TR"); 683 const col1 = document.createElement("TD"); 684 col1.textContent = "Required Labels (Missing)"; 685 const col2 = document.createElement("TD"); 686 col2.textContent = "Forbidden Labels (Shouldn't have)"; 687 const col3 = document.createElement("TD"); 688 689 const body = document.createElement("TBODY"); 690 queries.forEach(query => { 691 const row = document.createElement("TR"); 692 row.append(createMergeLabelCell(query.labels, true)); 693 row.append(createMergeLabelCell(query.missingLabels)); 694 695 const mergeIcon = document.createElement("TD"); 696 mergeIcon.classList.add("merge-table-icon"); 697 const iconButton = createIcon("information", "Clicks to see query details", [], true); 698 mergeIcon.appendChild(iconButton); 699 row.appendChild(mergeIcon); 700 701 body.appendChild(row); 702 const dialog = document.querySelector("#query-dialog"); 703 const allRequired = document.querySelector("#query-all-required"); 704 const allForbidden = document.querySelector("#query-all-forbidden"); 705 iconButton.addEventListener("click", () => { 706 appendLabelsToContainer(allRequired, query.labels.map(label => { 707 return label.name; 708 })); 709 appendLabelsToContainer(allForbidden, query.missingLabels.map(label => { 710 return label.name; 711 })); 712 dialog.showModal(); 713 }); 714 }); 715 716 tableRow.appendChild(col1); 717 tableRow.appendChild(col2); 718 tableRow.appendChild(col3); 719 thead.appendChild(tableRow); 720 table.appendChild(thead); 721 table.appendChild(body); 722 723 return table; 724 } 725 726 /** 727 * Creates the merge requirement status.b 728 * @param prLabels 729 * @param queries 730 * @return {Element} 731 */ 732 function createMergeStatus(prLabels, queries) { 733 prLabels = prLabels ? prLabels : []; 734 const statusContainer = document.createElement("DIV"); 735 statusContainer.classList.add("status-container"); 736 const status = document.createElement("DIV"); 737 const mergeAbility = isAbleToMerge(queries); 738 if (mergeAbility === 0) { 739 status.appendChild(createIcon("error", "", ["status-icon", "failed"])); 740 status.appendChild(document.createTextNode("Does not meet merge requirements")); 741 // Creates help button 742 const iconButton = createIcon("help", "", ["help-icon-button"], true); 743 status.appendChild(iconButton); 744 // Shows dialog 745 const dialog = document.querySelector("#merge-help-dialog"); 746 iconButton.addEventListener("click", (event) => { 747 dialog.showModal(); 748 event.stopPropagation(); 749 }); 750 } else if (mergeAbility === 1) { 751 status.appendChild(createIcon("check_circle", "", ["status-icon", "succeeded"])); 752 status.appendChild(document.createTextNode("Meets merge requirements")); 753 } else { 754 status.appendChild(document.createTextNode("No Tide query found")); 755 status.classList.add("no-status"); 756 const p = document.createElement("P"); 757 p.textContent = "This repo may not be configured to use Tide."; 758 status.appendChild(createStatusHelp("Tide query not found", [p])); 759 } 760 const arrowIcon= createIcon("expand_less"); 761 arrowIcon.classList.add("arrow-icon"); 762 763 status.classList.add("status"); 764 status.appendChild(arrowIcon); 765 766 const queriesTable = createQueriesTable(prLabels, queries); 767 if (mergeAbility !== 0) { 768 queriesTable.classList.add("hidden"); 769 arrowIcon.textContent = "expand_more"; 770 } 771 status.addEventListener("click", () => { 772 queriesTable.classList.toggle("hidden"); 773 if (queriesTable.classList.contains("hidden")) { 774 const offLabels = queriesTable.querySelectorAll( 775 ".merge-table-label.off"); 776 offLabels.forEach(offLabel => { 777 offLabel.classList.add("hidden"); 778 }); 779 } 780 arrowIcon.textContent = arrowIcon.textContent === "expand_more" 781 ? "expand_less" : "expand_more"; 782 }); 783 784 statusContainer.appendChild(status); 785 statusContainer.appendChild(queriesTable); 786 return statusContainer; 787 } 788 789 /** 790 * Creates a help button on the status. 791 * @param {string} title 792 * @param {Array<Element>} content 793 * @return {Element} 794 */ 795 function createStatusHelp(title, content) { 796 const dialog = document.querySelector("#status-help-dialog"); 797 const dialogTitle = dialog.querySelector(".mdl-dialog__title"); 798 const dialogContent = dialog.querySelector(".mdl-dialog__content"); 799 const helpIcon = createIcon("help", "", ["help-icon-button"], true); 800 helpIcon.addEventListener("click", (event) => { 801 dialogTitle.textContent = title; 802 while (dialogContent.firstChild) { 803 dialogContent.removeChild(dialogContent.firstChild); 804 } 805 content.forEach(el => { 806 dialogContent.appendChild(el); 807 }); 808 dialog.showModal(); 809 event.stopPropagation(); 810 }); 811 812 return helpIcon; 813 } 814 815 function createPRCardBody(pr, builds, queries) { 816 const cardBody = document.createElement("DIV"); 817 const title = document.createElement("H3"); 818 title.textContent = pr.Title; 819 820 cardBody.classList.add("mdl-card__supporting-text"); 821 cardBody.appendChild(title); 822 cardBody.appendChild(createJobStatus(builds)); 823 const nodes = pr.Labels && pr.Labels.Nodes ? pr.Labels.Nodes : []; 824 cardBody.appendChild(createMergeStatus(nodes, queries)); 825 826 return cardBody; 827 } 828 829 /** 830 * Compare function that prioritizes jobs which are in failure state. 831 * @param a 832 * @param b 833 * @return {number} 834 */ 835 function compareJobFn(a, b) { 836 const stateToPrio = []; 837 stateToPrio["success"] = stateToPrio["expected"] = 3; 838 stateToPrio["aborted"] = 2; 839 stateToPrio["pending"] = stateToPrio["triggered"] = 1; 840 stateToPrio["error"] = stateToPrio["failure"] = 0; 841 842 return stateToPrio[a.state] > stateToPrio[b.state] ? 1 843 : stateToPrio[a.state] < stateToPrio[b.state] ? -1 : 0; 844 } 845 846 /** 847 * Creates a PR card. 848 * @param {Object} pr 849 * @param {Array<Object>} builds 850 * @param {Array<Object>} queries 851 * @param {Array<Object>} tidePools 852 * @return {Element} 853 */ 854 function createPRCard(pr, builds = [], queries = [], tidePools = []) { 855 builds = builds ? builds : []; 856 queries = queries ? queries : []; 857 tidePools = tidePools ? tidePools : []; 858 const prCard = document.createElement("DIV"); 859 // jobs need to be sorted from high priority (failure, error) to low 860 // priority (success) 861 builds.sort(compareJobFn); 862 const prLabelsSet = new Set(); 863 if (pr.Labels && pr.Labels.Nodes) { 864 pr.Labels.Nodes.forEach(label => { 865 prLabelsSet.add(label.Label.Name); 866 }); 867 } 868 const processedQuery = []; 869 queries.forEach(query => { 870 let score = 0.0; 871 const labels = []; 872 const missingLabels = []; 873 query.labels.sort((a, b) => { 874 if (a.length === b.length) { 875 return 0; 876 } 877 return a.length < b.length ? -1 : 1; 878 }); 879 query.missingLabels.sort((a, b) => { 880 if (a.length === b.length) { 881 return 0; 882 } 883 return a.length < b.length ? -1 : 1; 884 }); 885 query.labels.forEach(label => { 886 labels.push({name: label, own: prLabelsSet.has(label)}); 887 score += labels[labels.length - 1].own ? 1 : 0; 888 }); 889 query.missingLabels.forEach(label => { 890 missingLabels.push({name: label, own: prLabelsSet.has(label)}); 891 score += missingLabels[missingLabels.length - 1].own ? 0 : 1; 892 }); 893 score = (labels.length + missingLabels.length > 0) ? score 894 / (labels.length + missingLabels.length) : 1.0; 895 processedQuery.push( 896 {score: score, labels: labels, missingLabels: missingLabels}); 897 }); 898 // Sort queries by descending score order. 899 processedQuery.sort((q1, q2) => { 900 if (Math.abs(q1.score - q2.score) < Number.EPSILON) { 901 return 0; 902 } 903 return q1.score > q2.score ? -1 : 1; 904 }); 905 prCard.classList.add("pr-card", "mdl-card"); 906 prCard.appendChild(createPRCardTitle(pr, tidePools, jobStatus(builds), isAbleToMerge(processedQuery))); 907 prCard.appendChild(createPRCardBody(pr, builds, processedQuery)); 908 return prCard; 909 } 910 911 /** 912 * Redirect to initiate github login flow. 913 */ 914 function forceGithubLogin() { 915 window.location.href = window.location.origin + "/github-login"; 916 } 917 918 /** 919 * Returns the job status based on its state. 920 * @param builds 921 * @return {string} 922 */ 923 function jobStatus(builds) { 924 if (builds.length === 0) { 925 return "unknown"; 926 } 927 switch (builds[0].state) { 928 case "success": 929 case "expected": 930 return "succeeded"; 931 case "failure": 932 case "error": 933 return "failed"; 934 default: 935 return "pending"; 936 } 937 } 938 939 /** 940 * Returns -1 if there is no query. 1 if the PR is able to be merged by checking 941 * the score of the first query in the query list (score === 1), the list has 942 * been sorted by scores, otherwise 0. 943 * @param queries 944 * @return {number} 945 */ 946 function isAbleToMerge(queries) { 947 if (queries.length === 0) { 948 return -1; 949 } 950 return Math.abs(queries[0].score - 1.0) < Number.EPSILON ? 1 : 0; 951 } 952 953 /** 954 * Returns an icon element. 955 * @param {string} iconString icon name 956 * @param {string} tooltip tooltip string 957 * @param {Array<string>} styles 958 * @param {boolean} isButton 959 * @return {Element} 960 */ 961 function createIcon(iconString, tooltip = "", styles = [], isButton = false) { 962 const icon = document.createElement("I"); 963 icon.classList.add("icon-button", "material-icons"); 964 icon.textContent = iconString; 965 if (tooltip !== "") { 966 icon.title = tooltip; 967 } 968 if (!isButton) { 969 icon.classList.add(...styles); 970 return icon; 971 } 972 const container = document.createElement("BUTTON"); 973 container.appendChild(icon); 974 container.classList.add("mdl-button", "mdl-js-button", "mdl-button--icon", 975 ...styles); 976 977 return container; 978 } 979 980 /** 981 * Create a simple message with an icon. 982 * @param msg 983 * @param icStr 984 * @return {HTMLElement} 985 */ 986 function createMessage(msg, icStr) { 987 const el = document.createElement("H3"); 988 el.textContent = msg; 989 if (icStr !== "") { 990 const ic = createIcon(icStr, "", ["message-icon"]); 991 el.appendChild((ic)); 992 } 993 const msgContainer = document.createElement("DIV"); 994 msgContainer.appendChild(el); 995 msgContainer.classList.add("message"); 996 997 return msgContainer; 998 } 999 1000 document.addEventListener("DOMContentLoaded", function(event) { 1001 configure(); 1002 }); 1003 1004 function configure() { 1005 if (!branding){ 1006 return; 1007 } 1008 if (branding.logo !== '') { 1009 document.getElementById('img').src = branding.logo; 1010 } 1011 if (branding.favicon !== '') { 1012 document.getElementById('favicon').href = branding.favicon; 1013 } 1014 if (branding.background_color !== '') { 1015 document.body.style.background = branding.background_color; 1016 } 1017 if (branding.header_color !== '') { 1018 document.getElementsByTagName('header')[0].style.backgroundColor = branding.header_color; 1019 } 1020 }