@@ -13,68 +13,6 @@ import re
import subprocess
from jinja2 import Template
-index_template = """
-<!DOCTYPE html>
-<html>
-<head>
- <meta charset="utf-8">
- <meta name="viewport" content="width=device-width, initial-scale=1">
- <title>Index of autobuilder test results</title>
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.1/css/bulma.min.css">
-</head>
-<body>
-
-<table class="table is-narrow is-striped">
-<thead>
-<tr>
- <th>Build</th>
- <th>Type</th>
- <th>Branch</th>
- <th>Test Results Report</th>
- <th>Performance Reports</th>
- <th>ptest Logs</th>
- <th>Buildhistory</th>
- <th>Host Data</th>
-</tr>
-</thead>
-<tdata>
-{% for entry in entries %}
-<tr>
- <td><a href="{{entry[1]}}">{{entry[0]}}</a></td>
- <td>{% if entry[2] %} {{entry[2]}}{% endif %}</td>
- <td>{% if entry[4] %} {{entry[4]}}{% endif %}</td>
- <td>
- {% if entry[3] %}<a href="{{entry[3]}}">Report</a>{% endif -%}
- {% if entry[9] %}<br><a href="{{entry[9]}}">Regressions</a>{% endif %}
- </td>
- <td>
- {% for perfrep in entry[6] %}
- <a href="{{perfrep[0]}}">{{perfrep[1]}}</a>
- {% endfor %}
- </td>
- <td>
- {% for ptest in entry[7] %}
- <a href="{{ptest[0]}}">{{ptest[1]}}</a>
- {% endfor %}
- </td>
- <td>
- {% for bh in entry[5] %}
- <a href="{{bh[0]}}">{{bh[1]}}</a>
- {% endfor %}
- </td>
- <td>
- {% for hd in entry[8] %}
- <a href="{{hd[0]}}">{{hd[1]}}</a>
- {% endfor %}
- </td>
-</tr>
-{% endfor %}
-</tdata>
-</table>
-</body>
-</html>
-"""
-
def parse_args(argv=None):
"""Parse command line arguments"""
parser = argparse.ArgumentParser(
@@ -88,6 +26,9 @@ def parse_args(argv=None):
args = parse_args()
path = os.path.abspath(args.path)
entries = []
+filter_items = dict()
+build_types = set()
+branch_list= set()
def get_build_branch(p):
for root, dirs, files in os.walk(p):
@@ -175,7 +116,34 @@ for build in sorted(os.listdir(path), key=keygen, reverse=True):
branch = get_build_branch(buildpath)
- entries.append((build, reldir, btype, testreport, branch, buildhistory, perfreports, ptestlogs, hd, regressionreport))
+ build_types.add(btype)
+ if branch: branch_list.add(branch)
+ # Creates a dictionary of items to be filtered for build types and branch
+ filter_items["build_types"] = build_types
+ filter_items["branch_list"] = branch_list
+
+ entry = {
+ 'build': build,
+ 'btype': btype,
+ 'reldir': reldir
+ }
+
+ if testreport:
+ entry['testreport'] = testreport
+ if branch:
+ entry['branch'] = branch
+ if buildhistory:
+ entry['buildhistory'] = buildhistory
+ if perfreports:
+ entry['perfreports'] = perfreports
+ if ptestlogs:
+ entry['ptestlogs'] = ptestlogs
+ if hd:
+ entry['hd'] = hd
+ if regressionreport:
+ entry['regressionreport'] = regressionreport
+
+ entries.append(entry)
# Also ensure we have saved out log data for ptest runs to aid debugging
if "ptest" in btype or btype in ["full", "quick"]:
@@ -191,6 +159,11 @@ for build in sorted(os.listdir(path), key=keygen, reverse=True):
with open(f + "/resulttool-done.log", "a+") as tf:
tf.write("\n")
-t = Template(index_template)
+with open("./index-table.html") as file_:
+ t = Template(file_.read())
+
+with open(os.path.join(path, "data.json"), 'w') as f:
+ json.dump(entries, f)
+
with open(os.path.join(path, "index.html"), 'w') as f:
- f.write(t.render(entries = entries))
+ f.write(t.render(entries = entries, filter_items = filter_items))
new file mode 100644
@@ -0,0 +1,401 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+ <title>Index of autobuilder test results</title>
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css" />
+ <style>
+ :root {
+ font-size: 16px;
+ }
+ .no-wrap {
+ white-space: nowrap;
+ }
+ .type-select {
+ align-items: center;
+ background-color: white;
+ display: flex;
+ position: sticky;
+ top: 0px;
+ }
+ .type-select label {
+ margin-right: 20px;
+ flex-shrink: 0;
+ }
+ .type-select select {
+ max-width: 300px;
+ }
+ table tr:nth-child(even) td {
+ background-color: rgba(111, 120, 135, 0.0375);
+ }
+ table tr td:not(:last-child) {
+ border-right: 0.0625rem solid #e7eaf0;
+ }
+ a {
+ cursor: pointer;
+ }
+ .hide {
+ visibility: hidden;
+ }
+ </style>
+ </head>
+
+ <body onload="mount()">
+ <header class="container">
+ <h1>Index of autobuilder test results</h1>
+ <p>The table below lists the test results of all the builds and branch from the yocto autobuilder repository.</p>
+ <p>The filters for showing build types and branch helps to navigate the table.</p>
+ </header>
+ <main class="container">
+ <nav class="type-select">
+ <ul>
+ <li>
+ <label for="build-type-select">Filter by type:</label>
+ <select id="build-type-select" class="hide" onchange="setFilterParams()">
+ <option value="all">Type (all)</option>
+ {% for type in filter_items.build_types %}
+ <option value="{{type}}">{{type}}</option>
+ {% endfor %}
+ </select>
+ </li>
+ <li>
+ <label for="branch-select">Filter by branch:</label>
+ <select id="branch-select" class="hide" onchange="setFilterParams()">
+ <option value="all">Branch (all)</option>
+ {% for branch in filter_items.branch_list %}
+ <option value="{{branch}}">{{branch}}</option>
+ {% endfor %}
+ </select>
+ </li>
+ <li>
+ <label for="date-filter">Filter by date:</label>
+ <input id="date-filter" type="date" name="date" aria-label="Date" onchange="setFilterParams()" />
+ </li>
+ </ul>
+ <ul class="pagination-nav"></ul>
+ </nav>
+
+ <table class="test-table hide">
+ <thead>
+ <tr>
+ <th>Build</th>
+ <th>Type</th>
+ <th>Branch</th>
+ <th>Test Results Report</th>
+ <th>Performance Reports</th>
+ <th>ptest Logs</th>
+ <th>Buildhistory</th>
+ <th>Host Data</th>
+ </tr>
+ </thead>
+ <tbody></tbody>
+ </table>
+
+ <p id="error-message"></p>
+ </main>
+</body>
+</html>
+
+<script type="text/javascript">
+ let entries = []
+ let rowsPerPage = 50
+ let currentPage = 0
+ const pageWidth = 6
+ const tbody = document.getElementsByTagName('tbody')[0]
+ let params
+
+ async function loadEntries() {
+ try {
+ const response = await fetch('data.json')
+ const data = await response.json()
+ if (areFiltersSet()) {
+ entries = setFilteredData(data)
+ } else {
+ entries = data
+ }
+ } catch (error) {
+ console.error('Error loading entries', error)
+ }
+ }
+
+ function setFilteredData(data) {
+ const { type, branch, date } = getParams()
+
+ return data.filter(entry => {
+ return (entry.btype === type || !type) && (entry.branch === branch || !branch) && (!date || areDatesEqual(entry.build, date))
+ })
+ }
+
+ function getParams() {
+ let params = new URL(document.location.toString()).searchParams
+ let page = params.get('page')
+ let type = params.get('type')
+ let branch = params.get('branch')
+ let date = params.get('date')
+ if (!page) {
+ page = 0
+ }
+ currentPage = parseInt(page)
+ return { page, type, branch, date }
+ }
+
+ function areFiltersSet() {
+ const { type, branch, date } = getParams()
+ if (!type && !branch && !date) {
+ return false
+ }
+ return true
+ }
+
+ async function setFilterParams() {
+ const build_type_value = document.getElementById('build-type-select').value
+ const branch_value = document.getElementById('branch-select').value
+ const date_value = document.getElementById('date-filter').value
+
+ if (build_type_value) {
+ if (build_type_value === 'all') {
+ params.delete('type')
+ } else {
+ params.set('type', build_type_value)
+ }
+ }
+ if (branch_value) {
+ if (branch_value === 'all') {
+ params.delete('branch')
+ } else {
+ params.set('branch', branch_value)
+ }
+ }
+ if (date_value) {
+ params.set('date', date_value)
+ } else {
+ params.delete('date')
+ }
+
+ window.location.search = params
+ }
+
+ function areDatesEqual(build, inputDate) {
+ if (!inputDate) {
+ return false
+ }
+ const buildDate = build.substring(0, build.indexOf('-'))
+ const date = inputDate.replaceAll('-', '')
+
+ if (buildDate === date) {
+ return true
+ }
+
+ return false
+ }
+
+ function displayTable() {
+ // delete current rows
+ let tbody = document.getElementsByTagName('tbody')[0]
+ while (tbody.rows.length > 0) {
+ tbody.deleteRow(0)
+ }
+
+ if (entries.length === 0) {
+ const p = document.getElementById('error-message')
+ p.innerHTML = 'No entries available'
+
+ const paginationNav = document.querySelector('.pagination-nav')
+ paginationNav.classList.add('hide')
+ }
+
+ const start = currentPage * rowsPerPage
+ let end = (currentPage + 1) * rowsPerPage
+ end = end <= entries.length ? end : entries.length
+
+ for (var i = start; i < end; i++) {
+ insertRow(tbody, entries[i])
+ }
+ }
+
+ function insertRow(tbody, entry, index) {
+ const row = tbody.insertRow()
+
+ let html = '<tr>'
+ html += `<td><a class="no-wrap" href="${entry['reldir']}">${entry['build']}</a></td>`
+ html += '</tr>'
+ html += `<td class="no-wrap">`
+ if (entry['btype']) {
+ html += `${entry['btype']}`
+ }
+ html += `<td class="no-wrap">`
+ if (entry['branch']) {
+ html += `${entry['branch']}`
+ }
+ html += `</td>`
+ html += `<td>`
+ if (entry['testreport']) {
+ html += `<a href="${entry['testreport']}">Report</a>`
+ }
+ if (entry['regressionreport']) {
+ html += `<br><a href="${entry['regressionreport']}">Regressions</a>`
+ }
+ html += `</td>`
+
+ html += `<td>`
+ if (entry['perfreports']) {
+ entry['perfreports'].forEach((r) => {
+ html += `<a href="${r[0]}">${r[1]}</a> `
+ })
+ }
+ html += `</td>`
+
+ html += `<td>`
+ if (entry['ptestlogs']) {
+ entry['ptestlogs'].forEach((r) => {
+ html += `<a href="${r[0]}">${r[1]}</a> `
+ })
+ }
+ html += `</td>`
+
+ html += `<td>`
+ if (entry['buildhistory']){
+ entry['buildhistory'].forEach((bh) => {
+ html += `<a href="${bh[0]}">${bh[1]}</a> `
+ })
+ }
+ html += `</td>`
+
+ html += `<td>`
+ if(entry['hd']) {
+ entry['hd'].forEach((hd) => {
+ html += `<small><a href="${hd[0]}">${hd[1]}</a></small> `
+ })
+ }
+ html += `</td>`
+
+ html += `</tr>`
+ row.innerHTML = html
+ }
+
+ function updateActiveButtonStates() {
+ const pageButtons = document.querySelectorAll('.pagination-nav button')
+ pageButtons.forEach(button => {
+ if (button.textContent == currentPage+1) {
+ button.classList.remove('outline')
+ } else {
+ button.classList.add('outline')
+ }
+ })
+ }
+
+ function setCurrentPage(page) {
+ params.set('page', page)
+ setFilterParams()
+ }
+
+ function generatePageItems(totalPages, current, pageWidth) {
+ // See https://gist.github.com/kottenator/9d936eb3e4e3c3e02598
+ if (totalPages < pageWidth) {
+ return [...new Array(totalPages).keys()];
+ }
+ const left = Math.max(0, Math.min(totalPages - pageWidth, current - Math.floor(pageWidth / 2)));
+ const items = new Array(pageWidth);
+ for (let i = 0; i < pageWidth; i += 1) {
+ items[i] = i + left;
+ }
+ // replace non-ending items with placeholders
+ if (items[0] > 0) {
+ items[0] = 0;
+ items[1] = 'prev-more';
+ }
+ if (items[items.length - 1] < totalPages - 1) {
+ items[items.length - 1] = totalPages - 1;
+ items[items.length - 2] = 'next-more';
+ }
+ return items;
+ }
+
+ function createPageButtons() {
+ const totalPages = Math.ceil(entries.length / rowsPerPage)
+ const paginationNav = document.querySelector('.pagination-nav')
+
+ // Add page navigation buttons
+ const previousLi = document.createElement('li')
+ const previousBtn = document.createElement('a')
+ previousBtn.textContent = '<'
+ previousLi.appendChild(previousBtn)
+ paginationNav.appendChild(previousLi)
+ previousBtn.addEventListener('click', () => {
+ if (currentPage > 0) {
+ setCurrentPage(currentPage - 1)
+ }
+ })
+
+ const nextLi = document.createElement('li')
+ const nextBtn = document.createElement('a')
+ nextBtn.textContent = '>'
+ nextLi.appendChild(nextBtn)
+ nextBtn.addEventListener('click', () => {
+ if (currentPage < totalPages - 1) {
+ setCurrentPage(currentPage + 1)
+ }
+ })
+
+ const pageItems = generatePageItems(totalPages, currentPage, pageWidth)
+
+ // Add page buttons
+ pageItems.forEach(item => {
+ const pageLi = document.createElement('li')
+ if(typeof item === 'number') {
+ const pageButton = document.createElement('button')
+ pageButton.classList.add('outline')
+ pageButton.textContent = item + 1
+ pageButton.addEventListener('click', () => {
+ currentPage = item
+ setCurrentPage(currentPage)
+ })
+ pageLi.appendChild(pageButton)
+ } else {
+ const pageEllipsis = document.createElement('span')
+ pageEllipsis.textContent = '...'
+ pageLi.appendChild(pageEllipsis)
+ }
+ paginationNav.appendChild(pageLi)
+ })
+ paginationNav.appendChild(nextLi)
+ }
+
+ function showPage() {
+ createPageButtons()
+ updateActiveButtonStates()
+ displayTable()
+ }
+
+ async function mount() {
+ const url = window.location.href
+ params = new URLSearchParams(url.search)
+
+ await loadEntries()
+
+ // Call this function to create the page buttons initially
+ showPage(currentPage)
+
+ const hiddenItem = document.querySelector('.test-table')
+ hiddenItem.classList.remove('hide')
+
+ // Set the select value in the dropdown
+ let { type, branch, date } = getParams()
+
+ const buildType = document.getElementById('build-type-select')
+ const branchSelect = document.getElementById('branch-select')
+ const dateFilter = document.getElementById('date-filter')
+ if (type) {
+ buildType.value = type
+ }
+ if (branch) {
+ branchSelect.value = branch
+ }
+ if (date) {
+ dateFilter.value = date
+ }
+ buildType.classList.remove('hide')
+ branchSelect.classList.remove('hide')
+ }
+</script>
From: Alba HerrerĂas <albaherreriasdev@gmail.com> - Added pico.css to make CSS improvements - Added filters to the table for better readability of the test results. Filters were added for build type, branch and date - Added pagination, which improved the performance of the website - The html index template has been added as a separate file for better maintainability Co-Authored-By: Ninette Adhikari <13760198+ninetteadhikari@users.noreply.github.com> --- scripts/generate-testresult-index.py | 103 +++---- scripts/index-table.html | 401 +++++++++++++++++++++++++++ 2 files changed, 439 insertions(+), 65 deletions(-) create mode 100644 scripts/index-table.html