From be6e9e003e1b605ffbbbf0d1319f460b4cb13636 Mon Sep 17 00:00:00 2001 From: zzxthehappiest Date: Wed, 21 May 2025 21:23:46 +0800 Subject: [PATCH 01/25] Migrate isPrettified --- package-lock.json | 2 +- src/components/AppController.tsx | 5 +++-- src/components/StatusBar/index.tsx | 6 +++++- src/contexts/UrlContextProvider.tsx | 15 +++++++++++---- src/stores/viewStore.ts | 4 ++-- 5 files changed, 22 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index b3bd3d3de..0ba7cb4ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,7 +30,7 @@ "@types/react": "^19.0.7", "@types/react-dom": "^19.0.3", "@vitejs/plugin-react-swc": "^3.9.0", - "eslint-config-yscope": "*", + "eslint-config-yscope": "latest", "globals": "^15.14.0", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", diff --git a/src/components/AppController.tsx b/src/components/AppController.tsx index 8294f5549..b801ea731 100644 --- a/src/components/AppController.tsx +++ b/src/components/AppController.tsx @@ -81,11 +81,12 @@ interface AppControllerProps { */ const AppController = ({children}: AppControllerProps) => { const {postPopUp} = useContext(NotificationContext); - const {filePath, isPrettified, logEventNum} = useContext(UrlContext); + const {filePath, logEventNum} = useContext(UrlContext); // States const beginLineNumToLogEventNum = useViewStore((state) => state.beginLineNumToLogEventNum); - const setIsPrettified = useViewStore((state) => state.updateIsPrettified); + const isPrettified = useViewStore((state) => state.isPrettified); + const setIsPrettified = useViewStore((state) => state.setIsPrettified); const loadFile = useLogFileStore((state) => state.loadFile); const {logFileManagerProxy} = useLogFileManagerStore.getState(); const numEvents = useLogFileStore((state) => state.numEvents); diff --git a/src/components/StatusBar/index.tsx b/src/components/StatusBar/index.tsx index 37864985c..e63340a53 100644 --- a/src/components/StatusBar/index.tsx +++ b/src/components/StatusBar/index.tsx @@ -25,6 +25,7 @@ import LogLevelSelect from "./LogLevelSelect"; import StatusBarToggleButton from "./StatusBarToggleButton"; import "./index.css"; +import useViewStore from "../../stores/viewStore.ts"; /** @@ -42,7 +43,9 @@ const handleCopyLinkButtonClick = () => { const StatusBar = () => { const numEvents = useLogFileStore((state) => state.numEvents); const uiState = useUiStore((state) => state.uiState); - const {isPrettified, logEventNum} = useContext(UrlContext); + const {logEventNum} = useContext(UrlContext); + const isPrettified = useViewStore((state) => state.isPrettified); + const setIsPrettified = useViewStore((state) => state.setIsPrettified); const handleStatusButtonClick = (ev: React.MouseEvent) => { const {actionName} = ev.currentTarget.dataset; @@ -52,6 +55,7 @@ const StatusBar = () => { updateWindowUrlHashParams({ [HASH_PARAM_NAMES.IS_PRETTIFIED]: !isPrettified, }); + setIsPrettified(!isPrettified); break; default: console.error(`Unexpected action: ${actionName}`); diff --git a/src/contexts/UrlContextProvider.tsx b/src/contexts/UrlContextProvider.tsx index 39c9ff8ec..2a55ff36d 100644 --- a/src/contexts/UrlContextProvider.tsx +++ b/src/contexts/UrlContextProvider.tsx @@ -15,6 +15,7 @@ import { UrlSearchParamUpdatesType, } from "../typings/url"; import {getAbsoluteUrl} from "../utils/url"; +import useViewStore from "../stores/viewStore.ts"; const UrlContext = createContext ({} as UrlParamsType); @@ -218,10 +219,10 @@ const getWindowUrlHashParams = () => { parsed; } - const isPrettified = hashParams.get(HASH_PARAM_NAMES.IS_PRETTIFIED); - if (null !== isPrettified) { - urlHashParams[HASH_PARAM_NAMES.IS_PRETTIFIED] = "true" === isPrettified; - } + // const isPrettified = hashParams.get(HASH_PARAM_NAMES.IS_PRETTIFIED); + // if (null !== isPrettified) { + // urlHashParams[HASH_PARAM_NAMES.IS_PRETTIFIED] = "true" === isPrettified; + // } return urlHashParams; }; @@ -247,6 +248,7 @@ const UrlContextProvider = ({children}: UrlContextProviderProps) => { ...searchParams, ...getWindowUrlHashParams(), }); + const setIsPrettified = useViewStore((state) => state.setIsPrettified); useEffect(() => { const handleHashChange = () => { @@ -256,6 +258,11 @@ const UrlContextProvider = ({children}: UrlContextProviderProps) => { ...searchParams, ...getWindowUrlHashParams(), }); + const hashParams = new URLSearchParams(window.location.hash.substring(1)); + const isPrettified = hashParams.get(HASH_PARAM_NAMES.IS_PRETTIFIED); + if (null !== isPrettified) { + setIsPrettified("true" === isPrettified); + } }; window.addEventListener("hashchange", handleHashChange); diff --git a/src/stores/viewStore.ts b/src/stores/viewStore.ts index d8934cf85..1896e15b2 100644 --- a/src/stores/viewStore.ts +++ b/src/stores/viewStore.ts @@ -43,7 +43,7 @@ interface ViewStoreActions { filterLogs: (filter: LogLevelFilter) => void; loadPageByAction: (navAction: NavigationAction) => void; - updateIsPrettified: (newIsPrettified: boolean) => void; + setIsPrettified: (newIsPrettified: boolean) => void; updatePageData: (pageData: PageData) => void; } @@ -200,7 +200,7 @@ const useViewStore = create((set, get) => ({ setPageNum: (newPageNum) => { set({pageNum: newPageNum}); }, - updateIsPrettified: (newIsPrettified: boolean) => { + setIsPrettified: (newIsPrettified: boolean) => { const {updatePageData} = get(); const {logEventNum, postPopUp} = useContextStore.getState(); const {logFileManagerProxy} = useLogFileManagerStore.getState(); From 0ae420cddac16e48a2ce1b974ee943163f8ba6d0 Mon Sep 17 00:00:00 2001 From: zzxthehappiest Date: Thu, 22 May 2025 19:31:12 +0800 Subject: [PATCH 02/25] Add logEventNum to viewStore and remove from Url --- src/components/AppController.tsx | 10 +++------- src/components/StatusBar/index.tsx | 2 +- src/contexts/UrlContextProvider.tsx | 27 +++++++++++++++++---------- src/stores/viewStore.ts | 10 ++++++++-- 4 files changed, 29 insertions(+), 20 deletions(-) diff --git a/src/components/AppController.tsx b/src/components/AppController.tsx index b801ea731..cac2f3da6 100644 --- a/src/components/AppController.tsx +++ b/src/components/AppController.tsx @@ -81,16 +81,16 @@ interface AppControllerProps { */ const AppController = ({children}: AppControllerProps) => { const {postPopUp} = useContext(NotificationContext); - const {filePath, logEventNum} = useContext(UrlContext); + const {filePath} = useContext(UrlContext); // States const beginLineNumToLogEventNum = useViewStore((state) => state.beginLineNumToLogEventNum); + const isPrettified = useViewStore((state) => state.isPrettified); - const setIsPrettified = useViewStore((state) => state.setIsPrettified); const loadFile = useLogFileStore((state) => state.loadFile); const {logFileManagerProxy} = useLogFileManagerStore.getState(); const numEvents = useLogFileStore((state) => state.numEvents); - const setLogEventNum = useContextStore((state) => state.setLogEventNum); + const logEventNum = useViewStore((state) => state.logEventNum); const setUiState = useUiStore((state) => state.setUiState); const setPostPopUp = useContextStore((state) => state.setPostPopUp); @@ -102,20 +102,16 @@ const AppController = ({children}: AppControllerProps) => { useEffect(() => { if (null !== logEventNum) { logEventNumRef.current = logEventNum; - setLogEventNum(logEventNum); } }, [ logEventNum, - setLogEventNum, ]); // Synchronize `isPrettifiedRef` with `isPrettified`. useEffect(() => { isPrettifiedRef.current = isPrettified ?? false; - setIsPrettified(isPrettifiedRef.current); }, [ isPrettified, - setIsPrettified, ]); // On `logEventNum` update, clamp it then switch page if necessary or simply update the URL. diff --git a/src/components/StatusBar/index.tsx b/src/components/StatusBar/index.tsx index e63340a53..fcb00ec56 100644 --- a/src/components/StatusBar/index.tsx +++ b/src/components/StatusBar/index.tsx @@ -45,7 +45,7 @@ const StatusBar = () => { const uiState = useUiStore((state) => state.uiState); const {logEventNum} = useContext(UrlContext); const isPrettified = useViewStore((state) => state.isPrettified); - const setIsPrettified = useViewStore((state) => state.setIsPrettified); + const setIsPrettified = useViewStore((state) => state.updateIsPrettified); const handleStatusButtonClick = (ev: React.MouseEvent) => { const {actionName} = ev.currentTarget.dataset; diff --git a/src/contexts/UrlContextProvider.tsx b/src/contexts/UrlContextProvider.tsx index 2a55ff36d..830fed43c 100644 --- a/src/contexts/UrlContextProvider.tsx +++ b/src/contexts/UrlContextProvider.tsx @@ -209,15 +209,15 @@ const getWindowUrlSearchParams = () => { const getWindowUrlHashParams = () => { const urlHashParams: NullableProperties = structuredClone(URL_HASH_PARAMS_DEFAULT); - const hashParams = new URLSearchParams(window.location.hash.substring(1)); - - const logEventNum = hashParams.get(HASH_PARAM_NAMES.LOG_EVENT_NUM); - if (null !== logEventNum) { - const parsed = Number(logEventNum); - urlHashParams[HASH_PARAM_NAMES.LOG_EVENT_NUM] = Number.isNaN(parsed) ? - null : - parsed; - } + // const hashParams = new URLSearchParams(window.location.hash.substring(1)); + + // const logEventNum = hashParams.get(HASH_PARAM_NAMES.LOG_EVENT_NUM); + // if (null !== logEventNum) { + // const parsed = Number(logEventNum); + // urlHashParams[HASH_PARAM_NAMES.LOG_EVENT_NUM] = Number.isNaN(parsed) ? + // null : + // parsed; + // } // const isPrettified = hashParams.get(HASH_PARAM_NAMES.IS_PRETTIFIED); // if (null !== isPrettified) { @@ -248,7 +248,8 @@ const UrlContextProvider = ({children}: UrlContextProviderProps) => { ...searchParams, ...getWindowUrlHashParams(), }); - const setIsPrettified = useViewStore((state) => state.setIsPrettified); + const setIsPrettified = useViewStore((state) => state.updateIsPrettified); + const setLogEventNum = useViewStore((state) => state.setLogEventNum); useEffect(() => { const handleHashChange = () => { @@ -259,6 +260,12 @@ const UrlContextProvider = ({children}: UrlContextProviderProps) => { ...getWindowUrlHashParams(), }); const hashParams = new URLSearchParams(window.location.hash.substring(1)); + const logEventNum = hashParams.get(HASH_PARAM_NAMES.LOG_EVENT_NUM); + if (null !== logEventNum) { + const parsed = Number(logEventNum); + setLogEventNum(Number.isNaN(parsed) ? 0 : parsed); + } + const isPrettified = hashParams.get(HASH_PARAM_NAMES.IS_PRETTIFIED); if (null !== isPrettified) { setIsPrettified("true" === isPrettified); diff --git a/src/stores/viewStore.ts b/src/stores/viewStore.ts index 1896e15b2..3c6e0b0df 100644 --- a/src/stores/viewStore.ts +++ b/src/stores/viewStore.ts @@ -31,6 +31,7 @@ interface ViewStoreValues { beginLineNumToLogEventNum: BeginLineNumToLogEventNumMap; isPrettified: boolean; logData: string; + logEventNum: number; numPages: number; pageNum: number; } @@ -38,12 +39,13 @@ interface ViewStoreValues { interface ViewStoreActions { setBeginLineNumToLogEventNum: (newMap: BeginLineNumToLogEventNumMap) => void; setLogData: (newLogData: string) => void; + setLogEventNum: (newLogEventNum: number) => void; setNumPages: (newNumPages: number) => void; setPageNum: (newPageNum: number) => void; filterLogs: (filter: LogLevelFilter) => void; loadPageByAction: (navAction: NavigationAction) => void; - setIsPrettified: (newIsPrettified: boolean) => void; + updateIsPrettified: (newIsPrettified: boolean) => void; updatePageData: (pageData: PageData) => void; } @@ -53,6 +55,7 @@ const VIEW_STORE_DEFAULT: ViewStoreValues = { beginLineNumToLogEventNum: new Map(), isPrettified: false, logData: "No file is open.", + logEventNum: 0, numPages: 0, pageNum: 0, }; @@ -194,13 +197,16 @@ const useViewStore = create((set, get) => ({ setLogData: (newLogData) => { set({logData: newLogData}); }, + setLogEventNum: (newLogEventNum) => { + set({logEventNum: newLogEventNum}); + }, setNumPages: (newNumPages) => { set({numPages: newNumPages}); }, setPageNum: (newPageNum) => { set({pageNum: newPageNum}); }, - setIsPrettified: (newIsPrettified: boolean) => { + updateIsPrettified: (newIsPrettified: boolean) => { const {updatePageData} = get(); const {logEventNum, postPopUp} = useContextStore.getState(); const {logFileManagerProxy} = useLogFileManagerStore.getState(); From cd36fb8d87f7361808e981c06ae1d3f74f75ea2b Mon Sep 17 00:00:00 2001 From: zzxthehappiest Date: Thu, 22 May 2025 20:02:09 +0800 Subject: [PATCH 03/25] Add fileSrc to logFileStore and remove from UrlContext --- src/components/AppController.tsx | 26 +++++++------- src/components/Editor/index.tsx | 5 ++- src/components/StatusBar/index.tsx | 5 +-- src/contexts/UrlContextProvider.tsx | 53 ++++++++++++++++++++--------- src/stores/logFileStore.ts | 4 +++ 5 files changed, 55 insertions(+), 38 deletions(-) diff --git a/src/components/AppController.tsx b/src/components/AppController.tsx index cac2f3da6..c2572690e 100644 --- a/src/components/AppController.tsx +++ b/src/components/AppController.tsx @@ -9,7 +9,6 @@ import { updateWindowUrlHashParams, URL_HASH_PARAMS_DEFAULT, URL_SEARCH_PARAMS_DEFAULT, - UrlContext, } from "../contexts/UrlContextProvider"; import useContextStore from "../stores/contextStore"; import useLogFileManagerStore from "../stores/logFileManagerProxyStore"; @@ -81,11 +80,10 @@ interface AppControllerProps { */ const AppController = ({children}: AppControllerProps) => { const {postPopUp} = useContext(NotificationContext); - const {filePath} = useContext(UrlContext); // States const beginLineNumToLogEventNum = useViewStore((state) => state.beginLineNumToLogEventNum); - + const fileSrc = useLogFileStore((state) => state.fileSrc); const isPrettified = useViewStore((state) => state.isPrettified); const loadFile = useLogFileStore((state) => state.loadFile); const {logFileManagerProxy} = useLogFileManagerStore.getState(); @@ -98,6 +96,13 @@ const AppController = ({children}: AppControllerProps) => { const isPrettifiedRef = useRef(isPrettified ?? false); const logEventNumRef = useRef(logEventNum); + // Synchronize `isPrettifiedRef` with `isPrettified`. + useEffect(() => { + isPrettifiedRef.current = isPrettified ?? false; + }, [ + isPrettified, + ]); + // Synchronize `logEventNumRef` with `logEventNum`. useEffect(() => { if (null !== logEventNum) { @@ -107,13 +112,6 @@ const AppController = ({children}: AppControllerProps) => { logEventNum, ]); - // Synchronize `isPrettifiedRef` with `isPrettified`. - useEffect(() => { - isPrettifiedRef.current = isPrettified ?? false; - }, [ - isPrettified, - ]); - // On `logEventNum` update, clamp it then switch page if necessary or simply update the URL. useEffect(() => { if (0 === numEvents || URL_HASH_PARAMS_DEFAULT.logEventNum === logEventNum) { @@ -157,9 +155,9 @@ const AppController = ({children}: AppControllerProps) => { postPopUp, ]); - // On `filePath` update, load file. + // On `fileSrc` update, load file. useEffect(() => { - if (URL_SEARCH_PARAMS_DEFAULT.filePath === filePath) { + if (URL_SEARCH_PARAMS_DEFAULT.filePath === fileSrc) { return; } @@ -170,9 +168,9 @@ const AppController = ({children}: AppControllerProps) => { args: {eventNum: logEventNumRef.current}, }; } - loadFile(filePath, cursor); + loadFile(fileSrc, cursor); }, [ - filePath, + fileSrc, loadFile, ]); diff --git a/src/components/Editor/index.tsx b/src/components/Editor/index.tsx index edb5acfd0..5ee3e296c 100644 --- a/src/components/Editor/index.tsx +++ b/src/components/Editor/index.tsx @@ -2,7 +2,6 @@ /* eslint max-lines-per-function: ["error", 170] */ import { useCallback, - useContext, useEffect, useRef, useState, @@ -13,7 +12,6 @@ import * as monaco from "monaco-editor/esm/vs/editor/editor.api.js"; import { updateWindowUrlHashParams, - UrlContext, } from "../../contexts/UrlContextProvider"; import useViewStore from "../../stores/viewStore"; import {Nullable} from "../../typings/common"; @@ -138,9 +136,10 @@ const Editor = () => { const {mode, systemMode} = useColorScheme(); const beginLineNumToLogEventNum = useViewStore((state) => state.beginLineNumToLogEventNum); + const isPrettified = useViewStore((state) => state.isPrettified); const logData = useViewStore((state) => state.logData); + const logEventNum = useViewStore((state) => state.logEventNum); const loadPageByAction = useViewStore((state) => state.loadPageByAction); - const {isPrettified, logEventNum} = useContext(UrlContext); const [lineNum, setLineNum] = useState(1); const beginLineNumToLogEventNumRef = useRef( diff --git a/src/components/StatusBar/index.tsx b/src/components/StatusBar/index.tsx index fcb00ec56..61fc748d9 100644 --- a/src/components/StatusBar/index.tsx +++ b/src/components/StatusBar/index.tsx @@ -1,5 +1,3 @@ -import React, {useContext} from "react"; - import { Button, Sheet, @@ -13,7 +11,6 @@ import AutoFixOffRoundedIcon from "@mui/icons-material/AutoFixOffRounded"; import { copyPermalinkToClipboard, updateWindowUrlHashParams, - UrlContext, } from "../../contexts/UrlContextProvider"; import useLogFileStore from "../../stores/logFileStore"; import useUiStore from "../../stores/uiStore"; @@ -43,8 +40,8 @@ const handleCopyLinkButtonClick = () => { const StatusBar = () => { const numEvents = useLogFileStore((state) => state.numEvents); const uiState = useUiStore((state) => state.uiState); - const {logEventNum} = useContext(UrlContext); const isPrettified = useViewStore((state) => state.isPrettified); + const logEventNum = useViewStore((state) => state.logEventNum); const setIsPrettified = useViewStore((state) => state.updateIsPrettified); const handleStatusButtonClick = (ev: React.MouseEvent) => { diff --git a/src/contexts/UrlContextProvider.tsx b/src/contexts/UrlContextProvider.tsx index 830fed43c..3addce813 100644 --- a/src/contexts/UrlContextProvider.tsx +++ b/src/contexts/UrlContextProvider.tsx @@ -16,6 +16,7 @@ import { } from "../typings/url"; import {getAbsoluteUrl} from "../utils/url"; import useViewStore from "../stores/viewStore.ts"; +import useLogFileStore from "../stores/logFileStore.ts"; const UrlContext = createContext ({} as UrlParamsType); @@ -181,22 +182,22 @@ const getWindowUrlSearchParams = () => { const searchParams : NullableProperties = structuredClone( URL_SEARCH_PARAMS_DEFAULT ); - const urlSearchParams = new URLSearchParams(window.location.search.substring(1)); - - 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`. - const [, filePath] = window.location.search.split("filePath="); - if ("undefined" !== typeof filePath && 0 !== filePath.length) { - let resolvedFilePath = filePath; - try { - resolvedFilePath = getAbsoluteUrl(filePath); - } catch (e) { - console.error("Unable to get absolute URL from filePath:", e); - } - searchParams[SEARCH_PARAM_NAMES.FILE_PATH] = resolvedFilePath; - } - } + // const urlSearchParams = new URLSearchParams(window.location.search.substring(1)); + // + // 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`. + // const [, filePath] = window.location.search.split("filePath="); + // if ("undefined" !== typeof filePath && 0 !== filePath.length) { + // let resolvedFilePath = filePath; + // try { + // resolvedFilePath = getAbsoluteUrl(filePath); + // } catch (e) { + // console.error("Unable to get absolute URL from filePath:", e); + // } + // searchParams[SEARCH_PARAM_NAMES.FILE_PATH] = resolvedFilePath; + // } + // } return searchParams; }; @@ -248,6 +249,7 @@ const UrlContextProvider = ({children}: UrlContextProviderProps) => { ...searchParams, ...getWindowUrlHashParams(), }); + const setFileSrc = useLogFileStore((state) => state.setFileSrc); const setIsPrettified = useViewStore((state) => state.updateIsPrettified); const setLogEventNum = useViewStore((state) => state.setLogEventNum); @@ -256,7 +258,7 @@ const UrlContextProvider = ({children}: UrlContextProviderProps) => { setUrlParams({ ...URL_SEARCH_PARAMS_DEFAULT, ...URL_HASH_PARAMS_DEFAULT, - ...searchParams, + ...getWindowUrlSearchParams(), ...getWindowUrlHashParams(), }); const hashParams = new URLSearchParams(window.location.hash.substring(1)); @@ -270,6 +272,23 @@ const UrlContextProvider = ({children}: UrlContextProviderProps) => { if (null !== isPrettified) { setIsPrettified("true" === isPrettified); } + + const searchParams = new URLSearchParams(window.location.search.substring(1)); + + if (searchParams.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`. + const [, filePath] = window.location.search.split("filePath="); + if ("undefined" !== typeof filePath && 0 !== filePath.length) { + let resolvedFilePath = filePath; + try { + resolvedFilePath = getAbsoluteUrl(filePath); + } catch (e) { + console.error("Unable to get absolute URL from filePath:", e); + } + setFileSrc(resolvedFilePath) + } + } }; window.addEventListener("hashchange", handleHashChange); diff --git a/src/stores/logFileStore.ts b/src/stores/logFileStore.ts index 706dfdeea..9cfa0e036 100644 --- a/src/stores/logFileStore.ts +++ b/src/stores/logFileStore.ts @@ -39,6 +39,7 @@ interface LogFileValues { interface LogFileActions { setFileName: (newFileName: string) => void; + setFileSrc: (newFileSrc: Nullable) => void; setNumEvents: (newNumEvents: number) => void; setOnDiskFileSizeInBytes: (newOnDiskFileSizeInBytes: number) => void; @@ -155,6 +156,9 @@ const useLogFileStore = create((set, get) => ({ setFileName: (newFileName) => { set({fileName: newFileName}); }, + setFileSrc: (newFileSrc) => { + set({fileSrc: newFileSrc}); + }, setNumEvents: (newNumEvents) => { set({numEvents: newNumEvents}); }, From 1f0aeda01b89e8701d4e6b5a6b0115a35a2331a9 Mon Sep 17 00:00:00 2001 From: zzxthehappiest Date: Thu, 22 May 2025 21:32:38 +0800 Subject: [PATCH 04/25] Remove UrlContextProvider --- src/App.tsx | 10 +- src/components/AppController.tsx | 51 ++- .../SidebarTabs/SearchTabPanel/Result.tsx | 3 +- src/components/Editor/index.tsx | 4 +- src/components/StatusBar/index.tsx | 5 +- src/contexts/UrlContextProvider.tsx | 316 ------------------ src/stores/logFileStore.ts | 2 +- src/stores/viewStore.ts | 2 +- src/utils/url.ts | 224 +++++++++++++ 9 files changed, 277 insertions(+), 340 deletions(-) delete mode 100644 src/contexts/UrlContextProvider.tsx diff --git a/src/App.tsx b/src/App.tsx index 4ac813e53..1ad884385 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,8 +1,6 @@ import AppController from "./components/AppController"; import Layout from "./components/Layout"; import NotificationContextProvider from "./contexts/NotificationContextProvider"; -import UrlContextProvider from "./contexts/UrlContextProvider"; - /** * Renders the main application. @@ -12,11 +10,9 @@ import UrlContextProvider from "./contexts/UrlContextProvider"; const App = () => { return ( - - - - - + + + ); }; diff --git a/src/components/AppController.tsx b/src/components/AppController.tsx index c2572690e..35ab0d4b6 100644 --- a/src/components/AppController.tsx +++ b/src/components/AppController.tsx @@ -5,11 +5,6 @@ import React, { } from "react"; import {NotificationContext} from "../contexts/NotificationContextProvider"; -import { - updateWindowUrlHashParams, - URL_HASH_PARAMS_DEFAULT, - URL_SEARCH_PARAMS_DEFAULT, -} from "../contexts/UrlContextProvider"; import useContextStore from "../stores/contextStore"; import useLogFileManagerStore from "../stores/logFileManagerProxyStore"; import useLogFileStore from "../stores/logFileStore"; @@ -27,6 +22,11 @@ import { isWithinBounds, } from "../utils/data"; import {clamp} from "../utils/math"; +import { + getWindowUrlHashParams, + getWindowUrlSearchParams, + updateWindowUrlHashParams, URL_HASH_PARAMS_DEFAULT, URL_SEARCH_PARAMS_DEFAULT +} from "../utils/url.ts"; /** @@ -83,12 +83,20 @@ const AppController = ({children}: AppControllerProps) => { // States const beginLineNumToLogEventNum = useViewStore((state) => state.beginLineNumToLogEventNum); + const fileSrc = useLogFileStore((state) => state.fileSrc); - const isPrettified = useViewStore((state) => state.isPrettified); const loadFile = useLogFileStore((state) => state.loadFile); + const setFileSrc = useLogFileStore((state) => state.setFileSrc); + + const isPrettified = useViewStore((state) => state.isPrettified); + const updateIsPrettified = useViewStore((state) => state.updateIsPrettified); + const {logFileManagerProxy} = useLogFileManagerStore.getState(); const numEvents = useLogFileStore((state) => state.numEvents); + const logEventNum = useViewStore((state) => state.logEventNum); + const setLogEventNum = useViewStore((state) => state.setLogEventNum); + const setUiState = useUiStore((state) => state.setUiState); const setPostPopUp = useContextStore((state) => state.setPostPopUp); @@ -96,6 +104,37 @@ const AppController = ({children}: AppControllerProps) => { const isPrettifiedRef = useRef(isPrettified ?? false); const logEventNumRef = useRef(logEventNum); + useEffect(() => { + const handleHashChange = () => { + const hashParams = getWindowUrlHashParams(); + + if (null !== hashParams.logEventNum) { + setLogEventNum(hashParams.logEventNum); + } + + if (null !== hashParams.isPrettified) { + updateIsPrettified(hashParams.isPrettified); + } + + // It is weird that updating search params when hash params changed, even there + // might be hidden condition that search params always change together with hash + // params. + const searchParams = getWindowUrlSearchParams(); + + if (null !== searchParams.filePath) { + setFileSrc(searchParams.filePath); + } + }; + + handleHashChange(); + + window.addEventListener("hashchange", handleHashChange); + + return () => { + window.removeEventListener("hashchange", handleHashChange); + }; + }, []); + // Synchronize `isPrettifiedRef` with `isPrettified`. useEffect(() => { isPrettifiedRef.current = isPrettified ?? false; diff --git a/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/Result.tsx b/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/Result.tsx index d38316a2f..7475e4e88 100644 --- a/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/Result.tsx +++ b/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/Result.tsx @@ -3,9 +3,8 @@ import { Typography, } from "@mui/joy"; -import {updateWindowUrlHashParams} from "../../../../../contexts/UrlContextProvider"; - import "./Result.css"; +import { updateWindowUrlHashParams } from "../../../../../utils/url.ts"; interface ResultProps { diff --git a/src/components/Editor/index.tsx b/src/components/Editor/index.tsx index 5ee3e296c..593e12483 100644 --- a/src/components/Editor/index.tsx +++ b/src/components/Editor/index.tsx @@ -10,9 +10,6 @@ import { import {useColorScheme} from "@mui/joy"; import * as monaco from "monaco-editor/esm/vs/editor/editor.api.js"; -import { - updateWindowUrlHashParams, -} from "../../contexts/UrlContextProvider"; import useViewStore from "../../stores/viewStore"; import {Nullable} from "../../typings/common"; import { @@ -38,6 +35,7 @@ import MonacoInstance from "./MonacoInstance"; import {goToPositionAndCenter} from "./MonacoInstance/utils"; import "./index.css"; +import { updateWindowUrlHashParams } from "../../utils/url.ts"; /** diff --git a/src/components/StatusBar/index.tsx b/src/components/StatusBar/index.tsx index 61fc748d9..9f77b24f2 100644 --- a/src/components/StatusBar/index.tsx +++ b/src/components/StatusBar/index.tsx @@ -8,10 +8,6 @@ import { import AutoFixHighRoundedIcon from "@mui/icons-material/AutoFixHighRounded"; import AutoFixOffRoundedIcon from "@mui/icons-material/AutoFixOffRounded"; -import { - copyPermalinkToClipboard, - updateWindowUrlHashParams, -} from "../../contexts/UrlContextProvider"; import useLogFileStore from "../../stores/logFileStore"; import useUiStore from "../../stores/uiStore"; import {UI_ELEMENT} from "../../typings/states"; @@ -23,6 +19,7 @@ import StatusBarToggleButton from "./StatusBarToggleButton"; import "./index.css"; import useViewStore from "../../stores/viewStore.ts"; +import { copyPermalinkToClipboard, updateWindowUrlHashParams } from "../../utils/url.ts"; /** diff --git a/src/contexts/UrlContextProvider.tsx b/src/contexts/UrlContextProvider.tsx deleted file mode 100644 index 3addce813..000000000 --- a/src/contexts/UrlContextProvider.tsx +++ /dev/null @@ -1,316 +0,0 @@ -import React, { - createContext, - useEffect, - useState, -} from "react"; - -import {NullableProperties} from "../typings/common"; -import { - HASH_PARAM_NAMES, - SEARCH_PARAM_NAMES, - UrlHashParams, - UrlHashParamUpdatesType, - UrlParamsType, - UrlSearchParams, - UrlSearchParamUpdatesType, -} from "../typings/url"; -import {getAbsoluteUrl} from "../utils/url"; -import useViewStore from "../stores/viewStore.ts"; -import useLogFileStore from "../stores/logFileStore.ts"; - - -const UrlContext = createContext ({} as UrlParamsType); - -/** - * Default values of the search parameters. - */ -const URL_SEARCH_PARAMS_DEFAULT = Object.freeze({ - [SEARCH_PARAM_NAMES.FILE_PATH]: null, -}); - -/** - * Default values of the hash parameters. - */ -const URL_HASH_PARAMS_DEFAULT = Object.freeze({ - [HASH_PARAM_NAMES.IS_PRETTIFIED]: false, - [HASH_PARAM_NAMES.LOG_EVENT_NUM]: null, -}); - -/** - * Regular expression pattern for identifying ambiguous characters in a URL. - */ -const AMBIGUOUS_URL_CHARS_REGEX = - new RegExp(`${encodeURIComponent("#")}|${encodeURIComponent("&")}`); - -/** - * Computes updated URL search parameters based on the provided key-value pairs. - * - * @param updates An object containing key-value pairs to update the search parameters. If a value - * is `null`, the corresponding kv-pair will be removed from the updated search parameters. - * @return The updated search parameters string. - */ -const getUpdatedSearchParams = (updates: UrlSearchParamUpdatesType) => { - const newSearchParams = new URLSearchParams(window.location.search.substring(1)); - const {filePath: newFilePath} = updates; - - for (const [key, value] of Object.entries(updates)) { - if (SEARCH_PARAM_NAMES.FILE_PATH as string === key) { - // Updates to `filePath` should be handled last. - continue; - } - if (null === value) { - newSearchParams.delete(key); - } else { - newSearchParams.set(key, String(value)); - } - } - - // `filePath` should always be the last search parameter so that: - // 1. Users can specify a remote filePath (a URL) that itself contains URL parameters without - // encoding them. E.g. "/?filePath=https://example.com/log/?p1=v1&p2=v2" - // 2. Users can easily modify it in the URL - // - // NOTE: We're relying on URLSearchParams.set() and URLSearchParams.toString() to store and - // serialize the parameters in the order that they were set. - const originalFilePath = newSearchParams.get(SEARCH_PARAM_NAMES.FILE_PATH); - newSearchParams.delete(SEARCH_PARAM_NAMES.FILE_PATH); - if ("undefined" === typeof newFilePath && null !== originalFilePath) { - // If no change in `filePath` is specified, put the original `filePath` back. - newSearchParams.set(SEARCH_PARAM_NAMES.FILE_PATH, originalFilePath); - } else if ("undefined" !== typeof newFilePath && null !== newFilePath) { - // If the new `filePath` has a non-null value, set the value. - newSearchParams.set(SEARCH_PARAM_NAMES.FILE_PATH, newFilePath); - } - - // If the stringified search params doesn't contain characters that would make the URL ambiguous - // to parse, URL-decode it so that the `filePath` remains human-readable. E.g. - // "filePath=https://example.com/log/?s1=1&s2=2#h1=0" would make the final URL ambiguous to - // parse since `filePath` itself contains URL parameters. - let searchString = newSearchParams.toString(); - if (false === AMBIGUOUS_URL_CHARS_REGEX.test(searchString)) { - searchString = decodeURIComponent(searchString); - } - - return searchString; -}; - -/** - * Computes updated URL hash parameters based on the provided key-value pairs. - * - * @param updates An object containing key-value pairs to update the hash parameters. If a key's - * value is `null`, the key will be removed from the updated hash parameters. - * @return The updated hash parameters string. - */ -const getUpdatedHashParams = (updates: UrlHashParamUpdatesType) => { - const newHashParams = new URLSearchParams(window.location.hash.substring(1)); - for (const [key, value] of Object.entries(updates)) { - if (null === value) { - newHashParams.delete(key); - } else { - newHashParams.set(key, String(value)); - } - } - - return newHashParams.toString(); -}; - -/** - * Updates search parameters in the current window's URL with the given key-value pairs. - * - * @param updates An object containing key-value pairs to update the search parameters. If a value - * is `null`, the corresponding kv-pair will be removed from the URL's search parameters. - */ -const updateWindowUrlSearchParams = (updates: UrlSearchParamUpdatesType) => { - const newUrl = new URL(window.location.href); - newUrl.search = getUpdatedSearchParams(updates); - window.history.pushState({}, "", newUrl); -}; - -/** - * Updates hash parameters in the current window's URL with the given key-value pairs. - * - * @param updates An object containing key-value pairs to update the hash parameters. If a value is - * `null`, the corresponding kv-pair will be removed from the URL's hash parameters. - */ -const updateWindowUrlHashParams = (updates: UrlHashParamUpdatesType) => { - const newHash = getUpdatedHashParams(updates); - const currHash = window.location.hash.substring(1); - if (newHash === currHash) { - return; - } - - const newUrl = new URL(window.location.href); - newUrl.hash = newHash; - window.history.pushState({}, "", newUrl); - - // `history.pushState` doesn't trigger a `hashchange`, so we need to dispatch one manually. - window.dispatchEvent(new HashChangeEvent("hashchange")); -}; - -/** - * Copies the current window's URL to the clipboard. If any `updates` parameters are specified, - * the copied URL will include these modifications, but the original window's URL will not be - * changed. - * - * @param searchParamUpdates An object containing key-value pairs to update the search parameters. - * If a value is `null`, the corresponding kv-pair will be removed from the URL's search parameters. - * @param hashParamsUpdates An object containing key-value pairs to update the hash parameters. - * If a value is `null`, the corresponding kv-pair will be removed from the URL's hash parameters. - */ -const copyPermalinkToClipboard = ( - searchParamUpdates: UrlSearchParamUpdatesType, - hashParamsUpdates: UrlHashParamUpdatesType, -) => { - const newUrl = new URL(window.location.href); - newUrl.search = getUpdatedSearchParams(searchParamUpdates); - newUrl.hash = getUpdatedHashParams(hashParamsUpdates); - navigator.clipboard.writeText(newUrl.toString()) - .then(() => { - console.log("URL copied to clipboard."); - }) - .catch((error: unknown) => { - console.error("Failed to copy URL to clipboard:", error); - }); -}; - -/** - * Retrieves all search parameters from the current window's URL. - * - * @return An object containing the search parameters. - */ -const getWindowUrlSearchParams = () => { - const searchParams : NullableProperties = structuredClone( - URL_SEARCH_PARAMS_DEFAULT - ); - // const urlSearchParams = new URLSearchParams(window.location.search.substring(1)); - // - // 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`. - // const [, filePath] = window.location.search.split("filePath="); - // if ("undefined" !== typeof filePath && 0 !== filePath.length) { - // let resolvedFilePath = filePath; - // try { - // resolvedFilePath = getAbsoluteUrl(filePath); - // } catch (e) { - // console.error("Unable to get absolute URL from filePath:", e); - // } - // searchParams[SEARCH_PARAM_NAMES.FILE_PATH] = resolvedFilePath; - // } - // } - - return searchParams; -}; - -/** - * Retrieves all hash parameters from the current window's URL. - * - * @return An object containing the hash parameters. - */ -const getWindowUrlHashParams = () => { - const urlHashParams: NullableProperties = - structuredClone(URL_HASH_PARAMS_DEFAULT); - // const hashParams = new URLSearchParams(window.location.hash.substring(1)); - - // const logEventNum = hashParams.get(HASH_PARAM_NAMES.LOG_EVENT_NUM); - // if (null !== logEventNum) { - // const parsed = Number(logEventNum); - // urlHashParams[HASH_PARAM_NAMES.LOG_EVENT_NUM] = Number.isNaN(parsed) ? - // null : - // parsed; - // } - - // const isPrettified = hashParams.get(HASH_PARAM_NAMES.IS_PRETTIFIED); - // if (null !== isPrettified) { - // urlHashParams[HASH_PARAM_NAMES.IS_PRETTIFIED] = "true" === isPrettified; - // } - - return urlHashParams; -}; - -const searchParams = getWindowUrlSearchParams(); - -interface UrlContextProviderProps { - children: React.ReactNode; -} - -/** - * Provides a context for managing URL search and hash parameters including utilities for setting - * each, and copying the current URL with these parameters to the clipboard. - * - * @param props - * @param props.children The child components that will have access to the context. - * @return - */ -const UrlContextProvider = ({children}: UrlContextProviderProps) => { - const [urlParams, setUrlParams] = useState({ - ...URL_SEARCH_PARAMS_DEFAULT, - ...URL_HASH_PARAMS_DEFAULT, - ...searchParams, - ...getWindowUrlHashParams(), - }); - const setFileSrc = useLogFileStore((state) => state.setFileSrc); - const setIsPrettified = useViewStore((state) => state.updateIsPrettified); - const setLogEventNum = useViewStore((state) => state.setLogEventNum); - - useEffect(() => { - const handleHashChange = () => { - setUrlParams({ - ...URL_SEARCH_PARAMS_DEFAULT, - ...URL_HASH_PARAMS_DEFAULT, - ...getWindowUrlSearchParams(), - ...getWindowUrlHashParams(), - }); - const hashParams = new URLSearchParams(window.location.hash.substring(1)); - const logEventNum = hashParams.get(HASH_PARAM_NAMES.LOG_EVENT_NUM); - if (null !== logEventNum) { - const parsed = Number(logEventNum); - setLogEventNum(Number.isNaN(parsed) ? 0 : parsed); - } - - const isPrettified = hashParams.get(HASH_PARAM_NAMES.IS_PRETTIFIED); - if (null !== isPrettified) { - setIsPrettified("true" === isPrettified); - } - - const searchParams = new URLSearchParams(window.location.search.substring(1)); - - if (searchParams.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`. - const [, filePath] = window.location.search.split("filePath="); - if ("undefined" !== typeof filePath && 0 !== filePath.length) { - let resolvedFilePath = filePath; - try { - resolvedFilePath = getAbsoluteUrl(filePath); - } catch (e) { - console.error("Unable to get absolute URL from filePath:", e); - } - setFileSrc(resolvedFilePath) - } - } - }; - - window.addEventListener("hashchange", handleHashChange); - - return () => { - window.removeEventListener("hashchange", handleHashChange); - }; - }, []); - - return ( - - {children} - - ); -}; - -export default UrlContextProvider; -export { - copyPermalinkToClipboard, - updateWindowUrlHashParams, - updateWindowUrlSearchParams, - URL_HASH_PARAMS_DEFAULT, - URL_SEARCH_PARAMS_DEFAULT, - UrlContext, -}; diff --git a/src/stores/logFileStore.ts b/src/stores/logFileStore.ts index 9cfa0e036..fd8d6a1f1 100644 --- a/src/stores/logFileStore.ts +++ b/src/stores/logFileStore.ts @@ -1,7 +1,6 @@ import * as Comlink from "comlink"; import {create} from "zustand"; -import {updateWindowUrlSearchParams} from "../contexts/UrlContextProvider"; import {FILE_TYPE} from "../services/LogFileManager"; import {Nullable} from "../typings/common"; import {CONFIG_KEY} from "../typings/config"; @@ -28,6 +27,7 @@ import useLogFileManagerProxyStore from "./logFileManagerProxyStore"; import useQueryStore from "./queryStore"; import useUiStore from "./uiStore"; import useViewStore from "./viewStore"; +import { updateWindowUrlSearchParams } from "../utils/url.ts"; interface LogFileValues { diff --git a/src/stores/viewStore.ts b/src/stores/viewStore.ts index 3c6e0b0df..d44d9c7d6 100644 --- a/src/stores/viewStore.ts +++ b/src/stores/viewStore.ts @@ -1,6 +1,5 @@ import {create} from "zustand"; -import {updateWindowUrlHashParams} from "../contexts/UrlContextProvider"; import {Nullable} from "../typings/common"; import { LOG_LEVEL, @@ -25,6 +24,7 @@ import useLogFileManagerStore from "./logFileManagerProxyStore"; import useLogFileStore from "./logFileStore"; import useQueryStore from "./queryStore"; import useUiStore from "./uiStore"; +import { updateWindowUrlHashParams } from "../utils/url.ts"; interface ViewStoreValues { diff --git a/src/utils/url.ts b/src/utils/url.ts index afd439632..da81b97f9 100644 --- a/src/utils/url.ts +++ b/src/utils/url.ts @@ -1,3 +1,58 @@ +import { + HASH_PARAM_NAMES, + SEARCH_PARAM_NAMES, UrlHashParams, + UrlHashParamUpdatesType, UrlSearchParams, + UrlSearchParamUpdatesType +} from "../typings/url.ts"; +import { NullableProperties } from "../typings/common.ts"; + +/** + * Default values of the search parameters. + */ +const URL_SEARCH_PARAMS_DEFAULT = Object.freeze({ + [SEARCH_PARAM_NAMES.FILE_PATH]: null, +}); + +/** + * Default values of the hash parameters. + */ +const URL_HASH_PARAMS_DEFAULT = Object.freeze({ + [HASH_PARAM_NAMES.IS_PRETTIFIED]: false, + [HASH_PARAM_NAMES.LOG_EVENT_NUM]: null, +}); + +/** + * Regular expression pattern for identifying ambiguous characters in a URL. + */ +const AMBIGUOUS_URL_CHARS_REGEX = + new RegExp(`${encodeURIComponent("#")}|${encodeURIComponent("&")}`); + +/** + * Copies the current window's URL to the clipboard. If any `updates` parameters are specified, + * the copied URL will include these modifications, but the original window's URL will not be + * changed. + * + * @param searchParamUpdates An object containing key-value pairs to update the search parameters. + * If a value is `null`, the corresponding kv-pair will be removed from the URL's search parameters. + * @param hashParamsUpdates An object containing key-value pairs to update the hash parameters. + * If a value is `null`, the corresponding kv-pair will be removed from the URL's hash parameters. + */ +const copyPermalinkToClipboard = ( + searchParamUpdates: UrlSearchParamUpdatesType, + hashParamsUpdates: UrlHashParamUpdatesType, +) => { + const newUrl = new URL(window.location.href); + newUrl.search = getUpdatedSearchParams(searchParamUpdates); + newUrl.hash = getUpdatedHashParams(hashParamsUpdates); + navigator.clipboard.writeText(newUrl.toString()) + .then(() => { + console.log("URL copied to clipboard."); + }) + .catch((error: unknown) => { + console.error("Failed to copy URL to clipboard:", error); + }); +}; + /** * Gets an absolute URL composed of a given path relative to the * window.location, if the given path is a relative reference; otherwise @@ -44,6 +99,135 @@ const getBasenameFromUrlOrDefault = ( return basename; }; +/** + * Computes updated URL search parameters based on the provided key-value pairs. + * + * @param updates An object containing key-value pairs to update the search parameters. If a value + * is `null`, the corresponding kv-pair will be removed from the updated search parameters. + * @return The updated search parameters string. + * @private + */ +const getUpdatedSearchParams = (updates: UrlSearchParamUpdatesType) => { + const newSearchParams = new URLSearchParams(window.location.search.substring(1)); + const {filePath: newFilePath} = updates; + + for (const [key, value] of Object.entries(updates)) { + if (SEARCH_PARAM_NAMES.FILE_PATH as string === key) { + // Updates to `filePath` should be handled last. + continue; + } + if (null === value) { + newSearchParams.delete(key); + } else { + newSearchParams.set(key, String(value)); + } + } + + // `filePath` should always be the last search parameter so that: + // 1. Users can specify a remote filePath (a URL) that itself contains URL parameters without + // encoding them. E.g. "/?filePath=https://example.com/log/?p1=v1&p2=v2" + // 2. Users can easily modify it in the URL + // + // NOTE: We're relying on URLSearchParams.set() and URLSearchParams.toString() to store and + // serialize the parameters in the order that they were set. + const originalFilePath = newSearchParams.get(SEARCH_PARAM_NAMES.FILE_PATH); + newSearchParams.delete(SEARCH_PARAM_NAMES.FILE_PATH); + if ("undefined" === typeof newFilePath && null !== originalFilePath) { + // If no change in `filePath` is specified, put the original `filePath` back. + newSearchParams.set(SEARCH_PARAM_NAMES.FILE_PATH, originalFilePath); + } else if ("undefined" !== typeof newFilePath && null !== newFilePath) { + // If the new `filePath` has a non-null value, set the value. + newSearchParams.set(SEARCH_PARAM_NAMES.FILE_PATH, newFilePath); + } + + // If the stringified search params doesn't contain characters that would make the URL ambiguous + // to parse, URL-decode it so that the `filePath` remains human-readable. E.g. + // "filePath=https://example.com/log/?s1=1&s2=2#h1=0" would make the final URL ambiguous to + // parse since `filePath` itself contains URL parameters. + let searchString = newSearchParams.toString(); + if (false === AMBIGUOUS_URL_CHARS_REGEX.test(searchString)) { + searchString = decodeURIComponent(searchString); + } + + return searchString; +}; + +/** + * Computes updated URL hash parameters based on the provided key-value pairs. + * + * @param updates An object containing key-value pairs to update the hash parameters. If a key's + * value is `null`, the key will be removed from the updated hash parameters. + * @return The updated hash parameters string. + * @private + */ +const getUpdatedHashParams = (updates: UrlHashParamUpdatesType) => { + const newHashParams = new URLSearchParams(window.location.hash.substring(1)); + for (const [key, value] of Object.entries(updates)) { + if (null === value) { + newHashParams.delete(key); + } else { + newHashParams.set(key, String(value)); + } + } + + return newHashParams.toString(); +}; + +/** + * Retrieves all hash parameters from the current window's URL. + * + * @return An object containing the hash parameters. + */ +const getWindowUrlHashParams = () => { + const urlHashParams: NullableProperties = + structuredClone(URL_HASH_PARAMS_DEFAULT); + const hashParams = new URLSearchParams(window.location.hash.substring(1)); + + const logEventNum = hashParams.get(HASH_PARAM_NAMES.LOG_EVENT_NUM); + if (null !== logEventNum) { + const parsed = Number(logEventNum); + urlHashParams[HASH_PARAM_NAMES.LOG_EVENT_NUM] = Number.isNaN(parsed) ? + null : + parsed; + } + + const isPrettified = hashParams.get(HASH_PARAM_NAMES.IS_PRETTIFIED); + if (null !== isPrettified) { + urlHashParams[HASH_PARAM_NAMES.IS_PRETTIFIED] = "true" === isPrettified; + } + + return urlHashParams; +}; + +/** + * Retrieves all search parameters from the current window's URL. + * + * @return An object containing the search parameters. + */ +const getWindowUrlSearchParams = () => { + const searchParams : NullableProperties = structuredClone( + URL_SEARCH_PARAMS_DEFAULT + ); + const urlSearchParams = new URLSearchParams(window.location.search.substring(1)); + + 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`. + const [, filePath] = window.location.search.split("filePath="); + if ("undefined" !== typeof filePath && 0 !== filePath.length) { + let resolvedFilePath = filePath; + try { + resolvedFilePath = getAbsoluteUrl(filePath); + } catch (e) { + console.error("Unable to get absolute URL from filePath:", e); + } + searchParams[SEARCH_PARAM_NAMES.FILE_PATH] = resolvedFilePath; + } + } + + return searchParams; +}; + /** * Opens a given URL in a new browser tab. * @@ -53,8 +237,48 @@ const openInNewTab = (url: string): void => { window.open(url, "_blank", "noopener"); }; +/** + * Updates hash parameters in the current window's URL with the given key-value pairs. + * + * @param updates An object containing key-value pairs to update the hash parameters. If a value is + * `null`, the corresponding kv-pair will be removed from the URL's hash parameters. + */ +const updateWindowUrlHashParams = (updates: UrlHashParamUpdatesType) => { + const newHash = getUpdatedHashParams(updates); + const currHash = window.location.hash.substring(1); + if (newHash === currHash) { + return; + } + + const newUrl = new URL(window.location.href); + newUrl.hash = newHash; + window.history.pushState({}, "", newUrl); + + // `history.pushState` doesn't trigger a `hashchange`, so we need to dispatch one manually. + window.dispatchEvent(new HashChangeEvent("hashchange")); +}; + +/** + * Updates search parameters in the current window's URL with the given key-value pairs. + * + * @param updates An object containing key-value pairs to update the search parameters. If a value + * is `null`, the corresponding kv-pair will be removed from the URL's search parameters. + */ +const updateWindowUrlSearchParams = (updates: UrlSearchParamUpdatesType) => { + const newUrl = new URL(window.location.href); + newUrl.search = getUpdatedSearchParams(updates); + window.history.pushState({}, "", newUrl); +}; + export { + URL_HASH_PARAMS_DEFAULT, + URL_SEARCH_PARAMS_DEFAULT, + copyPermalinkToClipboard, getAbsoluteUrl, getBasenameFromUrlOrDefault, + getWindowUrlHashParams, + getWindowUrlSearchParams, openInNewTab, + updateWindowUrlHashParams, + updateWindowUrlSearchParams, }; From 81585cbe90534d660d580be103bbd4033d10ba35 Mon Sep 17 00:00:00 2001 From: zzxthehappiest Date: Sat, 24 May 2025 14:29:10 +0800 Subject: [PATCH 05/25] Remove hashchange event when updateHashUrlParams --- src/components/AppController.tsx | 10 ++++---- .../SidebarTabs/SearchTabPanel/Result.tsx | 3 +++ src/components/Editor/index.tsx | 10 +++++++- src/stores/contextStore.ts | 6 ----- src/stores/viewStore.ts | 24 ++++++++++++------- src/utils/url.ts | 7 +++--- 6 files changed, 37 insertions(+), 23 deletions(-) diff --git a/src/components/AppController.tsx b/src/components/AppController.tsx index 35ab0d4b6..e84e3ad61 100644 --- a/src/components/AppController.tsx +++ b/src/components/AppController.tsx @@ -40,9 +40,9 @@ import { const updateUrlIfEventOnPage = ( logEventNum: number, logEventNumsOnPage: number[] -): boolean => { +): { isUpdated: boolean, nearestLogEventNum: number } => { if (false === isWithinBounds(logEventNumsOnPage, logEventNum)) { - return false; + return { isUpdated: false, nearestLogEventNum: 0 }; } const nearestIdx = findNearestLessThanOrEqualElement( @@ -64,7 +64,7 @@ const updateUrlIfEventOnPage = ( logEventNum: nearestLogEventNum, }); - return true; + return { isUpdated: true, nearestLogEventNum: nearestLogEventNum }; }; interface AppControllerProps { @@ -162,8 +162,10 @@ const AppController = ({children}: AppControllerProps) => { const clampedLogEventNum = clamp(logEventNum, 1, numEvents); - if (updateUrlIfEventOnPage(clampedLogEventNum, logEventNumsOnPage)) { + const {isUpdated, nearestLogEventNum} = updateUrlIfEventOnPage(clampedLogEventNum, logEventNumsOnPage); + if (isUpdated) { // No need to request a new page since the log event is on the current page. + setLogEventNum(nearestLogEventNum); return; } diff --git a/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/Result.tsx b/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/Result.tsx index 7475e4e88..3df24ee8e 100644 --- a/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/Result.tsx +++ b/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/Result.tsx @@ -5,6 +5,7 @@ import { import "./Result.css"; import { updateWindowUrlHashParams } from "../../../../../utils/url.ts"; +import useViewStore from "../../../../../stores/viewStore.ts"; interface ResultProps { @@ -35,8 +36,10 @@ const Result = ({logEventNum, message, matchRange}: ResultProps) => { message.slice(...matchRange), message.slice(matchRange[1]), ]; + const setLogEventNum = useViewStore((state) => state.setLogEventNum); const handleResultButtonClick = () => { updateWindowUrlHashParams({logEventNum}); + setLogEventNum(logEventNum); }; return ( diff --git a/src/components/Editor/index.tsx b/src/components/Editor/index.tsx index 593e12483..ee85ff811 100644 --- a/src/components/Editor/index.tsx +++ b/src/components/Editor/index.tsx @@ -135,8 +135,13 @@ const Editor = () => { const beginLineNumToLogEventNum = useViewStore((state) => state.beginLineNumToLogEventNum); const isPrettified = useViewStore((state) => state.isPrettified); + const updatePrettified = useViewStore((state) => state.updateIsPrettified) + const logData = useViewStore((state) => state.logData); + const logEventNum = useViewStore((state) => state.logEventNum); + const setLogEventNum = useViewStore((state) => state.setLogEventNum); + const loadPageByAction = useViewStore((state) => state.loadPageByAction); const [lineNum, setLineNum] = useState(1); @@ -174,9 +179,11 @@ const Editor = () => { handleCopyLogEventAction(editor, beginLineNumToLogEventNumRef.current); break; case ACTION_NAME.TOGGLE_PRETTIFY: + const newIsPrettified = !isPrettifiedRef.current; updateWindowUrlHashParams({ - [HASH_PARAM_NAMES.IS_PRETTIFIED]: !isPrettifiedRef.current, + [HASH_PARAM_NAMES.IS_PRETTIFIED]: newIsPrettified, }); + updatePrettified(newIsPrettified); break; case ACTION_NAME.TOGGLE_WORD_WRAP: handleToggleWordWrapAction(editor); @@ -252,6 +259,7 @@ const Editor = () => { return; } updateWindowUrlHashParams({logEventNum: newLogEventNum}); + setLogEventNum(newLogEventNum); }, []); // Synchronize `beginLineNumToLogEventNumRef` with `beginLineNumToLogEventNum`. diff --git a/src/stores/contextStore.ts b/src/stores/contextStore.ts index 9ab665461..e4f34f3a5 100644 --- a/src/stores/contextStore.ts +++ b/src/stores/contextStore.ts @@ -4,28 +4,22 @@ import {PopUpMessage} from "../typings/notifications"; interface ContextValues { - logEventNum: number; postPopUp: (message: PopUpMessage) => void; } interface ContextActions { - setLogEventNum: (newLogEventNum: number) => void; setPostPopUp: (postPopUp: (message: PopUpMessage) => void) => void; } type ContextState = ContextValues & ContextActions; const CONTEXT_STORE_DEFAULT: ContextValues = { - logEventNum: 0, postPopUp: () => { }, }; const useContextStore = create((set) => ({ ...CONTEXT_STORE_DEFAULT, - setLogEventNum: (newLogEventNum) => { - set({logEventNum: newLogEventNum}); - }, setPostPopUp: (postPopUp) => { set({postPopUp}); }, diff --git a/src/stores/viewStore.ts b/src/stores/viewStore.ts index d44d9c7d6..71d18ffb7 100644 --- a/src/stores/viewStore.ts +++ b/src/stores/viewStore.ts @@ -19,7 +19,7 @@ import { NavigationAction, } from "../utils/actions"; import {clamp} from "../utils/math"; -import useContextStore, {CONTEXT_STORE_DEFAULT} from "./contextStore"; +import useContextStore from "./contextStore"; import useLogFileManagerStore from "./logFileManagerProxyStore"; import useLogFileStore from "./logFileStore"; import useQueryStore from "./queryStore"; @@ -112,8 +112,9 @@ const getPageNumCursor = ( const useViewStore = create((set, get) => ({ ...VIEW_STORE_DEFAULT, filterLogs: (filter: LogLevelFilter) => { + const {logEventNum} = get(); const {updatePageData} = get(); - const {logEventNum, postPopUp} = useContextStore.getState(); + const {postPopUp} = useContextStore.getState(); const {logFileManagerProxy} = useLogFileManagerStore.getState(); const {setUiState} = useUiStore.getState(); setUiState(UI_STATE.FAST_LOADING); @@ -143,13 +144,13 @@ const useViewStore = create((set, get) => ({ startQuery(); }, loadPageByAction: (navAction: NavigationAction) => { - const {isPrettified, numPages, pageNum, updatePageData} = get(); - const {logEventNum, postPopUp} = useContextStore.getState(); + const {isPrettified, logEventNum, numPages, pageNum, updatePageData} = get(); + const {postPopUp} = useContextStore.getState(); const {logFileManagerProxy} = useLogFileManagerStore.getState(); const {fileSrc, loadFile} = useLogFileStore.getState(); const {uiState, setUiState} = useUiStore.getState(); if (navAction.code === ACTION_NAME.RELOAD) { - if (null === fileSrc || CONTEXT_STORE_DEFAULT.logEventNum === logEventNum) { + if (null === fileSrc || VIEW_STORE_DEFAULT.logEventNum === logEventNum) { throw new Error( `Unexpected fileSrc=${JSON.stringify( fileSrc @@ -207,8 +208,8 @@ const useViewStore = create((set, get) => ({ set({pageNum: newPageNum}); }, updateIsPrettified: (newIsPrettified: boolean) => { - const {updatePageData} = get(); - const {logEventNum, postPopUp} = useContextStore.getState(); + const {logEventNum, updatePageData} = get(); + const {postPopUp} = useContextStore.getState(); const {logFileManagerProxy} = useLogFileManagerStore.getState(); const {setUiState} = useUiStore.getState(); if (newIsPrettified === get().isPrettified) { @@ -218,7 +219,7 @@ const useViewStore = create((set, get) => ({ setUiState(UI_STATE.FAST_LOADING); let cursor: CursorType = {code: CURSOR_CODE.LAST_EVENT, args: null}; - if (CONTEXT_STORE_DEFAULT.logEventNum !== logEventNum) { + if (VIEW_STORE_DEFAULT.logEventNum !== logEventNum) { cursor = { code: CURSOR_CODE.EVENT_NUM, args: {eventNum: logEventNum}, @@ -238,6 +239,7 @@ const useViewStore = create((set, get) => ({ }); }, updatePageData: (pageData: PageData) => { + const {setLogEventNum} = get(); const {setUiState} = useUiStore.getState(); set({ logData: pageData.logs, @@ -245,9 +247,13 @@ const useViewStore = create((set, get) => ({ pageNum: pageData.pageNum, beginLineNumToLogEventNum: pageData.beginLineNumToLogEventNum, }); + const newLogEventNum = pageData.logEventNum; updateWindowUrlHashParams({ - logEventNum: pageData.logEventNum, + logEventNum: newLogEventNum, }); + if (null !== newLogEventNum) { + setLogEventNum(newLogEventNum); + } setUiState(UI_STATE.READY); }, })); diff --git a/src/utils/url.ts b/src/utils/url.ts index da81b97f9..db6275735 100644 --- a/src/utils/url.ts +++ b/src/utils/url.ts @@ -240,6 +240,8 @@ const openInNewTab = (url: string): void => { /** * Updates hash parameters in the current window's URL with the given key-value pairs. * + * Note that we need to call setters in corresponding Zustandard stores to update the state. + * * @param updates An object containing key-value pairs to update the hash parameters. If a value is * `null`, the corresponding kv-pair will be removed from the URL's hash parameters. */ @@ -253,14 +255,13 @@ const updateWindowUrlHashParams = (updates: UrlHashParamUpdatesType) => { const newUrl = new URL(window.location.href); newUrl.hash = newHash; window.history.pushState({}, "", newUrl); - - // `history.pushState` doesn't trigger a `hashchange`, so we need to dispatch one manually. - window.dispatchEvent(new HashChangeEvent("hashchange")); }; /** * Updates search parameters in the current window's URL with the given key-value pairs. * + * Note that we need to call setters in corresponding Zustandard stores to update the state. + * * @param updates An object containing key-value pairs to update the search parameters. If a value * is `null`, the corresponding kv-pair will be removed from the URL's search parameters. */ From 0dfe7b899b4fef7d55930f1e03337f19e1df668f Mon Sep 17 00:00:00 2001 From: zzxthehappiest Date: Sun, 25 May 2025 09:35:05 +0800 Subject: [PATCH 06/25] Fix lint --- src/App.tsx | 1 + src/components/AppController.tsx | 31 ++-- .../SidebarTabs/SearchTabPanel/Result.tsx | 5 +- src/components/Editor/index.tsx | 21 ++- src/components/StatusBar/index.tsx | 11 +- src/stores/logFileStore.ts | 3 +- src/stores/viewStore.ts | 2 +- src/utils/url.ts | 161 +++++++++--------- 8 files changed, 128 insertions(+), 107 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 1ad884385..73cc169b6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,6 +2,7 @@ import AppController from "./components/AppController"; import Layout from "./components/Layout"; import NotificationContextProvider from "./contexts/NotificationContextProvider"; + /** * Renders the main application. * diff --git a/src/components/AppController.tsx b/src/components/AppController.tsx index e84e3ad61..733df2a54 100644 --- a/src/components/AppController.tsx +++ b/src/components/AppController.tsx @@ -1,3 +1,4 @@ +/* eslint max-statements: ["error", 30] */ import React, { useContext, useEffect, @@ -25,7 +26,9 @@ import {clamp} from "../utils/math"; import { getWindowUrlHashParams, getWindowUrlSearchParams, - updateWindowUrlHashParams, URL_HASH_PARAMS_DEFAULT, URL_SEARCH_PARAMS_DEFAULT + updateWindowUrlHashParams, + URL_HASH_PARAMS_DEFAULT, + URL_SEARCH_PARAMS_DEFAULT, } from "../utils/url.ts"; @@ -40,9 +43,9 @@ import { const updateUrlIfEventOnPage = ( logEventNum: number, logEventNumsOnPage: number[] -): { isUpdated: boolean, nearestLogEventNum: number } => { +): {isUpdated: boolean; nearestLogEventNum: number} => { if (false === isWithinBounds(logEventNumsOnPage, logEventNum)) { - return { isUpdated: false, nearestLogEventNum: 0 }; + return {isUpdated: false, nearestLogEventNum: 0}; } const nearestIdx = findNearestLessThanOrEqualElement( @@ -64,7 +67,7 @@ const updateUrlIfEventOnPage = ( logEventNum: nearestLogEventNum, }); - return { isUpdated: true, nearestLogEventNum: nearestLogEventNum }; + return {isUpdated: true, nearestLogEventNum: nearestLogEventNum}; }; interface AppControllerProps { @@ -101,7 +104,7 @@ const AppController = ({children}: AppControllerProps) => { const setPostPopUp = useContextStore((state) => state.setPostPopUp); // Refs - const isPrettifiedRef = useRef(isPrettified ?? false); + const isPrettifiedRef = useRef(isPrettified); const logEventNumRef = useRef(logEventNum); useEffect(() => { @@ -133,20 +136,20 @@ const AppController = ({children}: AppControllerProps) => { return () => { window.removeEventListener("hashchange", handleHashChange); }; - }, []); + }, [setFileSrc, + setLogEventNum, + updateIsPrettified]); // Synchronize `isPrettifiedRef` with `isPrettified`. useEffect(() => { - isPrettifiedRef.current = isPrettified ?? false; + isPrettifiedRef.current = isPrettified; }, [ isPrettified, ]); // Synchronize `logEventNumRef` with `logEventNum`. useEffect(() => { - if (null !== logEventNum) { - logEventNumRef.current = logEventNum; - } + logEventNumRef.current = logEventNum; }, [ logEventNum, ]); @@ -162,10 +165,15 @@ const AppController = ({children}: AppControllerProps) => { const clampedLogEventNum = clamp(logEventNum, 1, numEvents); - const {isUpdated, nearestLogEventNum} = updateUrlIfEventOnPage(clampedLogEventNum, logEventNumsOnPage); + const { + isUpdated, + nearestLogEventNum, + } = updateUrlIfEventOnPage(clampedLogEventNum, logEventNumsOnPage); + if (isUpdated) { // No need to request a new page since the log event is on the current page. setLogEventNum(nearestLogEventNum); + return; } @@ -192,6 +200,7 @@ const AppController = ({children}: AppControllerProps) => { logEventNum, logFileManagerProxy, numEvents, + setLogEventNum, setUiState, postPopUp, ]); diff --git a/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/Result.tsx b/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/Result.tsx index 3df24ee8e..05571b432 100644 --- a/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/Result.tsx +++ b/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/Result.tsx @@ -3,9 +3,10 @@ import { Typography, } from "@mui/joy"; -import "./Result.css"; -import { updateWindowUrlHashParams } from "../../../../../utils/url.ts"; import useViewStore from "../../../../../stores/viewStore.ts"; +import {updateWindowUrlHashParams} from "../../../../../utils/url.ts"; + +import "./Result.css"; interface ResultProps { diff --git a/src/components/Editor/index.tsx b/src/components/Editor/index.tsx index ee85ff811..db8494277 100644 --- a/src/components/Editor/index.tsx +++ b/src/components/Editor/index.tsx @@ -1,5 +1,6 @@ /* eslint max-lines: ["error", 350] */ -/* eslint max-lines-per-function: ["error", 170] */ +/* eslint max-lines-per-function: ["error", 200] */ +/* eslint max-statements: ["error", 30] */ import { useCallback, useEffect, @@ -31,11 +32,11 @@ import { getMapKeyByValue, getMapValueWithNearestLessThanOrEqualKey, } from "../../utils/data"; +import {updateWindowUrlHashParams} from "../../utils/url.ts"; import MonacoInstance from "./MonacoInstance"; import {goToPositionAndCenter} from "./MonacoInstance/utils"; import "./index.css"; -import { updateWindowUrlHashParams } from "../../utils/url.ts"; /** @@ -135,7 +136,7 @@ const Editor = () => { const beginLineNumToLogEventNum = useViewStore((state) => state.beginLineNumToLogEventNum); const isPrettified = useViewStore((state) => state.isPrettified); - const updatePrettified = useViewStore((state) => state.updateIsPrettified) + const updateIsPrettified = useViewStore((state) => state.updateIsPrettified); const logData = useViewStore((state) => state.logData); @@ -149,7 +150,7 @@ const Editor = () => { beginLineNumToLogEventNum ); const editorRef = useRef>(null); - const isPrettifiedRef = useRef(isPrettified ?? false); + const isPrettifiedRef = useRef(isPrettified); const isMouseDownRef = useRef(false); const pageSizeRef = useRef(getConfig(CONFIG_KEY.PAGE_SIZE)); @@ -178,20 +179,22 @@ const Editor = () => { case ACTION_NAME.COPY_LOG_EVENT: handleCopyLogEventAction(editor, beginLineNumToLogEventNumRef.current); break; - case ACTION_NAME.TOGGLE_PRETTIFY: + case ACTION_NAME.TOGGLE_PRETTIFY: { const newIsPrettified = !isPrettifiedRef.current; updateWindowUrlHashParams({ [HASH_PARAM_NAMES.IS_PRETTIFIED]: newIsPrettified, }); - updatePrettified(newIsPrettified); + updateIsPrettified(newIsPrettified); break; + } case ACTION_NAME.TOGGLE_WORD_WRAP: handleToggleWordWrapAction(editor); break; default: break; } - }, [loadPageByAction]); + }, [loadPageByAction, + updateIsPrettified]); /** * Sets `editorRef` and configures callbacks for mouse down detection. @@ -260,7 +263,7 @@ const Editor = () => { } updateWindowUrlHashParams({logEventNum: newLogEventNum}); setLogEventNum(newLogEventNum); - }, []); + }, [setLogEventNum]); // Synchronize `beginLineNumToLogEventNumRef` with `beginLineNumToLogEventNum`. useEffect(() => { @@ -269,7 +272,7 @@ const Editor = () => { // Synchronize `isPrettifiedRef` with `isPrettified`. useEffect(() => { - isPrettifiedRef.current = isPrettified ?? false; + isPrettifiedRef.current = isPrettified; }, [isPrettified]); // On `logEventNum` update, update line number in the editor. diff --git a/src/components/StatusBar/index.tsx b/src/components/StatusBar/index.tsx index 9f77b24f2..d14b7c400 100644 --- a/src/components/StatusBar/index.tsx +++ b/src/components/StatusBar/index.tsx @@ -10,16 +10,19 @@ import AutoFixOffRoundedIcon from "@mui/icons-material/AutoFixOffRounded"; import useLogFileStore from "../../stores/logFileStore"; import useUiStore from "../../stores/uiStore"; +import useViewStore from "../../stores/viewStore.ts"; import {UI_ELEMENT} from "../../typings/states"; import {HASH_PARAM_NAMES} from "../../typings/url"; import {ACTION_NAME} from "../../utils/actions"; import {isDisabled} from "../../utils/states"; +import { + copyPermalinkToClipboard, + updateWindowUrlHashParams, +} from "../../utils/url.ts"; import LogLevelSelect from "./LogLevelSelect"; import StatusBarToggleButton from "./StatusBarToggleButton"; import "./index.css"; -import useViewStore from "../../stores/viewStore.ts"; -import { copyPermalinkToClipboard, updateWindowUrlHashParams } from "../../utils/url.ts"; /** @@ -87,12 +90,12 @@ const StatusBar = () => { , inactive: , }} - tooltipTitle={isPrettified ?? false ? + tooltipTitle={isPrettified ? "Turn off Prettify" : "Turn on Prettify"} onClick={handleStatusButtonClick}/> diff --git a/src/stores/logFileStore.ts b/src/stores/logFileStore.ts index fd8d6a1f1..c22df01e6 100644 --- a/src/stores/logFileStore.ts +++ b/src/stores/logFileStore.ts @@ -1,3 +1,4 @@ +/* eslint max-lines-per-function: ["error", 70] */ import * as Comlink from "comlink"; import {create} from "zustand"; @@ -21,13 +22,13 @@ import { FileSrcType, } from "../typings/worker"; import {getConfig} from "../utils/config"; +import {updateWindowUrlSearchParams} from "../utils/url.ts"; import useContextStore from "./contextStore"; import useLogExportStore, {LOG_EXPORT_STORE_DEFAULT} from "./logExportStore"; import useLogFileManagerProxyStore from "./logFileManagerProxyStore"; import useQueryStore from "./queryStore"; import useUiStore from "./uiStore"; import useViewStore from "./viewStore"; -import { updateWindowUrlSearchParams } from "../utils/url.ts"; interface LogFileValues { diff --git a/src/stores/viewStore.ts b/src/stores/viewStore.ts index 71d18ffb7..6f9ed7714 100644 --- a/src/stores/viewStore.ts +++ b/src/stores/viewStore.ts @@ -19,12 +19,12 @@ import { NavigationAction, } from "../utils/actions"; import {clamp} from "../utils/math"; +import {updateWindowUrlHashParams} from "../utils/url.ts"; import useContextStore from "./contextStore"; import useLogFileManagerStore from "./logFileManagerProxyStore"; import useLogFileStore from "./logFileStore"; import useQueryStore from "./queryStore"; import useUiStore from "./uiStore"; -import { updateWindowUrlHashParams } from "../utils/url.ts"; interface ViewStoreValues { diff --git a/src/utils/url.ts b/src/utils/url.ts index db6275735..9b812a693 100644 --- a/src/utils/url.ts +++ b/src/utils/url.ts @@ -1,10 +1,13 @@ +import {NullableProperties} from "../typings/common.ts"; import { HASH_PARAM_NAMES, - SEARCH_PARAM_NAMES, UrlHashParams, - UrlHashParamUpdatesType, UrlSearchParams, - UrlSearchParamUpdatesType + SEARCH_PARAM_NAMES, + UrlHashParams, + UrlHashParamUpdatesType, + UrlSearchParams, + UrlSearchParamUpdatesType, } from "../typings/url.ts"; -import { NullableProperties } from "../typings/common.ts"; + /** * Default values of the search parameters. @@ -18,7 +21,7 @@ const URL_SEARCH_PARAMS_DEFAULT = Object.freeze({ */ const URL_HASH_PARAMS_DEFAULT = Object.freeze({ [HASH_PARAM_NAMES.IS_PRETTIFIED]: false, - [HASH_PARAM_NAMES.LOG_EVENT_NUM]: null, + [HASH_PARAM_NAMES.LOG_EVENT_NUM]: -1, }); /** @@ -27,78 +30,6 @@ const URL_HASH_PARAMS_DEFAULT = Object.freeze({ const AMBIGUOUS_URL_CHARS_REGEX = new RegExp(`${encodeURIComponent("#")}|${encodeURIComponent("&")}`); -/** - * Copies the current window's URL to the clipboard. If any `updates` parameters are specified, - * the copied URL will include these modifications, but the original window's URL will not be - * changed. - * - * @param searchParamUpdates An object containing key-value pairs to update the search parameters. - * If a value is `null`, the corresponding kv-pair will be removed from the URL's search parameters. - * @param hashParamsUpdates An object containing key-value pairs to update the hash parameters. - * If a value is `null`, the corresponding kv-pair will be removed from the URL's hash parameters. - */ -const copyPermalinkToClipboard = ( - searchParamUpdates: UrlSearchParamUpdatesType, - hashParamsUpdates: UrlHashParamUpdatesType, -) => { - const newUrl = new URL(window.location.href); - newUrl.search = getUpdatedSearchParams(searchParamUpdates); - newUrl.hash = getUpdatedHashParams(hashParamsUpdates); - navigator.clipboard.writeText(newUrl.toString()) - .then(() => { - console.log("URL copied to clipboard."); - }) - .catch((error: unknown) => { - console.error("Failed to copy URL to clipboard:", error); - }); -}; - -/** - * Gets an absolute URL composed of a given path relative to the - * window.location, if the given path is a relative reference; otherwise - * the given path is returned verbatim. - * - * @param path The path to be resolved. - * @return The absolute URL of the given path. - * @throws {Error} if the given `path` is a relative reference but invalid. - */ -const getAbsoluteUrl = (path: string) => { - try { - // eslint-disable-next-line no-new - new URL(path); - } catch { - path = new URL(path, window.location.origin).href; - } - - return path; -}; - -/** - * Extracts the basename (filename) from a given string containing a URL. - * - * @param urlString a URL string that does not contain escaped `/` (%2F). - * @param defaultFileName - * @return The extracted basename or `defaultFileName` if extraction fails. - */ -const getBasenameFromUrlOrDefault = ( - urlString: string, - defaultFileName: string = "Unknown filename" -): string => { - let basename = defaultFileName; - try { - const url = new URL(urlString); - const parts = url.pathname.split("/"); - - // Explicit cast since typescript thinks `parts.pop()` can be undefined, but it can't be - // since `parts` can't be empty. - basename = parts.pop() as string; - } catch (e) { - console.error(`Failed to parse basename from ${urlString}.`, e); - } - - return basename; -}; - /** * Computes updated URL search parameters based on the provided key-value pairs. * @@ -173,6 +104,78 @@ const getUpdatedHashParams = (updates: UrlHashParamUpdatesType) => { return newHashParams.toString(); }; +/** + * Copies the current window's URL to the clipboard. If any `updates` parameters are specified, + * the copied URL will include these modifications, but the original window's URL will not be + * changed. + * + * @param searchParamUpdates An object containing key-value pairs to update the search parameters. + * If a value is `null`, the corresponding kv-pair will be removed from the URL's search parameters. + * @param hashParamsUpdates An object containing key-value pairs to update the hash parameters. + * If a value is `null`, the corresponding kv-pair will be removed from the URL's hash parameters. + */ +const copyPermalinkToClipboard = ( + searchParamUpdates: UrlSearchParamUpdatesType, + hashParamsUpdates: UrlHashParamUpdatesType, +) => { + const newUrl = new URL(window.location.href); + newUrl.search = getUpdatedSearchParams(searchParamUpdates); + newUrl.hash = getUpdatedHashParams(hashParamsUpdates); + navigator.clipboard.writeText(newUrl.toString()) + .then(() => { + console.log("URL copied to clipboard."); + }) + .catch((error: unknown) => { + console.error("Failed to copy URL to clipboard:", error); + }); +}; + +/** + * Gets an absolute URL composed of a given path relative to the + * window.location, if the given path is a relative reference; otherwise + * the given path is returned verbatim. + * + * @param path The path to be resolved. + * @return The absolute URL of the given path. + * @throws {Error} if the given `path` is a relative reference but invalid. + */ +const getAbsoluteUrl = (path: string) => { + try { + // eslint-disable-next-line no-new + new URL(path); + } catch { + path = new URL(path, window.location.origin).href; + } + + return path; +}; + +/** + * Extracts the basename (filename) from a given string containing a URL. + * + * @param urlString a URL string that does not contain escaped `/` (%2F). + * @param defaultFileName + * @return The extracted basename or `defaultFileName` if extraction fails. + */ +const getBasenameFromUrlOrDefault = ( + urlString: string, + defaultFileName: string = "Unknown filename" +): string => { + let basename = defaultFileName; + try { + const url = new URL(urlString); + const parts = url.pathname.split("/"); + + // Explicit cast since typescript thinks `parts.pop()` can be undefined, but it can't be + // since `parts` can't be empty. + basename = parts.pop() as string; + } catch (e) { + console.error(`Failed to parse basename from ${urlString}.`, e); + } + + return basename; +}; + /** * Retrieves all hash parameters from the current window's URL. * @@ -272,8 +275,6 @@ const updateWindowUrlSearchParams = (updates: UrlSearchParamUpdatesType) => { }; export { - URL_HASH_PARAMS_DEFAULT, - URL_SEARCH_PARAMS_DEFAULT, copyPermalinkToClipboard, getAbsoluteUrl, getBasenameFromUrlOrDefault, @@ -282,4 +283,6 @@ export { openInNewTab, updateWindowUrlHashParams, updateWindowUrlSearchParams, + URL_HASH_PARAMS_DEFAULT, + URL_SEARCH_PARAMS_DEFAULT, }; From c808cf2062e795f6eaa9b21acbafa0eac75171fb Mon Sep 17 00:00:00 2001 From: zzxthehappiest Date: Sun, 25 May 2025 10:32:18 +0800 Subject: [PATCH 07/25] Fix coderabit comments --- src/components/Editor/index.tsx | 2 +- src/components/StatusBar/index.tsx | 8 ++++---- src/utils/url.ts | 30 +++++++++++++++++------------- 3 files changed, 22 insertions(+), 18 deletions(-) diff --git a/src/components/Editor/index.tsx b/src/components/Editor/index.tsx index db8494277..7afd292a3 100644 --- a/src/components/Editor/index.tsx +++ b/src/components/Editor/index.tsx @@ -32,7 +32,7 @@ import { getMapKeyByValue, getMapValueWithNearestLessThanOrEqualKey, } from "../../utils/data"; -import {updateWindowUrlHashParams} from "../../utils/url.ts"; +import {updateWindowUrlHashParams} from "../../utils/url"; import MonacoInstance from "./MonacoInstance"; import {goToPositionAndCenter} from "./MonacoInstance/utils"; diff --git a/src/components/StatusBar/index.tsx b/src/components/StatusBar/index.tsx index d14b7c400..650f7a808 100644 --- a/src/components/StatusBar/index.tsx +++ b/src/components/StatusBar/index.tsx @@ -50,7 +50,7 @@ const StatusBar = () => { switch (actionName) { case ACTION_NAME.TOGGLE_PRETTIFY: updateWindowUrlHashParams({ - [HASH_PARAM_NAMES.IS_PRETTIFIED]: !isPrettified, + [HASH_PARAM_NAMES.IS_PRETTIFIED]: false === isPrettified, }); setIsPrettified(!isPrettified); break; @@ -95,9 +95,9 @@ const StatusBar = () => { active: , inactive: , }} - tooltipTitle={isPrettified ? - "Turn off Prettify" : - "Turn on Prettify"} + tooltipTitle={false === isPrettified ? + "Turn on Prettify" : + "Turn off Prettify"} onClick={handleStatusButtonClick}/> ); diff --git a/src/utils/url.ts b/src/utils/url.ts index 9b812a693..271b6aa72 100644 --- a/src/utils/url.ts +++ b/src/utils/url.ts @@ -21,7 +21,7 @@ const URL_SEARCH_PARAMS_DEFAULT = Object.freeze({ */ const URL_HASH_PARAMS_DEFAULT = Object.freeze({ [HASH_PARAM_NAMES.IS_PRETTIFIED]: false, - [HASH_PARAM_NAMES.LOG_EVENT_NUM]: -1, + [HASH_PARAM_NAMES.LOG_EVENT_NUM]: 0, }); /** @@ -214,17 +214,19 @@ const getWindowUrlSearchParams = () => { const urlSearchParams = new URLSearchParams(window.location.search.substring(1)); 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`. - const [, filePath] = window.location.search.split("filePath="); - if ("undefined" !== typeof filePath && 0 !== filePath.length) { - let resolvedFilePath = filePath; - try { - resolvedFilePath = getAbsoluteUrl(filePath); - } catch (e) { - console.error("Unable to get absolute URL from filePath:", e); + // Extract filePath value by finding the parameter and taking everything after it + const filePathIndex = window.location.search.indexOf("filePath="); + if (-1 !== filePathIndex) { + const filePath = window.location.search.substring(filePathIndex + "filePath=".length); + if (0 !== filePath.length) { + let resolvedFilePath = filePath; + try { + resolvedFilePath = getAbsoluteUrl(filePath); + } catch (e) { + console.error("Unable to get absolute URL from filePath:", e); + } + searchParams[SEARCH_PARAM_NAMES.FILE_PATH] = resolvedFilePath; } - searchParams[SEARCH_PARAM_NAMES.FILE_PATH] = resolvedFilePath; } } @@ -243,7 +245,8 @@ const openInNewTab = (url: string): void => { /** * Updates hash parameters in the current window's URL with the given key-value pairs. * - * Note that we need to call setters in corresponding Zustandard stores to update the state. + * Note: This function only updates the URL. Callers are responsible for updating corresponding + * Zustand store state to maintain synchronization. * * @param updates An object containing key-value pairs to update the hash parameters. If a value is * `null`, the corresponding kv-pair will be removed from the URL's hash parameters. @@ -263,7 +266,8 @@ const updateWindowUrlHashParams = (updates: UrlHashParamUpdatesType) => { /** * Updates search parameters in the current window's URL with the given key-value pairs. * - * Note that we need to call setters in corresponding Zustandard stores to update the state. + * Note: This function only updates the URL. Callers are responsible for updating corresponding + * Zustand store state to maintain synchronization. * * @param updates An object containing key-value pairs to update the search parameters. If a value * is `null`, the corresponding kv-pair will be removed from the URL's search parameters. From 00cadb96356cdfd6d569a47eede5ef8ff5de71ff Mon Sep 17 00:00:00 2001 From: zzxthehappiest Date: Sun, 25 May 2025 11:30:51 +0800 Subject: [PATCH 08/25] Fix coderabbit review --- src/components/AppController.tsx | 4 +--- src/components/Editor/index.tsx | 2 +- src/stores/logFileStore.ts | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/components/AppController.tsx b/src/components/AppController.tsx index d391d8700..634d1618f 100644 --- a/src/components/AppController.tsx +++ b/src/components/AppController.tsx @@ -114,9 +114,7 @@ const AppController = ({children}: AppControllerProps) => { updateIsPrettified(hashParams.isPrettified); } - // It is weird that updating search params when hash params changed, even there - // might be hidden condition that search params always change together with hash - // params. + // Also check search params to handle initial page load and maintain full URL state const searchParams = getWindowUrlSearchParams(); if (null !== searchParams.filePath) { diff --git a/src/components/Editor/index.tsx b/src/components/Editor/index.tsx index 7afd292a3..48e201594 100644 --- a/src/components/Editor/index.tsx +++ b/src/components/Editor/index.tsx @@ -180,7 +180,7 @@ const Editor = () => { handleCopyLogEventAction(editor, beginLineNumToLogEventNumRef.current); break; case ACTION_NAME.TOGGLE_PRETTIFY: { - const newIsPrettified = !isPrettifiedRef.current; + const newIsPrettified = false === isPrettifiedRef.current; updateWindowUrlHashParams({ [HASH_PARAM_NAMES.IS_PRETTIFIED]: newIsPrettified, }); diff --git a/src/stores/logFileStore.ts b/src/stores/logFileStore.ts index db08c3f63..5c3e69358 100644 --- a/src/stores/logFileStore.ts +++ b/src/stores/logFileStore.ts @@ -21,7 +21,7 @@ import { FileSrcType, } from "../typings/worker"; import {getConfig} from "../utils/config"; -import {updateWindowUrlSearchParams} from "../utils/url.ts"; +import {updateWindowUrlSearchParams} from "../utils/url"; import useLogExportStore, {LOG_EXPORT_STORE_DEFAULT} from "./logExportStore"; import useLogFileManagerProxyStore from "./logFileManagerProxyStore"; import useNotificationStore, {handleErrorWithNotification} from "./notificationStore"; From 6a873d4f241a3c608b60019beaa3bc3e9780ed6a Mon Sep 17 00:00:00 2001 From: zzxthehappiest Date: Sun, 25 May 2025 17:20:12 +0800 Subject: [PATCH 09/25] Add junhaoliao@b972113 --- src/utils/js.ts | 15 ++++++ src/utils/url.ts | 122 ++++++++++++++++++++++++----------------------- 2 files changed, 78 insertions(+), 59 deletions(-) diff --git a/src/utils/js.ts b/src/utils/js.ts index 81b41e547..6924a2584 100644 --- a/src/utils/js.ts +++ b/src/utils/js.ts @@ -1,8 +1,22 @@ +import {Nullable} from "../typings/common.ts"; import { JsonObject, JsonValue, } from "../typings/js"; +/** + * Returns a new object with null values filtered out, and all values converted to strings. + * + * @param obj + * @return The new object with string values. + */ +const filterNullValuesToStrings = ( + obj: Record> +): Record => Object.fromEntries( + Object.entries(obj) + .filter(([, v]) => v !== null) + .map(([k, v]) => [k, String(v)]) +) as Record; /** * Gets a nested value from a JSON object. @@ -41,6 +55,7 @@ const jsonValueToString = (input: JsonValue | undefined): string => { }; export { + filterNullValuesToStrings, getNestedJsonValue, jsonValueToString, }; diff --git a/src/utils/url.ts b/src/utils/url.ts index 271b6aa72..6a8bb7631 100644 --- a/src/utils/url.ts +++ b/src/utils/url.ts @@ -7,6 +7,7 @@ import { UrlSearchParams, UrlSearchParamUpdatesType, } from "../typings/url.ts"; +import {filterNullValuesToStrings} from "../utils/js.ts"; /** @@ -30,6 +31,63 @@ const URL_HASH_PARAMS_DEFAULT = Object.freeze({ const AMBIGUOUS_URL_CHARS_REGEX = new RegExp(`${encodeURIComponent("#")}|${encodeURIComponent("&")}`); +/** + * Retrieves all hash parameters from the current window's URL. + * + * @return An object containing the hash parameters. + */ +const getWindowUrlHashParams = () => { + const urlHashParams: NullableProperties = + structuredClone(URL_HASH_PARAMS_DEFAULT); + const hashParams = new URLSearchParams(window.location.hash.substring(1)); + + const logEventNum = hashParams.get(HASH_PARAM_NAMES.LOG_EVENT_NUM); + if (null !== logEventNum) { + const parsed = Number(logEventNum); + urlHashParams[HASH_PARAM_NAMES.LOG_EVENT_NUM] = Number.isNaN(parsed) ? + null : + parsed; + } + + const isPrettified = hashParams.get(HASH_PARAM_NAMES.IS_PRETTIFIED); + if (null !== isPrettified) { + urlHashParams[HASH_PARAM_NAMES.IS_PRETTIFIED] = "true" === isPrettified; + } + + return urlHashParams; +}; + +/** + * Retrieves all search parameters from the current window's URL. + * + * @return An object containing the search parameters. + */ +const getWindowUrlSearchParams = () => { + const searchParams : NullableProperties = structuredClone( + URL_SEARCH_PARAMS_DEFAULT + ); + const urlSearchParams = new URLSearchParams(window.location.search.substring(1)); + + if (urlSearchParams.has(SEARCH_PARAM_NAMES.FILE_PATH)) { + // Extract filePath value by finding the parameter and taking everything after it + const filePathIndex = window.location.search.indexOf("filePath="); + if (-1 !== filePathIndex) { + const filePath = window.location.search.substring(filePathIndex + "filePath=".length); + if (0 !== filePath.length) { + let resolvedFilePath = filePath; + try { + resolvedFilePath = getAbsoluteUrl(filePath); + } catch (e) { + console.error("Unable to get absolute URL from filePath:", e); + } + searchParams[SEARCH_PARAM_NAMES.FILE_PATH] = resolvedFilePath; + } + } + } + + return searchParams; +}; + /** * Computes updated URL search parameters based on the provided key-value pairs. * @@ -39,7 +97,8 @@ const AMBIGUOUS_URL_CHARS_REGEX = * @private */ const getUpdatedSearchParams = (updates: UrlSearchParamUpdatesType) => { - const newSearchParams = new URLSearchParams(window.location.search.substring(1)); + const currentParams = getWindowUrlSearchParams(); + const newSearchParams = new URLSearchParams(filterNullValuesToStrings(currentParams)); const {filePath: newFilePath} = updates; for (const [key, value] of Object.entries(updates)) { @@ -92,7 +151,9 @@ const getUpdatedSearchParams = (updates: UrlSearchParamUpdatesType) => { * @private */ const getUpdatedHashParams = (updates: UrlHashParamUpdatesType) => { - const newHashParams = new URLSearchParams(window.location.hash.substring(1)); + const currentParams = getWindowUrlHashParams(); + const newHashParams = new URLSearchParams(filterNullValuesToStrings(currentParams)); + for (const [key, value] of Object.entries(updates)) { if (null === value) { newHashParams.delete(key); @@ -176,63 +237,6 @@ const getBasenameFromUrlOrDefault = ( return basename; }; -/** - * Retrieves all hash parameters from the current window's URL. - * - * @return An object containing the hash parameters. - */ -const getWindowUrlHashParams = () => { - const urlHashParams: NullableProperties = - structuredClone(URL_HASH_PARAMS_DEFAULT); - const hashParams = new URLSearchParams(window.location.hash.substring(1)); - - const logEventNum = hashParams.get(HASH_PARAM_NAMES.LOG_EVENT_NUM); - if (null !== logEventNum) { - const parsed = Number(logEventNum); - urlHashParams[HASH_PARAM_NAMES.LOG_EVENT_NUM] = Number.isNaN(parsed) ? - null : - parsed; - } - - const isPrettified = hashParams.get(HASH_PARAM_NAMES.IS_PRETTIFIED); - if (null !== isPrettified) { - urlHashParams[HASH_PARAM_NAMES.IS_PRETTIFIED] = "true" === isPrettified; - } - - return urlHashParams; -}; - -/** - * Retrieves all search parameters from the current window's URL. - * - * @return An object containing the search parameters. - */ -const getWindowUrlSearchParams = () => { - const searchParams : NullableProperties = structuredClone( - URL_SEARCH_PARAMS_DEFAULT - ); - const urlSearchParams = new URLSearchParams(window.location.search.substring(1)); - - if (urlSearchParams.has(SEARCH_PARAM_NAMES.FILE_PATH)) { - // Extract filePath value by finding the parameter and taking everything after it - const filePathIndex = window.location.search.indexOf("filePath="); - if (-1 !== filePathIndex) { - const filePath = window.location.search.substring(filePathIndex + "filePath=".length); - if (0 !== filePath.length) { - let resolvedFilePath = filePath; - try { - resolvedFilePath = getAbsoluteUrl(filePath); - } catch (e) { - console.error("Unable to get absolute URL from filePath:", e); - } - searchParams[SEARCH_PARAM_NAMES.FILE_PATH] = resolvedFilePath; - } - } - } - - return searchParams; -}; - /** * Opens a given URL in a new browser tab. * From 6620583e6b2d3cf4ca2c6340d70ef16ab1aebe75 Mon Sep 17 00:00:00 2001 From: zzxthehappiest Date: Mon, 26 May 2025 18:55:14 +0800 Subject: [PATCH 10/25] Fix lint --- src/utils/js.ts | 6 ++++-- src/utils/url.ts | 40 ++++++++++++++++++++-------------------- 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/src/utils/js.ts b/src/utils/js.ts index 6924a2584..7bfcb4898 100644 --- a/src/utils/js.ts +++ b/src/utils/js.ts @@ -4,6 +4,7 @@ import { JsonValue, } from "../typings/js"; + /** * Returns a new object with null values filtered out, and all values converted to strings. * @@ -14,8 +15,9 @@ const filterNullValuesToStrings = ( obj: Record> ): Record => Object.fromEntries( Object.entries(obj) - .filter(([, v]) => v !== null) - .map(([k, v]) => [k, String(v)]) + .filter(([, v]) => null !== v) + .map(([k, v]) => [k, + String(v)]) ) as Record; /** diff --git a/src/utils/url.ts b/src/utils/url.ts index 6a8bb7631..3fbd9d130 100644 --- a/src/utils/url.ts +++ b/src/utils/url.ts @@ -31,6 +31,26 @@ const URL_HASH_PARAMS_DEFAULT = Object.freeze({ const AMBIGUOUS_URL_CHARS_REGEX = new RegExp(`${encodeURIComponent("#")}|${encodeURIComponent("&")}`); +/** + * Gets an absolute URL composed of a given path relative to the + * window.location, if the given path is a relative reference; otherwise + * the given path is returned verbatim. + * + * @param path The path to be resolved. + * @return The absolute URL of the given path. + * @throws {Error} if the given `path` is a relative reference but invalid. + */ +const getAbsoluteUrl = (path: string) => { + try { + // eslint-disable-next-line no-new + new URL(path); + } catch { + path = new URL(path, window.location.origin).href; + } + + return path; +}; + /** * Retrieves all hash parameters from the current window's URL. * @@ -191,26 +211,6 @@ const copyPermalinkToClipboard = ( }); }; -/** - * Gets an absolute URL composed of a given path relative to the - * window.location, if the given path is a relative reference; otherwise - * the given path is returned verbatim. - * - * @param path The path to be resolved. - * @return The absolute URL of the given path. - * @throws {Error} if the given `path` is a relative reference but invalid. - */ -const getAbsoluteUrl = (path: string) => { - try { - // eslint-disable-next-line no-new - new URL(path); - } catch { - path = new URL(path, window.location.origin).href; - } - - return path; -}; - /** * Extracts the basename (filename) from a given string containing a URL. * From 66abf153323a1b0f341a8b053e5a4f8b9701e47d Mon Sep 17 00:00:00 2001 From: zzxthehappiest Date: Mon, 26 May 2025 19:05:12 +0800 Subject: [PATCH 11/25] Fix coderabbit comments --- src/utils/url.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/utils/url.ts b/src/utils/url.ts index 3fbd9d130..e244d3e28 100644 --- a/src/utils/url.ts +++ b/src/utils/url.ts @@ -1,3 +1,4 @@ +/* eslint max-lines: ["error", 350] */ import {NullableProperties} from "../typings/common.ts"; import { HASH_PARAM_NAMES, @@ -277,8 +278,13 @@ const updateWindowUrlHashParams = (updates: UrlHashParamUpdatesType) => { * is `null`, the corresponding kv-pair will be removed from the URL's search parameters. */ const updateWindowUrlSearchParams = (updates: UrlSearchParamUpdatesType) => { + const newSearch = getUpdatedSearchParams(updates); + const currSearch = window.location.search.substring(1); + if (newSearch === currSearch) { + return; + } const newUrl = new URL(window.location.href); - newUrl.search = getUpdatedSearchParams(updates); + newUrl.search = newSearch; window.history.pushState({}, "", newUrl); }; From cf10ebe6b2324ed220f658a2a3d9be536271532d Mon Sep 17 00:00:00 2001 From: zzxthehappiest Date: Tue, 27 May 2025 18:35:21 +0800 Subject: [PATCH 12/25] Fix comments --- src/components/AppController.tsx | 23 ++++++++----------- .../SidebarTabs/SearchTabPanel/Result.tsx | 11 ++++++--- src/components/Editor/index.tsx | 8 ++++--- src/components/StatusBar/index.tsx | 4 ++-- src/contexts/UrlContextProvider.tsx | 0 src/stores/viewStore.ts | 2 +- src/typings/worker.ts | 5 ++-- 7 files changed, 28 insertions(+), 25 deletions(-) delete mode 100644 src/contexts/UrlContextProvider.tsx diff --git a/src/components/AppController.tsx b/src/components/AppController.tsx index 634d1618f..19daeab58 100644 --- a/src/components/AppController.tsx +++ b/src/components/AppController.tsx @@ -83,7 +83,7 @@ const AppController = ({children}: AppControllerProps) => { const fileSrc = useLogFileStore((state) => state.fileSrc); const loadFile = useLogFileStore((state) => state.loadFile); - const setFileSrc = useLogFileStore((state) => state.setFileSrc); + const {setFileSrc} = useLogFileStore.getState(); const isPrettified = useViewStore((state) => state.isPrettified); const updateIsPrettified = useViewStore((state) => state.updateIsPrettified); @@ -92,11 +92,11 @@ const AppController = ({children}: AppControllerProps) => { const numEvents = useLogFileStore((state) => state.numEvents); const logEventNum = useViewStore((state) => state.logEventNum); - const setLogEventNum = useViewStore((state) => state.setLogEventNum); + const {setLogEventNum} = useViewStore.getState(); const updatePageData = useViewStore((state) => state.updatePageData); - const setUiState = useUiStore((state) => state.setUiState); + const {setUiState} = useUiStore.getState(); // Refs const isPrettifiedRef = useRef(isPrettified); @@ -129,23 +129,21 @@ const AppController = ({children}: AppControllerProps) => { return () => { window.removeEventListener("hashchange", handleHashChange); }; - }, [setFileSrc, + }, [ + updateIsPrettified, + setFileSrc, setLogEventNum, - updateIsPrettified]); + ]); // Synchronize `isPrettifiedRef` with `isPrettified`. useEffect(() => { isPrettifiedRef.current = isPrettified; - }, [ - isPrettified, - ]); + }, [isPrettified]); // Synchronize `logEventNumRef` with `logEventNum`. useEffect(() => { logEventNumRef.current = logEventNum; - }, [ - logEventNum, - ]); + }, [logEventNum]); // On `logEventNum` update, clamp it then switch page if necessary or simply update the URL. useEffect(() => { @@ -158,8 +156,7 @@ const AppController = ({children}: AppControllerProps) => { Array.from(beginLineNumToLogEventNum.values()); const { - isUpdated, - nearestLogEventNum, + isUpdated, nearestLogEventNum, } = updateUrlIfEventOnPage(clampedLogEventNum, logEventNumsOnPage); if (isUpdated) { diff --git a/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/Result.tsx b/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/Result.tsx index 05571b432..a70c4ec67 100644 --- a/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/Result.tsx +++ b/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/Result.tsx @@ -1,3 +1,5 @@ +import {useCallback} from "react"; + import { ListItemButton, Typography, @@ -37,11 +39,14 @@ const Result = ({logEventNum, message, matchRange}: ResultProps) => { message.slice(...matchRange), message.slice(matchRange[1]), ]; - const setLogEventNum = useViewStore((state) => state.setLogEventNum); - const handleResultButtonClick = () => { + const {setLogEventNum} = useViewStore.getState(); + const handleResultButtonClick = useCallback(() => { updateWindowUrlHashParams({logEventNum}); setLogEventNum(logEventNum); - }; + }, [ + logEventNum, + setLogEventNum, + ]); return ( { const logData = useViewStore((state) => state.logData); const logEventNum = useViewStore((state) => state.logEventNum); - const setLogEventNum = useViewStore((state) => state.setLogEventNum); + const {setLogEventNum} = useViewStore.getState(); const loadPageByAction = useViewStore((state) => state.loadPageByAction); @@ -193,8 +193,10 @@ const Editor = () => { default: break; } - }, [loadPageByAction, - updateIsPrettified]); + }, [ + loadPageByAction, + updateIsPrettified, + ]); /** * Sets `editorRef` and configures callbacks for mouse down detection. diff --git a/src/components/StatusBar/index.tsx b/src/components/StatusBar/index.tsx index 650f7a808..995904b3b 100644 --- a/src/components/StatusBar/index.tsx +++ b/src/components/StatusBar/index.tsx @@ -42,7 +42,7 @@ const StatusBar = () => { const uiState = useUiStore((state) => state.uiState); const isPrettified = useViewStore((state) => state.isPrettified); const logEventNum = useViewStore((state) => state.logEventNum); - const setIsPrettified = useViewStore((state) => state.updateIsPrettified); + const updateIsPrettified = useViewStore((state) => state.updateIsPrettified); const handleStatusButtonClick = (ev: React.MouseEvent) => { const {actionName} = ev.currentTarget.dataset; @@ -52,7 +52,7 @@ const StatusBar = () => { updateWindowUrlHashParams({ [HASH_PARAM_NAMES.IS_PRETTIFIED]: false === isPrettified, }); - setIsPrettified(!isPrettified); + updateIsPrettified(!isPrettified); break; default: console.error(`Unexpected action: ${actionName}`); diff --git a/src/contexts/UrlContextProvider.tsx b/src/contexts/UrlContextProvider.tsx deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/stores/viewStore.ts b/src/stores/viewStore.ts index 70db50f0c..3fe18b0a1 100644 --- a/src/stores/viewStore.ts +++ b/src/stores/viewStore.ts @@ -234,7 +234,7 @@ const useViewStore = create((set, get) => ({ logEventNum: newLogEventNum, }); const {setLogEventNum} = get(); - setLogEventNum(newLogEventNum ?? 0); + setLogEventNum(newLogEventNum); const {setUiState} = useUiStore.getState(); setUiState(UI_STATE.READY); }, diff --git a/src/typings/worker.ts b/src/typings/worker.ts index 07dba509a..aaae6df36 100644 --- a/src/typings/worker.ts +++ b/src/typings/worker.ts @@ -1,5 +1,4 @@ import {FILE_TYPE} from "../services/LogFileManager"; -import {Nullable} from "./common"; import {ActiveLogCollectionEventIdx} from "./decoders"; @@ -78,7 +77,7 @@ type LogFileInfo = { type PageData = { beginLineNumToLogEventNum: BeginLineNumToLogEventNumMap; cursorLineNum: number; - logEventNum: Nullable; + logEventNum: number; logs: string; numPages: number; pageNum: number; @@ -90,7 +89,7 @@ type PageData = { const EMPTY_PAGE_RESP: PageData = Object.freeze({ beginLineNumToLogEventNum: new Map(), cursorLineNum: 1, - logEventNum: null, + logEventNum: 0, logs: "", numPages: 1, pageNum: 1, From 09106924f2e63deec4b47f055c7b5fc1960b004a Mon Sep 17 00:00:00 2001 From: zzxthehappiest Date: Tue, 27 May 2025 19:10:09 +0800 Subject: [PATCH 13/25] Fix comments --- src/components/AppController.tsx | 34 ++++++++++---------------------- src/components/Editor/index.tsx | 11 +++-------- 2 files changed, 13 insertions(+), 32 deletions(-) diff --git a/src/components/AppController.tsx b/src/components/AppController.tsx index 19daeab58..d41743d7f 100644 --- a/src/components/AppController.tsx +++ b/src/components/AppController.tsx @@ -1,8 +1,5 @@ /* eslint max-statements: ["error", 30] */ -import React, { - useEffect, - useRef, -} from "react"; +import React, {useEffect} from "react"; import useLogFileManagerStore from "../stores/logFileManagerProxyStore"; import useLogFileStore from "../stores/logFileStore"; @@ -85,23 +82,19 @@ const AppController = ({children}: AppControllerProps) => { const loadFile = useLogFileStore((state) => state.loadFile); const {setFileSrc} = useLogFileStore.getState(); - const isPrettified = useViewStore((state) => state.isPrettified); + const {isPrettified} = useViewStore.getState(); const updateIsPrettified = useViewStore((state) => state.updateIsPrettified); const {logFileManagerProxy} = useLogFileManagerStore.getState(); const numEvents = useLogFileStore((state) => state.numEvents); - const logEventNum = useViewStore((state) => state.logEventNum); + const {logEventNum} = useViewStore.getState(); const {setLogEventNum} = useViewStore.getState(); const updatePageData = useViewStore((state) => state.updatePageData); const {setUiState} = useUiStore.getState(); - // Refs - const isPrettifiedRef = useRef(isPrettified); - const logEventNumRef = useRef(logEventNum); - useEffect(() => { const handleHashChange = () => { const hashParams = getWindowUrlHashParams(); @@ -135,16 +128,6 @@ const AppController = ({children}: AppControllerProps) => { setLogEventNum, ]); - // Synchronize `isPrettifiedRef` with `isPrettified`. - useEffect(() => { - isPrettifiedRef.current = isPrettified; - }, [isPrettified]); - - // Synchronize `logEventNumRef` with `logEventNum`. - useEffect(() => { - logEventNumRef.current = logEventNum; - }, [logEventNum]); - // On `logEventNum` update, clamp it then switch page if necessary or simply update the URL. useEffect(() => { if (0 === numEvents || URL_HASH_PARAMS_DEFAULT.logEventNum === logEventNum) { @@ -156,7 +139,8 @@ const AppController = ({children}: AppControllerProps) => { Array.from(beginLineNumToLogEventNum.values()); const { - isUpdated, nearestLogEventNum, + isUpdated, + nearestLogEventNum, } = updateUrlIfEventOnPage(clampedLogEventNum, logEventNumsOnPage); if (isUpdated) { @@ -173,11 +157,12 @@ const AppController = ({children}: AppControllerProps) => { code: CURSOR_CODE.EVENT_NUM, args: {eventNum: clampedLogEventNum}, }; - const pageData = await logFileManagerProxy.loadPage(cursor, isPrettifiedRef.current); + const pageData = await logFileManagerProxy.loadPage(cursor, isPrettified); updatePageData(pageData); })().catch(handleErrorWithNotification); }, [ beginLineNumToLogEventNum, + isPrettified, logEventNum, logFileManagerProxy, numEvents, @@ -193,16 +178,17 @@ const AppController = ({children}: AppControllerProps) => { } let cursor: CursorType = {code: CURSOR_CODE.LAST_EVENT, args: null}; - if (URL_HASH_PARAMS_DEFAULT.logEventNum !== logEventNumRef.current) { + if (URL_HASH_PARAMS_DEFAULT.logEventNum !== logEventNum) { cursor = { code: CURSOR_CODE.EVENT_NUM, - args: {eventNum: logEventNumRef.current}, + args: {eventNum: logEventNum}, }; } loadFile(fileSrc, cursor); }, [ fileSrc, loadFile, + logEventNum, ]); return children; diff --git a/src/components/Editor/index.tsx b/src/components/Editor/index.tsx index 0da3cc519..21085c3f8 100644 --- a/src/components/Editor/index.tsx +++ b/src/components/Editor/index.tsx @@ -135,7 +135,7 @@ const Editor = () => { const {mode, systemMode} = useColorScheme(); const beginLineNumToLogEventNum = useViewStore((state) => state.beginLineNumToLogEventNum); - const isPrettified = useViewStore((state) => state.isPrettified); + const {isPrettified} = useViewStore.getState(); const updateIsPrettified = useViewStore((state) => state.updateIsPrettified); const logData = useViewStore((state) => state.logData); @@ -150,7 +150,6 @@ const Editor = () => { beginLineNumToLogEventNum ); const editorRef = useRef>(null); - const isPrettifiedRef = useRef(isPrettified); const isMouseDownRef = useRef(false); const pageSizeRef = useRef(getConfig(CONFIG_KEY.PAGE_SIZE)); @@ -180,7 +179,7 @@ const Editor = () => { handleCopyLogEventAction(editor, beginLineNumToLogEventNumRef.current); break; case ACTION_NAME.TOGGLE_PRETTIFY: { - const newIsPrettified = false === isPrettifiedRef.current; + const newIsPrettified = false === isPrettified; updateWindowUrlHashParams({ [HASH_PARAM_NAMES.IS_PRETTIFIED]: newIsPrettified, }); @@ -194,6 +193,7 @@ const Editor = () => { break; } }, [ + isPrettified, loadPageByAction, updateIsPrettified, ]); @@ -272,11 +272,6 @@ const Editor = () => { beginLineNumToLogEventNumRef.current = beginLineNumToLogEventNum; }, [beginLineNumToLogEventNum]); - // Synchronize `isPrettifiedRef` with `isPrettified`. - useEffect(() => { - isPrettifiedRef.current = isPrettified; - }, [isPrettified]); - // On `logEventNum` update, update line number in the editor. useEffect(() => { if (null === editorRef.current || isMouseDownRef.current) { From f4cc127581dfab6c2fecdfdf5435d0bb42573f8b Mon Sep 17 00:00:00 2001 From: Henry8192 <50559854+Henry8192@users.noreply.github.com> Date: Mon, 26 May 2025 08:13:45 -0700 Subject: [PATCH 14/25] refactor: Rename uiStoreState type to UiStoreState to follow PascalCase naming convention (fixes #291). (#302) --- src/stores/uiStore.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/stores/uiStore.ts b/src/stores/uiStore.ts index 8d27d1b88..150f7c864 100644 --- a/src/stores/uiStore.ts +++ b/src/stores/uiStore.ts @@ -16,14 +16,14 @@ interface UiStoreActions { setUiState: (newUIState: UI_STATE) => void; } -type uiStoreState = UiStoreValues & UiStoreActions; +type UiStoreState = UiStoreValues & UiStoreActions; const UI_STORE_DEFAULT: UiStoreValues = { activeTabName: getConfig(CONFIG_KEY.INITIAL_TAB_NAME), uiState: UI_STATE.UNOPENED, }; -const useUiStore = create((set) => ({ +const useUiStore = create((set) => ({ ...UI_STORE_DEFAULT, setActiveTabName: (tabName) => { set({activeTabName: tabName}); From 9879d0a3bbc29f5849d6ada4f0ee8154d9f339e4 Mon Sep 17 00:00:00 2001 From: Junhao Liao Date: Tue, 27 May 2025 02:49:29 -0400 Subject: [PATCH 15/25] docs: Add deployment instructions (resolves #217). (#236) Co-authored-by: Kirk Rodrigues <2454684+kirkrodrigues@users.noreply.github.com> --- docs/src/dev-guide/building-deploying.md | 94 ++++++++++++++++++++++++ docs/src/dev-guide/index.md | 8 ++ 2 files changed, 102 insertions(+) create mode 100644 docs/src/dev-guide/building-deploying.md diff --git a/docs/src/dev-guide/building-deploying.md b/docs/src/dev-guide/building-deploying.md new file mode 100644 index 000000000..50c7fba52 --- /dev/null +++ b/docs/src/dev-guide/building-deploying.md @@ -0,0 +1,94 @@ +# Deploying a distribution + +To deploy a [built](building-getting-started.md) distribution of the log viewer, you'll need to do +the following: + +1. [Serve the files using a static file host](#static-file-hosting). +2. Apply any of the following optimizations: + * [Compression](#enabling-compression) + * [Configure a MIME type for WebAssembly files](#configuring-a-webassembly-mime-type) + +## Static file hosting + +You can deploy the built distribution (the `dist` directory) to any static hosting service such as: + +* [GitHub Pages][github-pages] + + :::{tip} + If you fork this repository and [enable GitHub Actions][enable-gh-actions] in your fork, every + push will trigger the [deployment workflow][gh-workflow-deploy-gh-pages] to deploy the site to + `https://.github.io/yscope-log-viewer/` using GitHub Pages. + ::: + +* [Netlify] +* [Cloudflare Pages][cloudflare-pages] +* [Vercel] +* Object storage with a CDN (e.g., [AWS S3 with CloudFront][cloudfront-hosting]) + +Alternatively, you can set up your own web server (e.g., [Apache HTTP Server][apache-httpd] or +[Nginx]). + +:::{tip} +We recommend serving the distribution (and by extension, any log files the user wishes to view) over +a secure connection. +::: + +If you encounter any issues with serving the distribution with a static file host, check out the +[troubleshooting](#troubleshooting) section below for potential solutions. + +### Troubleshooting + +1. If users of the deployed distribution want to load log files from a different origin than the + static file host, you'll need to ensure that the host serving the log files supports + [cross-origin resource sharing (CORS)][mdn-cors]. + + :::{note} + Sites served over `http://` and `https://` are considered different origins, even if the domain + is the same. + ::: + +2. If your static file host serves the log viewer over a secure connection, modern browsers won't + allow users to load log files over an insecure connection due to + [mixed content restrictions][mdn-mixed-content-restrictions]. So you'll need to ensure either: + * all log files to be loaded are served over secure connections; or + * the log viewer is served over an insecure connection (for security, this is not recommended). + +## Enabling compression + +To improve load times, you can reduce file transfer sizes by enabling compression for the files in +the distribution (`.css`, `.js`, `.html`, and `.wasm` files). You can enable compression by +configuring the static file host to support popular [content-encoding][mdn-content-encoding] methods +(e.g., `gzip`). + +You can verify that compression is working by inspecting the response headers for any of the static +assets. The `Content-Encoding` header should show a supported content encoding method (e.g., `br`, +`gzip`, or `zstd`). + +## Configuring a WebAssembly MIME type + +To avoid unnecessary downloads of WebAssembly (`.wasm`) files (see +[emscripten-core/emscripten#18468]), the server should be configured to serve such files using the +`application/wasm` MIME type. The following web server deployments use this MIME type by default: + +* The [Apache HTTP Server][apache-httpd] on Debian-based systems (e.g., Debian v10+ or Ubuntu + v20.04+) + * This deployment relies on the system's `/etc/mime.types` file which is included in the + media-types v3.62 package). +* [Nginx] v1.21.0+. + +If your web server is not one of the above, ensure it is configured to use the aforementioned MIME +type. + +[apache-httpd]: https://httpd.apache.org/ +[cloudflare-pages]: https://pages.cloudflare.com/ +[cloudfront-hosting]: https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/getting-started-cloudfront-overview.html +[emscripten-core/emscripten#18468]: https://github.com/emscripten-core/emscripten/issues/18468 +[enable-gh-actions]: https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/enabling-features-for-your-repository/managing-github-actions-settings-for-a-repository +[gh-workflow-deploy-gh-pages]: https://github.com/y-scope/yscope-log-viewer/blob/main/.github/workflows/release.yaml +[github-pages]: https://pages.github.com/ +[mdn-content-encoding]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Encoding +[mdn-cors]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS +[mdn-mixed-content-restrictions]: https://developer.mozilla.org/en-US/docs/Web/Security/Mixed_content +[Netlify]: https://www.netlify.com/ +[Nginx]: https://nginx.org/ +[Vercel]: https://vercel.com/ diff --git a/docs/src/dev-guide/index.md b/docs/src/dev-guide/index.md index 7622ec818..babf540ae 100644 --- a/docs/src/dev-guide/index.md +++ b/docs/src/dev-guide/index.md @@ -15,6 +15,13 @@ Building Docs about building the viewer. ::: +:::{grid-item-card} +:link: building-deploying +Deploying +^^^ +Docs about deploying the viewer. +::: + :::{grid-item-card} :link: contributing-getting-started Contributing @@ -35,6 +42,7 @@ Docs about the viewer's design. :hidden: building-getting-started +building-deploying optimization-guide ::: From 7159bf2e5b2f8c0ebb346f8ec7acba3072acf920 Mon Sep 17 00:00:00 2001 From: zzxthehappiest Date: Wed, 28 May 2025 21:20:26 +0800 Subject: [PATCH 16/25] Fix comments --- src/components/AppController.tsx | 79 +++++++++++++------------------- src/stores/logFileStore.ts | 4 -- 2 files changed, 32 insertions(+), 51 deletions(-) diff --git a/src/components/AppController.tsx b/src/components/AppController.tsx index d41743d7f..0f1595629 100644 --- a/src/components/AppController.tsx +++ b/src/components/AppController.tsx @@ -63,6 +63,22 @@ const updateUrlIfEventOnPage = ( return {isUpdated: true, nearestLogEventNum: nearestLogEventNum}; }; +/** + * Handle the hash parameters change. + */ +const handleHashChange = () => { + const {setLogEventNum, updateIsPrettified} = useViewStore.getState(); + const hashParams = getWindowUrlHashParams(); + + if (null !== hashParams.logEventNum) { + setLogEventNum(hashParams.logEventNum); + } + + if (null !== hashParams.isPrettified) { + updateIsPrettified(hashParams.isPrettified); + } +}; + interface AppControllerProps { children: React.ReactNode; } @@ -78,12 +94,9 @@ const AppController = ({children}: AppControllerProps) => { // States const beginLineNumToLogEventNum = useViewStore((state) => state.beginLineNumToLogEventNum); - const fileSrc = useLogFileStore((state) => state.fileSrc); - const loadFile = useLogFileStore((state) => state.loadFile); - const {setFileSrc} = useLogFileStore.getState(); + const {loadFile} = useLogFileStore.getState(); const {isPrettified} = useViewStore.getState(); - const updateIsPrettified = useViewStore((state) => state.updateIsPrettified); const {logFileManagerProxy} = useLogFileManagerStore.getState(); const numEvents = useLogFileStore((state) => state.numEvents); @@ -96,36 +109,28 @@ const AppController = ({children}: AppControllerProps) => { const {setUiState} = useUiStore.getState(); useEffect(() => { - const handleHashChange = () => { - const hashParams = getWindowUrlHashParams(); - - if (null !== hashParams.logEventNum) { - setLogEventNum(hashParams.logEventNum); - } - - if (null !== hashParams.isPrettified) { - updateIsPrettified(hashParams.isPrettified); - } - - // Also check search params to handle initial page load and maintain full URL state - const searchParams = getWindowUrlSearchParams(); - - if (null !== searchParams.filePath) { - setFileSrc(searchParams.filePath); - } - }; - handleHashChange(); - window.addEventListener("hashchange", handleHashChange); + // Handle initial page load and maintain full URL state + const searchParams = getWindowUrlSearchParams(); + if (URL_SEARCH_PARAMS_DEFAULT.filePath !== searchParams.filePath) { + let cursor: CursorType = {code: CURSOR_CODE.LAST_EVENT, args: null}; + if (URL_HASH_PARAMS_DEFAULT.logEventNum !== logEventNum) { + cursor = { + code: CURSOR_CODE.EVENT_NUM, + args: {eventNum: logEventNum}, + }; + } + loadFile(searchParams.filePath, cursor); + } + return () => { window.removeEventListener("hashchange", handleHashChange); }; }, [ - updateIsPrettified, - setFileSrc, - setLogEventNum, + loadFile, + logEventNum, ]); // On `logEventNum` update, clamp it then switch page if necessary or simply update the URL. @@ -171,26 +176,6 @@ const AppController = ({children}: AppControllerProps) => { updatePageData, ]); - // On `fileSrc` update, load file. - useEffect(() => { - if (URL_SEARCH_PARAMS_DEFAULT.filePath === fileSrc) { - return; - } - - let cursor: CursorType = {code: CURSOR_CODE.LAST_EVENT, args: null}; - if (URL_HASH_PARAMS_DEFAULT.logEventNum !== logEventNum) { - cursor = { - code: CURSOR_CODE.EVENT_NUM, - args: {eventNum: logEventNum}, - }; - } - loadFile(fileSrc, cursor); - }, [ - fileSrc, - loadFile, - logEventNum, - ]); - return children; }; diff --git a/src/stores/logFileStore.ts b/src/stores/logFileStore.ts index 5c3e69358..6adc4e387 100644 --- a/src/stores/logFileStore.ts +++ b/src/stores/logFileStore.ts @@ -39,7 +39,6 @@ interface LogFileValues { interface LogFileActions { setFileName: (newFileName: string) => void; - setFileSrc: (newFileSrc: Nullable) => void; setNumEvents: (newNumEvents: number) => void; setOnDiskFileSizeInBytes: (newOnDiskFileSizeInBytes: number) => void; @@ -156,9 +155,6 @@ const useLogFileStore = create((set, get) => ({ setFileName: (newFileName) => { set({fileName: newFileName}); }, - setFileSrc: (newFileSrc) => { - set({fileSrc: newFileSrc}); - }, setNumEvents: (newNumEvents) => { set({numEvents: newNumEvents}); }, From 23aa8b3ec38bfc396f94d6f85dd6a768fdda499e Mon Sep 17 00:00:00 2001 From: zzxthehappiest Date: Thu, 29 May 2025 21:02:09 +0800 Subject: [PATCH 17/25] Merge queryStore to zustandard (not test yet) --- src/components/AppController.tsx | 26 +--------------- .../SidebarTabs/SearchTabPanel/index.tsx | 13 +++++++- src/utils/url.ts | 31 ++++++++++++++++--- 3 files changed, 40 insertions(+), 30 deletions(-) diff --git a/src/components/AppController.tsx b/src/components/AppController.tsx index 849ed8304..68d09ec5f 100644 --- a/src/components/AppController.tsx +++ b/src/components/AppController.tsx @@ -92,10 +92,6 @@ interface AppControllerProps { * @return */ const AppController = ({children}: AppControllerProps) => { - const { - queryString, queryIsRegex, queryIsCaseSensitive, - } = useContext(UrlContext); - // States const beginLineNumToLogEventNum = useViewStore((state) => state.beginLineNumToLogEventNum); @@ -111,6 +107,7 @@ const AppController = ({children}: AppControllerProps) => { const updatePageData = useViewStore((state) => state.updatePageData); + const uiState = useUiStore((state) => state.uiState); const {setUiState} = useUiStore.getState(); useEffect(() => { @@ -181,34 +178,13 @@ const AppController = ({children}: AppControllerProps) => { updatePageData, ]); - // Synchronize `queryIsCaseSensitive` with the Zustand QueryStore. - useEffect(() => { - if (null !== queryIsCaseSensitive) { - const {setQueryIsCaseSensitive} = useQueryStore.getState(); - setQueryIsCaseSensitive(queryIsCaseSensitive); - } - }, [queryIsCaseSensitive]); - - // Synchronize `queryIsRegex` with the Zustand QueryStore. useEffect(() => { - if (null !== queryIsRegex) { - const {setQueryIsRegex} = useQueryStore.getState(); - setQueryIsRegex(queryIsRegex); - } - }, [queryIsRegex]); - - useEffect(() => { - if (null !== queryString) { - const {setQueryString} = useQueryStore.getState(); - setQueryString(queryString); - } if (UI_STATE.READY === uiState) { const {startQuery} = useQueryStore.getState(); startQuery(); } }, [ uiState, - queryString, ]); return children; diff --git a/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/index.tsx b/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/index.tsx index 58aeff83b..4a3427f13 100644 --- a/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/index.tsx +++ b/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/index.tsx @@ -12,12 +12,12 @@ import ShareIcon from "@mui/icons-material/Share"; import UnfoldLessIcon from "@mui/icons-material/UnfoldLess"; import UnfoldMoreIcon from "@mui/icons-material/UnfoldMore"; -import {copyPermalinkToClipboard} from "../../../../../contexts/UrlContextProvider"; import useQueryStore from "../../../../../stores/queryStore"; import { TAB_DISPLAY_NAMES, TAB_NAME, } from "../../../../../typings/tab"; +import {copyPermalinkToClipboard} from "../../../../../utils/url.ts"; import CustomTabPanel from "../CustomTabPanel"; import PanelTitleButton from "../PanelTitleButton"; import QueryInputBox from "./QueryInputBox"; @@ -37,6 +37,10 @@ const SearchTabPanel = () => { const queryString = useQueryStore((state) => state.queryString); const queryResults = useQueryStore((state) => state.queryResults); + const {setQueryIsCaseSensitive} = useQueryStore.getState(); + const {setQueryIsRegex} = useQueryStore.getState(); + const {setQueryString} = useQueryStore.getState(); + const [isAllExpanded, setIsAllExpanded] = useState(true); const handleCollapseAllButtonClick = useCallback(() => { @@ -44,6 +48,10 @@ const SearchTabPanel = () => { }, []); const handleShareButtonClick = useCallback(() => { + setQueryIsCaseSensitive(queryIsCaseSensitive); + setQueryIsRegex(queryIsRegex); + setQueryString(queryString); + copyPermalinkToClipboard({}, { logEventNum: null, queryString: "" === queryString ? @@ -56,6 +64,9 @@ const SearchTabPanel = () => { queryIsCaseSensitive, queryIsRegex, queryString, + setQueryIsCaseSensitive, + setQueryIsRegex, + setQueryString ]); return ( diff --git a/src/utils/url.ts b/src/utils/url.ts index e244d3e28..8a93b37a8 100644 --- a/src/utils/url.ts +++ b/src/utils/url.ts @@ -24,6 +24,9 @@ const URL_SEARCH_PARAMS_DEFAULT = Object.freeze({ const URL_HASH_PARAMS_DEFAULT = Object.freeze({ [HASH_PARAM_NAMES.IS_PRETTIFIED]: false, [HASH_PARAM_NAMES.LOG_EVENT_NUM]: 0, + [HASH_PARAM_NAMES.QUERY_IS_CASE_SENSITIVE]: false, + [HASH_PARAM_NAMES.QUERY_IS_REGEX]: false, + [HASH_PARAM_NAMES.QUERY_STRING]: null, }); /** @@ -62,6 +65,11 @@ const getWindowUrlHashParams = () => { structuredClone(URL_HASH_PARAMS_DEFAULT); const hashParams = new URLSearchParams(window.location.hash.substring(1)); + const isPrettified = hashParams.get(HASH_PARAM_NAMES.IS_PRETTIFIED); + if (null !== isPrettified) { + urlHashParams[HASH_PARAM_NAMES.IS_PRETTIFIED] = "true" === isPrettified; + } + const logEventNum = hashParams.get(HASH_PARAM_NAMES.LOG_EVENT_NUM); if (null !== logEventNum) { const parsed = Number(logEventNum); @@ -70,9 +78,20 @@ const getWindowUrlHashParams = () => { parsed; } - const isPrettified = hashParams.get(HASH_PARAM_NAMES.IS_PRETTIFIED); - if (null !== isPrettified) { - urlHashParams[HASH_PARAM_NAMES.IS_PRETTIFIED] = "true" === isPrettified; + const queryString = hashParams.get(HASH_PARAM_NAMES.QUERY_STRING); + if (null !== queryString) { + urlHashParams[HASH_PARAM_NAMES.QUERY_STRING] = queryString; + } + + const queryIsCaseSensitive = hashParams.get(HASH_PARAM_NAMES.QUERY_IS_CASE_SENSITIVE); + if (null !== queryIsCaseSensitive) { + urlHashParams[HASH_PARAM_NAMES.QUERY_IS_CASE_SENSITIVE] = + "true" === queryIsCaseSensitive; + } + + const queryIsRegex = hashParams.get(HASH_PARAM_NAMES.QUERY_IS_REGEX); + if (null !== queryIsRegex) { + urlHashParams[HASH_PARAM_NAMES.QUERY_IS_REGEX] = "true" === queryIsRegex; } return urlHashParams; @@ -89,6 +108,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)) { // Extract filePath value by finding the parameter and taking everything after it const filePathIndex = window.location.search.indexOf("filePath="); @@ -176,7 +199,7 @@ const getUpdatedHashParams = (updates: UrlHashParamUpdatesType) => { const newHashParams = new URLSearchParams(filterNullValuesToStrings(currentParams)); 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)); From d204c9b037f7ce5d0db9ad0ceee585e0ec6ba801 Mon Sep 17 00:00:00 2001 From: zzxthehappiest Date: Thu, 29 May 2025 21:04:41 +0800 Subject: [PATCH 18/25] Fix lint --- .../Sidebar/SidebarTabs/SearchTabPanel/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/index.tsx b/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/index.tsx index 4a3427f13..57f231b54 100644 --- a/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/index.tsx +++ b/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/index.tsx @@ -66,7 +66,7 @@ const SearchTabPanel = () => { queryString, setQueryIsCaseSensitive, setQueryIsRegex, - setQueryString + setQueryString, ]); return ( From a464dc9b745f9b1bcc9ca4bb977b9ea6f0fdceec Mon Sep 17 00:00:00 2001 From: zzxthehappiest Date: Fri, 30 May 2025 21:17:39 +0800 Subject: [PATCH 19/25] Finish the rest --- src/components/AppController.tsx | 28 ++++++++++------------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/src/components/AppController.tsx b/src/components/AppController.tsx index 68d09ec5f..51ee6d58c 100644 --- a/src/components/AppController.tsx +++ b/src/components/AppController.tsx @@ -95,22 +95,16 @@ const AppController = ({children}: AppControllerProps) => { // States const beginLineNumToLogEventNum = useViewStore((state) => state.beginLineNumToLogEventNum); - const {loadFile} = useLogFileStore.getState(); - - const {isPrettified} = useViewStore.getState(); - - const {logFileManagerProxy} = useLogFileManagerStore.getState(); const numEvents = useLogFileStore((state) => state.numEvents); - const {logEventNum} = useViewStore.getState(); - const {setLogEventNum} = useViewStore.getState(); - const updatePageData = useViewStore((state) => state.updatePageData); const uiState = useUiStore((state) => state.uiState); - const {setUiState} = useUiStore.getState(); useEffect(() => { + const {loadFile} = useLogFileStore.getState(); + const {logEventNum} = useViewStore.getState(); + handleHashChange(); window.addEventListener("hashchange", handleHashChange); @@ -130,13 +124,16 @@ const AppController = ({children}: AppControllerProps) => { return () => { window.removeEventListener("hashchange", handleHashChange); }; - }, [ - loadFile, - logEventNum, - ]); + }, []); // On `logEventNum` update, clamp it then switch page if necessary or simply update the URL. useEffect(() => { + const {isPrettified} = useViewStore.getState(); + const {logEventNum} = useViewStore.getState(); + const {logFileManagerProxy} = useLogFileManagerStore.getState(); + const {setLogEventNum} = useViewStore.getState(); + const {setUiState} = useUiStore.getState(); + if (0 === numEvents || URL_HASH_PARAMS_DEFAULT.logEventNum === logEventNum) { return; } @@ -169,12 +166,7 @@ const AppController = ({children}: AppControllerProps) => { })().catch(handleErrorWithNotification); }, [ beginLineNumToLogEventNum, - isPrettified, - logEventNum, - logFileManagerProxy, numEvents, - setLogEventNum, - setUiState, updatePageData, ]); From 0dabdfcf65f0debdf2c4dcc73aeb2689a100256b Mon Sep 17 00:00:00 2001 From: zzxthehappiest Date: Sun, 1 Jun 2025 21:27:55 +0800 Subject: [PATCH 20/25] Fix #306 --- src/components/AppController.tsx | 24 +++++++++++++++---- .../SearchTabPanel/QueryInputBox.tsx | 13 +++++++--- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/src/components/AppController.tsx b/src/components/AppController.tsx index 51ee6d58c..deb0c0384 100644 --- a/src/components/AppController.tsx +++ b/src/components/AppController.tsx @@ -68,7 +68,12 @@ const updateUrlIfEventOnPage = ( * Handle the hash parameters change. */ const handleHashChange = () => { - const {setLogEventNum, updateIsPrettified} = useViewStore.getState(); + const {setLogEventNum} = useViewStore.getState(); + const {setQueryIsCaseSensitive} = useQueryStore.getState(); + const {setQueryIsRegex} = useQueryStore.getState(); + const {setQueryString} = useQueryStore.getState(); + const {updateIsPrettified} = useViewStore.getState(); + const hashParams = getWindowUrlHashParams(); if (null !== hashParams.logEventNum) { @@ -78,6 +83,18 @@ const handleHashChange = () => { if (null !== hashParams.isPrettified) { updateIsPrettified(hashParams.isPrettified); } + + if (null !== hashParams.queryIsCaseSensitive) { + setQueryIsCaseSensitive(hashParams.queryIsCaseSensitive); + } + + if (null !== hashParams.queryIsRegex) { + setQueryIsRegex(hashParams.queryIsRegex); + } + + if (null !== hashParams.queryString) { + setQueryString(hashParams.queryString); + } }; interface AppControllerProps { @@ -94,11 +111,9 @@ interface AppControllerProps { const AppController = ({children}: AppControllerProps) => { // States const beginLineNumToLogEventNum = useViewStore((state) => state.beginLineNumToLogEventNum); - const numEvents = useLogFileStore((state) => state.numEvents); - + const queryString = useQueryStore((state) => state.queryString); const updatePageData = useViewStore((state) => state.updatePageData); - const uiState = useUiStore((state) => state.uiState); useEffect(() => { @@ -176,6 +191,7 @@ const AppController = ({children}: AppControllerProps) => { startQuery(); } }, [ + queryString, uiState, ]); diff --git a/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/QueryInputBox.tsx b/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/QueryInputBox.tsx index 27ba08ac0..266eee4c8 100644 --- a/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/QueryInputBox.tsx +++ b/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/QueryInputBox.tsx @@ -11,6 +11,7 @@ import useUiStore from "../../../../../stores/uiStore"; import {QUERY_PROGRESS_VALUE_MAX} from "../../../../../typings/query"; import {UI_ELEMENT} from "../../../../../typings/states"; import {isDisabled} from "../../../../../utils/states"; +import {updateWindowUrlHashParams} from "../../../../../utils/url.ts"; import ToggleIconButton from "./ToggleIconButton"; import "./QueryInputBox.css"; @@ -33,17 +34,23 @@ const QueryInputBox = () => { const uiState = useUiStore((state) => state.uiState); const handleQueryInputChange = (ev: React.ChangeEvent) => { - setQueryString(ev.target.value); + const newQueryString = ev.target.value; + updateWindowUrlHashParams({queryString: newQueryString}); + setQueryString(newQueryString); startQuery(); }; const handleCaseSensitivityButtonClick = () => { - setQueryIsCaseSensitive(!isCaseSensitive); + const newQueryIsSensitive = !isCaseSensitive; + updateWindowUrlHashParams({queryIsCaseSensitive: newQueryIsSensitive}); + setQueryIsCaseSensitive(newQueryIsSensitive); startQuery(); }; const handleRegexButtonClick = () => { - setQueryIsRegex(!isRegex); + const newQueryIsRegex = !isRegex; + updateWindowUrlHashParams({queryIsRegex: newQueryIsRegex}); + setQueryIsRegex(newQueryIsRegex); startQuery(); }; From 3add41d1e3a3d293c6fc55c2ef1e6a0e91cd0bd0 Mon Sep 17 00:00:00 2001 From: zzxthehappiest Date: Sun, 1 Jun 2025 21:43:39 +0800 Subject: [PATCH 21/25] Fix coderabbit comments --- src/components/AppController.tsx | 2 +- .../SidebarTabs/SearchTabPanel/QueryInputBox.tsx | 2 +- .../Sidebar/SidebarTabs/SearchTabPanel/index.tsx | 11 ++++------- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/components/AppController.tsx b/src/components/AppController.tsx index deb0c0384..d7e12365e 100644 --- a/src/components/AppController.tsx +++ b/src/components/AppController.tsx @@ -39,7 +39,7 @@ const updateUrlIfEventOnPage = ( logEventNumsOnPage: number[] ): {isUpdated: boolean; nearestLogEventNum: number} => { if (false === isWithinBounds(logEventNumsOnPage, logEventNum)) { - return {isUpdated: false, nearestLogEventNum: 0}; + return {isUpdated: false, nearestLogEventNum: URL_HASH_PARAMS_DEFAULT.logEventNum}; } const nearestIdx = findNearestLessThanOrEqualElement( diff --git a/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/QueryInputBox.tsx b/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/QueryInputBox.tsx index 266eee4c8..741505ab0 100644 --- a/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/QueryInputBox.tsx +++ b/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/QueryInputBox.tsx @@ -11,7 +11,7 @@ import useUiStore from "../../../../../stores/uiStore"; import {QUERY_PROGRESS_VALUE_MAX} from "../../../../../typings/query"; import {UI_ELEMENT} from "../../../../../typings/states"; import {isDisabled} from "../../../../../utils/states"; -import {updateWindowUrlHashParams} from "../../../../../utils/url.ts"; +import {updateWindowUrlHashParams} from "../../../../../utils/url"; import ToggleIconButton from "./ToggleIconButton"; import "./QueryInputBox.css"; diff --git a/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/index.tsx b/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/index.tsx index 57f231b54..3a7f8209d 100644 --- a/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/index.tsx +++ b/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/index.tsx @@ -37,10 +37,6 @@ const SearchTabPanel = () => { const queryString = useQueryStore((state) => state.queryString); const queryResults = useQueryStore((state) => state.queryResults); - const {setQueryIsCaseSensitive} = useQueryStore.getState(); - const {setQueryIsRegex} = useQueryStore.getState(); - const {setQueryString} = useQueryStore.getState(); - const [isAllExpanded, setIsAllExpanded] = useState(true); const handleCollapseAllButtonClick = useCallback(() => { @@ -48,6 +44,10 @@ const SearchTabPanel = () => { }, []); const handleShareButtonClick = useCallback(() => { + const {setQueryIsCaseSensitive} = useQueryStore.getState(); + const {setQueryIsRegex} = useQueryStore.getState(); + const {setQueryString} = useQueryStore.getState(); + setQueryIsCaseSensitive(queryIsCaseSensitive); setQueryIsRegex(queryIsRegex); setQueryString(queryString); @@ -64,9 +64,6 @@ const SearchTabPanel = () => { queryIsCaseSensitive, queryIsRegex, queryString, - setQueryIsCaseSensitive, - setQueryIsRegex, - setQueryString, ]); return ( From 7166d2bb8d6bd63fba4ebf8ee0e4f7b42fee7cf5 Mon Sep 17 00:00:00 2001 From: zzxthehappiest Date: Wed, 4 Jun 2025 21:33:19 +0800 Subject: [PATCH 22/25] Fix comments except: Remove null in URL params --- src/components/AppController.tsx | 170 ++++++++++-------- .../SearchTabPanel/QueryInputBox.tsx | 21 ++- .../SidebarTabs/SearchTabPanel/Result.tsx | 12 +- .../SidebarTabs/SearchTabPanel/index.tsx | 24 ++- src/components/Editor/index.tsx | 107 +++++------ src/components/StatusBar/index.tsx | 6 +- src/stores/logFileStore.ts | 2 + src/stores/viewStore.ts | 5 +- src/typings/url.ts | 7 - src/utils/js.ts | 17 -- src/utils/url.ts | 155 ++++++++-------- 11 files changed, 268 insertions(+), 258 deletions(-) diff --git a/src/components/AppController.tsx b/src/components/AppController.tsx index d7e12365e..348277cdc 100644 --- a/src/components/AppController.tsx +++ b/src/components/AppController.tsx @@ -1,5 +1,7 @@ -/* eslint max-statements: ["error", 30] */ -import React, {useEffect} from "react"; +import React, { + useEffect, + useRef, +} from "react"; import useLogFileManagerStore from "../stores/logFileManagerProxyStore"; import useLogFileStore from "../stores/logFileStore"; @@ -7,7 +9,12 @@ import {handleErrorWithNotification} from "../stores/notificationStore"; import useQueryStore from "../stores/queryStore"; import useUiStore from "../stores/uiStore"; import useViewStore from "../stores/viewStore"; +import { + Nullable, + NullableProperties, +} from "../typings/common.ts"; import {UI_STATE} from "../typings/states"; +import {UrlHashParams} from "../typings/url"; import { CURSOR_CODE, CursorType, @@ -23,7 +30,7 @@ import { updateWindowUrlHashParams, URL_HASH_PARAMS_DEFAULT, URL_SEARCH_PARAMS_DEFAULT, -} from "../utils/url.ts"; +} from "../utils/url"; /** @@ -37,9 +44,9 @@ import { const updateUrlIfEventOnPage = ( logEventNum: number, logEventNumsOnPage: number[] -): {isUpdated: boolean; nearestLogEventNum: number} => { +): boolean => { if (false === isWithinBounds(logEventNumsOnPage, logEventNum)) { - return {isUpdated: false, nearestLogEventNum: URL_HASH_PARAMS_DEFAULT.logEventNum}; + return false; } const nearestIdx = findNearestLessThanOrEqualElement( @@ -61,40 +68,76 @@ const updateUrlIfEventOnPage = ( logEventNum: nearestLogEventNum, }); - return {isUpdated: true, nearestLogEventNum: nearestLogEventNum}; + return true; }; /** - * Handle the hash parameters change. + * Updates view-related parameters from URL hash. + * + * @param hashParams */ -const handleHashChange = () => { - const {setLogEventNum} = useViewStore.getState(); - const {setQueryIsCaseSensitive} = useQueryStore.getState(); - const {setQueryIsRegex} = useQueryStore.getState(); - const {setQueryString} = useQueryStore.getState(); - const {updateIsPrettified} = useViewStore.getState(); - - const hashParams = getWindowUrlHashParams(); - - if (null !== hashParams.logEventNum) { - setLogEventNum(hashParams.logEventNum); +const updateViewHashParams = (hashParams: NullableProperties): void => { + const {isPrettified, logEventNum} = hashParams; + const {updateIsPrettified, setLogEventNum} = useViewStore.getState(); + if (null !== isPrettified && URL_HASH_PARAMS_DEFAULT.isPrettified !== isPrettified) { + updateIsPrettified(isPrettified); } - - if (null !== hashParams.isPrettified) { - updateIsPrettified(hashParams.isPrettified); + if (null !== logEventNum && URL_HASH_PARAMS_DEFAULT.logEventNum !== logEventNum) { + setLogEventNum(logEventNum); } +}; - if (null !== hashParams.queryIsCaseSensitive) { - setQueryIsCaseSensitive(hashParams.queryIsCaseSensitive); +/** + * Updates query-related parameters from URL hash. + * + * @param hashParams + * @return Whether any query parameters were modified. + */ +const updateQueryHashParams = (hashParams: NullableProperties): boolean => { + const {queryIsCaseSensitive, queryIsRegex, queryString} = hashParams; + const { + queryIsCaseSensitive: currentQueryIsCaseSensitive, + queryIsRegex: currentQueryIsRegex, + queryString: currentQueryString, + setQueryIsCaseSensitive, + setQueryIsRegex, + setQueryString, + } = useQueryStore.getState(); + let isQueryModified = false; + if (null !== queryIsCaseSensitive && + URL_HASH_PARAMS_DEFAULT.queryIsCaseSensitive !== queryIsCaseSensitive) { + isQueryModified ||= queryIsCaseSensitive !== currentQueryIsCaseSensitive; + setQueryIsCaseSensitive(queryIsCaseSensitive); } - - if (null !== hashParams.queryIsRegex) { - setQueryIsRegex(hashParams.queryIsRegex); + if (null !== queryIsRegex && URL_HASH_PARAMS_DEFAULT.queryIsRegex !== queryIsRegex) { + isQueryModified ||= queryIsRegex !== currentQueryIsRegex; + setQueryIsRegex(queryIsRegex); } + if (null !== queryString && URL_HASH_PARAMS_DEFAULT.queryString !== queryString) { + isQueryModified ||= queryString !== currentQueryString; + setQueryString(queryString); + } + + return isQueryModified; +}; - if (null !== hashParams.queryString) { - setQueryString(hashParams.queryString); +/** + * Handles hash change events by updating the application state based on the URL hash parameters. + * + * @param [ev] The hash change event, or `null` when called on application initialization. + * @return The parsed URL hash parameters. + */ +const handleHashChange = (ev: Nullable): NullableProperties => { + const hashParams = getWindowUrlHashParams(); + updateViewHashParams(hashParams); + const isTriggeredByHashChange = null !== ev; + const isQueryModified = updateQueryHashParams(hashParams); + if (isTriggeredByHashChange && isQueryModified) { + const {startQuery} = useQueryStore.getState(); + startQuery(); } + + return hashParams; }; interface AppControllerProps { @@ -110,29 +153,34 @@ interface AppControllerProps { */ const AppController = ({children}: AppControllerProps) => { // States - const beginLineNumToLogEventNum = useViewStore((state) => state.beginLineNumToLogEventNum); - const numEvents = useLogFileStore((state) => state.numEvents); - const queryString = useQueryStore((state) => state.queryString); - const updatePageData = useViewStore((state) => state.updatePageData); - const uiState = useUiStore((state) => state.uiState); + const logEventNum = useViewStore((state) => state.logEventNum); - useEffect(() => { - const {loadFile} = useLogFileStore.getState(); - const {logEventNum} = useViewStore.getState(); + // Refs + const isInitialized = useRef(false); - handleHashChange(); + // On app init, register hash change handler, and handle hash and search parameters. + useEffect(() => { window.addEventListener("hashchange", handleHashChange); + // Prevent re-initialization on re-renders. + if (isInitialized.current) { + return () => null; + } + isInitialized.current = true; + // Handle initial page load and maintain full URL state + const hashParams = handleHashChange(null); const searchParams = getWindowUrlSearchParams(); if (URL_SEARCH_PARAMS_DEFAULT.filePath !== searchParams.filePath) { let cursor: CursorType = {code: CURSOR_CODE.LAST_EVENT, args: null}; - if (URL_HASH_PARAMS_DEFAULT.logEventNum !== logEventNum) { + if (null !== hashParams.logEventNum && + URL_HASH_PARAMS_DEFAULT.logEventNum !== hashParams.logEventNum) { cursor = { code: CURSOR_CODE.EVENT_NUM, - args: {eventNum: logEventNum}, + args: {eventNum: hashParams.logEventNum}, }; } + const {loadFile} = useLogFileStore.getState(); loadFile(searchParams.filePath, cursor); } @@ -143,57 +191,35 @@ const AppController = ({children}: AppControllerProps) => { // On `logEventNum` update, clamp it then switch page if necessary or simply update the URL. useEffect(() => { - const {isPrettified} = useViewStore.getState(); - const {logEventNum} = useViewStore.getState(); - const {logFileManagerProxy} = useLogFileManagerStore.getState(); - const {setLogEventNum} = useViewStore.getState(); - const {setUiState} = useUiStore.getState(); - + const {numEvents} = useLogFileStore.getState(); if (0 === numEvents || URL_HASH_PARAMS_DEFAULT.logEventNum === logEventNum) { return; } const clampedLogEventNum = clamp(logEventNum, 1, numEvents); - const logEventNumsOnPage: number [] = - Array.from(beginLineNumToLogEventNum.values()); - - const { - isUpdated, - nearestLogEventNum, - } = updateUrlIfEventOnPage(clampedLogEventNum, logEventNumsOnPage); - - if (isUpdated) { + const {beginLineNumToLogEventNum} = useViewStore.getState(); + const logEventNumsOnPage: number [] = Array.from(beginLineNumToLogEventNum.values()); + if (updateUrlIfEventOnPage(clampedLogEventNum, logEventNumsOnPage)) { // No need to request a new page since the log event is on the current page. - setLogEventNum(nearestLogEventNum); - return; } + // If the log event is not on the current page, request a new page. + const {setUiState} = useUiStore.getState(); setUiState(UI_STATE.FAST_LOADING); - (async () => { + const {logFileManagerProxy} = useLogFileManagerStore.getState(); const cursor: CursorType = { code: CURSOR_CODE.EVENT_NUM, args: {eventNum: clampedLogEventNum}, }; + const {isPrettified} = useViewStore.getState(); + const pageData = await logFileManagerProxy.loadPage(cursor, isPrettified); + const {updatePageData} = useViewStore.getState(); updatePageData(pageData); })().catch(handleErrorWithNotification); - }, [ - beginLineNumToLogEventNum, - numEvents, - updatePageData, - ]); - - useEffect(() => { - if (UI_STATE.READY === uiState) { - const {startQuery} = useQueryStore.getState(); - startQuery(); - } - }, [ - queryString, - uiState, - ]); + }, [logEventNum]); return children; }; diff --git a/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/QueryInputBox.tsx b/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/QueryInputBox.tsx index 741505ab0..a074b79e9 100644 --- a/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/QueryInputBox.tsx +++ b/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/QueryInputBox.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, {useCallback} from "react"; import { LinearProgress, @@ -26,33 +26,32 @@ const QueryInputBox = () => { const isCaseSensitive = useQueryStore((state) => state.queryIsCaseSensitive); const isRegex = useQueryStore((state) => state.queryIsRegex); const querystring = useQueryStore((state) => state.queryString); - const setQueryIsCaseSensitive = useQueryStore((state) => state.setQueryIsCaseSensitive); - const setQueryIsRegex = useQueryStore((state) => state.setQueryIsRegex); - const setQueryString = useQueryStore((state) => state.setQueryString); const queryProgress = useQueryStore((state) => state.queryProgress); - const startQuery = useQueryStore((state) => state.startQuery); const uiState = useUiStore((state) => state.uiState); - const handleQueryInputChange = (ev: React.ChangeEvent) => { + const handleQueryInputChange = useCallback((ev: React.ChangeEvent) => { const newQueryString = ev.target.value; updateWindowUrlHashParams({queryString: newQueryString}); + const {setQueryString, startQuery} = useQueryStore.getState(); setQueryString(newQueryString); startQuery(); - }; + }, []); - const handleCaseSensitivityButtonClick = () => { + const handleCaseSensitivityButtonClick = useCallback(() => { const newQueryIsSensitive = !isCaseSensitive; updateWindowUrlHashParams({queryIsCaseSensitive: newQueryIsSensitive}); + const {setQueryIsCaseSensitive, startQuery} = useQueryStore.getState(); setQueryIsCaseSensitive(newQueryIsSensitive); startQuery(); - }; + }, [isCaseSensitive]); - const handleRegexButtonClick = () => { + const handleRegexButtonClick = useCallback(() => { const newQueryIsRegex = !isRegex; updateWindowUrlHashParams({queryIsRegex: newQueryIsRegex}); + const {setQueryIsRegex, startQuery} = useQueryStore.getState(); setQueryIsRegex(newQueryIsRegex); startQuery(); - }; + }, [isRegex]); const isQueryInputBoxDisabled = isDisabled(uiState, UI_ELEMENT.QUERY_INPUT_BOX); diff --git a/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/Result.tsx b/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/Result.tsx index a70c4ec67..2cefbe7eb 100644 --- a/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/Result.tsx +++ b/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/Result.tsx @@ -5,8 +5,8 @@ import { Typography, } from "@mui/joy"; -import useViewStore from "../../../../../stores/viewStore.ts"; -import {updateWindowUrlHashParams} from "../../../../../utils/url.ts"; +import useViewStore from "../../../../../stores/viewStore"; +import {updateWindowUrlHashParams} from "../../../../../utils/url"; import "./Result.css"; @@ -39,14 +39,12 @@ const Result = ({logEventNum, message, matchRange}: ResultProps) => { message.slice(...matchRange), message.slice(matchRange[1]), ]; - const {setLogEventNum} = useViewStore.getState(); + const handleResultButtonClick = useCallback(() => { updateWindowUrlHashParams({logEventNum}); + const {setLogEventNum} = useViewStore.getState(); setLogEventNum(logEventNum); - }, [ - logEventNum, - setLogEventNum, - ]); + }, [logEventNum]); return ( { - const queryIsCaseSensitive = useQueryStore((state) => state.queryIsCaseSensitive); - const queryIsRegex = useQueryStore((state) => state.queryIsRegex); - const queryString = useQueryStore((state) => state.queryString); const queryResults = useQueryStore((state) => state.queryResults); const [isAllExpanded, setIsAllExpanded] = useState(true); @@ -44,9 +41,14 @@ const SearchTabPanel = () => { }, []); const handleShareButtonClick = useCallback(() => { - const {setQueryIsCaseSensitive} = useQueryStore.getState(); - const {setQueryIsRegex} = useQueryStore.getState(); - const {setQueryString} = useQueryStore.getState(); + const { + queryIsCaseSensitive, + queryIsRegex, + queryString, + setQueryIsCaseSensitive, + setQueryIsRegex, + setQueryString, + } = useQueryStore.getState(); setQueryIsCaseSensitive(queryIsCaseSensitive); setQueryIsRegex(queryIsRegex); @@ -54,17 +56,11 @@ const SearchTabPanel = () => { copyPermalinkToClipboard({}, { logEventNum: null, - queryString: "" === queryString ? - null : - queryString, + queryString: queryString, queryIsCaseSensitive: queryIsCaseSensitive, queryIsRegex: queryIsRegex, }); - }, [ - queryIsCaseSensitive, - queryIsRegex, - queryString, - ]); + }, []); return ( { + switch (actionName) { + case ACTION_NAME.FIRST_PAGE: + case ACTION_NAME.PREV_PAGE: + case ACTION_NAME.NEXT_PAGE: + case ACTION_NAME.LAST_PAGE: { + const {loadPageByAction} = useViewStore.getState(); + loadPageByAction({code: actionName, args: null}); + break; + } + case ACTION_NAME.PAGE_TOP: + goToPositionAndCenter(editor, {lineNumber: 1, column: 1}); + break; + case ACTION_NAME.PAGE_BOTTOM: { + const lineCount = editor.getModel()?.getLineCount(); + if ("undefined" === typeof lineCount) { + break; + } + goToPositionAndCenter(editor, {lineNumber: lineCount, column: 1}); + break; + } + case ACTION_NAME.COPY_LOG_EVENT: { + const {beginLineNumToLogEventNum} = useViewStore.getState(); + handleCopyLogEventAction(editor, beginLineNumToLogEventNum); + break; + } + case ACTION_NAME.TOGGLE_PRETTIFY: { + const {isPrettified, updateIsPrettified} = useViewStore.getState(); + const newIsPrettified = !isPrettified; + updateWindowUrlHashParams({ + [HASH_PARAM_NAMES.IS_PRETTIFIED]: newIsPrettified, + }); + updateIsPrettified(newIsPrettified); + break; + } + case ACTION_NAME.TOGGLE_WORD_WRAP: + handleToggleWordWrapAction(editor); + break; + default: + break; + } +}; + /** * Renders a read-only editor for viewing logs. * @@ -135,15 +185,10 @@ const Editor = () => { const {mode, systemMode} = useColorScheme(); const beginLineNumToLogEventNum = useViewStore((state) => state.beginLineNumToLogEventNum); - const {isPrettified} = useViewStore.getState(); - const updateIsPrettified = useViewStore((state) => state.updateIsPrettified); const logData = useViewStore((state) => state.logData); const logEventNum = useViewStore((state) => state.logEventNum); - const {setLogEventNum} = useViewStore.getState(); - - const loadPageByAction = useViewStore((state) => state.loadPageByAction); const [lineNum, setLineNum] = useState(1); const beginLineNumToLogEventNumRef = useRef( @@ -153,51 +198,6 @@ const Editor = () => { const isMouseDownRef = useRef(false); const pageSizeRef = useRef(getConfig(CONFIG_KEY.PAGE_SIZE)); - const handleEditorCustomAction = useCallback(( - editor: monaco.editor.IStandaloneCodeEditor, - actionName: ACTION_NAME - ) => { - switch (actionName) { - case ACTION_NAME.FIRST_PAGE: - case ACTION_NAME.PREV_PAGE: - case ACTION_NAME.NEXT_PAGE: - case ACTION_NAME.LAST_PAGE: - loadPageByAction({code: actionName, args: null}); - break; - case ACTION_NAME.PAGE_TOP: - goToPositionAndCenter(editor, {lineNumber: 1, column: 1}); - break; - case ACTION_NAME.PAGE_BOTTOM: { - const lineCount = editor.getModel()?.getLineCount(); - if ("undefined" === typeof lineCount) { - break; - } - goToPositionAndCenter(editor, {lineNumber: lineCount, column: 1}); - break; - } - case ACTION_NAME.COPY_LOG_EVENT: - handleCopyLogEventAction(editor, beginLineNumToLogEventNumRef.current); - break; - case ACTION_NAME.TOGGLE_PRETTIFY: { - const newIsPrettified = false === isPrettified; - updateWindowUrlHashParams({ - [HASH_PARAM_NAMES.IS_PRETTIFIED]: newIsPrettified, - }); - updateIsPrettified(newIsPrettified); - break; - } - case ACTION_NAME.TOGGLE_WORD_WRAP: - handleToggleWordWrapAction(editor); - break; - default: - break; - } - }, [ - isPrettified, - loadPageByAction, - updateIsPrettified, - ]); - /** * Sets `editorRef` and configures callbacks for mouse down detection. */ @@ -264,8 +264,9 @@ const Editor = () => { return; } updateWindowUrlHashParams({logEventNum: newLogEventNum}); + const {setLogEventNum} = useViewStore.getState(); setLogEventNum(newLogEventNum); - }, [setLogEventNum]); + }, []); // Synchronize `beginLineNumToLogEventNumRef` with `beginLineNumToLogEventNum`. useEffect(() => { diff --git a/src/components/StatusBar/index.tsx b/src/components/StatusBar/index.tsx index 995904b3b..ffb06be7f 100644 --- a/src/components/StatusBar/index.tsx +++ b/src/components/StatusBar/index.tsx @@ -1,3 +1,5 @@ +import React from "react"; + import { Button, Sheet, @@ -10,7 +12,7 @@ import AutoFixOffRoundedIcon from "@mui/icons-material/AutoFixOffRounded"; import useLogFileStore from "../../stores/logFileStore"; import useUiStore from "../../stores/uiStore"; -import useViewStore from "../../stores/viewStore.ts"; +import useViewStore from "../../stores/viewStore"; import {UI_ELEMENT} from "../../typings/states"; import {HASH_PARAM_NAMES} from "../../typings/url"; import {ACTION_NAME} from "../../utils/actions"; @@ -18,7 +20,7 @@ import {isDisabled} from "../../utils/states"; import { copyPermalinkToClipboard, updateWindowUrlHashParams, -} from "../../utils/url.ts"; +} from "../../utils/url"; import LogLevelSelect from "./LogLevelSelect"; import StatusBarToggleButton from "./StatusBarToggleButton"; diff --git a/src/stores/logFileStore.ts b/src/stores/logFileStore.ts index 6adc4e387..9c85657f5 100644 --- a/src/stores/logFileStore.ts +++ b/src/stores/logFileStore.ts @@ -140,6 +140,8 @@ const useLogFileStore = create((set, get) => ({ const pageData = await logFileManagerProxy.loadPage(cursor, isPrettified); updatePageData(pageData); + const {startQuery} = useQueryStore.getState(); + startQuery(); const canFormat = fileInfo.fileType === FILE_TYPE.CLP_KV_IR || fileInfo.fileType === FILE_TYPE.JSONL; diff --git a/src/stores/viewStore.ts b/src/stores/viewStore.ts index 3fe18b0a1..a3f4e2f4b 100644 --- a/src/stores/viewStore.ts +++ b/src/stores/viewStore.ts @@ -15,7 +15,7 @@ import { NavigationAction, } from "../utils/actions"; import {clamp} from "../utils/math"; -import {updateWindowUrlHashParams} from "../utils/url.ts"; +import {updateWindowUrlHashParams} from "../utils/url"; import useLogFileManagerStore from "./logFileManagerProxyStore"; import useLogFileStore from "./logFileStore"; import {handleErrorWithNotification} from "./notificationStore"; @@ -111,9 +111,6 @@ const useViewStore = create((set, get) => ({ const {setUiState} = useUiStore.getState(); setUiState(UI_STATE.FAST_LOADING); - const {clearQuery} = useQueryStore.getState(); - clearQuery(); - (async () => { const {logFileManagerProxy} = useLogFileManagerStore.getState(); const {isPrettified, logEventNum} = get(); diff --git a/src/typings/url.ts b/src/typings/url.ts index 210b23fe3..d8f5e4903 100644 --- a/src/typings/url.ts +++ b/src/typings/url.ts @@ -33,12 +33,6 @@ type UrlHashParamUpdatesType = { [T in keyof UrlHashParams]?: Nullable; }; -type UrlParamsType = { - [T in keyof UrlSearchParams]: Nullable; -} & { - [T in keyof UrlHashParams]: Nullable; -}; - export { HASH_PARAM_NAMES, SEARCH_PARAM_NAMES, @@ -46,7 +40,6 @@ export { export type { UrlHashParams, UrlHashParamUpdatesType, - UrlParamsType, UrlSearchParams, UrlSearchParamUpdatesType, }; diff --git a/src/utils/js.ts b/src/utils/js.ts index 7bfcb4898..81b41e547 100644 --- a/src/utils/js.ts +++ b/src/utils/js.ts @@ -1,25 +1,9 @@ -import {Nullable} from "../typings/common.ts"; import { JsonObject, JsonValue, } from "../typings/js"; -/** - * Returns a new object with null values filtered out, and all values converted to strings. - * - * @param obj - * @return The new object with string values. - */ -const filterNullValuesToStrings = ( - obj: Record> -): Record => Object.fromEntries( - Object.entries(obj) - .filter(([, v]) => null !== v) - .map(([k, v]) => [k, - String(v)]) -) as Record; - /** * Gets a nested value from a JSON object. * @@ -57,7 +41,6 @@ const jsonValueToString = (input: JsonValue | undefined): string => { }; export { - filterNullValuesToStrings, getNestedJsonValue, jsonValueToString, }; diff --git a/src/utils/url.ts b/src/utils/url.ts index 8a93b37a8..2e9680b70 100644 --- a/src/utils/url.ts +++ b/src/utils/url.ts @@ -1,5 +1,4 @@ /* eslint max-lines: ["error", 350] */ -import {NullableProperties} from "../typings/common.ts"; import { HASH_PARAM_NAMES, SEARCH_PARAM_NAMES, @@ -7,15 +6,14 @@ import { UrlHashParamUpdatesType, UrlSearchParams, UrlSearchParamUpdatesType, -} from "../typings/url.ts"; -import {filterNullValuesToStrings} from "../utils/js.ts"; +} from "../typings/url"; /** * Default values of the search parameters. */ const URL_SEARCH_PARAMS_DEFAULT = Object.freeze({ - [SEARCH_PARAM_NAMES.FILE_PATH]: null, + [SEARCH_PARAM_NAMES.FILE_PATH]: "", }); /** @@ -26,7 +24,7 @@ const URL_HASH_PARAMS_DEFAULT = Object.freeze({ [HASH_PARAM_NAMES.LOG_EVENT_NUM]: 0, [HASH_PARAM_NAMES.QUERY_IS_CASE_SENSITIVE]: false, [HASH_PARAM_NAMES.QUERY_IS_REGEX]: false, - [HASH_PARAM_NAMES.QUERY_STRING]: null, + [HASH_PARAM_NAMES.QUERY_STRING]: "", }); /** @@ -56,63 +54,19 @@ const getAbsoluteUrl = (path: string) => { }; /** - * Retrieves all hash parameters from the current window's URL. + * Parses the URL search parameters from the current window's URL. * - * @return An object containing the hash parameters. + * @return An object containing the parsed search parameters. */ -const getWindowUrlHashParams = () => { - const urlHashParams: NullableProperties = - structuredClone(URL_HASH_PARAMS_DEFAULT); - const hashParams = new URLSearchParams(window.location.hash.substring(1)); - - const isPrettified = hashParams.get(HASH_PARAM_NAMES.IS_PRETTIFIED); - if (null !== isPrettified) { - urlHashParams[HASH_PARAM_NAMES.IS_PRETTIFIED] = "true" === isPrettified; - } +const parseWindowUrlSearchParams = () : Partial => { + const parsedSearchParams : Partial = {}; + const searchParams = new URLSearchParams(window.location.search.substring(1)); - const logEventNum = hashParams.get(HASH_PARAM_NAMES.LOG_EVENT_NUM); - if (null !== logEventNum) { - const parsed = Number(logEventNum); - urlHashParams[HASH_PARAM_NAMES.LOG_EVENT_NUM] = Number.isNaN(parsed) ? - null : - parsed; - } - - const queryString = hashParams.get(HASH_PARAM_NAMES.QUERY_STRING); - if (null !== queryString) { - urlHashParams[HASH_PARAM_NAMES.QUERY_STRING] = queryString; - } - - const queryIsCaseSensitive = hashParams.get(HASH_PARAM_NAMES.QUERY_IS_CASE_SENSITIVE); - if (null !== queryIsCaseSensitive) { - urlHashParams[HASH_PARAM_NAMES.QUERY_IS_CASE_SENSITIVE] = - "true" === queryIsCaseSensitive; - } - - const queryIsRegex = hashParams.get(HASH_PARAM_NAMES.QUERY_IS_REGEX); - if (null !== queryIsRegex) { - urlHashParams[HASH_PARAM_NAMES.QUERY_IS_REGEX] = "true" === queryIsRegex; - } - - return urlHashParams; -}; - -/** - * Retrieves all search parameters from the current window's URL. - * - * @return An object containing the search parameters. - */ -const getWindowUrlSearchParams = () => { - const searchParams : NullableProperties = structuredClone( - URL_SEARCH_PARAMS_DEFAULT - ); - const urlSearchParams = new URLSearchParams(window.location.search.substring(1)); - - urlSearchParams.forEach((value, key) => { - searchParams[key as keyof UrlSearchParams] = value; + searchParams.forEach((value, key) => { + parsedSearchParams[key as keyof UrlSearchParams] = value; }); - if (urlSearchParams.has(SEARCH_PARAM_NAMES.FILE_PATH)) { + if (searchParams.has(SEARCH_PARAM_NAMES.FILE_PATH)) { // Extract filePath value by finding the parameter and taking everything after it const filePathIndex = window.location.search.indexOf("filePath="); if (-1 !== filePathIndex) { @@ -124,14 +78,77 @@ const getWindowUrlSearchParams = () => { } catch (e) { console.error("Unable to get absolute URL from filePath:", e); } - searchParams[SEARCH_PARAM_NAMES.FILE_PATH] = resolvedFilePath; + parsedSearchParams[SEARCH_PARAM_NAMES.FILE_PATH] = resolvedFilePath; } } } - return searchParams; + return parsedSearchParams; }; +/** + * Parses the URL hash parameters from the current window's URL. + * + * @return An object containing the parsed hash parameters. + */ +const parseWindowUrlHashParams = () : Partial => { + const hashParams = new URLSearchParams(window.location.hash.substring(1)); + const parsedHashParams: Partial = {}; + + hashParams.forEach((value, _key) => { + const key = _key as HASH_PARAM_NAMES; + if (HASH_PARAM_NAMES.IS_PRETTIFIED === key) { + parsedHashParams[HASH_PARAM_NAMES.IS_PRETTIFIED] = "true" === value; + } else if (HASH_PARAM_NAMES.LOG_EVENT_NUM === key) { + const parsed = Number(value); + parsedHashParams[HASH_PARAM_NAMES.LOG_EVENT_NUM] = Number.isNaN(parsed) ? + 0 : + parsed; + } else if (HASH_PARAM_NAMES.QUERY_STRING === key) { + parsedHashParams[HASH_PARAM_NAMES.QUERY_STRING] = value; + } else if (HASH_PARAM_NAMES.QUERY_IS_CASE_SENSITIVE === key) { + parsedHashParams[HASH_PARAM_NAMES.QUERY_IS_CASE_SENSITIVE] = "true" === value; + } else { + // (HASH_PARAM_NAMES.QUERY_IS_REGEX === key) + parsedHashParams[HASH_PARAM_NAMES.QUERY_IS_REGEX] = "true" === value; + } + }); + + return parsedHashParams; +}; + +/** + * Retrieves all hash parameters from the current window's URL. + * + * @return An object containing the hash parameters, including the default values. + */ +const getWindowUrlHashParams = (): UrlHashParams => ({ + ...URL_HASH_PARAMS_DEFAULT, + ...parseWindowUrlHashParams(), +}); + +/** + * Retrieves all search parameters from the current window's URL. + * + * @return An object containing the search parameters. + */ +const getWindowUrlSearchParams = (): UrlSearchParams => ({ + ...URL_SEARCH_PARAMS_DEFAULT, + ...parseWindowUrlSearchParams(), +}); + +/** + * Checks if a value is empty or falsy. + * + * @param value + * @return `true` if the value is empty or falsy, otherwise `false`. + */ +const isEmptyOrFalsy = (value: unknown): boolean => ( + null === value || + false === value || + ("string" === typeof value && 0 === value.length) +); + /** * Computes updated URL search parameters based on the provided key-value pairs. * @@ -141,8 +158,8 @@ const getWindowUrlSearchParams = () => { * @private */ const getUpdatedSearchParams = (updates: UrlSearchParamUpdatesType) => { - const currentParams = getWindowUrlSearchParams(); - const newSearchParams = new URLSearchParams(filterNullValuesToStrings(currentParams)); + const currentSearchParams = parseWindowUrlSearchParams(); + const newSearchParams = new URLSearchParams(currentSearchParams); const {filePath: newFilePath} = updates; for (const [key, value] of Object.entries(updates)) { @@ -150,7 +167,7 @@ const getUpdatedSearchParams = (updates: UrlSearchParamUpdatesType) => { // Updates to `filePath` should be handled last. continue; } - if (null === value) { + if (isEmptyOrFalsy(value)) { newSearchParams.delete(key); } else { newSearchParams.set(key, String(value)); @@ -195,11 +212,13 @@ const getUpdatedSearchParams = (updates: UrlSearchParamUpdatesType) => { * @private */ const getUpdatedHashParams = (updates: UrlHashParamUpdatesType) => { - const currentParams = getWindowUrlHashParams(); - const newHashParams = new URLSearchParams(filterNullValuesToStrings(currentParams)); + const currentHashParams = parseWindowUrlHashParams(); + + // For non-string values, URLSearchParams::new() will convert them to strings. + const newHashParams = new URLSearchParams(currentHashParams as Record); for (const [key, value] of Object.entries(updates)) { - if (null === value || false === value) { + if (isEmptyOrFalsy(value)) { newHashParams.delete(key); } else { newHashParams.set(key, String(value)); @@ -273,9 +292,6 @@ const openInNewTab = (url: string): void => { /** * Updates hash parameters in the current window's URL with the given key-value pairs. * - * Note: This function only updates the URL. Callers are responsible for updating corresponding - * Zustand store state to maintain synchronization. - * * @param updates An object containing key-value pairs to update the hash parameters. If a value is * `null`, the corresponding kv-pair will be removed from the URL's hash parameters. */ @@ -294,9 +310,6 @@ const updateWindowUrlHashParams = (updates: UrlHashParamUpdatesType) => { /** * Updates search parameters in the current window's URL with the given key-value pairs. * - * Note: This function only updates the URL. Callers are responsible for updating corresponding - * Zustand store state to maintain synchronization. - * * @param updates An object containing key-value pairs to update the search parameters. If a value * is `null`, the corresponding kv-pair will be removed from the URL's search parameters. */ From def27ea68ce38e1bceabd0f2682b158fb97054d6 Mon Sep 17 00:00:00 2001 From: zzxthehappiest Date: Thu, 5 Jun 2025 19:08:19 +0800 Subject: [PATCH 23/25] Final fix --- src/components/AppController.tsx | 49 ++++++++----------- .../SidebarTabs/SearchTabPanel/index.tsx | 2 +- src/components/StatusBar/index.tsx | 4 +- src/typings/common.ts | 5 -- src/utils/url.ts | 2 + 5 files changed, 26 insertions(+), 36 deletions(-) diff --git a/src/components/AppController.tsx b/src/components/AppController.tsx index 348277cdc..f7ba2432c 100644 --- a/src/components/AppController.tsx +++ b/src/components/AppController.tsx @@ -9,10 +9,7 @@ import {handleErrorWithNotification} from "../stores/notificationStore"; import useQueryStore from "../stores/queryStore"; import useUiStore from "../stores/uiStore"; import useViewStore from "../stores/viewStore"; -import { - Nullable, - NullableProperties, -} from "../typings/common.ts"; +import {Nullable} from "../typings/common"; import {UI_STATE} from "../typings/states"; import {UrlHashParams} from "../typings/url"; import { @@ -76,15 +73,12 @@ const updateUrlIfEventOnPage = ( * * @param hashParams */ -const updateViewHashParams = (hashParams: NullableProperties): void => { +const updateViewHashParams = (hashParams: UrlHashParams): void => { const {isPrettified, logEventNum} = hashParams; const {updateIsPrettified, setLogEventNum} = useViewStore.getState(); - if (null !== isPrettified && URL_HASH_PARAMS_DEFAULT.isPrettified !== isPrettified) { - updateIsPrettified(isPrettified); - } - if (null !== logEventNum && URL_HASH_PARAMS_DEFAULT.logEventNum !== logEventNum) { - setLogEventNum(logEventNum); - } + + updateIsPrettified(isPrettified); + setLogEventNum(logEventNum); }; /** @@ -93,7 +87,7 @@ const updateViewHashParams = (hashParams: NullableProperties): vo * @param hashParams * @return Whether any query parameters were modified. */ -const updateQueryHashParams = (hashParams: NullableProperties): boolean => { +const updateQueryHashParams = (hashParams: UrlHashParams): boolean => { const {queryIsCaseSensitive, queryIsRegex, queryString} = hashParams; const { queryIsCaseSensitive: currentQueryIsCaseSensitive, @@ -103,20 +97,17 @@ const updateQueryHashParams = (hashParams: NullableProperties): b setQueryIsRegex, setQueryString, } = useQueryStore.getState(); + let isQueryModified = false; - if (null !== queryIsCaseSensitive && - URL_HASH_PARAMS_DEFAULT.queryIsCaseSensitive !== queryIsCaseSensitive) { - isQueryModified ||= queryIsCaseSensitive !== currentQueryIsCaseSensitive; - setQueryIsCaseSensitive(queryIsCaseSensitive); - } - if (null !== queryIsRegex && URL_HASH_PARAMS_DEFAULT.queryIsRegex !== queryIsRegex) { - isQueryModified ||= queryIsRegex !== currentQueryIsRegex; - setQueryIsRegex(queryIsRegex); - } - if (null !== queryString && URL_HASH_PARAMS_DEFAULT.queryString !== queryString) { - isQueryModified ||= queryString !== currentQueryString; - setQueryString(queryString); - } + + isQueryModified ||= queryIsCaseSensitive !== currentQueryIsCaseSensitive; + setQueryIsCaseSensitive(queryIsCaseSensitive); + + isQueryModified ||= queryIsRegex !== currentQueryIsRegex; + setQueryIsRegex(queryIsRegex); + + isQueryModified ||= queryString !== currentQueryString; + setQueryString(queryString); return isQueryModified; }; @@ -127,7 +118,7 @@ const updateQueryHashParams = (hashParams: NullableProperties): b * @param [ev] The hash change event, or `null` when called on application initialization. * @return The parsed URL hash parameters. */ -const handleHashChange = (ev: Nullable): NullableProperties => { +const handleHashChange = (ev: Nullable): UrlHashParams => { const hashParams = getWindowUrlHashParams(); updateViewHashParams(hashParams); const isTriggeredByHashChange = null !== ev; @@ -173,8 +164,10 @@ const AppController = ({children}: AppControllerProps) => { const searchParams = getWindowUrlSearchParams(); if (URL_SEARCH_PARAMS_DEFAULT.filePath !== searchParams.filePath) { let cursor: CursorType = {code: CURSOR_CODE.LAST_EVENT, args: null}; - if (null !== hashParams.logEventNum && - URL_HASH_PARAMS_DEFAULT.logEventNum !== hashParams.logEventNum) { + + // Since the default logEventNum is 0, which is not a valid index, so if it is 0, we + // don't jump to 0 + if (URL_HASH_PARAMS_DEFAULT.logEventNum !== hashParams.logEventNum) { cursor = { code: CURSOR_CODE.EVENT_NUM, args: {eventNum: hashParams.logEventNum}, diff --git a/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/index.tsx b/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/index.tsx index 4a8cda029..9d7fbcd23 100644 --- a/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/index.tsx +++ b/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/index.tsx @@ -17,7 +17,7 @@ import { TAB_DISPLAY_NAMES, TAB_NAME, } from "../../../../../typings/tab"; -import {copyPermalinkToClipboard} from "../../../../../utils/url.ts"; +import {copyPermalinkToClipboard} from "../../../../../utils/url"; import CustomTabPanel from "../CustomTabPanel"; import PanelTitleButton from "../PanelTitleButton"; import QueryInputBox from "./QueryInputBox"; diff --git a/src/components/StatusBar/index.tsx b/src/components/StatusBar/index.tsx index ffb06be7f..2f231f64b 100644 --- a/src/components/StatusBar/index.tsx +++ b/src/components/StatusBar/index.tsx @@ -40,10 +40,10 @@ const handleCopyLinkButtonClick = () => { * @return */ const StatusBar = () => { - const numEvents = useLogFileStore((state) => state.numEvents); - const uiState = useUiStore((state) => state.uiState); const isPrettified = useViewStore((state) => state.isPrettified); const logEventNum = useViewStore((state) => state.logEventNum); + const numEvents = useLogFileStore((state) => state.numEvents); + const uiState = useUiStore((state) => state.uiState); const updateIsPrettified = useViewStore((state) => state.updateIsPrettified); const handleStatusButtonClick = (ev: React.MouseEvent) => { diff --git a/src/typings/common.ts b/src/typings/common.ts index ea386a4ed..99b65ff85 100644 --- a/src/typings/common.ts +++ b/src/typings/common.ts @@ -1,13 +1,8 @@ type Nullable = T | null; -type NullableProperties = { - [P in keyof T]: Nullable; -}; - type WithId = T & {id: number}; export type { Nullable, - NullableProperties, WithId, }; diff --git a/src/utils/url.ts b/src/utils/url.ts index 2e9680b70..b14fd74cb 100644 --- a/src/utils/url.ts +++ b/src/utils/url.ts @@ -331,6 +331,8 @@ export { getWindowUrlHashParams, getWindowUrlSearchParams, openInNewTab, + parseWindowUrlHashParams, + parseWindowUrlSearchParams, updateWindowUrlHashParams, updateWindowUrlSearchParams, URL_HASH_PARAMS_DEFAULT, From 336944957d0604b6e0bdacc9d0e29e23f72f66e8 Mon Sep 17 00:00:00 2001 From: zzxthehappiest Date: Thu, 5 Jun 2025 19:35:53 +0800 Subject: [PATCH 24/25] Minor fix --- src/components/AppController.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/AppController.tsx b/src/components/AppController.tsx index f7ba2432c..9f1d8e592 100644 --- a/src/components/AppController.tsx +++ b/src/components/AppController.tsx @@ -99,7 +99,6 @@ const updateQueryHashParams = (hashParams: UrlHashParams): boolean => { } = useQueryStore.getState(); let isQueryModified = false; - isQueryModified ||= queryIsCaseSensitive !== currentQueryIsCaseSensitive; setQueryIsCaseSensitive(queryIsCaseSensitive); @@ -123,6 +122,11 @@ const handleHashChange = (ev: Nullable): UrlHashParams => { updateViewHashParams(hashParams); const isTriggeredByHashChange = null !== ev; const isQueryModified = updateQueryHashParams(hashParams); + + // This can remove those empty or falsy parameters + updateWindowUrlHashParams({ + ...hashParams, + }); if (isTriggeredByHashChange && isQueryModified) { const {startQuery} = useQueryStore.getState(); startQuery(); From 9669cd5cf7656cc4a0870d33d9cef623eb372e5e Mon Sep 17 00:00:00 2001 From: zzxthehappiest Date: Sun, 8 Jun 2025 22:53:51 +0800 Subject: [PATCH 25/25] Fix latest comments --- src/components/AppController.tsx | 14 ++-- src/components/Editor/index.tsx | 2 - src/stores/logFileStore.ts | 3 - src/utils/url.ts | 126 +++++++++++++++---------------- 4 files changed, 69 insertions(+), 76 deletions(-) diff --git a/src/components/AppController.tsx b/src/components/AppController.tsx index 9f1d8e592..5ab15563d 100644 --- a/src/components/AppController.tsx +++ b/src/components/AppController.tsx @@ -120,18 +120,18 @@ const updateQueryHashParams = (hashParams: UrlHashParams): boolean => { const handleHashChange = (ev: Nullable): UrlHashParams => { const hashParams = getWindowUrlHashParams(); updateViewHashParams(hashParams); - const isTriggeredByHashChange = null !== ev; const isQueryModified = updateQueryHashParams(hashParams); - - // This can remove those empty or falsy parameters - updateWindowUrlHashParams({ - ...hashParams, - }); + const isTriggeredByHashChange = null !== ev; if (isTriggeredByHashChange && isQueryModified) { const {startQuery} = useQueryStore.getState(); startQuery(); } + // Remove empty or falsy parameters. + updateWindowUrlHashParams({ + ...hashParams, + }); + return hashParams; }; @@ -169,8 +169,6 @@ const AppController = ({children}: AppControllerProps) => { if (URL_SEARCH_PARAMS_DEFAULT.filePath !== searchParams.filePath) { let cursor: CursorType = {code: CURSOR_CODE.LAST_EVENT, args: null}; - // Since the default logEventNum is 0, which is not a valid index, so if it is 0, we - // don't jump to 0 if (URL_HASH_PARAMS_DEFAULT.logEventNum !== hashParams.logEventNum) { cursor = { code: CURSOR_CODE.EVENT_NUM, diff --git a/src/components/Editor/index.tsx b/src/components/Editor/index.tsx index 46aba107f..7ddd79207 100644 --- a/src/components/Editor/index.tsx +++ b/src/components/Editor/index.tsx @@ -185,9 +185,7 @@ const Editor = () => { const {mode, systemMode} = useColorScheme(); const beginLineNumToLogEventNum = useViewStore((state) => state.beginLineNumToLogEventNum); - const logData = useViewStore((state) => state.logData); - const logEventNum = useViewStore((state) => state.logEventNum); const [lineNum, setLineNum] = useState(1); diff --git a/src/stores/logFileStore.ts b/src/stores/logFileStore.ts index 9c85657f5..a9d98db37 100644 --- a/src/stores/logFileStore.ts +++ b/src/stores/logFileStore.ts @@ -110,9 +110,6 @@ const useLogFileStore = create((set, get) => ({ const {setExportProgress} = useLogExportStore.getState(); setExportProgress(LOG_EXPORT_STORE_DEFAULT.exportProgress); - const {clearQuery} = useQueryStore.getState(); - clearQuery(); - const {setLogData} = useViewStore.getState(); setLogData("Loading..."); diff --git a/src/utils/url.ts b/src/utils/url.ts index b14fd74cb..5ae3ea68d 100644 --- a/src/utils/url.ts +++ b/src/utils/url.ts @@ -53,6 +53,18 @@ const getAbsoluteUrl = (path: string) => { return path; }; +/** + * Checks if a value is empty or falsy. + * + * @param value + * @return `true` if the value is empty or falsy, otherwise `false`. + */ +const isEmptyOrFalsy = (value: unknown): boolean => ( + null === value || + false === value || + ("string" === typeof value && 0 === value.length) +); + /** * Parses the URL search parameters from the current window's URL. * @@ -86,69 +98,6 @@ const parseWindowUrlSearchParams = () : Partial => { return parsedSearchParams; }; -/** - * Parses the URL hash parameters from the current window's URL. - * - * @return An object containing the parsed hash parameters. - */ -const parseWindowUrlHashParams = () : Partial => { - const hashParams = new URLSearchParams(window.location.hash.substring(1)); - const parsedHashParams: Partial = {}; - - hashParams.forEach((value, _key) => { - const key = _key as HASH_PARAM_NAMES; - if (HASH_PARAM_NAMES.IS_PRETTIFIED === key) { - parsedHashParams[HASH_PARAM_NAMES.IS_PRETTIFIED] = "true" === value; - } else if (HASH_PARAM_NAMES.LOG_EVENT_NUM === key) { - const parsed = Number(value); - parsedHashParams[HASH_PARAM_NAMES.LOG_EVENT_NUM] = Number.isNaN(parsed) ? - 0 : - parsed; - } else if (HASH_PARAM_NAMES.QUERY_STRING === key) { - parsedHashParams[HASH_PARAM_NAMES.QUERY_STRING] = value; - } else if (HASH_PARAM_NAMES.QUERY_IS_CASE_SENSITIVE === key) { - parsedHashParams[HASH_PARAM_NAMES.QUERY_IS_CASE_SENSITIVE] = "true" === value; - } else { - // (HASH_PARAM_NAMES.QUERY_IS_REGEX === key) - parsedHashParams[HASH_PARAM_NAMES.QUERY_IS_REGEX] = "true" === value; - } - }); - - return parsedHashParams; -}; - -/** - * Retrieves all hash parameters from the current window's URL. - * - * @return An object containing the hash parameters, including the default values. - */ -const getWindowUrlHashParams = (): UrlHashParams => ({ - ...URL_HASH_PARAMS_DEFAULT, - ...parseWindowUrlHashParams(), -}); - -/** - * Retrieves all search parameters from the current window's URL. - * - * @return An object containing the search parameters. - */ -const getWindowUrlSearchParams = (): UrlSearchParams => ({ - ...URL_SEARCH_PARAMS_DEFAULT, - ...parseWindowUrlSearchParams(), -}); - -/** - * Checks if a value is empty or falsy. - * - * @param value - * @return `true` if the value is empty or falsy, otherwise `false`. - */ -const isEmptyOrFalsy = (value: unknown): boolean => ( - null === value || - false === value || - ("string" === typeof value && 0 === value.length) -); - /** * Computes updated URL search parameters based on the provided key-value pairs. * @@ -203,6 +152,47 @@ const getUpdatedSearchParams = (updates: UrlSearchParamUpdatesType) => { return searchString; }; +/** + * Retrieves all search parameters from the current window's URL. + * + * @return An object containing the search parameters. + */ +const getWindowUrlSearchParams = (): UrlSearchParams => ({ + ...URL_SEARCH_PARAMS_DEFAULT, + ...parseWindowUrlSearchParams(), +}); + +/** + * Parses the URL hash parameters from the current window's URL. + * + * @return An object containing the parsed hash parameters. + */ +const parseWindowUrlHashParams = () : Partial => { + const hashParams = new URLSearchParams(window.location.hash.substring(1)); + const parsedHashParams: Partial = {}; + + hashParams.forEach((value, _key) => { + const key = _key as HASH_PARAM_NAMES; + if (HASH_PARAM_NAMES.IS_PRETTIFIED === key) { + parsedHashParams[HASH_PARAM_NAMES.IS_PRETTIFIED] = "true" === value; + } else if (HASH_PARAM_NAMES.LOG_EVENT_NUM === key) { + const parsed = Number(value); + parsedHashParams[HASH_PARAM_NAMES.LOG_EVENT_NUM] = Number.isNaN(parsed) ? + 0 : + parsed; + } else if (HASH_PARAM_NAMES.QUERY_STRING === key) { + parsedHashParams[HASH_PARAM_NAMES.QUERY_STRING] = value; + } else if (HASH_PARAM_NAMES.QUERY_IS_CASE_SENSITIVE === key) { + parsedHashParams[HASH_PARAM_NAMES.QUERY_IS_CASE_SENSITIVE] = "true" === value; + } else { + // (HASH_PARAM_NAMES.QUERY_IS_REGEX === key) + parsedHashParams[HASH_PARAM_NAMES.QUERY_IS_REGEX] = "true" === value; + } + }); + + return parsedHashParams; +}; + /** * Computes updated URL hash parameters based on the provided key-value pairs. * @@ -228,6 +218,16 @@ const getUpdatedHashParams = (updates: UrlHashParamUpdatesType) => { return newHashParams.toString(); }; +/** + * Retrieves all hash parameters from the current window's URL. + * + * @return An object containing the hash parameters, including the default values. + */ +const getWindowUrlHashParams = (): UrlHashParams => ({ + ...URL_HASH_PARAMS_DEFAULT, + ...parseWindowUrlHashParams(), +}); + /** * Copies the current window's URL to the clipboard. If any `updates` parameters are specified, * the copied URL will include these modifications, but the original window's URL will not be