From 9ae4b61bbf42c0c17de89c41b26051fed33c5401 Mon Sep 17 00:00:00 2001 From: Junhao Liao Date: Sun, 1 Sep 2024 16:58:33 -0400 Subject: [PATCH 1/3] new-log-viewer: Add `ClpIrDecoder`. (#62) --- new-log-viewer/package-lock.json | 7 ++ new-log-viewer/package.json | 1 + new-log-viewer/src/services/LogFileManager.ts | 85 +++++++++++-------- .../src/services/decoders/ClpIrDecoder.ts | 51 +++++++++++ new-log-viewer/src/typings/decoders.ts | 8 +- 5 files changed, 115 insertions(+), 37 deletions(-) create mode 100644 new-log-viewer/src/services/decoders/ClpIrDecoder.ts diff --git a/new-log-viewer/package-lock.json b/new-log-viewer/package-lock.json index 1cb00d53..682c74d0 100644 --- a/new-log-viewer/package-lock.json +++ b/new-log-viewer/package-lock.json @@ -10,6 +10,7 @@ "license": "Apache-2.0", "dependencies": { "axios": "^1.7.2", + "clp-ffi-js": "^0.1.0", "dayjs": "^1.11.11", "monaco-editor": "^0.50.0", "react": "^18.3.1", @@ -3996,6 +3997,12 @@ "node": ">=6" } }, + "node_modules/clp-ffi-js": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/clp-ffi-js/-/clp-ffi-js-0.1.0.tgz", + "integrity": "sha512-/g1EBxKDd6syknCGj7c/pM4tl1nEhLfRRf8zwaAfDQBxWcO0isXREFya8+TBVm2KTuik9O8hb9HidR17LtI3jg==", + "license": "Apache-2.0" + }, "node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", diff --git a/new-log-viewer/package.json b/new-log-viewer/package.json index c236450b..607f0756 100644 --- a/new-log-viewer/package.json +++ b/new-log-viewer/package.json @@ -19,6 +19,7 @@ "homepage": "https://github.com/y-scope/yscope-log-viewer#readme", "dependencies": { "axios": "^1.7.2", + "clp-ffi-js": "^0.1.0", "dayjs": "^1.11.11", "monaco-editor": "^0.50.0", "react": "^18.3.1", diff --git a/new-log-viewer/src/services/LogFileManager.ts b/new-log-viewer/src/services/LogFileManager.ts index 2dcfbf62..37b31290 100644 --- a/new-log-viewer/src/services/LogFileManager.ts +++ b/new-log-viewer/src/services/LogFileManager.ts @@ -1,6 +1,7 @@ import { Decoder, DecoderOptionsType, + LOG_EVENT_FILE_END_IDX, } from "../typings/decoders"; import {MAX_V8_STRING_LENGTH} from "../typings/js"; import { @@ -13,6 +14,7 @@ import {getUint8ArrayFrom} from "../utils/http"; import {getChunkNum} from "../utils/math"; import {formatSizeInBytes} from "../utils/units"; import {getBasenameFromUrlOrDefault} from "../utils/url"; +import ClpIrDecoder from "./decoders/ClpIrDecoder"; import JsonlDecoder from "./decoders/JsonlDecoder"; @@ -48,8 +50,6 @@ const loadFile = async (fileSrc: FileSrcType) class LogFileManager { readonly #pageSize: number; - readonly #fileData: Uint8Array; - readonly #fileName: string; #decoder: Decoder; @@ -60,21 +60,27 @@ class LogFileManager { * Private constructor for LogFileManager. This is not intended to be invoked publicly. * Instead, use LogFileManager.create() to create a new instance of the class. * + * @param decoder * @param fileName - * @param fileData * @param pageSize Page size for setting up pagination. - * @param decoderOptions Initial decoder options. */ constructor ( + decoder: Decoder, fileName: string, - fileData: Uint8Array, pageSize: number, - decoderOptions: DecoderOptionsType ) { this.#fileName = fileName; - this.#fileData = fileData; this.#pageSize = pageSize; - this.#decoder = this.#initDecoder(decoderOptions); + this.#decoder = decoder; + + // Build index for the entire file + const buildIdxResult = decoder.buildIdx(0, LOG_EVENT_FILE_END_IDX); + if (null !== buildIdxResult && 0 < buildIdxResult.numInvalidEvents) { + console.error("Invalid events found in decoder.buildIdx():", buildIdxResult); + } + + this.#numEvents = decoder.getEstimatedNumEvents(); + console.log(`Found ${this.#numEvents} log events.`); } get numEvents () { @@ -96,7 +102,41 @@ class LogFileManager { decoderOptions: DecoderOptionsType ): Promise { const {fileName, fileData} = await loadFile(fileSrc); - return new LogFileManager(fileName, fileData, pageSize, decoderOptions); + const decoder = await LogFileManager.#initDecoder(fileName, fileData, decoderOptions); + + return new LogFileManager(decoder, fileName, pageSize); + } + + /** + * Constructs a decoder instance based on the file extension. + * + * @param fileName + * @param fileData + * @param decoderOptions Initial decoder options. + * @return The constructed decoder. + * @throws {Error} if no decoder supports a file with the given extension. + */ + static async #initDecoder ( + fileName: string, + fileData: Uint8Array, + decoderOptions: DecoderOptionsType + ): Promise { + let decoder: Decoder; + if (fileName.endsWith(".jsonl")) { + decoder = new JsonlDecoder(fileData, decoderOptions); + } else if (fileName.endsWith(".clp.zst")) { + decoder = await ClpIrDecoder.create(fileData); + } else { + throw new Error(`No decoder supports ${fileName}`); + } + + if (fileData.length > MAX_V8_STRING_LENGTH) { + throw new Error(`Cannot handle files larger than ${ + formatSizeInBytes(MAX_V8_STRING_LENGTH) + } due to a limitation in Chromium-based browsers.`); + } + + return decoder; } /** @@ -154,33 +194,6 @@ class LogFileManager { }; } - /** - * Constructs a decoder instance based on the file extension. - * - * @param decoderOptions Initial decoder options. - * @return The constructed decoder. - * @throws {Error} if a decoder cannot be found. - */ - #initDecoder = (decoderOptions: DecoderOptionsType): Decoder => { - let decoder: Decoder; - if (this.#fileName.endsWith(".jsonl")) { - decoder = new JsonlDecoder(this.#fileData, decoderOptions); - } else { - throw new Error(`No decoder supports ${this.#fileName}`); - } - - if (this.#fileData.length > MAX_V8_STRING_LENGTH) { - throw new Error(`Cannot handle files larger than ${ - formatSizeInBytes(MAX_V8_STRING_LENGTH) - } due to a limitation in Chromium-based browsers.`); - } - - this.#numEvents = decoder.getEstimatedNumEvents(); - console.log(`Found ${this.#numEvents} log events.`); - - return decoder; - }; - /** * Gets the range of log event numbers for the page containing the given cursor. * diff --git a/new-log-viewer/src/services/decoders/ClpIrDecoder.ts b/new-log-viewer/src/services/decoders/ClpIrDecoder.ts new file mode 100644 index 00000000..4cab4590 --- /dev/null +++ b/new-log-viewer/src/services/decoders/ClpIrDecoder.ts @@ -0,0 +1,51 @@ +import clpFfiJsModuleInit, {ClpIrStreamReader} from "clp-ffi-js"; + +import {Nullable} from "../../typings/common"; +import { + Decoder, + DecodeResultType, + LogEventCount, +} from "../../typings/decoders"; + + +class ClpIrDecoder implements Decoder { + #streamReader: ClpIrStreamReader; + + constructor (streamReader: ClpIrStreamReader) { + this.#streamReader = streamReader; + } + + /** + * Creates a new ClpIrDecoder instance. + * + * @param dataArray The input data array to be passed to the decoder. + * @return The created ClpIrDecoder instance. + */ + static async create (dataArray: Uint8Array): Promise { + const module = await clpFfiJsModuleInit(); + const streamReader = new module.ClpIrStreamReader(dataArray); + return new ClpIrDecoder(streamReader); + } + + getEstimatedNumEvents (): number { + return this.#streamReader.getNumEventsBuffered(); + } + + buildIdx (beginIdx: number, endIdx: number): Nullable { + return { + numInvalidEvents: 0, + numValidEvents: this.#streamReader.deserializeRange(beginIdx, endIdx), + }; + } + + // eslint-disable-next-line class-methods-use-this + setDecoderOptions (): boolean { + return true; + } + + decode (beginIdx: number, endIdx: number): Nullable { + return this.#streamReader.decodeRange(beginIdx, endIdx); + } +} + +export default ClpIrDecoder; diff --git a/new-log-viewer/src/typings/decoders.ts b/new-log-viewer/src/typings/decoders.ts index c30a3700..53408184 100644 --- a/new-log-viewer/src/typings/decoders.ts +++ b/new-log-viewer/src/typings/decoders.ts @@ -45,7 +45,7 @@ interface Decoder { * When applicable, deserializes log events in the range `[beginIdx, endIdx)`. * * @param beginIdx - * @param endIdx + * @param endIdx End index. To deserialize to the end of the file, use `LOG_EVENT_FILE_END_IDX`. * @return Count of the successfully deserialized ("valid") log events and count of any * un-deserializable ("invalid") log events within the range; or null if any log event in the * range doesn't exist (e.g., the range exceeds the number of log events in the file). @@ -71,7 +71,13 @@ interface Decoder { decode(beginIdx: number, endIdx: number): Nullable; } +/** + * Index for specifying the end of the file when the exact number of log events it contains is + * unknown. + */ +const LOG_EVENT_FILE_END_IDX: number = 0; +export {LOG_EVENT_FILE_END_IDX}; export type { Decoder, DecodeResultType, From 4b65341513ca5087750cbb1c71dcbc3aa453b16a Mon Sep 17 00:00:00 2001 From: Junhao Liao Date: Mon, 2 Sep 2024 12:37:38 -0400 Subject: [PATCH 2/3] new-log-viewer: Add support for loading files through open dialog or drag-and-drop; Display file name in the UI. (#56) --- new-log-viewer/public/index.html | 3 + .../components/DropFileContainer/index.css | 31 ++++++ .../components/DropFileContainer/index.tsx | 96 +++++++++++++++++++ new-log-viewer/src/components/Layout.tsx | 25 ++++- .../src/contexts/StateContextProvider.tsx | 18 +++- new-log-viewer/src/index.css | 20 ++++ new-log-viewer/src/services/LogFileManager.ts | 10 +- new-log-viewer/src/services/MainWorker.ts | 8 +- new-log-viewer/src/typings/file.ts | 3 + new-log-viewer/src/typings/worker.ts | 15 +-- new-log-viewer/src/utils/file.ts | 25 +++++ 11 files changed, 234 insertions(+), 20 deletions(-) create mode 100644 new-log-viewer/src/components/DropFileContainer/index.css create mode 100644 new-log-viewer/src/components/DropFileContainer/index.tsx create mode 100644 new-log-viewer/src/typings/file.ts create mode 100644 new-log-viewer/src/utils/file.ts diff --git a/new-log-viewer/public/index.html b/new-log-viewer/public/index.html index fd19449b..799ad411 100644 --- a/new-log-viewer/public/index.html +++ b/new-log-viewer/public/index.html @@ -7,6 +7,9 @@ + + +
diff --git a/new-log-viewer/src/components/DropFileContainer/index.css b/new-log-viewer/src/components/DropFileContainer/index.css new file mode 100644 index 00000000..2bf46acd --- /dev/null +++ b/new-log-viewer/src/components/DropFileContainer/index.css @@ -0,0 +1,31 @@ +.drop-file-container { + width: 100%; + height: 100%; + position: relative; +} + +.drop-file-children { + width: 100%; + height: 100%; +} + +.hover-mask { + position: absolute; + top: 0; + width: 100%; + height: 100%; + background-color: rgba(2, 88, 168, 0.2); + display: flex; + align-items: center; + justify-content: center; + z-index: var(--ylv-drop-file-container-hover-mask-z-index); +} + +.hover-message { + padding: 8px; + color: #616161; + font-size: 0.875rem; + font-family: var(--ylv-ui-font-family), sans-serif; + background-color: #f3f3f3; + z-index: var(--ylv-drop-file-container-hover-message-z-index); +} diff --git a/new-log-viewer/src/components/DropFileContainer/index.tsx b/new-log-viewer/src/components/DropFileContainer/index.tsx new file mode 100644 index 00000000..a355c9e8 --- /dev/null +++ b/new-log-viewer/src/components/DropFileContainer/index.tsx @@ -0,0 +1,96 @@ +import React, { + useContext, + useState, +} from "react"; + +import {StateContext} from "../../contexts/StateContextProvider"; +import {CURSOR_CODE} from "../../typings/worker"; + +import "./index.css"; + + +interface DropFileContextProviderProps { + children: React.ReactNode; +} + +/** + * A container element to add drag & drop functionality to the child elements. + * + * @param props + * @param props.children + * @return + */ +const DropFileContainer = ({children}: DropFileContextProviderProps) => { + const {loadFile} = useContext(StateContext); + const [isFileHovering, setIsFileHovering] = useState(false); + + const handleDrag = (ev: React.DragEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + if ("dragenter" === ev.type) { + setIsFileHovering(true); + } else if ("dragleave" === ev.type) { + // Only stop the hover effect if the pointer leaves the bounding rectangle of the + // DropFileContainer. + // + // NOTE: "dragleave" could get fired when the wrapped `children` receive focus. Setting + // `pointer-events: none` on the children is viable but could cause the children to be + // unresponsive. So instead, we use the solution below. + const {bottom, left, right, top} = ev.currentTarget.getBoundingClientRect(); + if (ev.clientX >= left && ev.clientX <= right && + ev.clientY >= top && ev.clientY <= bottom) { + return; + } + + setIsFileHovering(false); + } + }; + + const handleDrop = (ev: React.DragEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + setIsFileHovering(false); + + const [file] = ev.dataTransfer.files; + if ("undefined" === typeof file) { + console.warn("No file dropped."); + + return; + } + loadFile(file, {code: CURSOR_CODE.LAST_EVENT, args: null}); + }; + + return ( +
+
+ {children} + {isFileHovering && ( +
+
+ Drop file to view +
+
+ )} +
+
+ ); +}; + +export default DropFileContainer; diff --git a/new-log-viewer/src/components/Layout.tsx b/new-log-viewer/src/components/Layout.tsx index 6ee937f3..149e23e8 100644 --- a/new-log-viewer/src/components/Layout.tsx +++ b/new-log-viewer/src/components/Layout.tsx @@ -19,15 +19,18 @@ import { LOCAL_STORAGE_KEY, THEME_NAME, } from "../typings/config"; +import {CURSOR_CODE} from "../typings/worker"; import {ACTION_NAME} from "../utils/actions"; import { getConfig, 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"; @@ -164,8 +167,10 @@ const handleLogEventNumInputChange = (ev: React.ChangeEvent) = */ const Layout = () => { const { - pageNum, + fileName, + loadFile, numEvents, + pageNum, } = useContext(StateContext); const {logEventNum} = useContext(UrlContext); @@ -176,6 +181,12 @@ const Layout = () => { copyPermalinkToClipboard({}, {logEventNum: numEvents}); }; + const handleOpenFileButtonClick = () => { + openFile((file) => { + loadFile(file, {code: CURSOR_CODE.LAST_EVENT, args: null}); + }); + }; + /** * Handles custom actions in the editor. * @@ -251,15 +262,25 @@ const Layout = () => { PageNum - {" "} {pageNum} + {" "} + | FileName - + {" "} + {fileName} + +
- + + +
diff --git a/new-log-viewer/src/contexts/StateContextProvider.tsx b/new-log-viewer/src/contexts/StateContextProvider.tsx index 32c0b993..e84e46bf 100644 --- a/new-log-viewer/src/contexts/StateContextProvider.tsx +++ b/new-log-viewer/src/contexts/StateContextProvider.tsx @@ -9,6 +9,7 @@ import React, { import {Nullable} from "../typings/common"; import {CONFIG_KEY} from "../typings/config"; +import {SEARCH_PARAM_NAMES} from "../typings/url"; import { BeginLineNumToLogEventNumMap, CURSOR_CODE, @@ -26,6 +27,7 @@ import { } from "../utils/math"; import { updateWindowUrlHashParams, + updateWindowUrlSearchParams, URL_HASH_PARAMS_DEFAULT, URL_SEARCH_PARAMS_DEFAULT, UrlContext, @@ -34,6 +36,7 @@ import { interface StateContextType { beginLineNumToLogEventNum: BeginLineNumToLogEventNumMap, + fileName: string, loadFile: (fileSrc: FileSrcType, cursor: CursorType) => void, logData: string, numEvents: number, @@ -45,8 +48,9 @@ const StateContext = createContext({} as StateContextType); /** * Default values of the state object. */ -const STATE_DEFAULT = Object.freeze({ +const STATE_DEFAULT: Readonly = Object.freeze({ beginLineNumToLogEventNum: new Map(), + fileName: "", loadFile: () => null, logData: "Loading...", numEvents: 0, @@ -106,6 +110,7 @@ const getLastLogEventNum = (beginLineNumToLogEventNum: BeginLineNumToLogEventNum const StateContextProvider = ({children}: StateContextProviderProps) => { const {filePath, logEventNum} = useContext(UrlContext); + const [fileName, setFileName] = useState(STATE_DEFAULT.fileName); const [logData, setLogData] = useState(STATE_DEFAULT.logData); const [numEvents, setNumEvents] = useState(STATE_DEFAULT.numEvents); const beginLineNumToLogEventNumRef = @@ -127,6 +132,10 @@ const StateContextProvider = ({children}: StateContextProviderProps) => { const {code, args} = ev.data; console.log(`[MainWorker -> Renderer] code=${code}`); switch (code) { + case WORKER_RESP_CODE.LOG_FILE_INFO: + setFileName(args.fileName); + setNumEvents(args.numEvents); + break; case WORKER_RESP_CODE.PAGE_DATA: { setLogData(args.logs); beginLineNumToLogEventNumRef.current = args.beginLineNumToLogEventNum; @@ -134,9 +143,6 @@ const StateContextProvider = ({children}: StateContextProviderProps) => { updateLogEventNumInUrl(lastLogEventNum, logEventNumRef.current); break; } - case WORKER_RESP_CODE.NUM_EVENTS: - setNumEvents(args.numEvents); - break; case WORKER_RESP_CODE.NOTIFICATION: // TODO: notifications should be shown in the UI when the NotificationProvider // is added @@ -149,6 +155,9 @@ const StateContextProvider = ({children}: StateContextProviderProps) => { }, []); const loadFile = useCallback((fileSrc: FileSrcType, cursor: CursorType) => { + if ("string" !== typeof fileSrc) { + updateWindowUrlSearchParams({[SEARCH_PARAM_NAMES.FILE_PATH]: null}); + } if (null !== mainWorkerRef.current) { mainWorkerRef.current.terminate(); } @@ -244,6 +253,7 @@ const StateContextProvider = ({children}: StateContextProviderProps) => { null); } else { - // TODO: support file loading via Open / Drag-n-drop - throw new Error("Read from file not yet supported"); + fileName = fileSrc.name; + fileData = new Uint8Array(await fileSrc.arrayBuffer()); } return { @@ -83,6 +83,10 @@ class LogFileManager { console.log(`Found ${this.#numEvents} log events.`); } + get fileName () { + return this.#fileName; + } + get numEvents () { return this.#numEvents; } diff --git a/new-log-viewer/src/services/MainWorker.ts b/new-log-viewer/src/services/MainWorker.ts index 25034326..87a1bd89 100644 --- a/new-log-viewer/src/services/MainWorker.ts +++ b/new-log-viewer/src/services/MainWorker.ts @@ -48,10 +48,10 @@ onmessage = async (ev: MessageEvent) => { args.decoderOptions ); - postResp( - WORKER_RESP_CODE.NUM_EVENTS, - {numEvents: LOG_FILE_MANAGER.numEvents} - ); + postResp(WORKER_RESP_CODE.LOG_FILE_INFO, { + fileName: LOG_FILE_MANAGER.fileName, + numEvents: LOG_FILE_MANAGER.numEvents, + }); postResp( WORKER_RESP_CODE.PAGE_DATA, LOG_FILE_MANAGER.loadPage(args.cursor) diff --git a/new-log-viewer/src/typings/file.ts b/new-log-viewer/src/typings/file.ts new file mode 100644 index 00000000..8b6d4af3 --- /dev/null +++ b/new-log-viewer/src/typings/file.ts @@ -0,0 +1,3 @@ +type OnFileOpenCallback = (file: File) => void; + +export type {OnFileOpenCallback}; diff --git a/new-log-viewer/src/typings/worker.ts b/new-log-viewer/src/typings/worker.ts index d9450567..0c88beb6 100644 --- a/new-log-viewer/src/typings/worker.ts +++ b/new-log-viewer/src/typings/worker.ts @@ -43,9 +43,9 @@ enum WORKER_REQ_CODE { } enum WORKER_RESP_CODE { + LOG_FILE_INFO = "fileInfo", PAGE_DATA = "pageData", - NUM_EVENTS = "numEvents", - NOTIFICATION = "notification" + NOTIFICATION = "notification", } type WorkerReqMap = { @@ -62,18 +62,19 @@ type WorkerReqMap = { }; type WorkerRespMap = { + [WORKER_RESP_CODE.LOG_FILE_INFO]: { + fileName: string, + numEvents: number, + }, [WORKER_RESP_CODE.PAGE_DATA]: { logs: string, beginLineNumToLogEventNum: BeginLineNumToLogEventNumMap, cursorLineNum: number - }; - [WORKER_RESP_CODE.NUM_EVENTS]: { - numEvents: number - }; + }, [WORKER_RESP_CODE.NOTIFICATION]: { logLevel: LOG_LEVEL, message: string - }; + }, }; type WorkerReq = T extends keyof WorkerReqMap ? diff --git a/new-log-viewer/src/utils/file.ts b/new-log-viewer/src/utils/file.ts new file mode 100644 index 00000000..36980915 --- /dev/null +++ b/new-log-viewer/src/utils/file.ts @@ -0,0 +1,25 @@ +import type {OnFileOpenCallback} from "../typings/file"; + + +/** + * Opens a file and invokes the provided callback on the file. + * + * @param onOpen + */ +const openFile = (onOpen: OnFileOpenCallback) => { + const input = document.createElement("input"); + input.type = "file"; + input.onchange = (ev: Event) => { + const target = ev.target as HTMLInputElement; + const [file] = target.files as FileList; + if ("undefined" === typeof file) { + console.error("No file selected"); + + return; + } + onOpen(file); + }; + input.click(); +}; + +export {openFile}; From 867b72e4dbc5b73ef483b9d1102121bcf22ab347 Mon Sep 17 00:00:00 2001 From: Junhao Liao Date: Mon, 2 Sep 2024 12:50:21 -0400 Subject: [PATCH 3/3] gh-actions: Add lint workflow for new-log-viewer. (#61) --- .github/workflows/lint.yaml | 44 +++++++++++++++++++ new-log-viewer/package.json | 4 ++ .../components/Editor/MonacoInstance/utils.ts | 1 + .../src/contexts/StateContextProvider.tsx | 33 +++++++++----- .../src/services/decoders/JsonlDecoder.ts | 1 + .../services/formatters/LogbackFormatter.ts | 1 + 6 files changed, 72 insertions(+), 12 deletions(-) create mode 100644 .github/workflows/lint.yaml diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml new file mode 100644 index 00000000..73b7542e --- /dev/null +++ b/.github/workflows/lint.yaml @@ -0,0 +1,44 @@ +name: "lint" + +on: + pull_request: + types: ["opened", "reopened"] + push: + schedule: + # Run at midnight UTC every day with 15 minutes delay added to avoid high load periods + - cron: "15 0 * * *" + workflow_dispatch: + +permissions: + # So the workflow can cancel in-progress jobs + actions: "write" + +concurrency: + group: "${{github.workflow}}-${{github.ref}}" + # Cancel in-progress jobs for efficiency + cancel-in-progress: true + +jobs: + lint-check: + runs-on: "ubuntu-latest" + permissions: + # So `eslint-annotate-action` can create / update status checks + checks: "write" + # So `eslint-annotate-action` can get pull request files + contents: "read" + steps: + - uses: "actions/checkout@v4" + with: + submodules: "recursive" + - uses: "actions/setup-node@v4" + with: + node-version: 22 + - run: "npm --prefix new-log-viewer/ clean-install" + - run: "npm --prefix new-log-viewer/ run lint:ci" + continue-on-error: true + - uses: "ataylorme/eslint-annotate-action@v3" + with: + fail-on-error: true + fail-on-warning: true + only-pr-files: true + report-json: "./new-log-viewer/eslint-report.json" diff --git a/new-log-viewer/package.json b/new-log-viewer/package.json index 607f0756..ee1cfae1 100644 --- a/new-log-viewer/package.json +++ b/new-log-viewer/package.json @@ -5,6 +5,10 @@ "main": "src/index.tsx", "scripts": { "build": "webpack --config webpack.prod.js", + "lint": "npm run lint:check", + "lint:ci": "npm run lint:check -- --output-file eslint-report.json --format json", + "lint:check": "eslint src webpack.*.js", + "lint:fix": "npm run lint:check -- --fix", "start": "webpack serve --open --config webpack.dev.js" }, "repository": { diff --git a/new-log-viewer/src/components/Editor/MonacoInstance/utils.ts b/new-log-viewer/src/components/Editor/MonacoInstance/utils.ts index 45d8e24e..16b8e42c 100644 --- a/new-log-viewer/src/components/Editor/MonacoInstance/utils.ts +++ b/new-log-viewer/src/components/Editor/MonacoInstance/utils.ts @@ -41,6 +41,7 @@ const createMonacoEditor = ( const editor = monaco.editor.create( editorContainer, { + // eslint-disable-next-line no-warning-comments // TODO: Add custom observer to debounce automatic layout automaticLayout: true, maxTokenizationLineLength: 30_000, diff --git a/new-log-viewer/src/contexts/StateContextProvider.tsx b/new-log-viewer/src/contexts/StateContextProvider.tsx index e84e46bf..bf3d4ec0 100644 --- a/new-log-viewer/src/contexts/StateContextProvider.tsx +++ b/new-log-viewer/src/contexts/StateContextProvider.tsx @@ -99,6 +99,23 @@ const getLastLogEventNum = (beginLineNumToLogEventNum: BeginLineNumToLogEventNum return lastLogEventNum; }; +/** + * Sends a post message to a worker with the given code and arguments. This wrapper around + * `worker.postMessage()` ensures type safety for both the request code and its corresponding + * arguments. + * + * @param worker + * @param code + * @param args + */ +const workerPostReq = ( + worker: Worker, + code: T, + args: WorkerReq +) => { + worker.postMessage({code, args}); +}; + /** * Provides state management for the application. This provider must be wrapped by * UrlContextProvider to function correctly. @@ -121,13 +138,6 @@ const StateContextProvider = ({children}: StateContextProviderProps) => { const mainWorkerRef = useRef(null); - const mainWorkerPostReq = useCallback(( - code: T, - args: WorkerReq - ) => { - mainWorkerRef.current?.postMessage({code, args}); - }, []); - const handleMainWorkerResp = useCallback((ev: MessageEvent) => { const {code, args} = ev.data; console.log(`[MainWorker -> Renderer] code=${code}`); @@ -144,6 +154,7 @@ const StateContextProvider = ({children}: StateContextProviderProps) => { break; } case WORKER_RESP_CODE.NOTIFICATION: + // eslint-disable-next-line no-warning-comments // TODO: notifications should be shown in the UI when the NotificationProvider // is added console.error(args.logLevel, args.message); @@ -165,7 +176,7 @@ const StateContextProvider = ({children}: StateContextProviderProps) => { new URL("../services/MainWorker.ts", import.meta.url) ); mainWorkerRef.current.onmessage = handleMainWorkerResp; - mainWorkerPostReq(WORKER_REQ_CODE.LOAD_FILE, { + workerPostReq(mainWorkerRef.current, WORKER_REQ_CODE.LOAD_FILE, { fileSrc: fileSrc, pageSize: getConfig(CONFIG_KEY.PAGE_SIZE), cursor: cursor, @@ -173,7 +184,6 @@ const StateContextProvider = ({children}: StateContextProviderProps) => { }); }, [ handleMainWorkerResp, - mainWorkerPostReq, ]); // Synchronize `logEventNumRef` with `logEventNum`. @@ -192,7 +202,7 @@ const StateContextProvider = ({children}: StateContextProviderProps) => { // On `logEventNum` update, clamp it then switch page if necessary or simply update the URL. useEffect(() => { - if (URL_HASH_PARAMS_DEFAULT.logEventNum === logEventNum) { + if (null === mainWorkerRef.current || URL_HASH_PARAMS_DEFAULT.logEventNum === logEventNum) { return; } @@ -211,7 +221,7 @@ const StateContextProvider = ({children}: StateContextProviderProps) => { // NOTE: We don't need to call `updateLogEventNumInUrl()` since it's called when // handling the `WORKER_RESP_CODE.PAGE_DATA` response (the response to // `WORKER_REQ_CODE.LOAD_PAGE` requests) . - mainWorkerPostReq(WORKER_REQ_CODE.LOAD_PAGE, { + workerPostReq(mainWorkerRef.current, WORKER_REQ_CODE.LOAD_PAGE, { cursor: {code: CURSOR_CODE.PAGE_NUM, args: {pageNum: newPageNum}}, decoderOptions: getConfig(CONFIG_KEY.DECODER_OPTIONS), }); @@ -222,7 +232,6 @@ const StateContextProvider = ({children}: StateContextProviderProps) => { }, [ numEvents, logEventNum, - mainWorkerPostReq, ]); // On `filePath` update, load file. diff --git a/new-log-viewer/src/services/decoders/JsonlDecoder.ts b/new-log-viewer/src/services/decoders/JsonlDecoder.ts index d1ca1469..62a5b21e 100644 --- a/new-log-viewer/src/services/decoders/JsonlDecoder.ts +++ b/new-log-viewer/src/services/decoders/JsonlDecoder.ts @@ -82,6 +82,7 @@ class JsonlDecoder implements Decoder { return null; } + // eslint-disable-next-line no-warning-comments // TODO We could probably optimize this to avoid checking `#invalidLogEventIdxToRawLine` on // every iteration. const results: DecodeResultType[] = []; diff --git a/new-log-viewer/src/services/formatters/LogbackFormatter.ts b/new-log-viewer/src/services/formatters/LogbackFormatter.ts index 6f827c23..5d310df1 100644 --- a/new-log-viewer/src/services/formatters/LogbackFormatter.ts +++ b/new-log-viewer/src/services/formatters/LogbackFormatter.ts @@ -154,6 +154,7 @@ class LogbackFormatter implements Formatter { * @return The formatted string. */ #formatVariables (formatString: string, logEvent: JsonObject): string { + // eslint-disable-next-line no-warning-comments // TODO These don't handle the case where a variable value may contain a '%' itself for (const key of this.#keys) { if (false === (key in logEvent)) {