* Written in react.js for speed
* Does not require custom treeherder backend API's
* Improved scrolling
* Fixes issues in URL when linking directly to line numbers
This commit is contained in:
William Lachance 2016-12-29 15:14:48 -05:00 коммит произвёл GitHub
Родитель 2f128aa91d
Коммит bfa3def90f
12 изменённых файлов: 235 добавлений и 626 удалений

Просмотреть файл

@ -42,6 +42,8 @@ matrix:
# since caches are tied to the language/version combination.
directories:
- node_modules
addons:
firefox: latest
install:
- npm install
before_script:

Просмотреть файл

@ -73,6 +73,11 @@ body {
width: 3em;
}
.lv-line-no.label {
border-radius: 0px;
margin: 0em 0.6em 0em 0em;
}
.lv-line-text {
white-space: pre;
}
@ -105,25 +110,15 @@ body {
white-space: normal;
}
.lv-log-container {
.logview-container {
flex: 1;
overflow-x: auto;
font-family: monospace;
font-size: small;
background: #f8f8f8;
overflow: hidden;
}
.lv-log-container > div:nth-child(even) {
background: #fff;
}
/* Equal weight selector needs to follow the above nth-child() */
.lv-log-container > .lv-log-line > .text-danger {
background: #fbe3e3;
}
.lv-log-container > div.lv-log-line > div.lv-line-text.lv-selected-lines {
background: #F8EEC7;
.logview-container > iframe {
width: 100%;
height: 100%;
overflow: auto;
}
.lv-line-highlight {
@ -197,30 +192,6 @@ div.lv-step {
color: #8a6d3b;
}
.lv-line-no.label {
border-radius: 0px;
margin: 0em 0.6em 0em 0em;
}
.lv-log-msg {
position: relative;
padding: 26px 0 0 20px;
font-weight: bold;
font-family: sans-serif;
font-size: 11px;
}
.lv-log-overlay {
padding: 6px;
border: 2px solid #4cae4c;
border-radius: 3px;
background: #fff;
}
.lv-log-error {
border: 2px solid #ff0000;
}
.job-header {
max-height: 240px;
overflow-y: auto;

Просмотреть файл

@ -0,0 +1,32 @@
'use strict';
treeherder.component('thLogViewer', {
templateUrl: 'partials/logviewer/logviewer.html',
controller: ($sce, $location, $element, $scope) => {
const logParams = () => {
const q = $location.search();
let params = { lineHeight: 13 };
if (q.lineNumber) {
const lines = q.lineNumber.split('-');
params.lineNumber = lines[0];
params.highlightStart = lines[0];
params.highlightEnd = lines.length === 2 ? lines[1] : lines[0];
}
return Object.keys(params)
.reduce((qs, key) => `${qs}&${key}=${params[key]}`, '');
};
$element.find('iframe').bind('load', () => $scope.$parent.logviewerInit());
$scope.$parent.$watch('rawLogURL', () => {
const parent = $scope.$parent;
if ($scope.$parent.rawLogURL) {
$element[0].childNodes[0].src = $sce.trustAsResourceUrl(`${parent.logBasePath}?url=${parent.rawLogURL}${logParams()}`);
}
});
}
});

Просмотреть файл

@ -1,441 +1,262 @@
'use strict';
logViewerApp.controller('LogviewerCtrl', [
'$anchorScroll', '$http', '$location', '$q', '$rootScope', '$scope',
'$timeout', '$resource', 'ThTextLogStepModel', 'ThJobDetailModel', 'ThLog',
'ThLogSliceModel', 'ThJobModel', 'thNotify', 'dateFilter', 'ThResultSetModel',
'$location', '$window', '$document', '$rootScope', '$scope',
'$timeout', '$resource', 'ThTextLogStepModel', 'ThJobDetailModel',
'ThJobModel', 'thNotify', 'dateFilter', 'ThResultSetModel',
'thDateFormat', 'thReftestStatus',
function Logviewer(
$anchorScroll, $http, $location, $q, $rootScope, $scope,
$timeout, $resource, ThTextLogStepModel, ThJobDetailModel, ThLog,
ThLogSliceModel, ThJobModel, thNotify, dateFilter, ThResultSetModel,
$location, $window, $document, $rootScope, $scope,
$timeout, $resource, ThTextLogStepModel, ThJobDetailModel,
ThJobModel, thNotify, dateFilter, ThResultSetModel,
thDateFormat, thReftestStatus) {
// changes the size of chunks pulled from server
var LINE_BUFFER_SIZE = 100;
var LogSlice;
const query_string = $location.search();
$scope.css = '';
$rootScope.logBasePath = 'https://taskcluster.github.io/unified-logviewer/';
$rootScope.urlBasePath = $location.absUrl().split('logviewer')[0];
var query_string = $location.search();
if (query_string.repo !== "") {
$rootScope.repoName = query_string.repo;
}
if (query_string.job_id !== "") {
$scope.job_id= query_string.job_id;
LogSlice = new ThLogSliceModel($scope.job_id, LINE_BUFFER_SIZE);
}
$scope.displayedLogLines = [];
$scope.loading = false;
$scope.logError = false;
$scope.jobExists = true;
$scope.currentLineNumber = 0;
$scope.highestLine = 0;
$scope.showSuccessful = true;
$scope.willScroll = false;
$scope.$watch('steps', function () {
$scope.loading = false;
$scope.jobExists = true;
$scope.showSuccessful = true;
$scope.$watch('steps', () => {
if (!$scope.steps) {
return;
}
$scope.showSuccessful = !$scope.hasFailedSteps();
});
$scope.$watch('[selectedBegin, selectedEnd]', function(newVal) {
var newHash = (newVal[0] === newVal[1]) ? newVal[0] : newVal[0] + "-L" + newVal[1];
if (!isNaN(newVal[0])) {
$location.hash("L" + newHash);
} else if ($scope.steps) {
$location.hash("");
}
});
$scope.logPostMessage = (values) => {
const { lineNumber, highlightStart } = values;
$scope.$on("$locationChangeSuccess", function() {
var oldLine = parseInt($scope.currentLineNumber);
getSelectedLines();
if (lineNumber && !highlightStart) {
values.highlightStart = lineNumber;
values.highlightEnd = lineNumber;
}
var newLine = parseInt($scope.selectedBegin);
var range = LINE_BUFFER_SIZE / 2;
if ((newLine <= (oldLine - range) || newLine >= oldLine + range) && !$scope.willScroll) {
if ($scope.steps) {
moveScrollToLineNumber(newLine);
}
}
$scope.willScroll = false;
});
$scope.click = function(line, $event) {
$scope.willScroll = true;
if ($event.shiftKey) {
if (line.index < $scope.selectedBegin) {
$scope.selectedEnd = $scope.selectedBegin;
$scope.selectedBegin = line.index;
} else {
$scope.selectedEnd = line.index;
}
} else {
$scope.selectedBegin = $scope.selectedEnd = line.index;
}
updateQuery(values);
$document[0].getElementById('logview').contentWindow.postMessage(values, $rootScope.logBasePath);
};
// Erase the value of selectedBegin, used to erase the hash value when
// the user clicks on the error step button
$scope.eraseSelected = function() {
$scope.selectedBegin = 'undefined';
};
$scope.hasFailedSteps = () => {
const steps = $scope.steps;
$scope.setLineNumber = function(number) {
$scope.selectedBegin = number;
$scope.selectedEnd = number;
};
$scope.hasFailedSteps = function () {
var steps = $scope.steps;
if (!steps) {
return false;
}
for (var i = 0; i < steps.length; i++) {
for (let i = 0; i < steps.length; i++) {
// We only recently generated step results as part of ingestion,
// so we have to check the results property is present.
// TODO: Remove this when the old data has expired, so long as
// other data submitters also provide a step result.
if ('result' in steps[i] && steps[i].result !== 'success' &&
steps[i].result !== 'skipped') {
return true;
}
}
return false;
};
// Get the css class for the result, step buttons and other general use
$scope.getShadingClass = function(result) {
$scope.getShadingClass = (result) => {
return "result-status-shading-" + result;
};
$scope.loadMore = function(bounds, element) {
var deferred = $q.defer(), range, above, below;
if (!$scope.loading) {
// move the line number either up or down depending which boundary was hit
$scope.currentLineNumber = moveLineNumber(bounds);
range = {
start: $scope.currentLineNumber,
end: $scope.currentLineNumber
};
if (bounds.top) {
above = getChunkAbove(range);
} else if (bounds.bottom) {
below = getChunkBelow(range);
} else {
range = getChunksSurrounding($scope.currentLineNumber);
}
// dont do the call if we already have all the lines
if (range.start === range.end) {
return deferred.promise;
}
$scope.loading = true;
var lineRangeParams = {
job_id: $scope.job_id,
start_line: range.start,
end_line: range.end
};
LogSlice.get_line_range(lineRangeParams, {
buffer_size: LINE_BUFFER_SIZE
}).then(function(data) {
drawErrorLines(data);
if (bounds.top) {
for (var i = data.length - 1; i >= 0; i--) {
// make sure we are inserting at the right place
if ($scope.displayedLogLines[0].index !== data[i].index + 1) {
continue;
}
$scope.displayedLogLines.unshift(data[i]);
}
$timeout(function () {
if (above) {
removeChunkBelow();
}
}, 100);
} else if (bounds.bottom) {
var sh = element.scrollHeight;
var lines = $scope.displayedLogLines;
for (var j = 0; j < data.length; j++) {
// make sure we are inserting at the right place
if (lines[lines.length - 1].index !== data[j].index - 1) {
continue;
}
$scope.displayedLogLines.push(data[j]);
}
$timeout(function () {
if (below) {
removeChunkAbove();
element.scrollTop -= element.scrollHeight - sh;
}
}, 100);
} else {
$scope.displayedLogLines = data;
}
$scope.loading = false;
deferred.resolve();
}, function () {
$scope.loading = false;
$scope.logError = true;
thNotify.send("The log no longer exists or has expired", 'warning', true);
deferred.reject();
});
} else {
deferred.reject();
}
return deferred.promise;
};
// @@@ it may be possible to do this with the angular date filter?
$scope.formatTime = function(startedStr, finishedStr) {
$scope.formatTime = (startedStr, finishedStr) => {
if (!startedStr || !finishedStr) {
return "";
return '';
}
var sec = Math.abs(new Date(startedStr) - new Date(finishedStr)) / 1000.0;
var h = Math.floor(sec/3600);
var m = Math.floor(sec%3600/60);
var s = Math.floor(sec%3600 % 60);
var secStng = sec.toString();
var ms = secStng.substr(secStng.indexOf(".")+1, 2);
return ((h > 0 ? h + "h " : "") + (m > 0 ? m + "m " : "") +
(s > 0 ? s + "s " : "") + (ms > 0 ? ms + "ms " : "00ms"));
const sec = Math.abs(new Date(startedStr) - new Date(finishedStr)) / 1000.0;
const h = Math.floor(sec / 3600);
const m = Math.floor(sec % 3600 / 60);
const s = Math.floor(sec % 3600 % 60);
const secStng = sec.toString();
const ms = secStng.substr(secStng.indexOf(".") + 1, 2);
return ((h > 0 ? h + 'h ' : '') + (m > 0 ? m + 'm ' : '') +
(s > 0 ? s + 's ' : '') + (ms > 0 ? ms + 'ms ' : '00ms'));
};
$scope.displayTime = function(started, finished) {
var start = started ? started.substr(started.indexOf(" ")+1, 8) : '?';
var end = finished ? finished.substr(finished.indexOf(" ")+1, 8) : '?';
return start + "-" + end;
$scope.displayTime = (started, finished) => {
const start = started ? started.substr(started.indexOf(' ') + 1, 8) : '?';
const end = finished ? finished.substr(finished.indexOf(' ') + 1, 8) : '?';
return start + '-' + end;
};
$scope.init = function() {
$scope.init = () => {
$scope.logProperties = [];
// HACK: check if this was a link to an older job log with a
// project specific id, and rewrite the job_id if so
ThJobModel.get_list($scope.repoName, {
project_specific_id: $scope.job_id
}).then(function(jobList) {
if (jobList.length > 0) {
$scope.job_id = jobList[0]['id'];
}
ThJobModel.get($scope.repoName, $scope.job_id).then(function(job) {
ThJobModel.get($scope.repoName, $scope.job_id).then(job => {
// set the title of the browser window/tab
$scope.logViewerTitle = job.get_title();
if (job.logs && job.logs.length) {
$scope.rawLogURL = job.logs[0].url;
}
// set the result value and shading color class
$scope.result = {label: "Result", value: job.result};
$scope.result = {label: 'Result', value: job.result};
$scope.resultStatusShading = $scope.getShadingClass(job.result);
// other properties, in order of appearance
$scope.logProperties = [
{label: "Job", value: $scope.logViewerTitle},
{label: "Machine", value: job.machine_name},
{label: "Start", value: dateFilter(job.start_timestamp*1000, thDateFormat)},
{label: "End", value: dateFilter(job.end_timestamp*1000, thDateFormat)}
{label: 'Job', value: $scope.logViewerTitle},
{label: 'Machine', value: job.machine_name},
{label: 'Start', value: dateFilter(job.start_timestamp * 1000, thDateFormat)},
{label: 'End', value: dateFilter(job.end_timestamp * 1000, thDateFormat)}
];
// Test to expose the reftest button in the logviewer actionbar
$scope.isReftest = function() {
$scope.isReftest = () => {
if (job.job_group_name) {
return thReftestStatus(job);
}
};
// get the revision and linkify it
ThResultSetModel.getResultSet($scope.repoName, job.push_id).then(function(data){
var revision = data.data.revision;
$scope.logProperties.push({label: "Revision", value: revision});
ThResultSetModel.getResultSet($scope.repoName, job.result_set_id).then(data => {
const revision = data.data.revision;
$scope.logProperties.push({label: 'Revision', value: revision});
});
ThJobDetailModel.getJobDetails({job_guid: job.job_guid}).then(function(jobDetails) {
ThJobDetailModel.getJobDetails({job_guid: job.job_guid}).then(jobDetails => {
$scope.job_details = jobDetails;
});
}, function () {
}, () => {
$scope.loading = false;
$scope.jobExists = false;
thNotify.send("The job does not exist or has expired", 'danger', true);
thNotify.send('The job does not exist or has expired', 'danger', true);
});
});
};
$scope.logviewerInit = () => {
// Listen for messages from child frame
setLogListener();
ThTextLogStepModel.query({
project: $rootScope.repoName,
jobId: $scope.job_id
}, function(textLogSteps) {
}, textLogSteps => {
let shouldPost = true;
const allErrors = _.flatten(textLogSteps.map(s => s.errors));
const q = $location.search();
$scope.steps = textLogSteps;
// add an ordering to each step
textLogSteps.forEach((step, i) => {step.order = i;});
// If the log contains no errors load the head otherwise
// load the first failure step line. We also need to test
// for the 0th element for outlier jobs.
var allErrors = _.flatten(textLogSteps.map(s => s.errors));
if (allErrors.length === 0) {
angular.element(document).ready(function () {
if (isNaN($scope.selectedBegin)) {
for (var i = 0; i < $scope.steps.length; i++) {
var step = $scope.steps[i];
// load the first failure step line else load the head
if (allErrors.length) {
$scope.css = $scope.css + errorLinesCss(allErrors);
if (!q.lineNumber) {
$scope.logPostMessage({ lineNumber: allErrors[0].line_number + 1, customStyle: $scope.css });
shouldPost = false;
}
} else if (!q.lineNumber) {
for (let i = 0; i < $scope.steps.length; i++) {
let step = $scope.steps[i];
if (step.result !== "success") {
$scope.selectedBegin = step.started_line_number;
$scope.selectedEnd = step.finished_line_number;
$scope.logPostMessage({
lineNumber: step.started_line_number + 1,
highlightStart: step.started_line_number + 1,
highlightEnd: step.finished_line_number + 1,
customStyle: $scope.css
});
break;
}
}
}
moveScrollToLineNumber($scope.selectedBegin);
});
} else {
$scope.setLineNumber(allErrors[0].line_number);
moveScrollToLineNumber($scope.selectedBegin);
if (shouldPost) {
$scope.logPostMessage({ customStyle: $scope.css });
}
});
});
};
$scope.setDisplayedStep = (step) => {
const highlightStart = step.started_line_number + 1 || step.line_number + 1;
const highlightEnd = step.finished_line_number + 1 || step.line_number + 1;
$scope.displayedStep = step;
$scope.logPostMessage({ lineNumber: highlightStart, highlightStart, highlightEnd });
};
function errorLinesCss(errors) {
return errors
.map(({ line_number }) => `a[id="${line_number + 1}"]+span`)
.join(',')
.concat('{background:#fbe3e3;color:#a94442}');
}
function logCss() {
const hideToolbar = '#toolbar{display:none}';
const body = 'html,body{background:#f8f8f8;color:#333;font-size:12px}';
const highlight = '#log p.highlight a,#log p.highlight span{background:#f8eec7!important}';
const hover = '#log p:hover{background:transparent}#log p a:hover,#log p.highlight a:hover{background:#f8eec7;color:#000}';
const stripe = '.lazy-list p:nth-child(2n){background:#fff!important}.lazy-list p:nth-child(2n+1){background:#f8f8f8!important}';
const linePadding = '#log p{padding:0 15px 0 35px}';
const lineNumber = '#log p a,#log p.highlight a{color:rgba(0,0,0,.3)}';
const font = '#log{font-family:monospace}';
return hideToolbar + body + highlight + hover + stripe + lineNumber + linePadding + font;
}
/** utility functions **/
function moveScrollToLineNumber(linenumber) {
$scope.currentLineNumber = linenumber;
$scope.displayedStep = getStepFromLine(linenumber);
$scope.loadMore({}).then(function () {
$timeout(function () {
var raw = $('.lv-log-container')[0];
var line = $('.lv-log-line[line="' + linenumber + '"]');
raw.scrollTop += line.offset().top - $('.run-data').outerHeight() -
$('.navbar').outerHeight() - 120;
function updateQuery(values) {
const data = typeof values === 'string' ? JSON.parse(values) : values;
const { lineNumber, highlightStart, highlightEnd } = data;
if (highlightStart !== highlightEnd) {
$location.search('lineNumber', `${highlightStart}-${highlightEnd}`);
}
else if (highlightStart) {
$location.search('lineNumber', highlightStart);
} else {
$location.search('lineNumber', lineNumber);
}
}
function setLogListener() {
let workerReady = false;
$window.addEventListener('message', (e) => {
// Send initial css when child frame loads URL successfully
if (!workerReady) {
workerReady = true;
$scope.css = $scope.css + logCss();
$scope.logPostMessage({ customStyle: $scope.css });
}
$timeout(updateQuery(e.data));
});
});
}
function getStepFromLine(linenumber) {
return $scope.steps.find(function(step) {
return (step.started_linenumber <= linenumber &&
step.finished_linenumber >= linenumber);
});
}
function getSelectedLines () {
var urlHash = $location.hash();
var regexSelectedlines = /L(\d+)(-L(\d+))?$/;
if (regexSelectedlines.test(urlHash)) {
var matchSelectedLines = urlHash.match(regexSelectedlines);
if (isNaN(matchSelectedLines[3])) {
matchSelectedLines[3] = matchSelectedLines[1];
}
$scope.selectedBegin = matchSelectedLines[1];
$scope.selectedEnd = matchSelectedLines[3];
}
}
function logFileLineCount () {
var steps = $scope.steps;
return steps[ steps.length - 1 ].finished_line_number + 1;
}
function moveLineNumber (bounds) {
var lines = $scope.displayedLogLines, newLine;
if (bounds.top) {
return lines[0].index;
} else if (bounds.bottom) {
newLine = lines[lines.length - 1].index + 1;
return (newLine > logFileLineCount()) ? logFileLineCount(): newLine;
}
return $scope.currentLineNumber;
}
function drawErrorLines (data) {
if (data.length === 0) {
return;
}
var min = data[0].index;
var max = data[ data.length - 1 ].index;
$scope.steps.forEach(function(step) {
step.errors.forEach(function(err) {
var line = err.line_number;
if (line < min || line > max) {
return;
}
var index = line - min;
data[index].hasError = true;
});
});
}
function getChunksSurrounding(line) {
var request = {start: null, end: null};
getChunkContaining(line, request);
getChunkAbove(request);
getChunkBelow(request);
return request;
}
function getChunkContaining (line, request) {
var index = Math.floor(line/LINE_BUFFER_SIZE);
request.start = index * LINE_BUFFER_SIZE;
request.end = (index + 1) * LINE_BUFFER_SIZE;
}
function getChunkAbove (request) {
request.start -= LINE_BUFFER_SIZE;
request.start = Math.floor(request.start/LINE_BUFFER_SIZE)*LINE_BUFFER_SIZE;
if (request.start >= 0) {
return true;
}
request.start = 0;
return false;
}
function getChunkBelow (request) {
var lastLine = logFileLineCount();
request.end += LINE_BUFFER_SIZE;
request.end = Math.ceil(request.end/LINE_BUFFER_SIZE)*LINE_BUFFER_SIZE;
if (request.end <= lastLine) {
return true;
}
request.end = lastLine;
return false;
}
function removeChunkAbove () {
$scope.displayedLogLines = $scope.displayedLogLines.slice(LINE_BUFFER_SIZE);
}
function removeChunkBelow () {
var endSlice = $scope.displayedLogLines.length - LINE_BUFFER_SIZE;
$scope.displayedLogLines = $scope.displayedLogLines.slice(0, endSlice);
}
}
]);

Просмотреть файл

@ -1,22 +0,0 @@
'use strict';
treeherder.directive('lvInfiniteScroll', ['$timeout', function ($timeout) {
return function (scope, element) {
element.bind('scroll', function () {
var raw = element[0];
var sh = raw.scrollHeight;
if (raw.scrollTop <= 100) {
scope.loadMore({top: true}, raw).then(function(haltScrollTop) {
if (!haltScrollTop) {
$timeout(function () {
raw.scrollTop = raw.scrollHeight - sh;
});
}
});
} else if (raw.scrollTop >= (raw.scrollHeight - $(element).height() - 100)) {
scope.loadMore({bottom: true}, raw);
}
});
};
}]);

Просмотреть файл

@ -1,57 +0,0 @@
'use strict';
treeherder.directive('lvLogLines', ['$parse', function () {
function getOffsetOfStep (order) {
var el = $('.lv-step[order="' + order + '"]');
var parentOffset = el.parent().offset();
return el.offset().top -
parentOffset.top + el.parent().scrollTop() -
parseInt($('.steps-data').first().css('padding-bottom'));
}
function onScroll ($scope) {
var lines = $('.lv-log-line');
var scrollTop = $('.lv-log-container').scrollTop();
for (var i = 0, ll = lines.length; i < ll; i++) {
if (lines[i].offsetTop > scrollTop) {
var steps = $scope.steps;
var lineNumber = +$(lines[i]).attr('line');
for (var j = 0, sl = steps.length; j < sl; j++) {
if (lineNumber > (steps[j].started_line_number - 1) &&
lineNumber < (steps[j].finished_line_number + 1)) {
// make sure we aren't updating when its already correct
if ($scope.displayedStep &&
$scope.displayedStep.order === steps[j].order) {
return;
}
$scope.displayedStep = steps[j];
// scroll to the step
scrollTop = getOffsetOfStep(steps[j].order);
$('.steps-data').scrollTop(scrollTop);
if (!$scope.$$phase) {
$scope.$apply();
}
return;
}
}
}
}
}
/* -------------------------------------------------------------------- */
return {
restrict: 'A',
templateUrl: 'partials/logviewer/lvLogLines.html',
link: function (scope, element) {
$(element).scroll(onScroll.bind(this, scope));
}
};
}]);

Просмотреть файл

@ -1,9 +1,9 @@
'use strict';
treeherder.directive('lvLogSteps', ['$timeout', '$q', function ($timeout, $q) {
treeherder.directive('lvLogSteps', ['$timeout', $timeout => {
function getOffsetOfStep (order) {
var el = $('.lv-step[order="' + order + '"]');
var parentOffset = el.parent().offset();
const el = $('.lv-step[order="' + order + '"]');
const parentOffset = el.parent().offset();
return el.offset().top -
parentOffset.top + el.parent().scrollTop() -
@ -15,32 +15,12 @@ treeherder.directive('lvLogSteps', ['$timeout', '$q', function ($timeout, $q) {
return {
restrict: 'A',
templateUrl: 'partials/logviewer/lvLogSteps.html',
link: function (scope) {
scope.scrollTo = function($event, step, linenumber) {
scope.currentLineNumber = linenumber;
scope.loadMore({}).then(function () {
$timeout(function () {
var raw = $('.lv-log-container')[0];
var line = $('.lv-log-line[line="' + linenumber + '"]');
raw.scrollTop += line.offset().top - $('.run-data').outerHeight() -
$('.navbar').outerHeight() - 9;
});
}, function () {
// there is an error so bomb out
return $q.reject();
});
if (scope.displayedStep && scope.displayedStep.order === step.order) {
$event.stopPropagation();
}
};
scope.toggleSuccessfulSteps = function () {
link: (scope) => {
scope.toggleSuccessfulSteps = () => {
scope.showSuccessful = !scope.showSuccessful;
var firstError = scope.steps.filter(function (step) {
return step.result && step.result !== "success";
const firstError = scope.steps.filter(step => {
return step.result && step.result !== 'success';
})[0];
if (!firstError) {
@ -48,26 +28,10 @@ treeherder.directive('lvLogSteps', ['$timeout', '$q', function ($timeout, $q) {
}
// scroll to the first error
$timeout(function () {
var scrollTop = getOffsetOfStep(firstError.order);
$('.steps-data').scrollTop( scrollTop );
});
};
$timeout(() => {
const scrollTop = getOffsetOfStep(firstError.order);
scope.displayLog = function (step, state) {
scope.displayedStep = step;
scope.currentLineNumber = step.started_line_number;
scope.selectedBegin = step.started_line_number;
scope.selectedEnd = step.finished_line_number;
scope.loadMore({}).then(function () {
$timeout(function () {
var raw = $('.lv-log-container')[0];
var line = $('.lv-log-line[line="' + step.started_line_number + '"]');
if (state !== 'initialLoad') {
raw.scrollTop += line.offset().top - $('.run-data').outerHeight() -
$('.navbar').outerHeight() - 9;
}
});
$('.steps-data').scrollTop(scrollTop);
});
};
}

Просмотреть файл

@ -1,92 +0,0 @@
'use strict';
treeherder.factory('ThLogSliceModel', [
'$http', '$q', '$timeout', 'thUrl',
function($http, $q, $timeout, thUrl) {
// ThLogSliceModel is the js counterpart of logslice
var ThLogSliceModel = function(job_id, buffer_chunk_size, buffer_size) {
this.job_id = job_id;
this.chunk_size = buffer_chunk_size || 500;
this.buffer_size = buffer_size || 10;
this.buffer = {};
};
ThLogSliceModel.get_uri = function(){return thUrl.getProjectUrl("/logslice/");};
ThLogSliceModel.prototype.find_in_buffer = function (options) {
var ret = [], arr;
for (var i = options.start_line; i < options.end_line; i += this.chunk_size) {
arr = this.buffer[Math.floor(i/this.chunk_size)] || false;
if (arr) {
// update for LRU
arr.used = Date.now();
ret = ret.concat(arr.data);
} else {
return false;
}
}
return ret;
};
ThLogSliceModel.prototype.insert_into_buffer = function (options, res) {
for (var i = options.start_line, j = 0; i < options.end_line; i += this.chunk_size, j++) {
this.buffer[Math.floor(i/this.chunk_size)] = {
data: res.slice(j * this.chunk_size, (j+1) * this.chunk_size),
used: Date.now()
};
}
var size = this.buffer_size + 1;
while (size > this.buffer_size) {
size = 0;
var indexLRU = 0, baseDate = Date.now();
for (var k in this.buffer) {
if (this.buffer.hasOwnProperty(k)) {
size++;
if (this.buffer[k].used < baseDate) {
baseDate = this.buffer[k].used;
indexLRU = k;
}
}
}
if (size > this.buffer_size) {
delete this.buffer[indexLRU];
}
}
};
ThLogSliceModel.prototype.get_line_range = function(options, config) {
config = config || {};
var timeout = config.timeout || null;
var found = this.find_in_buffer(options);
var self = this;
var deferred = $q.defer();
if (found) {
deferred.resolve(found);
return deferred.promise;
}
return $http.get(ThLogSliceModel.get_uri(),{
params: options,
timeout: timeout
}).then(function (res) {
self.insert_into_buffer(options, res.data);
return res.data;
}, function () {
return $q.reject("Log not found");
});
};
return ThLogSliceModel;
}]);

Просмотреть файл

@ -113,11 +113,7 @@
<div class="col-md-6" lv-log-steps></div>
</div>
<!-- Log lines -->
<div class="lv-log-container"
lv-infinite-scroll
lv-log-lines="displayedLogLines">
</div>
<th-log-viewer class="logview-container"></th-log-viewer>
<th-notification-box></th-notification-box>
@ -139,11 +135,12 @@
<script src="js/values.js"></script>
<!-- Directives -->
<script src="js/directives/treeherder/log_viewer_infinite_scroll.js"></script>
<script src="js/directives/treeherder/log_viewer_lines.js"></script>
<script src="js/directives/treeherder/log_viewer_steps.js"></script>
<script src="js/directives/treeherder/main.js"></script>
<!-- Components -->
<script src="js/components/logviewer/logviewer.js"></script>
<!-- Main services -->
<script src="js/services/main.js"></script>
<script src="js/services/log.js"></script>
@ -153,7 +150,6 @@
<script src="js/models/job.js"></script>
<script src="js/models/runnable_job.js"></script>
<script src="js/models/resultset.js"></script>
<script src="js/models/log_slice.js"></script>
<script src="js/models/text_log_step.js"></script>
<!-- Filter -->

Просмотреть файл

@ -0,0 +1,3 @@
<iframe
id="logview"
frameBorder="0" />

Просмотреть файл

@ -1,11 +0,0 @@
<div class="lv-log-msg lv-log-overlay"
ng-if="loading"> Loading... </div>
<div ng-repeat="lv_line in displayedLogLines"
class="lv-log-line"
line="{{ ::lv_line.index }}">
<div class="lv-line-num" ng-click="click(lv_line, $event)"> {{::lv_line.index}} </div>
<div ng-class="{'text-danger': (lv_line.hasError == true),
'lv-selected-lines': (lv_line.index >= selectedBegin && lv_line.index <= selectedEnd)}"
class="lv-line-text"> {{::lv_line.text}}</div>
</div>

Просмотреть файл

@ -1,10 +1,11 @@
<div class="steps-data">
<div ng-repeat="step in steps"
ng-click="displayLog(step)"
ng-class="{'selected': (displayedStep.order === step.order)}"
ng-show="showSuccessful === true || step.result !== 'success'"
class="btn btn-block lv-step clearfix {{::getShadingClass(step.result)}}"
order="{{step.order}}">
<div ng-click="setDisplayedStep(step)">&nbsp;
<span class="pull-left clearfix text-left">
{{::step.order + 1}}. {{::step.name}}
</span>
@ -15,16 +16,17 @@
class="pull-right clearfix">
{{time}}
</span>
</div>
<div ng-if="step.errors.length > 0">
<div ng-repeat="error in step.errors"
ng-mouseover="check=(step==displayedStep)"
ng-mouseover="check=true"
ng-mouseleave="check=false"
ng-class="{'lv-line-highlight': check}"
ng-click="scrollTo($event, step, error.line_number); eraseSelected(); setLineNumber(error.line_number)"
ng-click="setDisplayedStep(error)"
class="text-left pull-left lv-error-line">
<span class="label label-default lv-line-no text-left">
{{::error.line_number}}
{{::error.line_number + 1}}
</span>
<span title="{{::error.line}}">