From b127bbf78775f97346dbfe90c2d467140e06ab26 Mon Sep 17 00:00:00 2001 From: Henry <50559854+Henry8192@users.noreply.github.com> Date: Sun, 15 Sep 2024 18:47:08 -0400 Subject: [PATCH] Export Logs draft --- .../src/components/MenuBar/index.tsx | 10 ++- .../src/contexts/StateContextProvider.tsx | 72 +++++++++++++++---- new-log-viewer/src/services/LogFileManager.ts | 25 +++++++ new-log-viewer/src/services/MainWorker.ts | 14 ++++ new-log-viewer/src/typings/worker.ts | 8 +++ new-log-viewer/src/utils/config.ts | 2 + 6 files changed, 116 insertions(+), 15 deletions(-) diff --git a/new-log-viewer/src/components/MenuBar/index.tsx b/new-log-viewer/src/components/MenuBar/index.tsx index e9955286..ab0154be 100644 --- a/new-log-viewer/src/components/MenuBar/index.tsx +++ b/new-log-viewer/src/components/MenuBar/index.tsx @@ -11,6 +11,7 @@ import { } from "@mui/joy"; import Description from "@mui/icons-material/Description"; +import Download from "@mui/icons-material/Download"; import FileOpenIcon from "@mui/icons-material/FileOpen"; import Settings from "@mui/icons-material/Settings"; @@ -30,7 +31,7 @@ import "./index.css"; * @return */ const MenuBar = () => { - const {fileName, loadFile} = useContext(StateContext); + const {fileName, exportLogs, loadFile} = useContext(StateContext); const [isSettingsModalOpen, setIsSettingsModalOpen] = useState(false); @@ -48,6 +49,10 @@ const MenuBar = () => { setIsSettingsModalOpen(true); }; + const handleExportLogsButtonClick = () => { + exportLogs(); + }; + return ( <> @@ -76,6 +81,9 @@ const MenuBar = () => { > + + + void, - loadPage: (newPageNum: number) => void, logData: string, numEvents: number, numPages: number, - pageNum: Nullable + pageNum: Nullable, + + exportLogs: () => void, + loadFile: (fileSrc: FileSrcType, cursor: CursorType) => void, + loadPage: (newPageNum: number) => void, } const StateContext = createContext({} as StateContextType); @@ -52,12 +57,14 @@ const StateContext = createContext({} as StateContextType); const STATE_DEFAULT: Readonly = Object.freeze({ beginLineNumToLogEventNum: new Map(), fileName: "", - loadFile: () => null, - loadPage: () => null, logData: "Loading...", numEvents: 0, numPages: 0, pageNum: 0, + + exportLogs: () => null, + loadFile: () => null, + loadPage: () => null, }); interface StateContextProviderProps { @@ -138,17 +145,44 @@ const StateContextProvider = ({children}: StateContextProviderProps) => { const logEventNumRef = useRef(logEventNum); const numPagesRef = useRef(STATE_DEFAULT.numPages); const pageNumRef = useRef>(STATE_DEFAULT.pageNum); + const receivedNumChunksRef = useRef(0); const mainWorkerRef = useRef(null); const handleMainWorkerResp = useCallback((ev: MessageEvent) => { const {code, args} = ev.data; + + // Create a file blob and push the data inside + const blob = new Blob(); + const url = URL.createObjectURL(blob); + const numChunks = Math.ceil(numEvents / EXPORT_LOGS_CHUNK_SIZE); + console.log(`[MainWorker -> Renderer] code=${code}`); switch (code) { + case WORKER_RESP_CODE.CHUNK_DATA: + receivedNumChunksRef.current += 1; + console.log(receivedNumChunksRef.current, args.logs); + + // If all chunks are received, trigger the download of the file + if (numChunks === receivedNumChunksRef.current) { + const link = document.createElement("a"); + link.href = url; + link.download = `${fileName}-exported-${new Date().toISOString() + .replace(/[:.]/g, "-")}.log`; + link.click(); + URL.revokeObjectURL(url); + } + break; case WORKER_RESP_CODE.LOG_FILE_INFO: setFileName(args.fileName); setNumEvents(args.numEvents); 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); + break; case WORKER_RESP_CODE.PAGE_DATA: { setLogData(args.logs); beginLineNumToLogEventNumRef.current = args.beginLineNumToLogEventNum; @@ -156,18 +190,26 @@ const StateContextProvider = ({children}: StateContextProviderProps) => { updateLogEventNumInUrl(lastLogEventNum, logEventNumRef.current); 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); - break; default: console.error(`Unexpected ev.data: ${JSON.stringify(ev.data)}`); break; } }, []); + const exportLogs = useCallback(() => { + if (null === mainWorkerRef.current) { + console.error("Unexpected null mainWorkerRef.current"); + + return; + } + receivedNumChunksRef.current = 0; + workerPostReq( + mainWorkerRef.current, + WORKER_REQ_CODE.EXPORT_LOG, + {decoderOptions: getConfig(CONFIG_KEY.DECODER_OPTIONS)} + ); + }, []); + const loadFile = useCallback((fileSrc: FileSrcType, cursor: CursorType) => { if ("string" !== typeof fileSrc) { updateWindowUrlSearchParams({[SEARCH_PARAM_NAMES.FILE_PATH]: null}); @@ -275,12 +317,14 @@ const StateContextProvider = ({children}: StateContextProviderProps) => { value={{ beginLineNumToLogEventNum: beginLineNumToLogEventNumRef.current, fileName: fileName, - loadFile: loadFile, - loadPage: loadPage, logData: logData, numEvents: numEvents, numPages: numPagesRef.current, pageNum: pageNumRef.current, + + exportLogs: exportLogs, + loadFile: loadFile, + loadPage: loadPage, }} > {children} diff --git a/new-log-viewer/src/services/LogFileManager.ts b/new-log-viewer/src/services/LogFileManager.ts index ea73d7fa..176973c8 100644 --- a/new-log-viewer/src/services/LogFileManager.ts +++ b/new-log-viewer/src/services/LogFileManager.ts @@ -10,6 +10,7 @@ import { CursorType, FileSrcType, } from "../typings/worker"; +import {EXPORT_LOGS_CHUNK_SIZE} from "../utils/config"; import {getUint8ArrayFrom} from "../utils/http"; import {getChunkNum} from "../utils/math"; import {formatSizeInBytes} from "../utils/units"; @@ -152,6 +153,30 @@ class LogFileManager { this.#decoder.setDecoderOptions(options); } + loadChunk (eventIdx: number): { + logs: string, + } { + const results = this.#decoder.decode( + eventIdx, + Math.min(eventIdx + EXPORT_LOGS_CHUNK_SIZE, this.#numEvents) + ); + + if (null === results) { + throw new Error("Error occurred during decoding chunk. " + + `eventIdx=${eventIdx});`); + } + + const messages: string[] = []; + results.forEach((r) => { + const [msg] = r; + messages.push(msg); + }); + + return { + logs: messages.join(""), + }; + } + /** * Loads a page of log events based on the provided cursor. * diff --git a/new-log-viewer/src/services/MainWorker.ts b/new-log-viewer/src/services/MainWorker.ts index 87a1bd89..3f8bc351 100644 --- a/new-log-viewer/src/services/MainWorker.ts +++ b/new-log-viewer/src/services/MainWorker.ts @@ -9,6 +9,7 @@ import { WORKER_RESP_CODE, WorkerResp, } from "../typings/worker"; +import {EXPORT_LOGS_CHUNK_SIZE} from "../utils/config"; import LogFileManager from "./LogFileManager"; @@ -17,6 +18,7 @@ dayjs.extend(dayjsUtc); dayjs.extend(dayjsTimezone); /* eslint-enable import/no-named-as-default-member */ + /** * Manager for the currently opened log file. */ @@ -41,6 +43,18 @@ onmessage = async (ev: MessageEvent) => { try { switch (code) { + case WORKER_REQ_CODE.EXPORT_LOG: { + if (null === LOG_FILE_MANAGER) { + throw new Error("Log file manager hasn't been initialized"); + } + + let decodedEventIdx = 0; + while (LOG_FILE_MANAGER.numEvents > decodedEventIdx) { + postResp(WORKER_RESP_CODE.CHUNK_DATA, LOG_FILE_MANAGER.loadChunk(decodedEventIdx)); + decodedEventIdx += EXPORT_LOGS_CHUNK_SIZE; + } + break; + } case WORKER_REQ_CODE.LOAD_FILE: { LOG_FILE_MANAGER = await LogFileManager.create( args.fileSrc, diff --git a/new-log-viewer/src/typings/worker.ts b/new-log-viewer/src/typings/worker.ts index 0c88beb6..82b186c0 100644 --- a/new-log-viewer/src/typings/worker.ts +++ b/new-log-viewer/src/typings/worker.ts @@ -38,17 +38,22 @@ type BeginLineNumToLogEventNumMap = Map; * Enum of the protocol code for communications between the renderer and MainWorker. */ enum WORKER_REQ_CODE { + EXPORT_LOG = "exportLog", LOAD_FILE = "loadFile", LOAD_PAGE = "loadPage", } enum WORKER_RESP_CODE { + CHUNK_DATA = "chunkData", LOG_FILE_INFO = "fileInfo", PAGE_DATA = "pageData", NOTIFICATION = "notification", } type WorkerReqMap = { + [WORKER_REQ_CODE.EXPORT_LOG]: { + decoderOptions: DecoderOptionsType + } [WORKER_REQ_CODE.LOAD_FILE]: { fileSrc: FileSrcType, pageSize: number, @@ -62,6 +67,9 @@ type WorkerReqMap = { }; type WorkerRespMap = { + [WORKER_RESP_CODE.CHUNK_DATA]: { + logs: string + }, [WORKER_RESP_CODE.LOG_FILE_INFO]: { fileName: string, numEvents: number, diff --git a/new-log-viewer/src/utils/config.ts b/new-log-viewer/src/utils/config.ts index 0661702d..65f6d44b 100644 --- a/new-log-viewer/src/utils/config.ts +++ b/new-log-viewer/src/utils/config.ts @@ -10,6 +10,7 @@ import {DecoderOptionsType} from "../typings/decoders"; const MAX_PAGE_SIZE = 1_000_000; +const EXPORT_LOGS_CHUNK_SIZE = 10_000; /** * The default configuration values. @@ -148,6 +149,7 @@ const getConfig = (key: T): ConfigMap[T] => { export { CONFIG_DEFAULT, + EXPORT_LOGS_CHUNK_SIZE, getConfig, setConfig, testConfig,