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 @@
-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"+{{ line }}
{{ span.text }}
No log line matching the '{{ $ctrl.state.search }}' filter
No logs available