diff --git a/src/analyzer/index.tsx b/src/analyzer/index.tsx index fe43f16..18c31be 100644 --- a/src/analyzer/index.tsx +++ b/src/analyzer/index.tsx @@ -93,7 +93,7 @@ function Analyzer() { -
+
params.data.id} getRowStyle={getRowStyle} enableCellTextSelection={true} - // ensureDomOrder={true} />
diff --git a/src/analyzer/useViewModel.test.tsx b/src/analyzer/useViewModel.test.tsx index b0bcc92..d139f09 100644 --- a/src/analyzer/useViewModel.test.tsx +++ b/src/analyzer/useViewModel.test.tsx @@ -91,6 +91,18 @@ describe("useViewModel", () => { [Processor.logKeys.fullData]: "test one two four", [Processor.logKeys.msg]: "test one two four", }, + { + [Processor.logKeys.id]: "23", + [Processor.logKeys.timestamp]: "2023-10-20T10:00:00.000Z", + [Processor.logKeys.fullData]: "test one two contains check four", + [Processor.logKeys.msg]: "test one two contains check four", + }, + { + [Processor.logKeys.id]: "24", + [Processor.logKeys.timestamp]: "2023-10-20T10:00:00.000Z", + [Processor.logKeys.fullData]: "test one two ands check four", + [Processor.logKeys.msg]: "test one two ands check four", + }, { [Processor.logKeys.id]: "30", [Processor.logKeys.timestamp]: "2023-10-20T12:00:00.000Z", @@ -100,6 +112,8 @@ describe("useViewModel", () => { ]; vi.spyOn(Processor, "isErrorLog") + .mockReturnValueOnce(true) + .mockReturnValueOnce(true) .mockReturnValueOnce(true) .mockReturnValueOnce(true) .mockReturnValueOnce(false); @@ -110,14 +124,39 @@ describe("useViewModel", () => { startTime: "2023-10-20T01:00:00.000Z", endTime: "2023-10-20T11:00:00.000Z", errorsOnly: true, - logs: [comparer.last().logs[0], comparer.last().logs[1]], + logs: [ + comparer.last().logs[1], + comparer.last().logs[2], + comparer.last().logs[3], + comparer.last().logs[4], + ], regex: "^tes.*our$", + terms: [ + { + and: true, + contains: true, + value: "ands", + }, + { + and: true, + contains: false, + value: "msg", + }, + { + and: false, + contains: true, + value: "check", + }, + ], }; const vm = useViewModel(); vm.handleFiltersChange(filters); - expect(vm.rows(), "rows").toEqual([comparer.last().logs[1]]); + expect(vm.rows(), "rows").toEqual([ + comparer.last().logs[2], + comparer.last().logs[3], + ]); expect(vm.timeJumps().prevDisabled, "prevDisabled").toEqual(true); expect(vm.timeJumps().nextDisabled, "nextDisabled").toEqual(false); @@ -133,22 +172,27 @@ describe("useViewModel", () => { { [Processor.logKeys.id]: "1", [Processor.logKeys.timestamp]: "2023-10-20T08:00:00.000Z", + [Processor.logKeys.fullData]: "json string 1", }, { [Processor.logKeys.id]: "2", [Processor.logKeys.timestamp]: "2023-10-20T10:00:00.000Z", + [Processor.logKeys.fullData]: "json string 2", }, { [Processor.logKeys.id]: "3", [Processor.logKeys.timestamp]: "2023-10-20T12:00:00.000Z", + [Processor.logKeys.fullData]: "json string 3", }, { [Processor.logKeys.id]: "4", [Processor.logKeys.timestamp]: "2023-10-20T14:00:00.000Z", + [Processor.logKeys.fullData]: "json string 4", }, { [Processor.logKeys.id]: "5", [Processor.logKeys.timestamp]: "2023-10-20T16:00:00.000Z", + [Processor.logKeys.fullData]: "json string 5", }, ]; vi.spyOn(comparer, "last").mockReturnValue(processor); diff --git a/src/analyzer/useViewModel.tsx b/src/analyzer/useViewModel.tsx index 51af8dd..a45b5d0 100644 --- a/src/analyzer/useViewModel.tsx +++ b/src/analyzer/useViewModel.tsx @@ -1,6 +1,9 @@ import Processor, { JSONLogs } from "../models/processor"; import { createSignal } from "solid-js"; -import { FiltersData } from "../components/filters/useViewModel"; +import type { + FiltersData, + SearchTerm, +} from "../components/filters/useViewModel"; import comparer from "../models/comparer"; import stringsUtils from "../utils/strings"; import filesUtils from "../utils/files"; @@ -48,13 +51,41 @@ function useViewModel() { if (keep && filtersData.errorsOnly) { keep = Processor.isErrorLog(log); } + + const fullData = log[Processor.logKeys.fullData].toLowerCase(); if (keep && filtersData.regex) { - keep = stringsUtils.regexMatch( - log[Processor.logKeys.fullData], - filtersData.regex - ); + keep = stringsUtils.regexMatch(fullData, filtersData.regex); + } + if (keep && filtersData.terms) { + const ands = filtersData.terms.filter((t) => t.and); + let currCondition = true; + const updateCurrCondition = (term: SearchTerm) => { + const val = term.value.trim(); + if (val) { + currCondition = term.contains + ? fullData.includes(val) + : !fullData.includes(val); + } + }; + + for (const term of ands) { + if (!currCondition) break; + updateCurrCondition(term); + } + + if (!currCondition) { + const ors = filtersData.terms.filter((t) => !t.and); + + for (const term of ors) { + if (currCondition) break; + updateCurrCondition(term); + } + } + + keep = currCondition; } + // Update time jumps if (keep) { const id = log[Processor.logKeys.id]; const time = new Date(log[Processor.logKeys.timestamp]); diff --git a/src/components/filters/index.tsx b/src/components/filters/index.tsx index bda61e0..a66b096 100644 --- a/src/components/filters/index.tsx +++ b/src/components/filters/index.tsx @@ -8,12 +8,26 @@ import { TextField, Typography, } from "@suid/material"; -import useViewModel, { type FiltersProps } from "./useViewModel"; +import useViewModel, { + type SearchTerm, + type FiltersProps, +} from "./useViewModel"; import AgGridSolid, { type AgGridSolidRef } from "ag-grid-solid"; import type { GridOptions } from "ag-grid-community"; import { GroupedMsg } from "../../models/processor"; -import { Show } from "solid-js"; +import { type Accessor, For, Show } from "solid-js"; import comparer from "../../models/comparer"; +import { Select } from "@thisbeyond/solid-select"; +import { IconButton } from "@suid/material"; +import AddIcon from "@suid/icons-material/Add"; +import RemoveIcon from "@suid/icons-material/Remove"; + +const texts = { + and: "AND", + or: "OR", + contains: "Contains", + notContains: "Not Contains", +}; function Filters(props: FiltersProps) { let topMsgsGridRef = {} as AgGridSolidRef; @@ -29,6 +43,7 @@ function Filters(props: FiltersProps) { handleLogsSelectionChanged, handleErrorsOnlyChange, handleResetClick, + handleNewSearchTerm, } = useViewModel(props); const commonGridOptions: GridOptions = { @@ -72,6 +87,7 @@ function Filters(props: FiltersProps) { { ...commonGridOptions.columnDefs![1] }, ], rowSelection: undefined, + onSelectionChanged: undefined, }; function handleEnterKey(e: KeyboardEvent) { @@ -80,6 +96,33 @@ function Filters(props: FiltersProps) { } } + function getSimpleSearchHTML(term: SearchTerm, i: Accessor) { + return ( + <> + + setFilters("terms", i(), "contains", val === texts.contains) + } + /> + setFilters("terms", i(), "value", val)} + onKeyDown={handleEnterKey} + /> + + ); + } + return ( @@ -102,6 +145,16 @@ function Filters(props: FiltersProps) { onChange={(_, val) => setFilters("regex", val)} onKeyDown={handleEnterKey} /> + {getSimpleSearchHTML} + handleNewSearchTerm(true)}> + + + handleNewSearchTerm(false)} + > + + diff --git a/src/components/filters/useViewModel.test.tsx b/src/components/filters/useViewModel.test.tsx index ed4ceb7..0fc9ce2 100644 --- a/src/components/filters/useViewModel.test.tsx +++ b/src/components/filters/useViewModel.test.tsx @@ -42,6 +42,13 @@ describe("useViewModel", () => { startTime: "", endTime: "", regex: "", + terms: [ + { + and: true, + contains: true, + value: "", + }, + ], logs: [], errorsOnly: false, }; @@ -188,4 +195,26 @@ describe("useViewModel", () => { }); }); }); + + test("handleNewSearchTerm", () => { + createRoot((dispose) => { + const vm = useViewModel(props); + const original = [...vm.filters.terms]; + + vm.handleNewSearchTerm(true); + expect(vm.filters.terms, "term added").toEqual([ + ...original, + { + and: true, + contains: true, + value: "", + }, + ]); + + vm.handleNewSearchTerm(false); + expect(vm.filters.terms, "term removed").toEqual(original); + + dispose(); + }); + }); }); diff --git a/src/components/filters/useViewModel.tsx b/src/components/filters/useViewModel.tsx index bc3a3e4..354ff95 100644 --- a/src/components/filters/useViewModel.tsx +++ b/src/components/filters/useViewModel.tsx @@ -9,10 +9,17 @@ interface FiltersProps { onFiltersChange: (filters: FiltersData) => void; } +interface SearchTerm { + and: boolean; + contains: boolean; + value: string; +} + interface FiltersData { startTime: string; endTime: string; regex: string; + terms: SearchTerm[]; logs: JSONLogs; errorsOnly: boolean; } @@ -22,6 +29,13 @@ function defaultFilters(): FiltersData { startTime: "", endTime: "", regex: "", + terms: [ + { + and: true, + contains: true, + value: "", + }, + ], logs: [], errorsOnly: false, }; @@ -75,6 +89,19 @@ function useViewModel(props: FiltersProps) { handleFiltersChange(); } + function handleNewSearchTerm(isAddition: boolean) { + if (isAddition) { + setFilters("terms", filters.terms.length, { + and: true, + contains: true, + value: "", + }); + return; + } + + setFilters("terms", filters.terms.slice(0, -1)); + } + return { filters, topLogs, @@ -85,8 +112,9 @@ function useViewModel(props: FiltersProps) { handleLogsSelectionChanged, handleErrorsOnlyChange, handleResetClick, + handleNewSearchTerm, }; } export default useViewModel; -export type { FiltersData, FiltersProps }; +export type { SearchTerm, FiltersData, FiltersProps };