core-components: refactor out LogViewer search
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -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<FixedSizeList | null>(null);
|
||||
const [selectedLine, setSelectedLine] = useState<number>();
|
||||
const [resultIndex, setResultIndex] = useState<number | undefined>();
|
||||
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 (
|
||||
<AutoSizer>
|
||||
{({ height, width }) => (
|
||||
<div style={{ width, height }} className={classes.root}>
|
||||
<div className={classes.header}>
|
||||
<LogViewerControls
|
||||
search={searchInput}
|
||||
onSearchChange={setSearchInput}
|
||||
resultIndex={resultIndex}
|
||||
resultCount={filter.results?.length}
|
||||
onResultIndexChange={setResultIndex}
|
||||
shouldFilter={shouldFilter}
|
||||
onToggleShouldFilter={toggleShouldFilter}
|
||||
/>
|
||||
<LogViewerControls {...search} />
|
||||
</div>
|
||||
<FixedSizeList
|
||||
ref={listRef}
|
||||
className={classes.log}
|
||||
height={height - HEADER_SIZE}
|
||||
width={width}
|
||||
itemData={displayLines}
|
||||
itemData={search.lines}
|
||||
itemSize={20}
|
||||
itemCount={displayLines.length}
|
||||
itemCount={search.lines.length}
|
||||
>
|
||||
{({ index, style, data }) => {
|
||||
const line = data[index];
|
||||
@@ -137,10 +84,10 @@ export function LogViewer(props: LogViewerProps) {
|
||||
<LogLine
|
||||
line={line}
|
||||
classes={classes}
|
||||
searchText={searchText}
|
||||
searchText={search.searchText}
|
||||
highlightResultIndex={
|
||||
searchResultLine === lineNumber
|
||||
? searchResult!.lineIndex
|
||||
search.resultLine === lineNumber
|
||||
? search.resultLineIndex
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -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<HTMLInputElement>) => {
|
||||
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)}
|
||||
/>
|
||||
<IconButton size="small" onClick={onToggleShouldFilter}>
|
||||
<IconButton size="small" onClick={toggleShouldFilter}>
|
||||
{props.shouldFilter ? (
|
||||
<FilterListIcon color="primary" />
|
||||
) : (
|
||||
|
||||
@@ -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<number | undefined>();
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user