Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Timezone basic functionality #6492

Merged
22 changes: 9 additions & 13 deletions frontend/src/components/CustomTimePicker/CustomTimePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ 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,
Expand All @@ -27,9 +28,7 @@ import {
import { useLocation } from 'react-router-dom';
import { popupContainer } from 'utils/selectPopupContainer';

import useUrlQuery from '../../hooks/useUrlQuery';
import CustomTimePickerPopoverContent from './CustomTimePickerPopoverContent';
import { getTimezoneObjectByTimezoneString } from './timezoneUtils';

const maxAllowedMinTimeInMonths = 6;
type ViewType = 'datetime' | 'timezone';
Expand Down Expand Up @@ -79,8 +78,6 @@ function CustomTimePicker({
setSelectedTimePlaceholderValue,
] = useState('Select / Enter Time Range');

const urlQuery = useUrlQuery();

const [inputValue, setInputValue] = useState('');
const [inputStatus, setInputStatus] = useState<'' | 'error' | 'success'>('');
const [inputErrorMessage, setInputErrorMessage] = useState<string | null>(
Expand All @@ -91,14 +88,12 @@ function CustomTimePicker({

const [activeView, setActiveView] = useState<ViewType>(DEFAULT_VIEW);

const activeTimezoneOffset = useMemo(() => {
const timezone = urlQuery.get('timezone');
if (timezone) {
const timezoneObj = getTimezoneObjectByTimezoneString(timezone);
return timezoneObj?.offset;
}
return '';
}, [urlQuery]);
const { timezone, browserTimezone } = useTimezone();
const activeTimezoneOffset = timezone?.offset;
const isTimezoneOverridden = useMemo(
() => timezone?.offset !== browserTimezone.offset,
[timezone, browserTimezone],
);

const handleViewChange = useCallback(
(newView: 'timezone' | 'datetime'): void => {
Expand Down Expand Up @@ -161,6 +156,7 @@ function CustomTimePicker({
setOpen(newOpen);
if (!newOpen) {
setCustomDTPickerVisible?.(false);
setActiveView('datetime');
}
};

Expand Down Expand Up @@ -349,7 +345,7 @@ function CustomTimePicker({
}
suffix={
<>
{!!activeTimezoneOffset && (
{!!isTimezoneOverridden && activeTimezoneOffset && (
<div
className="timezone-badge"
onClick={(e): void => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,13 @@ import {
Option,
RelativeDurationSuggestionOptions,
} from 'container/TopNav/DateTimeSelectionV2/config';
import useUrlQuery from 'hooks/useUrlQuery';
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';
import {
getTimezoneObjectByTimezoneString,
TIMEZONE_DATA,
} from './timezoneUtils';

interface CustomTimePickerPopoverContentProps {
options: any[];
Expand Down Expand Up @@ -56,15 +52,8 @@ function CustomTimePickerPopoverContent({
const isLogsExplorerPage = useMemo(() => pathname === ROUTES.LOGS_EXPLORER, [
pathname,
]);
const urlQuery = useUrlQuery();
const activeTimezoneOffset = useMemo(() => {
const timezone = urlQuery.get('timezone') ?? TIMEZONE_DATA[0].value;
if (timezone) {
const timezoneObj = getTimezoneObjectByTimezoneString(timezone);
return timezoneObj?.offset;
}
return '';
}, [urlQuery]);
const { timezone } = useTimezone();
const activeTimezoneOffset = timezone?.offset;

function getTimeChips(options: Option[]): JSX.Element {
return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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
Expand All @@ -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>
Expand Down
16 changes: 5 additions & 11 deletions frontend/src/components/CustomTimePicker/TimezonePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,15 @@ import { Color } from '@signozhq/design-tokens';
import cx from 'classnames';
import { TimezonePickerShortcuts } from 'constants/shortcuts/TimezonePickerShortcuts';
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
import history from 'lib/history';
import { Check, Search } from 'lucide-react';
import { useTimezone } from 'providers/Timezone';
import {
Dispatch,
SetStateAction,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import { useLocation } from 'react-use';

import { Timezone, TIMEZONE_DATA } from './timezoneUtils';

Expand Down Expand Up @@ -94,13 +92,10 @@ function TimezonePicker({
setActiveView,
setIsOpen,
}: TimezonePickerProps): JSX.Element {
const { search } = useLocation();
const searchParams = useMemo(() => new URLSearchParams(search), [search]);

const [searchTerm, setSearchTerm] = useState('');
// TODO(shaheer): get this from user's selected time zone
const { timezone, updateTimezone } = useTimezone();
const [selectedTimezone, setSelectedTimezone] = useState<string>(
searchParams.get('timezone') ?? TIMEZONE_DATA[0].name,
timezone?.name ?? TIMEZONE_DATA[0].name,
);

const getFilteredTimezones = useCallback((searchTerm: string): Timezone[] => {
Expand All @@ -120,12 +115,11 @@ function TimezonePicker({
const handleTimezoneSelect = useCallback(
(timezone: Timezone) => {
setSelectedTimezone(timezone.name);
searchParams.set('timezone', timezone.value);
history.push({ search: searchParams.toString() });
updateTimezone(timezone);
handleCloseTimezonePicker();
setIsOpen(false);
},
[handleCloseTimezonePicker, searchParams, setIsOpen],
[handleCloseTimezonePicker, setIsOpen, updateTimezone],
);

// Register keyboard shortcuts
Expand Down
17 changes: 10 additions & 7 deletions frontend/src/components/CustomTimePicker/timezoneUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,12 @@ const getOffsetByTimezone = (timezone: string): number => {
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,
Expand All @@ -113,24 +119,21 @@ const generateTimezoneData = (): Timezone[] => {
const timezones: Timezone[] = [];

// Add browser timezone
const browserTz = dayjs.tz.guess();
const browserOffset = getOffsetByTimezone(browserTz);
timezones.push(
createTimezoneEntry(browserTz, browserOffset, TIMEZONE_TYPES.BROWSER),
);
const browserTzObject = getBrowserTimezone();
timezones.push(browserTzObject);

// Add UTC timezone with divider
timezones.push(UTC_TIMEZONE);

// Add remaining timezones
timezones.push(...filterAndSortTimezones(allTimezones, browserTz));
timezones.push(...filterAndSortTimezones(allTimezones, browserTzObject.value));

return timezones;
};

export const getTimezoneObjectByTimezoneString = (
timezone: string,
): Timezone | null => {
): Timezone => {
const utcOffset = getOffsetByTimezone(timezone);

return createTimezoneEntry(timezone, utcOffset);
Expand Down
34 changes: 34 additions & 0 deletions frontend/src/components/Graph/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
_adapters,
BarController,
BarElement,
CategoryScale,
Expand All @@ -18,8 +19,10 @@ import {
} from 'chart.js';
import annotationPlugin from 'chartjs-plugin-annotation';
import { generateGridTitle } from 'container/GridPanelSwitch/utils';
import dayjs from 'dayjs';
import { useIsDarkMode } from 'hooks/useDarkMode';
import isEqual from 'lodash-es/isEqual';
import { useTimezone } from 'providers/Timezone';
import {
forwardRef,
memo,
Expand Down Expand Up @@ -62,6 +65,17 @@ Chart.register(

Tooltip.positioners.custom = TooltipPositionHandler;

// Map of Chart.js time formats to dayjs format strings
const formatMap = {
'HH:mm:ss': 'HH:mm:ss',
'HH:mm': 'HH:mm',
'MM/DD HH:mm': 'MM/DD HH:mm',
'MM/dd HH:mm': 'MM/DD HH:mm',
'MM/DD': 'MM/DD',
'YY-MM': 'YY-MM',
YY: 'YY',
};

const Graph = forwardRef<ToggleGraphProps | undefined, GraphProps>(
(
{
Expand All @@ -80,11 +94,13 @@ const Graph = forwardRef<ToggleGraphProps | undefined, GraphProps>(
dragSelectColor,
},
ref,
// eslint-disable-next-line sonarjs/cognitive-complexity
): JSX.Element => {
const nearestDatasetIndex = useRef<null | number>(null);
const chartRef = useRef<HTMLCanvasElement>(null);
const isDarkMode = useIsDarkMode();
const gridTitle = useMemo(() => generateGridTitle(title), [title]);
const { timezone } = useTimezone();

const currentTheme = isDarkMode ? 'dark' : 'light';
const xAxisTimeUnit = useXAxisTimeUnit(data); // Computes the relevant time unit for x axis by analyzing the time stamp data
Expand Down Expand Up @@ -112,6 +128,22 @@ const Graph = forwardRef<ToggleGraphProps | undefined, GraphProps>(
return 'rgba(231,233,237,0.8)';
}, [currentTheme]);

// Override Chart.js date adapter to use dayjs with timezone support
useEffect(() => {
_adapters._date.override({
format(time: number | Date, fmt: string) {
const dayjsTime = dayjs(time).tz(timezone?.value);
const format = formatMap[fmt as keyof typeof formatMap];
if (!format) {
console.warn(`Missing datetime format for ${fmt}`);
return dayjsTime.format('YYYY-MM-DD HH:mm:ss'); // fallback format
}

return dayjsTime.format(format);
},
});
}, [timezone]);

const buildChart = useCallback(() => {
if (lineChartRef.current !== undefined) {
lineChartRef.current.destroy();
Expand All @@ -132,6 +164,7 @@ const Graph = forwardRef<ToggleGraphProps | undefined, GraphProps>(
isStacked,
onClickHandler,
data,
timezone,
);

const chartHasData = hasData(data);
Expand Down Expand Up @@ -166,6 +199,7 @@ const Graph = forwardRef<ToggleGraphProps | undefined, GraphProps>(
isStacked,
onClickHandler,
data,
timezone,
name,
type,
]);
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/components/Graph/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Chart, ChartConfiguration, ChartData, Color } from 'chart.js';
import * as chartjsAdapter from 'chartjs-adapter-date-fns';
import { Timezone } from 'components/CustomTimePicker/timezoneUtils';
import dayjs from 'dayjs';
import { MutableRefObject } from 'react';

Expand Down Expand Up @@ -50,6 +51,7 @@ export const getGraphOptions = (
isStacked: boolean | undefined,
onClickHandler: GraphOnClickHandler | undefined,
data: ChartData,
timezone: Timezone,
// eslint-disable-next-line sonarjs/cognitive-complexity
): CustomChartOptions => ({
animation: {
Expand Down Expand Up @@ -97,7 +99,7 @@ export const getGraphOptions = (
callbacks: {
title(context): string | string[] {
const date = dayjs(context[0].parsed.x);
return date.format('MMM DD, YYYY, HH:mm:ss');
return date.tz(timezone?.value).format('MMM DD, YYYY, HH:mm:ss');
},
label(context): string | string[] {
let label = context.dataset.label || '';
Expand Down
16 changes: 12 additions & 4 deletions frontend/src/components/Logs/ListLogView/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ import LogDetail from 'components/LogDetail';
import { VIEW_TYPES } from 'components/LogDetail/constants';
import { unescapeString } from 'container/LogDetailedView/utils';
import { FontSize } from 'container/OptionsMenu/types';
import dayjs from 'dayjs';
import dompurify from 'dompurify';
import { useActiveLog } from 'hooks/logs/useActiveLog';
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
import { useIsDarkMode } from 'hooks/useDarkMode';
// utils
import { FlatLogData } from 'lib/logs/flatLogData';
import { useTimezone } from 'providers/Timezone';
import { useCallback, useMemo, useState } from 'react';
// interfaces
import { IField } from 'types/api/logs/fields';
Expand Down Expand Up @@ -174,12 +174,20 @@ function ListLogView({
[selectedFields],
);

const { formatTimezoneAdjustedTimestamp } = useTimezone();

const timestampValue = useMemo(
() =>
typeof flattenLogData.timestamp === 'string'
? dayjs(flattenLogData.timestamp).format('YYYY-MM-DD HH:mm:ss.SSS')
: dayjs(flattenLogData.timestamp / 1e6).format('YYYY-MM-DD HH:mm:ss.SSS'),
[flattenLogData.timestamp],
? formatTimezoneAdjustedTimestamp(
flattenLogData.timestamp,
'YYYY-MM-DD HH:mm:ss.SSS',
)
: formatTimezoneAdjustedTimestamp(
flattenLogData.timestamp / 1e6,
'YYYY-MM-DD HH:mm:ss.SSS',
),
[flattenLogData.timestamp, formatTimezoneAdjustedTimestamp],
);

const logType = getLogIndicatorType(logData);
Expand Down
Loading
Loading