diff --git a/packages/core-components/src/components/LogViewer/LogViewer.tsx b/packages/core-components/src/components/LogViewer/LogViewer.tsx index 1b7b7c2c38..7ff24acda5 100644 --- a/packages/core-components/src/components/LogViewer/LogViewer.tsx +++ b/packages/core-components/src/components/LogViewer/LogViewer.tsx @@ -17,15 +17,20 @@ import React, { useEffect, useMemo, useRef, useState } from 'react'; import AutoSizer from 'react-virtualized-auto-sizer'; import { FixedSizeList } from 'react-window'; -import { AnsiProcessor } from './AnsiProcessor'; +import Typography from '@material-ui/core/Typography'; +import IconButton from '@material-ui/core/IconButton'; +import ChevronLeftIcon from '@material-ui/icons/ChevronLeft'; +import ChevronRightIcon from '@material-ui/icons/ChevronRight'; +import FilterListIcon from '@material-ui/icons/FilterList'; +import { AnsiLine, AnsiProcessor } from './AnsiProcessor'; import { HEADER_SIZE, useStyles } from './styles'; import clsx from 'clsx'; import TextField from '@material-ui/core/TextField'; import { LogLine } from './LogLine'; +import { useToggle } from 'react-use'; export interface LogViewerProps { text: string; - noLineNumbers?: boolean; } export type AnsiColor = @@ -47,11 +52,117 @@ export interface ChunkModifiers { underline?: boolean; } +function applySearchFilter(lines: AnsiLine[], searchText: string) { + if (!searchText) { + return { lines }; + } + + const matchingLines = []; + const searchResults = []; + for (const line of lines) { + if (line.text.includes(searchText)) { + matchingLines.push(line); + + let offset = 0; + let lineResultIndex = 0; + for (;;) { + const start = line.text.indexOf(searchText, offset); + if (start === -1) { + break; + } + searchResults.push({ + lineNumber: line.lineNumber, + lineIndex: lineResultIndex++, + }); + offset = start + searchText.length; + } + } + } + + return { + lines: matchingLines, + results: searchResults, + }; +} + +function LogViewerControls(props: { + search: string; + onSearchChange: (search: string) => void; + resultIndex: number | undefined; + resultCount: number | undefined; + onResultIndexChange: (index: number) => void; + shouldFilter: boolean; + onToggleShouldFilter: () => void; +}) { + const { resultCount, onResultIndexChange, onToggleShouldFilter } = props; + const resultIndex = props.resultIndex ?? 0; + + const increment = () => { + if (resultCount !== undefined) { + const next = resultIndex + 1; + onResultIndexChange(next >= resultCount ? 0 : next); + } + }; + + const decrement = () => { + if (resultCount !== undefined) { + const next = resultIndex - 1; + onResultIndexChange(next < 0 ? resultCount - 1 : next); + } + }; + + const handleKeyPress = (event: React.KeyboardEvent) => { + if (event.key === 'Enter') { + if (event.metaKey || event.ctrlKey || event.altKey) { + onToggleShouldFilter(); + } else if (event.shiftKey) { + decrement(); + } else { + increment(); + } + } + }; + + return ( + <> + {resultCount !== undefined && ( + <> + + + + + {Math.min(resultIndex + 1, resultCount)}/{resultCount} + + + + + + )} + props.onSearchChange(e.target.value)} + /> + + {props.shouldFilter ? ( + + ) : ( + + )} + + + ); +} + export function LogViewer(props: LogViewerProps) { - const { noLineNumbers } = props; - const listRef = useRef(null); const classes = useStyles(); + const listRef = useRef(null); const [selectedLine, setSelectedLine] = useState(); + const [resultIndex, setResultIndex] = useState(); + const [shouldFilter, toggleShouldFilter] = useToggle(false); const [searchInput, setSearchInput] = useState(''); const searchText = searchInput.toLocaleLowerCase('en-US'); @@ -59,53 +170,35 @@ export function LogViewer(props: LogViewerProps) { const processor = useMemo(() => new AnsiProcessor(), []); const lines = processor.process(props.text); - const filteredLines = useMemo(() => { - if (!searchText) { - return lines; - } - const matchingLines = []; - const searchResults = []; - for (const line of lines) { - if (line.text.includes(searchText)) { - matchingLines.push(line); + const filter = useMemo( + () => applySearchFilter(lines, searchText), + [lines, searchText], + ); - 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]); + const searchResult = filter.results?.[resultIndex ?? 0]; + const searchResultLine = searchResult?.lineNumber; - const [foundLine] = filteredLines; - const scrollToLineNumber = foundLine?.lineNumber; + const displayLines = shouldFilter ? filter.lines : lines; useEffect(() => { - if (scrollToLineNumber !== undefined && listRef.current) { - listRef.current.scrollToItem(scrollToLineNumber - 1, 'center'); + if (searchResultLine !== undefined && listRef.current) { + listRef.current.scrollToItem(searchResultLine - 1, 'center'); } - }, [scrollToLineNumber]); + }, [searchResultLine]); return ( {({ height, width }) => (
- setSearchInput(e.target.value)} +
{({ index, style, data }) => { const line = data[index]; @@ -127,18 +220,16 @@ export function LogViewer(props: LogViewerProps) { [classes.lineSelected]: selectedLine === lineNumber, })} > - {!noLineNumbers && ( - setSelectedLine(lineNumber)} - onKeyPress={() => setSelectedLine(lineNumber)} - > - {lineNumber} - - )} + setSelectedLine(lineNumber)} + onKeyPress={() => setSelectedLine(lineNumber)} + > + {lineNumber} +