|
| 1 | +package utils |
| 2 | + |
| 3 | +type HTMLTemplateData struct { |
| 4 | + FeatureGates []HTMLFeatureGate |
| 5 | +} |
| 6 | + |
| 7 | +type HTMLFeatureGate struct { |
| 8 | + Name string |
| 9 | + Sufficient bool |
| 10 | + Variants []HTMLVariantColumn |
| 11 | + Tests []HTMLTestRow |
| 12 | +} |
| 13 | + |
| 14 | +type HTMLVariantColumn struct { |
| 15 | + Topology string |
| 16 | + Cloud string |
| 17 | + Architecture string |
| 18 | + NetworkStack string |
| 19 | + ColIndex int |
| 20 | +} |
| 21 | + |
| 22 | +type HTMLTestRow struct { |
| 23 | + TestName string |
| 24 | + Cells []HTMLTestCell |
| 25 | +} |
| 26 | + |
| 27 | +type HTMLTestCell struct { |
| 28 | + PassPercent int |
| 29 | + SuccessfulRuns int |
| 30 | + TotalRuns int |
| 31 | + FailedRuns int |
| 32 | + Failed bool |
| 33 | +} |
| 34 | + |
| 35 | +const HTMLTemplateSrc = `<!DOCTYPE html> |
| 36 | +<html> |
| 37 | +<head> |
| 38 | + <meta charset="UTF-8"> |
| 39 | + <title>FeatureGate Promotion Summary</title> |
| 40 | + <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/4.6.1/css/bootstrap.min.css" |
| 41 | + integrity="sha512-T584yQ/tdRR5QwOpfvDfVQUidzfgc2339Lc8uBDtcp/wYu80d7jwBgAxbyMh0a9YM9F8N3tdErpFI8iaGx6x5g==" |
| 42 | + crossorigin="anonymous"> |
| 43 | + <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> |
| 44 | + <style> |
| 45 | + body { padding: 20px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; } |
| 46 | + .sortable { cursor: pointer; user-select: none; } |
| 47 | + .sortable:hover { background-color: #e9ecef; } |
| 48 | + .sort-indicator::after { content: ' \2195'; opacity: 0.3; font-size: 0.8em; } |
| 49 | + .sort-asc::after { content: ' \2191'; opacity: 1; font-size: 0.8em; } |
| 50 | + .sort-desc::after { content: ' \2193'; opacity: 1; font-size: 0.8em; } |
| 51 | + .fail-cell { background-color: #f8d7da; } |
| 52 | + .pass-cell { background-color: #d4edda; } |
| 53 | + .test-name { max-width: 500px; word-wrap: break-word; font-size: 0.85em; text-align: left; } |
| 54 | + table { width: 100%; border-collapse: collapse; margin-bottom: 2em; } |
| 55 | + th, td { border: 1px solid #dee2e6; padding: 8px; text-align: center; vertical-align: middle; } |
| 56 | + th:first-child, td:first-child { text-align: left; } |
| 57 | + th { background-color: #f8f9fa; } |
| 58 | + .network-stack { font-weight: bold; color: #0056b3; } |
| 59 | + .alert { padding: 12px 20px; margin-bottom: 20px; border-radius: 4px; } |
| 60 | + .alert-success { background-color: #d4edda; border: 1px solid #c3e6cb; color: #155724; } |
| 61 | + .alert-danger { background-color: #f8d7da; border: 1px solid #f5c6cb; color: #721c24; } |
| 62 | + h1 { margin-bottom: 24px; } |
| 63 | + h2 { margin-top: 32px; margin-bottom: 12px; } |
| 64 | + </style> |
| 65 | +</head> |
| 66 | +<body> |
| 67 | +<div class="container-fluid"> |
| 68 | + <h1>FeatureGate Promotion Summary</h1> |
| 69 | + {{if not .FeatureGates}}<p>No new Default FeatureGates found.</p>{{end}} |
| 70 | + {{range $fgIdx, $fg := .FeatureGates}} |
| 71 | + <h2>{{$fg.Name}}</h2> |
| 72 | + {{if $fg.Sufficient}} |
| 73 | + <div class="alert alert-success">Sufficient CI testing for "{{$fg.Name}}".</div> |
| 74 | + {{else}} |
| 75 | + <div class="alert alert-danger"> |
| 76 | + <strong>INSUFFICIENT</strong> CI testing for "{{$fg.Name}}". |
| 77 | + <ul> |
| 78 | + <li>At least five tests are expected for a feature</li> |
| 79 | + <li>Tests must be run on every TechPreview platform (ask for an exception if your feature doesn't support a variant)</li> |
| 80 | + <li>All tests must run at least 14 times on every platform</li> |
| 81 | + <li>All tests must pass at least 95% of the time</li> |
| 82 | + </ul> |
| 83 | + </div> |
| 84 | + {{end}} |
| 85 | + {{if $fg.Tests}} |
| 86 | + <table id="table-{{$fgIdx}}"> |
| 87 | + <thead> |
| 88 | + <tr> |
| 89 | + <th class="sortable sort-indicator" data-table="{{$fgIdx}}" data-col="0" data-sort="text">Test Name</th> |
| 90 | + {{range $v := $fg.Variants}} |
| 91 | + <th class="sortable sort-indicator" data-table="{{$fgIdx}}" data-col="{{$v.ColIndex}}" data-sort="percent"> |
| 92 | + {{$v.Topology}}<br>{{$v.Cloud}}<br>{{$v.Architecture}}{{if $v.NetworkStack}}<br><span class="network-stack">{{$v.NetworkStack}}</span>{{end}} |
| 93 | + </th> |
| 94 | + {{end}} |
| 95 | + </tr> |
| 96 | + </thead> |
| 97 | + <tbody> |
| 98 | + {{range $test := $fg.Tests}} |
| 99 | + <tr> |
| 100 | + <td class="test-name">{{$test.TestName}}</td> |
| 101 | + {{range $cell := $test.Cells}} |
| 102 | + <td class="{{if $cell.Failed}}fail-cell{{else}}pass-cell{{end}}" data-pass-percent="{{$cell.PassPercent}}"> |
| 103 | + {{if $cell.Failed}}<strong>FAIL</strong><br>{{end}} |
| 104 | + {{$cell.PassPercent}}% ({{$cell.SuccessfulRuns}} / {{$cell.TotalRuns}}) |
| 105 | + {{if gt $cell.FailedRuns 0}}<br><small>{{$cell.FailedRuns}} failed</small>{{end}} |
| 106 | + </td> |
| 107 | + {{end}} |
| 108 | + </tr> |
| 109 | + {{end}} |
| 110 | + </tbody> |
| 111 | + </table> |
| 112 | + {{end}} |
| 113 | + {{end}} |
| 114 | +</div> |
| 115 | +<script> |
| 116 | +document.addEventListener('DOMContentLoaded', function() { |
| 117 | + document.querySelectorAll('th.sortable').forEach(function(th) { |
| 118 | + th.addEventListener('click', function() { |
| 119 | + sortTable(this.dataset.table, parseInt(this.dataset.col), this.dataset.sort, this); |
| 120 | + }); |
| 121 | + }); |
| 122 | +}); |
| 123 | +
|
| 124 | +var sortStates = {}; |
| 125 | +
|
| 126 | +function sortTable(tableIdx, colIdx, sortType, header) { |
| 127 | + var table = document.getElementById('table-' + tableIdx); |
| 128 | + if (!table) return; |
| 129 | + var tbody = table.querySelector('tbody'); |
| 130 | + var rows = Array.from(tbody.querySelectorAll('tr')); |
| 131 | + var headers = table.querySelectorAll('th'); |
| 132 | +
|
| 133 | + var key = tableIdx + '-' + colIdx; |
| 134 | + if (sortStates[key] === 'asc') { |
| 135 | + sortStates[key] = 'desc'; |
| 136 | + } else { |
| 137 | + sortStates[key] = 'asc'; |
| 138 | + } |
| 139 | + var ascending = sortStates[key] === 'asc'; |
| 140 | +
|
| 141 | + headers.forEach(function(h) { |
| 142 | + h.classList.remove('sort-asc', 'sort-desc'); |
| 143 | + h.classList.add('sort-indicator'); |
| 144 | + }); |
| 145 | + header.classList.remove('sort-indicator'); |
| 146 | + header.classList.add(ascending ? 'sort-asc' : 'sort-desc'); |
| 147 | +
|
| 148 | + rows.sort(function(a, b) { |
| 149 | + var valA, valB; |
| 150 | + if (sortType === 'text') { |
| 151 | + valA = a.cells[colIdx].textContent.trim().toLowerCase(); |
| 152 | + valB = b.cells[colIdx].textContent.trim().toLowerCase(); |
| 153 | + return ascending ? valA.localeCompare(valB) : valB.localeCompare(valA); |
| 154 | + } else { |
| 155 | + valA = parseInt(a.cells[colIdx].getAttribute('data-pass-percent') || '0', 10); |
| 156 | + valB = parseInt(b.cells[colIdx].getAttribute('data-pass-percent') || '0', 10); |
| 157 | + return ascending ? valA - valB : valB - valA; |
| 158 | + } |
| 159 | + }); |
| 160 | +
|
| 161 | + rows.forEach(function(row) { tbody.appendChild(row); }); |
| 162 | +} |
| 163 | +</script> |
| 164 | +</body> |
| 165 | +</html> |
| 166 | +` |
0 commit comments