Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Log search #1114

Merged
merged 46 commits into from Nov 9, 2020
Merged
Show file tree
Hide file tree
Changes from 44 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
edf59c9
Moving logs to virtual list
aleksfront Oct 20, 2020
7234d2e
Introducing log search
aleksfront Oct 20, 2020
118ed20
Setting ref for VirtualList to access its methods
aleksfront Oct 21, 2020
5c084b8
Introducing search store
aleksfront Oct 21, 2020
f8548d6
Centering overlay when scroll to it
aleksfront Oct 21, 2020
93ae4b6
Using SearchInput in PodLogSearch
aleksfront Oct 21, 2020
7ac13f5
Using Prev/Next icons for search
aleksfront Oct 21, 2020
14e88a5
No trigger logs load when scrolled by method
aleksfront Oct 22, 2020
de33f52
SearchInput refactoring
aleksfront Oct 22, 2020
cfb6c3c
Adding find counters
aleksfront Oct 22, 2020
45b6faf
Clean search query on dock tab change
aleksfront Oct 22, 2020
a6eb283
Refresh search when logs get changed
aleksfront Oct 22, 2020
e3a3fb4
Case-insensitive search
aleksfront Oct 22, 2020
01e485d
Improve logs scrolling experience
aleksfront Oct 22, 2020
6bb874d
Catching empty logs in various places
aleksfront Oct 22, 2020
4b0b635
Fixing downloading logs
aleksfront Oct 22, 2020
d5ed79d
Clean up some duplicated styles
aleksfront Oct 22, 2020
c099711
Removing jump-to-bottom animation
aleksfront Oct 22, 2020
c962ea6
Fixing since label
aleksfront Oct 22, 2020
99b6bab
Reducing container selector size
aleksfront Oct 22, 2020
fcf9422
Scroll down to bottom after each reload
aleksfront Oct 23, 2020
de536b8
Fix search within timestamps if they not provided
aleksfront Oct 23, 2020
d736f24
Use log row hover color from theme
aleksfront Oct 23, 2020
2e2eec7
Add search bindings for 'Esc' & 'Enter' hits
aleksfront Oct 26, 2020
b0c0794
Merge branch 'master' into logs-search
aleksfront Oct 30, 2020
61a2936
Merge branch 'master' into logs-search
aleksfront Oct 30, 2020
bc8a688
Focus input fields on CmdOrCtrl+F
aleksfront Oct 30, 2020
16cde00
Move search.store.ts in to /common folder
aleksfront Nov 1, 2020
d262c76
search.store.ts -> search-store.ts
aleksfront Nov 1, 2020
4ec29ce
Adding test for search store
aleksfront Nov 1, 2020
a054ae2
Adding integration tests for logs
aleksfront Nov 2, 2020
139c114
Fixing scroll jumping bug
aleksfront Nov 2, 2020
83c760d
Removing download icon check for testing purpose
aleksfront Nov 2, 2020
ea96a7f
Merge branch 'master' into logs-search
aleksfront Nov 2, 2020
858acc1
Removing clicking on nginx-create-pod-test
aleksfront Nov 3, 2020
94ed372
Moving log tests before cluster operations
aleksfront Nov 3, 2020
da12885
Build extensions before integration tests
nevalla Nov 3, 2020
cc3af77
Build also npm before integration tests
nevalla Nov 3, 2020
6283daa
Move npm build and extension build into own build step
nevalla Nov 3, 2020
4e1b901
Merge branch 'master' into logs-search
aleksfront Nov 4, 2020
cc8902c
Removing separator sketches
aleksfront Nov 4, 2020
c3c2e52
Horizontal scrolling to founded keyword
aleksfront Nov 5, 2020
4caddf5
Merge branch 'master' into logs-search
aleksfront Nov 5, 2020
871173e
Delaying horizontal scrolling
aleksfront Nov 5, 2020
b67d367
Merge branch 'master' into logs-search
aleksfront Nov 9, 2020
eb4435e
Merge branch 'master' into logs-search
aleksfront Nov 9, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 12 additions & 0 deletions .azure-pipelines.yml
Expand Up @@ -37,6 +37,10 @@ jobs:
displayName: Cache Yarn packages
- script: make install-deps
displayName: Install dependencies
- script: make build-npm
displayName: Generate npm package
- script: make build-extensions
displayName: Build bundled extensions
- script: make integration-win
displayName: Run integration tests
- script: make test-extensions
Expand Down Expand Up @@ -76,6 +80,10 @@ jobs:
condition: eq(variables.CACHE_RESTORED, 'true')
- script: make install-deps
displayName: Install dependencies
- script: make build-npm
displayName: Generate npm package
- script: make build-extensions
displayName: Build bundled extensions
- script: make test
displayName: Run tests
- script: make integration-mac
Expand Down Expand Up @@ -127,6 +135,10 @@ jobs:
displayName: Run In-tree Extension tests
- script: make lint
displayName: Lint
- script: make build-npm
displayName: Generate npm package
- script: make build-extensions
displayName: Build bundled extensions
- script: make test
displayName: Run tests
- bash: |
Expand Down
34 changes: 34 additions & 0 deletions integration/__tests__/app.tests.ts
Expand Up @@ -410,6 +410,40 @@ describe("Lens integration tests", () => {
})
})

