diff --git a/package.json b/package.json index 0a1eac59b21..0bf3e972a72 100644 --- a/package.json +++ b/package.json @@ -147,6 +147,7 @@ "angular-native-dragdrop": "1.2.2", "angular-route": "1.6.6", "angular-sanitize": "1.6.6", + "ansicolor": "1.1.78", "baron": "^3.0.3", "brace": "^0.10.0", "classnames": "^2.2.6", diff --git a/public/app/core/utils/text.ts b/public/app/core/utils/text.ts index 31a6c4759fd..bcd533ee51b 100644 --- a/public/app/core/utils/text.ts +++ b/public/app/core/utils/text.ts @@ -68,3 +68,7 @@ export function sanitize(unsanitizedString: string): string { return unsanitizedString; } } + +export function hasAnsiCodes(input: string): boolean { + return /\u001b\[\d{1,2}m/.test(input); +} diff --git a/public/app/features/explore/LogMessageAnsi.test.tsx b/public/app/features/explore/LogMessageAnsi.test.tsx new file mode 100644 index 00000000000..6560fd7b7dd --- /dev/null +++ b/public/app/features/explore/LogMessageAnsi.test.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import { LogMessageAnsi } from './LogMessageAnsi'; + +describe('', () => { + it('renders string without ANSI codes', () => { + const wrapper = shallow(); + + expect(wrapper.find('span').exists()).toBe(false); + expect(wrapper.text()).toBe('Lorem ipsum'); + }); + + it('renders string with ANSI codes', () => { + const value = 'Lorem \u001B[31mipsum\u001B[0m et dolor'; + const wrapper = shallow(); + + expect(wrapper.find('span')).toHaveLength(1); + expect(wrapper.find('span').first().prop('style')).toMatchObject(expect.objectContaining({ + color: expect.any(String) + })); + expect(wrapper.find('span').first().text()).toBe('ipsum'); + }); +}); diff --git a/public/app/features/explore/LogMessageAnsi.tsx b/public/app/features/explore/LogMessageAnsi.tsx new file mode 100644 index 00000000000..e4df16fa13c --- /dev/null +++ b/public/app/features/explore/LogMessageAnsi.tsx @@ -0,0 +1,70 @@ +import React, { PureComponent } from 'react'; +import ansicolor from 'ansicolor'; + +interface Style { + [key: string]: string; +} + +interface ParsedChunk { + style: Style; + text: string; +} + +function convertCSSToStyle(css: string): Style { + return css.split(/;\s*/).reduce((accumulated, line) => { + const match = line.match(/([^:\s]+)\s*:\s*(.+)/); + + if (match && match[1] && match[2]) { + const key = match[1].replace(/-(a-z)/g, (_, character) => character.toUpperCase()); + accumulated[key] = match[2]; + } + + return accumulated; + }, {}); +} + +interface Props { + value: string; +} + +interface State { + chunks: ParsedChunk[]; + prevValue: string; +} + +export class LogMessageAnsi extends PureComponent { + state = { + chunks: [], + prevValue: '', + }; + + static getDerivedStateFromProps(props, state) { + if (props.value === state.prevValue) { + return null; + } + + const parsed = ansicolor.parse(props.value); + + return { + chunks: parsed.spans.map((span) => { + return span.css ? + { + style: convertCSSToStyle(span.css), + text: span.text + } : + { text: span.text }; + }), + prevValue: props.value + }; + } + + render() { + const { chunks } = this.state; + + return chunks.map( + (chunk, index) => chunk.style ? + {chunk.text} : + chunk.text + ); + } +} diff --git a/public/app/features/explore/LogRow.tsx b/public/app/features/explore/LogRow.tsx index 66b0e6a69fe..d7ba0f8d12e 100644 --- a/public/app/features/explore/LogRow.tsx +++ b/public/app/features/explore/LogRow.tsx @@ -5,8 +5,9 @@ import classnames from 'classnames'; import { LogRowModel, LogLabelStatsModel, LogsParser, calculateFieldStats, getParser } from 'app/core/logs_model'; import { LogLabels } from './LogLabels'; -import { findHighlightChunksInText } from 'app/core/utils/text'; +import { findHighlightChunksInText, hasAnsiCodes } from 'app/core/utils/text'; import { LogLabelStats } from './LogLabelStats'; +import { LogMessageAnsi } from './LogMessageAnsi'; interface Props { highlighterExpressions?: string[]; @@ -135,6 +136,8 @@ export class LogRow extends PureComponent { const highlightClassName = classnames('logs-row__match-highlight', { 'logs-row__match-highlight--preview': previewHighlights, }); + const containsAnsiCodes = hasAnsiCodes(row.entry); + return (
{showDuplicates && ( @@ -157,16 +160,19 @@ export class LogRow extends PureComponent {
)}
- {parsed && ( - - )} - {!parsed && + {containsAnsiCodes && } + {!containsAnsiCodes && + parsed && ( + + )} + {!containsAnsiCodes && + !parsed && needsHighlighter && ( { highlightClassName={highlightClassName} /> )} - {!parsed && !needsHighlighter && row.entry} + {!containsAnsiCodes && !parsed && !needsHighlighter && row.entry} {showFieldStats && (