From 8e246c203cb63d619597fb0c2dc5c8b1b4d806cf Mon Sep 17 00:00:00 2001 From: Ricardo Matsui Date: Sat, 24 Oct 2020 00:34:40 -0700 Subject: [PATCH 1/3] feat(log-viewer): add ansi color support for logs --- .../components/log-viewer/logViewer.html | 2 +- app/docker/helpers/logHelper.js | 131 ++++++++++++++++-- package.json | 2 + yarn.lock | 10 ++ 4 files changed, 135 insertions(+), 10 deletions(-) diff --git a/app/docker/components/log-viewer/logViewer.html b/app/docker/components/log-viewer/logViewer.html index ac9095ebc..913406f4a 100644 --- a/app/docker/components/log-viewer/logViewer.html +++ b/app/docker/components/log-viewer/logViewer.html @@ -96,7 +96,7 @@
-      

{{ line }}

+

{{ span.text }}

No log line matching the '{{ $ctrl.state.search }}' filter

No logs available

diff --git a/app/docker/helpers/logHelper.js b/app/docker/helpers/logHelper.js index 052a69843..b59b87705 100644 --- a/app/docker/helpers/logHelper.js +++ b/app/docker/helpers/logHelper.js @@ -1,20 +1,133 @@ +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 }); + } + } + } + + return formattedLogs; }; return helper; diff --git a/package.json b/package.json index fb5a2ec7d..f02e2a3fb 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,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", @@ -88,6 +89,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 c1d064749..d68e3f0cd 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" @@ -11293,6 +11298,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= + xmlbuilder@0.4.2: version "0.4.2" resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-0.4.2.tgz#1776d65f3fdbad470a08d8604cdeb1c4e540ff83" From ae3809cefdadeef1f3cd5913fb53f26c9b775e84 Mon Sep 17 00:00:00 2001 From: Ricardo Matsui Date: Mon, 26 Oct 2020 16:35:54 -0700 Subject: [PATCH 2/3] fix(log-viewer): fix formatting last line without newline --- app/docker/helpers/logHelper.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/docker/helpers/logHelper.js b/app/docker/helpers/logHelper.js index b59b87705..e53d9046d 100644 --- a/app/docker/helpers/logHelper.js +++ b/app/docker/helpers/logHelper.js @@ -127,6 +127,10 @@ angular.module('portainer.docker').factory('LogHelper', [ } } + if (line) { + formattedLogs.push({ line, spans }); + } + return formattedLogs; }; From 3f9ff8460f2e855356b18b6a4847f305a1e9b0e0 Mon Sep 17 00:00:00 2001 From: Ricardo Matsui Date: Wed, 28 Oct 2020 23:43:53 -0700 Subject: [PATCH 3/3] fix(log-viewer): fix copy logs and log status --- app/docker/components/log-viewer/logViewer.html | 6 +++--- app/docker/components/log-viewer/logViewerController.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/docker/components/log-viewer/logViewer.html b/app/docker/components/log-viewer/logViewer.html index 913406f4a..f5e76728f 100644 --- a/app/docker/components/log-viewer/logViewer.html +++ b/app/docker/components/log-viewer/logViewer.html @@ -70,13 +70,13 @@
diff --git a/app/docker/components/log-viewer/logViewerController.js b/app/docker/components/log-viewer/logViewerController.js index d55fe555e..40fa3aed8 100644 --- a/app/docker/components/log-viewer/logViewerController.js +++ b/app/docker/components/log-viewer/logViewerController.js @@ -20,7 +20,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); };