diff --git a/docs/src/dev-guide/contributing-validation.md b/docs/src/dev-guide/contributing-validation.md index 12f597409..fd1fcf757 100644 --- a/docs/src/dev-guide/contributing-validation.md +++ b/docs/src/dev-guide/contributing-validation.md @@ -40,5 +40,6 @@ still be tested manually: * Toggling tabbed panels in the sidebar * Using keyboard shortcuts * Toggling "Prettify" in both the status bar and the address bar + * Select different timezone in the "Timezone" dropdown menu in the status bar or URL [gh-workflow-test]: https://github.com/y-scope/yscope-log-viewer/blob/main/.github/workflows/test.yaml diff --git a/src/components/AppController.tsx b/src/components/AppController.tsx index 5ab15563d..db5b29d94 100644 --- a/src/components/AppController.tsx +++ b/src/components/AppController.tsx @@ -208,9 +208,9 @@ const AppController = ({children}: AppControllerProps) => { code: CURSOR_CODE.EVENT_NUM, args: {eventNum: clampedLogEventNum}, }; - const {isPrettified} = useViewStore.getState(); + const {isPrettified, logTimezone} = useViewStore.getState(); - const pageData = await logFileManagerProxy.loadPage(cursor, isPrettified); + const pageData = await logFileManagerProxy.loadPage(cursor, isPrettified, logTimezone); const {updatePageData} = useViewStore.getState(); updatePageData(pageData); })().catch(handleErrorWithNotification); diff --git a/src/components/StatusBar/TimezoneSelect/index.css b/src/components/StatusBar/TimezoneSelect/index.css new file mode 100644 index 000000000..82ef6d1ac --- /dev/null +++ b/src/components/StatusBar/TimezoneSelect/index.css @@ -0,0 +1,31 @@ +/* Since timezone is not multi select, and multi select has a different style in Joy components, so we have to manually + tune the style +*/ +.timezone-select { + margin-right: 1px; +} + +.timezone-select-render-value-box { + display: flex; + gap: 2px; +} + +.timezone-select-render-value-box-label { + padding: 0 !important; + + font-size: 0.95rem !important; + font-weight: 500 !important; + line-height: 1.5 !important; + + background-color: initial !important; + border: none !important; + box-shadow: none !important; +} + +.timezone-select-render-value-box-label-disabled { + color: #686f76 !important; +} + +.timezone-select-listbox { + max-height: calc(100vh - var(--ylv-menu-bar-height) - var(--ylv-status-bar-height)) !important; +} diff --git a/src/components/StatusBar/TimezoneSelect/index.tsx b/src/components/StatusBar/TimezoneSelect/index.tsx new file mode 100644 index 000000000..f91144eb3 --- /dev/null +++ b/src/components/StatusBar/TimezoneSelect/index.tsx @@ -0,0 +1,222 @@ +import React, { + useCallback, + useEffect, + useState, +} from "react"; + +import {SelectValue} from "@mui/base/useSelect"; +import { + Box, + Chip, + ListDivider, + ListItemContent, + Option, + Select, + SelectOption, +} from "@mui/joy"; + +import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp"; + +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 {isDisabled} from "../../../utils/states"; +import {updateWindowUrlHashParams} from "../../../utils/url"; + +import "./index.css"; + + +const LOGGER_TIMEZONE = "Logger Timezone"; +const COMMON_TIMEZONES = [ + "America/New_York", + "Asia/Shanghai", + "Asia/Tokyo", + "Australia/Sydney", + "Pacific/Honolulu", + "America/Los_Angeles", + "America/Chicago", + "America/Denver", + "Asia/Kolkata", + "Europe/Berlin", + "Europe/Moscow", + "Asia/Dubai", + "Asia/Singapore", + "Asia/Seoul", + "Pacific/Auckland", +]; + +/** + * Convert the timezone string to GMT +/- minutes + * + * @param tz + * @return The GMT +/- minutes shown before the timezone string + */ +const getLongOffsetOfTimezone = (tz: string): string => { + return new Intl.DateTimeFormat("default", { + timeZone: tz, + timeZoneName: "longOffset", + }).formatToParts() + .find((p) => "timeZoneName" === p.type)?.value ?? "Unknown timezone"; +}; + +/** + * Render the selected timezone option in the status bar + * + * @param selected + * @return The selected timezone shown in the status bar + */ +const handleRenderValue = (selected: SelectValue, false>) => ( + + Timezone + + {selected?.label} + + +); + +/** + * Render the timezone options in the dropdown menu + * + * @param value + * @param label + * @param onClick + * @param suffix + * @return An option box in the dropdown menu + */ +const renderTimezoneOption = ( + value: string, + label: string, + onClick: React.MouseEventHandler, + suffix?: string +) => ( + +); + +/** + * The timezone select dropdown menu, the selectable options can be classified as three types: + * - Default (use the origin timezone of the log events) + * - Browser Timezone (use the timezone that the browser is currently using) + * - Frequently-used Timezone + * + * @return A timezone select dropdown menu + */ +const TimezoneSelect = () => { + const uiState = useUiStore((state) => state.uiState); + + const {logTimezone} = useViewStore.getState(); + const updateLogTimezone = useViewStore((state) => state.updateLogTimezone); + + const [browserTimezone, setBrowserTimezone] = useState(null); + const [selectedTimezone, setSelectedTimezone] = useState(null); + + const disabled = isDisabled(uiState, UI_ELEMENT.TIMEZONE_SETTER); + + useEffect(() => { + const tz = new Intl.DateTimeFormat().resolvedOptions().timeZone; + setBrowserTimezone(tz); + }, []); + + useEffect(() => { + if (!disabled && selectedTimezone !== logTimezone) { + const updatedTimezone = (LOGGER_TIMEZONE === selectedTimezone) ? + "" : + (selectedTimezone ?? ""); + + updateWindowUrlHashParams({ + [HASH_PARAM_NAMES.LOG_TIMEZONE]: updatedTimezone, + }); + updateLogTimezone(updatedTimezone); + } + }, [ + disabled, + logTimezone, + selectedTimezone, + updateLogTimezone, + ]); + + const handleOptionClick = useCallback((ev: React.MouseEvent) => { + const currentTarget = ev.currentTarget as HTMLElement; + const value = currentTarget.dataset.value ?? LOGGER_TIMEZONE; + setSelectedTimezone(value); + }, []); + + return ( + + ); +}; + +export default TimezoneSelect; diff --git a/src/components/StatusBar/index.tsx b/src/components/StatusBar/index.tsx index 2f231f64b..17e48e4cb 100644 --- a/src/components/StatusBar/index.tsx +++ b/src/components/StatusBar/index.tsx @@ -23,6 +23,7 @@ import { } from "../../utils/url"; import LogLevelSelect from "./LogLevelSelect"; import StatusBarToggleButton from "./StatusBarToggleButton"; +import TimezoneSelect from "./TimezoneSelect"; import "./index.css"; @@ -70,6 +71,7 @@ const StatusBar = () => { {/* This is left blank intentionally until status messages are implemented. */} +