Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

new-log-viewer: Add support for exporting decoded logs as a file. #72

Merged
merged 26 commits into from
Sep 23, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
b127bbf
Export Logs draft
Henry8192 Sep 15, 2024
b9ab9dd
Apply suggestions from code review
Henry8192 Sep 16, 2024
da3f0be
Move blob & numChunks to LogExportManager; Move downloadDecompressedL…
Henry8192 Sep 16, 2024
5abb640
Download logs can only be downloaded once; Need to check corner case …
Henry8192 Sep 17, 2024
4b5c276
logs can be downloaded multiple times
Henry8192 Sep 17, 2024
20d98a6
plausible fix for empty logs
Henry8192 Sep 17, 2024
f6023cf
address comments in code review
Henry8192 Sep 17, 2024
7bf9432
Apply suggestions from code review
Henry8192 Sep 17, 2024
73bc71f
address changes from code review
Henry8192 Sep 17, 2024
406b76c
Introduce 8px padding to .menu-bar for better spacing. Add .menu-bar-…
Henry8192 Sep 18, 2024
27659ff
change exportedFileName format; address two exports in ExportLogsButt…
Henry8192 Sep 18, 2024
a69e3d0
Apply suggestions from code review
Henry8192 Sep 18, 2024
8bcf115
fix issues in review
Henry8192 Sep 18, 2024
e2cc493
fix lint warnings; address the remaining issues in code review
Henry8192 Sep 18, 2024
c105eaf
Merge branch 'main' into export-logs
junhaoliao Sep 18, 2024
69ca0b7
Minor comment fixes.
kirkrodrigues Sep 19, 2024
0744f99
loadChunk: Refactor docstring and error.
kirkrodrigues Sep 19, 2024
bc4d463
StateContextProvider: Group states and refs; Reorder exportProgress s…
kirkrodrigues Sep 19, 2024
6b51ef9
Clear export progress when a new file is loaded.
kirkrodrigues Sep 19, 2024
5e5d24d
loadChunk: Apply renaming suggestions from review.
kirkrodrigues Sep 19, 2024
73bd8fa
Address suggestions from code review
Henry8192 Sep 19, 2024
c34ed9b
suppress todo warnings
Henry8192 Sep 19, 2024
1e99e07
Use EXPORT_LOGS_PROGRESS_VALUE_MIN instead of STATE_DEFAULT.exportPro…
kirkrodrigues Sep 19, 2024
623b9ed
Edit TODO about empty fileName check.
kirkrodrigues Sep 19, 2024
7d2051d
Revert "Use EXPORT_LOGS_PROGRESS_VALUE_MIN instead of STATE_DEFAULT.e…
kirkrodrigues Sep 20, 2024
f1699d6
Reset export progress before loading file.
kirkrodrigues Sep 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 13 additions & 5 deletions new-log-viewer/src/components/MenuBar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ import {
Typography,
} from "@mui/joy";

import Description from "@mui/icons-material/Description";
import DescriptionIcon from "@mui/icons-material/Description";
import DownloadIcon from "@mui/icons-material/Download";
import FileOpenIcon from "@mui/icons-material/FileOpen";
import Settings from "@mui/icons-material/Settings";
import SettingsIcon from "@mui/icons-material/Settings";

import {StateContext} from "../../contexts/StateContextProvider";
import {CURSOR_CODE} from "../../typings/worker";
Expand All @@ -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<boolean>(false);

Expand All @@ -48,6 +49,10 @@ const MenuBar = () => {
setIsSettingsModalOpen(true);
};

const handleExportLogsButtonClick = () => {
exportLogs();
};

