diff --git a/new-log-viewer/public/favicon.svg b/new-log-viewer/public/favicon.svg new file mode 100644 index 00000000..baeb3b46 --- /dev/null +++ b/new-log-viewer/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/new-log-viewer/public/index.html b/new-log-viewer/public/index.html index 799ad411..40bce47e 100644 --- a/new-log-viewer/public/index.html +++ b/new-log-viewer/public/index.html @@ -10,6 +10,8 @@ +
diff --git a/new-log-viewer/src/components/CentralContainer/Sidebar/ResizeHandle.css b/new-log-viewer/src/components/CentralContainer/Sidebar/ResizeHandle.css new file mode 100644 index 00000000..522a8ce7 --- /dev/null +++ b/new-log-viewer/src/components/CentralContainer/Sidebar/ResizeHandle.css @@ -0,0 +1,23 @@ +.resize-handle { + cursor: ew-resize; + + z-index: var(--ylv-resize-handle-z-index); + + box-sizing: border-box; + width: var(--ylv-panel-resize-handle-width); + height: 100%; + + /* stylelint-disable-next-line custom-property-pattern */ + background-color: var(--joy-palette-background-surface, #fbfcfe); + /* stylelint-disable-next-line custom-property-pattern */ + border-right: 1px solid var(--joy-palette-neutral-outlinedBorder, #cdd7e1); +} + +.resize-handle-holding, +.resize-handle:hover { + box-sizing: initial; + + /* stylelint-disable-next-line custom-property-pattern */ + background-color: var(--joy-palette-primary-solidHoverBg, #0258a8); + border-right: initial; +} diff --git a/new-log-viewer/src/components/CentralContainer/Sidebar/ResizeHandle.tsx b/new-log-viewer/src/components/CentralContainer/Sidebar/ResizeHandle.tsx new file mode 100644 index 00000000..48198d56 --- /dev/null +++ b/new-log-viewer/src/components/CentralContainer/Sidebar/ResizeHandle.tsx @@ -0,0 +1,87 @@ +import React, { + useCallback, + useEffect, + useState, +} from "react"; + +import "./ResizeHandle.css"; + + +interface ResizeHandleProps { + onHandleRelease: () => void, + + /** + * Gets triggered when a resize event occurs. + * + * @param resizeHandlePosition The horizontal distance, in pixels, between the mouse pointer + * and the left edge of the viewport. + */ + onResize: (resizeHandlePosition: number) => void, +} + +/** + * A vertical handle for resizing an object. + * + * @param props + * @param props.onResize The method to call when a resize occurs. + * @param props.onHandleRelease + * @return + */ +const ResizeHandle = ({ + onResize, + onHandleRelease, +}: ResizeHandleProps) => { + const [isMouseDown, setIsMouseDown] = useState(false); + + const handleMouseDown = (ev: React.MouseEvent) => { + ev.preventDefault(); + setIsMouseDown(true); + }; + + const handleMouseMove = useCallback((ev: MouseEvent) => { + ev.preventDefault(); + onResize(ev.clientX); + }, [onResize]); + + const handleMouseUp = useCallback((ev: MouseEvent) => { + ev.preventDefault(); + setIsMouseDown(false); + onHandleRelease(); + }, [onHandleRelease]); + + // Register the event listener for mouse up. + useEffect(() => { + window.addEventListener("mouseup", handleMouseUp); + + return () => { + window.removeEventListener("mouseup", handleMouseUp); + }; + }, [handleMouseUp]); + + // On mouse down, register the event listener for mouse move. + useEffect(() => { + if (false === isMouseDown) { + return () => null; + } + + window.addEventListener("mousemove", handleMouseMove); + + return () => { + window.removeEventListener("mousemove", handleMouseMove); + }; + }, [ + handleMouseMove, + isMouseDown, + ]); + + return ( +
+ ); +}; + + +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 ? + : + } + + } + > +