Skip to content

petri/logview: add screenshot rendering #1415

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
May 28, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
205 changes: 176 additions & 29 deletions petri/logview/test.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,30 +10,50 @@
font-size: 14px;
}

#logContainer {
box-sizing: border-box;
}

#logContainer table {
width: 100%;
border-collapse: collapse;
overflow: clip;
word-break: break-word;
}

#logContainer th,
#logContainer td {
border: 1px solid #ddd;
padding: 6px 10px;
text-align: left;
white-space: nowrap;
}

#logContainer col.screenshot {
width: 120px;
}

#logContainer td.screenshot {
border-top: 1px solid white;
border-bottom: 1px solid white;
}

#logContainer td.message {
white-space: normal;
}

#logContainer thead {
background-color: #f0f0f0;
font-weight: bold;
}

#logContainer tr.selected {
/* Don't select the screenshot column */
#logContainer tr.selected td:nth-child(-n+4) {
outline: 2px solid #007acc;
outline-offset: -2px;
}

/* Zebra striping for rows */
#logContainer tbody tr:nth-child(even) {
#logContainer tbody tr:nth-child(even) td:not(.screenshot) {
background-color: #fafafa;
}

Expand Down Expand Up @@ -62,10 +82,6 @@
color: #555;
}

#logContainer td.timestamp {
white-space: nowrap;
}

#filterBar {
position: sticky;
top: 0;
Expand Down Expand Up @@ -123,6 +139,26 @@
#clearFilter:hover {
color: #000;
}

#screenshotModal {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background-color: rgba(0, 0, 0, 0.25);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
}

#screenshotModal img {
max-width: 90%;
max-height: 90%;
box-shadow: 0 0 12px rgba(0, 0, 0, 0.8);
border-radius: 6px;
}
</style>
<script>
function removeTimestamp(orig, entryTimestamp) {
Expand Down Expand Up @@ -256,10 +292,11 @@
}

let logEntries = [];
let selectedRow = null;
const baseUrl = "https://openvmmghtestresults.blob.core.windows.net/results/";

function getTestResults(run, job, test) {
const url = `https://openvmmghtestresults.blob.core.windows.net/results/${run}/${job}/${test}/petri.jsonl`;
console.log(url);
const url = `${baseUrl}/${run}/${job}/${test}/petri.jsonl`;
fetch(url)
.then(response => response.text())
.then(data => {
Expand All @@ -274,6 +311,11 @@
let severity = line.severity || "INFO";
const source = line.source || "unknown";

if (line.attachment && line.attachment.endsWith(".png") && logEntries.length > 0) {
logEntries[logEntries.length - 1].screenshot = new URL(line.attachment, url);
continue;
}

if (!line.message) {
console.log(line);
continue;
Expand All @@ -293,16 +335,17 @@
message = ansiToHtml(message);

logEntries.push({
index: i,
timestamp: timestamp,
relative: relative,
severity: severity,
source: source,
message: message
message: message,
screenshot: null,
});
}

prepTestResults();

})
.catch(error => console.error('Error fetching test results:', error));
}
Expand All @@ -311,22 +354,28 @@
const container = document.getElementById("logContainer");
container.innerHTML = `<table>
<thead>
<colgroup>
<col span="4" />
<col class="screenshot" />
</colgroup>
<tr>
<th>Timestamp</th>
<th>Severity</th>
<th>Source</th>
<th>Message</th>
<th>Screenshot</th>
</tr>
</thead>
<tbody id="logBody">
</tbody>
</table>
<div id="floatingScreenshots"></div>
`;

// Clear on X click
document.getElementById("clearFilter").addEventListener('click', () => {
input.value = '';
renderLogTable(logEntries);
renderLogs(logEntries);
input.focus();
});

Expand All @@ -345,8 +394,10 @@
if (searchInput.value) {
searchInput.value = '';
renderLogs(logEntries);
} else {
} else if (searchInput === document.activeElement) {
searchInput.blur();
} else if (selectedRow) {
deselectRow();
}
}
});
Expand All @@ -360,22 +411,70 @@
});

enableRowSelectionAndCopy(container);

setupModalSupport();
}

