1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Introducing search store

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>
This commit is contained in:
Alex Andreev 2020-10-21 13:59:11 +03:00
parent 118ed207a1
commit 5c084b8557
5 changed files with 180 additions and 23 deletions

View File

@ -8,9 +8,9 @@ import { Icon } from "../icon";
import { _i18n } from "../../i18n";
import { cssNames, downloadFile } from "../../utils";
import { Pod } from "../../api/endpoints";
import { PodLogSearch } from "./pod-log-search";
import { PodLogSearch, PodLogSearchProps } from "./pod-log-search";
interface Props {
interface Props extends PodLogSearchProps {
ready: boolean
tabId: string
tabData: IPodLogsData
@ -18,7 +18,6 @@ interface Props {
save: (data: Partial<IPodLogsData>) => void
reload: () => void
onSearch: (query: string) => void
search: string
}
export const PodLogControls = observer((props: Props) => {
@ -114,7 +113,7 @@ export const PodLogControls = observer((props: Props) => {
onClick={downloadLogs}
tooltip={_i18n._(t`Save`)}
/>
<PodLogSearch onSearch={props.onSearch} search={props.search} />
<PodLogSearch {...props} />
</div>
</div>
);

View File

@ -2,24 +2,45 @@ import React from "react";
import { observer } from "mobx-react";
import { cssNames } from "../../utils";
import { Input } from "../input";
import { Button } from "@material-ui/core";
import { searchStore } from "./search.store";
interface Props {
export interface PodLogSearchProps {
onSearch: (query: string) => void
search: string
toPrevOverlay: () => void
toNextOverlay: () => void
logs: string[]
}
export const PodLogSearch = observer((props: Props) => {
const { onSearch, search } = props;
export const PodLogSearch = observer((props: PodLogSearchProps) => {
const { logs, onSearch, toPrevOverlay, toNextOverlay } = props;
const { setNextOverlayActive, setPrevOverlayActive } = searchStore;
const setSearch = (query: string) => {
searchStore.onSearch(logs, query);
onSearch(query);
};
const onPrevOverlay = () => {
setPrevOverlayActive();
toPrevOverlay();
}
const onNextOverlay = () => {
setNextOverlayActive();
toNextOverlay();
}
return (
<div className="PodLogsSearch">
<Input
className={cssNames("SearchInput")}
value={search}
value={searchStore.searchQuery}
onChange={setSearch}
/>
{/* <span>{activeOverlay} / {totalOverlays}</span> */}
<Button onClick={onPrevOverlay}>prev</Button>
<Button onClick={onNextOverlay}>next</Button>
</div>
)
);
});

View File

@ -40,6 +40,10 @@
border-radius: 2px;
background-color: #8cc474b8;
-webkit-font-smoothing: auto;
&.active {
background-color: orange;
}
}
}
}

View File

@ -13,6 +13,7 @@ import { IPodLogsData, logRange, podLogsStore } from "./pod-logs.store";
import { Button } from "../button";
import { PodLogControls } from "./pod-log-controls";
import { VirtualList } from "../virtual-list";
import { searchStore } from "./search.store";
import debounce from "lodash/debounce";
interface Props {
@ -27,7 +28,6 @@ export class PodLogs extends React.Component<Props> {
@observable ready = false;
@observable preloading = false; // Indicator for setting Spinner (loader) at the top of the logs
@observable showJumpToBottom = false;
@observable findQuery = ""; // A text from search field
private logsElement = React.createRef<HTMLDivElement>(); // A reference for outer container in VirtualList
private virtualListRef = React.createRef<VirtualList>(); // A reference for VirtualList component
@ -104,11 +104,22 @@ export class PodLogs extends React.Component<Props> {
}
/**
* Updating findQuery observable
* A function for various actions after search is happened
* @param query {string} A text from search field
*/
@autobind()
onSearch(query: string) {
this.findQuery = query;
this.toOverlay();
}
/**
* Scrolling to active overlay (search word highlight)
*/
@autobind()
toOverlay() {
const { activeOverlayLine } = searchStore;
if (!this.virtualListRef.current || activeOverlayLine == -1) return;
this.virtualListRef.current.scrollToItem(activeOverlayLine);
}
/**
@ -143,21 +154,27 @@ export class PodLogs extends React.Component<Props> {
/**
* A function is called by VirtualList for rendering each of the row
* @param index {Number} index of the log element in logs array
* @param rowIndex {Number} index of the log element in logs array
* @returns A react element with a row itself
*/
getLogRow = (index: number) => {
const isSeparator = this.logs[index] === "---newlogs---"; // TODO: Use constant separator
const { findQuery } = this;
const item = this.logs[index];
getLogRow = (rowIndex: number) => {
const isSeparator = this.logs[rowIndex] === "---newlogs---"; // TODO: Use constant separator
const { searchQuery, isActiveOverlay } = searchStore;
const item = this.logs[rowIndex];
const contents: React.ReactElement[] = [];
if (findQuery) {
if (searchQuery) {
// If search is enabled, replace keyword with backgrounded <span> to "highlight" searchable text
const pieces = item.split(findQuery);
const pieces = item.split(searchQuery);
pieces.forEach((piece, index) => {
const overlay = index !== pieces.length - 1 ? <span>{findQuery}</span> : null
const active = isActiveOverlay(rowIndex, index);
const lastItem = index === pieces.length - 1;
const overlay = !lastItem ?
<span className={cssNames({ active })}>{searchQuery}</span> :
null
contents.push(
<>{piece}{overlay}</>
<React.Fragment key={piece + index}>
{piece}{overlay}
</React.Fragment>
);
})
}
@ -231,8 +248,9 @@ export class PodLogs extends React.Component<Props> {
logs={this.logs}
save={this.save}
reload={this.reload}
search={this.findQuery}
onSearch={this.onSearch}
toPrevOverlay={this.toOverlay}
toNextOverlay={this.toOverlay}
/>
)
return (

View File

@ -0,0 +1,115 @@
import { action, computed, observable } from "mobx";
import { autobind } from "../../utils";
export class SearchStore {
@observable searchQuery = ""; // Text in the search input
@observable occurrences: number[] = []; // Array with line numbers, eg [0, 0, 10, 21, 21, 40...]
@observable activeOverlayIndex = -1; // Index withing the occurences array. Showing where is activeOverlay currently located
/**
* Sets default activeOverlayIndex
* @param text An array of any textual data (logs, for example)
* @param query Search query from input
*/
@action
onSearch(text: string[], query: string) {
this.searchQuery = query;
if (!query) {
this.reset();
return;
}
this.occurrences = this.findOccurencies(text, query);
if (!this.occurrences.length) return;
// If new highlighted keyword in exact same place as previous one, then no changing in active overlay
if (this.occurrences[this.activeOverlayIndex] !== undefined) return;
this.activeOverlayIndex = this.getNextOverlay(true);
}
/**
* Does searching within text array, create a list of search keyword occurences.
* Each keyword "occurency" is saved as index of the the line where keyword founded
* @param text An array of any textual data (logs, for example)
* @param query Search query from input
* @returns {Array} Array of line indexes [0, 0, 14, 17, 17, 17, 20...]
*/
findOccurencies(text: string[], query: string) {
const occurences: number[] = [];
text.forEach((line, index) => {
const regex = new RegExp(this.escapeRegex(query), "g");
const matches = [...line.matchAll(regex)];
matches.forEach(() => occurences.push(index));
});
return occurences;
}
/**
* Getting next overlay index within the occurences array
* @param loopOver Allows to jump from last element to first
* @returns {number} next overlay index
*/
getNextOverlay(loopOver = false) {
const next = this.activeOverlayIndex + 1;
if (next > this.occurrences.length - 1) {
return loopOver ? 0 : this.activeOverlayIndex;
}
return next;
}
/**
* Getting previous overlay index within the occurences array of occurences
* @param loopOver Allows to jump from first element to last one
* @returns {number} prev overlay index
*/
getPrevOverlay(loopOver = false) {
const prev = this.activeOverlayIndex - 1;
if (prev < 0) {
return loopOver ? this.occurrences.length - 1 : this.activeOverlayIndex;
}
return prev;
}
@autobind()
setNextOverlayActive() {
this.activeOverlayIndex = this.getNextOverlay(true);
}
@autobind()
setPrevOverlayActive() {
this.activeOverlayIndex = this.getPrevOverlay(true);
}
/**
* Gets line index of where active overlay is located
* @returns {number} A line index within the text/logs array
*/
@computed get activeOverlayLine(): number {
return this.occurrences[this.activeOverlayIndex];
}
/**
* Checks if overlay is active (to highlight it with orange background usually)
* @param line Index of the line where overlay is located
* @param occurence Number of the overlay within one line
*/
@autobind()
isActiveOverlay(line: number, occurence: number) {
const firstLineIndex = this.occurrences.findIndex(item => item === line);
return firstLineIndex + occurence === this.activeOverlayIndex;
}
/**
* An utility methods escaping user string to safely pass it into new Regex(variable)
* @param value Unescaped string
*/
escapeRegex(value: string) {
return value.replace( /[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&" );
}
reset() {
this.searchQuery = "";
this.activeOverlayIndex = -1
}
}
export const searchStore = new SearchStore;