@percy/report 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/summary.js ADDED
@@ -0,0 +1,138 @@
1
+ const { Axios } = require('axios')
2
+ const { startOfDay, startOfWeek, endOfDay, endOfWeek, isAfter, isBefore,intervalToDuration } = require('date-fns')
3
+ const fs = require('fs')
4
+ const { HtmlSummary } = require('./html-render')
5
+ const defaultOptions = {
6
+ percyToken: process.env.PERCY_TOKEN,
7
+ apiUrl: 'https://percy.io/api/v1',
8
+ }
9
+ module.exports.Summary = async function (opts) {
10
+ let { percyToken, startDate, endDate, projectSlug, apiUrl } = Object.assign(defaultOptions, opts)
11
+ startDate = new Date(startDate)
12
+ endDate = new Date(endDate)
13
+
14
+ if(!startDate){
15
+ throw new Error("Start Date is required")
16
+ }
17
+ if(!endDate){
18
+ throw new Error("End Date is required")
19
+ }
20
+ if(intervalToDuration({start:startDate,end:endDate}).days > 30){
21
+ throw new Error("Interval between start & end date should be less than 30days")
22
+ }
23
+
24
+ let axios = new Axios({
25
+ baseURL: apiUrl,
26
+ headers: {
27
+ "Authorization": `Token ${percyToken}`
28
+ }
29
+ })
30
+
31
+ let project = await axios.get(`/projects?project_slug=${projectSlug}`, { responseType: 'json' }).then((res) => {
32
+ if (res.status == 200) {
33
+ return JSON.parse(res.data)
34
+ } else {
35
+ throw res.data
36
+ }
37
+ })
38
+ const isApp = project['data']['attributes']['type'] == 'app'
39
+ let projectId = project.data.id
40
+ let projectURL = "https://percy.io/"+project.data.attributes['full-slug']
41
+ let projectName = project.data.attributes.name
42
+ let browsers = project["included"].filter((i) => i['type'] == 'browser-families')?.map((v) => v['attributes'].name)
43
+ const buildSummary = async (cursor, _summary) => {
44
+ let done = false
45
+ let urlParams = {
46
+ project_id: projectId,
47
+ 'page[limit]': 100,
48
+ }
49
+ let summary = Object.assign({
50
+ totalBuilds: 0,
51
+ totalBuildsApproved: 0,
52
+ totalBuildsUnreviewed:0,
53
+ totalBuildsFailed:0,
54
+ totalBuildsRequestingChanges:0,
55
+ totalSnapshots: 0,
56
+ totalSnapshotsRequestingChanges: 0,
57
+ totalSnapshotsUnreviewed: 0,
58
+ totalSnapshotsReviewed: 0,
59
+ totalComparisons: 0,
60
+ projectURL: projectURL,
61
+ unreviewedBuilds:[],
62
+ failedBuilds:[]
63
+ },_summary)
64
+ if (cursor) {
65
+ urlParams['page[cursor]'] = cursor
66
+ }
67
+ let _builds = await axios.get('/builds', { params: urlParams, responseType: 'json' }).then((res) => {
68
+ if (res.status == 200) {
69
+ return JSON.parse(res.data)
70
+ } else {
71
+ throw res.data
72
+ }
73
+ })
74
+ for (let build of _builds.data) {
75
+ let createdAt = new Date(build.attributes["created-at"])
76
+ if (isBefore(createdAt, startDate)) {
77
+ done = true;
78
+ continue
79
+ }
80
+ if (isBefore(createdAt, endDate) && isAfter(createdAt, startDate)) {
81
+ summary['totalBuilds']++;
82
+ if (build['attributes']['review-state'] == 'approved') {
83
+ summary['totalBuildsApproved']++
84
+ }
85
+ if(build['attributes']['review-state'] == 'unreviewed'){
86
+ summary['totalBuildsUnreviewed'] ++
87
+ summary['unreviewedBuilds'].push({
88
+ timestamp:new Date(build.attributes["created-at"]),
89
+ buildUrl:build['attributes']['web-url'],
90
+ buildNo:build['attributes']['build-number']
91
+ })
92
+ }
93
+ if(build['attributes']['review-state'] == null){
94
+ summary['totalBuildsFailed'] ++;
95
+ summary['failedBuilds'].push({
96
+ timestamp:new Date(build.attributes["created-at"]),
97
+ buildUrl:build['attributes']['web-url'],
98
+ buildNo:build['attributes']['build-number']
99
+ })
100
+ }
101
+ if(build['attributes']['review-state'] == 'changes_requested'){
102
+ summary['totalBuildsRequestingChanges'] ++;
103
+ }
104
+ summary['totalSnapshots'] += build['attributes']['total-snapshots']
105
+ summary['totalSnapshotsRequestingChanges'] += build['attributes']['total-snapshots-requesting-changes']
106
+ summary['totalSnapshotsUnreviewed'] += build['attributes']['total-snapshots-unreviewed']
107
+ summary['totalSnapshotsReviewed'] += (build['attributes']['total-snapshots'] - build['attributes']['total-snapshots-unreviewed'])
108
+ summary['totalComparisons'] += build['attributes']['total-comparisons']
109
+ }
110
+ }
111
+ if (done) {
112
+ return summary
113
+ } else {
114
+ if (_builds.data.length > 0) {
115
+ return buildSummary(_builds.data[_builds.data.length - 1].id,summary)
116
+ } else {
117
+ return summary;
118
+ }
119
+ }
120
+ }
121
+
122
+ let summary = await buildSummary()
123
+ summary = {
124
+ ...summary,
125
+ browsers,
126
+ projectName,
127
+ startDate,
128
+ endDate
129
+ }
130
+ if(!fs.existsSync('Summary') ){
131
+ fs.mkdirSync('Summary',{recursive:true})
132
+ }
133
+ fs.writeFileSync(`Summary/${summary.projectName}-${Date.now()}.json`,JSON.stringify(summary,undefined,2))
134
+ HtmlSummary(summary,`Summary/${summary.projectName}-${Date.now()}.html`, isApp)
135
+ console.log("Summary report generated")
136
+ return summary;
137
+
138
+ }
@@ -0,0 +1,180 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
7
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
8
+ <title>Document</title>
9
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet"
10
+ integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
11
+ <style>
12
+ img {
13
+ width: 100%;
14
+ object-fit: contain;
15
+ border: 1px solid gray;
16
+ }
17
+
18
+ .comparison-container {
19
+ position: relative;
20
+ cursor: pointer;
21
+ }
22
+
23
+ .overlay {
24
+ position: absolute;
25
+ top: 0;
26
+ left: 0;
27
+ right: 0;
28
+ bottom: 0;
29
+ z-index: 50;
30
+ width: 100%;
31
+ height: 100%;
32
+ background-color: rgba(0, 0, 0, 0.7);
33
+ }
34
+
35
+ .color-red{
36
+ background-color: red !important;
37
+ color: white !important;
38
+ }
39
+ .color-green{
40
+ background-color: green !important;
41
+ color: white !important;
42
+ }
43
+ select{
44
+ max-width: 100%;
45
+ }
46
+ </style>
47
+ <script type="text/javascript">
48
+ function onClickOverlay(e, element) {
49
+ let hidden = e.currentTarget.querySelector(".overlay").classList.contains('d-none')
50
+ if(hidden){
51
+ e.currentTarget.querySelector(".overlay").classList.remove('d-none')
52
+ }else{
53
+ e.currentTarget.querySelector(".overlay").classList.add('d-none')
54
+ }
55
+ }
56
+ const snapshots = JSON.parse('<%- JSON.stringify(details.map(snp=>snp.name)) %>')
57
+ const widths = JSON.parse('<%- JSON.stringify(widths) %>')
58
+ const devices = JSON.parse('<%- JSON.stringify(devices) %>')
59
+ var snapshotSelect, widthSelect,deviceSelect,images
60
+ window.addEventListener('DOMContentLoaded', () => {
61
+ snapshotSelect = document.getElementById('select-snapshot')
62
+ deviceSelect = document.getElementById('select-device')
63
+ images = document.getElementsByTagName('img')
64
+ tables = document.getElementsByTagName('table')
65
+ snapshotSelect.addEventListener('change', ApplyFilter)
66
+ deviceSelect.addEventListener('change', ApplyFilter)
67
+ ApplyFilter()
68
+ })
69
+
70
+ function ApplyFilter() {
71
+ const shouldEnable = (dataset) => {
72
+ return (snapshotSelect.value == "All" || snapshotSelect.value == dataset.name) && deviceSelect.value == dataset.device
73
+ }
74
+ for (let i = 0; i < images.length; i++) {
75
+ let image = images.item(i)
76
+ image.hidden = !shouldEnable(image.dataset)
77
+ }
78
+
79
+ for(let i = 0; i < tables.length; i++){
80
+ let table = tables.item(i)
81
+ if(table.id !== "build-details"){
82
+ table.hidden = !shouldEnable(table.dataset)
83
+ }
84
+ }
85
+ }
86
+ </script>
87
+ </head>
88
+
89
+ <body class="p-5">
90
+ <div class="container">
91
+ <h1 class="my-3 text-center"> <u> <%= projectName %> </u></h1>
92
+ <h4 class="my-3 text-center"> <u> <a href="<%= buildURL %>">Go to Percy Dashboard Build</a> </u></h3>
93
+ <table class="table table-bordered table-striped" id="build-details">
94
+ <thead>
95
+ <th class="text-center">Build Number</th>
96
+ <th class="text-center">Device Count</th>
97
+ <th class="text-center">Total Snapshots</th>
98
+ <th class="text-center">Total Screenshots</th>
99
+ <th class="text-center">Snapshots Unreviewed</th>
100
+ <th class="text-center">Screenshots Unreviewed</th>
101
+ </thead>
102
+ <tbody>
103
+ <tr>
104
+ <td class="text-center">
105
+ <%= buildNumber %>
106
+ </td>
107
+ <td class="text-center"><%= devices.length %></td>
108
+ <td class="text-center">
109
+ <%= totalSnapshots %>
110
+ </td>
111
+ <td class="text-center">
112
+ <%= totalScreenshots %>
113
+ </td>
114
+ <td class="text-center">
115
+ <%= unreviewedSnapshots %>
116
+ </td>
117
+ <td class="text-center">
118
+ <%= unreviewedScreenshots %>
119
+ </td>
120
+ </tr>
121
+ </tbody>
122
+ </table>
123
+ <div class="row g-0 my-3">
124
+ <div class="col-md-6 text-center border p-2">
125
+ <label>Snapshot Name</label>
126
+ <select id="select-snapshot">
127
+ <option selected>All</option>
128
+ <% for(let snapshot of details){ %>
129
+ <option value="<%= snapshot['name'] %>">
130
+ <%= snapshot['name'] %>
131
+ </option>
132
+ <% } %>
133
+ </select>
134
+ </div>
135
+ <div class="col-md-6 text-center border p-2">
136
+ <label>Devices</label>
137
+ <select id="select-device">
138
+ <% for(let device of devices){ %>
139
+ <option value="<%= device %>">
140
+ <%= device %>
141
+ </option>
142
+ <% } %>
143
+ </select>
144
+ </div>
145
+ </div>
146
+ <hr>
147
+ <% for(let snapshot of details){ %>
148
+ <% for(let comparison of snapshot.comparisons){ %>
149
+ <div class="row gx-2">
150
+ <div class="col-md-12">
151
+ <table hidden data-type="diff-ratio" data-name="<%= snapshot.name %>" data-device="<%= comparison.device %>" class="m-3">
152
+ <tbody>
153
+ <tr>
154
+ <td>Diff Ratio:</td>
155
+ <td class="color-<%= comparison['diff-color'] %>" ><%= comparison['diff-percentage'] %></td>
156
+ </tr>
157
+ </tbody>
158
+ </table>
159
+ </div>
160
+ <div class="col-md-6 image-container">
161
+ <img hidden data-type="base" data-name="<%= snapshot.name %>" data-device="<%= comparison.device %>"
162
+ src="<%= comparison.images['base']?.file || comparison.images['base']?.url %>" alt="No Base Image Found">
163
+ </div>
164
+ <% if(!comparison.images['base'] || comparison.images['diff'] || comparison.images['head']){ %>
165
+ <div onclick="onClickOverlay(event,this)" class="col-md-6 image-container comparison-container">
166
+ <img hidden data-type="head" data-name="<%= snapshot.name %>" data-device="<%= comparison.device %>"
167
+ src="<%= comparison.images['head'].file || comparison.images['head']?.url %>" alt="No Head Image Found">
168
+ <% if (comparison.images['diff']) { %> <img hidden data-type="diff"
169
+ data-name="<%= snapshot.name %>"
170
+ data-device="<%= comparison.device %>" class="overlay"
171
+ src="<%= comparison.images['diff']?.file || comparison.images['diff']?.url %>" alt="">
172
+ <% } %>
173
+ </div>
174
+ <% } %>
175
+ </div>
176
+ <% }} %>
177
+ </div>
178
+ </body>
179
+
180
+ </html>
@@ -0,0 +1,165 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
7
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
8
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet"
9
+ integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
10
+ <title>Document</title>
11
+ </head>
12
+ <style>
13
+ .table {
14
+ max-width: 768px !important;
15
+ }
16
+
17
+ .logo {
18
+ display: block;
19
+ width: 150px;
20
+ height: auto;
21
+ margin: auto;
22
+ }
23
+ </style>
24
+
25
+ <body class="p-5">
26
+ <div class="container">
27
+ <svg class="logo my-5" width="116" height="32" fill="none" xmlns="http://www.w3.org/2000/svg">
28
+ <path fill-rule="evenodd" clip-rule="evenodd"
29
+ d="M109.723 26.385c-.066.211-.175.36-.324.443l-.043.021c-.162.066-.335.057-.539.007-.657-.162-3.706-.505-2.487-4.067l.723-2.112-.164-.41-.792-1.885-4.966-11.538a.614.614 0 0 1 .028-.606c.101-.155.279-.233.531-.238h2.825c.194 0 .359.052.485.15.127.098.219.248.298.432l3.645 8.528 2.615-8.504a.8.8 0 0 1 .289-.446c.131-.1.298-.16.493-.16h3.018c.256 0 .438.074.545.21.107.14.121.338.042.6l-4.058 12.734-1.565 4.942-.599 1.9zM97.368 14.95h3.085c.477 0 .757.303.589.773-.87 2.873-3.618 4.861-6.9 4.861-4.263 0-7.432-3.176-7.432-7.291C86.71 9.175 89.879 6 94.142 6c3.282 0 6.03 1.988 6.9 4.86.168.47-.112.774-.589.774h-3.085c-.365 0-.617-.165-.842-.469-.504-.69-1.374-1.077-2.384-1.077-1.823 0-3.225 1.298-3.225 3.204 0 1.905 1.402 3.203 3.225 3.203 1.01 0 1.88-.386 2.384-1.049.225-.332.449-.497.842-.497zm-15.11-8.202v.72c.35-.61 1.146-1.474 3.223-1.447.15.002.272.059.36.15.132.134.195.315.195.549v3.375c0 .23-.049.4-.15.515-.098.114-.243.172-.431.172a4.08 4.08 0 0 0-1.187.129c-.378.1-.721.271-1.022.505-.3.239-.54.559-.716.949-.178.396-.268.877-.271 1.45l-.02 3.6c-.02 3.76-2.932 3.19-3.612 3.151-.231-.013-.412-.062-.537-.185-.126-.124-.19-.301-.19-.53V6.747c0-.229.064-.406.19-.53s.305-.185.537-.185h2.905c.233 0 .412.061.538.185.126.124.189.3.189.53zm-12.586-.74c1.025.004 1.966.188 2.824.55a6.743 6.743 0 0 1 2.232 1.524 6.957 6.957 0 0 1 1.463 2.303c.345.881.522 1.845.527 2.891 0 .18-.005.354-.019.519-.01.17-.023.335-.037.5-.028.22-.112.38-.242.482-.13.101-.303.151-.513.151h-9.338c.177.542.424.982.75 1.322.326.335.708.578 1.136.734.434.156.895.23 1.385.23a3.658 3.658 0 0 0 1.141-.188c.36-.124.671-.29.928-.5.135-.11.27-.198.4-.262.136-.064.29-.097.466-.097l2.685-.027c.256.005.442.078.554.22.107.142.112.326.005.55a5.947 5.947 0 0 1-1.468 2.006 6.162 6.162 0 0 1-2.125 1.22 8.256 8.256 0 0 1-2.642.41c-1.165-.006-2.214-.194-3.146-.56-.936-.368-1.733-.877-2.395-1.538a6.74 6.74 0 0 1-1.528-2.303c-.354-.882-.532-1.836-.536-2.869.005-1.032.186-1.987.55-2.867a6.913 6.913 0 0 1 1.542-2.304 7 7 0 0 1 2.367-1.538c.914-.367 1.925-.555 3.034-.56zm-19.617 1.5a6.578 6.578 0 0 1 1.88-1.09c.7-.262 1.44-.398 2.19-.398 1.856 0 3.67.817 4.887 2.138 1.215 1.319 1.967 3.14 1.967 5.152 0 2.01-.752 3.832-1.967 5.151a6.746 6.746 0 0 1-4.888 2.138c-1.36 0-2.786-.42-3.78-1.36l.003 4.473c.003 3.76-2.932 3.19-3.612 3.151-.23-.013-.411-.062-.537-.186-.126-.123-.189-.3-.189-.53L46 6.767c0-.233.065-.417.194-.544.13-.127.315-.19.552-.19H49.3c.238 0 .423.063.552.19.13.127.191.31.194.544l.01.741zm7.255 5.763c0-1.774-1.45-3.211-3.24-3.211-1.789 0-3.24 1.438-3.24 3.211 0 1.774 1.451 3.212 3.24 3.212 1.79 0 3.24-1.438 3.24-3.212zm10.363-3.055c-.527.357-.9.885-1.118 1.574h5.987c-.149-.519-.372-.937-.67-1.248a2.618 2.618 0 0 0-1.026-.67 3.446 3.446 0 0 0-1.202-.202c-.788.004-1.45.183-1.97.546z"
30
+ fill="#333"></path>
31
+ <path fill-rule="evenodd" clip-rule="evenodd"
32
+ d="M39.482 7.275c-.23-.758-.84-1.503-1.565-1.2A5.688 5.688 0 0 1 36 6.5c-1.429-1.341-5.631-4-8.398-5.505 0 0 .338.987.804 2.831 0 0-2.978-2.326-6.348-3.826 0 0 .948 1.946 1.026 2.576 0 0-3.092-1.076-7.43-1.484 0 0 2.084 1.453 2.892 2.564 0 0-3.066-.156-7.645 0 0 0 2.603 1.055 3.752 2.025 0 0-5.434.552-8.504 1.435 0 0 3.46 1.22 4.42 2.127 0 0-4.412 1.257-8.86 3.825 0 0 2.471.432 4.405 1.277 0 0-2.463 1.722-6.114 6.42 0 0 2.698-.515 4.549-.37 0 0-2.345 2.74-3.569 6.93 0 0 1.357-.864 2.67-1.177 0 0 .142 4.998 2.875 5.798l.002-.003c.134.042.261.057.372.057.134 0 .273-.02.415-.061.867-.248 1.523-1.104 2.283-2.095.249-.324.505-.657.776-.981a6.595 6.595 0 0 1 1.176-1.27c.966-.797 2.303-1.4 3.931-1.159 1.834.155 2.966 1.982 3.877 3.45.473.764 1.1 2.116 2.149 2.116 1.16 0 1.567-1.39 2.084-3.104a16.946 16.946 0 0 1 2.149-4.553c1.81-2.693 4.124-4.157 6.9-5.6 2.659-1.383 5.172-2.69 6.432-4.672.63-.991.943-2.207.928-3.615-.012-1.262-.288-2.427-.517-3.18zM26.352 6.5l-4.294-2 6.348 1.575-2.055.425zm-9.758.616l5.305 1.357 2.314-.768-7.62-.59zm10.46 2.66l1.132-.858L23.084 10l3.97-.224zm-8.508 0l-1.815 1.216-5.03-.479 6.845-.737zm5.667 3.039l1.841-1.03-6.844.737 5.003.293zm-5.07 1.143l-1.283 2.109-6.422 1.57 7.705-3.679zm-8.023 1.298L6.15 17.445l6.102-4.377-1.13 2.188zM9.401 17.88l-.196 2.19L6 23l3.401-5.12z"
33
+ fill="#333"></path>
34
+ <path
35
+ d="M26.332 31.457c-.558-.009-1.15-.486-1.8-1.454.383-2.192 1.897-4.991 3.718-6.869-.465 1.744-.527 3.495-.576 4.901v.002c-.032.91-.06 1.695-.19 2.267-.174.765-.555 1.154-1.133 1.154h-.02z"
36
+ fill="#333"></path>
37
+ <path
38
+ d="M10.901 29.875c.473 1.068.978 1.588 1.542 1.588a.893.893 0 0 0 .137-.01c.766-.121 1.476-1.606 2.07-3.043.154-.373.298-.745.427-1.09-1.696.02-3.345 1.384-4.176 2.555z"
39
+ fill="#333"></path>
40
+ </svg>
41
+ <table class="table table-bordered m-auto">
42
+ <thead>
43
+ <tr>
44
+ <th>Summary Report</th>
45
+ <th> <%=new Date(startDate).toDateString()%> - <%=new Date(endDate).toDateString()%> </th>
46
+ </tr>
47
+ </thead>
48
+ <tbody>
49
+ <tr>
50
+ <td>Project Name</td>
51
+ <td> <a href="<%=projectURL%>"> <%=projectName%> </a></td>
52
+ </tr>
53
+ <tr>
54
+ <td> Total Builds </td>
55
+ <td> <%=totalBuilds%> </td>
56
+ </tr>
57
+ <tr>
58
+ <td> Total Builds Approved </td>
59
+ <td> <%=totalBuildsApproved%> </td>
60
+ </tr>
61
+ <tr>
62
+ <td> Total Builds Unreviewed </td>
63
+ <td>
64
+ <% if(totalBuildsUnreviewed) { %>
65
+ <a href="#unreviewedBuilds">
66
+ <% } %>
67
+ <%=totalBuildsUnreviewed%>
68
+ </a>
69
+ </td>
70
+ </tr>
71
+ <tr>
72
+ <td> Total Builds Failed </td>
73
+ <td>
74
+ <% if(totalBuildsFailed) { %>
75
+ <a href="#failedBuilds">
76
+ <% } %>
77
+ <%=totalBuildsFailed%>
78
+ </a>
79
+ </td>
80
+ </tr>
81
+ <tr>
82
+ <td> Total Builds Requesting Changes</td>
83
+ <td> <%=totalBuildsRequestingChanges%> </td>
84
+ </tr>
85
+ <tr>
86
+ <td> Total Snapshots</td>
87
+ <td> <%=totalSnapshots%> </td>
88
+ </tr>
89
+ <tr>
90
+ <td> Total Snapshots Requesting Changes</td>
91
+ <td> <%=totalSnapshotsRequestingChanges%> </td>
92
+ </tr>
93
+ <tr>
94
+ <td> Total Snapshots Unreviewed</td>
95
+ <td> <%=totalSnapshotsUnreviewed%> </td>
96
+ </tr>
97
+ <tr>
98
+ <td> Total Snapshots Reviewed</td>
99
+ <td> <%=totalSnapshotsReviewed%> </td>
100
+ </tr>
101
+
102
+ <tr>
103
+ <td> Total Comparisons</td>
104
+ <td> <%=totalComparisons%> </td>
105
+ </tr>
106
+ </tbody>
107
+ </table>
108
+ <% if(totalBuildsUnreviewed) { %>
109
+ <section id="unreviewedBuilds">
110
+ <table class="table table-bordered mx-auto mt-5">
111
+ <thead>
112
+ <tr>
113
+ <th colspan="2">Unreviewed Builds</th>
114
+ </tr>
115
+ <tr>
116
+ <th>Build ID</th>
117
+ <th>Build Created At</th>
118
+ </tr>
119
+ </thead>
120
+ <tbody>
121
+ <% for(let unreviewedBuild of unreviewedBuilds){ %>
122
+ <tr>
123
+ <td>
124
+ <a href="<%= unreviewedBuild['buildUrl'] %>">Build <%= unreviewedBuild['buildNo'] %></a>
125
+ </td>
126
+ <td>
127
+ <%= new Date(unreviewedBuild['timestamp']) %>
128
+ </td>
129
+ </tr>
130
+ <% } %>
131
+ </tbody>
132
+ </table>
133
+ </section>
134
+ <% } %>
135
+ <% if(totalBuildsFailed) { %>
136
+ <section id="failedBuilds">
137
+ <table class="table table-bordered mx-auto mt-5">
138
+ <thead>
139
+ <tr>
140
+ <th colspan="2">Failed Builds</th>
141
+ </tr>
142
+ <tr>
143
+ <th>Build ID</th>
144
+ <th>Build Created At</th>
145
+ </tr>
146
+ </thead>
147
+ <tbody>
148
+ <% for(let failedBuild of failedBuilds){ %>
149
+ <tr>
150
+ <td>
151
+ <a href="<%= failedBuild['buildUrl'] %>">Build <%= failedBuild['buildNo'] %></a>
152
+ </td>
153
+ <td>
154
+ <%= new Date(failedBuild['timestamp']) %>
155
+ </td>
156
+ </tr>
157
+ <% } %>
158
+ </tbody>
159
+ </table>
160
+ </section>
161
+ <% } %>
162
+ </div>
163
+ </body>
164
+
165
+ </html>