Cascade different refactorings to eliminate global shared state from app paths

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>
This commit is contained in:
Janne SavolainenandMikko Aspiala committed 2022-01-05 13:26:49 +02:00
1 parent 908a40975e
commit 9d33fff906
336 files changed
+9105 -3307

No files matched your search

@@ -0,0 +1,33 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import dockStoreInjectable from "../components/dock/dock-store/dock-store.injectable";
import { SearchStore } from "./search-store";
const searchStoreInjectable = getInjectable({
instantiate: (di) => new SearchStore({
dockStore: di.inject(dockStoreInjectable),
}),
lifecycle: lifecycleEnum.singleton,
});
export default searchStoreInjectable;
@@ -0,0 +1,118 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { SearchStore } from "./search-store";
import { Console } from "console";
import { stdout, stderr } from "process";
import { getDiForUnitTesting } from "../components/getDiForUnitTesting";
import searchStoreInjectable from "./search-store.injectable";
import directoryForUserDataInjectable
from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
jest.mock("electron", () => ({
app: {
getPath: () => "/foo",
},
}));
console = new Console(stdout, stderr);
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", () => {
let searchStore: SearchStore;
beforeEach(async () => {
const di = getDiForUnitTesting({ doGeneralOverrides: true });
di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
await di.runSetups();
searchStore = di.inject(searchStoreInjectable);
});
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 occurrences across 3 lines", () => {
searchStore.onSearch(logs, "172");
expect(searchStore.occurrences).toEqual([0, 1, 2]);
});
it("find occurrences within 1 line (case-insensitive)", () => {
searchStore.onSearch(logs, "Starting");
expect(searchStore.occurrences).toEqual([2, 2]);
});
it("sets overlay index equal to first occurrence", () => {
searchStore.onSearch(logs, "Replica");
expect(searchStore.activeOverlayIndex).toBe(0);
});
it("set overlay index to next occurrence", () => {
searchStore.onSearch(logs, "172");
searchStore.setNextOverlayActive();
expect(searchStore.activeOverlayIndex).toBe(1);
});
it("sets overlay to last occurrence", () => {
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);
});
});
+180
View File
@@ -0,0 +1,180 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { action, computed, observable, reaction, makeObservable } from "mobx";
import type { DockStore } from "../components/dock/dock-store/dock.store";
import { boundMethod } from "../utils";
interface Dependencies {
dockStore: DockStore
}
export class SearchStore {
/**
* An utility methods escaping user string to safely pass it into new Regex(variable)
* @param value Unescaped string
*/
public static escapeRegex(value?: string): string {
return value ? value.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&") : "";
}
/**
* Text in the search input
*
* @observable
*/
@observable searchQuery = "";
/**
* Array with line numbers, eg [0, 0, 10, 21, 21, 40...]
*
* @observable
*/
@observable occurrences: number[] = [];
/**
* Index within the occurrences array. Showing where is activeOverlay currently located
*
* @observable
*/
@observable activeOverlayIndex = -1;
constructor(dependencies: Dependencies) {
makeObservable(this);
reaction(() => dependencies.dockStore.selectedTabId, () => {
this.reset();
});
}
/**
* Sets default activeOverlayIndex
* @param text An array of any textual data (logs, for example)
* @param query Search query from input
*/
@action
public onSearch(text?: string[] | null, query = this.searchQuery): void {
this.searchQuery = query;
if (!query) {
return this.reset();
}
this.occurrences = this.findOccurrences(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) {
this.activeOverlayIndex = this.getNextOverlay(true);
}
}
/**
* Does searching within text array, create a list of search keyword occurrences.
* Each keyword "occurrence" is saved as index of the line where keyword was found
* @param lines An array of any textual data (logs, for example)
* @param query Search query from input
* @returns Array of line indexes [0, 0, 14, 17, 17, 17, 20...]
*/
private findOccurrences(lines: string[], query?: string): number[] {
const regex = new RegExp(SearchStore.escapeRegex(query), "gi");
return lines
.flatMap((line, index) => Array.from(line.matchAll(regex), () => index));
}
/**
* Getting next overlay index within the occurrences array
* @param loopOver Allows to jump from last element to first
* @returns next overlay index
*/
private getNextOverlay(loopOver = false): number {
const next = this.activeOverlayIndex + 1;
if (next > this.occurrences.length - 1) {
return loopOver ? 0 : this.activeOverlayIndex;
}
return next;
}
/**
* Getting previous overlay index within the occurrences array of occurrences
* @param loopOver Allows to jump from first element to last one
* @returns previous overlay index
*/
private getPrevOverlay(loopOver = false): number {
const prev = this.activeOverlayIndex - 1;
if (prev < 0) {
return loopOver ? this.occurrences.length - 1 : this.activeOverlayIndex;
}
return prev;
}
@boundMethod
public setNextOverlayActive(): void {
this.activeOverlayIndex = this.getNextOverlay(true);
}
@boundMethod
public setPrevOverlayActive(): void {
this.activeOverlayIndex = this.getPrevOverlay(true);
}
/**
* Gets line index of where active overlay is located
* @returns 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 occurrence Number of the overlay within one line
*/
@boundMethod
public isActiveOverlay(line: number, occurrence: number): boolean {
const firstLineIndex = this.occurrences.findIndex(item => item === line);
return firstLineIndex + occurrence === this.activeOverlayIndex;
}
@action
private reset(): void {
this.searchQuery = "";
this.activeOverlayIndex = -1;
this.occurrences = [];
}
}