mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Log search (#1114)
* Moving logs to virtual list Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Introducing log search Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Setting ref for VirtualList to access its methods Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Introducing search store Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Centering overlay when scroll to it Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Using SearchInput in PodLogSearch Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Using Prev/Next icons for search Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * No trigger logs load when scrolled by method Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * SearchInput refactoring Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Adding find counters Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Clean search query on dock tab change Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Refresh search when logs get changed Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Case-insensitive search Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Improve logs scrolling experience Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Catching empty logs in various places Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Fixing downloading logs Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Clean up some duplicated styles Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Removing jump-to-bottom animation Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Fixing since label Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Reducing container selector size Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Scroll down to bottom after each reload Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Fix search within timestamps if they not provided Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Use log row hover color from theme Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Add search bindings for 'Esc' & 'Enter' hits Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Focus input fields on CmdOrCtrl+F Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Move search.store.ts in to /common folder Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * search.store.ts -> search-store.ts Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Adding test for search store Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Adding integration tests for logs Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Fixing scroll jumping bug Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Removing download icon check for testing purpose Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Removing clicking on nginx-create-pod-test Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Moving log tests before cluster operations Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Build extensions before integration tests Signed-off-by: Lauri Nevala <lauri.nevala@gmail.com> * Build also npm before integration tests Signed-off-by: Lauri Nevala <lauri.nevala@gmail.com> * Move npm build and extension build into own build step Signed-off-by: Lauri Nevala <lauri.nevala@gmail.com> * Removing separator sketches Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Horizontal scrolling to founded keyword Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Delaying horizontal scrolling Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> Co-authored-by: Lauri Nevala <lauri.nevala@gmail.com>
This commit is contained in:
1 parent
f69f8c793f
commit
16fb35e3f9
25 files changed
+697
-144
No files matched your search
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
|
||||
import { SearchStore } from "../search-store"
|
||||
|
||||
let searchStore: SearchStore = null;
|
||||
|
||||
const logs = [
|
||||
"1:M 30 Oct 2020 16:17:41.553 # Connection with replica 172.17.0.12:6379 lost",
|
||||
"1:M 30 Oct 2020 16:17:41.623 * Replica 172.17.0.12:6379 asks for synchronization",
|
||||
"1:M 30 Oct 2020 16:17:41.623 * Starting Partial resynchronization request from 172.17.0.12:6379 accepted. Sending 0 bytes of backlog starting from offset 14407."
|
||||
]
|
||||
|
||||
describe("search store tests", () => {
|
||||
beforeEach(async () => {
|
||||
searchStore = new SearchStore();
|
||||
})
|
||||
|
||||
it("does nothing with empty search query", () => {
|
||||
searchStore.onSearch([], "");
|
||||
expect(searchStore.occurrences).toEqual([]);
|
||||
})
|
||||
|
||||
it("doesn't break if no text provided", () => {
|
||||
searchStore.onSearch(null, "replica");
|
||||
expect(searchStore.occurrences).toEqual([]);
|
||||
|
||||
searchStore.onSearch([], "replica");
|
||||
expect(searchStore.occurrences).toEqual([]);
|
||||
})
|
||||
|
||||
it("find 3 occurences across 3 lines", () => {
|
||||
searchStore.onSearch(logs, "172");
|
||||
expect(searchStore.occurrences).toEqual([0, 1, 2]);
|
||||
})
|
||||
|
||||
it("find occurences within 1 line (case-insensitive)", () => {
|
||||
searchStore.onSearch(logs, "Starting");
|
||||
expect(searchStore.occurrences).toEqual([2, 2]);
|
||||
})
|
||||
|
||||
it("sets overlay index equal to first occurence", () => {
|
||||
searchStore.onSearch(logs, "Replica");
|
||||
expect(searchStore.activeOverlayIndex).toBe(0);
|
||||
})
|
||||
|
||||
it("set overlay index to next occurence", () => {
|
||||
searchStore.onSearch(logs, "172");
|
||||
searchStore.setNextOverlayActive();
|
||||
expect(searchStore.activeOverlayIndex).toBe(1);
|
||||
})
|
||||
|
||||
it("sets overlay to last occurence", () => {
|
||||
searchStore.onSearch(logs, "172");
|
||||
searchStore.setPrevOverlayActive();
|
||||
expect(searchStore.activeOverlayIndex).toBe(2);
|
||||
})
|
||||
|
||||
it("gets line index where overlay is located", () => {
|
||||
searchStore.onSearch(logs, "synchronization");
|
||||
expect(searchStore.activeOverlayLine).toBe(1);
|
||||
})
|
||||
|
||||
it("escapes string for using in regex", () => {
|
||||
const regex = searchStore.escapeRegex("some.interesting-query\\#?()[]");
|
||||
expect(regex).toBe("some\\.interesting\\-query\\\\\\#\\?\\(\\)\\[\\]");
|
||||
})
|
||||
|
||||
it("gets active find number", () => {
|
||||
searchStore.onSearch(logs, "172");
|
||||
searchStore.setNextOverlayActive();
|
||||
expect(searchStore.activeFind).toBe(2);
|
||||
})
|
||||
|
||||
it("gets total finds number", () => {
|
||||
searchStore.onSearch(logs, "Starting");
|
||||
expect(searchStore.totalFinds).toBe(2);
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,126 @@
|
||||
import { action, computed, observable } from "mobx";
|
||||
import { autobind } from "../renderer/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 = this.searchQuery) {
|
||||
this.searchQuery = query;
|
||||
if (!query) {
|
||||
this.reset();
|
||||
return;
|
||||
}
|
||||
this.occurrences = this.findOccurences(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...]
|
||||
*/
|
||||
findOccurences(text: string[], query: string) {
|
||||
if (!text) return [];
|
||||
const occurences: number[] = [];
|
||||
text.forEach((line, index) => {
|
||||
const regex = new RegExp(this.escapeRegex(query), "gi");
|
||||
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];
|
||||
}
|
||||
|
||||
@computed get activeFind(): number {
|
||||
return this.activeOverlayIndex + 1;
|
||||
}
|
||||
|
||||
@computed get totalFinds(): number {
|
||||
return this.occurrences.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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, "\\$&" );
|
||||
}
|
||||
|
||||
@action
|
||||
reset() {
|
||||
this.searchQuery = "";
|
||||
this.activeOverlayIndex = -1;
|
||||
this.occurrences = [];
|
||||
}
|
||||
}
|
||||
|
||||
export const searchStore = new SearchStore;
|
||||
Reference in new issue
Block a user