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},