-
+
No log line matching the '{{ $ctrl.state.search }}' filter
-
+
diff --git a/app/docker/components/log-viewer/logViewerController.js b/app/docker/components/log-viewer/logViewerController.js
index db8b3054a..9fe0b979c 100644
--- a/app/docker/components/log-viewer/logViewerController.js
+++ b/app/docker/components/log-viewer/logViewerController.js
@@ -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);
};
diff --git a/app/docker/helpers/logHelper.js b/app/docker/helpers/logHelper.js
index 052a69843..e53d9046d 100644
--- a/app/docker/helpers/logHelper.js
+++ b/app/docker/helpers/logHelper.js
@@ -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;
diff --git a/package.json b/package.json
index 1c5572bb8..f84a16f8c 100644
--- a/package.json
+++ b/package.json
@@ -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"
},
diff --git a/yarn.lock b/yarn.lock
index 4c246b814..2f6daba4d 100644
--- a/yarn.lock
+++ b/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"