describe("viewing pod logs", () => {
beforeEach(appStartAddCluster, 40000)

afterEach(async () => {
if (app && app.isRunning()) {
return util.tearDown(app)
}
})

it(`shows a logs for a pod`, async () => {
expect(clusterAdded).toBe(true)
// Go to Pods page
await app.client.click(".sidebar-nav #workloads span.link-text")
await app.client.waitUntilTextExists('a[href^="/pods"]', "Pods")
await app.client.click('a[href^="/pods"]')
await app.client.waitUntilTextExists("div.TableCell", "kube-apiserver")
// Open logs tab in dock
await app.client.click(".list .TableRow:first-child")
await app.client.waitForVisible(".Drawer")
await app.client.click(".drawer-title .Menu li:nth-child(2)")
// Check if controls are available
await app.client.waitForVisible(".PodLogs .VirtualList")
await app.client.waitForVisible(".PodLogControls")
await app.client.waitForVisible(".PodLogControls .SearchInput")
await app.client.waitForVisible(".PodLogControls .SearchInput input")
// Search for semicolon
await app.client.keys(":")
await app.client.waitForVisible(".PodLogs .list span.active")
// Click through controls
await app.client.click(".PodLogControls .timestamps-icon")
await app.client.click(".PodLogControls .undo-icon")
})
})

describe("cluster operations", () => {
beforeEach(appStartAddCluster, 40000)

Expand Down
80 changes: 80 additions & 0 deletions src/common/__tests__/search-store.test.ts
@@ -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);
})
})
126 changes: 126 additions & 0 deletions src/common/search-store.ts
@@ -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;
4 changes: 2 additions & 2 deletions src/renderer/components/+apps-helm-charts/helm-charts.tsx
Expand Up @@ -11,7 +11,7 @@ import { navigation } from "../../navigation";
import { ItemListLayout } from "../item-object-list/item-list-layout";
import { t, Trans } from "@lingui/macro";
import { _i18n } from "../../i18n";
import { SearchInput } from "../input";
import { SearchInputUrl } from "../input";

enum sortBy {
name = "name",
Expand Down Expand Up @@ -72,7 +72,7 @@ export class HelmCharts extends Component<Props> {
(items: HelmChart[]) => items.filter(item => !item.deprecated)
]}
customizeHeader={() => (
<SearchInput placeholder={_i18n._(t`Search Helm Charts`)} />
<SearchInputUrl placeholder={_i18n._(t`Search Helm Charts`)} />
)}
renderTableHeader={[
{ className: "icon" },
Expand Down
19 changes: 12 additions & 7 deletions src/renderer/components/dock/pod-log-controls.tsx
Expand Up @@ -8,22 +8,26 @@ import { Icon } from "../icon";
import { _i18n } from "../../i18n";
import { cssNames, downloadFile } from "../../utils";
import { Pod } from "../../api/endpoints";
import { PodLogSearch, PodLogSearchProps } from "./pod-log-search";

interface Props {
interface Props extends PodLogSearchProps {
ready: boolean
tabId: string
tabData: IPodLogsData
logs: string[][]
logs: string[]
save: (data: Partial<IPodLogsData>) => void
reload: () => void
onSearch: (query: string) => void
}

export const PodLogControls = observer((props: Props) => {
if (!props.ready) return null;
const { tabData, tabId, save, reload, logs } = props;
const { tabData, save, reload, tabId, logs } = props;
const { selectedContainer, showTimestamps, previous } = tabData;
const since = podLogsStore.getTimestamps(podLogsStore.logs.get(tabId)[0]);
const rawLogs = podLogsStore.logs.get(tabId);
const since = rawLogs.length ? podLogsStore.getTimestamps(rawLogs[0]) : null;
const pod = new Pod(tabData.pod);

const toggleTimestamps = () => {
save({ showTimestamps: !showTimestamps });
}
Expand All @@ -35,8 +39,7 @@ export const PodLogControls = observer((props: Props) => {

const downloadLogs = () => {
const fileName = selectedContainer ? selectedContainer.name : pod.getName();
const [oldLogs, newLogs] = logs;
downloadFile(fileName + ".log", [...oldLogs, ...newLogs].join("\n"), "text/plain");
downloadFile(fileName + ".log", logs.join("\n"), "text/plain");
}

const onContainerChange = (option: SelectOption) => {
Expand Down Expand Up @@ -92,7 +95,7 @@ export const PodLogControls = observer((props: Props) => {
</>
)}
</div>
<div className="flex gaps">
<div className="flex box grow gaps align-center">
<Icon
material="av_timer"
onClick={toggleTimestamps}
Expand All @@ -109,7 +112,9 @@ export const PodLogControls = observer((props: Props) => {
material="get_app"
onClick={downloadLogs}
tooltip={_i18n._(t`Save`)}
className="download-icon"
/>
<PodLogSearch {...props} />
</div>
</div>
);
Expand Down
10 changes: 10 additions & 0 deletions src/renderer/components/dock/pod-log-search.scss
@@ -0,0 +1,10 @@
.PodLogsSearch {
.SearchInput {
min-width: 150px;
width: 150px;

.find-count {
margin-left: 2px;
}
}
}