Merge pull request #4406 from ricmatsui/feat1654-colorize-logs

feat(log-viewer): add ansi color support for logs
pull/5033/head
zees-dev 2021-05-03 09:25:24 +12:00 committed by GitHub
commit daabce2b8f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 143 additions and 14 deletions

View File

@ -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>

View File

@ -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);
}; };

View File

@ -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;

View File

@ -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"
}, },

View File

@ -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"