function renderLogs(filteredLogs) {
let html = "";
for (const log of filteredLogs) {
html += `<tr class="severity-${log.severity}">
<td class="timestamp" title="${log.timestamp}">${log.relative}</td>
filteredLogs.forEach((log, i) => {
html += `<tr class="severity-${log.severity}" id="log-${log.index}" data-log-index="${i}">
<td title="${log.timestamp}">${log.relative}</td>
<td>${log.severity}</td>
<td>${log.source}</td>
<td>${log.message}</td>
<td class="message">${log.message}</td>
<td class="screenshot"></td>
</tr>`;

}
});
const logBody = document.getElementById("logBody");
logBody.innerHTML = html;
if (selectedRow) {
selectedRow.tr = null;
const tr = document.getElementById(selectedRow.id);
if (tr) {
selectRow(tr);
tr.scrollIntoView({ block: 'center' });
}
}
insertFloatingScreenshots(filteredLogs);
}

function insertFloatingScreenshots(logs) {
const container = document.getElementById('logContainer');
const screenshots = document.getElementById('floatingScreenshots');
screenshots.innerHTML = ''; // Clear previous screenshots

logs.forEach((log, i) => {
if (!log.screenshot || i == 0) return;

const prevRow = container.querySelector(`tr[data-log-index="${i - 1}"]`);
const nextRow = container.querySelector(`tr[data-log-index="${i + 1}"]`);
const prevBox = prevRow?.getBoundingClientRect();
const nextBox = nextRow?.getBoundingClientRect();

if (!prevBox || !nextBox) {
return;
}

const containerBox = container.getBoundingClientRect();
const offsetTop = (prevBox.bottom + nextBox.top) / 2 - containerBox.top;

const img = document.createElement('img');
img.src = log.screenshot;
img.alt = 'screenshot';
img.classList.add('screenshot-thumb');
img.style.position = 'absolute';
img.style.top = `${offsetTop}px`;
img.style.right = '8px';
img.style.width = '120px';
img.style.border = '1px solid #ccc';
img.style.borderRadius = '4px';
img.style.boxShadow = '0 2px 6px rgba(0,0,0,0.1)';
img.style.zIndex = '5';
img.style.cursor = 'zoom-in';

screenshots.appendChild(img);
});
}

function tokenizeSearchQuery(query) {
Expand Down Expand Up @@ -415,35 +514,80 @@
});
}

function selectRow(tr) {
if (selectedRow?.tr) {
selectedRow.tr.classList.remove('selected');
}
selectedRow = { id: tr.id, tr: tr };
tr.classList.add('selected');
history.replaceState(null, '', `#${tr.id}`);
}

function deselectRow() {
if (selectedRow?.tr) {
selectedRow.tr.classList.remove('selected');
}
selectedRow = null;
history.replaceState(null, '', '#');
}

function enableRowSelectionAndCopy(container) {
let selectedRow = null;
const hash = location.hash;
if (hash.startsWith('#log-')) {
let row = document.querySelector(hash);
if (row) {
// Scroll into view and simulate selection
row.scrollIntoView({ block: 'center' });
selectRow(row);
}
}

container.addEventListener('click', (e) => {
const row = e.target.closest('tr');
if (!row || !row.parentElement.matches('tbody')) return;

// Remove previous selection
if (selectedRow) selectedRow.classList.remove('selected');

// Select new row
selectedRow = row;
selectedRow.classList.add('selected');
if (row === selectedRow?.tr) {
deselectRow();
} else {
selectRow(row);
}
});

document.addEventListener('copy', (e) => {
if (!selectedRow) return;
const tr = selectedRow?.tr;
if (!tr) return;
const selection = window.getSelection();
if (selection && selection.toString().trim()) return; // user selected text, let it be

// Copy text content of row, tab-separated
const cells = Array.from(selectedRow.querySelectorAll('td'));
const cells = Array.from(tr.querySelectorAll('td'));
const text = cells.map(td => td.textContent.trim()).join('\t');

e.clipboardData.setData('text/plain', text);
e.preventDefault();
});
}

function setupModalSupport() {
const modal = document.getElementById('screenshotModal');
const modalImg = document.getElementById('modalImage');

document.addEventListener('click', (e) => {
if (e.target.matches('.screenshot-thumb')) {
if (e.ctrlKey || e.metaKey) {
// Ctrl+click or Cmd+click opens in a new tab
window.open(e.target.src, '_blank');
} else {
// Regular click opens in modal
modalImg.src = e.target.src;
modal.style.display = 'flex';
}
} else if (e.target === modal || e.target === modalImg) {
modal.style.display = 'none';
modalImg.src = '';
}
});
}

window.onload = function () {
const urlParams = new URLSearchParams(window.location.search);
const run = urlParams.get('run');
Expand Down Expand Up @@ -474,6 +618,9 @@ <h2 id="jobName"></h2>
</div>
</div>
<div id="logContainer"></div>
<div id="screenshotModal" style="display: none;">
<img id="modalImage" src="" alt="screenshot" />
</div>
</body>

</html>