diff --git a/components/src/TimeRangeSelector/DateTimeRangePicker.tsx b/components/src/TimeRangeSelector/DateTimeRangePicker.tsx index 282a3cf..8a1691f 100644 --- a/components/src/TimeRangeSelector/DateTimeRangePicker.tsx +++ b/components/src/TimeRangeSelector/DateTimeRangePicker.tsx @@ -11,20 +11,21 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { ReactElement, useState } from 'react'; +import { ReactElement, useMemo, useState } from 'react'; import { Box, Stack, Typography, Button } from '@mui/material'; import { DateTimeField, LocalizationProvider, StaticDateTimePicker } from '@mui/x-date-pickers'; import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFnsV3'; import { AbsoluteTimeRange } from '@perses-dev/core'; -import { useTimeZone } from '../context'; import { ErrorBoundary } from '../ErrorBoundary'; import { ErrorAlert } from '../ErrorAlert'; +import { formatWithTimeZone } from '../utils/format'; import { DATE_TIME_FORMAT, validateDateRange } from './utils'; interface AbsoluteTimeFormProps { initialTimeRange: AbsoluteTimeRange; onChange: (timeRange: AbsoluteTimeRange) => void; onCancel: () => void; + timeZone: string; } type AbsoluteTimeRangeInputValue = { @@ -40,62 +41,47 @@ type AbsoluteTimeRangeInputValue = { * @param onCancel event received when user click on cancel * @constructor */ -export const DateTimeRangePicker = ({ initialTimeRange, onChange, onCancel }: AbsoluteTimeFormProps): ReactElement => { - const { formatWithUserTimeZone } = useTimeZone(); - +export const DateTimeRangePicker = ({ + initialTimeRange, + onChange, + onCancel, + timeZone, +}: AbsoluteTimeFormProps): ReactElement => { // Time range values as dates that can be used as a time range. const [timeRange, setTimeRange] = useState(initialTimeRange); - - // Time range values as strings used to populate the text inputs. May not - // be valid as dates when the user is typing. - const [timeRangeInputs, setTimeRangeInputs] = useState({ - start: formatWithUserTimeZone(initialTimeRange.start, DATE_TIME_FORMAT), - end: formatWithUserTimeZone(initialTimeRange.end, DATE_TIME_FORMAT), - }); + const timeRangeInputs = useMemo(() => { + return { + start: formatWithTimeZone(timeRange.start, DATE_TIME_FORMAT, timeZone), + end: formatWithTimeZone(timeRange.end, DATE_TIME_FORMAT, timeZone), + }; + }, [timeRange.start, timeRange.end, timeZone]); const [showStartCalendar, setShowStartCalendar] = useState(true); - const changeTimeRange = (newTime: string | Date, segment: keyof AbsoluteTimeRange): void => { - const isInputChange = typeof newTime === 'string'; - const newInputTime = isInputChange ? newTime : formatWithUserTimeZone(newTime, DATE_TIME_FORMAT); - - setTimeRangeInputs((prevTimeRangeInputs) => { + const changeTimeRange = (newTime: Date, segment: keyof AbsoluteTimeRange): void => { + setTimeRange((prevTimeRange) => { return { - ...prevTimeRangeInputs, - [segment]: newInputTime, + ...prevTimeRange, + [segment]: newTime, }; }); - - // When the change is a string from an input, do not try to convert it to - // a date because there are likely to be interim stages of editing where it - // is not valid as a date. When the change is a Date from the calendar/clock - // interface, we can be sure it is a date. - if (!isInputChange) { - setTimeRange((prevTimeRange) => { - return { - ...prevTimeRange, - [segment]: newTime, - }; - }); - } }; - const onChangeStartTime = (newStartTime: string | Date): void => { + const onChangeStartTime = (newStartTime: Date): void => { changeTimeRange(newStartTime, 'start'); }; - const onChangeEndTime = (newEndTime: string | Date): void => { + const onChangeEndTime = (newEndTime: Date): void => { changeTimeRange(newEndTime, 'end'); }; const updateDateRange = (): { start: Date; end: Date } | undefined => { const newDates = { - start: new Date(timeRangeInputs.start), - end: new Date(timeRangeInputs.end), + start: timeRange.start, + end: timeRange.end, }; const isValidDateRange = validateDateRange(newDates.start, newDates.end); if (isValidDateRange) { - setTimeRange(newDates); return newDates; } }; @@ -131,7 +117,7 @@ export const DateTimeRangePicker = ({ initialTimeRange, onChange, onCancel }: Ab displayStaticWrapperAs="desktop" openTo="day" disableHighlightToday={true} - value={initialTimeRange.start} + value={timeRange.start} onChange={(newValue) => { if (newValue === null) return; onChangeStartTime(newValue); @@ -157,7 +143,7 @@ export const DateTimeRangePicker = ({ initialTimeRange, onChange, onCancel }: Ab displayStaticWrapperAs="desktop" openTo="day" disableHighlightToday={true} - value={initialTimeRange.end} + value={timeRange.end} minDateTime={timeRange.start} onChange={(newValue) => { if (newValue === null) return; diff --git a/components/src/TimeRangeSelector/TimeRangeSelector.tsx b/components/src/TimeRangeSelector/TimeRangeSelector.tsx index 69308ef..83cc6d7 100644 --- a/components/src/TimeRangeSelector/TimeRangeSelector.tsx +++ b/components/src/TimeRangeSelector/TimeRangeSelector.tsx @@ -11,12 +11,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Box, MenuItem, Popover, Select } from '@mui/material'; +import { Box, MenuItem, Popover, Select, IconButton, TextField } from '@mui/material'; import Calendar from 'mdi-material-ui/Calendar'; +import EarthIcon from 'mdi-material-ui/Earth'; import { TimeRangeValue, isRelativeTimeRange, AbsoluteTimeRange, toAbsoluteTimeRange } from '@perses-dev/core'; import { ReactElement, useMemo, useRef, useState } from 'react'; import { useTimeZone } from '../context'; +import { TimeZoneOption, getTimeZoneOptions } from '../model/timeZoneOption'; import { TimeOption } from '../model'; +import { SettingsAutocomplete, SettingsAutocompleteOption } from '../SettingsAutocomplete'; import { DateTimeRangePicker } from './DateTimeRangePicker'; import { buildCustomTimeOption, formatTimeRange } from './utils'; @@ -43,6 +46,10 @@ interface TimeRangeSelectorProps { * Defaults to true. */ showCustomTimeRange?: boolean; + /** Optional explicit timezone and change handler to enable changing tz from the selector */ + timeZone?: string; + timeZoneOptions?: TimeZoneOption[]; + onTimeZoneChange?: (timeZone: TimeZoneOption) => void; } /** @@ -57,48 +64,74 @@ export function TimeRangeSelector({ onChange, height, showCustomTimeRange = true, + timeZone: timeZoneProp, + timeZoneOptions, + onTimeZoneChange, }: TimeRangeSelectorProps): ReactElement { - const { timeZone } = useTimeZone(); + const { timeZone: ctxTimeZone } = useTimeZone(); + const timeZone = timeZoneProp ?? ctxTimeZone; - const anchorEl = useRef(); // Used to position the absolute time range picker - - // Control the open state of the absolute time range picker + const anchorEl = useRef(); const [showCustomDateSelector, setShowCustomDateSelector] = useState(false); - // Build the initial value of the absolute time range picker const convertedTimeRange = useMemo(() => { return isRelativeTimeRange(value) ? toAbsoluteTimeRange(value) : value; }, [value]); - // Last option is the absolute time range option (custom) - // If the value is an absolute time range, we display this time range - // If the value is a relative time range, we make a default CustomTimeOption built from undefined value const lastOption = useMemo( () => buildCustomTimeOption(isRelativeTimeRange(value) ? undefined : value, timeZone), [value, timeZone] ); - // Control the open state of the select component to prevent the menu from closing when the custom date picker is - // opened. - // - // Note that the value state of the select is here for display only. The real value (the one from props) is managed - // by click events on each menu item. - // This is a trick to get around the limitation of select with menu item that doesn't support objects as value... const [open, setOpen] = useState(false); + const tzOptions = timeZoneOptions ?? getTimeZoneOptions(); + const [tzAnchorEl, setTzAnchorEl] = useState(null); + const tzOpen = Boolean(tzAnchorEl); + const tzLabel = tzOptions.find((o) => o.value === timeZone)?.display ?? timeZone; + const tzAutocompleteOptions = tzOptions.map((o) => ({ id: o.value, label: o.display })); + let tzAutocompleteValue: SettingsAutocompleteOption | undefined = undefined; + { + const current = tzOptions.find((o) => o.value === timeZone); + if (current) tzAutocompleteValue = { id: current.value, label: current.display }; + } return ( <> + setTzAnchorEl(null)} + sx={(theme) => ({ padding: theme.spacing(1) })} + > + { + e.stopPropagation(); + }} + > + { + if (option) { + const selected = tzOptions.find((o) => o.value === option.id); + if (selected) onTimeZoneChange?.(selected); + } + setTzAnchorEl(null); + }} + disableClearable + renderInput={(params) => } + /> + + setShowCustomDateSelector(false)} - sx={(theme) => ({ - padding: theme.spacing(2), - })} + sx={(theme) => ({ padding: theme.spacing(2) })} > setShowCustomDateSelector(false)} + timeZone={timeZone} /> @@ -116,22 +150,41 @@ export function TimeRangeSelector({ value={formatTimeRange(value, timeZone)} onClick={() => setOpen(!open)} IconComponent={Calendar} - inputProps={{ - 'aria-label': `Select time range. Currently set to ${value}`, - }} + inputProps={{ 'aria-label': `Select time range. Currently set to ${value}` }} sx={{ - // `transform: none` prevents calendar icon from flipping over when menu is open - '.MuiSelect-icon': { - marginTop: '1px', - transform: 'none', - }, - // paddingRight creates more space for the calendar icon (it's a bigger icon) - '.MuiSelect-select.MuiSelect-outlined.MuiInputBase-input': { - paddingRight: '36px', - }, + '.MuiSelect-icon': { marginTop: '1px', transform: 'none' }, + '.MuiSelect-select.MuiSelect-outlined.MuiInputBase-input': { paddingRight: '36px' }, '.MuiSelect-select': height ? { lineHeight: height, paddingY: 0 } : {}, }} > + { + e.preventDefault(); + e.stopPropagation(); + }} + sx={{ cursor: 'default', '&:hover': { backgroundColor: 'transparent' }, py: 0.5, px: 1 }} + > + + + Time Range + Timezone: {tzLabel} + + + { + e.preventDefault(); + e.stopPropagation(); + setTzAnchorEl(e.currentTarget); + }} + > + + + + + {timeOptions.map((item, idx) => ( { - const { formattedTime, formattedDate } = getDateAndTime(timeMs); + const date = new Date(timeMs); + const formattedDate = formatWithUserTimeZone(date, 'MMM dd, yyyy - '); + const formattedTime = formatWithUserTimeZone(date, 'HH:mm:ss'); return ( { + value: string; + onChange?: (timeZoneOption: TimeZoneOption) => void; + timeZoneOptions?: TimeZoneOption[]; + variant?: 'standard' | 'compact'; + heightPx?: string | number; +} + +/** + * Timezone selector component + * Allows users to select a timezone from a dropdown list + */ +export function TimeZoneSelector({ + value, + onChange, + timeZoneOptions, + variant = 'standard', + heightPx, + ...selectProps +}: TimeZoneSelectorProps): ReactElement { + const options = useMemo(() => timeZoneOptions ?? getTimeZoneOptions(), [timeZoneOptions]); + + const height = heightPx ? (typeof heightPx === 'number' ? `${heightPx}px` : heightPx) : undefined; + + const handleChange = (selectedValue: string): void => { + const selectedOption = options.find((opt: TimeZoneOption) => opt.value === selectedValue); + if (selectedOption && onChange) { + onChange(selectedOption); + } + }; + + const sxStyles = useMemo( + () => ({ + minWidth: variant === 'compact' ? '80px' : '150px', + ...(height && { lineHeight: height, paddingY: 0 }), + ...selectProps.sx, + }), + [variant, height, selectProps.sx] + ); + + return ( + + ); +} diff --git a/components/src/index.ts b/components/src/index.ts index 77aad8b..72d7d93 100644 --- a/components/src/index.ts +++ b/components/src/index.ts @@ -36,6 +36,7 @@ export * from './SortSelector'; export * from './Table'; export * from './ThresholdsEditor'; export * from './TimeRangeSelector'; +export * from './TimeZoneSelector'; export * from './TimeSeriesTooltip'; export * from './ToolbarIconButton'; export * from './FormatControls'; diff --git a/components/src/model/index.ts b/components/src/model/index.ts index b1d0562..716c0a8 100644 --- a/components/src/model/index.ts +++ b/components/src/model/index.ts @@ -14,3 +14,4 @@ export * from './graph'; export * from './theme'; export * from './timeOption'; +export * from './timeZoneOption'; diff --git a/components/src/model/timeZoneOption.ts b/components/src/model/timeZoneOption.ts new file mode 100644 index 0000000..e3ac916 --- /dev/null +++ b/components/src/model/timeZoneOption.ts @@ -0,0 +1,28 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +export interface TimeZoneOption { + value: string; + display: string; +} + +/** + * Get all available timezone options + * @returns Array of timezone options + */ +export function getTimeZoneOptions(): TimeZoneOption[] { + const native = + (Intl as unknown as { supportedValuesOf?: (k: string) => string[] }).supportedValuesOf?.('timeZone') ?? []; + const values = ['local', 'UTC', ...native].filter((v, i, arr) => arr.indexOf(v) === i); + return values.map((value) => ({ value, display: value === 'local' ? 'Local' : value.replace(/_/g, ' ') })); +} diff --git a/dashboards/src/components/DashboardStickyToolbar/DashboardStickyToolbar.tsx b/dashboards/src/components/DashboardStickyToolbar/DashboardStickyToolbar.tsx index f4dec0b..fd5ca70 100644 --- a/dashboards/src/components/DashboardStickyToolbar/DashboardStickyToolbar.tsx +++ b/dashboards/src/components/DashboardStickyToolbar/DashboardStickyToolbar.tsx @@ -25,7 +25,7 @@ import { } from '@mui/material'; import PinOutline from 'mdi-material-ui/PinOutline'; import PinOffOutline from 'mdi-material-ui/PinOffOutline'; -import { TimeRangeControls } from '@perses-dev/plugin-system'; +import { TimeRangeControls, useTimeZoneParams } from '@perses-dev/plugin-system'; import { VariableList } from '../Variables'; interface DashboardStickyToolbarProps { @@ -41,6 +41,8 @@ export function DashboardStickyToolbar(props: DashboardStickyToolbarProps): Reac const isBiggerThanMd = useMediaQuery(useTheme().breakpoints.up('md')); + const { timeZone, setTimeZone } = useTimeZoneParams('local'); + return ( // marginBottom={-1} counteracts the marginBottom={1} on every variable input. // The margin on the inputs is for spacing between inputs, but is not meant to add space to bottom of the container. @@ -96,7 +98,7 @@ export function DashboardStickyToolbar(props: DashboardStickyToolbarProps): Reac direction="row" justifyContent="end" > - + setTimeZone(tz.value)} /> )} diff --git a/dashboards/src/components/DashboardToolbar/DashboardToolbar.tsx b/dashboards/src/components/DashboardToolbar/DashboardToolbar.tsx index 9f35d32..2109d23 100644 --- a/dashboards/src/components/DashboardToolbar/DashboardToolbar.tsx +++ b/dashboards/src/components/DashboardToolbar/DashboardToolbar.tsx @@ -13,7 +13,7 @@ import { Typography, Stack, Button, Box, useTheme, useMediaQuery, Alert } from '@mui/material'; import { ErrorBoundary, ErrorAlert } from '@perses-dev/components'; -import { TimeRangeControls } from '@perses-dev/plugin-system'; +import { TimeRangeControls, useTimeZoneParams } from '@perses-dev/plugin-system'; import { ReactElement, ReactNode } from 'react'; import { OnSaveDashboard, useEditMode } from '../../context'; import { AddPanelButton } from '../AddPanelButton'; @@ -52,6 +52,7 @@ export const DashboardToolbar = (props: DashboardToolbarProps): ReactElement => } = props; const { isEditMode } = useEditMode(); + const { timeZone, setTimeZone } = useTimeZoneParams('local'); const isBiggerThanSm = useMediaQuery(useTheme().breakpoints.up('sm')); const isBiggerThanMd = useMediaQuery(useTheme().breakpoints.up('md')); @@ -125,7 +126,7 @@ export const DashboardToolbar = (props: DashboardToolbarProps): ReactElement => - + setTimeZone(tz.value)} /> diff --git a/explore/src/components/ExploreToolbar/ExploreToolbar.tsx b/explore/src/components/ExploreToolbar/ExploreToolbar.tsx index 74274d7..0334b73 100644 --- a/explore/src/components/ExploreToolbar/ExploreToolbar.tsx +++ b/explore/src/components/ExploreToolbar/ExploreToolbar.tsx @@ -12,7 +12,7 @@ // limitations under the License. import { Stack, Box, useTheme, useMediaQuery } from '@mui/material'; -import { TimeRangeControls } from '@perses-dev/plugin-system'; +import { TimeRangeControls, useTimeZoneParams } from '@perses-dev/plugin-system'; import React, { ReactElement } from 'react'; export interface ExploreToolbarProps { @@ -23,6 +23,7 @@ export const ExploreToolbar = (props: ExploreToolbarProps): ReactElement => { const { exploreTitleComponent } = props; const isBiggerThanLg = useMediaQuery(useTheme().breakpoints.up('lg')); + const { timeZone, setTimeZone } = useTimeZoneParams('local'); const testId = 'explore-toolbar'; @@ -37,7 +38,7 @@ export const ExploreToolbar = (props: ExploreToolbarProps): ReactElement => { flexWrap={isBiggerThanLg ? 'nowrap' : 'wrap-reverse'} justifyContent="end" > - + setTimeZone(tz.value)} /> diff --git a/plugin-system/src/components/TimeRangeControls/TimeRangeControls.test.tsx b/plugin-system/src/components/TimeRangeControls/TimeRangeControls.test.tsx index af9fb54..281efa1 100644 --- a/plugin-system/src/components/TimeRangeControls/TimeRangeControls.test.tsx +++ b/plugin-system/src/components/TimeRangeControls/TimeRangeControls.test.tsx @@ -18,6 +18,10 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import React, { ReactElement } from 'react'; import { SnackbarProvider } from '@perses-dev/components'; import { TimeRangeProviderBasic, TimeRangeProviderWithQueryParams } from '@perses-dev/plugin-system'; +import { MemoryRouter } from 'react-router-dom'; +import { QueryParamProvider } from 'use-query-params'; +import { ReactRouter6Adapter } from 'use-query-params/adapters/react-router-6'; +import { useTimeZoneParams } from '../../runtime/TimeRangeProvider/query-params'; import { TimeRangeControls } from './TimeRangeControls'; /** @@ -28,9 +32,13 @@ export function renderWithContext(ui: React.ReactElement, options?: Omit ( - - {ui} - + + + + {ui} + + + ); return render(, options); @@ -40,6 +48,11 @@ describe('TimeRangeControls', () => { const testDefaultTimeRange = { pastDuration: '30m' as DurationString }; const testDefaultRefreshInterval = '0s'; + const ControlsWithTZ = (): ReactElement => { + const { timeZone, setTimeZone } = useTimeZoneParams('local'); + return setTimeZone(tz.value)} />; + }; + const renderTimeRangeControls = (testURLParams: boolean): void => { renderWithContext( <> @@ -48,14 +61,14 @@ describe('TimeRangeControls', () => { initialRefreshInterval={testDefaultRefreshInterval} initialTimeRange={testDefaultTimeRange} > - + ) : ( - + )} , diff --git a/plugin-system/src/components/TimeRangeControls/TimeRangeControls.tsx b/plugin-system/src/components/TimeRangeControls/TimeRangeControls.tsx index b24445e..6f615b3 100644 --- a/plugin-system/src/components/TimeRangeControls/TimeRangeControls.tsx +++ b/plugin-system/src/components/TimeRangeControls/TimeRangeControls.tsx @@ -23,6 +23,8 @@ import { TimeOption, ToolbarIconButton, TimeRangeSelector, + TimeZoneOption, + getTimeZoneOptions, buildRelativeTimeOption, } from '@perses-dev/components'; import { AbsoluteTimeRange, DurationString, parseDurationString, RelativeTimeRange } from '@perses-dev/core'; @@ -34,7 +36,6 @@ import { useTimeRangeOptionsSetting, useShowZoomRangeSetting, } from '../../runtime'; - export const DEFAULT_REFRESH_INTERVAL_OPTIONS: TimeOption[] = [ { value: { pastDuration: '0s' }, display: 'Off' }, { value: { pastDuration: '5s' }, display: '5s' }, @@ -55,6 +56,8 @@ interface TimeRangeControlsProps { showCustomTimeRange?: boolean; showZoomButtons?: boolean; timePresets?: TimeOption[]; + timeZone: string; + onTimeZoneChange: (timeZone: TimeZoneOption) => void; } export function TimeRangeControls({ @@ -65,6 +68,8 @@ export function TimeRangeControls({ showCustomTimeRange, showZoomButtons = true, timePresets, + timeZone, + onTimeZoneChange, }: TimeRangeControlsProps): ReactElement { const { timeRange, setTimeRange, refresh, refreshInterval, setRefreshInterval } = useTimeRange(); @@ -159,6 +164,13 @@ export function TimeRangeControls({ const setHalfTimeRange = (): void => setTimeRange(halfTimeRange()); const setDoubleTimeRange = (): void => setTimeRange(doubleTimeRange()); + const handleTimeZoneChange = useCallback( + (tz: TimeZoneOption) => { + onTimeZoneChange(tz); + }, + [onTimeZoneChange] + ); + return ( {showTimeRangeSelector && ( @@ -168,6 +180,9 @@ export function TimeRangeControls({ onChange={setTimeRange} height={height} showCustomTimeRange={showCustomTimeRangeValue} + timeZone={timeZone} + timeZoneOptions={getTimeZoneOptions()} + onTimeZoneChange={handleTimeZoneChange} /> )} {showZoomInOutButtons && ( diff --git a/plugin-system/src/runtime/TimeRangeProvider/TimeRangeProviders.tsx b/plugin-system/src/runtime/TimeRangeProvider/TimeRangeProviders.tsx index af3fe3e..8cdfb07 100644 --- a/plugin-system/src/runtime/TimeRangeProvider/TimeRangeProviders.tsx +++ b/plugin-system/src/runtime/TimeRangeProvider/TimeRangeProviders.tsx @@ -13,8 +13,9 @@ import { DurationString, TimeRangeValue } from '@perses-dev/core'; import React, { ReactElement } from 'react'; +import { TimeZoneProvider } from '@perses-dev/components'; import { TimeRangeProvider } from './TimeRangeProvider'; -import { useSetRefreshIntervalParams, useTimeRangeParams } from './query-params'; +import { useSetRefreshIntervalParams, useTimeRangeParams, useTimeZoneParams } from './query-params'; export interface TimeRangeProvidersProps { initialTimeRange: TimeRangeValue; @@ -29,6 +30,7 @@ export function TimeRangeProviderWithQueryParams({ }: TimeRangeProvidersProps): ReactElement { const { timeRange, setTimeRange } = useTimeRangeParams(initialTimeRange); const { refreshInterval, setRefreshInterval } = useSetRefreshIntervalParams(initialRefreshInterval); + const { timeZone } = useTimeZoneParams('local'); return ( - {children} + {children} ); } diff --git a/plugin-system/src/runtime/TimeRangeProvider/query-params.ts b/plugin-system/src/runtime/TimeRangeProvider/query-params.ts index e922a5b..78d64ab 100644 --- a/plugin-system/src/runtime/TimeRangeProvider/query-params.ts +++ b/plugin-system/src/runtime/TimeRangeProvider/query-params.ts @@ -12,7 +12,7 @@ // limitations under the License. import { useMemo, useCallback, useEffect, useState } from 'react'; -import { QueryParamConfig, useQueryParams } from 'use-query-params'; +import { QueryParamConfig, useQueryParams, StringParam } from 'use-query-params'; import { getUnixTime, isDate } from 'date-fns'; import { TimeRangeValue, @@ -92,6 +92,10 @@ export const refreshIntervalQueryConfig = { refresh: TimeRangeParam, }; +export const timeZoneQueryConfig = { + tz: StringParam, +}; + /** * Gets the initial time range taking into account URL params and dashboard JSON duration * Sets start query param if it is empty on page load @@ -200,3 +204,23 @@ export function useSetRefreshIntervalParams( setRefreshInterval: setRefreshInterval, }; } + +/** + * Returns timezone getter and setter, taking the URL query params. + * Defaults to 'local' when not set. + */ +export function useTimeZoneParams(initialTimeZone?: string): { timeZone: string; setTimeZone: (tz: string) => void } { + const [query, setQuery] = useQueryParams(timeZoneQueryConfig, { updateType: 'replaceIn' }); + const { tz } = query; + + const timeZone = (tz as string | undefined) ?? initialTimeZone ?? 'local'; + + const setTimeZone = useCallback( + (newTz: string) => { + setQuery({ tz: newTz }); + }, + [setQuery] + ); + + return { timeZone, setTimeZone }; +}