diff --git a/new-log-viewer/src/components/CustomListItem.tsx b/new-log-viewer/src/components/CustomListItem.tsx
new file mode 100644
index 00000000..5772b392
--- /dev/null
+++ b/new-log-viewer/src/components/CustomListItem.tsx
@@ -0,0 +1,44 @@
+import React from "react";
+
+import {
+ ListItem,
+ ListItemContent,
+ ListItemDecorator,
+ Typography,
+} from "@mui/joy";
+
+
+interface CustomListItemProps {
+ content: string,
+ icon: React.ReactNode,
+ title: string
+}
+
+/**
+ * Renders a custom list item with an icon, a title and a context text.
+ *
+ * @param props
+ * @param props.content
+ * @param props.icon
+ * @param props.title
+ * @return
+ */
+const CustomListItem = ({content, icon, title}: CustomListItemProps) => (
+
+
+ {icon}
+
+
+
+ {title}
+
+
+ {content}
+
+
+
+);
+
+export default CustomListItem;
diff --git a/new-log-viewer/src/components/DropFileContainer/index.css b/new-log-viewer/src/components/DropFileContainer/index.css
index 6c6fc50b..7935d373 100644
--- a/new-log-viewer/src/components/DropFileContainer/index.css
+++ b/new-log-viewer/src/components/DropFileContainer/index.css
@@ -2,11 +2,6 @@
position: relative;
}
-.drop-file-children {
- width: 100%;
- height: 100%;
-}
-
.hover-mask {
position: absolute;
top: 0;
diff --git a/new-log-viewer/src/components/DropFileContainer/index.tsx b/new-log-viewer/src/components/DropFileContainer/index.tsx
index a355c9e8..f37fa6fa 100644
--- a/new-log-viewer/src/components/DropFileContainer/index.tsx
+++ b/new-log-viewer/src/components/DropFileContainer/index.tsx
@@ -70,10 +70,7 @@ const DropFileContainer = ({children}: DropFileContextProviderProps) => {
onDragOver={handleDrag}
onDrop={handleDrop}
>
-
+
{children}
{isFileHovering && (
{
theme={APP_THEME}
>
-
-
-
+
+
+
+
+
);
diff --git a/new-log-viewer/src/components/MenuBar/index.tsx b/new-log-viewer/src/components/MenuBar/index.tsx
index e9955286..5d702830 100644
--- a/new-log-viewer/src/components/MenuBar/index.tsx
+++ b/new-log-viewer/src/components/MenuBar/index.tsx
@@ -1,7 +1,4 @@
-import {
- useContext,
- useState,
-} from "react";
+import {useContext} from "react";
import {
Divider,
@@ -11,15 +8,9 @@ import {
} from "@mui/joy";
import Description from "@mui/icons-material/Description";
-import FileOpenIcon from "@mui/icons-material/FileOpen";
-import Settings from "@mui/icons-material/Settings";
import {StateContext} from "../../contexts/StateContextProvider";
-import {CURSOR_CODE} from "../../typings/worker";
-import {openFile} from "../../utils/file";
-import SettingsModal from "../modals/SettingsModal";
import NavigationBar from "./NavigationBar";
-import SmallIconButton from "./SmallIconButton";
import "./index.css";
@@ -30,23 +21,7 @@ import "./index.css";
* @return
*/
const MenuBar = () => {
- const {fileName, loadFile} = useContext(StateContext);
-
- const [isSettingsModalOpen, setIsSettingsModalOpen] = useState
(false);
-
- const handleOpenFileButtonClick = () => {
- openFile((file) => {
- loadFile(file, {code: CURSOR_CODE.LAST_EVENT, args: null});
- });
- };
-
- const handleSettingsModalClose = () => {
- setIsSettingsModalOpen(false);
- };
-
- const handleSettingsModalOpen = () => {
- setIsSettingsModalOpen(true);
- };
+ const {fileName} = useContext(StateContext);
return (
<>
@@ -66,20 +41,7 @@ const MenuBar = () => {
-
-
-
-
-
-
-
-
-
>
);
};
diff --git a/new-log-viewer/src/components/SidebarContainer/PanelTabs/FileInfoTab.tsx b/new-log-viewer/src/components/SidebarContainer/PanelTabs/FileInfoTab.tsx
new file mode 100644
index 00000000..13ed6ebc
--- /dev/null
+++ b/new-log-viewer/src/components/SidebarContainer/PanelTabs/FileInfoTab.tsx
@@ -0,0 +1,51 @@
+import {useContext} from "react";
+
+import {
+ DialogContent,
+ DialogTitle,
+ Divider,
+ List,
+ TabPanel,
+} from "@mui/joy";
+
+import AbcIcon from "@mui/icons-material/Abc";
+import StorageIcon from "@mui/icons-material/Storage";
+
+import {StateContext} from "../../../contexts/StateContextProvider";
+import {formatSizeInBytes} from "../../../utils/units";
+import CustomListItem from "../../CustomListItem";
+import {TAB_NAME} from "./index";
+
+import "./index.css";
+
+
+/**
+ * Display the file name and original size of the selected file.
+ *
+ * @return
+ */
+const FileInfoTab = () => {
+ const {fileName, originalFileSizeInBytes} = useContext(StateContext);
+
+ return (
+
+ File Info
+
+
+ }
+ title={"File Info"}/>
+
+ }
+ title={"Original Size"}/>
+
+
+
+ );
+};
+
+
+export default FileInfoTab;
diff --git a/new-log-viewer/src/components/SidebarContainer/PanelTabs/index.css b/new-log-viewer/src/components/SidebarContainer/PanelTabs/index.css
new file mode 100644
index 00000000..46c985c5
--- /dev/null
+++ b/new-log-viewer/src/components/SidebarContainer/PanelTabs/index.css
@@ -0,0 +1,9 @@
+.sidebar-tabs {
+ height: 100%;
+ flex-grow: 1;
+ width: calc(100% - var(--ylv-panel-resize-handle-width));
+}
+
+.sidebar-tab-list-spacing {
+ flex-grow: 1;
+}
\ No newline at end of file
diff --git a/new-log-viewer/src/components/SidebarContainer/PanelTabs/index.tsx b/new-log-viewer/src/components/SidebarContainer/PanelTabs/index.tsx
new file mode 100644
index 00000000..a6ea7dd6
--- /dev/null
+++ b/new-log-viewer/src/components/SidebarContainer/PanelTabs/index.tsx
@@ -0,0 +1,109 @@
+import React, {
+ forwardRef,
+ useContext,
+ useState,
+} from "react";
+
+import {
+ Tab,
+ TabList,
+ Tabs,
+} from "@mui/joy";
+
+import FileOpenIcon from "@mui/icons-material/FileOpen";
+import InfoIcon from "@mui/icons-material/Info";
+import SettingsIcon from "@mui/icons-material/Settings";
+
+import {StateContext} from "../../../contexts/StateContextProvider";
+import {CURSOR_CODE} from "../../../typings/worker";
+import {openFile} from "../../../utils/file";
+import SettingsModal from "../../modals/SettingsModal";
+import FileInfoTab from "./FileInfoTab";
+
+
+enum TAB_NAME {
+ OPEN_FILE = "openFile",
+ FILE_INFO = "fileInfo",
+ SETTINGS = "settings",
+}
+
+/**
+ * Displays a set of tabs in a vertical orientation.
+ *
+ * @param tabListRef Reference object used to access the TabList DOM element.
+ * @return
+ */
+const PanelTabs = forwardRef((_, tabListRef) => {
+ const {loadFile} = useContext(StateContext);
+
+ const [activeTabName, setActiveTabName] = useState(TAB_NAME.FILE_INFO);
+ const [isSettingsModalOpen, setIsSettingsModalOpen] = useState(false);
+
+ const handleOpenFile = () => {
+ openFile((file) => {
+ loadFile(file, {code: CURSOR_CODE.LAST_EVENT, args: null});
+ });
+ };
+
+ const handleSettingsModalClose = () => {
+ setIsSettingsModalOpen(false);
+ };
+
+ const handleTabChange = (__: React.SyntheticEvent | null, value: number | string | null) => {
+ switch (value) {
+ case TAB_NAME.OPEN_FILE:
+ handleOpenFile();
+ break;
+ case TAB_NAME.SETTINGS:
+ setIsSettingsModalOpen(true);
+ break;
+ default:
+ setActiveTabName(value as TAB_NAME);
+ }
+ };
+
+ return (
+ <>
+
+
+ {[
+ {tabName: TAB_NAME.OPEN_FILE, icon: },
+ {tabName: TAB_NAME.FILE_INFO, icon: },
+ ].map(({tabName, icon}) => (
+
+ {icon}
+
+ ))}
+
+
+
+
+
+
+
+
+ >
+ );
+});
+
+PanelTabs.displayName = "PanelTabs";
+export default PanelTabs;
+export {TAB_NAME};
diff --git a/new-log-viewer/src/components/SidebarContainer/ResizeHandle.css b/new-log-viewer/src/components/SidebarContainer/ResizeHandle.css
new file mode 100644
index 00000000..637f2dfc
--- /dev/null
+++ b/new-log-viewer/src/components/SidebarContainer/ResizeHandle.css
@@ -0,0 +1,11 @@
+.resize-handle {
+ cursor: ew-resize;
+ width: 3px;
+ height: 100%;
+ background-color: var(--joy-palette-neutral-outlinedBorder, #cdd7e1);
+ z-index: 1;
+}
+
+.resize-handle:hover {
+ background-color: var(--joy-palette-primary-solidHoverBg, #0258a8);
+}
diff --git a/new-log-viewer/src/components/SidebarContainer/ResizeHandle.tsx b/new-log-viewer/src/components/SidebarContainer/ResizeHandle.tsx
new file mode 100644
index 00000000..30ea68d9
--- /dev/null
+++ b/new-log-viewer/src/components/SidebarContainer/ResizeHandle.tsx
@@ -0,0 +1,66 @@
+import React, {
+ useEffect,
+ useState,
+} from "react";
+
+import "./ResizeHandle.css";
+
+
+interface ResizeHandleProps {
+ onResize: (offset: number) => void,
+}
+
+/**
+ * A vertical handle for resizing an object.
+ *
+ * @param props
+ * @param props.onResize The method to call when a resize occurs.
+ * @return
+ */
+const ResizeHandle = ({onResize}: ResizeHandleProps) => {
+ const [isMouseDown, setIsMouseDown] = useState(false);
+
+ const handleMouseDown = (ev: React.MouseEvent) => {
+ ev.preventDefault();
+ setIsMouseDown(true);
+ };
+
+ const handleMouseUp = (ev: MouseEvent) => {
+ ev.preventDefault();
+ setIsMouseDown(false);
+ };
+
+ useEffect(() => {
+ if (false === isMouseDown) {
+ return () => null;
+ }
+
+ window.addEventListener("mouseup", handleMouseUp);
+
+ const handleMouseMove = (ev: MouseEvent) => {
+ ev.preventDefault();
+ onResize(ev.clientX);
+ };
+
+ window.addEventListener("mousemove", handleMouseMove);
+
+ return () => {
+ window.removeEventListener("mousemove", handleMouseMove);
+ window.removeEventListener("mouseup", handleMouseUp);
+ };
+ }, [
+ isMouseDown,
+ onResize,
+ ]);
+
+ return (
+ <>
+
+ >
+ );
+};
+
+
+export default ResizeHandle;
diff --git a/new-log-viewer/src/components/SidebarContainer/index.css b/new-log-viewer/src/components/SidebarContainer/index.css
new file mode 100644
index 00000000..32047cfa
--- /dev/null
+++ b/new-log-viewer/src/components/SidebarContainer/index.css
@@ -0,0 +1,18 @@
+:root {
+ --ylv-panel-width: 300px;
+ --ylv-panel-resize-handle-width: 4px
+}
+
+.sidebar-container {
+ display: grid;
+ grid-template-columns: var(--ylv-panel-width) 1fr;
+ width: 100vw;
+}
+
+.sidebar-tabs-container {
+ display: flex;
+}
+
+.sidebar-children-container {
+ width: calc(100vw - var(--ylv-panel-width));
+}
\ No newline at end of file
diff --git a/new-log-viewer/src/components/SidebarContainer/index.tsx b/new-log-viewer/src/components/SidebarContainer/index.tsx
new file mode 100644
index 00000000..c59e0e1e
--- /dev/null
+++ b/new-log-viewer/src/components/SidebarContainer/index.tsx
@@ -0,0 +1,67 @@
+import React, {
+ useCallback,
+ useEffect,
+ useRef,
+ useState,
+} from "react";
+
+import PanelTabs from "./PanelTabs";
+import ResizeHandle from "./ResizeHandle";
+
+import "./index.css";
+
+
+interface SidebarContainerProps {
+ children: React.ReactNode,
+}
+
+const RESIZE_HANDLE_WIDTH_IN_PIXEL = 3;
+const PANEL_DEFAULT_WIDTH_IN_PIXEL = 300;
+const PANEL_CLIP_THRESHOLD_IN_PIXEL = 80;
+const PANEL_MAX_WIDTH_TO_WINDOW_WIDTH_RATIO = 0.8;
+
+
+/**
+ * Wraps a children with a sidebar component on the left.
+ *
+ * @param props
+ * @param props.children
+ * @return
+ */
+const SidebarContainer = ({children}: SidebarContainerProps) => {
+ const [panelWidth, setPanelWidth] = useState
(PANEL_DEFAULT_WIDTH_IN_PIXEL);
+
+ const tabListRef = useRef(null);
+
+ const handleResize = useCallback((offset: number) => {
+ if (null === tabListRef.current) {
+ console.error("Unexpected null tabListRef.current");
+
+ return;
+ }
+ if (tabListRef.current.clientWidth + PANEL_CLIP_THRESHOLD_IN_PIXEL > offset) {
+ setPanelWidth(tabListRef.current.clientWidth);
+ } else if (offset < window.innerWidth * PANEL_MAX_WIDTH_TO_WINDOW_WIDTH_RATIO) {
+ setPanelWidth(offset + RESIZE_HANDLE_WIDTH_IN_PIXEL);
+ }
+ }, []);
+
+ // On `panelWidth` change, update CSS variable `--ylv-panel-width`.
+ useEffect(() => {
+ document.body.style.setProperty("--ylv-panel-width", `${panelWidth}px`);
+ }, [panelWidth]);
+
+ return (
+
+ );
+};
+
+export default SidebarContainer;
diff --git a/new-log-viewer/src/contexts/StateContextProvider.tsx b/new-log-viewer/src/contexts/StateContextProvider.tsx
index 5d506d2c..4ad1e609 100644
--- a/new-log-viewer/src/contexts/StateContextProvider.tsx
+++ b/new-log-viewer/src/contexts/StateContextProvider.tsx
@@ -42,6 +42,7 @@ interface StateContextType {
logData: string,
numEvents: number,
numPages: number,
+ originalFileSizeInBytes: number,
pageNum: Nullable
}
const StateContext = createContext({} as StateContextType);
@@ -57,6 +58,7 @@ const STATE_DEFAULT: Readonly = Object.freeze({
logData: "Loading...",
numEvents: 0,
numPages: 0,
+ originalFileSizeInBytes: 0,
pageNum: 0,
});
@@ -133,6 +135,8 @@ const StateContextProvider = ({children}: StateContextProviderProps) => {
const [fileName, setFileName] = useState(STATE_DEFAULT.fileName);
const [logData, setLogData] = useState(STATE_DEFAULT.logData);
const [numEvents, setNumEvents] = useState(STATE_DEFAULT.numEvents);
+ const [originalFileSizeInBytes, setOriginalFileSizeInBytes] =
+ useState(STATE_DEFAULT.originalFileSizeInBytes);
const beginLineNumToLogEventNumRef =
useRef(STATE_DEFAULT.beginLineNumToLogEventNum);
const logEventNumRef = useRef(logEventNum);
@@ -148,6 +152,7 @@ const StateContextProvider = ({children}: StateContextProviderProps) => {
case WORKER_RESP_CODE.LOG_FILE_INFO:
setFileName(args.fileName);
setNumEvents(args.numEvents);
+ setOriginalFileSizeInBytes(args.originalFileSizeInBytes);
break;
case WORKER_RESP_CODE.PAGE_DATA: {
setLogData(args.logs);
@@ -280,6 +285,7 @@ const StateContextProvider = ({children}: StateContextProviderProps) => {
logData: logData,
numEvents: numEvents,
numPages: numPagesRef.current,
+ originalFileSizeInBytes: originalFileSizeInBytes,
pageNum: pageNumRef.current,
}}
>
diff --git a/new-log-viewer/src/services/LogFileManager.ts b/new-log-viewer/src/services/LogFileManager.ts
index ea73d7fa..ab567e1e 100644
--- a/new-log-viewer/src/services/LogFileManager.ts
+++ b/new-log-viewer/src/services/LogFileManager.ts
@@ -52,6 +52,8 @@ class LogFileManager {
readonly #fileName: string;
+ readonly #originalFileSizeInBytes: number;
+
#decoder: Decoder;
#numEvents: number = 0;
@@ -62,14 +64,17 @@ class LogFileManager {
*
* @param decoder
* @param fileName
+ * @param originalFileSizeInBytes
* @param pageSize Page size for setting up pagination.
*/
constructor (
decoder: Decoder,
fileName: string,
+ originalFileSizeInBytes: number,
pageSize: number,
) {
this.#fileName = fileName;
+ this.#originalFileSizeInBytes = originalFileSizeInBytes;
this.#pageSize = pageSize;
this.#decoder = decoder;
@@ -91,6 +96,10 @@ class LogFileManager {
return this.#numEvents;
}
+ get originalFileSizeInBytes () {
+ return this.#originalFileSizeInBytes;
+ }
+
/**
* Creates a new LogFileManager.
*
@@ -108,7 +117,7 @@ class LogFileManager {
const {fileName, fileData} = await loadFile(fileSrc);
const decoder = await LogFileManager.#initDecoder(fileName, fileData, decoderOptions);
- return new LogFileManager(decoder, fileName, pageSize);
+ return new LogFileManager(decoder, fileName, fileData.length, pageSize);
}
/**
diff --git a/new-log-viewer/src/services/MainWorker.ts b/new-log-viewer/src/services/MainWorker.ts
index 87a1bd89..d3091d83 100644
--- a/new-log-viewer/src/services/MainWorker.ts
+++ b/new-log-viewer/src/services/MainWorker.ts
@@ -51,6 +51,7 @@ onmessage = async (ev: MessageEvent) => {
postResp(WORKER_RESP_CODE.LOG_FILE_INFO, {
fileName: LOG_FILE_MANAGER.fileName,
numEvents: LOG_FILE_MANAGER.numEvents,
+ originalFileSizeInBytes: LOG_FILE_MANAGER.originalFileSizeInBytes,
});
postResp(
WORKER_RESP_CODE.PAGE_DATA,
diff --git a/new-log-viewer/src/typings/worker.ts b/new-log-viewer/src/typings/worker.ts
index 0c88beb6..72987d07 100644
--- a/new-log-viewer/src/typings/worker.ts
+++ b/new-log-viewer/src/typings/worker.ts
@@ -65,6 +65,7 @@ type WorkerRespMap = {
[WORKER_RESP_CODE.LOG_FILE_INFO]: {
fileName: string,
numEvents: number,
+ originalFileSizeInBytes: number,
},
[WORKER_RESP_CODE.PAGE_DATA]: {
logs: string,