diff --git a/src/analyzer/gridService.ts b/src/analyzer/gridService.ts index b4e4883..60a31d5 100644 --- a/src/analyzer/gridService.ts +++ b/src/analyzer/gridService.ts @@ -1,5 +1,6 @@ import type { ICellRendererParams, ColDef } from "ag-grid-community"; import JSONCellRenderer from "../components/jsonCellRenderer"; +import FullDataCellRenderer from "../components/fullDataCellRenderer"; import Processor from "../models/processor"; const defaultColDef: ColDef = { @@ -10,17 +11,21 @@ const defaultColDef: ColDef = { function defaultCols(): ColDef[] { return [ + { + field: Processor.logKeys.id, + width: 100, + sortable: true, + sort: "asc", + sortingOrder: ["asc", "desc"], + }, { field: Processor.logKeys.fullData, - cellRenderer: JSONCellRenderer, + cellRenderer: FullDataCellRenderer, flex: 2, }, { - field: "timestamp", + field: Processor.logKeys.timestamp, width: 270, - sortable: true, - sort: "asc", - sortingOrder: ["asc", "desc"], }, ]; } @@ -34,7 +39,7 @@ function getCol(field: string): ColDef { return { field: field, flex: 0.75, - cellRenderer: (params: ICellRendererParams) => { + cellRenderer: (params: ICellRendererParams) => { const val = typeof params.value === "object" ? JSONCellRenderer(params) diff --git a/src/analyzer/index.tsx b/src/analyzer/index.tsx index 18c31be..90075a7 100644 --- a/src/analyzer/index.tsx +++ b/src/analyzer/index.tsx @@ -5,7 +5,7 @@ import Filters from "../components/filters"; import gridService from "./gridService"; import comparer from "../models/comparer"; import { Select, createOptions } from "@thisbeyond/solid-select"; -import type { RowStyle, RowClassParams } from "ag-grid-community"; +import type { GridOptions } from "ag-grid-community"; import Processor, { type JSONLog } from "../models/processor"; function Analyzer() { @@ -21,13 +21,24 @@ function Analyzer() { setInitialCols, timeJumps, handleTimeJump, + handleContextClick, } = useViewModel(); - function getRowStyle(params: RowClassParams): RowStyle | undefined { - return params.data && Processor.isErrorLog(params.data) - ? { background: "#FFBFBF" } - : undefined; - } + const gridOptions = (): GridOptions => ({ + enableCellTextSelection: true, + suppressMaintainUnsortedOrder: true, + defaultColDef: gridService.defaultColDef, + context: { + handleContextClick, + }, + rowData: rows(), + columnDefs: cols(), + getRowId: (params) => params.data.id, + getRowStyle: (params) => + params.data && Processor.isErrorLog(params.data) + ? { background: "#FFBFBF" } + : undefined, + }); return ( @@ -93,16 +104,8 @@ function Analyzer() { -
- params.data.id} - getRowStyle={getRowStyle} - enableCellTextSelection={true} - /> +
+
diff --git a/src/analyzer/useViewModel.test.tsx b/src/analyzer/useViewModel.test.tsx index d139f09..8b36906 100644 --- a/src/analyzer/useViewModel.test.tsx +++ b/src/analyzer/useViewModel.test.tsx @@ -320,4 +320,107 @@ describe("useViewModel", () => { }); }); }); + + test("handleContextClick", () => { + createRoot((dispose) => { + comparer.last().logs = [ + { + [Processor.logKeys.id]: "0", + [Processor.logKeys.fullData]: "msg a", + }, + { + [Processor.logKeys.id]: "1", + [Processor.logKeys.fullData]: "test b", + }, + { + [Processor.logKeys.id]: "2", + [Processor.logKeys.fullData]: "msg c", + }, + { + [Processor.logKeys.id]: "3", + [Processor.logKeys.fullData]: "test d", + }, + { + [Processor.logKeys.id]: "4", + [Processor.logKeys.fullData]: "msg e", + }, + { + [Processor.logKeys.id]: "5", + [Processor.logKeys.fullData]: "test f", + }, + { + [Processor.logKeys.id]: "6", + [Processor.logKeys.fullData]: "msg g", + }, + { + [Processor.logKeys.id]: "7", + [Processor.logKeys.fullData]: "test h", + }, + { + [Processor.logKeys.id]: "8", + [Processor.logKeys.fullData]: "msg i", + }, + ]; + + const vm = useViewModel(); + const filtersData: FiltersData = { + regex: "test", + logs: [], + } as any; + vm.handleFiltersChange(filtersData); + expect(vm.rows(), "rows: test filtered").toEqual([ + comparer.last().logs[1], + comparer.last().logs[3], + comparer.last().logs[5], + comparer.last().logs[7], + ]); + + const gridApi = { + getRowNode: (i: number): boolean => + vm.rows().includes(comparer.last().logs[i]), + }; + + vm.handleContextClick(gridApi as any, 1); + + expect(vm.rows(), "rows: logID 1").toEqual([ + comparer.last().logs[1], + comparer.last().logs[3], + comparer.last().logs[5], + comparer.last().logs[7], + comparer.last().logs[0], + comparer.last().logs[2], + comparer.last().logs[4], + comparer.last().logs[6], + ]); + + vm.handleContextClick(gridApi as any, 1); + + expect(vm.rows(), "rows: logID 1 again: no change").toEqual([ + comparer.last().logs[1], + comparer.last().logs[3], + comparer.last().logs[5], + comparer.last().logs[7], + comparer.last().logs[0], + comparer.last().logs[2], + comparer.last().logs[4], + comparer.last().logs[6], + ]); + + vm.handleContextClick(gridApi as any, 7); + + expect(vm.rows(), "rows: logID 7").toEqual([ + comparer.last().logs[1], + comparer.last().logs[3], + comparer.last().logs[5], + comparer.last().logs[7], + comparer.last().logs[0], + comparer.last().logs[2], + comparer.last().logs[4], + comparer.last().logs[6], + comparer.last().logs[8], + ]); + + dispose(); + }); + }); }); diff --git a/src/analyzer/useViewModel.tsx b/src/analyzer/useViewModel.tsx index a45b5d0..a473466 100644 --- a/src/analyzer/useViewModel.tsx +++ b/src/analyzer/useViewModel.tsx @@ -10,6 +10,7 @@ import filesUtils from "../utils/files"; import gridService from "./gridService"; import timesUtils from "../utils/times"; import { AgGridSolidRef } from "ag-grid-solid"; +import type { GridApi } from "ag-grid-community"; let zeroJump: string; let prevJumps: string[] = []; @@ -139,6 +140,22 @@ function useViewModel() { })); } + function handleContextClick(gridApi: GridApi, logID: number) { + const contextLogs: JSONLogs = []; + const limit = 5; + const currIdx = logID; + const firstIdx = Math.max(currIdx - limit, 0); + const lastIdx = Math.min(currIdx + limit, comparer.last().logs.length - 1); + + for (let i = firstIdx; i <= lastIdx; i++) { + if (!gridApi.getRowNode(i.toString())) { + contextLogs.push(comparer.last().logs[i]); + } + } + + setRows((pre) => [...pre, ...contextLogs]); + } + return { handleFiltersChange, handleColsChange, @@ -149,6 +166,7 @@ function useViewModel() { setInitialCols, timeJumps, handleTimeJump, + handleContextClick, }; } diff --git a/src/components/fullDataCellRenderer/index.tsx b/src/components/fullDataCellRenderer/index.tsx new file mode 100644 index 0000000..751da69 --- /dev/null +++ b/src/components/fullDataCellRenderer/index.tsx @@ -0,0 +1,20 @@ +import type { ICellRendererParams } from "ag-grid-community"; +import JSONCellRenderer from "../jsonCellRenderer"; +import { Button } from "@suid/material"; + +function FullDataCellRenderer(props: ICellRendererParams) { + return ( + <> + {JSONCellRenderer(props)} + + + ); +} + +export default FullDataCellRenderer; diff --git a/src/models/processor.test.ts b/src/models/processor.test.ts index 07e3deb..72f41ab 100644 --- a/src/models/processor.test.ts +++ b/src/models/processor.test.ts @@ -41,19 +41,19 @@ describe("init", () => { const cutOffLen = Processor["msgCutOffLen"]; it("init", async () => { - const log1 = { + const log0 = { [Processor.logKeys.error]: "some error", [Processor.logKeys.msg]: "has errors", parentKey1: "k1", }; - const log1String = getJSONString(log1); - const log2 = { + const log0String = getJSONString(log0); + const log1 = { [Processor.logKeys.level]: "error", [Processor.logKeys.msg]: "dbg msg", parentKey1: "k1", }; - const log2String = getJSONString(log2); - const log3 = { + const log1String = getJSONString(log1); + const log2 = { [Processor.logKeys.level]: "info", [Processor.logKeys.msg]: "abc ".repeat(cutOffLen) + "group 1", parentKey1: { @@ -61,23 +61,23 @@ describe("init", () => { childKey2: 12, }, }; - const log3String = getJSONString(log3); - const log4 = { + const log2String = getJSONString(log2); + const log3 = { [Processor.logKeys.level]: "error", [Processor.logKeys.msg]: "abc ".repeat(cutOffLen) + "group 1", parentKey1: "k1", }; - const log4String = getJSONString(log4); - const log5 = { + const log3String = getJSONString(log3); + const log4 = { [Processor.logKeys.level]: "info", [Processor.logKeys.msg]: "qwe ".repeat(cutOffLen) + "group 2", }; - const log5String = getJSONString(log5); - const log6 = { + const log4String = getJSONString(log4); + const log5 = { [Processor.logKeys.level]: "info", [Processor.logKeys.msg]: "qwe ".repeat(cutOffLen) + "group 2", }; - const log6String = getJSONString(log6); + const log5String = getJSONString(log5); function getJSONString(obj: any) { return JSON.stringify(obj); @@ -89,15 +89,15 @@ describe("init", () => { text: () => Promise.resolve( ` + ${log0String} ${log1String} - ${log2String} "non-json log" - ${log3String} + ${log2String} - ${log4String} + ${log3String} + ${log4String} ${log5String} - ${log6String} ` ), }; @@ -109,12 +109,12 @@ describe("init", () => { expect(processor.fileInfo.size, "file size").toEqual(file.size); const expectedLogs = [ - { ...log1, id: "1", [Processor.logKeys.fullData]: log1String }, - { ...log2, id: "2", [Processor.logKeys.fullData]: log2String }, - { ...log3, id: "3", [Processor.logKeys.fullData]: log3String }, - { ...log4, id: "4", [Processor.logKeys.fullData]: log4String }, - { ...log5, id: "5", [Processor.logKeys.fullData]: log5String }, - { ...log6, id: "6", [Processor.logKeys.fullData]: log6String }, + { ...log0, id: 0, [Processor.logKeys.fullData]: log0String }, + { ...log1, id: 1, [Processor.logKeys.fullData]: log1String }, + { ...log2, id: 2, [Processor.logKeys.fullData]: log2String }, + { ...log3, id: 3, [Processor.logKeys.fullData]: log3String }, + { ...log4, id: 4, [Processor.logKeys.fullData]: log4String }, + { ...log5, id: 5, [Processor.logKeys.fullData]: log5String }, ]; expect(processor.logs, "logs").toEqual(expectedLogs); @@ -126,33 +126,33 @@ describe("init", () => { } const expectedTopLogsMap = new Map([ [ - getCutOffMsg(log1), + getCutOffMsg(log0), { - msg: getCutOffMsg(log1), + msg: getCutOffMsg(log0), logs: [expectedLogs[0]] as any, hasErrors: true, }, ], [ - getCutOffMsg(log2), + getCutOffMsg(log1), { - msg: getCutOffMsg(log2), + msg: getCutOffMsg(log1), logs: [expectedLogs[1]] as any, hasErrors: true, }, ], [ - getCutOffMsg(log3), + getCutOffMsg(log2), { - msg: getCutOffMsg(log3), + msg: getCutOffMsg(log2), logs: [expectedLogs[2], expectedLogs[3]] as any, hasErrors: true, }, ], [ - getCutOffMsg(log5), + getCutOffMsg(log4), { - msg: getCutOffMsg(log5), + msg: getCutOffMsg(log4), logs: [expectedLogs[4], expectedLogs[5]] as any, hasErrors: false, }, diff --git a/src/models/processor.ts b/src/models/processor.ts index c0ff47e..68ad1b1 100644 --- a/src/models/processor.ts +++ b/src/models/processor.ts @@ -41,14 +41,14 @@ class Processor { this.initFileInfo(file); const keysSet = new Set(); - let count = 1; + let count = 0; for (const line of await Processor.getLines(file)) { const log = this.addLog(line.trim()); if (log == null) { continue; } - log[Processor.logKeys.id] = (count++).toString(); + log[Processor.logKeys.id] = count++ as any; this.initTopLogsMap(log); Processor.initKeysSet(log, keysSet); @@ -113,6 +113,55 @@ class Processor { private static async getLines(file: File): Promise { return (await file.text()).split(/\r?\n/); + + // Sample Test JSON Lines + // return Promise.resolve([ + // JSON.stringify({ + // [this.logKeys.level]: "info", + // [this.logKeys.msg]: "msg a", + // [this.logKeys.timestamp]: "2023-08-22 02:59:54.879 +10:00", + // }), + // JSON.stringify({ + // [this.logKeys.level]: "info", + // [this.logKeys.msg]: "test b", + // [this.logKeys.timestamp]: "2023-08-22 02:59:54.879 +10:00", + // }), + // JSON.stringify({ + // [this.logKeys.level]: "info", + // [this.logKeys.msg]: "msg c", + // [this.logKeys.timestamp]: "2023-08-22 02:59:54.879 +10:00", + // }), + // JSON.stringify({ + // [this.logKeys.level]: "info", + // [this.logKeys.msg]: "test d", + // [this.logKeys.timestamp]: "2023-08-22 02:59:54.879 +10:00", + // }), + // JSON.stringify({ + // [this.logKeys.level]: "info", + // [this.logKeys.msg]: "msg e", + // [this.logKeys.timestamp]: "2023-08-22 02:59:54.879 +10:00", + // }), + // JSON.stringify({ + // [this.logKeys.level]: "info", + // [this.logKeys.msg]: "test f", + // [this.logKeys.timestamp]: "2023-08-22 02:59:54.879 +10:00", + // }), + // JSON.stringify({ + // [this.logKeys.level]: "info", + // [this.logKeys.msg]: "msg g", + // [this.logKeys.timestamp]: "2023-08-22 02:59:54.879 +10:00", + // }), + // JSON.stringify({ + // [this.logKeys.level]: "info", + // [this.logKeys.msg]: "test h", + // [this.logKeys.timestamp]: "2023-08-22 02:59:54.879 +10:00", + // }), + // JSON.stringify({ + // [this.logKeys.level]: "info", + // [this.logKeys.msg]: "msg i", + // [this.logKeys.timestamp]: "2023-08-22 02:59:54.879 +10:00", + // }), + // ]); } }