From 8884af06e8f0b4e7b077610aef0c0ade6116b71e Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 4 Dec 2021 18:58:51 +0100 Subject: [PATCH] core-components: highlight search matches in LogViewer Signed-off-by: Patrik Oldsberg --- .../src/components/LogViewer/AnsiProcessor.ts | 10 +- .../src/components/LogViewer/LogViewer.tsx | 166 +++++++++++++++--- 2 files changed, 148 insertions(+), 28 deletions(-) diff --git a/packages/core-components/src/components/LogViewer/AnsiProcessor.ts b/packages/core-components/src/components/LogViewer/AnsiProcessor.ts index a33d0db450..5c4bf58865 100644 --- a/packages/core-components/src/components/LogViewer/AnsiProcessor.ts +++ b/packages/core-components/src/components/LogViewer/AnsiProcessor.ts @@ -88,7 +88,10 @@ export class AnsiLine { readonly lineNumber: number = 1, readonly chunks: AnsiChunk[] = [], ) { - this.text = chunks.map(c => c.text).join(''); + this.text = chunks + .map(c => c.text) + .join('') + .toLocaleLowerCase('en-US'); } lastChunk(): AnsiChunk | undefined { @@ -98,7 +101,10 @@ export class AnsiLine { replaceLastChunk(newChunks?: AnsiChunk[]) { if (newChunks) { this.chunks.splice(this.chunks.length - 1, 1, ...newChunks); - this.text = this.chunks.map(c => c.text).join(''); + this.text = this.chunks + .map(c => c.text) + .join('') + .toLocaleLowerCase('en-US'); } } } diff --git a/packages/core-components/src/components/LogViewer/LogViewer.tsx b/packages/core-components/src/components/LogViewer/LogViewer.tsx index 1adcb6df7e..c1a5d06b34 100644 --- a/packages/core-components/src/components/LogViewer/LogViewer.tsx +++ b/packages/core-components/src/components/LogViewer/LogViewer.tsx @@ -14,11 +14,11 @@ * limitations under the License. */ -import { makeStyles } from '@material-ui/core/styles'; +import { alpha, makeStyles } from '@material-ui/core/styles'; import React, { useMemo, useState } from 'react'; import AutoSizer from 'react-virtualized-auto-sizer'; import { FixedSizeList } from 'react-window'; -import { AnsiProcessor } from './AnsiProcessor'; +import { AnsiChunk, AnsiLine, AnsiProcessor } from './AnsiProcessor'; import startCase from 'lodash/startCase'; import * as colors from '@material-ui/core/colors'; import clsx from 'clsx'; @@ -85,6 +85,9 @@ const useStyles = makeStyles(theme => ({ marginRight: theme.spacing(1), cursor: 'pointer', }, + textHighlight: { + background: alpha(theme.palette.primary.main, 0.3), + }, modifierBold: { fontWeight: theme.typography.fontWeightBold, }, @@ -122,31 +125,31 @@ const useStyles = makeStyles(theme => ({ color: colors.grey[500], }, modifierBackgroundBlack: { - color: colors.common.black, + background: colors.common.black, }, modifierBackgroundRed: { - color: colors.red[500], + background: colors.red[500], }, modifierBackgroundGreen: { - color: colors.green[500], + background: colors.green[500], }, modifierBackgroundYellow: { - color: colors.yellow[500], + background: colors.yellow[500], }, modifierBackgroundBlue: { - color: colors.blue[500], + background: colors.blue[500], }, modifierBackgroundMagenta: { - color: colors.purple[500], + background: colors.purple[500], }, modifierBackgroundCyan: { - color: colors.cyan[500], + background: colors.cyan[500], }, modifierBackgroundWhite: { - color: colors.common.white, + background: colors.common.white, }, modifierBackgroundGrey: { - color: colors.grey[500], + background: colors.grey[500], }, })); @@ -179,22 +182,135 @@ function getModifierClasses( return classNames.length > 0 ? classNames.join(' ') : undefined; } +export function LogLine({ + line, + classes, + searchText, +}: { + line: AnsiLine; + classes: ReturnType; + searchText: string; +}) { + let searchResults: Array<{ start: number; end: number }> | undefined = + undefined; + if (searchText && line.text.includes(searchText)) { + searchResults = []; + let offset = 0; + for (;;) { + const start = line.text.indexOf(searchText, offset); + if (start === -1) { + break; + } + const end = start + searchText.length; + searchResults.push({ start, end }); + offset = end; + } + } + + const output = new Array(line.chunks.length); + + let key = 0; + let chunkOffset = 0; + let nextResult = searchResults?.shift(); + for (const { text, modifiers } of line.chunks) { + if (!nextResult || chunkOffset + text.length < nextResult.start) { + output.push( + + {text} + , + ); + chunkOffset += text.length; + continue; + } + + let localOffset = 0; + while (nextResult) { + let localStart = nextResult.start - chunkOffset; + if (localStart < 0) { + localStart = 0; + } + const localEnd = nextResult.end - chunkOffset; + const beforeMatch = text.slice(localOffset, localStart); + const match = text.slice(localStart, localEnd); + + if (beforeMatch) { + output.push( + + {beforeMatch} + , + ); + } + output.push( + + {match} + , + ); + + localOffset = localStart + match.length; + + if (match.length === searchText.length) { + nextResult = searchResults?.shift(); + } else { + break; + } + } + + if (localOffset < text.length) { + output.push( + + {text.slice(localOffset)} + , + ); + } + + chunkOffset += text.length; + } + return <>{output}; +} + export function LogViewer(props: LogViewerProps) { const { noLineNumbers } = props; const classes = useStyles(); const [selectedLine, setSelectedLine] = useState(); - const [filter, setFilter] = useState(''); + const [searchInput, setSearchInput] = useState(''); + const searchText = searchInput.toLocaleLowerCase('en-US'); // The processor keeps state that optimizes appending to the text const processor = useMemo(() => new AnsiProcessor(), []); const lines = processor.process(props.text); const filteredLines = useMemo(() => { - if (!filter) { + if (!searchText) { return lines; } - return lines.filter(line => line.text.includes(filter)); - }, [lines, filter]); + const matchingLines = []; + const searchResults = []; + for (const line of lines) { + if (line.text.includes(searchText)) { + matchingLines.push(line); + + const lineResults = []; + let offset = 0; + for (;;) { + const start = line.text.indexOf(searchText, offset); + if (start === -1) { + break; + } + const end = start + searchText.length; + lineResults.push({ start, end }); + offset = end; + } + searchResults.push(lineResults); + } + } + return lines.filter(line => line.text.includes(searchText)); + }, [lines, searchText]); return ( @@ -205,8 +321,8 @@ export function LogViewer(props: LogViewerProps) { size="small" variant="standard" placeholder="Search" - value={filter} - onChange={e => setFilter(e.target.value)} + value={searchInput} + onChange={e => setSearchInput(e.target.value)} /> {({ index, style, data }) => { - const { chunks, lineNumber } = data[index]; + const line = data[index]; + const { lineNumber } = line; return (
)} - {chunks.map(({ text, modifiers }, i) => ( - - {text} - - ))} +
); }}