Skip to content

Commit

Permalink
Merge pull request #7249 from tdesveaux/react-base/log-search/case-in…
Browse files Browse the repository at this point in the history
…sensitive

www/react: Implement case insensitive search in logs as an option
  • Loading branch information
p12tic committed Dec 20, 2023
2 parents 775748d + 6052fe3 commit dce2ec1
Show file tree
Hide file tree
Showing 10 changed files with 210 additions and 41 deletions.
1 change: 1 addition & 0 deletions newsfragments/www-case-insensitive-search.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add support for case insensitive search within the logs.
28 changes: 25 additions & 3 deletions www/react-base/src/components/LogSearchField/LogSearchField.scss
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@

.bb-log-search-field {
position: relative;
width: 270px;
width: 300px;
display: inline-block;
margin-right: 1rem;
margin-right: 0.5rem;
}

.bb-log-search-field-icon {
Expand All @@ -28,6 +28,28 @@
padding: 0.5rem;
}

.bb-log-search-field-options {
position: absolute;
top: 0.4rem;
color: $body-color;
padding: 0;
border: none;
background: none;
}

.bb-log-search-field-options-toggled {
border: rgb(0, 0, 255);
background-color: rgba(0, 0, 255, 0.4);
}
.bb-log-search-field-options-untoggled {
border: none;
background-color: rgba(255, 255, 255, 0);
}

.bb-log-search-field-options-case {
margin-left: 1.75rem;
}

