mirror of https://github.com/portainer/portainer
Merge pull request #4406 from ricmatsui/feat1654-colorize-logs
feat(log-viewer): add ansi color support for logspull/5033/head
commit
daabce2b8f
|
@ -71,13 +71,13 @@
|
||||||
<button
|
<button
|
||||||
class="btn btn-primary btn-sm"
|
class="btn btn-primary btn-sm"
|
||||||
ng-click="$ctrl.copy()"
|
ng-click="$ctrl.copy()"
|
||||||
ng-disabled="($ctrl.state.filteredLogs.length === 1 && !$ctrl.state.filteredLogs[0]) || !$ctrl.state.filteredLogs.length"
|
ng-disabled="($ctrl.state.filteredLogs.length === 1 && !$ctrl.state.filteredLogs[0].line) || !$ctrl.state.filteredLogs.length"
|
||||||
><i class="fa fa-copy space-right" aria-hidden="true"></i>Copy</button
|
><i class="fa fa-copy space-right" aria-hidden="true"></i>Copy</button
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="btn btn-primary btn-sm"
|
class="btn btn-primary btn-sm"
|
||||||
ng-click="$ctrl.copySelection()"
|
ng-click="$ctrl.copySelection()"
|
||||||
ng-disabled="($ctrl.state.filteredLogs.length === 1 && !$ctrl.state.filteredLogs[0]) || !$ctrl.state.filteredLogs.length || !$ctrl.state.selectedLines.length"
|
ng-disabled="($ctrl.state.filteredLogs.length === 1 && !$ctrl.state.filteredLogs[0].line) || !$ctrl.state.filteredLogs.length || !$ctrl.state.selectedLines.length"
|
||||||
><i class="fa fa-copy space-right" aria-hidden="true"></i>Copy selected lines</button
|
><i class="fa fa-copy space-right" aria-hidden="true"></i>Copy selected lines</button
|
||||||
>
|
>
|
||||||
<button class="btn btn-primary btn-sm" ng-click="$ctrl.clearSelection()" ng-disabled="$ctrl.state.selectedLines.length === 0"
|
<button class="btn btn-primary btn-sm" ng-click="$ctrl.clearSelection()" ng-disabled="$ctrl.state.selectedLines.length === 0"
|
||||||
|
@ -97,9 +97,9 @@
|
||||||
<div class="row" style="height: 54%;">
|
<div class="row" style="height: 54%;">
|
||||||
<div class="col-sm-12" style="height: 100%;">
|
<div class="col-sm-12" style="height: 100%;">
|
||||||
<pre ng-class="{ wrap_lines: $ctrl.state.wrapLines }" class="log_viewer" scroll-glue="$ctrl.state.autoScroll" force-glue>
|
<pre ng-class="{ wrap_lines: $ctrl.state.wrapLines }" class="log_viewer" scroll-glue="$ctrl.state.autoScroll" force-glue>
|
||||||
<div ng-repeat="line in $ctrl.state.filteredLogs = ($ctrl.data | filter:$ctrl.state.search) track by $index" class="line" ng-if="line"><p class="inner_line" ng-click="$ctrl.selectLine(line)" ng-class="{ 'line_selected': $ctrl.state.selectedLines.indexOf(line) > -1 }">{{ line }}</p></div>
|
<div ng-repeat="log in $ctrl.state.filteredLogs = ($ctrl.data | filter:{ 'line': $ctrl.state.search }) track by $index" class="line" ng-if="log.line"><p class="inner_line" ng-click="$ctrl.selectLine(log.line)" ng-class="{ 'line_selected': $ctrl.state.selectedLines.indexOf(log.line) > -1 }"><span ng-repeat="span in log.spans" ng-style="{ 'color': span.foregroundColor, 'background-color': span.backgroundColor }">{{ span.text }}</span></p></div>
|
||||||
<div ng-if="!$ctrl.state.filteredLogs.length" class="line"><p class="inner_line">No log line matching the '{{ $ctrl.state.search }}' filter</p></div>
|
<div ng-if="!$ctrl.state.filteredLogs.length" class="line"><p class="inner_line">No log line matching the '{{ $ctrl.state.search }}' filter</p></div>
|
||||||
<div ng-if="$ctrl.state.filteredLogs.length === 1 && !$ctrl.state.filteredLogs[0]" class="line"><p class="inner_line">No logs available</p></div>
|
<div ng-if="$ctrl.state.filteredLogs.length === 1 && !$ctrl.state.filteredLogs[0].line" class="line"><p class="inner_line">No logs available</p></div>
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -23,7 +23,7 @@ angular.module('portainer.docker').controller('LogViewerController', [
|
||||||
};
|
};
|
||||||
|
|
||||||
this.copy = function () {
|
this.copy = function () {
|
||||||
clipboard.copyText(this.state.filteredLogs);
|
clipboard.copyText(this.state.filteredLogs.map((log) => log.line));
|
||||||
$('#refreshRateChange').show();
|
$('#refreshRateChange').show();
|
||||||
$('#refreshRateChange').fadeOut(2000);
|
$('#refreshRateChange').fadeOut(2000);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,20 +1,137 @@
|
||||||
|
import tokenize from '@nxmix/tokenize-ansi';
|
||||||
|
import x256 from 'x256';
|
||||||
|
|
||||||
|
const FOREGROUND_COLORS_BY_ANSI = {
|
||||||
|
black: x256.colors[0],
|
||||||
|
red: x256.colors[1],
|
||||||
|
green: x256.colors[2],
|
||||||
|
yellow: x256.colors[3],
|
||||||
|
blue: x256.colors[4],
|
||||||
|
magenta: x256.colors[5],
|
||||||
|
cyan: x256.colors[6],
|
||||||
|
white: x256.colors[7],
|
||||||
|
brightBlack: x256.colors[8],
|
||||||
|
brightRed: x256.colors[9],
|
||||||
|
brightGreen: x256.colors[10],
|
||||||
|
brightYellow: x256.colors[11],
|
||||||
|
brightBlue: x256.colors[12],
|
||||||
|
brightMagenta: x256.colors[13],
|
||||||
|
brightCyan: x256.colors[14],
|
||||||
|
brightWhite: x256.colors[15],
|
||||||
|
};
|
||||||
|
|
||||||
|
const BACKGROUND_COLORS_BY_ANSI = {
|
||||||
|
bgBlack: x256.colors[0],
|
||||||
|
bgRed: x256.colors[1],
|
||||||
|
bgGreen: x256.colors[2],
|
||||||
|
bgYellow: x256.colors[3],
|
||||||
|
bgBlue: x256.colors[4],
|
||||||
|
bgMagenta: x256.colors[5],
|
||||||
|
bgCyan: x256.colors[6],
|
||||||
|
bgWhite: x256.colors[7],
|
||||||
|
bgBrightBlack: x256.colors[8],
|
||||||
|
bgBrightRed: x256.colors[9],
|
||||||
|
bgBrightGreen: x256.colors[10],
|
||||||
|
bgBrightYellow: x256.colors[11],
|
||||||
|
bgBrightBlue: x256.colors[12],
|
||||||
|
bgBrightMagenta: x256.colors[13],
|
||||||
|
bgBrightCyan: x256.colors[14],
|
||||||
|
bgBrightWhite: x256.colors[15],
|
||||||
|
};
|
||||||
|
|
||||||
angular.module('portainer.docker').factory('LogHelper', [
|
angular.module('portainer.docker').factory('LogHelper', [
|
||||||
function LogHelperFactory() {
|
function LogHelperFactory() {
|
||||||
'use strict';
|
'use strict';
|
||||||
var helper = {};
|
var helper = {};
|
||||||
|
|
||||||
// Return an array with each line being an entry.
|
function stripHeaders(logs) {
|
||||||
// It will also remove any ANSI code related character sequences.
|
logs = logs.substring(8);
|
||||||
// If the skipHeaders param is specified, it will strip the 8 first characters of each line.
|
logs = logs.replace(/\n(.{8})/g, '\n\r');
|
||||||
helper.formatLogs = function (logs, skipHeaders) {
|
|
||||||
logs = logs.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '');
|
|
||||||
|
|
||||||
if (skipHeaders) {
|
return logs;
|
||||||
logs = logs.substring(8);
|
}
|
||||||
logs = logs.replace(/\n(.{8})/g, '\n\r');
|
|
||||||
|
function stripEscapeCodes(logs) {
|
||||||
|
return logs.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function cssColorFromRgb(rgb) {
|
||||||
|
const [r, g, b] = rgb;
|
||||||
|
|
||||||
|
return `rgb(${r}, ${g}, ${b})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extendedColorForToken(token) {
|
||||||
|
const colorMode = token[1];
|
||||||
|
|
||||||
|
if (colorMode === 2) {
|
||||||
|
return cssColorFromRgb(token.slice(2));
|
||||||
}
|
}
|
||||||
|
|
||||||
return logs.split('\n');
|
if (colorMode === 5 && x256.colors[token[2]]) {
|
||||||
|
return cssColorFromRgb(x256.colors[token[2]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return an array with each log including a line and styled spans for each entry.
|
||||||
|
// If the skipHeaders param is specified, it will strip the 8 first characters of each line.
|
||||||
|
helper.formatLogs = function (logs, skipHeaders) {
|
||||||
|
if (skipHeaders) {
|
||||||
|
logs = stripHeaders(logs);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokens = tokenize(logs);
|
||||||
|
const formattedLogs = [];
|
||||||
|
|
||||||
|
let foregroundColor = null;
|
||||||
|
let backgroundColor = null;
|
||||||
|
let line = '';
|
||||||
|
let spans = [];
|
||||||
|
|
||||||
|
for (const token of tokens) {
|
||||||
|
const type = token[0];
|
||||||
|
|
||||||
|
if (FOREGROUND_COLORS_BY_ANSI[type]) {
|
||||||
|
foregroundColor = cssColorFromRgb(FOREGROUND_COLORS_BY_ANSI[type]);
|
||||||
|
} else if (type === 'moreColor') {
|
||||||
|
foregroundColor = extendedColorForToken(token);
|
||||||
|
} else if (type === 'fgDefault') {
|
||||||
|
foregroundColor = null;
|
||||||
|
} else if (BACKGROUND_COLORS_BY_ANSI[type]) {
|
||||||
|
backgroundColor = cssColorFromRgb(BACKGROUND_COLORS_BY_ANSI[type]);
|
||||||
|
} else if (type === 'bgMoreColor') {
|
||||||
|
backgroundColor = extendedColorForToken(token);
|
||||||
|
} else if (type === 'bgDefault') {
|
||||||
|
backgroundColor = null;
|
||||||
|
} else if (type === 'reset') {
|
||||||
|
foregroundColor = null;
|
||||||
|
backgroundColor = null;
|
||||||
|
} else if (type === 'text') {
|
||||||
|
const tokenLines = token[1].split('\n');
|
||||||
|
|
||||||
|
for (let i = 0; i < tokenLines.length; i++) {
|
||||||
|
if (i !== 0) {
|
||||||
|
formattedLogs.push({ line, spans });
|
||||||
|
|
||||||
|
line = '';
|
||||||
|
spans = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = stripEscapeCodes(tokenLines[i]);
|
||||||
|
|
||||||
|
line += text;
|
||||||
|
spans.push({ foregroundColor, backgroundColor, text });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line) {
|
||||||
|
formattedLogs.push({ line, spans });
|
||||||
|
}
|
||||||
|
|
||||||
|
return formattedLogs;
|
||||||
};
|
};
|
||||||
|
|
||||||
return helper;
|
return helper;
|
||||||
|
|
|
@ -55,6 +55,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/polyfill": "^7.2.5",
|
"@babel/polyfill": "^7.2.5",
|
||||||
"@fortawesome/fontawesome-free": "^5.11.2",
|
"@fortawesome/fontawesome-free": "^5.11.2",
|
||||||
|
"@nxmix/tokenize-ansi": "^3.0.0",
|
||||||
"@uirouter/angularjs": "1.0.11",
|
"@uirouter/angularjs": "1.0.11",
|
||||||
"angular": "1.8.0",
|
"angular": "1.8.0",
|
||||||
"angular-clipboard": "^1.6.2",
|
"angular-clipboard": "^1.6.2",
|
||||||
|
@ -97,6 +98,7 @@
|
||||||
"toastr": "^2.1.4",
|
"toastr": "^2.1.4",
|
||||||
"ui-select": "^0.19.8",
|
"ui-select": "^0.19.8",
|
||||||
"uuid": "^3.3.2",
|
"uuid": "^3.3.2",
|
||||||
|
"x256": "^0.0.2",
|
||||||
"xterm": "^3.8.0",
|
"xterm": "^3.8.0",
|
||||||
"yaml": "^1.10.0"
|
"yaml": "^1.10.0"
|
||||||
},
|
},
|
||||||
|
|
10
yarn.lock
10
yarn.lock
|
@ -875,6 +875,11 @@
|
||||||
"@nodelib/fs.scandir" "2.1.3"
|
"@nodelib/fs.scandir" "2.1.3"
|
||||||
fastq "^1.6.0"
|
fastq "^1.6.0"
|
||||||
|
|
||||||
|
"@nxmix/tokenize-ansi@^3.0.0":
|
||||||
|
version "3.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@nxmix/tokenize-ansi/-/tokenize-ansi-3.0.0.tgz#9a7bdae1a0cf5317d5b9176038c026e374e62a58"
|
||||||
|
integrity sha512-37QMpFIiQ6J31tavjMFCuWs3YIqXIDCuGvPiDVofFqvgXq6vM+8LqU4sqibsvb9JX/1SIeDp+SedOqpq2qc7TA==
|
||||||
|
|
||||||
"@samverschueren/stream-to-observable@^0.3.0":
|
"@samverschueren/stream-to-observable@^0.3.0":
|
||||||
version "0.3.1"
|
version "0.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.1.tgz#a21117b19ee9be70c379ec1877537ef2e1c63301"
|
resolved "https://registry.yarnpkg.com/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.1.tgz#a21117b19ee9be70c379ec1877537ef2e1c63301"
|
||||||
|
@ -10977,6 +10982,11 @@ ws@^6.2.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
async-limiter "~1.0.0"
|
async-limiter "~1.0.0"
|
||||||
|
|
||||||
|
x256@^0.0.2:
|
||||||
|
version "0.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/x256/-/x256-0.0.2.tgz#c9af18876f7a175801d564fe70ad9e8317784934"
|
||||||
|
integrity sha1-ya8Yh296F1gB1WT+cK2egxd4STQ=
|
||||||
|
|
||||||
xtend@^4.0.0, xtend@~4.0.1:
|
xtend@^4.0.0, xtend@~4.0.1:
|
||||||
version "4.0.2"
|
version "4.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
|
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
|
||||||
|
|
Loading…
Reference in New Issue