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
|
||||
class="btn btn-primary btn-sm"
|
||||
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
|
||||
>
|
||||
<button
|
||||
class="btn btn-primary btn-sm"
|
||||
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
|
||||
>
|
||||
<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="col-sm-12" style="height: 100%;">
|
||||
<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 === 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>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -23,7 +23,7 @@ angular.module('portainer.docker').controller('LogViewerController', [
|
|||
};
|
||||
|
||||
this.copy = function () {
|
||||
clipboard.copyText(this.state.filteredLogs);
|
||||
clipboard.copyText(this.state.filteredLogs.map((log) => log.line));
|
||||
$('#refreshRateChange').show();
|
||||
$('#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', [
|
||||
function LogHelperFactory() {
|
||||
'use strict';
|
||||
var helper = {};
|
||||
|
||||
// Return an array with each line being an entry.
|
||||
// It will also remove any ANSI code related character sequences.
|
||||
// If the skipHeaders param is specified, it will strip the 8 first characters of each line.
|
||||
helper.formatLogs = function (logs, skipHeaders) {
|
||||
logs = logs.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '');
|
||||
function stripHeaders(logs) {
|
||||
logs = logs.substring(8);
|
||||
logs = logs.replace(/\n(.{8})/g, '\n\r');
|
||||
|
||||
if (skipHeaders) {
|
||||
logs = logs.substring(8);
|
||||
logs = logs.replace(/\n(.{8})/g, '\n\r');
|
||||
return logs;
|
||||
}
|
||||
|
||||
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;
|
||||
|
|
|
@ -55,6 +55,7 @@
|
|||
"dependencies": {
|
||||
"@babel/polyfill": "^7.2.5",
|
||||
"@fortawesome/fontawesome-free": "^5.11.2",
|
||||
"@nxmix/tokenize-ansi": "^3.0.0",
|
||||
"@uirouter/angularjs": "1.0.11",
|
||||
"angular": "1.8.0",
|
||||
"angular-clipboard": "^1.6.2",
|
||||
|
@ -97,6 +98,7 @@
|
|||
"toastr": "^2.1.4",
|
||||
"ui-select": "^0.19.8",
|
||||
"uuid": "^3.3.2",
|
||||
"x256": "^0.0.2",
|
||||
"xterm": "^3.8.0",
|
||||
"yaml": "^1.10.0"
|
||||
},
|
||||
|
|
10
yarn.lock
10
yarn.lock
|
@ -875,6 +875,11 @@
|
|||
"@nodelib/fs.scandir" "2.1.3"
|
||||
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":
|
||||
version "0.3.1"
|
||||
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:
|
||||
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:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
|
||||
|
|
Loading…
Reference in New Issue