Compare commits
31 Commits
main
...
feat/timez
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6da2694bb4 | ||
|
|
c71351d42e | ||
|
|
b66328e818 | ||
|
|
d5dc65a699 | ||
|
|
234d71a80b | ||
|
|
429c87a12f | ||
|
|
d35ee883a5 | ||
|
|
4cd2935945 | ||
|
|
f239132d2f | ||
|
|
d47cba5641 | ||
|
|
932d9d970a | ||
|
|
f8411ab72a | ||
|
|
e012f10395 | ||
|
|
676e32ea09 | ||
|
|
28045772b8 | ||
|
|
b1120c7d16 | ||
|
|
55c9205aad | ||
|
|
5b4f423f9f | ||
|
|
cc376ce6a8 | ||
|
|
20e00c597a | ||
|
|
ff7da5c05b | ||
|
|
14ccadaeb5 | ||
|
|
984f3829dd | ||
|
|
31a9ead2fc | ||
|
|
65ce8eaf14 | ||
|
|
daec491c79 | ||
|
|
49e29567f4 | ||
|
|
8edd5fe7d6 | ||
|
|
178a3153dd | ||
|
|
e7f1b27a5b | ||
|
|
dbf0f236be |
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
156
frontend/src/components/CustomTimePicker/TimezonePicker.tsx
Normal file
156
frontend/src/components/CustomTimePicker/TimezonePicker.tsx
Normal 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;
|
||||
142
frontend/src/components/CustomTimePicker/timezoneUtils.ts
Normal file
142
frontend/src/components/CustomTimePicker/timezoneUtils.ts
Normal 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();
|
||||
@@ -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',
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export const TimezonePickerShortcuts = {
|
||||
CloseTimezonePicker: 'escape',
|
||||
};
|
||||
@@ -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} />;
|
||||
}
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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()}
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
84
frontend/src/providers/Timezone.tsx
Normal file
84
frontend/src/providers/Timezone.tsx
Normal 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;
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user