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 ( +
+
+ + +
+
+ {children} +
+
+ ); +}; + +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,