+ );
+};
+
+
+export default ResizeHandle;
diff --git a/new-log-viewer/src/components/CentralContainer/Sidebar/SidebarTabs/CustomListItem.tsx b/new-log-viewer/src/components/CentralContainer/Sidebar/SidebarTabs/CustomListItem.tsx
new file mode 100644
index 00000000..85b03a07
--- /dev/null
+++ b/new-log-viewer/src/components/CentralContainer/Sidebar/SidebarTabs/CustomListItem.tsx
@@ -0,0 +1,50 @@
+import React from "react";
+
+import {
+ ListItem,
+ ListItemContent,
+ ListItemDecorator,
+ Typography,
+ TypographyProps,
+} from "@mui/joy";
+
+
+interface CustomListItemProps {
+ content: string,
+ icon: React.ReactNode,
+ slotProps?: {
+ content?: TypographyProps
+ },
+ 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
+ * @param props.slotProps
+ * @return
+ */
+const CustomListItem = ({content, icon, slotProps, title}: CustomListItemProps) => (
+
+
+ {icon}
+
+
+
+ {title}
+
+
+ {content}
+
+
+
+);
+
+export default CustomListItem;
diff --git a/new-log-viewer/src/components/CentralContainer/Sidebar/SidebarTabs/CustomTabPanel.css b/new-log-viewer/src/components/CentralContainer/Sidebar/SidebarTabs/CustomTabPanel.css
new file mode 100644
index 00000000..b40021f3
--- /dev/null
+++ b/new-log-viewer/src/components/CentralContainer/Sidebar/SidebarTabs/CustomTabPanel.css
@@ -0,0 +1,16 @@
+.sidebar-tab-panel {
+ padding: 0.75rem;
+}
+
+.sidebar-tab-panel-title-container {
+ user-select: none;
+ align-items: center;
+ margin-bottom: 0.5rem !important;
+}
+
+.sidebar-tab-panel-title {
+ flex-grow: 1;
+ font-size: 0.875rem !important;
+ font-weight: 400 !important;
+ text-transform: uppercase;
+}
diff --git a/new-log-viewer/src/components/CentralContainer/Sidebar/SidebarTabs/CustomTabPanel.tsx b/new-log-viewer/src/components/CentralContainer/Sidebar/SidebarTabs/CustomTabPanel.tsx
new file mode 100644
index 00000000..eaa9a7b0
--- /dev/null
+++ b/new-log-viewer/src/components/CentralContainer/Sidebar/SidebarTabs/CustomTabPanel.tsx
@@ -0,0 +1,66 @@
+import React from "react";
+
+import {
+ ButtonGroup,
+ DialogContent,
+ DialogTitle,
+ TabPanel,
+ Typography,
+} from "@mui/joy";
+
+import "./CustomTabPanel.css";
+
+
+interface CustomTabPanelProps {
+ children: React.ReactNode,
+ tabName: string,
+ title: string,
+ titleButtons?: React.ReactNode,
+}
+
+/**
+ * Renders a customized tab panel to be extended for displaying extra information in the sidebar.
+ *
+ * @param props
+ * @param props.children
+ * @param props.tabName
+ * @param props.title
+ * @param props.titleButtons a React fragment containing `
`s, to be displayed next to
+ * the title
+ * @return
+ */
+const CustomTabPanel = ({
+ children,
+ tabName,
+ title,
+ titleButtons,
+}: CustomTabPanelProps) => {
+ return (
+
+
+
+ {title}
+
+
+ {titleButtons}
+
+
+
+ {children}
+
+
+ );
+};
+
+
+export default CustomTabPanel;
diff --git a/new-log-viewer/src/components/CentralContainer/Sidebar/SidebarTabs/FileInfoTabPanel.tsx b/new-log-viewer/src/components/CentralContainer/Sidebar/SidebarTabs/FileInfoTabPanel.tsx
new file mode 100644
index 00000000..67554a06
--- /dev/null
+++ b/new-log-viewer/src/components/CentralContainer/Sidebar/SidebarTabs/FileInfoTabPanel.tsx
@@ -0,0 +1,52 @@
+import {useContext} from "react";
+
+import {
+ Divider,
+ List,
+} from "@mui/joy";
+
+import AbcIcon from "@mui/icons-material/Abc";
+import StorageIcon from "@mui/icons-material/Storage";
+
+import {StateContext} from "../../../../contexts/StateContextProvider";
+import {
+ TAB_DISPLAY_NAMES,
+ TAB_NAME,
+} from "../../../../typings/tab";
+import {formatSizeInBytes} from "../../../../utils/units";
+import CustomListItem from "./CustomListItem";
+import CustomTabPanel from "./CustomTabPanel";
+
+
+/**
+ * Displays a panel containing the file name and original size of the selected file.
+ *
+ * @return
+ */
+const FileInfoTabPanel = () => {
+ const {fileName, originalFileSizeInBytes} = useContext(StateContext);
+
+ return (
+
+ {0 === fileName.length ?
+ "No file is open." :
+
+ }
+ slotProps={{content: {sx: {wordBreak: "break-word"}}}}
+ title={"Name"}/>
+
+ }
+ title={"Original Size"}/>
+
}
+
+ );
+};
+
+export default FileInfoTabPanel;
diff --git a/new-log-viewer/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/Result.css b/new-log-viewer/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/Result.css
new file mode 100644
index 00000000..e68e46be
--- /dev/null
+++ b/new-log-viewer/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/Result.css
@@ -0,0 +1,15 @@
+.result-button {
+ user-select: none;
+
+ overflow: hidden;
+ display: block !important;
+
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.result-text {
+ display: inline !important;
+ font-size: 0.875rem !important;
+ font-weight: 400 !important;
+}
diff --git a/new-log-viewer/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/Result.tsx b/new-log-viewer/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/Result.tsx
new file mode 100644
index 00000000..591bd835
--- /dev/null
+++ b/new-log-viewer/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/Result.tsx
@@ -0,0 +1,61 @@
+import {
+ ListItemButton,
+ Typography,
+} from "@mui/joy";
+
+import "./Result.css";
+
+
+interface ResultProps {
+ message: string,
+ matchRange: [number, number]
+}
+
+/**
+ * Displays a button containing a message, which highlights a specific range of text.
+ *
+ * @param props
+ * @param props.message
+ * @param props.matchRange A two-element array indicating the start and end indices of the substring
+ * to be highlighted.
+ * @return
+ */
+const Result = ({message, matchRange}: ResultProps) => {
+ const [
+ beforeMatch,
+ match,
+ afterMatch,
+ ] = [
+ message.slice(0, matchRange[0]),
+ message.slice(...matchRange),
+ message.slice(matchRange[1]),
+ ];
+
+ return (
+
+
+ {beforeMatch}
+
+
+ {match}
+
+
+ {afterMatch}
+
+
+ );
+};
+
+export default Result;
diff --git a/new-log-viewer/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/ResultsGroup.css b/new-log-viewer/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/ResultsGroup.css
new file mode 100644
index 00000000..5244d6a1
--- /dev/null
+++ b/new-log-viewer/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/ResultsGroup.css
@@ -0,0 +1,21 @@
+.results-group-title-button {
+ flex-direction: row-reverse !important;
+ gap: 2px !important;
+ padding-inline-start: 0 !important;
+}
+
+.results-group-title-container {
+ display: flex;
+ flex-grow: 1;
+}
+
+.results-group-title-text-container {
+ flex-grow: 1;
+ gap: 0.1rem;
+ align-items: center;
+}
+
+.results-group-content {
+ margin-left: 1.5px !important;
+ border-left: 1px solid var(--joy-palette-neutral-outlinedBorder, #cdd7e1);
+}
diff --git a/new-log-viewer/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/ResultsGroup.tsx b/new-log-viewer/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/ResultsGroup.tsx
new file mode 100644
index 00000000..fe9e7b73
--- /dev/null
+++ b/new-log-viewer/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/ResultsGroup.tsx
@@ -0,0 +1,107 @@
+import {
+ useEffect,
+ useState,
+} from "react";
+import * as React from "react";
+
+import {
+ Accordion,
+ AccordionDetails,
+ AccordionSummary,
+ Box,
+ Chip,
+ List,
+ Stack,
+ Typography,
+} from "@mui/joy";
+
+import DescriptionOutlinedIcon from "@mui/icons-material/DescriptionOutlined";
+
+import Result from "./Result";
+
+import "./ResultsGroup.css";
+
+
+interface SearchResultOnPage {
+ logEventNum: number,
+ message: string,
+ matchRange: [number, number],
+}
+
+interface ResultsGroupProps {
+ isAllExpanded: boolean,
+ pageNum: number,
+ results: SearchResultOnPage[]
+}
+
+/**
+ *
+ * @param props
+ * @param props.isAllCollapsed
+ * @param props.pageNum
+ * @param props.results
+ * @param props.isAllExpanded
+ */
+const ResultsGroup = ({
+ isAllExpanded,
+ pageNum,
+ results,
+}: ResultsGroupProps) => {
+ const [isExpanded, setIsExpanded] = useState
(isAllExpanded);
+
+ const handleAccordionChange = (
+ _: React.SyntheticEvent,
+ newValue: boolean
+ ) => {
+ setIsExpanded(newValue);
+ };
+
+ // On `isAllExpanded` updates, sync current results group's expand status.
+ useEffect(() => {
+ setIsExpanded(isAllExpanded);
+ }, [isAllExpanded]);
+
+ return (
+
+
+
+
+
+
+ Page
+ {" "}
+ {pageNum}
+
+
+
+ {results.length}
+
+
+
+
+
+ {results.map((r, index) => (
+
+ ))}
+
+
+
+ );
+};
+
+export default ResultsGroup;
diff --git a/new-log-viewer/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/index.tsx b/new-log-viewer/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/index.tsx
new file mode 100644
index 00000000..933609b8
--- /dev/null
+++ b/new-log-viewer/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/index.tsx
@@ -0,0 +1,119 @@
+import {useState} from "react";
+
+import {
+ AccordionGroup,
+ IconButton,
+ Textarea,
+ ToggleButtonGroup,
+} from "@mui/joy";
+
+import UnfoldLessIcon from "@mui/icons-material/UnfoldLess";
+import UnfoldMoreIcon from "@mui/icons-material/UnfoldMore";
+
+import {
+ TAB_DISPLAY_NAMES,
+ TAB_NAME,
+} from "../../../../../typings/tab";
+import CustomTabPanel from "../CustomTabPanel";
+import TitleButton from "../TitleButton";
+import ResultsGroup from "./ResultsGroup";
+
+
+enum SEARCH_OPTION {
+ IS_CASE_SENSITIVE = "isCaseSensitive",
+ IS_REGEX = "isRegex"
+}
+
+/**
+ *
+ */
+const SAMPLE_RESULTS = Object.freeze({
+ 1: [{logEventNum: 1,
+ message: "hi how are you",
+ matchRange: [0,
+ 2]}],
+ 2: [{logEventNum: 202,
+ message: "i'm a super long message that supposedly overflows in the panel width.",
+ matchRange: [4,
+ 6]}],
+ 8: [{logEventNum: 808,
+ message: "hi how are you",
+ matchRange: [4,
+ 6]},
+ {logEventNum: 809,
+ message: "hi how are you",
+ matchRange: [4,
+ 6]}],
+});
+
+/**
+ * Displays a panel for submitting search queries and viewing query results.
+ *
+ * @return
+ */
+const SearchTabPanel = () => {
+ const [isAllExpanded, setIsAllExpanded] = useState(true);
+ const [searchOptions, setSearchOptions] = useState([]);
+
+ return (
+
+ { setIsAllExpanded((v) => !v); }}>
+ {isAllExpanded ?
+ :
+ }
+
+ >}
+ >
+
+ );
+};
+
+export default SearchTabPanel;
diff --git a/new-log-viewer/src/components/CentralContainer/Sidebar/SidebarTabs/TabButton.css b/new-log-viewer/src/components/CentralContainer/Sidebar/SidebarTabs/TabButton.css
new file mode 100644
index 00000000..11fe18bd
--- /dev/null
+++ b/new-log-viewer/src/components/CentralContainer/Sidebar/SidebarTabs/TabButton.css
@@ -0,0 +1,10 @@
+.sidebar-tab-button {
+ justify-content: center !important;
+ width: 48px;
+ height: 48px;
+ padding: 0 !important;
+}
+
+.sidebar-tab-button-icon {
+ font-size: 32px !important;
+}
diff --git a/new-log-viewer/src/components/CentralContainer/Sidebar/SidebarTabs/TabButton.tsx b/new-log-viewer/src/components/CentralContainer/Sidebar/SidebarTabs/TabButton.tsx
new file mode 100644
index 00000000..846860f2
--- /dev/null
+++ b/new-log-viewer/src/components/CentralContainer/Sidebar/SidebarTabs/TabButton.tsx
@@ -0,0 +1,57 @@
+import {
+ Tab,
+ Tooltip,
+} from "@mui/joy";
+
+import {SvgIconComponent} from "@mui/icons-material";
+
+import {
+ TAB_DISPLAY_NAMES,
+ TAB_NAME,
+} from "../../../../typings/tab";
+
+import "./TabButton.css";
+
+
+interface TabButtonProps {
+ tabName: TAB_NAME,
+ Icon: SvgIconComponent,
+ onTabButtonClick: (tabName: TAB_NAME) => void
+}
+
+/**
+ * Renders a tooltip-wrapped tab button.
+ *
+ * @param props
+ * @param props.tabName
+ * @param props.Icon
+ * @param props.onTabButtonClick
+ * @return
+ */
+const TabButton = ({tabName, Icon, onTabButtonClick}: TabButtonProps) => {
+ const handleClick = () => {
+ onTabButtonClick(tabName);
+ };
+
+ return (
+
+
+
+
+
+ );
+};
+
+export default TabButton;
diff --git a/new-log-viewer/src/components/CentralContainer/Sidebar/SidebarTabs/TitleButton.css b/new-log-viewer/src/components/CentralContainer/Sidebar/SidebarTabs/TitleButton.css
new file mode 100644
index 00000000..2bb9eb0f
--- /dev/null
+++ b/new-log-viewer/src/components/CentralContainer/Sidebar/SidebarTabs/TitleButton.css
@@ -0,0 +1,4 @@
+.sidebar-tab-title-button {
+ min-width: 0 !important;
+ min-height: 0 !important;
+}
diff --git a/new-log-viewer/src/components/CentralContainer/Sidebar/SidebarTabs/TitleButton.tsx b/new-log-viewer/src/components/CentralContainer/Sidebar/SidebarTabs/TitleButton.tsx
new file mode 100644
index 00000000..117037cd
--- /dev/null
+++ b/new-log-viewer/src/components/CentralContainer/Sidebar/SidebarTabs/TitleButton.tsx
@@ -0,0 +1,21 @@
+import {
+ IconButton,
+ IconButtonProps,
+} from "@mui/joy";
+
+import "./TitleButton.css";
+
+
+/**
+ * Renders an IconButton with an additional CSS class 'sidebar-tab-title-button'.
+ *
+ * @param props
+ * @return
+ */
+const TitleButton = (props: IconButtonProps) => (
+
+);
+
+export default TitleButton;
diff --git a/new-log-viewer/src/components/CentralContainer/Sidebar/SidebarTabs/index.css b/new-log-viewer/src/components/CentralContainer/Sidebar/SidebarTabs/index.css
new file mode 100644
index 00000000..f9b4e2c3
--- /dev/null
+++ b/new-log-viewer/src/components/CentralContainer/Sidebar/SidebarTabs/index.css
@@ -0,0 +1,9 @@
+.sidebar-tabs {
+ flex-grow: 1;
+ width: calc(100% - var(--ylv-panel-resize-handle-width));
+ height: 100%;
+}
+
+.sidebar-tab-list-spacing {
+ flex-grow: 1;
+}
diff --git a/new-log-viewer/src/components/CentralContainer/Sidebar/SidebarTabs/index.tsx b/new-log-viewer/src/components/CentralContainer/Sidebar/SidebarTabs/index.tsx
new file mode 100644
index 00000000..51258b0d
--- /dev/null
+++ b/new-log-viewer/src/components/CentralContainer/Sidebar/SidebarTabs/index.tsx
@@ -0,0 +1,109 @@
+import {
+ forwardRef,
+ useState,
+} from "react";
+
+import {
+ TabList,
+ Tabs,
+} from "@mui/joy";
+
+import {SvgIconComponent} from "@mui/icons-material";
+import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined";
+import SearchIcon from "@mui/icons-material/Search";
+import SettingsOutlinedIcon from "@mui/icons-material/SettingsOutlined";
+
+import {TAB_NAME} from "../../../../typings/tab";
+import SettingsModal from "../../../modals/SettingsModal";
+import FileInfoTabPanel from "./FileInfoTabPanel";
+import SearchTabPanel from "./SearchTabPanel";
+import TabButton from "./TabButton";
+
+import "./index.css";
+
+
+/**
+ * Lists information for each tab.
+ */
+const TABS_INFO_LIST: Readonly> = Object.freeze([
+ {tabName: TAB_NAME.FILE_INFO, Icon: InfoOutlinedIcon},
+ {tabName: TAB_NAME.SEARCH, Icon: SearchIcon},
+]);
+
+interface SidebarTabsProps {
+ activeTabName: TAB_NAME,
+ onActiveTabNameChange: (newValue: TAB_NAME) => void,
+}
+
+/**
+ * Displays a set of tabs in a vertical orientation.
+ *
+ * @param tabListRef Reference object used to access the TabList DOM element.
+ * @return
+ */
+const SidebarTabs = forwardRef((
+ {
+ activeTabName,
+ onActiveTabNameChange,
+ },
+ tabListRef
+) => {
+ const [isSettingsModalOpen, setIsSettingsModalOpen] = useState(false);
+
+ const handleSettingsModalClose = () => {
+ setIsSettingsModalOpen(false);
+ };
+
+ const handleTabButtonClick = (tabName: TAB_NAME) => {
+ switch (tabName) {
+ case TAB_NAME.SETTINGS:
+ setIsSettingsModalOpen(true);
+ break;
+ default:
+ onActiveTabNameChange(tabName);
+ }
+ };
+
+ return (
+ <>
+
+
+ {TABS_INFO_LIST.map(({tabName, Icon}) => (
+
+ ))}
+
+ {/* Forces the settings tab to bottom of sidebar. */}
+
+
+
+
+
+
+
+
+ >
+ );
+});
+
+SidebarTabs.displayName = "SidebarTabs";
+export default SidebarTabs;
diff --git a/new-log-viewer/src/components/CentralContainer/Sidebar/index.css b/new-log-viewer/src/components/CentralContainer/Sidebar/index.css
new file mode 100644
index 00000000..323fb710
--- /dev/null
+++ b/new-log-viewer/src/components/CentralContainer/Sidebar/index.css
@@ -0,0 +1,3 @@
+.sidebar-tabs-container {
+ display: flex;
+}
diff --git a/new-log-viewer/src/components/CentralContainer/Sidebar/index.tsx b/new-log-viewer/src/components/CentralContainer/Sidebar/index.tsx
new file mode 100644
index 00000000..1ef8f3d7
--- /dev/null
+++ b/new-log-viewer/src/components/CentralContainer/Sidebar/index.tsx
@@ -0,0 +1,105 @@
+import {
+ useCallback,
+ useRef,
+ useState,
+} from "react";
+
+import {TAB_NAME} from "../../../typings/tab";
+import ResizeHandle from "./ResizeHandle";
+import SidebarTabs from "./SidebarTabs";
+
+import "./index.css";
+
+
+const PANEL_DEFAULT_WIDTH_IN_PIXEL = 360;
+const PANEL_CLIP_THRESHOLD_IN_PIXEL = 250;
+const PANEL_MAX_WIDTH_TO_WINDOW_WIDTH_RATIO = 0.8;
+
+/**
+ * Gets width of the panel from body style properties.
+ *
+ * @return the width in pixels as a number.
+ */
+const getPanelWidth = () => parseInt(
+ document.body.style.getPropertyValue("--ylv-panel-width"),
+ 10
+);
+
+/**
+ * Sets width of the panel in body style properties.
+ *
+ * @param newValue in pixels.
+ */
+const setPanelWidth = (newValue: number) => {
+ document.body.style.setProperty("--ylv-panel-width", `${newValue}px`);
+};
+
+
+/**
+ * Renders a sidebar component that displays tabbed panels and a resize handle.
+ * The active tab can be changed and the sidebar can be resized by dragging the handle.
+ *
+ * @return
+ */
+const Sidebar = () => {
+ const [activeTabName, setActiveTabName] = useState(TAB_NAME.SEARCH);
+
+ const tabListRef = useRef(null);
+
+ const handleActiveTabNameChange = useCallback((tabName: TAB_NAME) => {
+ if (null === tabListRef.current) {
+ console.error("Unexpected null tabListRef.current");
+
+ return;
+ }
+
+ if (activeTabName === tabName) {
+ setActiveTabName(TAB_NAME.NONE);
+ setPanelWidth(tabListRef.current.clientWidth);
+
+ return;
+ }
+ setActiveTabName(tabName);
+ setPanelWidth(PANEL_DEFAULT_WIDTH_IN_PIXEL);
+ }, [activeTabName]);
+
+ const handleResizeHandleRelease = useCallback(() => {
+ if (getPanelWidth() === tabListRef.current?.clientWidth) {
+ setActiveTabName(TAB_NAME.NONE);
+ }
+ }, []);
+
+ const handleResize = useCallback((resizeHandlePosition: number) => {
+ if (null === tabListRef.current) {
+ console.error("Unexpected null tabListRef.current");
+
+ return;
+ }
+ if (tabListRef.current.clientWidth + PANEL_CLIP_THRESHOLD_IN_PIXEL > resizeHandlePosition) {
+ // If the resize handle is positioned to the right of the 's right edge
+ // with a clipping threshold accounted, close the panel.
+ setPanelWidth(tabListRef.current.clientWidth);
+ } else if (
+ resizeHandlePosition < window.innerWidth * PANEL_MAX_WIDTH_TO_WINDOW_WIDTH_RATIO
+ ) {
+ // If the resize handle is positioned to the left of 80% of the window's width,
+ // update the panel width with the distance between the mouse pointer and the
+ // window's left edge.
+ setPanelWidth(resizeHandlePosition);
+ }
+ }, []);
+
+ return (
+
+
+ {TAB_NAME.NONE !== activeTabName && }
+
+ );
+};
+
+export default Sidebar;
diff --git a/new-log-viewer/src/components/CentralContainer/index.css b/new-log-viewer/src/components/CentralContainer/index.css
new file mode 100644
index 00000000..b4c9192a
--- /dev/null
+++ b/new-log-viewer/src/components/CentralContainer/index.css
@@ -0,0 +1,13 @@
+:root {
+ --ylv-panel-width: 360px;
+}
+
+.central-container {
+ display: grid;
+ grid-template-columns: var(--ylv-panel-width) 1fr;
+ width: 100vw;
+}
+
+.central-container-children-container {
+ width: calc(100vw - var(--ylv-panel-width));
+}
diff --git a/new-log-viewer/src/components/CentralContainer/index.tsx b/new-log-viewer/src/components/CentralContainer/index.tsx
new file mode 100644
index 00000000..1e65da63
--- /dev/null
+++ b/new-log-viewer/src/components/CentralContainer/index.tsx
@@ -0,0 +1,30 @@
+import React from "react";
+
+import Sidebar from "./Sidebar";
+
+import "./index.css";
+
+
+interface CentralContainerProps {
+ children: React.ReactNode,
+}
+
+/**
+ * Locates in the center of the and wraps a children with a sidebar component on its left.
+ *
+ * @param props
+ * @param props.children
+ * @return
+ */
+const CentralContainer = ({children}: CentralContainerProps) => {
+ return (
+
+ );
+};
+
+export default CentralContainer;
diff --git a/new-log-viewer/src/components/DropFileContainer/index.css b/new-log-viewer/src/components/DropFileContainer/index.css
index 6d6ca94b..0b653f15 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;
z-index: var(--ylv-drop-file-container-hover-mask-z-index);
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 && (
{
modeStorageKey={CONFIG_KEY.THEME}
theme={APP_THEME}
>
-
-
+
+
-
-
+
+
);
};
diff --git a/new-log-viewer/src/components/MenuBar/index.css b/new-log-viewer/src/components/MenuBar/index.css
index c9f0f0c6..08fe4c66 100644
--- a/new-log-viewer/src/components/MenuBar/index.css
+++ b/new-log-viewer/src/components/MenuBar/index.css
@@ -1,6 +1,28 @@
.menu-bar {
+ z-index: var(--ylv-menu-bar-z-index);
+
display: flex;
flex-direction: row;
align-items: center;
+
height: var(--ylv-status-bar-height);
+
+ /* stylelint-disable-next-line custom-property-pattern */
+ box-shadow: 0 1px 0 0 var(--joy-palette-neutral-outlinedBorder);
+}
+
+.menu-bar-logo-container {
+ display: flex;
+ justify-content: center;
+ width: 48px;
+ height: var(--ylv-status-bar-height);
+}
+
+.menu-bar-open-file-icon {
+ font-size: 24px !important;
+}
+
+.menu-bar-filename {
+ flex-grow: 1;
+ padding-left: 10px;
}
diff --git a/new-log-viewer/src/components/MenuBar/index.tsx b/new-log-viewer/src/components/MenuBar/index.tsx
index e2a21487..aac5c979 100644
--- a/new-log-viewer/src/components/MenuBar/index.tsx
+++ b/new-log-viewer/src/components/MenuBar/index.tsx
@@ -1,26 +1,20 @@
-import {
- useContext,
- useState,
-} from "react";
+import {useContext} from "react";
import {
Divider,
+ IconButton,
Sheet,
- Stack,
+ Tooltip,
Typography,
} from "@mui/joy";
-import DescriptionIcon from "@mui/icons-material/Description";
-import FileOpenIcon from "@mui/icons-material/FileOpen";
-import SettingsIcon from "@mui/icons-material/Settings";
+import FolderOpenIcon from "@mui/icons-material/FolderOpen";
import {StateContext} from "../../contexts/StateContextProvider";
import {CURSOR_CODE} from "../../typings/worker";
import {openFile} from "../../utils/file";
-import SettingsModal from "../modals/SettingsModal";
import ExportLogsButton from "./ExportLogsButton";
import NavigationBar from "./NavigationBar";
-import SmallIconButton from "./SmallIconButton";
import "./index.css";
@@ -33,55 +27,51 @@ import "./index.css";
const MenuBar = () => {
const {fileName, loadFile} = useContext(StateContext);
- const [isSettingsModalOpen, setIsSettingsModalOpen] = useState
(false);
-
- const handleOpenFileButtonClick = () => {
+ const handleOpenFile = () => {
openFile((file) => {
loadFile(file, {code: CURSOR_CODE.LAST_EVENT, args: null});
});
};
- const handleSettingsModalClose = () => {
- setIsSettingsModalOpen(false);
- };
-
- const handleSettingsModalOpen = () => {
- setIsSettingsModalOpen(true);
- };
-
return (
<>
-
+
+
+
+
+
+
+
+
+
+
+
+
-
-
- {fileName}
-
-
+ {fileName}
+
-
-
-
-
-
-
-
+
-
>
);
};
diff --git a/new-log-viewer/src/components/StatusBar/index.css b/new-log-viewer/src/components/StatusBar/index.css
index 4804adc6..e5cf5af6 100644
--- a/new-log-viewer/src/components/StatusBar/index.css
+++ b/new-log-viewer/src/components/StatusBar/index.css
@@ -1,11 +1,15 @@
.status-bar {
position: absolute;
+ z-index: var(--ylv-status-bar-z-index);
bottom: 0;
display: flex;
align-items: center;
width: 100%;
+
+ /* stylelint-disable-next-line custom-property-pattern */
+ box-shadow: 0 -1px 0 0 var(--joy-palette-neutral-outlinedBorder);
}
.status-message {
diff --git a/new-log-viewer/src/components/theme.tsx b/new-log-viewer/src/components/theme.tsx
index a183c719..f040dbf2 100644
--- a/new-log-viewer/src/components/theme.tsx
+++ b/new-log-viewer/src/components/theme.tsx
@@ -46,29 +46,10 @@ const APP_THEME = extendTheme({
body: "var(--ylv-ui-font-family)",
},
components: {
- JoyButton: {
- styleOverrides: {
- root: {
- borderRadius: "2px",
- },
- },
- },
JoySelect: {
defaultProps: {
indicator: ,
},
- styleOverrides: {
- root: {
- borderRadius: "2px",
- },
- },
- },
- JoyInput: {
- styleOverrides: {
- root: {
- borderRadius: "2px",
- },
- },
},
JoyFormControl: {
styleOverrides: {
@@ -83,6 +64,13 @@ const APP_THEME = extendTheme({
},
},
},
+ radius: {
+ xs: "2px",
+ sm: "2px",
+ md: "2px",
+ lg: "2px",
+ xl: "2px",
+ },
});
export default APP_THEME;
diff --git a/new-log-viewer/src/contexts/StateContextProvider.tsx b/new-log-viewer/src/contexts/StateContextProvider.tsx
index c6e07803..3de5c67d 100644
--- a/new-log-viewer/src/contexts/StateContextProvider.tsx
+++ b/new-log-viewer/src/contexts/StateContextProvider.tsx
@@ -46,6 +46,7 @@ interface StateContextType {
logData: string,
numEvents: number,
numPages: number,
+ originalFileSizeInBytes: number,
pageNum: Nullable,
exportLogs: () => void,
@@ -61,9 +62,10 @@ const STATE_DEFAULT: Readonly = Object.freeze({
beginLineNumToLogEventNum: new Map(),
exportProgress: null,
fileName: "",
- logData: "Loading...",
+ logData: "No file is open.",
numEvents: 0,
numPages: 0,
+ originalFileSizeInBytes: 0,
pageNum: 0,
exportLogs: () => null,
@@ -137,7 +139,7 @@ const workerPostReq = (
* @param props.children
* @return
*/
-// eslint-disable-next-line max-lines-per-function
+// eslint-disable-next-line max-lines-per-function, max-statements
const StateContextProvider = ({children}: StateContextProviderProps) => {
const {filePath, logEventNum} = useContext(UrlContext);
@@ -145,6 +147,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 [exportProgress, setExportProgress] =
@@ -170,6 +174,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.NOTIFICATION:
// eslint-disable-next-line no-warning-comments
@@ -235,6 +240,9 @@ const StateContextProvider = ({children}: StateContextProviderProps) => {
decoderOptions: getConfig(CONFIG_KEY.DECODER_OPTIONS),
});
+ setFileName("Loading...");
+ setLogData("Loading...");
+ setOriginalFileSizeInBytes(STATE_DEFAULT.originalFileSizeInBytes);
setExportProgress(STATE_DEFAULT.exportProgress);
}, [
handleMainWorkerResp,
@@ -330,6 +338,7 @@ const StateContextProvider = ({children}: StateContextProviderProps) => {
logData: logData,
numEvents: numEvents,
numPages: numPagesRef.current,
+ originalFileSizeInBytes: originalFileSizeInBytes,
pageNum: pageNumRef.current,
exportLogs: exportLogs,
@@ -342,6 +351,5 @@ const StateContextProvider = ({children}: StateContextProviderProps) => {
);
};
-
export default StateContextProvider;
export {StateContext};
diff --git a/new-log-viewer/src/index.css b/new-log-viewer/src/index.css
index e354c3b9..ecf9a72c 100644
--- a/new-log-viewer/src/index.css
+++ b/new-log-viewer/src/index.css
@@ -18,6 +18,7 @@ html {
/* size globals */
--ylv-status-bar-height: 32px;
--ylv-menu-bar-height: 32px;
+ --ylv-panel-resize-handle-width: 4px;
/* z-index globals
*
@@ -26,6 +27,9 @@ html {
* .monaco-editor .minimap { z-index: 5; }
* ```
*/
+ --ylv-resize-handle-z-index: 1;
+ --ylv-menu-bar-z-index: 6;
+ --ylv-status-bar-z-index: 6;
--ylv-drop-file-container-hover-mask-z-index: 10;
--ylv-drop-file-container-hover-message-z-index: 11;
}
diff --git a/new-log-viewer/src/services/LogFileManager.ts b/new-log-viewer/src/services/LogFileManager.ts
index 2ceded83..37ed6072 100644
--- a/new-log-viewer/src/services/LogFileManager.ts
+++ b/new-log-viewer/src/services/LogFileManager.ts
@@ -53,6 +53,8 @@ class LogFileManager {
readonly #fileName: string;
+ readonly #originalFileSizeInBytes: number;
+
#decoder: Decoder;
#numEvents: number = 0;
@@ -63,14 +65,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;
@@ -92,6 +97,10 @@ class LogFileManager {
return this.#numEvents;
}
+ get originalFileSizeInBytes () {
+ return this.#originalFileSizeInBytes;
+ }
+
/**
* Creates a new LogFileManager.
*
@@ -109,7 +118,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 a04d134c..d44d88fd 100644
--- a/new-log-viewer/src/services/MainWorker.ts
+++ b/new-log-viewer/src/services/MainWorker.ts
@@ -73,6 +73,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/tab.ts b/new-log-viewer/src/typings/tab.ts
new file mode 100644
index 00000000..8f4ba857
--- /dev/null
+++ b/new-log-viewer/src/typings/tab.ts
@@ -0,0 +1,21 @@
+enum TAB_NAME {
+ NONE = "none",
+ FILE_INFO = "fileInfo",
+ SEARCH = "search",
+ SETTINGS = "settings",
+}
+
+/**
+ * Maps the TAB_NAME enum values to their corresponding display names.
+ */
+const TAB_DISPLAY_NAMES: Record = Object.freeze({
+ [TAB_NAME.NONE]: "None",
+ [TAB_NAME.FILE_INFO]: "File info",
+ [TAB_NAME.SEARCH]: "Search",
+ [TAB_NAME.SETTINGS]: "Settings",
+});
+
+export {
+ TAB_DISPLAY_NAMES,
+ TAB_NAME,
+};
diff --git a/new-log-viewer/src/typings/worker.ts b/new-log-viewer/src/typings/worker.ts
index 82b186c0..c16b1230 100644
--- a/new-log-viewer/src/typings/worker.ts
+++ b/new-log-viewer/src/typings/worker.ts
@@ -73,6 +73,7 @@ type WorkerRespMap = {
[WORKER_RESP_CODE.LOG_FILE_INFO]: {
fileName: string,
numEvents: number,
+ originalFileSizeInBytes: number,
},
[WORKER_RESP_CODE.PAGE_DATA]: {
logs: string,