.bb-log-search-field-nav-button {
border-radius: 0.25rem;
border: 0px solid transparent;
Expand All @@ -44,7 +66,7 @@

.bb-log-search-field-text {
width: 100%;
padding: 0.375rem 0.75rem 0.375rem 2rem;
padding: 0.375rem 0.75rem 0.375rem 3rem;
border-radius: 0.25rem;
overflow: visible;
}
36 changes: 32 additions & 4 deletions www/react-base/src/components/LogSearchField/LogSearchField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,27 +18,52 @@
import './LogSearchField.scss'
import {Ref, useState} from "react";
import {FaChevronDown, FaChevronUp, FaSearch} from "react-icons/fa";
import {VscCaseSensitive} from "react-icons/vsc";

export type LogSearchButtonProps = {
currentResult: number;
totalResults: number;
onTextChanged: (text: string) => void;
onSearchInputChanged: (text: string, caseSensitive: boolean) => void;
onPrevClicked: () => void;
onNextClicked: () => void;
inputRef: Ref<HTMLInputElement>;
};

export const LogSearchField = ({currentResult, totalResults,
onTextChanged, onPrevClicked, onNextClicked,
onSearchInputChanged, onPrevClicked, onNextClicked,
inputRef}: LogSearchButtonProps) => {
const [searchText, setSearchText] = useState<string>('');
const [hasFocus, setHasFocus] = useState<boolean>(false);
const [isCaseSensitive, setIsCaseSensitive] = useState<boolean>(true);

const onSearchTextChanged = (text: string) => {
setSearchText(text);
onTextChanged(text);
onSearchInputChanged(text, isCaseSensitive);
};

const onCaseInsensitiveToggled = () => {
const newValue = !isCaseSensitive;
setIsCaseSensitive(newValue);
onSearchInputChanged(searchText, newValue);
}

const optionButtonClass = (optionName: string, isToggled: boolean) => {
return [
'bb-log-search-field-options',
`bb-log-search-field-options-${optionName}`,
`bb-log-search-field-options-${isToggled ? 'toggled' : 'untoggled'}`,
].join(' ');
};
const renderOptions = () => (
<>
<button
className={optionButtonClass('case', isCaseSensitive)}
onClick={onCaseInsensitiveToggled}>
<VscCaseSensitive />
</button>
</>
);

const renderSearchNav = () => (
<div className="bb-log-search-field-nav">
<span className="bb-log-search-field-result-count">{currentResult}/{totalResults}</span>
Expand All @@ -58,16 +83,19 @@ export const LogSearchField = ({currentResult, totalResults,
}
}

const shouldRenderOptionals = (hasFocus || searchText !== '');

return (
<form role="search" className="bb-log-search-field">
<FaSearch className="bb-log-search-field-icon"/>
{shouldRenderOptionals ? renderOptions() : <></>}
<input className="bb-log-search-field-text" type="text" value={searchText}
ref={inputRef}
onFocus={() => setHasFocus(true)} onBlur={() => setHasFocus(false)}
onChange={e => onSearchTextChanged(e.target.value)}
onKeyDown={e => onKeyDown(e)}
placeholder="Search log"/>
{(hasFocus || searchText !== '') ? renderSearchNav() : <></>}
{shouldRenderOptionals ? renderSearchNav() : <></>}
</form>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,14 @@ describe('LogTextManager', () => {
{offset: 90, limit: 10},
]);
expect(manager.totalSearchResultCount).toEqual(100);

manager.setSearchString("aaAAa");

expect(manager.totalSearchResultCount).toEqual(0);

manager.setSearchCaseSensitivity(false);

expect(manager.totalSearchResultCount).toEqual(100);
});
});
});
27 changes: 21 additions & 6 deletions www/react-base/src/components/LogViewer/LogTextManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
parseLogChunk
} from "../../util/LogChunkParsing";
import {
ChunkSearchOptions,
ChunkSearchResults, findNextSearchResult, findPrevSearchResult, findTextInChunk,
overlaySearchResultsOnLine
} from "../../util/LogChunkSearch";
Expand Down Expand Up @@ -111,6 +112,7 @@ export class LogTextManager {

// Current search string or null if no search is being performed at the moment
searchString: string|null = null;
searchOptions: ChunkSearchOptions = {caseInsensitive: false}
// Valid only if searchString !== null. Indices are the same as this.chunks
chunkSearchResults: ChunkSearchResults[] = [];
// Valid only if searchString !== null
Expand Down Expand Up @@ -448,19 +450,33 @@ export class LogTextManager {
this.maybeUpdatePendingRequest(0, 0);
}

setSearchCaseSensitivity(sensitive: boolean) {
const caseInsensitive = !sensitive;
if (this.searchOptions.caseInsensitive === caseInsensitive) {
return;
}
this.searchOptions.caseInsensitive = caseInsensitive;

this.onSearchInputChanged();
}

setSearchString(searchString: string|null) {
if (searchString === this.searchString) {
return;
}
this.searchString = searchString;

this.onSearchInputChanged();
}

private onSearchInputChanged() {
this.currentSearchResultChunkIndex = -1;
this.currentSearchResultIndexInChunk = -1;
this.currentSearchResultIndex = -1;
this.totalSearchResultCount = 0;

if (searchString === null) {
if (this.searchString === null) {
this.chunkSearchResults = [];
this.searchString = null;
this.renderedLinesForSearch = [];
this.onStateChange();
return;
Expand All @@ -470,14 +486,13 @@ export class LogTextManager {
this.searchWasEnabled = true;
this.maybeUpdatePendingRequest(0, 0);
}
this.searchString = searchString;
this.chunkSearchResults = new Array(this.chunks.length);
this.renderedLinesForSearch = [];
this.totalSearchResultCount = 0;
this.currentSearchResultIndex = 0;

for (let i = 0; i < this.chunks.length; ++i) {
const newResult = findTextInChunk(this.chunks[i], this.searchString);
const newResult = findTextInChunk(this.chunks[i], this.searchString, this.searchOptions);
this.chunkSearchResults[i] = newResult;
this.totalSearchResultCount += newResult.results.length;
if (this.currentSearchResultChunkIndex < 0 && newResult.results.length > 0) {
Expand Down Expand Up @@ -537,7 +552,7 @@ export class LogTextManager {
if (this.searchString === null) {
return;
}
const newResult = findTextInChunk(this.chunks[insertIndex], this.searchString!);
const newResult = findTextInChunk(this.chunks[insertIndex], this.searchString!, this.searchOptions);
this.chunkSearchResults.splice(insertIndex, 0, newResult);
this.totalSearchResultCount += newResult.results.length;

Expand All @@ -558,7 +573,7 @@ export class LogTextManager {
return;
}
const prevResult = this.chunkSearchResults[mergeIndex];
const newResult = findTextInChunk(this.chunks[mergeIndex], this.searchString!);
const newResult = findTextInChunk(this.chunks[mergeIndex], this.searchString!, this.searchOptions);
const additionalResultsCount = newResult.results.length - prevResult!.results.length;

this.chunkSearchResults[mergeIndex] = newResult;
Expand Down
5 changes: 3 additions & 2 deletions www/react-base/src/components/LogViewer/LogViewerText.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,9 @@ export const LogViewerText = observer(({log, downloadInitiateOverscanRowCount, d
return [overscanStartIndex, overscanStopIndex, visibleStartIndex, visibleStopIndex];
}

const onSearchTextChanged = (text: string) => {
const onSearchInputChanged = (text: string, caseSensitive: boolean) => {
manager.setSearchString(text === '' ? null : text);
manager.setSearchCaseSensitivity(caseSensitive);
}

const listRef = useRef<FixedSizeList<any>>(null);
Expand Down Expand Up @@ -128,7 +129,7 @@ export const LogViewerText = observer(({log, downloadInitiateOverscanRowCount, d
<div>
<LogSearchField currentResult={manager.currentSearchResultIndex + 1}
totalResults={Math.max(manager.totalSearchResultCount, 0)}
onTextChanged={onSearchTextChanged}
onSearchInputChanged={onSearchInputChanged}
onPrevClicked={() => manager.setPrevSearchResult()}
onNextClicked={() => manager.setNextSearchResult()}
inputRef={searchInputRef}/>
Expand Down
20 changes: 16 additions & 4 deletions www/react-base/src/util/LogChunkSearch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ import {
ChunkSearchResults, findNextSearchResult, findPrevSearchResult,
findTextInChunkRaw,
overlaySearchResultsOnLine,
resultsListToLineIndexMap
resultsListToLineIndexMap,
getMatcher,
} from "./LogChunkSearch";

describe('LogChunkSearch', () => {
Expand Down Expand Up @@ -115,7 +116,7 @@ describe('LogChunkSearch', () => {

describe('findTextInChunkRaw', () => {
it('no escapes', () => {
expect(findTextInChunkRaw(parseLogChunk(20, 'oaaa\nboaaabaaa\no\noaaab\n', 's'), 'aaa'))
expect(findTextInChunkRaw(parseLogChunk(20, 'oaaa\nboaaabaaa\no\noaaab\n', 's'), getMatcher('aaa')))
.toEqual([
{lineIndex: 20, lineStart: 0},
{lineIndex: 21, lineStart: 1},
Expand All @@ -125,7 +126,7 @@ describe('LogChunkSearch', () => {
});
it('many escapes', () => {
expect(findTextInChunkRaw(parseLogChunk(20,
'oaaa\nboa\x1b[36maabaa\x1b[36ma\no\no\x1b[36maaa\x1b[36mb\n', 's', true), 'aaa'))
'oaaa\nboa\x1b[36maabaa\x1b[36ma\no\no\x1b[36maaa\x1b[36mb\n', 's', true), getMatcher('aaa')))
.toEqual([
{lineIndex: 20, lineStart: 0},
{lineIndex: 21, lineStart: 1},
Expand All @@ -136,7 +137,18 @@ describe('LogChunkSearch', () => {
it('some escapes', () => {
expect(findTextInChunkRaw(parseLogChunk(20,
'o\no\no\no\no\no\noaaa\nboa\x1b[36maabaa\x1b[36ma\no\no\x1b[36maaa\x1b[36mb\n', 's'),
'aaa'))
getMatcher('aaa')))
.toEqual([
{lineIndex: 26, lineStart: 0},
{lineIndex: 27, lineStart: 1},
{lineIndex: 27, lineStart: 5},
{lineIndex: 29, lineStart: 0},
]);
});
it('case insensitive', () => {
expect(findTextInChunkRaw(parseLogChunk(20,
'o\no\no\no\no\no\noaaa\nboa\x1b[36maabaa\x1b[36ma\no\no\x1b[36maaa\x1b[36mb\n', 's'),
getMatcher('aAa', {caseInsensitive: true})))
.toEqual([
{lineIndex: 26, lineStart: 0},
{lineIndex: 27, lineStart: 1},
Expand Down

0 comments on commit dce2ec1

Please sign in to comment.