Skip to content
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

Add interactive log viewer #5836

Merged
merged 1 commit into from
Aug 15, 2024
Merged
Show file tree
Hide file tree
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
3 changes: 3 additions & 0 deletions assets/javascripts/anser-import.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@ const module = {};
function ansiToHtml(data) {
return Anser.linkify(Anser.ansiToHtml(Anser.escapeForHtml(data), {use_classes: true}));
}
function ansiToText(data) {
return Anser.ansiToText(data);
}
101 changes: 99 additions & 2 deletions assets/javascripts/test_result.js
Original file line number Diff line number Diff line change
Expand Up @@ -578,14 +578,97 @@ function setupResult(jobid, state, result, status_url) {
setInfoPanelClassName(state, result);
}

function loadEmbeddedLogFiles() {
function delay(callback, ms) {
let timer;
return function () {
clearTimeout(timer);
timer = setTimeout(callback.bind(this, ...arguments), ms || 0);
};
}

function filterLogLines(input) {
if (input === undefined) {
return;
}
const string = input.value;
let regex = undefined;
const match = string.match(/^\/(.*)\/([i]*)$/);
if (match) {
regex = new RegExp(match[1], match[2]);
}
displaySearchInfo('Searching…');
$('.embedded-logfile').each(function (index, logFileElement) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd avoid using jQuery in new code. This could be written using plain JavaScript like this:

Suggested change
$('.embedded-logfile').each(function (index, logFileElement) {
Array.from(document.getElementsByClassName('embedded-logfile')).forEach((logFileElement) => {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's using the same code as in the existing loadEmbeddedLogFiles and it would have been weird to use one style in one function and one in the other. And when looking for removing jquery from the existing code, how far should I go?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can keep it. I'd just avoid it in new code even though it means we don't have a consistent style everywhere.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, and you can actually avoid the outer Array.from(…) in my example. The node list should also have a forEach method.

let content = logFileElement.dataset.content;
if (content === undefined) {
return;
}
if (string.length > 0) {
const lines = content.split(/\r?\n/);
const wanted = [];
for (const line of lines) {
if (regex) {
// For searching for /^something/ we need to remove ansi control characters
const text = ansiToText(line);
if (text.match(regex)) {
wanted.push(line);
}
continue;
}
if (line.includes(string)) {
wanted.push(line);
}
}
content = wanted.join('\n');
displaySearchInfo(`Showing ${wanted.length} / ${lines.length} lines`);
} else {
displaySearchInfo('');
}
logFileElement.innerHTML = ansiToHtml(content);
});
const fullCurrentUrl = window.location.href;
const urlParts = fullCurrentUrl.split('#');
const currentUrl = urlParts[0];
const fragment = urlParts[1];
if (string.length > 0) {
window.location.href = `${currentUrl}#filter=${encodeURIComponent(string)}`;
} else if (fragment) {
// leaving off the # here would reload the page
window.location.href = currentUrl + '#';
}
}

function filterEmbeddedLogFiles() {
const searchBox = document.getElementById('filter-log-file');
if (searchBox) {
const currentUrl = window.location.href;
const fragment = currentUrl.split('#')[1];
if (fragment) {
const params = fragment.split('&');
for (let i = 0; i < params.length; i++) {
const keyval = params[i].split('=');
if (keyval[0] === 'filter') {
searchBox.value = decodeURIComponent(keyval[1]);
}
}
}
}
const filter = filterLogLines.bind(null, searchBox);
loadEmbeddedLogFiles(filter);
}

function loadEmbeddedLogFiles(filter) {
$('.embedded-logfile').each(function (index, logFileElement) {
if (logFileElement.dataset.contentsLoaded) {
return;
}
$.ajax(logFileElement.dataset.src)
.done(function (response) {
logFileElement.innerHTML = ansiToHtml(response);
logFileElement.dataset.content = response;
if (filter) {
filter();
} else {
logFileElement.innerHTML = ansiToHtml(response);
}
logFileElement.dataset.contentsLoaded = true;
})
.fail(function (jqXHR, textStatus, errorThrown) {
Expand All @@ -594,6 +677,20 @@ function loadEmbeddedLogFiles() {
});
}

window.onload = function () {
const searchBox = document.getElementById('filter-log-file');
if (!searchBox) {
return;
}
Martchus marked this conversation as resolved.
Show resolved Hide resolved
const filter = filterLogLines.bind(null, searchBox);
searchBox.addEventListener('keyup', delay(filter), 1000);
searchBox.addEventListener('change', filter, false);
};

function displaySearchInfo(text) {
document.getElementById('filter-info').innerHTML = text;
}

function setCurrentPreviewFromStepLinkIfPossible(stepLink) {
if (tabConfiguration.details.hasContents && !stepLink.parent().is('.current_preview')) {
setCurrentPreview(stepLink.parent());
Expand Down
12 changes: 12 additions & 0 deletions t/ui/18-tests-details.t
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,18 @@ subtest 'misc details: title, favicon, go back, go to source view, go to log vie

wait_for_ajax msg => 'log contents';
like $driver->find_element('.embedded-logfile .ansi-blue-fg')->get_text, qr/send(autotype|key)/, 'log is colorful';
like $driver->find_element('.embedded-logfile')->get_text, qr{/usr/bin/qemu-kvm}, 'qemu-kvm is shown in log viewer';

$driver->find_element('#filter-log-file')->send_keys('kate');
like $driver->find_element('#filter-info')->get_text, qr{Showing 3 / 1292 lines},
'Showing filter result info for substring';
unlike $driver->find_element('.embedded-logfile')->get_text, qr{/usr/bin/qemu-kvm},
'qemu-kvm is not shown when filtering for something else';
$driver->find_element('#filter-log-file')->clear;
like $driver->find_element('#filter-info')->get_text, qr{^$}, 'Filter result info cleared';
$driver->find_element('#filter-log-file')->send_keys('/kate-[12]/');
like $driver->find_element('#filter-info')->get_text, qr{Showing 2 / 1292 lines},
'Showing filter result info for regex';
};

my $t = Test::Mojo->new('OpenQA::WebAPI');
Expand Down
4 changes: 3 additions & 1 deletion templates/webapi/test/logfile.html.ep
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@
%= asset $_ for qw(test_result.js anser.js ansi-colors.css)
% end
% content_for 'ready_function' => begin
loadEmbeddedLogFiles();
filterEmbeddedLogFiles();
% end

% my $url = url_for('test_file', testid => $testid, filename => $filename);
<div class="corner-buttons" style="margin-top: -5px;">
<span id="filter-info"></span>
<input id="filter-log-file" placeholder="substring or /regex/i" type="search">
<a class="btn btn-light" href=".#downloads">
<i class="fa fa-chevron-left"></i> Back to job <%= $testid %>
</a>
Expand Down
Loading