From e708a0109f23fbc9373880a04dae5e6b05119f9b Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 5 Dec 2021 18:38:44 +0100 Subject: [PATCH] core-components: refactor out LogViewer search Signed-off-by: Patrik Oldsberg --- .../src/components/LogViewer/LogViewer.tsx | 77 +++----------- .../LogViewer/LogViewerControls.tsx | 25 ++--- .../LogViewer/useLogViewerSearch.tsx | 100 ++++++++++++++++++ 3 files changed, 121 insertions(+), 81 deletions(-) create mode 100644 packages/core-components/src/components/LogViewer/useLogViewerSearch.tsx diff --git a/packages/core-components/src/components/LogViewer/LogViewer.tsx b/packages/core-components/src/components/LogViewer/LogViewer.tsx index 24f10ca5f6..2ce1e5f04f 100644 --- a/packages/core-components/src/components/LogViewer/LogViewer.tsx +++ b/packages/core-components/src/components/LogViewer/LogViewer.tsx @@ -17,102 +17,49 @@ import React, { useEffect, useMemo, useRef, useState } from 'react'; import AutoSizer from 'react-virtualized-auto-sizer'; import { FixedSizeList } from 'react-window'; -import { AnsiLine, AnsiProcessor } from './AnsiProcessor'; +import { AnsiProcessor } from './AnsiProcessor'; import { HEADER_SIZE, useStyles } from './styles'; import clsx from 'clsx'; import { LogLine } from './LogLine'; import { LogViewerControls } from './LogViewerControls'; -import { useToggle } from 'react-use'; +import { useLogViewerSearch } from './useLogViewerSearch'; export interface LogViewerProps { text: string; } -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, - }; -} - export function LogViewer(props: LogViewerProps) { 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'); // The processor keeps state that optimizes appending to the text const processor = useMemo(() => new AnsiProcessor(), []); const lines = processor.process(props.text); - const filter = useMemo( - () => applySearchFilter(lines, searchText), - [lines, searchText], - ); - - const searchResult = filter.results?.[resultIndex ?? 0]; - const searchResultLine = searchResult?.lineNumber; - - const displayLines = shouldFilter ? filter.lines : lines; + const search = useLogViewerSearch(lines); useEffect(() => { - if (searchResultLine !== undefined && listRef.current) { - listRef.current.scrollToItem(searchResultLine - 1, 'center'); + if (search.resultLine !== undefined && listRef.current) { + listRef.current.scrollToItem(search.resultLine - 1, 'center'); } - }, [searchResultLine]); + }, [search.resultLine]); return ( {({ height, width }) => (
- +
{({ index, style, data }) => { const line = data[index]; @@ -137,10 +84,10 @@ export function LogViewer(props: LogViewerProps) { diff --git a/packages/core-components/src/components/LogViewer/LogViewerControls.tsx b/packages/core-components/src/components/LogViewer/LogViewerControls.tsx index f7c06edd05..6d55e6f76f 100644 --- a/packages/core-components/src/components/LogViewer/LogViewerControls.tsx +++ b/packages/core-components/src/components/LogViewer/LogViewerControls.tsx @@ -21,39 +21,32 @@ import Typography from '@material-ui/core/Typography'; import ChevronLeftIcon from '@material-ui/icons/ChevronLeft'; import ChevronRightIcon from '@material-ui/icons/ChevronRight'; import FilterListIcon from '@material-ui/icons/FilterList'; +import { LogViewerSearch } from './useLogViewerSearch'; -export interface LogViewerControlsProps { - search: string; - onSearchChange: (search: string) => void; - resultIndex: number | undefined; - resultCount: number | undefined; - onResultIndexChange: (index: number) => void; - shouldFilter: boolean; - onToggleShouldFilter: () => void; -} +export interface LogViewerControlsProps extends LogViewerSearch {} export function LogViewerControls(props: LogViewerControlsProps) { - const { resultCount, onResultIndexChange, onToggleShouldFilter } = props; + const { resultCount, setResultIndex, toggleShouldFilter } = props; const resultIndex = props.resultIndex ?? 0; const increment = () => { if (resultCount !== undefined) { const next = resultIndex + 1; - onResultIndexChange(next >= resultCount ? 0 : next); + setResultIndex(next >= resultCount ? 0 : next); } }; const decrement = () => { if (resultCount !== undefined) { const next = resultIndex - 1; - onResultIndexChange(next < 0 ? resultCount - 1 : next); + setResultIndex(next < 0 ? resultCount - 1 : next); } }; const handleKeyPress = (event: React.KeyboardEvent) => { if (event.key === 'Enter') { if (event.metaKey || event.ctrlKey || event.altKey) { - onToggleShouldFilter(); + toggleShouldFilter(); } else if (event.shiftKey) { decrement(); } else { @@ -81,11 +74,11 @@ export function LogViewerControls(props: LogViewerControlsProps) { size="small" variant="standard" placeholder="Search" - value={props.search} + value={props.searchInput} onKeyPress={handleKeyPress} - onChange={e => props.onSearchChange(e.target.value)} + onChange={e => props.setSearchInput(e.target.value)} /> - + {props.shouldFilter ? ( ) : ( diff --git a/packages/core-components/src/components/LogViewer/useLogViewerSearch.tsx b/packages/core-components/src/components/LogViewer/useLogViewerSearch.tsx new file mode 100644 index 0000000000..4e93178c20 --- /dev/null +++ b/packages/core-components/src/components/LogViewer/useLogViewerSearch.tsx @@ -0,0 +1,100 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { useMemo, useState } from 'react'; +import { useToggle } from 'react-use'; +import { AnsiLine } from './AnsiProcessor'; + +export 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, + }; +} + +export interface LogViewerSearch { + lines: AnsiLine[]; + + searchText: string; + searchInput: string; + setSearchInput: (searchInput: string) => void; + + shouldFilter: boolean; + toggleShouldFilter: () => void; + + resultIndex: number | undefined; + resultCount: number | undefined; + setResultIndex: (number: number) => void; + + resultLine: number | undefined; + resultLineIndex: number | undefined; +} + +export function useLogViewerSearch(lines: AnsiLine[]): LogViewerSearch { + const [searchInput, setSearchInput] = useState(''); + const searchText = searchInput.toLocaleLowerCase('en-US'); + + const [resultIndex, setResultIndex] = useState(); + + const [shouldFilter, toggleShouldFilter] = useToggle(false); + + const filter = useMemo( + () => applySearchFilter(lines, searchText), + [lines, searchText], + ); + + const searchResult = filter.results?.[resultIndex ?? 0]; + + return { + lines: shouldFilter ? filter.lines : lines, + searchText, + searchInput, + setSearchInput, + shouldFilter, + toggleShouldFilter, + resultIndex, + resultCount: filter.results?.length, + setResultIndex, + resultLine: searchResult?.lineNumber, + resultLineIndex: searchResult?.lineIndex, + }; +}