diff --git a/src/components/CentralContainer/Sidebar/SidebarTabs/CustomTabPanel.css b/src/components/CentralContainer/Sidebar/SidebarTabs/CustomTabPanel.css index 8169315f..f6b1f720 100644 --- a/src/components/CentralContainer/Sidebar/SidebarTabs/CustomTabPanel.css +++ b/src/components/CentralContainer/Sidebar/SidebarTabs/CustomTabPanel.css @@ -2,12 +2,19 @@ padding: 0.75rem; } +.sidebar-tab-panel-container { + display: flex; + flex-direction: column; + height: 100%; +} + .sidebar-tab-panel-title-container { user-select: none; 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/src/components/CentralContainer/Sidebar/SidebarTabs/CustomTabPanel.tsx b/src/components/CentralContainer/Sidebar/SidebarTabs/CustomTabPanel.tsx index 9d1ab3e7..2b5d5bc2 100644 --- a/src/components/CentralContainer/Sidebar/SidebarTabs/CustomTabPanel.tsx +++ b/src/components/CentralContainer/Sidebar/SidebarTabs/CustomTabPanel.tsx @@ -1,6 +1,8 @@ import React from "react"; import { + Box, + ButtonGroup, DialogContent, DialogTitle, TabPanel, @@ -14,6 +16,7 @@ interface CustomTabPanelProps { children: React.ReactNode, tabName: string, title: string, + titleButtons?: React.ReactNode, } /** @@ -23,25 +26,40 @@ interface CustomTabPanelProps { * @param props.children * @param props.tabName * @param props.title + * @param props.titleButtons * @return */ -const CustomTabPanel = ({children, tabName, title}: CustomTabPanelProps) => { +const CustomTabPanel = ({ + children, + tabName, + title, + titleButtons, +}: CustomTabPanelProps) => { return ( - - - {title} - - - - {children} - + + + + {title} + + + {titleButtons} + + + + {children} + + ); }; diff --git a/src/components/CentralContainer/Sidebar/SidebarTabs/PanelTitleButton.css b/src/components/CentralContainer/Sidebar/SidebarTabs/PanelTitleButton.css new file mode 100644 index 00000000..27a464ec --- /dev/null +++ b/src/components/CentralContainer/Sidebar/SidebarTabs/PanelTitleButton.css @@ -0,0 +1,4 @@ +.tab-panel-title-button { + min-width: 0 !important; + min-height: 0 !important; +} diff --git a/src/components/CentralContainer/Sidebar/SidebarTabs/PanelTitleButton.tsx b/src/components/CentralContainer/Sidebar/SidebarTabs/PanelTitleButton.tsx new file mode 100644 index 00000000..b527a564 --- /dev/null +++ b/src/components/CentralContainer/Sidebar/SidebarTabs/PanelTitleButton.tsx @@ -0,0 +1,25 @@ +import { + IconButton, + IconButtonProps, +} from "@mui/joy"; + +import "./PanelTitleButton.css"; + + +/** + * Renders an IconButton for use in sidebar tab titles. + * + * @param props + * @return + */ +const PanelTitleButton = (props: IconButtonProps) => { + const {className, ...rest} = props; + return ( + + ); +}; + + +export default PanelTitleButton; diff --git a/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/Result.css b/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/Result.css new file mode 100644 index 00000000..eac317d2 --- /dev/null +++ b/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/Result.css @@ -0,0 +1,20 @@ +.result-button { + user-select: none; + + overflow-x: hidden; + + width: 100%; + padding-left: 12px; + + text-align: left; + text-overflow: ellipsis; + white-space: nowrap; +} + +.result-button:hover { + cursor: default; +} + +.result-button-text { + font-family: Inter, sans-serif !important; +} diff --git a/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/Result.tsx b/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/Result.tsx new file mode 100644 index 00000000..e3d0576b --- /dev/null +++ b/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/Result.tsx @@ -0,0 +1,70 @@ +import { + ListItemButton, + Typography, +} from "@mui/joy"; + +import {updateWindowUrlHashParams} from "../../../../../contexts/UrlContextProvider"; + +import "./Result.css"; + + +interface ResultProps { + logEventNum: number, + message: string, + matchRange: [number, number] +} + +const QUERY_RESULT_PREFIX_MAX_CHARACTERS = 20; + +/** + * Renders a query result as a button with a message, highlighting the first matching text range. + * + * @param props + * @param props.message + * @param props.matchRange A two-element array [begin, end) representing the indices of the matching + * text range. + * @param props.logEventNum + * @return + */ +const Result = ({logEventNum, message, matchRange}: ResultProps) => { + const [ + beforeMatch, + match, + afterMatch, + ] = [ + message.slice(0, matchRange[0]), + message.slice(...matchRange), + message.slice(matchRange[1]), + ]; + const handleResultButtonClick = () => { + updateWindowUrlHashParams({logEventNum}); + }; + + return ( + + + + {(QUERY_RESULT_PREFIX_MAX_CHARACTERS < beforeMatch.length) && "..."} + {beforeMatch.slice(-QUERY_RESULT_PREFIX_MAX_CHARACTERS)} + + + {match} + + {afterMatch} + + + ); +}; + + +export default Result; diff --git a/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/ResultsGroup.css b/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/ResultsGroup.css new file mode 100644 index 00000000..2729ecdd --- /dev/null +++ b/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/ResultsGroup.css @@ -0,0 +1,31 @@ +.results-group-summary-button { + cursor: default !important; + flex-direction: row-reverse !important; + gap: 2px !important; + padding-inline-start: 0 !important; +} + +.results-group-summary-container { + display: flex; + flex-grow: 1; +} + +.results-group-summary-text-container { + flex-grow: 1; + gap: 0.2rem; + align-items: center; +} + +.results-group-summary-count { + border-radius: 4px !important; +} + +.results-group-details { + margin-left: 1.5px !important; + /* stylelint-disable-next-line custom-property-pattern */ + border-left: 1px solid var(--joy-palette-neutral-outlinedBorder, #cdd7e1); +} + +.results-group-details-content { + padding-block: 0 !important; +} diff --git a/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/ResultsGroup.tsx b/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/ResultsGroup.tsx new file mode 100644 index 00000000..28e1ce4a --- /dev/null +++ b/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/ResultsGroup.tsx @@ -0,0 +1,114 @@ +import React, { + memo, + useEffect, + useState, +} from "react"; + +import { + Accordion, + AccordionDetails, + AccordionSummary, + Box, + Chip, + List, + Stack, + Typography, +} from "@mui/joy"; + +import DescriptionOutlinedIcon from "@mui/icons-material/DescriptionOutlined"; + +import {QueryResultsType} from "../../../../../typings/worker"; +import Result from "./Result"; + +import "./ResultsGroup.css"; + + +interface ResultsGroupProps { + isAllExpanded: boolean, + pageNum: number, + results: QueryResultsType[], +} + +/** + * Renders a group of results, where each group represents a list of results from a single page. + * + * @param props + * @param props.isAllExpanded + * @param props.pageNum + * @param props.results + * @return + */ +const ResultsGroup = memo(({ + isAllExpanded, + pageNum, + results, +}: ResultsGroupProps) => { + const [isExpanded, setIsExpanded] = useState(isAllExpanded); + + const handleAccordionChange = ( + _: React.SyntheticEvent, + newValue: boolean + ) => { + setIsExpanded(newValue); + }; + + // On `isAllExpanded` update, sync current results group's expand status. + useEffect(() => { + setIsExpanded(isAllExpanded); + }, [isAllExpanded]); + + return ( + + + + + + + {"Page "} + {pageNum} + + + + {results.length} + + + + + + {results.map((r, index) => ( + + ))} + + + + ); +}); + +ResultsGroup.displayName = "ResultsGroup"; + + +export default ResultsGroup; diff --git a/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/index.css b/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/index.css new file mode 100644 index 00000000..0670f821 --- /dev/null +++ b/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/index.css @@ -0,0 +1,40 @@ +.search-tab-container { + overflow-y: hidden; + display: flex; + flex-direction: column; + height: 100%; +} + +.query-input-box-with-progress { + /* JoyUI has a rounding issue when calculating the Textarea width, causing it to overflow its + container. */ + margin-right: 1px; +} + +.query-input-box { + flex-direction: row !important; + border-radius: 0 !important; +} + +.query-option-button { + font-family: Inter, sans-serif !important; +} + +.query-input-box-textarea { + width: 0; +} + +.query-input-box-end-decorator { + display: block !important; + margin-block-start: 0 !important; +} + +.query-input-box-linear-progress { + /* stylelint-disable-next-line custom-property-pattern */ + --LinearProgress-radius: 0 !important; +} + +.query-results { + overflow-y: auto; + flex-grow: 1; +} diff --git a/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/index.tsx b/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/index.tsx new file mode 100644 index 00000000..631878f1 --- /dev/null +++ b/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/index.tsx @@ -0,0 +1,135 @@ +import React, { + useContext, + useState, +} from "react"; + +import { + AccordionGroup, + Box, + IconButton, + LinearProgress, + Textarea, + ToggleButtonGroup, +} from "@mui/joy"; + +import UnfoldLessIcon from "@mui/icons-material/UnfoldLess"; +import UnfoldMoreIcon from "@mui/icons-material/UnfoldMore"; + +import {StateContext} from "../../../../../contexts/StateContextProvider"; +import {UI_ELEMENT} from "../../../../../typings/states"; +import { + TAB_DISPLAY_NAMES, + TAB_NAME, +} from "../../../../../typings/tab"; +import {QUERY_PROGRESS_DONE} from "../../../../../typings/worker"; +import {isDisabled} from "../../../../../utils/states"; +import CustomTabPanel from "../CustomTabPanel"; +import PanelTitleButton from "../PanelTitleButton"; +import ResultsGroup from "./ResultsGroup"; + +import "./index.css"; + + +enum QUERY_OPTION { + IS_CASE_SENSITIVE = "isCaseSensitive", + IS_REGEX = "isRegex" +} + +/** + * Displays a panel for submitting queries and viewing query results. + * + * @return + */ +const SearchTabPanel = () => { + const {queryProgress, queryResults, startQuery, uiState} = useContext(StateContext); + const [isAllExpanded, setIsAllExpanded] = useState(true); + const [queryOptions, setQueryOptions] = useState([]); + + const handleQueryInputChange = (ev: React.ChangeEvent) => { + const isCaseSensitive = queryOptions.includes(QUERY_OPTION.IS_CASE_SENSITIVE); + const isRegex = queryOptions.includes(QUERY_OPTION.IS_REGEX); + startQuery(ev.target.value, isRegex, isCaseSensitive); + }; + const handleQueryOptionsChange = ( + _: React.MouseEvent, + newOptions: QUERY_OPTION[] + ) => { + setQueryOptions(newOptions); + }; + + return ( + { setIsAllExpanded((v) => !v); }}> + {isAllExpanded ? + : + } + + } + > + +
+