diff --git a/src/components/AppController.tsx b/src/components/AppController.tsx index 9b0ec4af0..6dda8fbf8 100644 --- a/src/components/AppController.tsx +++ b/src/components/AppController.tsx @@ -3,156 +3,35 @@ import React, { useRef, } from "react"; -import useLogFileManagerProxyStore from "../stores/logFileManagerProxyStore"; import useLogFileStore from "../stores/logFileStore"; -import {handleErrorWithNotification} from "../stores/notificationStore"; import useQueryStore from "../stores/queryStore"; import useViewStore from "../stores/viewStore"; -import {Nullable} from "../typings/common"; import { CURSOR_CODE, CursorType, } from "../typings/worker"; -import {clamp} from "../utils/math.ts"; import { getWindowUrlHashParams, getWindowUrlSearchParams, - updateUrlIfEventOnPage, updateWindowUrlHashParams, URL_HASH_PARAMS_DEFAULT, URL_SEARCH_PARAMS_DEFAULT, } from "../utils/url"; +import { + updateQueryHashParams, + updateViewHashParams, +} from "../utils/url/urlHash"; -/** - * Determines the cursor for navigating log events based on URL hash parameters. - * - * @param params An object containing the following properties: - * @param params.isPrettified Whether the log view is in prettified mode. - * @param params.logEventNum The log event number from the URL hash. - * @param params.timestamp The timestamp from the URL hash. - * @return `CursorType` object if a navigation action is needed, or `null` if no action is required. - */ -const getCursorFromHashParams = ({isPrettified, logEventNum, timestamp}: { - isPrettified: boolean; logEventNum: number; timestamp: number; -}): Nullable => { - const {numEvents} = useLogFileStore.getState(); - if (0 === numEvents) { - updateWindowUrlHashParams({logEventNum: URL_HASH_PARAMS_DEFAULT.logEventNum}); - - return null; - } - - const { - isPrettified: prevIsPrettified, updateIsPrettified, updateLogEventNum, - } = useViewStore.getState(); - const clampedLogEventNum = clamp(logEventNum, 1, numEvents); - - if (isPrettified !== prevIsPrettified) { - updateIsPrettified(isPrettified); - - return { - code: CURSOR_CODE.EVENT_NUM, - args: {eventNum: clampedLogEventNum}, - }; - } - - if (timestamp !== URL_HASH_PARAMS_DEFAULT.timestamp) { - return { - code: CURSOR_CODE.TIMESTAMP, - args: {timestamp: timestamp}, - }; - } else if (logEventNum !== URL_HASH_PARAMS_DEFAULT.logEventNum) { - updateLogEventNum(logEventNum); - 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. - return null; - } - - return { - code: CURSOR_CODE.EVENT_NUM, - args: {eventNum: clampedLogEventNum}, - }; - } - - // If we reach here, we have no valid cursor. - return null; -}; - -/** - * Updates view-related states from URL hash parameters. - * NOTE: this may modify the URL parameters. - */ -const updateViewHashParams = () => { - const {isPrettified, logEventNum, timestamp} = getWindowUrlHashParams(); - updateWindowUrlHashParams({ - isPrettified: URL_HASH_PARAMS_DEFAULT.isPrettified, - timestamp: URL_HASH_PARAMS_DEFAULT.timestamp, - }); - - const cursor = getCursorFromHashParams({isPrettified, logEventNum, timestamp}); - if (null === cursor) { - // If no cursor was set, we can return early. - return; - } - - (async () => { - const {logFileManagerProxy} = useLogFileManagerProxyStore.getState(); - const {updatePageData} = useViewStore.getState(); - const pageData = await logFileManagerProxy.loadPage(cursor, isPrettified); - updatePageData(pageData); - })().catch(handleErrorWithNotification); -}; - -/** - * Updates query-related states from URL hash parameters. - * NOTE: this may modify the URL parameters. - * - * @return Whether any query-related parameters were modified. - */ -const updateQueryHashParams = () => { - const {queryIsCaseSensitive, queryIsRegex, queryString} = getWindowUrlHashParams(); - updateWindowUrlHashParams({queryIsCaseSensitive, queryIsRegex, queryString}); - - const { - queryIsCaseSensitive: currentQueryIsCaseSensitive, - queryIsRegex: currentQueryIsRegex, - queryString: currentQueryString, - setQueryIsCaseSensitive, - setQueryIsRegex, - setQueryString, - } = useQueryStore.getState(); - - let isQueryModified = false; - isQueryModified ||= queryIsCaseSensitive !== currentQueryIsCaseSensitive; - setQueryIsCaseSensitive(queryIsCaseSensitive); - - isQueryModified ||= queryIsRegex !== currentQueryIsRegex; - setQueryIsRegex(queryIsRegex); - - isQueryModified ||= queryString !== currentQueryString; - setQueryString(queryString); - - return isQueryModified; -}; - /** * 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. */ -const handleHashChange = (ev: Nullable) => { +const handleHashChange = () => { updateViewHashParams(); - const isTriggeredByHashChange = null !== ev; - if (isTriggeredByHashChange && updateQueryHashParams()) { + if (updateQueryHashParams()) { const {startQuery} = useQueryStore.getState(); startQuery(); } - - // eslint-disable-next-line no-warning-comments - // TODO: Remove empty or falsy parameters. }; interface AppControllerProps { @@ -181,8 +60,14 @@ const AppController = ({children}: AppControllerProps) => { isInitialized.current = true; // Handle initial page load and maintain full URL state - handleHashChange(null); const hashParams = getWindowUrlHashParams(); + updateWindowUrlHashParams({ + isPrettified: URL_HASH_PARAMS_DEFAULT.isPrettified, + timestamp: URL_HASH_PARAMS_DEFAULT.timestamp, + }); + const {setIsPrettified} = useViewStore.getState(); + setIsPrettified(hashParams.isPrettified); + const searchParams = getWindowUrlSearchParams(); if (URL_SEARCH_PARAMS_DEFAULT.filePath !== searchParams.filePath) { let cursor: CursorType = {code: CURSOR_CODE.LAST_EVENT, args: null}; @@ -193,6 +78,8 @@ const AppController = ({children}: AppControllerProps) => { args: {timestamp: hashParams.timestamp}, }; } else if (URL_HASH_PARAMS_DEFAULT.logEventNum !== hashParams.logEventNum) { + const {setLogEventNum} = useViewStore.getState(); + setLogEventNum(hashParams.logEventNum); cursor = { code: CURSOR_CODE.EVENT_NUM, args: {eventNum: hashParams.logEventNum}, @@ -210,5 +97,4 @@ const AppController = ({children}: AppControllerProps) => { return children; }; - export default AppController; diff --git a/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/Result.tsx b/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/Result.tsx index 450eac076..7d115ff5f 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"; import {updateWindowUrlHashParams} from "../../../../../utils/url"; +import {updateViewHashParams} from "../../../../../utils/url/urlHash"; import "./Result.css"; @@ -42,8 +42,7 @@ const Result = ({logEventNum, message, matchRange}: ResultProps) => { const handleResultButtonClick = useCallback(() => { updateWindowUrlHashParams({logEventNum}); - const {updateLogEventNum} = useViewStore.getState(); - updateLogEventNum(logEventNum); + updateViewHashParams(); }, [logEventNum]); return ( diff --git a/src/components/Editor/MonacoInstance/index.tsx b/src/components/Editor/MonacoInstance/index.tsx index ee59b5dfd..7e389ad81 100644 --- a/src/components/Editor/MonacoInstance/index.tsx +++ b/src/components/Editor/MonacoInstance/index.tsx @@ -14,18 +14,14 @@ import { MountCallback, TextUpdateCallback, } from "./typings"; -import { - createMonacoEditor, - goToPositionAndCenter, -} from "./utils"; +import {createMonacoEditor} from "./utils"; -import "./bootstrap.ts"; +import "./bootstrap"; import "./index.css"; interface MonacoEditorProps { actions: EditorAction[]; - lineNum: number; text: string; themeName: "dark" | "light"; @@ -44,7 +40,6 @@ interface MonacoEditorProps { * * @param props * @param props.actions - * @param props.lineNum * @param props.text * @param props.themeName * @param props.beforeMount @@ -57,7 +52,6 @@ interface MonacoEditorProps { */ const MonacoInstance = ({ actions, - lineNum, text, themeName, beforeMount, @@ -69,12 +63,6 @@ const MonacoInstance = ({ }: MonacoEditorProps) => { const editorRef = useRef(null); const editorContainerRef = useRef(null); - const lineNumRef = useRef(lineNum); - - // Synchronize `lineNumRef` with `lineNum`. - useEffect(() => { - lineNumRef.current = lineNum; - }, [lineNum]); useEffect(() => { console.log("Initializing Monaco instance..."); @@ -115,7 +103,6 @@ const MonacoInstance = ({ } beforeTextUpdate?.(editorRef.current); editorRef.current.setValue(text); - goToPositionAndCenter(editorRef.current, {lineNumber: lineNumRef.current, column: 1}); onTextUpdate?.(editorRef.current); }, [ text, @@ -123,14 +110,6 @@ const MonacoInstance = ({ onTextUpdate, ]); - // On `lineNum` update, update the cursor's position in the editor. - useEffect(() => { - if (null === editorRef.current) { - return; - } - goToPositionAndCenter(editorRef.current, {lineNumber: lineNum, column: 1}); - }, [lineNum]); - return (
{ const queryIsCaseSensitive = useQueryStore((state) => state.queryIsCaseSensitive); const queryIsRegex = useQueryStore((state) => state.queryIsRegex); - const [lineNum, setLineNum] = useState(1); const beginLineNumToLogEventNumRef = useRef( beginLineNumToLogEventNum ); @@ -271,8 +271,7 @@ const Editor = () => { return; } updateWindowUrlHashParams({logEventNum: newLogEventNum}); - const {updateLogEventNum} = useViewStore.getState(); - updateLogEventNum(newLogEventNum); + updateViewHashParams(); }, []); // Synchronize `beginLineNumToLogEventNumRef` with `beginLineNumToLogEventNum`. @@ -332,7 +331,7 @@ const Editor = () => { return; } - setLineNum(logEventLineNum); + goToPositionAndCenter(editorRef.current, {lineNumber: logEventLineNum, column: 1}); }, [ logEventNum, beginLineNumToLogEventNum, @@ -343,7 +342,6 @@ const Editor = () => { { const {actionName} = ev.currentTarget.dataset; switch (actionName) { - case ACTION_NAME.TOGGLE_PRETTIFY: { + case ACTION_NAME.TOGGLE_PRETTIFY: updateWindowUrlHashParams({ [HASH_PARAM_NAMES.IS_PRETTIFIED]: false === isPrettified, }); - const {updateIsPrettified} = useViewStore.getState(); - updateIsPrettified(!isPrettified); + useUiStore.getState().setUiState(UI_STATE.FAST_LOADING); + updateViewHashParams(); break; - } default: console.error(`Unexpected action: ${actionName}`); break; diff --git a/src/services/LogFileManager/index.ts b/src/services/LogFileManager/index.ts index d525c1f03..daab85847 100644 --- a/src/services/LogFileManager/index.ts +++ b/src/services/LogFileManager/index.ts @@ -282,7 +282,6 @@ class LogFileManager { return { beginLineNumToLogEventNum: beginLineNumToLogEventNum, - cursorLineNum: 1, logEventNum: matchingLogEventNum, logs: messages.join(""), numPages: newNumPages, diff --git a/src/stores/logFileStore.ts b/src/stores/logFileStore.ts index a29c873dd..929379704 100644 --- a/src/stores/logFileStore.ts +++ b/src/stores/logFileStore.ts @@ -21,13 +21,13 @@ import { } from "../typings/worker"; import {getConfig} from "../utils/config"; import {updateWindowUrlSearchParams} from "../utils/url"; +import {updateQueryHashParams} from "../utils/url/urlHash"; import useLogExportStore, {LOG_EXPORT_STORE_DEFAULT} from "./logExportStore"; import useLogFileManagerProxyStore from "./logFileManagerProxyStore"; import useNotificationStore, {handleErrorWithNotification} from "./notificationStore"; import useQueryStore from "./queryStore"; import useUiStore from "./uiStore"; import useViewStore from "./viewStore"; -import {VIEW_EVENT_DEFAULT} from "./viewStore/createViewEventSlice"; import {VIEW_PAGE_DEFAULT} from "./viewStore/createViewPageSlice"; @@ -116,11 +116,10 @@ const useLogFileStore = create((set) => ({ const {setExportProgress} = useLogExportStore.getState(); setExportProgress(LOG_EXPORT_STORE_DEFAULT.exportProgress); - const {setPageData} = useViewStore.getState(); - setPageData({ + const {updatePageData} = useViewStore.getState(); + updatePageData({ beginLineNumToLogEventNum: VIEW_PAGE_DEFAULT.beginLineNumToLogEventNum, - cursorLineNum: 1, - logEventNum: VIEW_EVENT_DEFAULT.logEventNum, + logEventNum: useViewStore.getState().logEventNum, logs: "Loading...", numPages: VIEW_PAGE_DEFAULT.numPages, pageNum: VIEW_PAGE_DEFAULT.pageNum, @@ -148,11 +147,13 @@ const useLogFileStore = create((set) => ({ const {isPrettified} = useViewStore.getState(); const pageData = await logFileManagerProxy.loadPage(cursor, isPrettified); - const {updatePageData} = useViewStore.getState(); updatePageData(pageData); + setUiState(UI_STATE.READY); - const {startQuery} = useQueryStore.getState(); - startQuery(); + if (updateQueryHashParams()) { + const {startQuery} = useQueryStore.getState(); + startQuery(); + } if (0 === decoderOptions.formatString.length && fileInfo.fileTypeInfo.isStructured) { const {postPopUp} = useNotificationStore.getState(); diff --git a/src/stores/viewStore/createViewEventSlice.ts b/src/stores/viewStore/createViewEventSlice.ts index fde3d22e3..b7a6bb578 100644 --- a/src/stores/viewStore/createViewEventSlice.ts +++ b/src/stores/viewStore/createViewEventSlice.ts @@ -1,16 +1,5 @@ import {StateCreator} from "zustand"; -import {UI_STATE} from "../../typings/states"; -import { - CURSOR_CODE, - CursorType, -} from "../../typings/worker"; -import {clamp} from "../../utils/math"; -import {updateUrlIfEventOnPage} from "../../utils/url"; -import useLogFileManagerStore from "../logFileManagerProxyStore"; -import useLogFileStore from "../logFileStore"; -import {handleErrorWithNotification} from "../notificationStore"; -import useUiStore from "../uiStore"; import { ViewEventSlice, ViewEventValues, @@ -26,43 +15,14 @@ const VIEW_EVENT_DEFAULT: ViewEventValues = { * Creates a slice for updating log events. * * @param set - * @param get * @return */ const createViewEventSlice: StateCreator< ViewState, [], [], ViewEventSlice -> = (set, get) => ({ +> = (set) => ({ ...VIEW_EVENT_DEFAULT, - updateLogEventNum: (newLogEventNum) => { - const {numEvents} = useLogFileStore.getState(); - if (0 === numEvents) { - return; - } - - const clampedLogEventNum = clamp(newLogEventNum, 1, numEvents); - set({logEventNum: clampedLogEventNum}); - const {beginLineNumToLogEventNum} = get(); - 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. - 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} = get(); - - const pageData = await logFileManagerProxy.loadPage(cursor, isPrettified); - const {updatePageData} = get(); - updatePageData(pageData); - })().catch(handleErrorWithNotification); + setLogEventNum: (logEventNum: number) => { + set({logEventNum}); }, }); diff --git a/src/stores/viewStore/createViewFilterSlice.ts b/src/stores/viewStore/createViewFilterSlice.ts index bf90ee9d8..700c4fe27 100644 --- a/src/stores/viewStore/createViewFilterSlice.ts +++ b/src/stores/viewStore/createViewFilterSlice.ts @@ -42,6 +42,7 @@ const createViewFilterSlice: StateCreator< const {updatePageData} = get(); updatePageData(pageData); + setUiState(UI_STATE.READY); const {startQuery} = useQueryStore.getState(); startQuery(); diff --git a/src/stores/viewStore/createViewFormattingSlice.ts b/src/stores/viewStore/createViewFormattingSlice.ts index 7f70fc78a..3439f6619 100644 --- a/src/stores/viewStore/createViewFormattingSlice.ts +++ b/src/stores/viewStore/createViewFormattingSlice.ts @@ -1,14 +1,5 @@ import {StateCreator} from "zustand"; -import {UI_STATE} from "../../typings/states"; -import { - CURSOR_CODE, - CursorType, -} from "../../typings/worker"; -import useLogFileManagerStore from "../logFileManagerProxyStore"; -import {handleErrorWithNotification} from "../notificationStore"; -import useUiStore from "../uiStore"; -import {VIEW_EVENT_DEFAULT} from "./createViewEventSlice"; import { ViewFormattingSlice, ViewFormattingValues, @@ -24,40 +15,14 @@ const VIEW_FORMATTING_DEFAULT: ViewFormattingValues = { * Creates a slice for managing log formatting state. * * @param set - * @param get * @return */ const createViewFormattingSlice: StateCreator< ViewState, [], [], ViewFormattingSlice -> = (set, get) => ({ +> = (set) => ({ ...VIEW_FORMATTING_DEFAULT, - updateIsPrettified: (newIsPrettified: boolean) => { - const {isPrettified} = get(); - if (newIsPrettified === isPrettified) { - return; - } - - const {setUiState} = useUiStore.getState(); - setUiState(UI_STATE.FAST_LOADING); - - set({isPrettified: newIsPrettified}); - - const {logEventNum} = get(); - let cursor: CursorType = {code: CURSOR_CODE.LAST_EVENT, args: null}; - if (VIEW_EVENT_DEFAULT.logEventNum !== logEventNum) { - cursor = { - code: CURSOR_CODE.EVENT_NUM, - args: {eventNum: logEventNum}, - }; - } - - (async () => { - const {logFileManagerProxy} = useLogFileManagerStore.getState(); - const pageData = await logFileManagerProxy.loadPage(cursor, newIsPrettified); - - const {updatePageData} = get(); - updatePageData(pageData); - })().catch(handleErrorWithNotification); + setIsPrettified: (isPrettified: boolean) => { + set({isPrettified}); }, }); diff --git a/src/stores/viewStore/createViewPageSlice.ts b/src/stores/viewStore/createViewPageSlice.ts index a356cc9e2..0007d0525 100644 --- a/src/stores/viewStore/createViewPageSlice.ts +++ b/src/stores/viewStore/createViewPageSlice.ts @@ -92,23 +92,15 @@ const createViewPageSlice: StateCreator< ViewState, [], [], ViewPageSlice > = (set, get) => ({ ...VIEW_PAGE_DEFAULT, - setPageData: (pageData: PageData) => { + updatePageData: (pageData: PageData) => { set({ + beginLineNumToLogEventNum: pageData.beginLineNumToLogEventNum, logData: pageData.logs, + logEventNum: pageData.logEventNum, numPages: pageData.numPages, pageNum: pageData.pageNum, - beginLineNumToLogEventNum: pageData.beginLineNumToLogEventNum, }); - }, - updatePageData: (pageData: PageData) => { - const {setPageData} = get(); - setPageData(pageData); - const newLogEventNum = pageData.logEventNum; - updateWindowUrlHashParams({logEventNum: newLogEventNum}); - const {updateLogEventNum} = get(); - updateLogEventNum(newLogEventNum); - const {setUiState} = useUiStore.getState(); - setUiState(UI_STATE.READY); + updateWindowUrlHashParams({logEventNum: pageData.logEventNum}); }, loadPageByAction: (navAction: NavigationAction) => { if (navAction.code === ACTION_NAME.RELOAD) { @@ -149,9 +141,9 @@ const createViewPageSlice: StateCreator< const {logFileManagerProxy} = useLogFileManagerStore.getState(); const {isPrettified} = get(); const pageData = await logFileManagerProxy.loadPage(cursor, isPrettified); - const {updatePageData} = get(); updatePageData(pageData); + setUiState(UI_STATE.READY); })().catch(handleErrorWithNotification); }, }); diff --git a/src/stores/viewStore/types.ts b/src/stores/viewStore/types.ts index 428a50d9b..1ade75a28 100644 --- a/src/stores/viewStore/types.ts +++ b/src/stores/viewStore/types.ts @@ -15,7 +15,6 @@ interface ViewPageValues { interface ViewPageActions { loadPageByAction: (navAction: NavigationAction) => void; - setPageData: (pageData: PageData) => void; updatePageData: (pageData: PageData) => void; } @@ -26,7 +25,7 @@ interface ViewEventValues { } interface ViewEventActions { - updateLogEventNum: (newLogEventNum: number) => void; + setLogEventNum: (newLogEventNum: number) => void; } type ViewEventSlice = ViewEventValues & ViewEventActions; @@ -36,7 +35,7 @@ interface ViewFormattingValues { } interface ViewFormattingActions { - updateIsPrettified: (newIsPrettified: boolean) => void; + setIsPrettified: (newIsPrettified: boolean) => void; } type ViewFormattingSlice = ViewFormattingValues & ViewFormattingActions; diff --git a/src/typings/worker.ts b/src/typings/worker.ts index fa01ecbea..44704d614 100644 --- a/src/typings/worker.ts +++ b/src/typings/worker.ts @@ -80,7 +80,6 @@ type LogFileInfo = { type PageData = { beginLineNumToLogEventNum: BeginLineNumToLogEventNumMap; - cursorLineNum: number; logEventNum: number; logs: string; numPages: number; @@ -92,7 +91,6 @@ type PageData = { */ const EMPTY_PAGE_RESP: PageData = Object.freeze({ beginLineNumToLogEventNum: new Map(), - cursorLineNum: 1, logEventNum: 0, logs: "", numPages: 1, diff --git a/src/utils/url.ts b/src/utils/url/index.ts similarity index 99% rename from src/utils/url.ts rename to src/utils/url/index.ts index b90b13f01..a9398c5ee 100644 --- a/src/utils/url.ts +++ b/src/utils/url/index.ts @@ -6,11 +6,11 @@ import { UrlHashParamUpdatesType, UrlSearchParams, UrlSearchParamUpdatesType, -} from "../typings/url"; +} from "../../typings/url"; import { findNearestLessThanOrEqualElement, isWithinBounds, -} from "../utils/data"; +} from "../data"; /** diff --git a/src/utils/url/urlHash.ts b/src/utils/url/urlHash.ts new file mode 100644 index 000000000..b9a37e17a --- /dev/null +++ b/src/utils/url/urlHash.ts @@ -0,0 +1,142 @@ +import useLogFileManagerProxyStore from "../../stores/logFileManagerProxyStore.ts"; +import useLogFileStore from "../../stores/logFileStore.ts"; +import {handleErrorWithNotification} from "../../stores/notificationStore.ts"; +import useQueryStore from "../../stores/queryStore"; +import useUiStore from "../../stores/uiStore"; +import useViewStore from "../../stores/viewStore"; +import {Nullable} from "../../typings/common"; +import {UI_STATE} from "../../typings/states"; +import { + CURSOR_CODE, + CursorType, +} from "../../typings/worker"; +import {clamp} from "../math"; +import { + getWindowUrlHashParams, + updateUrlIfEventOnPage, + updateWindowUrlHashParams, + URL_HASH_PARAMS_DEFAULT, +} from "./index"; + + +/** + * Determines the cursor for navigating log events based on URL hash parameters. + * + * @param params An object containing the following properties: + * @param params.isPrettified Whether the log view is in prettified mode. + * @param params.logEventNum The log event number from the URL hash. + * @param params.timestamp The timestamp from the URL hash. + * @return `CursorType` object if a navigation action is needed, or `null` if no action is required. + */ +const getCursorFromHashParams = ({isPrettified, logEventNum, timestamp}: { + isPrettified: boolean; logEventNum: number; timestamp: number; +}): Nullable => { + const {numEvents} = useLogFileStore.getState(); + if (0 === numEvents) { + updateWindowUrlHashParams({logEventNum: URL_HASH_PARAMS_DEFAULT.logEventNum}); + + return null; + } + + const { + isPrettified: prevIsPrettified, setIsPrettified, setLogEventNum, + } = useViewStore.getState(); + const clampedLogEventNum = clamp(logEventNum, 1, numEvents); + + if (isPrettified !== prevIsPrettified) { + setIsPrettified(isPrettified); + + return { + code: CURSOR_CODE.EVENT_NUM, + args: {eventNum: clampedLogEventNum}, + }; + } + + if (timestamp !== URL_HASH_PARAMS_DEFAULT.timestamp) { + return { + code: CURSOR_CODE.TIMESTAMP, + args: {timestamp: timestamp}, + }; + } else if (logEventNum !== URL_HASH_PARAMS_DEFAULT.logEventNum) { + setLogEventNum(clampedLogEventNum); + updateWindowUrlHashParams({logEventNum: clampedLogEventNum}); + 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. + return null; + } + + return { + code: CURSOR_CODE.EVENT_NUM, + args: {eventNum: clampedLogEventNum}, + }; + } + + // If we reach here, we have no valid cursor. + return null; +}; + +/** + * Updates view-related states from URL hash parameters. + * NOTE: this may modify the URL parameters. + */ +const updateViewHashParams = () => { + const {isPrettified, logEventNum, timestamp} = getWindowUrlHashParams(); + updateWindowUrlHashParams({ + isPrettified: URL_HASH_PARAMS_DEFAULT.isPrettified, + timestamp: URL_HASH_PARAMS_DEFAULT.timestamp, + }); + + const cursor = getCursorFromHashParams({isPrettified, logEventNum, timestamp}); + if (null === cursor) { + // If no cursor was set, we can return early. + return; + } + + (async () => { + const {logFileManagerProxy} = useLogFileManagerProxyStore.getState(); + const {updatePageData} = useViewStore.getState(); + const pageData = await logFileManagerProxy.loadPage(cursor, isPrettified); + updatePageData(pageData); + const {setUiState} = useUiStore.getState(); + setUiState(UI_STATE.READY); + })().catch(handleErrorWithNotification); +}; + +/** + * Updates query-related states from URL hash parameters. + * NOTE: this may modify the URL parameters. + * + * @return Whether any query-related parameters were modified. + */ +const updateQueryHashParams = () => { + const {queryIsCaseSensitive, queryIsRegex, queryString} = getWindowUrlHashParams(); + updateWindowUrlHashParams({queryIsCaseSensitive, queryIsRegex, queryString}); + + const { + queryIsCaseSensitive: currentQueryIsCaseSensitive, + queryIsRegex: currentQueryIsRegex, + queryString: currentQueryString, + setQueryIsCaseSensitive, + setQueryIsRegex, + setQueryString, + } = useQueryStore.getState(); + + let isQueryModified = false; + isQueryModified ||= queryIsCaseSensitive !== currentQueryIsCaseSensitive; + setQueryIsCaseSensitive(queryIsCaseSensitive); + + isQueryModified ||= queryIsRegex !== currentQueryIsRegex; + setQueryIsRegex(queryIsRegex); + + isQueryModified ||= queryString !== currentQueryString; + setQueryString(queryString); + + return isQueryModified; +}; + +export { + updateQueryHashParams, + updateViewHashParams, +};