Compare commits

...

31 Commits

Author SHA1 Message Date
Shaheer Kochai
6da2694bb4 fix: make x axis and tooltip time format consistent in uPlot graphs (#6523) 2024-12-02 09:59:18 +04:30
Shaheer Kochai
c71351d42e fix: open the timepicker on clicking the input after closing the timezone picker by clicking outside (#6522) 2024-11-26 01:46:52 +05:30
ahmadshaheer
b66328e818 feat: display timezone adjusted time range in time picker 2024-11-25 09:17:49 +04:30
ahmadshaheer
d5dc65a699 Revert "feat: display timezone adjusted custom time range in time picker placeholder"
This reverts commit 429c87a12f.
2024-11-25 09:15:48 +04:30
ahmadshaheer
234d71a80b Revert "chore: custom hook for handling timezone formatting and caching"
This reverts commit f8411ab72a.
2024-11-24 19:32:29 +04:30
ahmadshaheer
429c87a12f feat: display timezone adjusted custom time range in time picker placeholder 2024-11-24 19:23:43 +04:30
ahmadshaheer
d35ee883a5 feat: apply timezone to trace details page 2024-11-24 10:18:34 +04:30
ahmadshaheer
4cd2935945 feat: apply timezone adjustment to alert history graph 2024-11-24 09:31:18 +04:30
ahmadshaheer
f239132d2f feat: apply timezone adjustment to anomaly based alerts graph 2024-11-24 09:27:51 +04:30
ahmadshaheer
d47cba5641 feat: handle timezone adjusted timestamps for consumers of getUPlotChartOptions
traces, logs time series, dashboard, service details (time series, bar), Node, Pod, Alert graphs
2024-11-21 10:58:00 +04:30
ahmadshaheer
932d9d970a chore: remove undefined from Timezone type 2024-11-21 09:53:41 +04:30
ahmadshaheer
f8411ab72a chore: custom hook for handling timezone formatting and caching 2024-11-21 09:53:04 +04:30
ahmadshaheer
e012f10395 chore: overall improvements 2024-11-20 18:42:41 +04:30
ahmadshaheer
676e32ea09 chore: store timezone string in local storage instead of object 2024-11-20 18:20:48 +04:30
ahmadshaheer
28045772b8 fix: get active timezone in timepicker hint 2024-11-20 09:23:52 +04:30
ahmadshaheer
b1120c7d16 fix: if timezone is undefined, fallback to browser time zone 2024-11-20 09:19:55 +04:30
ahmadshaheer
55c9205aad feat: timezone setting functionality in timepicker, timezone picker, and timezone preferences 2024-11-20 09:19:55 +04:30
ahmadshaheer
5b4f423f9f feat: create a context for timezone and handle the timezone configuration 2024-11-20 09:19:16 +04:30
ahmadshaheer
cc376ce6a8 chore: improve timezoneUtils by adding a function to get browser timezone 2024-11-20 09:19:16 +04:30
ahmadshaheer
20e00c597a fix: display the timezone in timepicker hint 'You are at' 2024-11-20 09:18:43 +04:30
ahmadshaheer
ff7da5c05b fix: fix the issue of timezone breaking for browser and utc timezones 2024-11-19 18:42:20 +04:30
ahmadshaheer
14ccadaeb5 fix: don't focus on time picker when timezone is clicked 2024-11-19 13:55:42 +04:30
ahmadshaheer
984f3829dd chore: fix the typo 2024-11-19 13:37:18 +04:30
ahmadshaheer
31a9ead2fc feat: display timezone in timepicker input 2024-11-19 13:34:15 +04:30
ahmadshaheer
65ce8eaf14 chore: change timezone item from div to button 2024-11-19 13:33:14 +04:30
ahmadshaheer
daec491c79 chore: improve timezone utils 2024-11-19 13:29:33 +04:30
ahmadshaheer
49e29567f4 feat: timezone preferences UI 2024-11-19 10:02:00 +04:30
ahmadshaheer
8edd5fe7d6 fix: overall improvement + add searchIndex to timezone 2024-11-18 18:49:22 +04:30
ahmadshaheer
178a3153dd chore: add the selected timezone as url param and close timezone picker on select 2024-11-18 18:38:45 +04:30
ahmadshaheer
e7f1b27a5b feat: add support for esc keypress to close the timezone picker 2024-11-18 18:27:38 +04:30
ahmadshaheer
dbf0f236be feat: time picker hint and timezone picker UI with basic functionality + helper to get timezones 2024-11-18 17:47:48 +04:30
29 changed files with 958 additions and 71 deletions

View File

@@ -186,6 +186,7 @@
"@types/webpack-dev-server": "^4.7.2",
"@typescript-eslint/eslint-plugin": "^4.33.0",
"@typescript-eslint/parser": "^4.33.0",
"@vvo/tzdb": "6.149.0",
"autoprefixer": "10.4.19",
"babel-plugin-styled-components": "^1.12.0",
"compression-webpack-plugin": "9.0.0",

View File

@@ -119,3 +119,42 @@
color: var(--bg-slate-400) !important;
}
}
.date-time-popover-footer {
border-top: 1px solid var(--bg-ink-200);
padding: 8px 14px;
.timezone-container {
&,
.timezone {
font-family: Inter;
font-size: 12px;
line-height: 16px;
letter-spacing: -0.06px;
}
display: flex;
align-items: center;
color: var(--bg-vanilla-400);
gap: 6px;
.timezone {
cursor: pointer;
padding: 0;
color: var(--bg-vanilla-100);
background-color: transparent;
border: none;
}
}
}
.timezone-badge {
display: flex;
align-items: center;
justify-content: center;
padding: 0 4px;
border-radius: 2px;
background: rgba(171, 189, 255, 0.04);
color: var(--bg-vanilla-100);
font-size: 12px;
font-weight: 400;
line-height: 16px;
letter-spacing: -0.06px;
cursor: pointer;
}

View File