Henry8192 marked this conversation as resolved.
Show resolved Hide resolved
return (
<>
<Sheet className={"menu-bar"}>
Expand All @@ -58,7 +63,7 @@ const MenuBar = () => {
flexGrow={1}
gap={0.5}
>
<Description/>
<DescriptionIcon/>
<Typography level={"body-md"}>
{fileName}
</Typography>
Expand All @@ -74,7 +79,10 @@ const MenuBar = () => {
<SmallIconButton
onClick={handleSettingsModalOpen}
>
<Settings/>
<SettingsIcon/>
</SmallIconButton>
<SmallIconButton onClick={handleExportLogsButtonClick}>
<DownloadIcon/>
</SmallIconButton>
</Sheet>
<SettingsModal
Expand Down
69 changes: 55 additions & 14 deletions new-log-viewer/src/contexts/StateContextProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint max-lines: ["error", 400] */
import React, {
createContext,
useCallback,
Expand All @@ -7,6 +8,7 @@ import React, {
useState,
} from "react";

import LogExportManager from "../services/LogExportManager";
import {Nullable} from "../typings/common";
import {CONFIG_KEY} from "../typings/config";
import {SEARCH_PARAM_NAMES} from "../typings/url";
Expand All @@ -20,7 +22,10 @@ import {
WORKER_RESP_CODE,
WorkerReq,
} from "../typings/worker";
import {getConfig} from "../utils/config";
import {
EXPORT_LOGS_CHUNK_SIZE,
getConfig,
} from "../utils/config";
import {
clamp,
getChunkNum,
Expand All @@ -37,12 +42,14 @@ import {
interface StateContextType {
beginLineNumToLogEventNum: BeginLineNumToLogEventNumMap,
fileName: string,
loadFile: (fileSrc: FileSrcType, cursor: CursorType) => void,
loadPage: (newPageNum: number) => void,
logData: string,
numEvents: number,
numPages: number,
pageNum: Nullable<number>
pageNum: Nullable<number>,

exportLogs: () => void,
loadFile: (fileSrc: FileSrcType, cursor: CursorType) => void,
loadPage: (newPageNum: number) => void,
}
const StateContext = createContext<StateContextType>({} as StateContextType);

Expand All @@ -52,12 +59,14 @@ const StateContext = createContext<StateContextType>({} as StateContextType);
const STATE_DEFAULT: Readonly<StateContextType> = Object.freeze({
beginLineNumToLogEventNum: new Map<number, number>(),
fileName: "",
loadFile: () => null,
loadPage: () => null,
logData: "Loading...",
numEvents: 0,
numPages: 0,
pageNum: 0,

exportLogs: () => null,
loadFile: () => null,
loadPage: () => null,
});

interface StateContextProviderProps {
Expand Down Expand Up @@ -138,36 +147,66 @@ const StateContextProvider = ({children}: StateContextProviderProps) => {
const logEventNumRef = useRef(logEventNum);
const numPagesRef = useRef<number>(STATE_DEFAULT.numPages);
const pageNumRef = useRef<Nullable<number>>(STATE_DEFAULT.pageNum);
const logExportManagerRef = useRef<null|LogExportManager>(null);

const mainWorkerRef = useRef<null|Worker>(null);

const handleMainWorkerResp = useCallback((ev: MessageEvent<MainWorkerRespMessage>) => {
const {code, args} = ev.data;

Henry8192 marked this conversation as resolved.
Show resolved Hide resolved
console.log(`[MainWorker -> Renderer] code=${code}`);
switch (code) {
case WORKER_RESP_CODE.CHUNK_DATA:
if (null !== logExportManagerRef.current) {
logExportManagerRef.current.appendChunkData(args.logs);
}
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;
const lastLogEventNum = getLastLogEventNum(args.beginLineNumToLogEventNum);
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;
}
if (STATE_DEFAULT.numEvents === numEvents && STATE_DEFAULT.fileName === fileName) {
console.error("numEvents and fileName not initialized yet");

return;
}
logExportManagerRef.current = new LogExportManager(
Henry8192 marked this conversation as resolved.
Show resolved Hide resolved
Henry8192 marked this conversation as resolved.
Show resolved Hide resolved
Math.ceil(numEvents / EXPORT_LOGS_CHUNK_SIZE),
fileName
);
workerPostReq(
mainWorkerRef.current,
WORKER_REQ_CODE.EXPORT_LOG,
{decoderOptions: getConfig(CONFIG_KEY.DECODER_OPTIONS)}
);
}, [numEvents,
fileName]);
Henry8192 marked this conversation as resolved.
Show resolved Hide resolved

const loadFile = useCallback((fileSrc: FileSrcType, cursor: CursorType) => {
if ("string" !== typeof fileSrc) {
updateWindowUrlSearchParams({[SEARCH_PARAM_NAMES.FILE_PATH]: null});
Expand Down Expand Up @@ -275,12 +314,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}
Expand Down
73 changes: 73 additions & 0 deletions new-log-viewer/src/services/LogExportManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import {downloadBlob} from "../utils/file";


/**
* Manager for exporting logs to a file.
Henry8192 marked this conversation as resolved.
Show resolved Hide resolved
*/
class LogExportManager {
Henry8192 marked this conversation as resolved.
Show resolved Hide resolved
/**
* Array to store chunks of log data.
Henry8192 marked this conversation as resolved.
Show resolved Hide resolved
*
* @type {string[]}
* @private
Henry8192 marked this conversation as resolved.
Show resolved Hide resolved
*/
readonly #chunks: string[] = [];

/**
* Total number of chunks to export.
*
* @type {number}
* @private
Henry8192 marked this conversation as resolved.
Show resolved Hide resolved
*/
readonly #numChunks: number;

/**
* Name of the file to export to.
Henry8192 marked this conversation as resolved.
Show resolved Hide resolved
*
* @type {string}
* @private
Henry8192 marked this conversation as resolved.
Show resolved Hide resolved
*/
readonly #fileName: string;

constructor (numChunks: number, fileName: string) {
this.#numChunks = numChunks;
this.#fileName = fileName;
}

/**
* Append a chunk of log string.
Henry8192 marked this conversation as resolved.
Show resolved Hide resolved
* If the number of chunks reaches the specified limit, trigger a download.
*
* @param chunkData The chunk of log string to append.
Henry8192 marked this conversation as resolved.
Show resolved Hide resolved
* @return The current download progress.
Henry8192 marked this conversation as resolved.
Show resolved Hide resolved
*/
appendChunkData (chunkData: string) {
jackluo923 marked this conversation as resolved.
Show resolved Hide resolved
Henry8192 marked this conversation as resolved.
Show resolved Hide resolved
if (0 === this.#numChunks) {
this.#download();

return 1;
}
this.#chunks.push(chunkData);
if (this.#chunks.length === this.#numChunks) {
this.#download();
this.#chunks.length = 0;
Henry8192 marked this conversation as resolved.
Show resolved Hide resolved
}

return this.#chunks.length / this.#numChunks;
}

/**
* Trigger a download of the accumulated log data chunks.
Henry8192 marked this conversation as resolved.
Show resolved Hide resolved
*
* @private
Henry8192 marked this conversation as resolved.
Show resolved Hide resolved
*/
#download () {
const blob = new Blob(this.#chunks, {type: "text/plain"});
const fileNameTimeStamped = `${this.#fileName}-exported-${new Date().toISOString()
.replace(/[:.]/g, "-")}.log`;

downloadBlob(blob, fileNameTimeStamped);
}
}

export default LogExportManager;
21 changes: 21 additions & 0 deletions new-log-viewer/src/services/LogFileManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -152,6 +153,26 @@ class LogFileManager {
this.#decoder.setDecoderOptions(options);
}

loadChunk (eventIdx: number): {
Henry8192 marked this conversation as resolved.
Show resolved Hide resolved
logs: string,
} {
Henry8192 marked this conversation as resolved.
Show resolved Hide resolved
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});`);
Henry8192 marked this conversation as resolved.
Show resolved Hide resolved
}

const messages = results.map(([msg]) => msg);

return {
logs: messages.join(""),
};
}

/**
* Loads a page of log events based on the provided cursor.
*
Expand Down
20 changes: 20 additions & 0 deletions new-log-viewer/src/services/MainWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
WORKER_RESP_CODE,
WorkerResp,
} from "../typings/worker";
import {EXPORT_LOGS_CHUNK_SIZE} from "../utils/config";
import LogFileManager from "./LogFileManager";


Expand All @@ -35,12 +36,31 @@ const postResp = <T extends WORKER_RESP_CODE>(
postMessage({code, args});
};

// eslint-disable-next-line max-lines-per-function,max-statements
Henry8192 marked this conversation as resolved.
Show resolved Hide resolved
onmessage = async (ev: MessageEvent<MainWorkerReqMessage>) => {
const {code, args} = ev.data;
console.log(`[Renderer -> MainWorker] code=${code}: args=${JSON.stringify(args)}`);

try {
switch (code) {
case WORKER_REQ_CODE.EXPORT_LOG: {
Henry8192 marked this conversation as resolved.
Show resolved Hide resolved
if (null === LOG_FILE_MANAGER) {
throw new Error("Log file manager hasn't been initialized");
}
if ("undefined" !== typeof args.decoderOptions) {
LOG_FILE_MANAGER.setDecoderOptions(args.decoderOptions);
}

let decodedEventIdx = 0;
while (decodedEventIdx < LOG_FILE_MANAGER.numEvents) {
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,
Expand Down
8 changes: 8 additions & 0 deletions new-log-viewer/src/typings/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,22 @@ type BeginLineNumToLogEventNumMap = Map<number, number>;
* 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,
Expand All @@ -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,
Expand Down
Loading