зеркало из https://github.com/mozilla/treeherder.git
Bug 1315808 - New log viewer (#2050)
* 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:
Родитель
2f128aa91d
Коммит
bfa3def90f
|
@ -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();
|
||||
|
||||
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);
|
||||
}
|
||||
if (lineNumber && !highlightStart) {
|
||||
values.highlightStart = lineNumber;
|
||||
values.highlightEnd = lineNumber;
|
||||
}
|
||||
$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);
|
||||
});
|
||||
|
||||
ThTextLogStepModel.query({
|
||||
project: $rootScope.repoName,
|
||||
jobId: $scope.job_id
|
||||
}, function(textLogSteps) {
|
||||
$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];
|
||||
if (step.result !== "success") {
|
||||
$scope.selectedBegin = step.started_line_number;
|
||||
$scope.selectedEnd = step.finished_line_number;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
moveScrollToLineNumber($scope.selectedBegin);
|
||||
});
|
||||
} else {
|
||||
$scope.setLineNumber(allErrors[0].line_number);
|
||||
moveScrollToLineNumber($scope.selectedBegin);
|
||||
}
|
||||
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
|
||||
}, 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;});
|
||||
|
||||
// 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.logPostMessage({
|
||||
lineNumber: step.started_line_number + 1,
|
||||
highlightStart: step.started_line_number + 1,
|
||||
highlightEnd: step.finished_line_number + 1,
|
||||
customStyle: $scope.css
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 getStepFromLine(linenumber) {
|
||||
return $scope.steps.find(function(step) {
|
||||
return (step.started_linenumber <= linenumber &&
|
||||
step.finished_linenumber >= linenumber);
|
||||
});
|
||||
}
|
||||
function setLogListener() {
|
||||
let workerReady = false;
|
||||
|
||||
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];
|
||||
$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 });
|
||||
}
|
||||
$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;
|
||||
});
|
||||
$timeout(updateQuery(e.data));
|
||||
});
|
||||
}
|
||||
|
||||
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,30 +1,32 @@
|
|||
<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}}">
|
||||
<span class="pull-left clearfix text-left">
|
||||
{{::step.order+1}}. {{::step.name}}
|
||||
</span>
|
||||
|
||||
<span ng-init="time=(step.duration !== null ? formatTime(step.started, step.finished) : 'Duration unknown')"
|
||||
ng-mouseover="time=displayTime(step.started, step.finished)"
|
||||
ng-mouseleave="time=(step.duration !== null ? formatTime(step.duration) : 'Duration unknown')"
|
||||
class="pull-right clearfix">
|
||||
{{time}}
|
||||
</span>
|
||||
<div ng-click="setDisplayedStep(step)">
|
||||
<span class="pull-left clearfix text-left">
|
||||
{{::step.order + 1}}. {{::step.name}}
|
||||
</span>
|
||||
|
||||
<span ng-init="time=(step.duration !== null ? formatTime(step.started, step.finished) : 'Duration unknown')"
|
||||
ng-mouseover="time=displayTime(step.started, step.finished)"
|
||||
ng-mouseleave="time=(step.duration !== null ? formatTime(step.duration) : 'Duration unknown')"
|
||||
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}}">
|
||||
|
|
Загрузка…
Ссылка в новой задаче