@@ -15,11 +15,14 @@ import { isValidTimeFormat } from 'lib/getMinMax';
import { defaultTo, isFunction, noop } from 'lodash-es';
import debounce from 'lodash-es/debounce';
import { CheckCircle, ChevronDown, Clock } from 'lucide-react';
import { useTimezone } from 'providers/Timezone';
import {
ChangeEvent,
Dispatch,
SetStateAction,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import { useLocation } from 'react-router-dom';
@@ -28,6 +31,8 @@ import { popupContainer } from 'utils/selectPopupContainer';
import CustomTimePickerPopoverContent from './CustomTimePickerPopoverContent';
const maxAllowedMinTimeInMonths = 6;
type ViewType = 'datetime' | 'timezone';
const DEFAULT_VIEW: ViewType = 'datetime';
interface CustomTimePickerProps {
onSelect: (value: string) => void;
@@ -81,6 +86,25 @@ function CustomTimePicker({
const location = useLocation();
const [isInputFocused, setIsInputFocused] = useState(false);
const [activeView, setActiveView] = useState<ViewType>(DEFAULT_VIEW);
const { timezone, browserTimezone } = useTimezone();
const activeTimezoneOffset = timezone?.offset;
const isTimezoneOverridden = useMemo(
() => timezone?.offset !== browserTimezone.offset,
[timezone, browserTimezone],
);
const handleViewChange = useCallback(
(newView: 'timezone' | 'datetime'): void => {
if (activeView !== newView) {
setActiveView(newView);
}
setOpen(!open);
},
[activeView, open, setOpen],
);
const getSelectedTimeRangeLabel = (
selectedTime: string,
selectedTimeValue: string,
@@ -132,6 +156,7 @@ function CustomTimePicker({
setOpen(newOpen);
if (!newOpen) {
setCustomDTPickerVisible?.(false);
setActiveView('datetime');
}
};
@@ -281,6 +306,8 @@ function CustomTimePicker({
handleGoLive={defaultTo(handleGoLive, noop)}
options={items}
selectedTime={selectedTime}
activeView={activeView}
setActiveView={setActiveView}
/>
) : (
content
@@ -317,12 +344,23 @@ function CustomTimePicker({
)
}
suffix={
<ChevronDown
size={14}
onClick={(): void => {
setOpen(!open);
}}
/>
<>
{!!isTimezoneOverridden && activeTimezoneOffset && (
<div
className="timezone-badge"
onClick={(e): void => {
e.stopPropagation();
handleViewChange('timezone');
}}
>
<span>{activeTimezoneOffset}</span>
</div>
)}
<ChevronDown
size={14}
onClick={(): void => handleViewChange('datetime')}
/>
</>
}
/>
</Popover>

View File

@@ -1,5 +1,6 @@
import './CustomTimePicker.styles.scss';
import { Color } from '@signozhq/design-tokens';
import { Button } from 'antd';
import cx from 'classnames';
import ROUTES from 'constants/routes';
@@ -9,10 +10,13 @@ import {
Option,
RelativeDurationSuggestionOptions,
} from 'container/TopNav/DateTimeSelectionV2/config';
import { Clock } from 'lucide-react';
import { useTimezone } from 'providers/Timezone';
import { Dispatch, SetStateAction, useMemo } from 'react';
import { useLocation } from 'react-router-dom';
import RangePickerModal from './RangePickerModal';
import TimezonePicker from './TimezonePicker';
interface CustomTimePickerPopoverContentProps {
options: any[];
@@ -26,8 +30,11 @@ interface CustomTimePickerPopoverContentProps {
onSelectHandler: (label: string, value: string) => void;
handleGoLive: () => void;
selectedTime: string;
activeView: 'datetime' | 'timezone';
setActiveView: Dispatch<SetStateAction<'datetime' | 'timezone'>>;
}
// eslint-disable-next-line sonarjs/cognitive-complexity
function CustomTimePickerPopoverContent({
options,
setIsOpen,
@@ -37,12 +44,16 @@ function CustomTimePickerPopoverContent({
onSelectHandler,
handleGoLive,
selectedTime,
activeView,
setActiveView,
}: CustomTimePickerPopoverContentProps): JSX.Element {
const { pathname } = useLocation();
const isLogsExplorerPage = useMemo(() => pathname === ROUTES.LOGS_EXPLORER, [
pathname,
]);
const { timezone } = useTimezone();
const activeTimezoneOffset = timezone?.offset;
function getTimeChips(options: Option[]): JSX.Element {
return (
@@ -63,55 +74,75 @@ function CustomTimePickerPopoverContent({
);
}
return (
<div className="date-time-popover">
<div className="date-time-options">
{isLogsExplorerPage && (
<Button className="data-time-live" type="text" onClick={handleGoLive}>
Live
</Button>
)}
{options.map((option) => (
<Button
type="text"
key={option.label + option.value}
onClick={(): void => {
onSelectHandler(option.label, option.value);
}}
className={cx(
'date-time-options-btn',
customDateTimeVisible
? option.value === 'custom' && 'active'
: selectedTime === option.value && 'active',
)}
return activeView === 'datetime' ? (
<div>
<div className="date-time-popover">
<div className="date-time-options">
{isLogsExplorerPage && (
<Button className="data-time-live" type="text" onClick={handleGoLive}>
Live
</Button>
)}
{options.map((option) => (
<Button
type="text"
key={option.label + option.value}
onClick={(): void => {
onSelectHandler(option.label, option.value);
}}
className={cx(
'date-time-options-btn',
customDateTimeVisible
? option.value === 'custom' && 'active'
: selectedTime === option.value && 'active',
)}
>
{option.label}
</Button>
))}
</div>
<div
className={cx(
'relative-date-time',
selectedTime === 'custom' || customDateTimeVisible
? 'date-picker'
: 'relative-times',
)}
>
{selectedTime === 'custom' || customDateTimeVisible ? (
<RangePickerModal
setCustomDTPickerVisible={setCustomDTPickerVisible}
setIsOpen={setIsOpen}
onCustomDateHandler={onCustomDateHandler}
selectedTime={selectedTime}
/>
) : (
<div className="relative-times-container">
<div className="time-heading">RELATIVE TIMES</div>
<div>{getTimeChips(RelativeDurationSuggestionOptions)}</div>
</div>
)}
</div>
</div>
<div className="date-time-popover-footer">
<div className="timezone-container">
<Clock color={Color.BG_VANILLA_400} height={12} width={12} />
<span className="timezone-text">You are at</span>
<button
type="button"
className="timezone"
onClick={(): void => setActiveView('timezone')}
>
{option.label}
</Button>
))}
</div>
<div
className={cx(
'relative-date-time',
selectedTime === 'custom' || customDateTimeVisible
? 'date-picker'
: 'relative-times',
)}
>
{selectedTime === 'custom' || customDateTimeVisible ? (
<RangePickerModal
setCustomDTPickerVisible={setCustomDTPickerVisible}
setIsOpen={setIsOpen}
onCustomDateHandler={onCustomDateHandler}
selectedTime={selectedTime}
/>
) : (
<div className="relative-times-container">
<div className="time-heading">RELATIVE TIMES</div>
<div>{getTimeChips(RelativeDurationSuggestionOptions)}</div>
</div>
)}
{activeTimezoneOffset}
</button>
</div>
</div>
</div>
) : (
<div className="date-time-popover">
<TimezonePicker setActiveView={setActiveView} setIsOpen={setIsOpen} />
</div>
);
}

View File

@@ -4,6 +4,7 @@ import { DatePicker } from 'antd';
import { DateTimeRangeType } from 'container/TopNav/CustomDateTimeModal';
import { LexicalContext } from 'container/TopNav/DateTimeSelectionV2/config';
import dayjs, { Dayjs } from 'dayjs';
import { useTimezone } from 'providers/Timezone';
import { Dispatch, SetStateAction } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
@@ -49,6 +50,8 @@ function RangePickerModal(props: RangePickerModalProps): JSX.Element {
}
onCustomDateHandler(date_time, LexicalContext.CUSTOM_DATE_PICKER);
};
const { timezone } = useTimezone();
return (
<div className="custom-date-picker">
<RangePicker
@@ -58,7 +61,10 @@ function RangePickerModal(props: RangePickerModalProps): JSX.Element {
onOk={onModalOkHandler}
// eslint-disable-next-line react/jsx-props-no-spreading
{...(selectedTime === 'custom' && {
defaultValue: [dayjs(minTime / 1000000), dayjs(maxTime / 1000000)],
defaultValue: [
dayjs(minTime / 1000000).tz(timezone.value),
dayjs(maxTime / 1000000).tz(timezone.value),
],
})}
/>
</div>

View File

@@ -0,0 +1,125 @@
// Variables
$font-family: 'Inter';
$border-color: var(--bg-slate-400);
$item-spacing: 8px;
// Mixins
@mixin text-style-base {
font-family: $font-family;
font-style: normal;
font-weight: 400;
}
@mixin flex-center {
display: flex;
align-items: center;
}
.timezone-picker {
width: 532px;
color: var(--bg-vanilla-400);
font-family: $font-family;
&__search {
@include flex-center;
justify-content: space-between;
padding: 12px 14px;
border-bottom: 1px solid $border-color;
}
&__input-container {
@include flex-center;
gap: 6px;
width: -webkit-fill-available;
}
&__input {
@include text-style-base;
width: 100%;
background: transparent;
border: none;
outline: none;
color: var(--bg-vanilla-100);
font-size: 14px;
line-height: 20px;
letter-spacing: -0.07px;
padding: 0;
&::placeholder {
color: var(--bg-vanilla-400);
}
}
&__esc-key {
@include text-style-base;
font-size: 8px;
color: var(--bg-vanilla-400);
letter-spacing: -0.04px;
border-radius: 2.286px;
border: 1.143px solid var(--bg-ink-200);
border-bottom-width: 2.286px;
background: var(--bg-ink-400);
padding: 0 1px;
}
&__list {
max-height: 310px;
overflow-y: auto;
}
&__item {
@include flex-center;
justify-content: space-between;
padding: 7.5px 6px 7.5px $item-spacing;
margin: 4px $item-spacing;
cursor: pointer;
background: transparent;
border: none;
width: -webkit-fill-available;
color: var(--bg-vanilla-400);
font-family: $font-family;
&:hover,
&.selected {
border-radius: 2px;
background: rgba(171, 189, 255, 0.04);
color: var(--bg-vanilla-100);
}
&.has-divider {
position: relative;
&::after {
content: '';
position: absolute;
bottom: -2px;
left: -$item-spacing;
right: -$item-spacing;
border-bottom: 1px solid $border-color;
}
}
}
&__name {
@include text-style-base;
font-size: 14px;
line-height: 20px;
letter-spacing: -0.07px;
}
&__offset {
color: var(--bg-vanilla-100);
font-size: 12px;
line-height: 16px;
letter-spacing: -0.06px;
}
}
.timezone-name-wrapper {
@include flex-center;
gap: 6px;
&__selected-icon {
height: 15px;
width: 15px;
}
}

View File

@@ -0,0 +1,156 @@
import './TimezonePicker.styles.scss';
import { Color } from '@signozhq/design-tokens';
import cx from 'classnames';
import { TimezonePickerShortcuts } from 'constants/shortcuts/TimezonePickerShortcuts';
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
import { Check, Search } from 'lucide-react';
import { useTimezone } from 'providers/Timezone';
import {
Dispatch,
SetStateAction,
useCallback,
useEffect,
useState,
} from 'react';
import { Timezone, TIMEZONE_DATA } from './timezoneUtils';
interface SearchBarProps {
value: string;
onChange: (value: string) => void;
}
interface TimezoneItemProps {
timezone: Timezone;
isSelected?: boolean;
onClick?: () => void;
}
const ICON_SIZE = 14;
function SearchBar({ value, onChange }: SearchBarProps): JSX.Element {
return (
<div className="timezone-picker__search">
<div className="timezone-picker__input-container">
<Search color={Color.BG_VANILLA_400} height={ICON_SIZE} width={ICON_SIZE} />
<input
type="text"
className="timezone-picker__input"
placeholder="Search timezones..."
value={value}
onChange={(e): void => onChange(e.target.value)}
/>
</div>
<kbd className="timezone-picker__esc-key">esc</kbd>
</div>
);
}
function TimezoneItem({
timezone,
isSelected = false,
onClick,
}: TimezoneItemProps): JSX.Element {
return (
<button
type="button"
className={cx('timezone-picker__item', {
selected: isSelected,
'has-divider': timezone.hasDivider,
})}
onClick={onClick}
>
<div className="timezone-name-wrapper">
<div className="timezone-name-wrapper__selected-icon">
{isSelected && (
<Check
color={Color.BG_VANILLA_100}
height={ICON_SIZE}
width={ICON_SIZE}
/>
)}
</div>
<div className="timezone-picker__name">{timezone.name}</div>
</div>
<div className="timezone-picker__offset">{timezone.offset}</div>
</button>
);
}
TimezoneItem.defaultProps = {
isSelected: false,
onClick: undefined,
};
interface TimezonePickerProps {
setActiveView: Dispatch<SetStateAction<'datetime' | 'timezone'>>;
setIsOpen: Dispatch<SetStateAction<boolean>>;
}
function TimezonePicker({
setActiveView,
setIsOpen,
}: TimezonePickerProps): JSX.Element {
const [searchTerm, setSearchTerm] = useState('');
const { timezone, updateTimezone } = useTimezone();
const [selectedTimezone, setSelectedTimezone] = useState<string>(
timezone?.name ?? TIMEZONE_DATA[0].name,
);
const getFilteredTimezones = useCallback((searchTerm: string): Timezone[] => {
const normalizedSearch = searchTerm.toLowerCase();
return TIMEZONE_DATA.filter(
(tz) =>
tz.name.toLowerCase().includes(normalizedSearch) ||
tz.offset.toLowerCase().includes(normalizedSearch) ||
tz.searchIndex.toLowerCase().includes(normalizedSearch),
);
}, []);
const handleCloseTimezonePicker = useCallback(() => {
setActiveView('datetime');
}, [setActiveView]);
const handleTimezoneSelect = useCallback(
(timezone: Timezone) => {
setSelectedTimezone(timezone.name);
updateTimezone(timezone);
handleCloseTimezonePicker();
setIsOpen(false);
},
[handleCloseTimezonePicker, setIsOpen, updateTimezone],
);
// Register keyboard shortcuts
const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys();
useEffect(() => {
registerShortcut(
TimezonePickerShortcuts.CloseTimezonePicker,
handleCloseTimezonePicker,
);
return (): void => {
deregisterShortcut(TimezonePickerShortcuts.CloseTimezonePicker);
};
}, [deregisterShortcut, handleCloseTimezonePicker, registerShortcut]);
return (
<div className="timezone-picker">
<SearchBar value={searchTerm} onChange={setSearchTerm} />
<div className="timezone-picker__list">
{getFilteredTimezones(searchTerm).map((timezone) => (
<TimezoneItem
key={timezone.value}
timezone={timezone}
isSelected={timezone.name === selectedTimezone}
onClick={(): void => handleTimezoneSelect(timezone)}
/>
))}
</div>
</div>
);
}
export default TimezonePicker;

View File

@@ -0,0 +1,142 @@
import { getTimeZones } from '@vvo/tzdb';
import dayjs from 'dayjs';
import timezone from 'dayjs/plugin/timezone';
import utc from 'dayjs/plugin/utc';
dayjs.extend(utc);
dayjs.extend(timezone);
export interface Timezone {
name: string;
value: string;
offset: string;
searchIndex: string;
hasDivider?: boolean;
}
// Constants
const TIMEZONE_TYPES = {
BROWSER: 'BROWSER',
UTC: 'UTC',
STANDARD: 'STANDARD',
} as const;
type TimezoneType = typeof TIMEZONE_TYPES[keyof typeof TIMEZONE_TYPES];
const UTC_TIMEZONE: Timezone = {
name: 'Coordinated Universal Time — UTC, GMT',
value: 'UTC',
offset: 'UTC',
searchIndex: 'UTC',
hasDivider: true,
};
// Helper functions
const isValidTimezone = (tzName: string): boolean => {
try {
dayjs.tz(dayjs(), tzName);
return true;
} catch {
return false;
}
};
const formatOffset = (offsetMinutes: number): string => {
if (offsetMinutes === 0) return 'UTC';
const hours = Math.floor(Math.abs(offsetMinutes) / 60);
const minutes = Math.abs(offsetMinutes) % 60;
const sign = offsetMinutes > 0 ? '+' : '-';
return `UTC ${sign} ${hours}${
minutes ? `:${minutes.toString().padStart(2, '0')}` : ':00'
}`;
};
const createTimezoneEntry = (
name: string,
offsetMinutes: number,
type: TimezoneType = TIMEZONE_TYPES.STANDARD,
hasDivider = false,
): Timezone => {
const offset = formatOffset(offsetMinutes);
let value = name;
let displayName = name;
switch (type) {
case TIMEZONE_TYPES.BROWSER:
displayName = `Browser time — ${name}`;
value = name;
break;
case TIMEZONE_TYPES.UTC:
displayName = 'Coordinated Universal Time — UTC, GMT';
value = 'UTC';
break;
case TIMEZONE_TYPES.STANDARD:
displayName = name;
value = name;
break;
default:
console.error(`Invalid timezone type: ${type}`);
}
return {
name: displayName,
value,
offset,
searchIndex: offset.replace(/ /g, ''),
...(hasDivider && { hasDivider }),
};
};
const getOffsetByTimezone = (timezone: string): number => {
const dayjsTimezone = dayjs().tz(timezone);
return dayjsTimezone.utcOffset();
};
export const getBrowserTimezone = (): Timezone => {
const browserTz = dayjs.tz.guess();
const browserOffset = getOffsetByTimezone(browserTz);
return createTimezoneEntry(browserTz, browserOffset, TIMEZONE_TYPES.BROWSER);
};
const filterAndSortTimezones = (
allTimezones: ReturnType<typeof getTimeZones>,
browserTzName?: string,
): Timezone[] =>
allTimezones
.filter(
(tz) =>
!tz.name.startsWith('Etc/') &&
isValidTimezone(tz.name) &&
tz.name !== browserTzName,
)
.sort((a, b) => a.name.localeCompare(b.name))
.map((tz) => createTimezoneEntry(tz.name, tz.rawOffsetInMinutes));
const generateTimezoneData = (): Timezone[] => {
const allTimezones = getTimeZones();
const timezones: Timezone[] = [];
// Add browser timezone
const browserTzObject = getBrowserTimezone();
timezones.push(browserTzObject);
// Add UTC timezone with divider
timezones.push(UTC_TIMEZONE);
// Add remaining timezones
timezones.push(...filterAndSortTimezones(allTimezones, browserTzObject.value));
return timezones;
};
export const getTimezoneObjectByTimezoneString = (
timezone: string,
): Timezone => {
const utcOffset = getOffsetByTimezone(timezone);
return createTimezoneEntry(timezone, utcOffset);
};
export const TIMEZONE_DATA = generateTimezoneData();

View File

@@ -21,4 +21,5 @@ export enum LOCALSTORAGE {
THEME_ANALYTICS_V1 = 'THEME_ANALYTICS_V1',
LAST_USED_SAVED_VIEWS = 'LAST_USED_SAVED_VIEWS',
SHOW_LOGS_QUICK_FILTERS = 'SHOW_LOGS_QUICK_FILTERS',
PREFERRED_TIMEZONE = 'PREFERRED_TIMEZONE',
}

View File

@@ -0,0 +1,3 @@
export const TimezonePickerShortcuts = {
CloseTimezonePicker: 'escape',
};

View File

@@ -7,6 +7,7 @@ import useUrlQuery from 'hooks/useUrlQuery';
import history from 'lib/history';
import heatmapPlugin from 'lib/uPlotLib/plugins/heatmapPlugin';
import timelinePlugin from 'lib/uPlotLib/plugins/timelinePlugin';
import { useTimezone } from 'providers/Timezone';
import { useMemo, useRef } from 'react';
import { useDispatch } from 'react-redux';
import { UpdateTimeInterval } from 'store/actions';
@@ -48,6 +49,7 @@ function HorizontalTimelineGraph({
const urlQuery = useUrlQuery();
const dispatch = useDispatch();
const { timezone } = useTimezone();
const options: uPlot.Options = useMemo(
() => ({
@@ -116,8 +118,18 @@ function HorizontalTimelineGraph({
}),
]
: [],
tzDate: (timestamp: number): Date =>
uPlot.tzDate(new Date(timestamp * 1e3), timezone?.value),
}),
[width, isDarkMode, transformedData.length, urlQuery, dispatch],
[
width,
isDarkMode,
transformedData.length,
urlQuery,
dispatch,
timezone?.value,
],
);
return <Uplot data={transformedData} options={options} />;
}

View File

@@ -10,6 +10,7 @@ import getAxes from 'lib/uPlotLib/utils/getAxes';
import { getUplotChartDataForAnomalyDetection } from 'lib/uPlotLib/utils/getUplotChartData';
import { getYAxisScaleForAnomalyDetection } from 'lib/uPlotLib/utils/getYAxisScale';
import { LineChart } from 'lucide-react';
import { useTimezone } from 'providers/Timezone';
import { useEffect, useRef, useState } from 'react';
import uPlot from 'uplot';
@@ -148,10 +149,12 @@ function AnomalyAlertEvaluationView({
]
: [];
const { timezone } = useTimezone();
const options = {
width: dimensions.width,
height: dimensions.height - 36,
plugins: [bandsPlugin, tooltipPlugin(isDarkMode)],
plugins: [bandsPlugin, tooltipPlugin(isDarkMode, timezone?.value)],
focus: {
alpha: 0.3,
},
@@ -256,6 +259,8 @@ function AnomalyAlertEvaluationView({
show: true,
},
axes: getAxes(isDarkMode, yAxisUnit),
tzDate: (timestamp: number): Date =>
uPlot.tzDate(new Date(timestamp * 1e3), timezone?.value),
};
const handleSearch = (searchText: string): void => {

View File

@@ -1,8 +1,10 @@
import { themeColors } from 'constants/theme';
import dayjs from 'dayjs';
import { generateColor } from 'lib/uPlotLib/utils/generateColor';
const tooltipPlugin = (
isDarkMode: boolean,
timezone: string,
): { hooks: { init: (u: any) => void } } => {
let tooltip: HTMLDivElement;
const tooltipLeftOffset = 10;
@@ -17,7 +19,7 @@ const tooltipPlugin = (
return value.toFixed(3);
}
if (value instanceof Date) {
return value.toLocaleString();
return dayjs(value).tz(timezone).format('MM/DD/YYYY, h:mm:ss A');
}
if (value == null) {
return 'N/A';

View File

@@ -25,6 +25,7 @@ import getTimeString from 'lib/getTimeString';
import history from 'lib/history';
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
import { useTimezone } from 'providers/Timezone';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
@@ -35,6 +36,7 @@ import { AlertDef } from 'types/api/alerts/def';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { GlobalReducer } from 'types/reducer/globalTime';
import uPlot from 'uplot';
import { getGraphType } from 'utils/getGraphType';
import { getSortedSeriesData } from 'utils/getSortedSeriesData';
import { getTimeRange } from 'utils/getTimeRange';
@@ -201,6 +203,8 @@ function ChartPreview({
[dispatch, location.pathname, urlQuery],
);
const { timezone } = useTimezone();
const options = useMemo(
() =>
getUPlotChartOptions({
@@ -236,6 +240,9 @@ function ChartPreview({
softMax: null,
softMin: null,
panelType: graphType,
tzDate: (timestamp: number) =>
uPlot.tzDate(new Date(timestamp * 1e3), timezone?.value),
timezone: timezone?.value,
}),
[
yAxisUnit,
@@ -250,6 +257,7 @@ function ChartPreview({
optionName,
alertDef?.condition.targetUnit,
graphType,
timezone?.value,
],
);

View File

@@ -4,6 +4,7 @@ import { Popover, Typography } from 'antd';
import { convertTimeToRelevantUnit } from 'container/TraceDetail/utils';
import dayjs from 'dayjs';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useTimezone } from 'providers/Timezone';
import { useEffect } from 'react';
import { toFixed } from 'utils/toFixed';
@@ -32,13 +33,17 @@ function Span(props: SpanLengthProps): JSX.Element {
const isDarkMode = useIsDarkMode();
const { time, timeUnitName } = convertTimeToRelevantUnit(inMsCount);
const { timezone } = useTimezone();
useEffect(() => {
document.documentElement.scrollTop = document.documentElement.clientHeight;
document.documentElement.scrollLeft = document.documentElement.clientWidth;
}, []);
const getContent = (): JSX.Element => {
const timeStamp = dayjs(startTime).format('h:mm:ss:SSS A');
const timeStamp = dayjs(startTime)
.tz(timezone.value)
.format('h:mm:ss:SSS A (UTC Z)');
const startTimeInMs = startTime - globalStart;
return (
<div>

View File

@@ -8,10 +8,12 @@ import { useResizeObserver } from 'hooks/useDimensions';
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
import { useTimezone } from 'providers/Timezone';
import { useMemo, useRef } from 'react';
import { useQueries, UseQueryResult } from 'react-query';
import { SuccessResponse } from 'types/api';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import uPlot from 'uplot';
import {
getHostQueryPayload,
@@ -73,6 +75,8 @@ function NodeMetrics({
[queries],
);
const { timezone } = useTimezone();
const options = useMemo(
() =>
queries.map(({ data }, idx) =>
@@ -86,6 +90,9 @@ function NodeMetrics({
minTimeScale: start,
maxTimeScale: end,
verticalLineTimestamp,
tzDate: (timestamp: number) =>
uPlot.tzDate(new Date(timestamp * 1e3), timezone?.value),
timezone: timezone?.value,
}),
),
[
@@ -96,6 +103,7 @@ function NodeMetrics({
start,
verticalLineTimestamp,
end,
timezone?.value,
],
);

View File

@@ -8,10 +8,12 @@ import { useResizeObserver } from 'hooks/useDimensions';
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
import { useTimezone } from 'providers/Timezone';
import { useMemo, useRef } from 'react';
import { useQueries, UseQueryResult } from 'react-query';
import { SuccessResponse } from 'types/api';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import uPlot from 'uplot';
import { getPodQueryPayload, podWidgetInfo } from './constants';
@@ -60,6 +62,7 @@ function PodMetrics({
() => queries.map(({ data }) => getUPlotChartData(data?.payload)),
[queries],
);
const { timezone } = useTimezone();
const options = useMemo(
() =>
@@ -74,9 +77,20 @@ function PodMetrics({
minTimeScale: start,
maxTimeScale: end,
verticalLineTimestamp,
tzDate: (timestamp: number) =>
uPlot.tzDate(new Date(timestamp * 1e3), timezone?.value),
timezone: timezone?.value,
}),
),
[queries, isDarkMode, dimensions, start, verticalLineTimestamp, end],
[
queries,
isDarkMode,
dimensions,
start,
end,
verticalLineTimestamp,
timezone?.value,
],
);
const renderCardContent = (

View File

@@ -0,0 +1,81 @@
.timezone-adaption {
padding: 16px;
background: var(--bg-ink-400);
border: 1px solid var(--bg-ink-500);
border-radius: 4px;
&__header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
&__title {
color: var(--bg-vanilla-300);
font-size: 14px;
font-weight: 500;
margin: 0;
}
&__description {
color: var(--bg-vanilla-400);
font-size: 14px;
line-height: 20px;
margin: 0 0 12px 0;
}
&__note {
display: flex;
align-items: center;
justify-content: space-between;
padding: 7.5px 12px;
background: rgba(78, 116, 248, 0.1);
border: 1px solid rgba(78, 116, 248, 0.1);
border-radius: 4px;
}
&__bullet {
color: var(--bg-robin-400);
font-size: 16px;
line-height: 20px;
}
&__note-text-container {
display: flex;
align-items: center;
gap: 10px;
}
&__note-text {
display: flex;
align-items: center;
gap: 4px;
color: var(--bg-robin-400);
font-size: 14px;
line-height: 20px;
}
&__note-text-overridden {
display: flex;
align-items: center;
padding: 0 2px;
background: rgba(171, 189, 255, 0.04);
border-radius: 2px;
font-size: 12px;
line-height: 16px;
color: var(--bg-vanilla-100);
}
&__clear-override {
display: flex;
align-items: center;
gap: 6px;
background: transparent;
border: none;
padding: 0;
color: var(--bg-robin-300);
font-size: 12px;
line-height: 16px; /* 133.333% */
letter-spacing: 0.12px;
cursor: pointer;
}
}

View File

@@ -0,0 +1,78 @@
import './TimezoneAdaptation.styles.scss';
import { Color } from '@signozhq/design-tokens';
import { Switch } from 'antd';
import { Delete } from 'lucide-react';
import { useTimezone } from 'providers/Timezone';
import { useMemo, useState } from 'react';
function TimezoneAdaptation(): JSX.Element {
const { timezone, browserTimezone, updateTimezone } = useTimezone();
const isTimezoneOverridden = useMemo(
() => timezone?.offset !== browserTimezone.offset,
[timezone, browserTimezone],
);
const [isAdaptationEnabled, setIsAdaptationEnabled] = useState(true);
const getSwitchStyles = (): React.CSSProperties => ({
backgroundColor:
isAdaptationEnabled && isTimezoneOverridden ? Color.BG_AMBER_400 : undefined,
});
const handleOverrideClear = (): void => {
updateTimezone(browserTimezone);
};
return (
<div className="timezone-adaption">
<div className="timezone-adaption__header">
<h2 className="timezone-adaption__title">Adapt to my timezone</h2>
<Switch
checked={isAdaptationEnabled}
onChange={setIsAdaptationEnabled}
style={getSwitchStyles()}
/>
</div>
<p className="timezone-adaption__description">
Adapt the timestamps shown in the SigNoz console to my active timezone.
</p>
<div className="timezone-adaption__note">
<div className="timezone-adaption__note-text-container">
<span className="timezone-adaption__bullet"></span>
<span className="timezone-adaption__note-text">
{isTimezoneOverridden ? (
<>
Your current timezone is overridden to
<span className="timezone-adaption__note-text-overridden">
{timezone?.offset}
</span>
</>
) : (
<>
You can override the timezone adaption for any view with the time
picker.
</>
)}
</span>
</div>
{!!isTimezoneOverridden && (
<button
type="button"
className="timezone-adaption__clear-override"
onClick={handleOverrideClear}
>
<Delete height={12} width={12} color={Color.BG_ROBIN_300} />
Clear override
</button>
)}
</div>
</div>
);
}
export default TimezoneAdaptation;

View File

@@ -7,6 +7,7 @@ import { LogOut, Moon, Sun } from 'lucide-react';
import { useState } from 'react';
import Password from './Password';
import TimezoneAdaptation from './TimezoneAdaptation/TimezoneAdaptation';
import UserInfo from './UserInfo';
function MySettings(): JSX.Element {
@@ -78,6 +79,8 @@ function MySettings(): JSX.Element {
<Password />
</div>
<TimezoneAdaptation />
<Button
className="flexBtn"
onClick={(): void => Logout()}

View File

@@ -14,7 +14,9 @@ import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
import { cloneDeep, isEqual, isUndefined } from 'lodash-es';
import _noop from 'lodash-es/noop';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { useTimezone } from 'providers/Timezone';
import { useEffect, useMemo, useRef, useState } from 'react';
import uPlot from 'uplot';
import { getSortedSeriesData } from 'utils/getSortedSeriesData';
import { getTimeRange } from 'utils/getTimeRange';
@@ -105,6 +107,8 @@ function UplotPanelWrapper({
}
}, [graphVisibility, hiddenGraph, widget.panelTypes, widget?.stackedBarChart]);
const { timezone } = useTimezone();
const options = useMemo(
() =>
getUPlotChartOptions({
@@ -128,6 +132,9 @@ function UplotPanelWrapper({
hiddenGraph,
setHiddenGraph,
customTooltipElement,
tzDate: (timestamp: number) =>
uPlot.tzDate(new Date(timestamp * 1e3), timezone?.value),
timezone: timezone?.value,
}),
[
widget?.id,
@@ -150,6 +157,7 @@ function UplotPanelWrapper({
currentQuery,
hiddenGraph,
customTooltipElement,
timezone?.value,
],
);

View File

@@ -17,6 +17,7 @@ import history from 'lib/history';
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
import { isEmpty } from 'lodash-es';
import { useTimezone } from 'providers/Timezone';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useLocation } from 'react-router-dom';
@@ -26,6 +27,7 @@ import { SuccessResponse } from 'types/api';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { DataSource } from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import uPlot from 'uplot';
import { getTimeRange } from 'utils/getTimeRange';
import { Container } from './styles';
@@ -118,6 +120,8 @@ function TimeSeriesView({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const { timezone } = useTimezone();
const chartOptions = getUPlotChartOptions({
onDragSelect,
yAxisUnit: yAxisUnit || '',
@@ -131,6 +135,9 @@ function TimeSeriesView({
maxTimeScale,
softMax: null,
softMin: null,
tzDate: (timestamp: number) =>
uPlot.tzDate(new Date(timestamp * 1e3), timezone?.value),
timezone: timezone?.value,
});
return (

View File

@@ -28,6 +28,7 @@ import getTimeString from 'lib/getTimeString';
import history from 'lib/history';
import { isObject } from 'lodash-es';
import { Check, Copy, Info, Send, Undo } from 'lucide-react';
import { useTimezone } from 'providers/Timezone';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useQueryClient } from 'react-query';
import { connect, useSelector } from 'react-redux';
@@ -613,6 +614,8 @@ function DateTimeSelection({
);
};
const { timezone } = useTimezone();
return (
<div className="date-time-selector">
{showResetButton && selectedTime !== defaultRelativeTime && (
@@ -664,8 +667,8 @@ function DateTimeSelection({
setIsValidteRelativeTime(isValid);
}}
selectedValue={getInputLabel(
dayjs(minTime / 1000000),
dayjs(maxTime / 1000000),
dayjs(minTime / 1000000).tz(timezone.value),
dayjs(maxTime / 1000000).tz(timezone.value),
selectedTime,
)}
data-testid="dropDown"

View File

@@ -24,6 +24,7 @@ import history from 'lib/history';
import { map } from 'lodash-es';
import { PanelRight } from 'lucide-react';
import { SPAN_DETAILS_LEFT_COL_WIDTH } from 'pages/TraceDetail/constants';
import { useTimezone } from 'providers/Timezone';
import { useEffect, useMemo, useState } from 'react';
import { ITraceForest, PayloadProps } from 'types/api/trace/getTraceItem';
import { getSpanTreeMetadata } from 'utils/getSpanTreeMetadata';
@@ -139,6 +140,8 @@ function TraceDetail({ response }: TraceDetailProps): JSX.Element {
const isDarkMode = useIsDarkMode();
const { timezone } = useTimezone();
return (
<StyledRow styledclass={[Flex({ flex: 1 })]}>
<StyledCol flex="auto" styledclass={styles.leftContainer}>
@@ -195,7 +198,9 @@ function TraceDetail({ response }: TraceDetailProps): JSX.Element {
{isGlobalTimeVisible && (
<styles.TimeStampContainer flex={`${SPAN_DETAILS_LEFT_COL_WIDTH}px`}>
<Typography>
{dayjs(traceMetaData.globalStart).format('hh:mm:ss a MM/DD')}
{dayjs(traceMetaData.globalStart)
.tz(timezone.value)
.format('hh:mm:ss a (UTC Z) MM/DD')}
</Typography>
</styles.TimeStampContainer>
)}

View File

@@ -7,6 +7,7 @@ import { AxiosError } from 'axios';
import { ThemeProvider } from 'hooks/useDarkMode';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import posthog from 'posthog-js';
import TimezoneProvider from 'providers/Timezone';
import { createRoot } from 'react-dom/client';
import { HelmetProvider } from 'react-helmet-async';
import { QueryClient, QueryClientProvider } from 'react-query';
@@ -69,14 +70,16 @@ if (container) {
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
<HelmetProvider>
<ThemeProvider>
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<AppRoutes />
</Provider>
{process.env.NODE_ENV === 'development' && (
<ReactQueryDevtools initialIsOpen={false} />
)}
</QueryClientProvider>
<TimezoneProvider>
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<AppRoutes />
</Provider>
{process.env.NODE_ENV === 'development' && (
<ReactQueryDevtools initialIsOpen={false} />
)}
</QueryClientProvider>
</TimezoneProvider>
</ThemeProvider>
</HelmetProvider>
</Sentry.ErrorBoundary>,

View File

@@ -55,6 +55,8 @@ export interface GetUPlotChartOptions {
>;
customTooltipElement?: HTMLDivElement;
verticalLineTimestamp?: number;
tzDate?: (timestamp: number) => Date;
timezone?: string;
}
/** the function converts series A , series B , series C to
@@ -158,6 +160,8 @@ export const getUPlotChartOptions = ({
setHiddenGraph,
customTooltipElement,
verticalLineTimestamp,
tzDate,
timezone,
}: GetUPlotChartOptions): uPlot.Options => {
const timeScaleProps = getXAxisScale(minTimeScale, maxTimeScale);
@@ -196,6 +200,7 @@ export const getUPlotChartOptions = ({
fill: (): string => '#fff',
},
},
tzDate,
padding: [16, 16, 8, 8],
bands,
scales: {
@@ -222,6 +227,7 @@ export const getUPlotChartOptions = ({
stackBarChart,
isDarkMode,
customTooltipElement,
timezone,
}),
onClickPlugin({
onClick: onClickHandler,

View File

@@ -46,6 +46,7 @@ const generateTooltipContent = (
isHistogramGraphs?: boolean,
isMergedSeries?: boolean,
stackBarChart?: boolean,
timezone?: string,
// eslint-disable-next-line sonarjs/cognitive-complexity
): HTMLElement => {
const container = document.createElement('div');
@@ -69,9 +70,13 @@ const generateTooltipContent = (
series.forEach((item, index) => {
if (index === 0) {
if (isBillingUsageGraphs) {
tooltipTitle = dayjs(data[0][idx] * 1000).format('MMM DD YYYY');
tooltipTitle = dayjs(data[0][idx] * 1000)
.tz(timezone)
.format('MMM DD YYYY');
} else {
tooltipTitle = dayjs(data[0][idx] * 1000).format('MMM DD YYYY HH:mm:ss');
tooltipTitle = dayjs(data[0][idx] * 1000)
.tz(timezone)
.format('MMM DD YYYY h:mm:ss A');
}
} else if (item.show) {
const {
@@ -223,6 +228,7 @@ type ToolTipPluginProps = {
stackBarChart?: boolean;
isDarkMode: boolean;
customTooltipElement?: HTMLDivElement;
timezone?: string;
};
const tooltipPlugin = ({
@@ -234,6 +240,7 @@ const tooltipPlugin = ({
stackBarChart,
isDarkMode,
customTooltipElement,
timezone,
}: // eslint-disable-next-line sonarjs/cognitive-complexity
ToolTipPluginProps): any => {
let over: HTMLElement;
@@ -300,6 +307,7 @@ ToolTipPluginProps): any => {
isHistogramGraphs,
isMergedSeries,
stackBarChart,
timezone,
);
if (customTooltipElement) {
content.appendChild(customTooltipElement);

View File

@@ -0,0 +1,84 @@
import {
getBrowserTimezone,
getTimezoneObjectByTimezoneString,
Timezone,
} from 'components/CustomTimePicker/timezoneUtils';
import { LOCALSTORAGE } from 'constants/localStorage';
import React, {
createContext,
useCallback,
useContext,
useMemo,
useState,
} from 'react';
interface TimezoneContextType {
timezone: Timezone;
browserTimezone: Timezone;
updateTimezone: (timezone: Timezone) => void;
}
const TimezoneContext = createContext<TimezoneContextType | undefined>(
undefined,
);
function TimezoneProvider({
children,
}: {
children: React.ReactNode;
}): JSX.Element {
const getStoredTimezoneValue = (): Timezone | null => {
try {
const timezoneValue = localStorage.getItem(LOCALSTORAGE.PREFERRED_TIMEZONE);
if (timezoneValue) {
return getTimezoneObjectByTimezoneString(timezoneValue);
}
} catch (error) {
console.error('Error reading timezone from localStorage:', error);
}
return null;
};
const setStoredTimezoneValue = (value: string): void => {
try {
localStorage.setItem(LOCALSTORAGE.PREFERRED_TIMEZONE, value);
} catch (error) {
console.error('Error saving timezone to localStorage:', error);
}
};
const browserTimezone = useMemo(() => getBrowserTimezone(), []);
const [timezone, setTimezone] = useState<Timezone>(
getStoredTimezoneValue() ?? browserTimezone,
);
const updateTimezone = useCallback((timezone: Timezone): void => {
// TODO(shaheer): replace this with user preferences API
setStoredTimezoneValue(timezone.value);
setTimezone(timezone);
}, []);
const value = React.useMemo(
() => ({
timezone,
browserTimezone,
updateTimezone,
}),
[timezone, browserTimezone, updateTimezone],
);
return (
<TimezoneContext.Provider value={value}>{children}</TimezoneContext.Provider>
);
}
export const useTimezone = (): TimezoneContextType => {
const context = useContext(TimezoneContext);
if (context === undefined) {
throw new Error('useTimezone must be used within a TimezoneProvider');
}
return context;
};
export default TimezoneProvider;

View File

@@ -4764,6 +4764,11 @@
d3-time-format "4.1.0"
internmap "2.0.3"
"@vvo/tzdb@6.149.0":
version "6.149.0"
resolved "https://registry.yarnpkg.com/@vvo/tzdb/-/tzdb-6.149.0.tgz#e4fcca3c49b90d5910a8679267540cb532809075"
integrity sha512-d68+oW1TE60Ho9FlCDO5Ks4suk6hp5umjNIrtWytVB0B/X0/P1T9yWdnH7EhNb2fx1CQE+MM1qmLUGzT+QAqdw==
"@webassemblyjs/ast@1.12.1":
version "1.12.1"
resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.12.1.tgz#bb16a0e8b1914f979f45864c23819cc3e3f0d4bb"