diff --git a/new-log-viewer/src/components/Layout.tsx b/new-log-viewer/src/components/Layout.tsx index 8fda302a..dffc071c 100644 --- a/new-log-viewer/src/components/Layout.tsx +++ b/new-log-viewer/src/components/Layout.tsx @@ -33,10 +33,6 @@ import { setConfig, } from "../utils/config"; import {openFile} from "../utils/file"; -import { - getFirstItemNumInNextChunk, - getLastItemNumInPrevChunk, -} from "../utils/math"; import DropFileContainer from "./DropFileContainer"; import Editor from "./Editor"; import {goToPositionAndCenter} from "./Editor/MonacoInstance/utils"; @@ -183,6 +179,8 @@ const handleLogEventNumInputChange = (ev: React.ChangeEvent) = const Layout = () => { const { fileName, + firstLogEventNumPerPage, + lastLogEventNumPerPage, numEvents, numFilteredEvents, pageNum, @@ -194,8 +192,10 @@ const Layout = () => { const [selectedLogLevels, setSelectedLogLevels] = useState(LOG_LEVEL_NAMES_LIST as number[]); + const firstLogEventNumPerPageRef = useRef(firstLogEventNumPerPage); + const lastLogEventNumPerPageRef = useRef(lastLogEventNumPerPage); const logEventNumRef = useRef>(logEventNum); - const numFilteredEventsRef = useRef>(numFilteredEvents); + const numEventsRef = useRef>(numEvents); const handleCopyLinkButtonClick = () => { copyPermalinkToClipboard({}, {logEventNum: numEvents}); @@ -226,28 +226,42 @@ const Layout = () => { editor: monaco.editor.IStandaloneCodeEditor, actionName: ACTION_NAME ) => { - const pageSize = getConfig(CONFIG_KEY.PAGE_SIZE); + const [firstFilteredLogEventNum] = firstLogEventNumPerPageRef.current; + const lastFilteredLogEventNum = firstLogEventNumPerPageRef.current.at(-1); switch (actionName) { case ACTION_NAME.FIRST_PAGE: - updateWindowUrlHashParams({logEventNum: 1}); + updateWindowUrlHashParams({logEventNum: firstFilteredLogEventNum || 1}); break; case ACTION_NAME.PREV_PAGE: if (null !== logEventNumRef.current) { + const lastLogEventNumOnPrevPage = lastLogEventNumPerPageRef.current.findLast( + (value: number) => (logEventNumRef.current as number > value) + ) || firstFilteredLogEventNum; + updateWindowUrlHashParams({ - logEventNum: getLastItemNumInPrevChunk(logEventNumRef.current, pageSize), + logEventNum: lastLogEventNumOnPrevPage || firstFilteredLogEventNum || 1, }); } break; case ACTION_NAME.NEXT_PAGE: if (null !== logEventNumRef.current) { + if ("undefined" === typeof lastFilteredLogEventNum) { + return; + } + const firstLogEventNumOnNextPage = firstLogEventNumPerPageRef.current.find( + (value: number) => (logEventNumRef.current as number < value) + ); + updateWindowUrlHashParams({ - logEventNum: getFirstItemNumInNextChunk(logEventNumRef.current, pageSize), + logEventNum: firstLogEventNumOnNextPage || logEventNumRef.current < lastFilteredLogEventNum ? + lastFilteredLogEventNum : + numEventsRef.current, }); } break; case ACTION_NAME.LAST_PAGE: - updateWindowUrlHashParams({logEventNum: numFilteredEventsRef.current}); + updateWindowUrlHashParams({logEventNum: lastFilteredLogEventNum || numEventsRef.current}); break; case ACTION_NAME.PAGE_TOP: goToPositionAndCenter(editor, {lineNumber: 1, column: 1}); @@ -260,19 +274,30 @@ const Layout = () => { goToPositionAndCenter(editor, {lineNumber: lineCount, column: 1}); break; } - default: break; + default: + break; } }, []); + // Synchronize `firstLogEventNumPerPageRef` with `firstLogEventNumPerPage`. + useEffect(() => { + firstLogEventNumPerPageRef.current = firstLogEventNumPerPage; + }, [firstLogEventNumPerPage]); + + // Synchronize `lastLogEventNumPerPageRef` with `lastLogEventNumPerPage`. + useEffect(() => { + lastLogEventNumPerPageRef.current = lastLogEventNumPerPage; + }, [lastLogEventNumPerPage]); + // Synchronize `logEventNumRef` with `logEventNum`. useEffect(() => { logEventNumRef.current = logEventNum; }, [logEventNum]); - // Synchronize `numFilteredEventsRef` with `numFilteredEvents`. + // Synchronize `numEventsRef` with `numEvents`. useEffect(() => { - numFilteredEventsRef.current = numFilteredEvents; - }, [numFilteredEvents]); + numEventsRef.current = numEvents; + }, [numEvents]); return ( <> @@ -287,8 +312,8 @@ const Layout = () => { logEventNum} onChange={handleLogEventNumInputChange}/> {" / "} - {numFilteredEvents} - {" | "} + {numEvents} + {` (NumFilteredEvents - ${numFilteredEvents}) | `} PageNum - {" "} {pageNum} diff --git a/new-log-viewer/src/contexts/StateContextProvider.tsx b/new-log-viewer/src/contexts/StateContextProvider.tsx index 0753bf65..4d7d1795 100644 --- a/new-log-viewer/src/contexts/StateContextProvider.tsx +++ b/new-log-viewer/src/contexts/StateContextProvider.tsx @@ -41,6 +41,8 @@ import { interface StateContextType { beginLineNumToLogEventNum: BeginLineNumToLogEventNumMap, fileName: string, + firstLogEventNumPerPage: number[], + lastLogEventNumPerPage: number[], logData: string, logLevelFilter: LogLevelFilter, numEvents: number, @@ -59,6 +61,8 @@ const StateContext = createContext({} as StateContextType); const STATE_DEFAULT: Readonly = Object.freeze({ beginLineNumToLogEventNum: new Map(), fileName: "", + firstLogEventNumPerPage: [], + lastLogEventNumPerPage: [], logData: "Loading...", logLevelFilter: null, numEvents: 0, @@ -140,6 +144,8 @@ const StateContextProvider = ({children}: StateContextProviderProps) => { const {filePath, logEventNum} = useContext(UrlContext); const [fileName, setFileName] = useState(STATE_DEFAULT.fileName); + const [firstLogEventNumPerPage, setFirstLogEventNumPerPage] = useState([]); + const [lastLogEventNumPerPage, setLastLogEventNumPerPage] = useState([]); const [logData, setLogData] = useState(STATE_DEFAULT.logData); const [numEvents, setNumEvents] = useState(STATE_DEFAULT.numEvents); const [numFilteredEvents, setNumFilteredEvents] = @@ -171,11 +177,19 @@ const StateContextProvider = ({children}: StateContextProviderProps) => { setLogData(args.logs); beginLineNumToLogEventNumRef.current = args.beginLineNumToLogEventNum; const lastLogEventNum = getLastLogEventNum(args.beginLineNumToLogEventNum); - updateLogEventNumInUrl(lastLogEventNum, logEventNumRef.current); + updateLogEventNumInUrl( + lastLogEventNum, + Array.from(args.beginLineNumToLogEventNum.values()) + .includes(logEventNumRef.current as number) ? + logEventNumRef.current : + lastLogEventNum + ); break; } case WORKER_RESP_CODE.VIEW_INFO: setNumFilteredEvents(args.numFilteredEvents); + setFirstLogEventNumPerPage(args.firstLogEventNumPerPage); + setLastLogEventNumPerPage(args.lastLogEventNumPerPage); break; default: console.error(`Unexpected ev.data: ${JSON.stringify(ev.data)}`); @@ -234,21 +248,26 @@ const StateContextProvider = ({children}: StateContextProviderProps) => { // On `logEventNum` update, clamp it then switch page if necessary or simply update the URL. useEffect(() => { - if (null === mainWorkerRef.current || URL_HASH_PARAMS_DEFAULT.logEventNum === logEventNum) { + if (null === mainWorkerRef.current || URL_HASH_PARAMS_DEFAULT.logEventNum === logEventNum || + 0 === firstLogEventNumPerPage.length) { return; } - const newPageNum = clamp( - getChunkNum(logEventNum, getConfig(CONFIG_KEY.PAGE_SIZE)), - 1, - numPagesRef.current - ); + const newPageNum = 1 + + firstLogEventNumPerPage.findLastIndex((value: number) => value <= logEventNum); + + if (0 === newPageNum) { + return; + } // Request a page switch only if it's not the initial page load. if (STATE_DEFAULT.pageNum !== pageNumRef.current) { if (newPageNum === pageNumRef.current) { // Don't need to switch pages so just update `logEventNum` in the URL. - updateLogEventNumInUrl(numFilteredEvents, logEventNumRef.current); + updateLogEventNumInUrl( + numEvents, + logEventNumRef.current + ); } else { // NOTE: We don't need to call `updateLogEventNumInUrl()` since it's called when // handling the `WORKER_RESP_CODE.PAGE_DATA` response (the response to @@ -265,6 +284,7 @@ const StateContextProvider = ({children}: StateContextProviderProps) => { pageNumRef.current = newPageNum; }, [ + firstLogEventNumPerPage, numFilteredEvents, logEventNum, ]); @@ -298,6 +318,8 @@ const StateContextProvider = ({children}: StateContextProviderProps) => { value={{ beginLineNumToLogEventNum: beginLineNumToLogEventNumRef.current, fileName: fileName, + firstLogEventNumPerPage: firstLogEventNumPerPage, + lastLogEventNumPerPage: lastLogEventNumPerPage, logData: logData, logLevelFilter: logLevelFilterRef.current, numEvents: numEvents, diff --git a/new-log-viewer/src/services/LogFileManager.ts b/new-log-viewer/src/services/LogFileManager.ts index 07a3e992..48f7583f 100644 --- a/new-log-viewer/src/services/LogFileManager.ts +++ b/new-log-viewer/src/services/LogFileManager.ts @@ -54,10 +54,14 @@ class LogFileManager { #decoder: Decoder; - #numEvents: number = 0; + readonly #numEvents: number = 0; #numFilteredEvents: number = 0; + #firstLogEventNumPerPage: number[] = []; + + #lastLogEventNumPerPage: number[] = []; + /** * Private constructor for LogFileManager. This is not intended to be invoked publicly. * Instead, use LogFileManager.create() to create a new instance of the class. @@ -82,7 +86,7 @@ class LogFileManager { } this.#numEvents = decoder.getEstimatedNumEvents(); - this.#numFilteredEvents = decoder.getNumFilteredEvents(); + this.#computerPageBoundaries(); console.log( `Found ${this.#numEvents} log events.`, `${this.#numFilteredEvents} matches current filter.` @@ -101,6 +105,14 @@ class LogFileManager { return this.#numFilteredEvents; } + get firstLogEventNumPerPage () { + return this.#firstLogEventNumPerPage; + } + + get lastLogEventNumPerPage () { + return this.#lastLogEventNumPerPage; + } + /** * Creates a new LogFileManager. * @@ -160,7 +172,7 @@ class LogFileManager { */ setDecoderOptions (options: DecoderOptions) { this.#decoder.setDecoderOptions(options); - this.#numFilteredEvents = this.#decoder.getNumFilteredEvents(); + this.#computerPageBoundaries(); } /** @@ -209,6 +221,18 @@ class LogFileManager { }; } + #computerPageBoundaries () { + this.#firstLogEventNumPerPage.length = 0; + this.#lastLogEventNumPerPage.length = 0; + const filteredLogEvents = this.#decoder.getFilteredLogEvents(); + this.#numFilteredEvents = filteredLogEvents.length; + for (let i = 0; i < this.#numFilteredEvents; i += this.#pageSize) { + this.#firstLogEventNumPerPage.push(1 + (filteredLogEvents[i] as number)); + this.#lastLogEventNumPerPage.push(1 + + (filteredLogEvents[i + this.#pageSize - 1] as number)); + } + } + /** * Gets the range of log event numbers for the page containing the given cursor. * diff --git a/new-log-viewer/src/services/MainWorker.ts b/new-log-viewer/src/services/MainWorker.ts index 8cf10758..4a4cd94e 100644 --- a/new-log-viewer/src/services/MainWorker.ts +++ b/new-log-viewer/src/services/MainWorker.ts @@ -54,6 +54,8 @@ onmessage = async (ev: MessageEvent) => { }); postResp(WORKER_RESP_CODE.VIEW_INFO, { numFilteredEvents: LOG_FILE_MANAGER.numFilteredEvents, + firstLogEventNumPerPage: LOG_FILE_MANAGER.firstLogEventNumPerPage, + lastLogEventNumPerPage: LOG_FILE_MANAGER.lastLogEventNumPerPage, }); postResp( WORKER_RESP_CODE.PAGE_DATA, @@ -69,6 +71,8 @@ onmessage = async (ev: MessageEvent) => { LOG_FILE_MANAGER.setDecoderOptions(args.decoderOptions); postResp(WORKER_RESP_CODE.VIEW_INFO, { numFilteredEvents: LOG_FILE_MANAGER.numFilteredEvents, + firstLogEventNumPerPage: LOG_FILE_MANAGER.firstLogEventNumPerPage, + lastLogEventNumPerPage: LOG_FILE_MANAGER.lastLogEventNumPerPage, }); } postResp( diff --git a/new-log-viewer/src/services/decoders/ClpIrDecoder.ts b/new-log-viewer/src/services/decoders/ClpIrDecoder.ts index 71e5b1a0..b441f363 100644 --- a/new-log-viewer/src/services/decoders/ClpIrDecoder.ts +++ b/new-log-viewer/src/services/decoders/ClpIrDecoder.ts @@ -31,10 +31,10 @@ class ClpIrDecoder implements Decoder { return this.#streamReader.getNumEventsBuffered(); } - getNumFilteredEvents (): number { + getFilteredLogEvents (): number[] { // eslint-disable-next-line no-warning-comments // TODO: fix this after log level filtering is implemented in clp-ffi-js - return this.#streamReader.getNumEventsBuffered(); + return Array.from({length: this.#streamReader.getNumEventsBuffered()}, (_, index) => index); } buildIdx (beginIdx: number, endIdx: number): Nullable { diff --git a/new-log-viewer/src/services/decoders/JsonlDecoder.ts b/new-log-viewer/src/services/decoders/JsonlDecoder.ts index 90d8dc9b..d2aac2f0 100644 --- a/new-log-viewer/src/services/decoders/JsonlDecoder.ts +++ b/new-log-viewer/src/services/decoders/JsonlDecoder.ts @@ -68,8 +68,8 @@ class JsonlDecoder implements Decoder { return this.#logEvents.length; } - getNumFilteredEvents () : number { - return this.#filteredLogIndices.length; + getFilteredLogEvents () : number[] { + return this.#filteredLogIndices; } buildIdx (beginIdx: number, endIdx: number): Nullable { diff --git a/new-log-viewer/src/typings/decoders.ts b/new-log-viewer/src/typings/decoders.ts index 70462ec8..532ea611 100644 --- a/new-log-viewer/src/typings/decoders.ts +++ b/new-log-viewer/src/typings/decoders.ts @@ -62,11 +62,11 @@ interface Decoder { getEstimatedNumEvents(): number; /** - * Retrieves a number of filtered log events usually based on log level. + * Retrieves the filtered log events indices, which is usually based on log level. * - * @return The number of filtered events. + * @return Indices of the filtered events. */ - getNumFilteredEvents(): number; + getFilteredLogEvents(): number[]; /** * When applicable, deserializes log events in the range `[beginIdx, endIdx)`. diff --git a/new-log-viewer/src/typings/worker.ts b/new-log-viewer/src/typings/worker.ts index 94c871e7..bd2dd6cc 100644 --- a/new-log-viewer/src/typings/worker.ts +++ b/new-log-viewer/src/typings/worker.ts @@ -54,11 +54,11 @@ type WorkerReqMap = { fileSrc: FileSrcType, pageSize: number, cursor: CursorType, - decoderOptions: DecoderOptions + decoderOptions: DecoderOptions, }, [WORKER_REQ_CODE.LOAD_PAGE]: { cursor: CursorType, - decoderOptions?: DecoderOptions + decoderOptions?: DecoderOptions, }, }; @@ -77,7 +77,9 @@ type WorkerRespMap = { cursorLineNum: number, }, [WORKER_RESP_CODE.VIEW_INFO]: { + firstLogEventNumPerPage: number[], numFilteredEvents: number, + lastLogEventNumPerPage: number[], } }; diff --git a/new-log-viewer/tsconfig.json b/new-log-viewer/tsconfig.json index 8637e001..a8c8979d 100644 --- a/new-log-viewer/tsconfig.json +++ b/new-log-viewer/tsconfig.json @@ -21,9 +21,9 @@ /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ /* Specify a set of bundled library declaration files that describe the target runtime environment. */ "lib": [ - "ES2022", "DOM", "DOM.Iterable", + "ES2023", "WebWorker", ], "jsx": "react-jsx",