diff --git a/src/renderer/components/dock/pod-log-controls.tsx b/src/renderer/components/dock/pod-log-controls.tsx index 58121f0806..21e76f028d 100644 --- a/src/renderer/components/dock/pod-log-controls.tsx +++ b/src/renderer/components/dock/pod-log-controls.tsx @@ -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) => 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`)} /> - + ); diff --git a/src/renderer/components/dock/pod-log-search.tsx b/src/renderer/components/dock/pod-log-search.tsx index b7334ae55b..e716aa1616 100644 --- a/src/renderer/components/dock/pod-log-search.tsx +++ b/src/renderer/components/dock/pod-log-search.tsx @@ -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 (
+ {/* {activeOverlay} / {totalOverlays} */} + +
- ) + ); }); \ No newline at end of file diff --git a/src/renderer/components/dock/pod-logs.scss b/src/renderer/components/dock/pod-logs.scss index 56254e5327..6a69afcd23 100644 --- a/src/renderer/components/dock/pod-logs.scss +++ b/src/renderer/components/dock/pod-logs.scss @@ -40,6 +40,10 @@ border-radius: 2px; background-color: #8cc474b8; -webkit-font-smoothing: auto; + + &.active { + background-color: orange; + } } } } diff --git a/src/renderer/components/dock/pod-logs.tsx b/src/renderer/components/dock/pod-logs.tsx index dd343f10d3..2336f55afe 100644 --- a/src/renderer/components/dock/pod-logs.tsx +++ b/src/renderer/components/dock/pod-logs.tsx @@ -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 { @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(); // A reference for outer container in VirtualList private virtualListRef = React.createRef(); // A reference for VirtualList component @@ -104,11 +104,22 @@ export class PodLogs extends React.Component { } /** - * 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 { /** * 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 to "highlight" searchable text - const pieces = item.split(findQuery); + const pieces = item.split(searchQuery); pieces.forEach((piece, index) => { - const overlay = index !== pieces.length - 1 ? {findQuery} : null + const active = isActiveOverlay(rowIndex, index); + const lastItem = index === pieces.length - 1; + const overlay = !lastItem ? + {searchQuery} : + null contents.push( - <>{piece}{overlay} + + {piece}{overlay} + ); }) } @@ -231,8 +248,9 @@ export class PodLogs extends React.Component { logs={this.logs} save={this.save} reload={this.reload} - search={this.findQuery} onSearch={this.onSearch} + toPrevOverlay={this.toOverlay} + toNextOverlay={this.toOverlay} /> ) return ( diff --git a/src/renderer/components/dock/search.store.ts b/src/renderer/components/dock/search.store.ts new file mode 100644 index 0000000000..1759767372 --- /dev/null +++ b/src/renderer/components/dock/search.store.ts @@ -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; \ No newline at end of file