diff --git a/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/index.tsx b/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/index.tsx index 18c977af..65a4af46 100644 --- a/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/index.tsx +++ b/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/index.tsx @@ -1,5 +1,7 @@ import React, { useContext, + useEffect, + useRef, useState, } from "react"; @@ -11,19 +13,30 @@ import { Textarea, } from "@mui/joy"; +import ShareIcon from "@mui/icons-material/Share"; import UnfoldLessIcon from "@mui/icons-material/UnfoldLess"; import UnfoldMoreIcon from "@mui/icons-material/UnfoldMore"; import {StateContext} from "../../../../../contexts/StateContextProvider"; +import { + copyPermalinkToClipboard, + updateWindowUrlHashParams, + URL_HASH_PARAMS_DEFAULT, + UrlContext, +} from "../../../../../contexts/UrlContextProvider"; import { QUERY_PROGRESS_VALUE_MAX, QueryArgs, } from "../../../../../typings/query"; -import {UI_ELEMENT} from "../../../../../typings/states"; +import { + UI_ELEMENT, + UI_STATE, +} from "../../../../../typings/states"; import { TAB_DISPLAY_NAMES, TAB_NAME, } from "../../../../../typings/tab"; +import {HASH_PARAM_NAMES} from "../../../../../typings/url"; import {isDisabled} from "../../../../../utils/states"; import CustomTabPanel from "../CustomTabPanel"; import PanelTitleButton from "../PanelTitleButton"; @@ -38,21 +51,77 @@ import "./index.css"; * * @return */ +// eslint-disable-next-line max-lines-per-function const SearchTabPanel = () => { const {queryProgress, queryResults, startQuery, uiState} = useContext(StateContext); + const { + queryString: urlQueryString, + queryIsCaseSensitive: urlQueryIsCaseSensitive, + queryIsRegex: urlQueryIsRegex, + } = useContext(UrlContext); const [isAllExpanded, setIsAllExpanded] = useState(true); const [queryString, setQueryString] = useState(""); - const [isCaseSensitive, setIsCaseSensitive] = useState(false); - const [isRegex, setIsRegex] = useState(false); + const [queryIsCaseSensitive, setQueryIsCaseSensitive] = useState(false); + const [queryIsRegex, setQueryIsRegex] = useState(false); + + const queryIsCaseSensitiveRef = useRef(false); + const queryIsRegexRef = useRef(false); + + useEffect(() => { + queryIsCaseSensitiveRef.current = urlQueryIsCaseSensitive ?? false; + }, [urlQueryIsCaseSensitive]); + + useEffect(() => { + queryIsRegexRef.current = urlQueryIsRegex ?? false; + }, [urlQueryIsRegex]); + + useEffect(() => { + if (uiState === UI_STATE.FILE_LOADING) { + setQueryString(""); + setQueryIsCaseSensitive(false); + setQueryIsRegex(false); + } else if (uiState === UI_STATE.READY) { + if (null !== urlQueryString) { + setQueryString(urlQueryString); + setQueryIsCaseSensitive(queryIsCaseSensitiveRef.current); + setQueryIsRegex(queryIsRegexRef.current); + + startQuery({ + queryIsCaseSensitive: queryIsCaseSensitiveRef.current, + queryIsRegex: queryIsRegexRef.current, + queryString: urlQueryString, + }); + + updateWindowUrlHashParams({ + [HASH_PARAM_NAMES.QUERY_STRING]: URL_HASH_PARAMS_DEFAULT.queryString, + [HASH_PARAM_NAMES.QUERY_IS_CASE_SENSITIVE]: + URL_HASH_PARAMS_DEFAULT.queryIsCaseSensitive, + [HASH_PARAM_NAMES.QUERY_IS_REGEX]: URL_HASH_PARAMS_DEFAULT.queryIsRegex, + }); + } + } + }, [ + startQuery, + uiState, + urlQueryString, + ]); const handleCollapseAllButtonClick = () => { setIsAllExpanded((v) => !v); }; + const handleShareButtonClick = () => { + copyPermalinkToClipboard({}, { + logEventNum: null, + queryString: queryString, + queryIsCaseSensitive: queryIsCaseSensitive, + queryIsRegex: queryIsRegex, + }); + }; const handleQuerySubmit = (newArgs: Partial) => { startQuery({ - isCaseSensitive: isCaseSensitive, - isRegex: isRegex, + queryIsCaseSensitive: queryIsCaseSensitive, + queryIsRegex: queryIsRegex, queryString: queryString, ...newArgs, }); @@ -64,13 +133,13 @@ const SearchTabPanel = () => { }; const handleCaseSensitivityButtonClick = () => { - handleQuerySubmit({isCaseSensitive: !isCaseSensitive}); - setIsCaseSensitive(!isCaseSensitive); + handleQuerySubmit({queryIsCaseSensitive: !queryIsCaseSensitive}); + setQueryIsCaseSensitive(!queryIsCaseSensitive); }; const handleRegexButtonClick = () => { - handleQuerySubmit({isRegex: !isRegex}); - setIsRegex(!isRegex); + handleQuerySubmit({queryIsRegex: !queryIsRegex}); + setQueryIsRegex(!queryIsRegex); }; const isQueryInputBoxDisabled = isDisabled(uiState, UI_ELEMENT.QUERY_INPUT_BOX); @@ -80,16 +149,24 @@ const SearchTabPanel = () => { tabName={TAB_NAME.SEARCH} title={TAB_DISPLAY_NAMES[TAB_NAME.SEARCH]} titleButtons={ - - {isAllExpanded ? - : - } - + <> + + {isAllExpanded ? + : + } + + + + + } > @@ -99,6 +176,7 @@ const SearchTabPanel = () => { maxRows={7} placeholder={"Search"} size={"sm"} + value={queryString} endDecorator={ { { { const {postPopUp} = useContext(NotificationContext); - const {filePath, logEventNum} = useContext(UrlContext); + const { + filePath, + logEventNum, + } = useContext(UrlContext); // States const [exportProgress, setExportProgress] = useState>(STATE_DEFAULT.exportProgress); + const [fileName, setFileName] = useState(STATE_DEFAULT.fileName); const [isSettingsModalOpen, setIsSettingsModalOpen] = useState(STATE_DEFAULT.isSettingsModalOpen); - const [fileName, setFileName] = useState(STATE_DEFAULT.fileName); const [logData, setLogData] = useState(STATE_DEFAULT.logData); const [numEvents, setNumEvents] = useState(STATE_DEFAULT.numEvents); const [numPages, setNumPages] = useState(STATE_DEFAULT.numPages); @@ -276,6 +279,20 @@ const StateContextProvider = ({children}: StateContextProviderProps) => { const pageNumRef = useRef(pageNum); const uiStateRef = useRef(uiState); + const startQuery = useCallback((queryArgs: { + queryString: string; + queryIsCaseSensitive: boolean; + queryIsRegex: boolean; + }) => { + setQueryResults(STATE_DEFAULT.queryResults); + if (null === mainWorkerRef.current) { + console.error("Unexpected null mainWorkerRef.current"); + + return; + } + workerPostReq(mainWorkerRef.current, WORKER_REQ_CODE.START_QUERY, queryArgs); + }, []); + const handleMainWorkerResp = useCallback((ev: MessageEvent) => { const {code, args} = ev.data; console.log(`[MainWorker -> Renderer] code=${code}`); @@ -368,16 +385,6 @@ const StateContextProvider = ({children}: StateContextProviderProps) => { } }, [postPopUp]); - const startQuery = useCallback((queryArgs: QueryArgs) => { - setQueryResults(STATE_DEFAULT.queryResults); - if (null === mainWorkerRef.current) { - console.error("Unexpected null mainWorkerRef.current"); - - return; - } - workerPostReq(mainWorkerRef.current, WORKER_REQ_CODE.START_QUERY, queryArgs); - }, []); - const exportLogs = useCallback(() => { if (null === mainWorkerRef.current) { console.error("Unexpected null mainWorkerRef.current"); @@ -406,6 +413,8 @@ const StateContextProvider = ({children}: StateContextProviderProps) => { setLogData("Loading..."); setOnDiskFileSizeInBytes(STATE_DEFAULT.onDiskFileSizeInBytes); setExportProgress(STATE_DEFAULT.exportProgress); + setQueryResults(STATE_DEFAULT.queryResults); + setQueryProgress(QUERY_PROGRESS_VALUE_MIN); // Cache `fileSrc` for reloads. fileSrcRef.current = fileSrc; diff --git a/src/contexts/UrlContextProvider.tsx b/src/contexts/UrlContextProvider.tsx index fe02d6d2..f7d8515e 100644 --- a/src/contexts/UrlContextProvider.tsx +++ b/src/contexts/UrlContextProvider.tsx @@ -31,6 +31,9 @@ const URL_SEARCH_PARAMS_DEFAULT = Object.freeze({ */ const URL_HASH_PARAMS_DEFAULT = Object.freeze({ [HASH_PARAM_NAMES.LOG_EVENT_NUM]: null, + [HASH_PARAM_NAMES.QUERY_IS_CASE_SENSITIVE]: false, + [HASH_PARAM_NAMES.QUERY_IS_REGEX]: false, + [HASH_PARAM_NAMES.QUERY_STRING]: null, }); /** @@ -101,7 +104,7 @@ const getUpdatedSearchParams = (updates: UrlSearchParamUpdatesType) => { const getUpdatedHashParams = (updates: UrlHashParamUpdatesType) => { const newHashParams = new URLSearchParams(window.location.hash.substring(1)); for (const [key, value] of Object.entries(updates)) { - if (null === value) { + if (null === value || false === value) { newHashParams.delete(key); } else { newHashParams.set(key, String(value)); @@ -181,6 +184,10 @@ const getWindowUrlSearchParams = () => { ); const urlSearchParams = new URLSearchParams(window.location.search.substring(1)); + urlSearchParams.forEach((value, key) => { + searchParams[key as keyof UrlSearchParams] = value; + }); + if (urlSearchParams.has(SEARCH_PARAM_NAMES.FILE_PATH)) { // Split the search string and take everything after as `filePath` value. // This ensures any parameters following `filePath=` are incorporated into the `filePath`. @@ -211,6 +218,21 @@ const getWindowUrlHashParams = () => { null : parsed; } + const queryString = hashParams.get(HASH_PARAM_NAMES.QUERY_STRING); + if (null !== queryString) { + urlHashParams[HASH_PARAM_NAMES.QUERY_STRING] = queryString; + } + + const isCaseSensitive = hashParams.get(HASH_PARAM_NAMES.QUERY_IS_CASE_SENSITIVE); + if (null !== isCaseSensitive) { + urlHashParams[HASH_PARAM_NAMES.QUERY_IS_CASE_SENSITIVE] = + "true" === isCaseSensitive.toLowerCase(); + } + + const isRegex = hashParams.get(HASH_PARAM_NAMES.QUERY_IS_REGEX); + if (null !== isRegex) { + urlHashParams[HASH_PARAM_NAMES.QUERY_IS_REGEX] = "true" === isRegex.toLowerCase(); + } return urlHashParams; }; diff --git a/src/services/LogFileManager/index.ts b/src/services/LogFileManager/index.ts index 5753be00..266edb4d 100644 --- a/src/services/LogFileManager/index.ts +++ b/src/services/LogFileManager/index.ts @@ -291,11 +291,16 @@ class LogFileManager { * * @param queryArgs * @param queryArgs.queryString - * @param queryArgs.isRegex - * @param queryArgs.isCaseSensitive + * @param queryArgs.queryIsRegex + * @param queryArgs.queryIsCaseSensitive * @throws {SyntaxError} if the query regex string is invalid. */ - startQuery ({queryString, isRegex, isCaseSensitive}: QueryArgs): void { + startQuery ({queryString, queryIsRegex, queryIsCaseSensitive}: QueryArgs): void { + // If the query string is empty, or there are no logs, return + if ("" === queryString || 0 === this.#numEvents) { + return; + } + this.#queryId++; this.#queryCount = 0; @@ -303,16 +308,11 @@ class LogFileManager { // because there could be results sent by previous task before `startQuery()` runs. this.#onQueryResults(0, new Map()); - // If the query string is empty, or there are no logs, return - if ("" === queryString || 0 === this.#numEvents) { - return; - } - // Construct query RegExp - const regexPattern = isRegex ? + const regexPattern = queryIsRegex ? queryString : queryString.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); - const regexFlags = isCaseSensitive ? + const regexFlags = queryIsCaseSensitive ? "" : "i"; diff --git a/src/typings/query.ts b/src/typings/query.ts index f252be43..0a874e61 100644 --- a/src/typings/query.ts +++ b/src/typings/query.ts @@ -1,7 +1,7 @@ interface QueryArgs { queryString: string; - isCaseSensitive: boolean; - isRegex: boolean; + queryIsCaseSensitive: boolean; + queryIsRegex: boolean; } type TextRange = [number, number]; diff --git a/src/typings/url.ts b/src/typings/url.ts index dbbd9d34..359ec90c 100644 --- a/src/typings/url.ts +++ b/src/typings/url.ts @@ -7,14 +7,21 @@ enum SEARCH_PARAM_NAMES { enum HASH_PARAM_NAMES { LOG_EVENT_NUM = "logEventNum", + QUERY_IS_CASE_SENSITIVE = "queryIsCaseSensitive", + QUERY_IS_REGEX = "queryIsRegex", + QUERY_STRING = "queryString", } interface UrlSearchParams { [SEARCH_PARAM_NAMES.FILE_PATH]: string; + } interface UrlHashParams { - logEventNum: number; + [HASH_PARAM_NAMES.LOG_EVENT_NUM]: number; + [HASH_PARAM_NAMES.QUERY_IS_CASE_SENSITIVE]: boolean; + [HASH_PARAM_NAMES.QUERY_IS_REGEX]: boolean; + [HASH_PARAM_NAMES.QUERY_STRING]: string; } type UrlSearchParamUpdatesType = { diff --git a/src/typings/worker.ts b/src/typings/worker.ts index c36370ca..e4c8840f 100644 --- a/src/typings/worker.ts +++ b/src/typings/worker.ts @@ -112,8 +112,8 @@ type WorkerReqMap = { }; [WORKER_REQ_CODE.START_QUERY]: { queryString: string; - isRegex: boolean; - isCaseSensitive: boolean; + queryIsCaseSensitive: boolean; + queryIsRegex: boolean; }; };