diff --git a/src/components/MenuBar/TimestampQueryContainer.css b/src/components/MenuBar/TimestampQueryContainer.css new file mode 100644 index 000000000..b828f8a20 --- /dev/null +++ b/src/components/MenuBar/TimestampQueryContainer.css @@ -0,0 +1,3 @@ +.timestamp-query-container { + display: flex; +} diff --git a/src/components/MenuBar/TimestampQueryContainer.tsx b/src/components/MenuBar/TimestampQueryContainer.tsx new file mode 100644 index 000000000..9194e7cb3 --- /dev/null +++ b/src/components/MenuBar/TimestampQueryContainer.tsx @@ -0,0 +1,61 @@ +import { + useCallback, + useState, +} from "react"; + +import { + Box, + Divider, +} from "@mui/joy"; + +import CalendarTodayIcon from "@mui/icons-material/CalendarToday"; + +import useUiStore from "../../stores/uiStore"; +import {UI_ELEMENT} from "../../typings/states"; +import {isDisabled} from "../../utils/states"; +import MenuBarIconButton from "./MenuBarIconButton"; +import TimestampQueryInput from "./TimestampQueryInput"; + +import "./TimestampQueryContainer.css"; + + +/** + * Wraps the timestamp query input and toggles its visibility using a calendar button. + * + * @return + */ +const TimestampQueryContainer = () => { + const [isInputVisible, setIsInputVisible] = useState(false); + const uiState = useUiStore((state) => state.uiState); + + const handleInputVisibilityToggle = useCallback(() => { + setIsInputVisible((prev) => !prev); + }, []); + + return ( + + {false === isInputVisible && ( + <> + + + + + + + )} +
+ +
+
+ ); +}; + +export default TimestampQueryContainer; diff --git a/src/components/MenuBar/TimestampQueryInput.css b/src/components/MenuBar/TimestampQueryInput.css new file mode 100644 index 000000000..b7d1a173a --- /dev/null +++ b/src/components/MenuBar/TimestampQueryInput.css @@ -0,0 +1,6 @@ +.timestamp-query-input { + display: flex; + flex-direction: row; + align-items: center; + height: var(--ylv-menu-bar-height); +} diff --git a/src/components/MenuBar/TimestampQueryInput.tsx b/src/components/MenuBar/TimestampQueryInput.tsx new file mode 100644 index 000000000..51d71a279 --- /dev/null +++ b/src/components/MenuBar/TimestampQueryInput.tsx @@ -0,0 +1,87 @@ +import React, {useCallback} from "react"; + +import { + Box, + Input, + Tooltip, +} from "@mui/joy"; + +import CollapseIcon from "@mui/icons-material/KeyboardDoubleArrowRight"; +import SearchIcon from "@mui/icons-material/Search"; + +import useUiStore from "../../stores/uiStore"; +import useViewStore from "../../stores/viewStore"; +import {UI_ELEMENT} from "../../typings/states"; +import {isDisabled} from "../../utils/states"; +import {updateWindowUrlHashParams} from "../../utils/url"; +import {updateViewHashParams} from "../../utils/url/urlHash"; +import MenuBarIconButton from "./MenuBarIconButton"; + +import "./TimestampQueryInput.css"; + + +interface TimestampQueryInputProps { + onInputCollapse: () => void; +} + +/** + * Renders an input allowing the user to jump to the nearest log event at or before a specified UTC + * datetime. Collapses the input when requested. + * + * @param props + * @param props.onInputCollapse + * @return + */ +const TimestampQueryInput = ({onInputCollapse}: TimestampQueryInputProps) => { + const uiState = useUiStore((state) => state.uiState); + const dateTimeString = useViewStore((state) => state.dateTimeString); + + const handleTimestampQuery = useCallback(() => { + const timestamp = new Date(`${dateTimeString}Z`).getTime(); + updateWindowUrlHashParams({timestamp: timestamp}); + updateViewHashParams(); + }, [dateTimeString]); + + const handleKeyboardEnterPress = useCallback((ev: React.KeyboardEvent) => { + if ("Enter" === ev.key) { + handleTimestampQuery(); + } + }, [handleTimestampQuery]); + + const handleDateTimeInputChange = useCallback((e: React.ChangeEvent) => { + const {setDateTimeString} = useViewStore.getState(); + setDateTimeString(e.currentTarget.value); + }, []); + + return ( + + + + + + } + startDecorator={ + + + + } + onChange={handleDateTimeInputChange} + onKeyDown={handleKeyboardEnterPress}/> + + + ); +}; + +export default TimestampQueryInput; diff --git a/src/components/MenuBar/index.css b/src/components/MenuBar/index.css index f9e036567..1f5fe6bac 100644 --- a/src/components/MenuBar/index.css +++ b/src/components/MenuBar/index.css @@ -47,3 +47,13 @@ z-index: var(--ylv-loading-progress-z-index); margin-bottom: -2px; } + +.timestamp-query-input-wrapper { + overflow: hidden; + max-width: 0; + transition: max-width 0.3s ease; +} + +.timestamp-query-input-wrapper.expanded { + max-width: 320px; +} diff --git a/src/components/MenuBar/index.tsx b/src/components/MenuBar/index.tsx index 6594415a9..dd03d1172 100644 --- a/src/components/MenuBar/index.tsx +++ b/src/components/MenuBar/index.tsx @@ -21,6 +21,7 @@ import {isDisabled} from "../../utils/states"; import ExportLogsButton from "./ExportLogsButton"; import MenuBarIconButton from "./MenuBarIconButton"; import NavigationBar from "./NavigationBar"; +import TimestampQueryContainer from "./TimestampQueryContainer"; import "./index.css"; @@ -86,7 +87,8 @@ const MenuBar = () => { - + + diff --git a/src/stores/viewStore/createViewEventSlice.ts b/src/stores/viewStore/createViewEventSlice.ts index b7a6bb578..72fda9ec7 100644 --- a/src/stores/viewStore/createViewEventSlice.ts +++ b/src/stores/viewStore/createViewEventSlice.ts @@ -9,6 +9,8 @@ import { const VIEW_EVENT_DEFAULT: ViewEventValues = { logEventNum: 0, + dateTimeString: new Date().toISOString() + .slice(0, -1), }; /** @@ -24,6 +26,9 @@ const createViewEventSlice: StateCreator< setLogEventNum: (logEventNum: number) => { set({logEventNum}); }, + setDateTimeString: (dateTimeString: string) => { + set({dateTimeString}); + }, }); export {VIEW_EVENT_DEFAULT}; diff --git a/src/stores/viewStore/types.ts b/src/stores/viewStore/types.ts index d2d016330..5849c0b93 100644 --- a/src/stores/viewStore/types.ts +++ b/src/stores/viewStore/types.ts @@ -24,10 +24,12 @@ type ViewPageSlice = ViewPageValues & ViewPageActions; interface ViewEventValues { logEventNum: number; + dateTimeString: string; } interface ViewEventActions { setLogEventNum: (newLogEventNum: number) => void; + setDateTimeString: (newDateTimeString: string) => void; } type ViewEventSlice = ViewEventValues & ViewEventActions; diff --git a/src/utils/url/urlHash.ts b/src/utils/url/urlHash.ts index 6b44109c7..e07a43774 100644 --- a/src/utils/url/urlHash.ts +++ b/src/utils/url/urlHash.ts @@ -17,6 +17,19 @@ import { } from "./index"; +/** + * Converts a timestamp to an ISO 8601 date-time string (without the 'Z' suffix) + * + * @param timestamp + */ +const updateDateTimeString = (timestamp: number) => { + const dateTimeString = new Date(timestamp).toISOString() + .slice(0, -1); + + const {setDateTimeString} = useViewStore.getState(); + setDateTimeString(dateTimeString); +}; + /** * Determines the cursor for navigating log events based on URL hash parameters. * @@ -26,6 +39,7 @@ import { * @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. */ +// eslint-disable-next-line max-statements const getCursorFromHashParams = ({isPrettified, logEventNum, timestamp}: { isPrettified: boolean; logEventNum: number; timestamp: number; }): Nullable => { @@ -56,6 +70,8 @@ const getCursorFromHashParams = ({isPrettified, logEventNum, timestamp}: { } if (timestamp !== URL_HASH_PARAMS_DEFAULT.timestamp) { + updateDateTimeString(timestamp); + return { code: CURSOR_CODE.TIMESTAMP, args: {timestamp: timestamp},