mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-11 23:59:00 +08:00
Feat: Timezone picker feature (#6474)
* feat: time picker hint and timezone picker UI with basic functionality + helper to get timezones * feat: add support for esc keypress to close the timezone picker * chore: add the selected timezone as url param and close timezone picker on select * fix: overall improvement + add searchIndex to timezone * feat: timezone preferences UI * chore: improve timezone utils * chore: change timezone item from div to button * feat: display timezone in timepicker input * chore: fix the typo * fix: don't focus on time picker when timezone is clicked * fix: fix the issue of timezone breaking for browser and utc timezones * fix: display the timezone in timepicker hint 'You are at' * feat: timezone basic functionality (#6492) * chore: change div to fragment + change type to any as the ESLint complains otherwise * chore: manage etc timezone filtering with an arg * chore: update timezone wrapper class name * fix: add timezone support to downloaded logs * feat: add current timezone to dashboard list and configure metadata modal * fix: add pencil icon next to timezone hint + change the copy to Current timezone * fix: properly handle the escape button behavior for timezone picker * chore: replace @vvo/tzdb with native Intl API for timezones * feat: lightmode for timezone picker and timezone adaptation components * fix: use normald tz in browser timezone * fix: timezone picker lightmode fixes * feat: display selected time range in 12 hour format * chore: remove unnecessary optional chaining * fix: fix the typo in css variable * chore: add em dash and change icon for timezone hint in date/time picker * chore: move pen line icon to the right of timezone offset * fix: fix the failing tests * feat: handle switching off the timezone adaptation
This commit is contained in:
parent
e3caa6a8f5
commit
b333aa3775
@ -40,7 +40,7 @@
|
|||||||
|
|
||||||
&.custom-time {
|
&.custom-time {
|
||||||
input:not(:focus) {
|
input:not(:focus) {
|
||||||
min-width: 240px;
|
min-width: 280px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -119,3 +119,69 @@
|
|||||||
color: var(--bg-slate-400) !important;
|
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 {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
border-radius: 2px;
|
||||||
|
background: rgba(171, 189, 255, 0.04);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0px 4px;
|
||||||
|
color: var(--bg-vanilla-100);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.date-time-popover__footer {
|
||||||
|
border-color: var(--bg-vanilla-400);
|
||||||
|
}
|
||||||
|
.timezone-container {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
&__clock-icon {
|
||||||
|
stroke: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
.timezone {
|
||||||
|
color: var(--bg-ink-100);
|
||||||
|
background: rgb(179 179 179 / 15%);
|
||||||
|
&__icon {
|
||||||
|
stroke: var(--bg-ink-100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.timezone-badge {
|
||||||
|
color: var(--bg-ink-100);
|
||||||
|
background: rgb(179 179 179 / 15%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -15,11 +15,14 @@ import { isValidTimeFormat } from 'lib/getMinMax';
|
|||||||
import { defaultTo, isFunction, noop } from 'lodash-es';
|
import { defaultTo, isFunction, noop } from 'lodash-es';
|
||||||
import debounce from 'lodash-es/debounce';
|
import debounce from 'lodash-es/debounce';
|
||||||
import { CheckCircle, ChevronDown, Clock } from 'lucide-react';
|
import { CheckCircle, ChevronDown, Clock } from 'lucide-react';
|
||||||
|
import { useTimezone } from 'providers/Timezone';
|
||||||
import {
|
import {
|
||||||
ChangeEvent,
|
ChangeEvent,
|
||||||
Dispatch,
|
Dispatch,
|
||||||
SetStateAction,
|
SetStateAction,
|
||||||
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
|
useMemo,
|
||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
@ -28,6 +31,8 @@ import { popupContainer } from 'utils/selectPopupContainer';
|
|||||||
import CustomTimePickerPopoverContent from './CustomTimePickerPopoverContent';
|
import CustomTimePickerPopoverContent from './CustomTimePickerPopoverContent';
|
||||||
|
|
||||||
const maxAllowedMinTimeInMonths = 6;
|
const maxAllowedMinTimeInMonths = 6;
|
||||||
|
type ViewType = 'datetime' | 'timezone';
|
||||||
|
const DEFAULT_VIEW: ViewType = 'datetime';
|
||||||
|
|
||||||
interface CustomTimePickerProps {
|
interface CustomTimePickerProps {
|
||||||
onSelect: (value: string) => void;
|
onSelect: (value: string) => void;
|
||||||
@ -81,11 +86,42 @@ function CustomTimePicker({
|
|||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const [isInputFocused, setIsInputFocused] = useState(false);
|
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(true);
|
||||||
|
},
|
||||||
|
[activeView, setOpen],
|
||||||
|
);
|
||||||
|
|
||||||
|
const [isOpenedFromFooter, setIsOpenedFromFooter] = useState(false);
|
||||||
|
|
||||||
const getSelectedTimeRangeLabel = (
|
const getSelectedTimeRangeLabel = (
|
||||||
selectedTime: string,
|
selectedTime: string,
|
||||||
selectedTimeValue: string,
|
selectedTimeValue: string,
|
||||||
): string => {
|
): string => {
|
||||||
if (selectedTime === 'custom') {
|
if (selectedTime === 'custom') {
|
||||||
|
// Convert the date range string to 12-hour format
|
||||||
|
const dates = selectedTimeValue.split(' - ');
|
||||||
|
if (dates.length === 2) {
|
||||||
|
const startDate = dayjs(dates[0], 'DD/MM/YYYY HH:mm');
|
||||||
|
const endDate = dayjs(dates[1], 'DD/MM/YYYY HH:mm');
|
||||||
|
|
||||||
|
return `${startDate.format('DD/MM/YYYY hh:mm A')} - ${endDate.format(
|
||||||
|
'DD/MM/YYYY hh:mm A',
|
||||||
|
)}`;
|
||||||
|
}
|
||||||
return selectedTimeValue;
|
return selectedTimeValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -131,6 +167,7 @@ function CustomTimePicker({
|
|||||||
setOpen(newOpen);
|
setOpen(newOpen);
|
||||||
if (!newOpen) {
|
if (!newOpen) {
|
||||||
setCustomDTPickerVisible?.(false);
|
setCustomDTPickerVisible?.(false);
|
||||||
|
setActiveView('datetime');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -244,6 +281,7 @@ function CustomTimePicker({
|
|||||||
|
|
||||||
const handleFocus = (): void => {
|
const handleFocus = (): void => {
|
||||||
setIsInputFocused(true);
|
setIsInputFocused(true);
|
||||||
|
setActiveView('datetime');
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBlur = (): void => {
|
const handleBlur = (): void => {
|
||||||
@ -280,6 +318,10 @@ function CustomTimePicker({
|
|||||||
handleGoLive={defaultTo(handleGoLive, noop)}
|
handleGoLive={defaultTo(handleGoLive, noop)}
|
||||||
options={items}
|
options={items}
|
||||||
selectedTime={selectedTime}
|
selectedTime={selectedTime}
|
||||||
|
activeView={activeView}
|
||||||
|
setActiveView={setActiveView}
|
||||||
|
setIsOpenedFromFooter={setIsOpenedFromFooter}
|
||||||
|
isOpenedFromFooter={isOpenedFromFooter}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
content
|
content
|
||||||
@ -316,12 +358,24 @@ function CustomTimePicker({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
suffix={
|
suffix={
|
||||||
<ChevronDown
|
<>
|
||||||
size={14}
|
{!!isTimezoneOverridden && activeTimezoneOffset && (
|
||||||
onClick={(): void => {
|
<div
|
||||||
setOpen(!open);
|
className="timezone-badge"
|
||||||
}}
|
onClick={(e): void => {
|
||||||
/>
|
e.stopPropagation();
|
||||||
|
handleViewChange('timezone');
|
||||||
|
setIsOpenedFromFooter(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>{activeTimezoneOffset}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<ChevronDown
|
||||||
|
size={14}
|
||||||
|
onClick={(): void => handleViewChange('datetime')}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import './CustomTimePicker.styles.scss';
|
import './CustomTimePicker.styles.scss';
|
||||||
|
|
||||||
|
import { Color } from '@signozhq/design-tokens';
|
||||||
import { Button } from 'antd';
|
import { Button } from 'antd';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
@ -9,10 +10,13 @@ import {
|
|||||||
Option,
|
Option,
|
||||||
RelativeDurationSuggestionOptions,
|
RelativeDurationSuggestionOptions,
|
||||||
} from 'container/TopNav/DateTimeSelectionV2/config';
|
} from 'container/TopNav/DateTimeSelectionV2/config';
|
||||||
|
import { Clock, PenLine } from 'lucide-react';
|
||||||
|
import { useTimezone } from 'providers/Timezone';
|
||||||
import { Dispatch, SetStateAction, useMemo } from 'react';
|
import { Dispatch, SetStateAction, useMemo } from 'react';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
import RangePickerModal from './RangePickerModal';
|
import RangePickerModal from './RangePickerModal';
|
||||||
|
import TimezonePicker from './TimezonePicker';
|
||||||
|
|
||||||
interface CustomTimePickerPopoverContentProps {
|
interface CustomTimePickerPopoverContentProps {
|
||||||
options: any[];
|
options: any[];
|
||||||
@ -26,8 +30,13 @@ interface CustomTimePickerPopoverContentProps {
|
|||||||
onSelectHandler: (label: string, value: string) => void;
|
onSelectHandler: (label: string, value: string) => void;
|
||||||
handleGoLive: () => void;
|
handleGoLive: () => void;
|
||||||
selectedTime: string;
|
selectedTime: string;
|
||||||
|
activeView: 'datetime' | 'timezone';
|
||||||
|
setActiveView: Dispatch<SetStateAction<'datetime' | 'timezone'>>;
|
||||||
|
isOpenedFromFooter: boolean;
|
||||||
|
setIsOpenedFromFooter: Dispatch<SetStateAction<boolean>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
function CustomTimePickerPopoverContent({
|
function CustomTimePickerPopoverContent({
|
||||||
options,
|
options,
|
||||||
setIsOpen,
|
setIsOpen,
|
||||||
@ -37,12 +46,18 @@ function CustomTimePickerPopoverContent({
|
|||||||
onSelectHandler,
|
onSelectHandler,
|
||||||
handleGoLive,
|
handleGoLive,
|
||||||
selectedTime,
|
selectedTime,
|
||||||
|
activeView,
|
||||||
|
setActiveView,
|
||||||
|
isOpenedFromFooter,
|
||||||
|
setIsOpenedFromFooter,
|
||||||
}: CustomTimePickerPopoverContentProps): JSX.Element {
|
}: CustomTimePickerPopoverContentProps): JSX.Element {
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
|
|
||||||
const isLogsExplorerPage = useMemo(() => pathname === ROUTES.LOGS_EXPLORER, [
|
const isLogsExplorerPage = useMemo(() => pathname === ROUTES.LOGS_EXPLORER, [
|
||||||
pathname,
|
pathname,
|
||||||
]);
|
]);
|
||||||
|
const { timezone } = useTimezone();
|
||||||
|
const activeTimezoneOffset = timezone.offset;
|
||||||
|
|
||||||
function getTimeChips(options: Option[]): JSX.Element {
|
function getTimeChips(options: Option[]): JSX.Element {
|
||||||
return (
|
return (
|
||||||
@ -63,55 +78,99 @@ function CustomTimePickerPopoverContent({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleTimezoneHintClick = (): void => {
|
||||||
|
setActiveView('timezone');
|
||||||
|
setIsOpenedFromFooter(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (activeView === 'timezone') {
|
||||||
|
return (
|
||||||
|
<div className="date-time-popover">
|
||||||
|
<TimezonePicker
|
||||||
|
setActiveView={setActiveView}
|
||||||
|
setIsOpen={setIsOpen}
|
||||||
|
isOpenedFromFooter={isOpenedFromFooter}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="date-time-popover">
|
<>
|
||||||
<div className="date-time-options">
|
<div className="date-time-popover">
|
||||||
{isLogsExplorerPage && (
|
<div className="date-time-options">
|
||||||
<Button className="data-time-live" type="text" onClick={handleGoLive}>
|
{isLogsExplorerPage && (
|
||||||
Live
|
<Button className="data-time-live" type="text" onClick={handleGoLive}>
|
||||||
</Button>
|
Live
|
||||||
)}
|
</Button>
|
||||||
{options.map((option) => (
|
)}
|
||||||
<Button
|
{options.map((option) => (
|
||||||
type="text"
|
<Button
|
||||||
key={option.label + option.value}
|
type="text"
|
||||||
onClick={(): void => {
|
key={option.label + option.value}
|
||||||
onSelectHandler(option.label, option.value);
|
onClick={(): void => {
|
||||||
}}
|
onSelectHandler(option.label, option.value);
|
||||||
className={cx(
|
}}
|
||||||
'date-time-options-btn',
|
className={cx(
|
||||||
customDateTimeVisible
|
'date-time-options-btn',
|
||||||
? option.value === 'custom' && 'active'
|
customDateTimeVisible
|
||||||
: selectedTime === option.value && 'active',
|
? option.value === 'custom' && 'active'
|
||||||
)}
|
: selectedTime === option.value && 'active',
|
||||||
>
|
)}
|
||||||
{option.label}
|
>
|
||||||
</Button>
|
{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>
|
||||||
<div
|
|
||||||
className={cx(
|
<div className="date-time-popover__footer">
|
||||||
'relative-date-time',
|
<div className="timezone-container">
|
||||||
selectedTime === 'custom' || customDateTimeVisible
|
<Clock
|
||||||
? 'date-picker'
|
color={Color.BG_VANILLA_400}
|
||||||
: 'relative-times',
|
className="timezone-container__clock-icon"
|
||||||
)}
|
height={12}
|
||||||
>
|
width={12}
|
||||||
{selectedTime === 'custom' || customDateTimeVisible ? (
|
|
||||||
<RangePickerModal
|
|
||||||
setCustomDTPickerVisible={setCustomDTPickerVisible}
|
|
||||||
setIsOpen={setIsOpen}
|
|
||||||
onCustomDateHandler={onCustomDateHandler}
|
|
||||||
selectedTime={selectedTime}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
<span className="timezone__icon">Current timezone</span>
|
||||||
<div className="relative-times-container">
|
<div>⎯</div>
|
||||||
<div className="time-heading">RELATIVE TIMES</div>
|
<button
|
||||||
<div>{getTimeChips(RelativeDurationSuggestionOptions)}</div>
|
type="button"
|
||||||
</div>
|
className="timezone"
|
||||||
)}
|
onClick={handleTimezoneHintClick}
|
||||||
|
>
|
||||||
|
<span>{activeTimezoneOffset}</span>
|
||||||
|
<PenLine
|
||||||
|
color={Color.BG_VANILLA_100}
|
||||||
|
className="timezone__icon"
|
||||||
|
size={10}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,7 +3,8 @@ import './RangePickerModal.styles.scss';
|
|||||||
import { DatePicker } from 'antd';
|
import { DatePicker } from 'antd';
|
||||||
import { DateTimeRangeType } from 'container/TopNav/CustomDateTimeModal';
|
import { DateTimeRangeType } from 'container/TopNav/CustomDateTimeModal';
|
||||||
import { LexicalContext } from 'container/TopNav/DateTimeSelectionV2/config';
|
import { LexicalContext } from 'container/TopNav/DateTimeSelectionV2/config';
|
||||||
import dayjs, { Dayjs } from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
import { useTimezone } from 'providers/Timezone';
|
||||||
import { Dispatch, SetStateAction } from 'react';
|
import { Dispatch, SetStateAction } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
@ -31,7 +32,10 @@ function RangePickerModal(props: RangePickerModalProps): JSX.Element {
|
|||||||
(state) => state.globalTime,
|
(state) => state.globalTime,
|
||||||
);
|
);
|
||||||
|
|
||||||
const disabledDate = (current: Dayjs): boolean => {
|
// Using any type here because antd's DatePicker expects its own internal Dayjs type
|
||||||
|
// which conflicts with our project's Dayjs type that has additional plugins (tz, utc etc).
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
|
||||||
|
const disabledDate = (current: any): boolean => {
|
||||||
const currentDay = dayjs(current);
|
const currentDay = dayjs(current);
|
||||||
return currentDay.isAfter(dayjs());
|
return currentDay.isAfter(dayjs());
|
||||||
};
|
};
|
||||||
@ -49,16 +53,22 @@ function RangePickerModal(props: RangePickerModalProps): JSX.Element {
|
|||||||
}
|
}
|
||||||
onCustomDateHandler(date_time, LexicalContext.CUSTOM_DATE_PICKER);
|
onCustomDateHandler(date_time, LexicalContext.CUSTOM_DATE_PICKER);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { timezone } = useTimezone();
|
||||||
return (
|
return (
|
||||||
<div className="custom-date-picker">
|
<div className="custom-date-picker">
|
||||||
<RangePicker
|
<RangePicker
|
||||||
disabledDate={disabledDate}
|
disabledDate={disabledDate}
|
||||||
allowClear
|
allowClear
|
||||||
showTime
|
showTime
|
||||||
|
format="YYYY-MM-DD hh:mm A"
|
||||||
onOk={onModalOkHandler}
|
onOk={onModalOkHandler}
|
||||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
{...(selectedTime === 'custom' && {
|
{...(selectedTime === 'custom' && {
|
||||||
defaultValue: [dayjs(minTime / 1000000), dayjs(maxTime / 1000000)],
|
defaultValue: [
|
||||||
|
dayjs(minTime / 1000000).tz(timezone.value),
|
||||||
|
dayjs(maxTime / 1000000).tz(timezone.value),
|
||||||
|
],
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -0,0 +1,166 @@
|
|||||||
|
// Variables
|
||||||
|
$font-family: 'Inter';
|
||||||
|
$item-spacing: 8px;
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--border-color: var(--bg-slate-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
--border-color: var(--bg-vanilla-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 var(--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;
|
||||||
|
&.ant-input:focus {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::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 var(--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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.timezone-picker {
|
||||||
|
&__search {
|
||||||
|
.search-icon {
|
||||||
|
stroke: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&__input {
|
||||||
|
color: var(--bg-ink-100);
|
||||||
|
}
|
||||||
|
&__esc-key {
|
||||||
|
background-color: var(--bg-vanilla-100);
|
||||||
|
border-color: var(--bg-vanilla-400);
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
&__item {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
&__offset {
|
||||||
|
color: var(--bg-ink-100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.timezone-name-wrapper {
|
||||||
|
&__selected-icon {
|
||||||
|
.check-icon {
|
||||||
|
stroke: var(--bg-ink-100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
201
frontend/src/components/CustomTimePicker/TimezonePicker.tsx
Normal file
201
frontend/src/components/CustomTimePicker/TimezonePicker.tsx
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
import './TimezonePicker.styles.scss';
|
||||||
|
|
||||||
|
import { Color } from '@signozhq/design-tokens';
|
||||||
|
import { Input } from 'antd';
|
||||||
|
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;
|
||||||
|
setIsOpen: Dispatch<SetStateAction<boolean>>;
|
||||||
|
setActiveView: Dispatch<SetStateAction<'datetime' | 'timezone'>>;
|
||||||
|
isOpenedFromFooter: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TimezoneItemProps {
|
||||||
|
timezone: Timezone;
|
||||||
|
isSelected?: boolean;
|
||||||
|
onClick?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ICON_SIZE = 14;
|
||||||
|
|
||||||
|
function SearchBar({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
setIsOpen,
|
||||||
|
setActiveView,
|
||||||
|
isOpenedFromFooter = false,
|
||||||
|
}: SearchBarProps): JSX.Element {
|
||||||
|
const handleKeyDown = useCallback(
|
||||||
|
(e: React.KeyboardEvent): void => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
if (isOpenedFromFooter) {
|
||||||
|
setActiveView('datetime');
|
||||||
|
} else {
|
||||||
|
setIsOpen(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[setActiveView, setIsOpen, isOpenedFromFooter],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="timezone-picker__search">
|
||||||
|
<div className="timezone-picker__input-container">
|
||||||
|
<Search
|
||||||
|
color={Color.BG_VANILLA_400}
|
||||||
|
className="search-icon"
|
||||||
|
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)}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
tabIndex={0}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</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
|
||||||
|
className="check-icon"
|
||||||
|
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>>;
|
||||||
|
isOpenedFromFooter: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TimezonePicker({
|
||||||
|
setActiveView,
|
||||||
|
setIsOpen,
|
||||||
|
isOpenedFromFooter,
|
||||||
|
}: TimezonePickerProps): JSX.Element {
|
||||||
|
console.log({ isOpenedFromFooter });
|
||||||
|
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(() => {
|
||||||
|
if (isOpenedFromFooter) {
|
||||||
|
setActiveView('datetime');
|
||||||
|
} else {
|
||||||
|
setIsOpen(false);
|
||||||
|
}
|
||||||
|
}, [isOpenedFromFooter, setActiveView, setIsOpen]);
|
||||||
|
|
||||||
|
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}
|
||||||
|
setIsOpen={setIsOpen}
|
||||||
|
setActiveView={setActiveView}
|
||||||
|
isOpenedFromFooter={isOpenedFromFooter}
|
||||||
|
/>
|
||||||
|
<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;
|
152
frontend/src/components/CustomTimePicker/timezoneUtils.ts
Normal file
152
frontend/src/components/CustomTimePicker/timezoneUtils.ts
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TIMEZONE_TYPES = {
|
||||||
|
BROWSER: 'BROWSER',
|
||||||
|
UTC: 'UTC',
|
||||||
|
STANDARD: 'STANDARD',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
type TimezoneType = typeof TIMEZONE_TYPES[keyof typeof TIMEZONE_TYPES];
|
||||||
|
|
||||||
|
export const UTC_TIMEZONE: Timezone = {
|
||||||
|
name: 'Coordinated Universal Time — UTC, GMT',
|
||||||
|
value: 'UTC',
|
||||||
|
offset: 'UTC',
|
||||||
|
searchIndex: 'UTC',
|
||||||
|
hasDivider: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeTimezoneName = (timezone: string): string => {
|
||||||
|
// https://github.com/tc39/proposal-temporal/issues/1076
|
||||||
|
if (timezone === 'Asia/Calcutta') {
|
||||||
|
return 'Asia/Kolkata';
|
||||||
|
}
|
||||||
|
return timezone;
|
||||||
|
};
|
||||||
|
|
||||||
|
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 normalizedTz = normalizeTimezoneName(browserTz);
|
||||||
|
const browserOffset = getOffsetByTimezone(normalizedTz);
|
||||||
|
return createTimezoneEntry(
|
||||||
|
normalizedTz,
|
||||||
|
browserOffset,
|
||||||
|
TIMEZONE_TYPES.BROWSER,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const filterAndSortTimezones = (
|
||||||
|
allTimezones: string[],
|
||||||
|
browserTzName?: string,
|
||||||
|
includeEtcTimezones = false,
|
||||||
|
): Timezone[] =>
|
||||||
|
allTimezones
|
||||||
|
.filter((tz) => {
|
||||||
|
const isNotBrowserTz = tz !== browserTzName;
|
||||||
|
const isNotEtcTz = includeEtcTimezones || !tz.startsWith('Etc/');
|
||||||
|
return isNotBrowserTz && isNotEtcTz;
|
||||||
|
})
|
||||||
|
.sort((a, b) => a.localeCompare(b))
|
||||||
|
.map((tz) => {
|
||||||
|
const normalizedTz = normalizeTimezoneName(tz);
|
||||||
|
const offset = getOffsetByTimezone(normalizedTz);
|
||||||
|
return createTimezoneEntry(normalizedTz, offset);
|
||||||
|
});
|
||||||
|
|
||||||
|
const generateTimezoneData = (includeEtcTimezones = false): Timezone[] => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const allTimezones = (Intl as any).supportedValuesOf('timeZone');
|
||||||
|
const timezones: Timezone[] = [];
|
||||||
|
|
||||||
|
// Add browser timezone
|
||||||
|
const browserTzObject = getBrowserTimezone();
|
||||||
|
timezones.push(browserTzObject);
|
||||||
|
|
||||||
|
// Add UTC timezone with divider
|
||||||
|
timezones.push(UTC_TIMEZONE);
|
||||||
|
|
||||||
|
timezones.push(
|
||||||
|
...filterAndSortTimezones(
|
||||||
|
allTimezones,
|
||||||
|
browserTzObject.value,
|
||||||
|
includeEtcTimezones,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return timezones;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getTimezoneObjectByTimezoneString = (
|
||||||
|
timezone: string,
|
||||||
|
): Timezone => {
|
||||||
|
const utcOffset = getOffsetByTimezone(timezone);
|
||||||
|
return createTimezoneEntry(timezone, utcOffset);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TIMEZONE_DATA = generateTimezoneData();
|
@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
_adapters,
|
||||||
BarController,
|
BarController,
|
||||||
BarElement,
|
BarElement,
|
||||||
CategoryScale,
|
CategoryScale,
|
||||||
@ -18,8 +19,10 @@ import {
|
|||||||
} from 'chart.js';
|
} from 'chart.js';
|
||||||
import annotationPlugin from 'chartjs-plugin-annotation';
|
import annotationPlugin from 'chartjs-plugin-annotation';
|
||||||
import { generateGridTitle } from 'container/GridPanelSwitch/utils';
|
import { generateGridTitle } from 'container/GridPanelSwitch/utils';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import isEqual from 'lodash-es/isEqual';
|
import isEqual from 'lodash-es/isEqual';
|
||||||
|
import { useTimezone } from 'providers/Timezone';
|
||||||
import {
|
import {
|
||||||
forwardRef,
|
forwardRef,
|
||||||
memo,
|
memo,
|
||||||
@ -62,6 +65,17 @@ Chart.register(
|
|||||||
|
|
||||||
Tooltip.positioners.custom = TooltipPositionHandler;
|
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>(
|
const Graph = forwardRef<ToggleGraphProps | undefined, GraphProps>(
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
@ -80,11 +94,13 @@ const Graph = forwardRef<ToggleGraphProps | undefined, GraphProps>(
|
|||||||
dragSelectColor,
|
dragSelectColor,
|
||||||
},
|
},
|
||||||
ref,
|
ref,
|
||||||
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
): JSX.Element => {
|
): JSX.Element => {
|
||||||
const nearestDatasetIndex = useRef<null | number>(null);
|
const nearestDatasetIndex = useRef<null | number>(null);
|
||||||
const chartRef = useRef<HTMLCanvasElement>(null);
|
const chartRef = useRef<HTMLCanvasElement>(null);
|
||||||
const isDarkMode = useIsDarkMode();
|
const isDarkMode = useIsDarkMode();
|
||||||
const gridTitle = useMemo(() => generateGridTitle(title), [title]);
|
const gridTitle = useMemo(() => generateGridTitle(title), [title]);
|
||||||
|
const { timezone } = useTimezone();
|
||||||
|
|
||||||
const currentTheme = isDarkMode ? 'dark' : 'light';
|
const currentTheme = isDarkMode ? 'dark' : 'light';
|
||||||
const xAxisTimeUnit = useXAxisTimeUnit(data); // Computes the relevant time unit for x axis by analyzing the time stamp data
|
const xAxisTimeUnit = useXAxisTimeUnit(data); // Computes the relevant time unit for x axis by analyzing the time stamp data
|
||||||
@ -112,6 +128,22 @@ const Graph = forwardRef<ToggleGraphProps | undefined, GraphProps>(
|
|||||||
return 'rgba(231,233,237,0.8)';
|
return 'rgba(231,233,237,0.8)';
|
||||||
}, [currentTheme]);
|
}, [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(() => {
|
const buildChart = useCallback(() => {
|
||||||
if (lineChartRef.current !== undefined) {
|
if (lineChartRef.current !== undefined) {
|
||||||
lineChartRef.current.destroy();
|
lineChartRef.current.destroy();
|
||||||
@ -132,6 +164,7 @@ const Graph = forwardRef<ToggleGraphProps | undefined, GraphProps>(
|
|||||||
isStacked,
|
isStacked,
|
||||||
onClickHandler,
|
onClickHandler,
|
||||||
data,
|
data,
|
||||||
|
timezone,
|
||||||
);
|
);
|
||||||
|
|
||||||
const chartHasData = hasData(data);
|
const chartHasData = hasData(data);
|
||||||
@ -166,6 +199,7 @@ const Graph = forwardRef<ToggleGraphProps | undefined, GraphProps>(
|
|||||||
isStacked,
|
isStacked,
|
||||||
onClickHandler,
|
onClickHandler,
|
||||||
data,
|
data,
|
||||||
|
timezone,
|
||||||
name,
|
name,
|
||||||
type,
|
type,
|
||||||
]);
|
]);
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { Chart, ChartConfiguration, ChartData, Color } from 'chart.js';
|
import { Chart, ChartConfiguration, ChartData, Color } from 'chart.js';
|
||||||
import * as chartjsAdapter from 'chartjs-adapter-date-fns';
|
import * as chartjsAdapter from 'chartjs-adapter-date-fns';
|
||||||
|
import { Timezone } from 'components/CustomTimePicker/timezoneUtils';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { MutableRefObject } from 'react';
|
import { MutableRefObject } from 'react';
|
||||||
|
|
||||||
@ -50,6 +51,7 @@ export const getGraphOptions = (
|
|||||||
isStacked: boolean | undefined,
|
isStacked: boolean | undefined,
|
||||||
onClickHandler: GraphOnClickHandler | undefined,
|
onClickHandler: GraphOnClickHandler | undefined,
|
||||||
data: ChartData,
|
data: ChartData,
|
||||||
|
timezone: Timezone,
|
||||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
): CustomChartOptions => ({
|
): CustomChartOptions => ({
|
||||||
animation: {
|
animation: {
|
||||||
@ -97,7 +99,7 @@ export const getGraphOptions = (
|
|||||||
callbacks: {
|
callbacks: {
|
||||||
title(context): string | string[] {
|
title(context): string | string[] {
|
||||||
const date = dayjs(context[0].parsed.x);
|
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[] {
|
label(context): string | string[] {
|
||||||
let label = context.dataset.label || '';
|
let label = context.dataset.label || '';
|
||||||
|
@ -8,13 +8,13 @@ import LogDetail from 'components/LogDetail';
|
|||||||
import { VIEW_TYPES } from 'components/LogDetail/constants';
|
import { VIEW_TYPES } from 'components/LogDetail/constants';
|
||||||
import { unescapeString } from 'container/LogDetailedView/utils';
|
import { unescapeString } from 'container/LogDetailedView/utils';
|
||||||
import { FontSize } from 'container/OptionsMenu/types';
|
import { FontSize } from 'container/OptionsMenu/types';
|
||||||
import dayjs from 'dayjs';
|
|
||||||
import dompurify from 'dompurify';
|
import dompurify from 'dompurify';
|
||||||
import { useActiveLog } from 'hooks/logs/useActiveLog';
|
import { useActiveLog } from 'hooks/logs/useActiveLog';
|
||||||
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
// utils
|
// utils
|
||||||
import { FlatLogData } from 'lib/logs/flatLogData';
|
import { FlatLogData } from 'lib/logs/flatLogData';
|
||||||
|
import { useTimezone } from 'providers/Timezone';
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
// interfaces
|
// interfaces
|
||||||
import { IField } from 'types/api/logs/fields';
|
import { IField } from 'types/api/logs/fields';
|
||||||
@ -174,12 +174,20 @@ function ListLogView({
|
|||||||
[selectedFields],
|
[selectedFields],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { formatTimezoneAdjustedTimestamp } = useTimezone();
|
||||||
|
|
||||||
const timestampValue = useMemo(
|
const timestampValue = useMemo(
|
||||||
() =>
|
() =>
|
||||||
typeof flattenLogData.timestamp === 'string'
|
typeof flattenLogData.timestamp === 'string'
|
||||||
? dayjs(flattenLogData.timestamp).format('YYYY-MM-DD HH:mm:ss.SSS')
|
? formatTimezoneAdjustedTimestamp(
|
||||||
: dayjs(flattenLogData.timestamp / 1e6).format('YYYY-MM-DD HH:mm:ss.SSS'),
|
flattenLogData.timestamp,
|
||||||
[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);
|
const logType = getLogIndicatorType(logData);
|
||||||
|
@ -6,7 +6,6 @@ import LogDetail from 'components/LogDetail';
|
|||||||
import { VIEW_TYPES, VIEWS } from 'components/LogDetail/constants';
|
import { VIEW_TYPES, VIEWS } from 'components/LogDetail/constants';
|
||||||
import { unescapeString } from 'container/LogDetailedView/utils';
|
import { unescapeString } from 'container/LogDetailedView/utils';
|
||||||
import LogsExplorerContext from 'container/LogsExplorerContext';
|
import LogsExplorerContext from 'container/LogsExplorerContext';
|
||||||
import dayjs from 'dayjs';
|
|
||||||
import dompurify from 'dompurify';
|
import dompurify from 'dompurify';
|
||||||
import { useActiveLog } from 'hooks/logs/useActiveLog';
|
import { useActiveLog } from 'hooks/logs/useActiveLog';
|
||||||
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
||||||
@ -14,6 +13,7 @@ import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
|||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import { FlatLogData } from 'lib/logs/flatLogData';
|
import { FlatLogData } from 'lib/logs/flatLogData';
|
||||||
import { isEmpty, isNumber, isUndefined } from 'lodash-es';
|
import { isEmpty, isNumber, isUndefined } from 'lodash-es';
|
||||||
|
import { useTimezone } from 'providers/Timezone';
|
||||||
import {
|
import {
|
||||||
KeyboardEvent,
|
KeyboardEvent,
|
||||||
MouseEvent,
|
MouseEvent,
|
||||||
@ -89,16 +89,24 @@ function RawLogView({
|
|||||||
attributesText += ' | ';
|
attributesText += ' | ';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { formatTimezoneAdjustedTimestamp } = useTimezone();
|
||||||
|
|
||||||
const text = useMemo(() => {
|
const text = useMemo(() => {
|
||||||
const date =
|
const date =
|
||||||
typeof data.timestamp === 'string'
|
typeof data.timestamp === 'string'
|
||||||
? dayjs(data.timestamp)
|
? formatTimezoneAdjustedTimestamp(data.timestamp, 'YYYY-MM-DD HH:mm:ss.SSS')
|
||||||
: dayjs(data.timestamp / 1e6);
|
: formatTimezoneAdjustedTimestamp(
|
||||||
|
data.timestamp / 1e6,
|
||||||
|
'YYYY-MM-DD HH:mm:ss.SSS',
|
||||||
|
);
|
||||||
|
|
||||||
return `${date.format('YYYY-MM-DD HH:mm:ss.SSS')} | ${attributesText} ${
|
return `${date} | ${attributesText} ${data.body}`;
|
||||||
data.body
|
}, [
|
||||||
}`;
|
data.timestamp,
|
||||||
}, [data.timestamp, data.body, attributesText]);
|
data.body,
|
||||||
|
attributesText,
|
||||||
|
formatTimezoneAdjustedTimestamp,
|
||||||
|
]);
|
||||||
|
|
||||||
const handleClickExpand = useCallback(() => {
|
const handleClickExpand = useCallback(() => {
|
||||||
if (activeContextLog || isReadOnly) return;
|
if (activeContextLog || isReadOnly) return;
|
||||||
|
@ -5,10 +5,10 @@ import { Typography } from 'antd';
|
|||||||
import { ColumnsType } from 'antd/es/table';
|
import { ColumnsType } from 'antd/es/table';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
import { unescapeString } from 'container/LogDetailedView/utils';
|
import { unescapeString } from 'container/LogDetailedView/utils';
|
||||||
import dayjs from 'dayjs';
|
|
||||||
import dompurify from 'dompurify';
|
import dompurify from 'dompurify';
|
||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import { FlatLogData } from 'lib/logs/flatLogData';
|
import { FlatLogData } from 'lib/logs/flatLogData';
|
||||||
|
import { useTimezone } from 'providers/Timezone';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { FORBID_DOM_PURIFY_TAGS } from 'utils/app';
|
import { FORBID_DOM_PURIFY_TAGS } from 'utils/app';
|
||||||
|
|
||||||
@ -44,6 +44,8 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
|
|||||||
logs,
|
logs,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const { formatTimezoneAdjustedTimestamp } = useTimezone();
|
||||||
|
|
||||||
const columns: ColumnsType<Record<string, unknown>> = useMemo(() => {
|
const columns: ColumnsType<Record<string, unknown>> = useMemo(() => {
|
||||||
const fieldColumns: ColumnsType<Record<string, unknown>> = fields
|
const fieldColumns: ColumnsType<Record<string, unknown>> = fields
|
||||||
.filter((e) => e.name !== 'id')
|
.filter((e) => e.name !== 'id')
|
||||||
@ -81,8 +83,11 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
|
|||||||
render: (field, item): ColumnTypeRender<Record<string, unknown>> => {
|
render: (field, item): ColumnTypeRender<Record<string, unknown>> => {
|
||||||
const date =
|
const date =
|
||||||
typeof field === 'string'
|
typeof field === 'string'
|
||||||
? dayjs(field).format('YYYY-MM-DD HH:mm:ss.SSS')
|
? formatTimezoneAdjustedTimestamp(field, 'YYYY-MM-DD HH:mm:ss.SSS')
|
||||||
: dayjs(field / 1e6).format('YYYY-MM-DD HH:mm:ss.SSS');
|
: formatTimezoneAdjustedTimestamp(
|
||||||
|
field / 1e6,
|
||||||
|
'YYYY-MM-DD HH:mm:ss.SSS',
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
children: (
|
children: (
|
||||||
<div className="table-timestamp">
|
<div className="table-timestamp">
|
||||||
@ -125,7 +130,15 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
|
|||||||
},
|
},
|
||||||
...(appendTo === 'end' ? fieldColumns : []),
|
...(appendTo === 'end' ? fieldColumns : []),
|
||||||
];
|
];
|
||||||
}, [fields, isListViewPanel, appendTo, isDarkMode, linesPerRow, fontSize]);
|
}, [
|
||||||
|
fields,
|
||||||
|
isListViewPanel,
|
||||||
|
appendTo,
|
||||||
|
isDarkMode,
|
||||||
|
linesPerRow,
|
||||||
|
fontSize,
|
||||||
|
formatTimezoneAdjustedTimestamp,
|
||||||
|
]);
|
||||||
|
|
||||||
return { columns, dataSource: flattenLogData };
|
return { columns, dataSource: flattenLogData };
|
||||||
};
|
};
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
import { Typography } from 'antd';
|
import { Typography } from 'antd';
|
||||||
import convertDateToAmAndPm from 'lib/convertDateToAmAndPm';
|
import { useTimezone } from 'providers/Timezone';
|
||||||
import getFormattedDate from 'lib/getFormatedDate';
|
|
||||||
|
|
||||||
function Time({ CreatedOrUpdateTime }: DateProps): JSX.Element {
|
function Time({ CreatedOrUpdateTime }: DateProps): JSX.Element {
|
||||||
|
const { formatTimezoneAdjustedTimestamp } = useTimezone();
|
||||||
const time = new Date(CreatedOrUpdateTime);
|
const time = new Date(CreatedOrUpdateTime);
|
||||||
const date = getFormattedDate(time);
|
const timeString = formatTimezoneAdjustedTimestamp(
|
||||||
const timeString = `${date} ${convertDateToAmAndPm(time)}`;
|
time,
|
||||||
|
'MM/DD/YYYY hh:mm:ss A (UTC Z)',
|
||||||
|
);
|
||||||
return <Typography>{timeString}</Typography>;
|
return <Typography>{timeString}</Typography>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,4 +21,5 @@ export enum LOCALSTORAGE {
|
|||||||
THEME_ANALYTICS_V1 = 'THEME_ANALYTICS_V1',
|
THEME_ANALYTICS_V1 = 'THEME_ANALYTICS_V1',
|
||||||
LAST_USED_SAVED_VIEWS = 'LAST_USED_SAVED_VIEWS',
|
LAST_USED_SAVED_VIEWS = 'LAST_USED_SAVED_VIEWS',
|
||||||
SHOW_LOGS_QUICK_FILTERS = 'SHOW_LOGS_QUICK_FILTERS',
|
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 history from 'lib/history';
|
||||||
import heatmapPlugin from 'lib/uPlotLib/plugins/heatmapPlugin';
|
import heatmapPlugin from 'lib/uPlotLib/plugins/heatmapPlugin';
|
||||||
import timelinePlugin from 'lib/uPlotLib/plugins/timelinePlugin';
|
import timelinePlugin from 'lib/uPlotLib/plugins/timelinePlugin';
|
||||||
|
import { useTimezone } from 'providers/Timezone';
|
||||||
import { useMemo, useRef } from 'react';
|
import { useMemo, useRef } from 'react';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { UpdateTimeInterval } from 'store/actions';
|
import { UpdateTimeInterval } from 'store/actions';
|
||||||
@ -48,6 +49,7 @@ function HorizontalTimelineGraph({
|
|||||||
|
|
||||||
const urlQuery = useUrlQuery();
|
const urlQuery = useUrlQuery();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
const { timezone } = useTimezone();
|
||||||
|
|
||||||
const options: uPlot.Options = useMemo(
|
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} />;
|
return <Uplot data={transformedData} options={options} />;
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ import {
|
|||||||
useGetAlertRuleDetailsTimelineTable,
|
useGetAlertRuleDetailsTimelineTable,
|
||||||
useTimelineTable,
|
useTimelineTable,
|
||||||
} from 'pages/AlertDetails/hooks';
|
} from 'pages/AlertDetails/hooks';
|
||||||
|
import { useTimezone } from 'providers/Timezone';
|
||||||
import { HTMLAttributes, useMemo, useState } from 'react';
|
import { HTMLAttributes, useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { AlertRuleTimelineTableResponse } from 'types/api/alerts/def';
|
import { AlertRuleTimelineTableResponse } from 'types/api/alerts/def';
|
||||||
@ -41,6 +42,8 @@ function TimelineTable(): JSX.Element {
|
|||||||
|
|
||||||
const { t } = useTranslation('common');
|
const { t } = useTranslation('common');
|
||||||
|
|
||||||
|
const { formatTimezoneAdjustedTimestamp } = useTimezone();
|
||||||
|
|
||||||
if (isError || !isValidRuleId || !ruleId) {
|
if (isError || !isValidRuleId || !ruleId) {
|
||||||
return <div>{t('something_went_wrong')}</div>;
|
return <div>{t('something_went_wrong')}</div>;
|
||||||
}
|
}
|
||||||
@ -64,6 +67,7 @@ function TimelineTable(): JSX.Element {
|
|||||||
filters,
|
filters,
|
||||||
labels: labels ?? {},
|
labels: labels ?? {},
|
||||||
setFilters,
|
setFilters,
|
||||||
|
formatTimezoneAdjustedTimestamp,
|
||||||
})}
|
})}
|
||||||
onRow={handleRowClick}
|
onRow={handleRowClick}
|
||||||
dataSource={timelineData}
|
dataSource={timelineData}
|
||||||
|
@ -8,6 +8,7 @@ import ClientSideQBSearch, {
|
|||||||
import { ConditionalAlertPopover } from 'container/AlertHistory/AlertPopover/AlertPopover';
|
import { ConditionalAlertPopover } from 'container/AlertHistory/AlertPopover/AlertPopover';
|
||||||
import { transformKeyValuesToAttributeValuesMap } from 'container/QueryBuilder/filters/utils';
|
import { transformKeyValuesToAttributeValuesMap } from 'container/QueryBuilder/filters/utils';
|
||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
|
import { TimestampInput } from 'hooks/useTimezoneFormatter/useTimezoneFormatter';
|
||||||
import { Search } from 'lucide-react';
|
import { Search } from 'lucide-react';
|
||||||
import AlertLabels, {
|
import AlertLabels, {
|
||||||
AlertLabelsProps,
|
AlertLabelsProps,
|
||||||
@ -16,7 +17,6 @@ import AlertState from 'pages/AlertDetails/AlertHeader/AlertState/AlertState';
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { AlertRuleTimelineTableResponse } from 'types/api/alerts/def';
|
import { AlertRuleTimelineTableResponse } from 'types/api/alerts/def';
|
||||||
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
import { formatEpochTimestamp } from 'utils/timeUtils';
|
|
||||||
|
|
||||||
const transformLabelsToQbKeys = (
|
const transformLabelsToQbKeys = (
|
||||||
labels: AlertRuleTimelineTableResponse['labels'],
|
labels: AlertRuleTimelineTableResponse['labels'],
|
||||||
@ -74,10 +74,15 @@ export const timelineTableColumns = ({
|
|||||||
filters,
|
filters,
|
||||||
labels,
|
labels,
|
||||||
setFilters,
|
setFilters,
|
||||||
|
formatTimezoneAdjustedTimestamp,
|
||||||
}: {
|
}: {
|
||||||
filters: TagFilter;
|
filters: TagFilter;
|
||||||
labels: AlertLabelsProps['labels'];
|
labels: AlertLabelsProps['labels'];
|
||||||
setFilters: (filters: TagFilter) => void;
|
setFilters: (filters: TagFilter) => void;
|
||||||
|
formatTimezoneAdjustedTimestamp: (
|
||||||
|
input: TimestampInput,
|
||||||
|
format?: string,
|
||||||
|
) => string;
|
||||||
}): ColumnsType<AlertRuleTimelineTableResponse> => [
|
}): ColumnsType<AlertRuleTimelineTableResponse> => [
|
||||||
{
|
{
|
||||||
title: 'STATE',
|
title: 'STATE',
|
||||||
@ -106,7 +111,9 @@ export const timelineTableColumns = ({
|
|||||||
dataIndex: 'unixMilli',
|
dataIndex: 'unixMilli',
|
||||||
width: 200,
|
width: 200,
|
||||||
render: (value): JSX.Element => (
|
render: (value): JSX.Element => (
|
||||||
<div className="alert-rule__created-at">{formatEpochTimestamp(value)}</div>
|
<div className="alert-rule__created-at">
|
||||||
|
{formatTimezoneAdjustedTimestamp(value, 'MMM D, YYYY ⎯ HH:mm:ss')}
|
||||||
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -17,14 +17,15 @@ import getAll from 'api/errors/getAll';
|
|||||||
import getErrorCounts from 'api/errors/getErrorCounts';
|
import getErrorCounts from 'api/errors/getErrorCounts';
|
||||||
import { ResizeTable } from 'components/ResizeTable';
|
import { ResizeTable } from 'components/ResizeTable';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import dayjs from 'dayjs';
|
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import useResourceAttribute from 'hooks/useResourceAttribute';
|
import useResourceAttribute from 'hooks/useResourceAttribute';
|
||||||
import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils';
|
import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils';
|
||||||
|
import { TimestampInput } from 'hooks/useTimezoneFormatter/useTimezoneFormatter';
|
||||||
import useUrlQuery from 'hooks/useUrlQuery';
|
import useUrlQuery from 'hooks/useUrlQuery';
|
||||||
import createQueryParams from 'lib/createQueryParams';
|
import createQueryParams from 'lib/createQueryParams';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import { isUndefined } from 'lodash-es';
|
import { isUndefined } from 'lodash-es';
|
||||||
|
import { useTimezone } from 'providers/Timezone';
|
||||||
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useQueries } from 'react-query';
|
import { useQueries } from 'react-query';
|
||||||
@ -155,8 +156,16 @@ function AllErrors(): JSX.Element {
|
|||||||
}
|
}
|
||||||
}, [data?.error, data?.payload, t, notifications]);
|
}, [data?.error, data?.payload, t, notifications]);
|
||||||
|
|
||||||
const getDateValue = (value: string): JSX.Element => (
|
const getDateValue = (
|
||||||
<Typography>{dayjs(value).format('DD/MM/YYYY HH:mm:ss A')}</Typography>
|
value: string,
|
||||||
|
formatTimezoneAdjustedTimestamp: (
|
||||||
|
input: TimestampInput,
|
||||||
|
format?: string,
|
||||||
|
) => string,
|
||||||
|
): JSX.Element => (
|
||||||
|
<Typography>
|
||||||
|
{formatTimezoneAdjustedTimestamp(value, 'DD/MM/YYYY hh:mm:ss A')}
|
||||||
|
</Typography>
|
||||||
);
|
);
|
||||||
|
|
||||||
const filterIcon = useCallback(() => <SearchOutlined />, []);
|
const filterIcon = useCallback(() => <SearchOutlined />, []);
|
||||||
@ -283,6 +292,8 @@ function AllErrors(): JSX.Element {
|
|||||||
[filterIcon, filterDropdownWrapper],
|
[filterIcon, filterDropdownWrapper],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { formatTimezoneAdjustedTimestamp } = useTimezone();
|
||||||
|
|
||||||
const columns: ColumnsType<Exception> = [
|
const columns: ColumnsType<Exception> = [
|
||||||
{
|
{
|
||||||
title: 'Exception Type',
|
title: 'Exception Type',
|
||||||
@ -342,7 +353,8 @@ function AllErrors(): JSX.Element {
|
|||||||
dataIndex: 'lastSeen',
|
dataIndex: 'lastSeen',
|
||||||
width: 80,
|
width: 80,
|
||||||
key: 'lastSeen',
|
key: 'lastSeen',
|
||||||
render: getDateValue,
|
render: (value): JSX.Element =>
|
||||||
|
getDateValue(value, formatTimezoneAdjustedTimestamp),
|
||||||
sorter: true,
|
sorter: true,
|
||||||
defaultSortOrder: getDefaultOrder(
|
defaultSortOrder: getDefaultOrder(
|
||||||
getUpdatedParams,
|
getUpdatedParams,
|
||||||
@ -355,7 +367,8 @@ function AllErrors(): JSX.Element {
|
|||||||
dataIndex: 'firstSeen',
|
dataIndex: 'firstSeen',
|
||||||
width: 80,
|
width: 80,
|
||||||
key: 'firstSeen',
|
key: 'firstSeen',
|
||||||
render: getDateValue,
|
render: (value): JSX.Element =>
|
||||||
|
getDateValue(value, formatTimezoneAdjustedTimestamp),
|
||||||
sorter: true,
|
sorter: true,
|
||||||
defaultSortOrder: getDefaultOrder(
|
defaultSortOrder: getDefaultOrder(
|
||||||
getUpdatedParams,
|
getUpdatedParams,
|
||||||
|
@ -10,6 +10,7 @@ import getAxes from 'lib/uPlotLib/utils/getAxes';
|
|||||||
import { getUplotChartDataForAnomalyDetection } from 'lib/uPlotLib/utils/getUplotChartData';
|
import { getUplotChartDataForAnomalyDetection } from 'lib/uPlotLib/utils/getUplotChartData';
|
||||||
import { getYAxisScaleForAnomalyDetection } from 'lib/uPlotLib/utils/getYAxisScale';
|
import { getYAxisScaleForAnomalyDetection } from 'lib/uPlotLib/utils/getYAxisScale';
|
||||||
import { LineChart } from 'lucide-react';
|
import { LineChart } from 'lucide-react';
|
||||||
|
import { useTimezone } from 'providers/Timezone';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import uPlot from 'uplot';
|
import uPlot from 'uplot';
|
||||||
|
|
||||||
@ -148,10 +149,12 @@ function AnomalyAlertEvaluationView({
|
|||||||
]
|
]
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
|
const { timezone } = useTimezone();
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
width: dimensions.width,
|
width: dimensions.width,
|
||||||
height: dimensions.height - 36,
|
height: dimensions.height - 36,
|
||||||
plugins: [bandsPlugin, tooltipPlugin(isDarkMode)],
|
plugins: [bandsPlugin, tooltipPlugin(isDarkMode, timezone.value)],
|
||||||
focus: {
|
focus: {
|
||||||
alpha: 0.3,
|
alpha: 0.3,
|
||||||
},
|
},
|
||||||
@ -256,6 +259,8 @@ function AnomalyAlertEvaluationView({
|
|||||||
show: true,
|
show: true,
|
||||||
},
|
},
|
||||||
axes: getAxes(isDarkMode, yAxisUnit),
|
axes: getAxes(isDarkMode, yAxisUnit),
|
||||||
|
tzDate: (timestamp: number): Date =>
|
||||||
|
uPlot.tzDate(new Date(timestamp * 1e3), timezone.value),
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSearch = (searchText: string): void => {
|
const handleSearch = (searchText: string): void => {
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import { themeColors } from 'constants/theme';
|
import { themeColors } from 'constants/theme';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
import { generateColor } from 'lib/uPlotLib/utils/generateColor';
|
import { generateColor } from 'lib/uPlotLib/utils/generateColor';
|
||||||
|
|
||||||
const tooltipPlugin = (
|
const tooltipPlugin = (
|
||||||
isDarkMode: boolean,
|
isDarkMode: boolean,
|
||||||
|
timezone: string,
|
||||||
): { hooks: { init: (u: any) => void } } => {
|
): { hooks: { init: (u: any) => void } } => {
|
||||||
let tooltip: HTMLDivElement;
|
let tooltip: HTMLDivElement;
|
||||||
const tooltipLeftOffset = 10;
|
const tooltipLeftOffset = 10;
|
||||||
@ -17,7 +19,7 @@ const tooltipPlugin = (
|
|||||||
return value.toFixed(3);
|
return value.toFixed(3);
|
||||||
}
|
}
|
||||||
if (value instanceof Date) {
|
if (value instanceof Date) {
|
||||||
return value.toLocaleString();
|
return dayjs(value).tz(timezone).format('MM/DD/YYYY, h:mm:ss A');
|
||||||
}
|
}
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
return 'N/A';
|
return 'N/A';
|
||||||
|
@ -6,12 +6,12 @@ import getNextPrevId from 'api/errors/getNextPrevId';
|
|||||||
import Editor from 'components/Editor';
|
import Editor from 'components/Editor';
|
||||||
import { ResizeTable } from 'components/ResizeTable';
|
import { ResizeTable } from 'components/ResizeTable';
|
||||||
import { getNanoSeconds } from 'container/AllError/utils';
|
import { getNanoSeconds } from 'container/AllError/utils';
|
||||||
import dayjs from 'dayjs';
|
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import createQueryParams from 'lib/createQueryParams';
|
import createQueryParams from 'lib/createQueryParams';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import { isUndefined } from 'lodash-es';
|
import { isUndefined } from 'lodash-es';
|
||||||
import { urlKey } from 'pages/ErrorDetails/utils';
|
import { urlKey } from 'pages/ErrorDetails/utils';
|
||||||
|
import { useTimezone } from 'providers/Timezone';
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useQuery } from 'react-query';
|
import { useQuery } from 'react-query';
|
||||||
@ -103,8 +103,6 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const timeStamp = dayjs(errorDetail.timestamp);
|
|
||||||
|
|
||||||
const data: { key: string; value: string }[] = Object.keys(errorDetail)
|
const data: { key: string; value: string }[] = Object.keys(errorDetail)
|
||||||
.filter((e) => !keyToExclude.includes(e))
|
.filter((e) => !keyToExclude.includes(e))
|
||||||
.map((key) => ({
|
.map((key) => ({
|
||||||
@ -136,6 +134,8 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
|
const { formatTimezoneAdjustedTimestamp } = useTimezone();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Typography>{errorDetail.exceptionType}</Typography>
|
<Typography>{errorDetail.exceptionType}</Typography>
|
||||||
@ -145,7 +145,12 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
|
|||||||
<EventContainer>
|
<EventContainer>
|
||||||
<div>
|
<div>
|
||||||
<Typography>Event {errorDetail.errorId}</Typography>
|
<Typography>Event {errorDetail.errorId}</Typography>
|
||||||
<Typography>{timeStamp.format('MMM DD YYYY hh:mm:ss A')}</Typography>
|
<Typography>
|
||||||
|
{formatTimezoneAdjustedTimestamp(
|
||||||
|
errorDetail.timestamp,
|
||||||
|
'DD/MM/YYYY hh:mm:ss A (UTC Z)',
|
||||||
|
)}
|
||||||
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Space align="end" direction="horizontal">
|
<Space align="end" direction="horizontal">
|
||||||
|
@ -25,6 +25,7 @@ import getTimeString from 'lib/getTimeString';
|
|||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
|
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
|
||||||
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
|
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
|
||||||
|
import { useTimezone } from 'providers/Timezone';
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
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 { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
import { EQueryType } from 'types/common/dashboard';
|
import { EQueryType } from 'types/common/dashboard';
|
||||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
|
import uPlot from 'uplot';
|
||||||
import { getGraphType } from 'utils/getGraphType';
|
import { getGraphType } from 'utils/getGraphType';
|
||||||
import { getSortedSeriesData } from 'utils/getSortedSeriesData';
|
import { getSortedSeriesData } from 'utils/getSortedSeriesData';
|
||||||
import { getTimeRange } from 'utils/getTimeRange';
|
import { getTimeRange } from 'utils/getTimeRange';
|
||||||
@ -201,6 +203,8 @@ function ChartPreview({
|
|||||||
[dispatch, location.pathname, urlQuery],
|
[dispatch, location.pathname, urlQuery],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { timezone } = useTimezone();
|
||||||
|
|
||||||
const options = useMemo(
|
const options = useMemo(
|
||||||
() =>
|
() =>
|
||||||
getUPlotChartOptions({
|
getUPlotChartOptions({
|
||||||
@ -236,6 +240,9 @@ function ChartPreview({
|
|||||||
softMax: null,
|
softMax: null,
|
||||||
softMin: null,
|
softMin: null,
|
||||||
panelType: graphType,
|
panelType: graphType,
|
||||||
|
tzDate: (timestamp: number) =>
|
||||||
|
uPlot.tzDate(new Date(timestamp * 1e3), timezone.value),
|
||||||
|
timezone: timezone.value,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
yAxisUnit,
|
yAxisUnit,
|
||||||
@ -250,6 +257,7 @@ function ChartPreview({
|
|||||||
optionName,
|
optionName,
|
||||||
alertDef?.condition.targetUnit,
|
alertDef?.condition.targetUnit,
|
||||||
graphType,
|
graphType,
|
||||||
|
timezone.value,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ import { Popover, Typography } from 'antd';
|
|||||||
import { convertTimeToRelevantUnit } from 'container/TraceDetail/utils';
|
import { convertTimeToRelevantUnit } from 'container/TraceDetail/utils';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
|
import { useTimezone } from 'providers/Timezone';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { toFixed } from 'utils/toFixed';
|
import { toFixed } from 'utils/toFixed';
|
||||||
|
|
||||||
@ -32,13 +33,17 @@ function Span(props: SpanLengthProps): JSX.Element {
|
|||||||
const isDarkMode = useIsDarkMode();
|
const isDarkMode = useIsDarkMode();
|
||||||
const { time, timeUnitName } = convertTimeToRelevantUnit(inMsCount);
|
const { time, timeUnitName } = convertTimeToRelevantUnit(inMsCount);
|
||||||
|
|
||||||
|
const { timezone } = useTimezone();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.documentElement.scrollTop = document.documentElement.clientHeight;
|
document.documentElement.scrollTop = document.documentElement.clientHeight;
|
||||||
document.documentElement.scrollLeft = document.documentElement.clientWidth;
|
document.documentElement.scrollLeft = document.documentElement.clientWidth;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const getContent = (): JSX.Element => {
|
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;
|
const startTimeInMs = startTime - globalStart;
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
@ -31,7 +31,7 @@ import { AxiosError } from 'axios';
|
|||||||
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
|
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
|
||||||
import Tags from 'components/Tags/Tags';
|
import Tags from 'components/Tags/Tags';
|
||||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||||
import dayjs, { Dayjs } from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { useGetAllIngestionsKeys } from 'hooks/IngestionKeys/useGetAllIngestionKeys';
|
import { useGetAllIngestionsKeys } from 'hooks/IngestionKeys/useGetAllIngestionKeys';
|
||||||
import useDebouncedFn from 'hooks/useDebouncedFunction';
|
import useDebouncedFn from 'hooks/useDebouncedFunction';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
@ -51,6 +51,7 @@ import {
|
|||||||
Trash2,
|
Trash2,
|
||||||
X,
|
X,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
import { useTimezone } from 'providers/Timezone';
|
||||||
import { ChangeEvent, useEffect, useState } from 'react';
|
import { ChangeEvent, useEffect, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useMutation } from 'react-query';
|
import { useMutation } from 'react-query';
|
||||||
@ -70,7 +71,10 @@ const { Option } = Select;
|
|||||||
|
|
||||||
const BYTES = 1073741824;
|
const BYTES = 1073741824;
|
||||||
|
|
||||||
export const disabledDate = (current: Dayjs): boolean =>
|
// Using any type here because antd's DatePicker expects its own internal Dayjs type
|
||||||
|
// which conflicts with our project's Dayjs type that has additional plugins (tz, utc etc).
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
|
||||||
|
export const disabledDate = (current: any): boolean =>
|
||||||
// Disable all dates before today
|
// Disable all dates before today
|
||||||
current && current < dayjs().endOf('day');
|
current && current < dayjs().endOf('day');
|
||||||
|
|
||||||
@ -393,8 +397,11 @@ function MultiIngestionSettings(): JSX.Element {
|
|||||||
|
|
||||||
const gbToBytes = (gb: number): number => Math.round(gb * 1024 ** 3);
|
const gbToBytes = (gb: number): number => Math.round(gb * 1024 ** 3);
|
||||||
|
|
||||||
const getFormattedTime = (date: string): string =>
|
const getFormattedTime = (
|
||||||
dayjs(date).format('MMM DD,YYYY, hh:mm a');
|
date: string,
|
||||||
|
formatTimezoneAdjustedTimestamp: (date: string, format: string) => string,
|
||||||
|
): string =>
|
||||||
|
formatTimezoneAdjustedTimestamp(date, 'MMM DD,YYYY, hh:mm a (UTC Z)');
|
||||||
|
|
||||||
const showDeleteLimitModal = (
|
const showDeleteLimitModal = (
|
||||||
APIKey: IngestionKeyProps,
|
APIKey: IngestionKeyProps,
|
||||||
@ -544,17 +551,27 @@ function MultiIngestionSettings(): JSX.Element {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { formatTimezoneAdjustedTimestamp } = useTimezone();
|
||||||
|
|
||||||
const columns: AntDTableProps<IngestionKeyProps>['columns'] = [
|
const columns: AntDTableProps<IngestionKeyProps>['columns'] = [
|
||||||
{
|
{
|
||||||
title: 'Ingestion Key',
|
title: 'Ingestion Key',
|
||||||
key: 'ingestion-key',
|
key: 'ingestion-key',
|
||||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
render: (APIKey: IngestionKeyProps): JSX.Element => {
|
render: (APIKey: IngestionKeyProps): JSX.Element => {
|
||||||
const createdOn = getFormattedTime(APIKey.created_at);
|
const createdOn = getFormattedTime(
|
||||||
|
APIKey.created_at,
|
||||||
|
formatTimezoneAdjustedTimestamp,
|
||||||
|
);
|
||||||
const formattedDateAndTime =
|
const formattedDateAndTime =
|
||||||
APIKey && APIKey?.expires_at && getFormattedTime(APIKey?.expires_at);
|
APIKey &&
|
||||||
|
APIKey?.expires_at &&
|
||||||
|
getFormattedTime(APIKey?.expires_at, formatTimezoneAdjustedTimestamp);
|
||||||
|
|
||||||
const updatedOn = getFormattedTime(APIKey?.updated_at);
|
const updatedOn = getFormattedTime(
|
||||||
|
APIKey?.updated_at,
|
||||||
|
formatTimezoneAdjustedTimestamp,
|
||||||
|
);
|
||||||
|
|
||||||
const limits: { [key: string]: LimitProps } = {};
|
const limits: { [key: string]: LimitProps } = {};
|
||||||
|
|
||||||
|
@ -1,8 +1,20 @@
|
|||||||
|
import { Typography } from 'antd';
|
||||||
import { ColumnsType } from 'antd/lib/table';
|
import { ColumnsType } from 'antd/lib/table';
|
||||||
import { ResizeTable } from 'components/ResizeTable';
|
import { ResizeTable } from 'components/ResizeTable';
|
||||||
|
import { useTimezone } from 'providers/Timezone';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { License } from 'types/api/licenses/def';
|
import { License } from 'types/api/licenses/def';
|
||||||
|
|
||||||
|
function ValidityColumn({ value }: { value: string }): JSX.Element {
|
||||||
|
const { formatTimezoneAdjustedTimestamp } = useTimezone();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Typography>
|
||||||
|
{formatTimezoneAdjustedTimestamp(value, 'YYYY-MM-DD HH:mm:ss (UTC Z)')}
|
||||||
|
</Typography>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function ListLicenses({ licenses }: ListLicensesProps): JSX.Element {
|
function ListLicenses({ licenses }: ListLicensesProps): JSX.Element {
|
||||||
const { t } = useTranslation(['licenses']);
|
const { t } = useTranslation(['licenses']);
|
||||||
|
|
||||||
@ -23,12 +35,14 @@ function ListLicenses({ licenses }: ListLicensesProps): JSX.Element {
|
|||||||
title: t('column_valid_from'),
|
title: t('column_valid_from'),
|
||||||
dataIndex: 'ValidFrom',
|
dataIndex: 'ValidFrom',
|
||||||
key: 'valid from',
|
key: 'valid from',
|
||||||
|
render: (value: string): JSX.Element => ValidityColumn({ value }),
|
||||||
width: 80,
|
width: 80,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t('column_valid_until'),
|
title: t('column_valid_until'),
|
||||||
dataIndex: 'ValidUntil',
|
dataIndex: 'ValidUntil',
|
||||||
key: 'valid until',
|
key: 'valid until',
|
||||||
|
render: (value: string): JSX.Element => ValidityColumn({ value }),
|
||||||
width: 80,
|
width: 80,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
@ -867,7 +867,7 @@
|
|||||||
|
|
||||||
.configure-metadata-root {
|
.configure-metadata-root {
|
||||||
.ant-modal-content {
|
.ant-modal-content {
|
||||||
width: 400px;
|
width: 500px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: 1px solid var(--Slate-500, #161922);
|
border: 1px solid var(--Slate-500, #161922);
|
||||||
@ -1039,7 +1039,6 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 336px;
|
|
||||||
padding: 0px 0px 0px 14.634px;
|
padding: 0px 0px 0px 14.634px;
|
||||||
|
|
||||||
.left {
|
.left {
|
||||||
|
@ -57,6 +57,7 @@ import {
|
|||||||
// see more: https://github.com/lucide-icons/lucide/issues/94
|
// see more: https://github.com/lucide-icons/lucide/issues/94
|
||||||
import { handleContactSupport } from 'pages/Integrations/utils';
|
import { handleContactSupport } from 'pages/Integrations/utils';
|
||||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||||
|
import { useTimezone } from 'providers/Timezone';
|
||||||
import {
|
import {
|
||||||
ChangeEvent,
|
ChangeEvent,
|
||||||
Key,
|
Key,
|
||||||
@ -343,31 +344,13 @@ function DashboardsList(): JSX.Element {
|
|||||||
}
|
}
|
||||||
}, [state.error, state.value, t, notifications]);
|
}, [state.error, state.value, t, notifications]);
|
||||||
|
|
||||||
|
const { formatTimezoneAdjustedTimestamp } = useTimezone();
|
||||||
|
|
||||||
function getFormattedTime(dashboard: Dashboard, option: string): string {
|
function getFormattedTime(dashboard: Dashboard, option: string): string {
|
||||||
const timeOptions: Intl.DateTimeFormatOptions = {
|
return formatTimezoneAdjustedTimestamp(
|
||||||
hour: '2-digit',
|
get(dashboard, option, ''),
|
||||||
minute: '2-digit',
|
'MMM D, YYYY ⎯ hh:mm:ss A (UTC Z)',
|
||||||
second: '2-digit',
|
|
||||||
hour12: false,
|
|
||||||
};
|
|
||||||
const formattedTime = new Date(get(dashboard, option, '')).toLocaleTimeString(
|
|
||||||
'en-US',
|
|
||||||
timeOptions,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const dateOptions: Intl.DateTimeFormatOptions = {
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
year: 'numeric',
|
|
||||||
};
|
|
||||||
|
|
||||||
const formattedDate = new Date(get(dashboard, option, '')).toLocaleDateString(
|
|
||||||
'en-US',
|
|
||||||
dateOptions,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Combine time and date
|
|
||||||
return `${formattedDate} ⎯ ${formattedTime}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const onLastUpdated = (time: string): string => {
|
const onLastUpdated = (time: string): string => {
|
||||||
@ -410,31 +393,11 @@ function DashboardsList(): JSX.Element {
|
|||||||
title: 'Dashboards',
|
title: 'Dashboards',
|
||||||
key: 'dashboard',
|
key: 'dashboard',
|
||||||
render: (dashboard: Data, _, index): JSX.Element => {
|
render: (dashboard: Data, _, index): JSX.Element => {
|
||||||
const timeOptions: Intl.DateTimeFormatOptions = {
|
const formattedDateAndTime = formatTimezoneAdjustedTimestamp(
|
||||||
hour: '2-digit',
|
dashboard.createdAt,
|
||||||
minute: '2-digit',
|
'MMM D, YYYY ⎯ hh:mm:ss A (UTC Z)',
|
||||||
second: '2-digit',
|
|
||||||
hour12: false,
|
|
||||||
};
|
|
||||||
const formattedTime = new Date(dashboard.createdAt).toLocaleTimeString(
|
|
||||||
'en-US',
|
|
||||||
timeOptions,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const dateOptions: Intl.DateTimeFormatOptions = {
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
year: 'numeric',
|
|
||||||
};
|
|
||||||
|
|
||||||
const formattedDate = new Date(dashboard.createdAt).toLocaleDateString(
|
|
||||||
'en-US',
|
|
||||||
dateOptions,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Combine time and date
|
|
||||||
const formattedDateAndTime = `${formattedDate} ⎯ ${formattedTime}`;
|
|
||||||
|
|
||||||
const getLink = (): string => `${ROUTES.ALL_DASHBOARD}/${dashboard.id}`;
|
const getLink = (): string => `${ROUTES.ALL_DASHBOARD}/${dashboard.id}`;
|
||||||
|
|
||||||
const onClickHandler = (event: React.MouseEvent<HTMLElement>): void => {
|
const onClickHandler = (event: React.MouseEvent<HTMLElement>): void => {
|
||||||
|
@ -8,10 +8,12 @@ import { useResizeObserver } from 'hooks/useDimensions';
|
|||||||
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
|
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
|
||||||
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
|
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
|
||||||
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
|
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
|
||||||
|
import { useTimezone } from 'providers/Timezone';
|
||||||
import { useMemo, useRef } from 'react';
|
import { useMemo, useRef } from 'react';
|
||||||
import { useQueries, UseQueryResult } from 'react-query';
|
import { useQueries, UseQueryResult } from 'react-query';
|
||||||
import { SuccessResponse } from 'types/api';
|
import { SuccessResponse } from 'types/api';
|
||||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||||
|
import uPlot from 'uplot';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getHostQueryPayload,
|
getHostQueryPayload,
|
||||||
@ -73,6 +75,8 @@ function NodeMetrics({
|
|||||||
[queries],
|
[queries],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { timezone } = useTimezone();
|
||||||
|
|
||||||
const options = useMemo(
|
const options = useMemo(
|
||||||
() =>
|
() =>
|
||||||
queries.map(({ data }, idx) =>
|
queries.map(({ data }, idx) =>
|
||||||
@ -86,6 +90,9 @@ function NodeMetrics({
|
|||||||
minTimeScale: start,
|
minTimeScale: start,
|
||||||
maxTimeScale: end,
|
maxTimeScale: end,
|
||||||
verticalLineTimestamp,
|
verticalLineTimestamp,
|
||||||
|
tzDate: (timestamp: number) =>
|
||||||
|
uPlot.tzDate(new Date(timestamp * 1e3), timezone.value),
|
||||||
|
timezone: timezone.value,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
[
|
[
|
||||||
@ -96,6 +103,7 @@ function NodeMetrics({
|
|||||||
start,
|
start,
|
||||||
verticalLineTimestamp,
|
verticalLineTimestamp,
|
||||||
end,
|
end,
|
||||||
|
timezone.value,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -8,10 +8,12 @@ import { useResizeObserver } from 'hooks/useDimensions';
|
|||||||
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
|
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
|
||||||
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
|
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
|
||||||
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
|
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
|
||||||
|
import { useTimezone } from 'providers/Timezone';
|
||||||
import { useMemo, useRef } from 'react';
|
import { useMemo, useRef } from 'react';
|
||||||
import { useQueries, UseQueryResult } from 'react-query';
|
import { useQueries, UseQueryResult } from 'react-query';
|
||||||
import { SuccessResponse } from 'types/api';
|
import { SuccessResponse } from 'types/api';
|
||||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||||
|
import uPlot from 'uplot';
|
||||||
|
|
||||||
import { getPodQueryPayload, podWidgetInfo } from './constants';
|
import { getPodQueryPayload, podWidgetInfo } from './constants';
|
||||||
|
|
||||||
@ -60,6 +62,7 @@ function PodMetrics({
|
|||||||
() => queries.map(({ data }) => getUPlotChartData(data?.payload)),
|
() => queries.map(({ data }) => getUPlotChartData(data?.payload)),
|
||||||
[queries],
|
[queries],
|
||||||
);
|
);
|
||||||
|
const { timezone } = useTimezone();
|
||||||
|
|
||||||
const options = useMemo(
|
const options = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@ -74,9 +77,20 @@ function PodMetrics({
|
|||||||
minTimeScale: start,
|
minTimeScale: start,
|
||||||
maxTimeScale: end,
|
maxTimeScale: end,
|
||||||
verticalLineTimestamp,
|
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 = (
|
const renderCardContent = (
|
||||||
|
@ -11,7 +11,8 @@ import ROUTES from 'constants/routes';
|
|||||||
import dompurify from 'dompurify';
|
import dompurify from 'dompurify';
|
||||||
import { isEmpty } from 'lodash-es';
|
import { isEmpty } from 'lodash-es';
|
||||||
import { ArrowDownToDot, ArrowUpFromDot, Ellipsis } from 'lucide-react';
|
import { ArrowDownToDot, ArrowUpFromDot, Ellipsis } from 'lucide-react';
|
||||||
import { useMemo, useState } from 'react';
|
import { useTimezone } from 'providers/Timezone';
|
||||||
|
import React, { useMemo, useState } from 'react';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
import { FORBID_DOM_PURIFY_TAGS } from 'utils/app';
|
import { FORBID_DOM_PURIFY_TAGS } from 'utils/app';
|
||||||
@ -68,6 +69,8 @@ export function TableViewActions(
|
|||||||
|
|
||||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const { formatTimezoneAdjustedTimestamp } = useTimezone();
|
||||||
|
|
||||||
if (record.field === 'body') {
|
if (record.field === 'body') {
|
||||||
const parsedBody = recursiveParseJSON(fieldData.value);
|
const parsedBody = recursiveParseJSON(fieldData.value);
|
||||||
if (!isEmpty(parsedBody)) {
|
if (!isEmpty(parsedBody)) {
|
||||||
@ -100,33 +103,44 @@ export function TableViewActions(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let cleanTimestamp: string;
|
||||||
|
if (record.field === 'timestamp') {
|
||||||
|
cleanTimestamp = fieldData.value.replace(/^["']|["']$/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderFieldContent = (): JSX.Element => {
|
||||||
|
const commonStyles: React.CSSProperties = {
|
||||||
|
color: Color.BG_SIENNA_400,
|
||||||
|
whiteSpace: 'pre-wrap',
|
||||||
|
tabSize: 4,
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (record.field) {
|
||||||
|
case 'body':
|
||||||
|
return <span style={commonStyles} dangerouslySetInnerHTML={bodyHtml} />;
|
||||||
|
|
||||||
|
case 'timestamp':
|
||||||
|
return (
|
||||||
|
<span style={commonStyles}>
|
||||||
|
{formatTimezoneAdjustedTimestamp(
|
||||||
|
cleanTimestamp,
|
||||||
|
'MM/DD/YYYY, HH:mm:ss.SSS (UTC Z)',
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return (
|
||||||
|
<span style={commonStyles}>{removeEscapeCharacters(fieldData.value)}</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cx('value-field', isOpen ? 'open-popover' : '')}>
|
<div className={cx('value-field', isOpen ? 'open-popover' : '')}>
|
||||||
{record.field === 'body' ? (
|
<CopyClipboardHOC entityKey={fieldFilterKey} textToCopy={textToCopy}>
|
||||||
<CopyClipboardHOC entityKey={fieldFilterKey} textToCopy={textToCopy}>
|
{renderFieldContent()}
|
||||||
<span
|
</CopyClipboardHOC>
|
||||||
style={{
|
|
||||||
color: Color.BG_SIENNA_400,
|
|
||||||
whiteSpace: 'pre-wrap',
|
|
||||||
tabSize: 4,
|
|
||||||
}}
|
|
||||||
dangerouslySetInnerHTML={bodyHtml}
|
|
||||||
/>
|
|
||||||
</CopyClipboardHOC>
|
|
||||||
) : (
|
|
||||||
<CopyClipboardHOC entityKey={fieldFilterKey} textToCopy={textToCopy}>
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
color: Color.BG_SIENNA_400,
|
|
||||||
whiteSpace: 'pre-wrap',
|
|
||||||
tabSize: 4,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{removeEscapeCharacters(fieldData.value)}
|
|
||||||
</span>
|
|
||||||
</CopyClipboardHOC>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!isListViewPanel && (
|
{!isListViewPanel && (
|
||||||
<span className="action-btn">
|
<span className="action-btn">
|
||||||
<Tooltip title="Filter for value">
|
<Tooltip title="Filter for value">
|
||||||
|
@ -50,6 +50,7 @@ import {
|
|||||||
} from 'lodash-es';
|
} from 'lodash-es';
|
||||||
import { Sliders } from 'lucide-react';
|
import { Sliders } from 'lucide-react';
|
||||||
import { SELECTED_VIEWS } from 'pages/LogsExplorer/utils';
|
import { SELECTED_VIEWS } from 'pages/LogsExplorer/utils';
|
||||||
|
import { useTimezone } from 'providers/Timezone';
|
||||||
import {
|
import {
|
||||||
memo,
|
memo,
|
||||||
MutableRefObject,
|
MutableRefObject,
|
||||||
@ -669,13 +670,19 @@ function LogsExplorerViews({
|
|||||||
setIsLoadingQueries,
|
setIsLoadingQueries,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const { timezone } = useTimezone();
|
||||||
|
|
||||||
const flattenLogData = useMemo(
|
const flattenLogData = useMemo(
|
||||||
() =>
|
() =>
|
||||||
logs.map((log) => {
|
logs.map((log) => {
|
||||||
const timestamp =
|
const timestamp =
|
||||||
typeof log.timestamp === 'string'
|
typeof log.timestamp === 'string'
|
||||||
? dayjs(log.timestamp).format('YYYY-MM-DD HH:mm:ss.SSS')
|
? dayjs(log.timestamp)
|
||||||
: dayjs(log.timestamp / 1e6).format('YYYY-MM-DD HH:mm:ss.SSS');
|
.tz(timezone.value)
|
||||||
|
.format('YYYY-MM-DD HH:mm:ss.SSS')
|
||||||
|
: dayjs(log.timestamp / 1e6)
|
||||||
|
.tz(timezone.value)
|
||||||
|
.format('YYYY-MM-DD HH:mm:ss.SSS');
|
||||||
|
|
||||||
return FlatLogData({
|
return FlatLogData({
|
||||||
timestamp,
|
timestamp,
|
||||||
@ -683,7 +690,7 @@ function LogsExplorerViews({
|
|||||||
...omit(log, 'timestamp', 'body'),
|
...omit(log, 'timestamp', 'body'),
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
[logs],
|
[logs, timezone.value],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -7,6 +7,7 @@ import { rest } from 'msw';
|
|||||||
import { SELECTED_VIEWS } from 'pages/LogsExplorer/utils';
|
import { SELECTED_VIEWS } from 'pages/LogsExplorer/utils';
|
||||||
import { QueryBuilderProvider } from 'providers/QueryBuilder';
|
import { QueryBuilderProvider } from 'providers/QueryBuilder';
|
||||||
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
|
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
|
||||||
|
import TimezoneProvider from 'providers/Timezone';
|
||||||
import { I18nextProvider } from 'react-i18next';
|
import { I18nextProvider } from 'react-i18next';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
@ -91,17 +92,19 @@ const renderer = (): RenderResult =>
|
|||||||
<I18nextProvider i18n={i18n}>
|
<I18nextProvider i18n={i18n}>
|
||||||
<MockQueryClientProvider>
|
<MockQueryClientProvider>
|
||||||
<QueryBuilderProvider>
|
<QueryBuilderProvider>
|
||||||
<VirtuosoMockContext.Provider
|
<TimezoneProvider>
|
||||||
value={{ viewportHeight: 300, itemHeight: 100 }}
|
<VirtuosoMockContext.Provider
|
||||||
>
|
value={{ viewportHeight: 300, itemHeight: 100 }}
|
||||||
<LogsExplorerViews
|
>
|
||||||
selectedView={SELECTED_VIEWS.SEARCH}
|
<LogsExplorerViews
|
||||||
showFrequencyChart
|
selectedView={SELECTED_VIEWS.SEARCH}
|
||||||
setIsLoadingQueries={(): void => {}}
|
showFrequencyChart
|
||||||
listQueryKeyRef={{ current: {} }}
|
setIsLoadingQueries={(): void => {}}
|
||||||
chartQueryKeyRef={{ current: {} }}
|
listQueryKeyRef={{ current: {} }}
|
||||||
/>
|
chartQueryKeyRef={{ current: {} }}
|
||||||
</VirtuosoMockContext.Provider>
|
/>
|
||||||
|
</VirtuosoMockContext.Provider>
|
||||||
|
</TimezoneProvider>
|
||||||
</QueryBuilderProvider>
|
</QueryBuilderProvider>
|
||||||
</MockQueryClientProvider>
|
</MockQueryClientProvider>
|
||||||
</I18nextProvider>
|
</I18nextProvider>
|
||||||
|
@ -15,6 +15,7 @@ import { useLogsData } from 'hooks/useLogsData';
|
|||||||
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||||
import { FlatLogData } from 'lib/logs/flatLogData';
|
import { FlatLogData } from 'lib/logs/flatLogData';
|
||||||
import { RowData } from 'lib/query/createTableColumnsFromQuery';
|
import { RowData } from 'lib/query/createTableColumnsFromQuery';
|
||||||
|
import { useTimezone } from 'providers/Timezone';
|
||||||
import {
|
import {
|
||||||
Dispatch,
|
Dispatch,
|
||||||
HTMLAttributes,
|
HTMLAttributes,
|
||||||
@ -76,7 +77,12 @@ function LogsPanelComponent({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const columns = getLogPanelColumnsList(widget.selectedLogFields);
|
const { formatTimezoneAdjustedTimestamp } = useTimezone();
|
||||||
|
|
||||||
|
const columns = getLogPanelColumnsList(
|
||||||
|
widget.selectedLogFields,
|
||||||
|
formatTimezoneAdjustedTimestamp,
|
||||||
|
);
|
||||||
|
|
||||||
const dataLength =
|
const dataLength =
|
||||||
queryResponse.data?.payload?.data?.newResult?.data?.result[0]?.list?.length;
|
queryResponse.data?.payload?.data?.newResult?.data?.result[0]?.list?.length;
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { ColumnsType } from 'antd/es/table';
|
import { ColumnsType } from 'antd/es/table';
|
||||||
import { Typography } from 'antd/lib';
|
import { Typography } from 'antd/lib';
|
||||||
import { OPERATORS } from 'constants/queryBuilder';
|
import { OPERATORS } from 'constants/queryBuilder';
|
||||||
|
import { TimestampInput } from 'hooks/useTimezoneFormatter/useTimezoneFormatter';
|
||||||
// import Typography from 'antd/es/typography/Typography';
|
// import Typography from 'antd/es/typography/Typography';
|
||||||
import { RowData } from 'lib/query/createTableColumnsFromQuery';
|
import { RowData } from 'lib/query/createTableColumnsFromQuery';
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
@ -13,18 +14,31 @@ import { v4 as uuid } from 'uuid';
|
|||||||
|
|
||||||
export const getLogPanelColumnsList = (
|
export const getLogPanelColumnsList = (
|
||||||
selectedLogFields: Widgets['selectedLogFields'],
|
selectedLogFields: Widgets['selectedLogFields'],
|
||||||
|
formatTimezoneAdjustedTimestamp: (
|
||||||
|
input: TimestampInput,
|
||||||
|
format?: string,
|
||||||
|
) => string,
|
||||||
): ColumnsType<RowData> => {
|
): ColumnsType<RowData> => {
|
||||||
const initialColumns: ColumnsType<RowData> = [];
|
const initialColumns: ColumnsType<RowData> = [];
|
||||||
|
|
||||||
const columns: ColumnsType<RowData> =
|
const columns: ColumnsType<RowData> =
|
||||||
selectedLogFields?.map((field: IField) => {
|
selectedLogFields?.map((field: IField) => {
|
||||||
const { name } = field;
|
const { name } = field;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: name,
|
title: name,
|
||||||
dataIndex: name,
|
dataIndex: name,
|
||||||
key: name,
|
key: name,
|
||||||
width: name === 'body' ? 350 : 100,
|
width: name === 'body' ? 350 : 100,
|
||||||
render: (value: ReactNode): JSX.Element => {
|
render: (value: ReactNode): JSX.Element => {
|
||||||
|
if (name === 'timestamp') {
|
||||||
|
return (
|
||||||
|
<Typography.Text>
|
||||||
|
{formatTimezoneAdjustedTimestamp(value as string)}
|
||||||
|
</Typography.Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (name === 'body') {
|
if (name === 'body') {
|
||||||
return (
|
return (
|
||||||
<Typography.Paragraph ellipsis={{ rows: 1 }} data-testid={name}>
|
<Typography.Paragraph ellipsis={{ rows: 1 }} data-testid={name}>
|
||||||
|
@ -0,0 +1,96 @@
|
|||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.timezone-adaption {
|
||||||
|
background-color: var(--bg-vanilla-100);
|
||||||
|
border-color: var(--bg-vanilla-300);
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
color: var(--text-ink-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__description {
|
||||||
|
color: var(--text-ink-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,82 @@
|
|||||||
|
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 } from 'react';
|
||||||
|
|
||||||
|
function TimezoneAdaptation(): JSX.Element {
|
||||||
|
const {
|
||||||
|
timezone,
|
||||||
|
browserTimezone,
|
||||||
|
updateTimezone,
|
||||||
|
isAdaptationEnabled,
|
||||||
|
setIsAdaptationEnabled,
|
||||||
|
} = useTimezone();
|
||||||
|
|
||||||
|
const isTimezoneOverridden = useMemo(
|
||||||
|
() => timezone.offset !== browserTimezone.offset,
|
||||||
|
[timezone, browserTimezone],
|
||||||
|
);
|
||||||
|
|
||||||
|
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 { useState } from 'react';
|
||||||
|
|
||||||
import Password from './Password';
|
import Password from './Password';
|
||||||
|
import TimezoneAdaptation from './TimezoneAdaptation/TimezoneAdaptation';
|
||||||
import UserInfo from './UserInfo';
|
import UserInfo from './UserInfo';
|
||||||
|
|
||||||
function MySettings(): JSX.Element {
|
function MySettings(): JSX.Element {
|
||||||
@ -78,6 +79,8 @@ function MySettings(): JSX.Element {
|
|||||||
<Password />
|
<Password />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<TimezoneAdaptation />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
className="flexBtn"
|
className="flexBtn"
|
||||||
onClick={(): void => Logout()}
|
onClick={(): void => Logout()}
|
||||||
|
@ -14,7 +14,9 @@ import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
|
|||||||
import { cloneDeep, isEqual, isUndefined } from 'lodash-es';
|
import { cloneDeep, isEqual, isUndefined } from 'lodash-es';
|
||||||
import _noop from 'lodash-es/noop';
|
import _noop from 'lodash-es/noop';
|
||||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||||
|
import { useTimezone } from 'providers/Timezone';
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import uPlot from 'uplot';
|
||||||
import { getSortedSeriesData } from 'utils/getSortedSeriesData';
|
import { getSortedSeriesData } from 'utils/getSortedSeriesData';
|
||||||
import { getTimeRange } from 'utils/getTimeRange';
|
import { getTimeRange } from 'utils/getTimeRange';
|
||||||
|
|
||||||
@ -105,6 +107,8 @@ function UplotPanelWrapper({
|
|||||||
}
|
}
|
||||||
}, [graphVisibility, hiddenGraph, widget.panelTypes, widget?.stackedBarChart]);
|
}, [graphVisibility, hiddenGraph, widget.panelTypes, widget?.stackedBarChart]);
|
||||||
|
|
||||||
|
const { timezone } = useTimezone();
|
||||||
|
|
||||||
const options = useMemo(
|
const options = useMemo(
|
||||||
() =>
|
() =>
|
||||||
getUPlotChartOptions({
|
getUPlotChartOptions({
|
||||||
@ -128,6 +132,9 @@ function UplotPanelWrapper({
|
|||||||
hiddenGraph,
|
hiddenGraph,
|
||||||
setHiddenGraph,
|
setHiddenGraph,
|
||||||
customTooltipElement,
|
customTooltipElement,
|
||||||
|
tzDate: (timestamp: number) =>
|
||||||
|
uPlot.tzDate(new Date(timestamp * 1e3), timezone.value),
|
||||||
|
timezone: timezone.value,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
widget?.id,
|
widget?.id,
|
||||||
@ -150,6 +157,7 @@ function UplotPanelWrapper({
|
|||||||
currentQuery,
|
currentQuery,
|
||||||
hiddenGraph,
|
hiddenGraph,
|
||||||
customTooltipElement,
|
customTooltipElement,
|
||||||
|
timezone.value,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1,8 +1,14 @@
|
|||||||
import dayjs from 'dayjs';
|
import { useTimezone } from 'providers/Timezone';
|
||||||
|
|
||||||
function DeploymentTime(deployTime: string): JSX.Element {
|
function DeploymentTime(deployTime: string): JSX.Element {
|
||||||
|
const { formatTimezoneAdjustedTimestamp } = useTimezone();
|
||||||
return (
|
return (
|
||||||
<span>{dayjs(deployTime).locale('en').format('MMMM DD, YYYY hh:mm A')}</span>
|
<span>
|
||||||
|
{formatTimezoneAdjustedTimestamp(
|
||||||
|
deployTime,
|
||||||
|
'MMMM DD, YYYY hh:mm A (UTC Z)',
|
||||||
|
)}{' '}
|
||||||
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { render } from '@testing-library/react';
|
import { render } from '@testing-library/react';
|
||||||
|
import TimezoneProvider from 'providers/Timezone';
|
||||||
import { I18nextProvider } from 'react-i18next';
|
import { I18nextProvider } from 'react-i18next';
|
||||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
@ -38,7 +39,9 @@ describe('ChangeHistory test', () => {
|
|||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<I18nextProvider i18n={i18n}>
|
<I18nextProvider i18n={i18n}>
|
||||||
<ChangeHistory pipelineData={pipelineData} />
|
<TimezoneProvider>
|
||||||
|
<ChangeHistory pipelineData={pipelineData} />
|
||||||
|
</TimezoneProvider>
|
||||||
</I18nextProvider>
|
</I18nextProvider>
|
||||||
</Provider>
|
</Provider>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
@ -65,12 +68,14 @@ describe('ChangeHistory test', () => {
|
|||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<I18nextProvider i18n={i18n}>
|
<I18nextProvider i18n={i18n}>
|
||||||
<ChangeHistory
|
<TimezoneProvider>
|
||||||
pipelineData={{
|
<ChangeHistory
|
||||||
...pipelineData,
|
pipelineData={{
|
||||||
history: pipelineDataHistory,
|
...pipelineData,
|
||||||
}}
|
history: pipelineDataHistory,
|
||||||
/>
|
}}
|
||||||
|
/>
|
||||||
|
</TimezoneProvider>
|
||||||
</I18nextProvider>
|
</I18nextProvider>
|
||||||
</Provider>
|
</Provider>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
|
@ -3,8 +3,8 @@ import './styles.scss';
|
|||||||
import { ExpandAltOutlined } from '@ant-design/icons';
|
import { ExpandAltOutlined } from '@ant-design/icons';
|
||||||
import LogDetail from 'components/LogDetail';
|
import LogDetail from 'components/LogDetail';
|
||||||
import { VIEW_TYPES } from 'components/LogDetail/constants';
|
import { VIEW_TYPES } from 'components/LogDetail/constants';
|
||||||
import dayjs from 'dayjs';
|
|
||||||
import { useActiveLog } from 'hooks/logs/useActiveLog';
|
import { useActiveLog } from 'hooks/logs/useActiveLog';
|
||||||
|
import { useTimezone } from 'providers/Timezone';
|
||||||
import { ILog } from 'types/api/logs/log';
|
import { ILog } from 'types/api/logs/log';
|
||||||
|
|
||||||
function LogsList({ logs }: LogsListProps): JSX.Element {
|
function LogsList({ logs }: LogsListProps): JSX.Element {
|
||||||
@ -18,12 +18,17 @@ function LogsList({ logs }: LogsListProps): JSX.Element {
|
|||||||
|
|
||||||
const makeLogDetailsHandler = (log: ILog) => (): void => onSetActiveLog(log);
|
const makeLogDetailsHandler = (log: ILog) => (): void => onSetActiveLog(log);
|
||||||
|
|
||||||
|
const { formatTimezoneAdjustedTimestamp } = useTimezone();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="logs-preview-list-container">
|
<div className="logs-preview-list-container">
|
||||||
{logs.map((log) => (
|
{logs.map((log) => (
|
||||||
<div key={log.id} className="logs-preview-list-item">
|
<div key={log.id} className="logs-preview-list-item">
|
||||||
<div className="logs-preview-list-item-timestamp">
|
<div className="logs-preview-list-item-timestamp">
|
||||||
{dayjs(log.timestamp).format('MMM DD HH:mm:ss.SSS')}
|
{formatTimezoneAdjustedTimestamp(
|
||||||
|
log.timestamp,
|
||||||
|
'MMM DD HH:mm:ss.SSS (UTC Z)',
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="logs-preview-list-item-body">{log.body}</div>
|
<div className="logs-preview-list-item-body">{log.body}</div>
|
||||||
<div
|
<div
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import dayjs from 'dayjs';
|
import { useTimezone } from 'providers/Timezone';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { PipelineData, ProcessorData } from 'types/api/pipeline/def';
|
import { PipelineData, ProcessorData } from 'types/api/pipeline/def';
|
||||||
|
|
||||||
@ -6,13 +6,18 @@ import { PipelineIndexIcon } from '../AddNewProcessor/styles';
|
|||||||
import { ColumnDataStyle, ListDataStyle, ProcessorIndexIcon } from '../styles';
|
import { ColumnDataStyle, ListDataStyle, ProcessorIndexIcon } from '../styles';
|
||||||
import PipelineFilterSummary from './PipelineFilterSummary';
|
import PipelineFilterSummary from './PipelineFilterSummary';
|
||||||
|
|
||||||
|
function CreatedAtComponent({ record }: { record: Record }): JSX.Element {
|
||||||
|
const { formatTimezoneAdjustedTimestamp } = useTimezone();
|
||||||
|
return (
|
||||||
|
<ColumnDataStyle>
|
||||||
|
{formatTimezoneAdjustedTimestamp(record, 'MMMM DD, YYYY hh:mm A (UTC Z)')}
|
||||||
|
</ColumnDataStyle>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const componentMap: ComponentMap = {
|
const componentMap: ComponentMap = {
|
||||||
orderId: ({ record }) => <PipelineIndexIcon>{record}</PipelineIndexIcon>,
|
orderId: ({ record }) => <PipelineIndexIcon>{record}</PipelineIndexIcon>,
|
||||||
createdAt: ({ record }) => (
|
createdAt: ({ record }) => <CreatedAtComponent record={record} />,
|
||||||
<ColumnDataStyle>
|
|
||||||
{dayjs(record).locale('en').format('MMMM DD, YYYY hh:mm A')}
|
|
||||||
</ColumnDataStyle>
|
|
||||||
),
|
|
||||||
id: ({ record }) => <ProcessorIndexIcon>{record}</ProcessorIndexIcon>,
|
id: ({ record }) => <ProcessorIndexIcon>{record}</ProcessorIndexIcon>,
|
||||||
name: ({ record }) => <ListDataStyle>{record}</ListDataStyle>,
|
name: ({ record }) => <ListDataStyle>{record}</ListDataStyle>,
|
||||||
filter: ({ record }) => <PipelineFilterSummary filter={record} />,
|
filter: ({ record }) => <PipelineFilterSummary filter={record} />,
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
/* eslint-disable sonarjs/no-duplicate-string */
|
/* eslint-disable sonarjs/no-duplicate-string */
|
||||||
import { findByText, render, waitFor } from '@testing-library/react';
|
import { findByText, render, waitFor } from '@testing-library/react';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import TimezoneProvider from 'providers/Timezone';
|
||||||
import { I18nextProvider } from 'react-i18next';
|
import { I18nextProvider } from 'react-i18next';
|
||||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
@ -70,14 +71,16 @@ describe('PipelinePage container test', () => {
|
|||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<I18nextProvider i18n={i18n}>
|
<I18nextProvider i18n={i18n}>
|
||||||
<PipelineListsView
|
<TimezoneProvider>
|
||||||
setActionType={jest.fn()}
|
<PipelineListsView
|
||||||
isActionMode="viewing-mode"
|
setActionType={jest.fn()}
|
||||||
setActionMode={jest.fn()}
|
isActionMode="viewing-mode"
|
||||||
pipelineData={pipelineApiResponseMockData}
|
setActionMode={jest.fn()}
|
||||||
isActionType=""
|
pipelineData={pipelineApiResponseMockData}
|
||||||
refetchPipelineLists={jest.fn()}
|
isActionType=""
|
||||||
/>
|
refetchPipelineLists={jest.fn()}
|
||||||
|
/>
|
||||||
|
</TimezoneProvider>
|
||||||
</I18nextProvider>
|
</I18nextProvider>
|
||||||
</Provider>
|
</Provider>
|
||||||
</MemoryRouter>,
|
</MemoryRouter>,
|
||||||
@ -107,14 +110,16 @@ describe('PipelinePage container test', () => {
|
|||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<I18nextProvider i18n={i18n}>
|
<I18nextProvider i18n={i18n}>
|
||||||
<PipelineListsView
|
<TimezoneProvider>
|
||||||
setActionType={jest.fn()}
|
<PipelineListsView
|
||||||
isActionMode="editing-mode"
|
setActionType={jest.fn()}
|
||||||
setActionMode={jest.fn()}
|
isActionMode="editing-mode"
|
||||||
pipelineData={pipelineApiResponseMockData}
|
setActionMode={jest.fn()}
|
||||||
isActionType=""
|
pipelineData={pipelineApiResponseMockData}
|
||||||
refetchPipelineLists={jest.fn()}
|
isActionType=""
|
||||||
/>
|
refetchPipelineLists={jest.fn()}
|
||||||
|
/>
|
||||||
|
</TimezoneProvider>
|
||||||
</I18nextProvider>
|
</I18nextProvider>
|
||||||
</Provider>
|
</Provider>
|
||||||
</MemoryRouter>,
|
</MemoryRouter>,
|
||||||
@ -144,14 +149,16 @@ describe('PipelinePage container test', () => {
|
|||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<I18nextProvider i18n={i18n}>
|
<I18nextProvider i18n={i18n}>
|
||||||
<PipelineListsView
|
<TimezoneProvider>
|
||||||
setActionType={jest.fn()}
|
<PipelineListsView
|
||||||
isActionMode="editing-mode"
|
setActionType={jest.fn()}
|
||||||
setActionMode={jest.fn()}
|
isActionMode="editing-mode"
|
||||||
pipelineData={pipelineApiResponseMockData}
|
setActionMode={jest.fn()}
|
||||||
isActionType=""
|
pipelineData={pipelineApiResponseMockData}
|
||||||
refetchPipelineLists={jest.fn()}
|
isActionType=""
|
||||||
/>
|
refetchPipelineLists={jest.fn()}
|
||||||
|
/>
|
||||||
|
</TimezoneProvider>
|
||||||
</I18nextProvider>
|
</I18nextProvider>
|
||||||
</Provider>
|
</Provider>
|
||||||
</MemoryRouter>,
|
</MemoryRouter>,
|
||||||
@ -209,14 +216,16 @@ describe('PipelinePage container test', () => {
|
|||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<I18nextProvider i18n={i18n}>
|
<I18nextProvider i18n={i18n}>
|
||||||
<PipelineListsView
|
<TimezoneProvider>
|
||||||
setActionType={jest.fn()}
|
<PipelineListsView
|
||||||
isActionMode="editing-mode"
|
setActionType={jest.fn()}
|
||||||
setActionMode={jest.fn()}
|
isActionMode="editing-mode"
|
||||||
pipelineData={pipelineApiResponseMockData}
|
setActionMode={jest.fn()}
|
||||||
isActionType=""
|
pipelineData={pipelineApiResponseMockData}
|
||||||
refetchPipelineLists={jest.fn()}
|
isActionType=""
|
||||||
/>
|
refetchPipelineLists={jest.fn()}
|
||||||
|
/>
|
||||||
|
</TimezoneProvider>
|
||||||
</I18nextProvider>
|
</I18nextProvider>
|
||||||
</Provider>
|
</Provider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
|
@ -17,6 +17,7 @@ import history from 'lib/history';
|
|||||||
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
|
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
|
||||||
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
|
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
|
||||||
import { isEmpty } from 'lodash-es';
|
import { isEmpty } from 'lodash-es';
|
||||||
|
import { useTimezone } from 'providers/Timezone';
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
@ -26,6 +27,7 @@ import { SuccessResponse } from 'types/api';
|
|||||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||||
import { DataSource } from 'types/common/queryBuilder';
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
|
import uPlot from 'uplot';
|
||||||
import { getTimeRange } from 'utils/getTimeRange';
|
import { getTimeRange } from 'utils/getTimeRange';
|
||||||
|
|
||||||
import { Container } from './styles';
|
import { Container } from './styles';
|
||||||
@ -118,6 +120,8 @@ function TimeSeriesView({
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const { timezone } = useTimezone();
|
||||||
|
|
||||||
const chartOptions = getUPlotChartOptions({
|
const chartOptions = getUPlotChartOptions({
|
||||||
onDragSelect,
|
onDragSelect,
|
||||||
yAxisUnit: yAxisUnit || '',
|
yAxisUnit: yAxisUnit || '',
|
||||||
@ -131,6 +135,9 @@ function TimeSeriesView({
|
|||||||
maxTimeScale,
|
maxTimeScale,
|
||||||
softMax: null,
|
softMax: null,
|
||||||
softMin: null,
|
softMin: null,
|
||||||
|
tzDate: (timestamp: number) =>
|
||||||
|
uPlot.tzDate(new Date(timestamp * 1e3), timezone.value),
|
||||||
|
timezone: timezone.value,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -19,7 +19,10 @@ function CustomDateTimeModal({
|
|||||||
setDateTime(date_time);
|
setDateTime(date_time);
|
||||||
};
|
};
|
||||||
|
|
||||||
const disabledDate = (current: Dayjs): boolean => {
|
// Using any type here because antd's DatePicker expects its own internal Dayjs type
|
||||||
|
// which conflicts with our project's Dayjs type that has additional plugins (tz, utc etc).
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
|
||||||
|
const disabledDate = (current: any): boolean => {
|
||||||
const currentDay = dayjs(current);
|
const currentDay = dayjs(current);
|
||||||
return currentDay.isAfter(dayjs());
|
return currentDay.isAfter(dayjs());
|
||||||
};
|
};
|
||||||
|
@ -28,6 +28,7 @@ import getTimeString from 'lib/getTimeString';
|
|||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import { isObject } from 'lodash-es';
|
import { isObject } from 'lodash-es';
|
||||||
import { Check, Copy, Info, Send, Undo } from 'lucide-react';
|
import { Check, Copy, Info, Send, Undo } from 'lucide-react';
|
||||||
|
import { useTimezone } from 'providers/Timezone';
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useQueryClient } from 'react-query';
|
import { useQueryClient } from 'react-query';
|
||||||
import { connect, useSelector } from 'react-redux';
|
import { connect, useSelector } from 'react-redux';
|
||||||
@ -660,6 +661,8 @@ function DateTimeSelection({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { timezone } = useTimezone();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="date-time-selector">
|
<div className="date-time-selector">
|
||||||
{showResetButton && selectedTime !== defaultRelativeTime && (
|
{showResetButton && selectedTime !== defaultRelativeTime && (
|
||||||
@ -713,8 +716,12 @@ function DateTimeSelection({
|
|||||||
setIsValidteRelativeTime(isValid);
|
setIsValidteRelativeTime(isValid);
|
||||||
}}
|
}}
|
||||||
selectedValue={getInputLabel(
|
selectedValue={getInputLabel(
|
||||||
dayjs(isModalTimeSelection ? modalStartTime : minTime / 1000000),
|
dayjs(isModalTimeSelection ? modalStartTime : minTime / 1000000).tz(
|
||||||
dayjs(isModalTimeSelection ? modalEndTime : maxTime / 1000000),
|
timezone.value,
|
||||||
|
),
|
||||||
|
dayjs(isModalTimeSelection ? modalEndTime : maxTime / 1000000).tz(
|
||||||
|
timezone.value,
|
||||||
|
),
|
||||||
isModalTimeSelection ? modalSelectedInterval : selectedTime,
|
isModalTimeSelection ? modalSelectedInterval : selectedTime,
|
||||||
)}
|
)}
|
||||||
data-testid="dropDown"
|
data-testid="dropDown"
|
||||||
|
@ -24,6 +24,7 @@ import history from 'lib/history';
|
|||||||
import { map } from 'lodash-es';
|
import { map } from 'lodash-es';
|
||||||
import { PanelRight } from 'lucide-react';
|
import { PanelRight } from 'lucide-react';
|
||||||
import { SPAN_DETAILS_LEFT_COL_WIDTH } from 'pages/TraceDetail/constants';
|
import { SPAN_DETAILS_LEFT_COL_WIDTH } from 'pages/TraceDetail/constants';
|
||||||
|
import { useTimezone } from 'providers/Timezone';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { ITraceForest, PayloadProps } from 'types/api/trace/getTraceItem';
|
import { ITraceForest, PayloadProps } from 'types/api/trace/getTraceItem';
|
||||||
import { getSpanTreeMetadata } from 'utils/getSpanTreeMetadata';
|
import { getSpanTreeMetadata } from 'utils/getSpanTreeMetadata';
|
||||||
@ -139,6 +140,8 @@ function TraceDetail({ response }: TraceDetailProps): JSX.Element {
|
|||||||
|
|
||||||
const isDarkMode = useIsDarkMode();
|
const isDarkMode = useIsDarkMode();
|
||||||
|
|
||||||
|
const { timezone } = useTimezone();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledRow styledclass={[Flex({ flex: 1 })]}>
|
<StyledRow styledclass={[Flex({ flex: 1 })]}>
|
||||||
<StyledCol flex="auto" styledclass={styles.leftContainer}>
|
<StyledCol flex="auto" styledclass={styles.leftContainer}>
|
||||||
@ -195,7 +198,9 @@ function TraceDetail({ response }: TraceDetailProps): JSX.Element {
|
|||||||
{isGlobalTimeVisible && (
|
{isGlobalTimeVisible && (
|
||||||
<styles.TimeStampContainer flex={`${SPAN_DETAILS_LEFT_COL_WIDTH}px`}>
|
<styles.TimeStampContainer flex={`${SPAN_DETAILS_LEFT_COL_WIDTH}px`}>
|
||||||
<Typography>
|
<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>
|
</Typography>
|
||||||
</styles.TimeStampContainer>
|
</styles.TimeStampContainer>
|
||||||
)}
|
)}
|
||||||
|
@ -15,6 +15,7 @@ import useDragColumns from 'hooks/useDragColumns';
|
|||||||
import { getDraggedColumns } from 'hooks/useDragColumns/utils';
|
import { getDraggedColumns } from 'hooks/useDragColumns/utils';
|
||||||
import useUrlQueryData from 'hooks/useUrlQueryData';
|
import useUrlQueryData from 'hooks/useUrlQueryData';
|
||||||
import { RowData } from 'lib/query/createTableColumnsFromQuery';
|
import { RowData } from 'lib/query/createTableColumnsFromQuery';
|
||||||
|
import { useTimezone } from 'providers/Timezone';
|
||||||
import { memo, useCallback, useMemo } from 'react';
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
@ -97,10 +98,15 @@ function ListView({ isFilterApplied }: ListViewProps): JSX.Element {
|
|||||||
queryTableDataResult,
|
queryTableDataResult,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const { formatTimezoneAdjustedTimestamp } = useTimezone();
|
||||||
|
|
||||||
const columns = useMemo(() => {
|
const columns = useMemo(() => {
|
||||||
const updatedColumns = getListColumns(options?.selectColumns || []);
|
const updatedColumns = getListColumns(
|
||||||
|
options?.selectColumns || [],
|
||||||
|
formatTimezoneAdjustedTimestamp,
|
||||||
|
);
|
||||||
return getDraggedColumns(updatedColumns, draggedColumns);
|
return getDraggedColumns(updatedColumns, draggedColumns);
|
||||||
}, [options?.selectColumns, draggedColumns]);
|
}, [options?.selectColumns, formatTimezoneAdjustedTimestamp, draggedColumns]);
|
||||||
|
|
||||||
const transformedQueryTableData = useMemo(
|
const transformedQueryTableData = useMemo(
|
||||||
() => transformDataWithDate(queryTableData) || [],
|
() => transformDataWithDate(queryTableData) || [],
|
||||||
|
@ -3,7 +3,7 @@ import { ColumnsType } from 'antd/es/table';
|
|||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import { getMs } from 'container/Trace/Filters/Panel/PanelBody/Duration/util';
|
import { getMs } from 'container/Trace/Filters/Panel/PanelBody/Duration/util';
|
||||||
import { formUrlParams } from 'container/TraceDetail/utils';
|
import { formUrlParams } from 'container/TraceDetail/utils';
|
||||||
import dayjs from 'dayjs';
|
import { TimestampInput } from 'hooks/useTimezoneFormatter/useTimezoneFormatter';
|
||||||
import { RowData } from 'lib/query/createTableColumnsFromQuery';
|
import { RowData } from 'lib/query/createTableColumnsFromQuery';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { ILog } from 'types/api/logs/log';
|
import { ILog } from 'types/api/logs/log';
|
||||||
@ -46,6 +46,10 @@ export const getTraceLink = (record: RowData): string =>
|
|||||||
|
|
||||||
export const getListColumns = (
|
export const getListColumns = (
|
||||||
selectedColumns: BaseAutocompleteData[],
|
selectedColumns: BaseAutocompleteData[],
|
||||||
|
formatTimezoneAdjustedTimestamp: (
|
||||||
|
input: TimestampInput,
|
||||||
|
format?: string,
|
||||||
|
) => string | number,
|
||||||
): ColumnsType<RowData> => {
|
): ColumnsType<RowData> => {
|
||||||
const initialColumns: ColumnsType<RowData> = [
|
const initialColumns: ColumnsType<RowData> = [
|
||||||
{
|
{
|
||||||
@ -56,8 +60,8 @@ export const getListColumns = (
|
|||||||
render: (value, item): JSX.Element => {
|
render: (value, item): JSX.Element => {
|
||||||
const date =
|
const date =
|
||||||
typeof value === 'string'
|
typeof value === 'string'
|
||||||
? dayjs(value).format('YYYY-MM-DD HH:mm:ss.SSS')
|
? formatTimezoneAdjustedTimestamp(value, 'YYYY-MM-DD HH:mm:ss.SSS')
|
||||||
: dayjs(value / 1e6).format('YYYY-MM-DD HH:mm:ss.SSS');
|
: formatTimezoneAdjustedTimestamp(value / 1e6, 'YYYY-MM-DD HH:mm:ss.SSS');
|
||||||
return (
|
return (
|
||||||
<BlockLink to={getTraceLink(item)} openInNewTab={false}>
|
<BlockLink to={getTraceLink(item)} openInNewTab={false}>
|
||||||
<Typography.Text>{date}</Typography.Text>
|
<Typography.Text>{date}</Typography.Text>
|
||||||
|
@ -15,6 +15,7 @@ import { Pagination } from 'hooks/queryPagination';
|
|||||||
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import { RowData } from 'lib/query/createTableColumnsFromQuery';
|
import { RowData } from 'lib/query/createTableColumnsFromQuery';
|
||||||
|
import { useTimezone } from 'providers/Timezone';
|
||||||
import {
|
import {
|
||||||
Dispatch,
|
Dispatch,
|
||||||
HTMLAttributes,
|
HTMLAttributes,
|
||||||
@ -49,7 +50,12 @@ function TracesTableComponent({
|
|||||||
}));
|
}));
|
||||||
}, [pagination, setRequestData]);
|
}, [pagination, setRequestData]);
|
||||||
|
|
||||||
const columns = getListColumns(widget.selectedTracesFields || []);
|
const { formatTimezoneAdjustedTimestamp } = useTimezone();
|
||||||
|
|
||||||
|
const columns = getListColumns(
|
||||||
|
widget.selectedTracesFields || [],
|
||||||
|
formatTimezoneAdjustedTimestamp,
|
||||||
|
);
|
||||||
|
|
||||||
const dataLength =
|
const dataLength =
|
||||||
queryResponse.data?.payload?.data?.newResult?.data?.result[0]?.list?.length;
|
queryResponse.data?.payload?.data?.newResult?.data?.result[0]?.list?.length;
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import { Tag, Typography } from 'antd';
|
import { Tag, Typography } from 'antd';
|
||||||
import convertDateToAmAndPm from 'lib/convertDateToAmAndPm';
|
import { useTimezone } from 'providers/Timezone';
|
||||||
import getFormattedDate from 'lib/getFormatedDate';
|
|
||||||
import { Alerts } from 'types/api/alerts/getTriggered';
|
import { Alerts } from 'types/api/alerts/getTriggered';
|
||||||
|
|
||||||
import Status from '../TableComponents/AlertStatus';
|
import Status from '../TableComponents/AlertStatus';
|
||||||
import { TableCell, TableRow } from './styles';
|
import { TableCell, TableRow } from './styles';
|
||||||
|
|
||||||
function ExapandableRow({ allAlerts }: ExapandableRowProps): JSX.Element {
|
function ExapandableRow({ allAlerts }: ExapandableRowProps): JSX.Element {
|
||||||
|
const { formatTimezoneAdjustedTimestamp } = useTimezone();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{allAlerts.map((alert) => {
|
{allAlerts.map((alert) => {
|
||||||
@ -40,8 +40,9 @@ function ExapandableRow({ allAlerts }: ExapandableRowProps): JSX.Element {
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Typography>{`${getFormattedDate(formatedDate)} ${convertDateToAmAndPm(
|
<Typography>{`${formatTimezoneAdjustedTimestamp(
|
||||||
formatedDate,
|
formatedDate,
|
||||||
|
'MM/DD/YYYY hh:mm:ss A (UTC Z)',
|
||||||
)}`}</Typography>
|
)}`}</Typography>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
|
@ -4,8 +4,7 @@ import { ColumnsType } from 'antd/lib/table';
|
|||||||
import { ResizeTable } from 'components/ResizeTable';
|
import { ResizeTable } from 'components/ResizeTable';
|
||||||
import LabelColumn from 'components/TableRenderer/LabelColumn';
|
import LabelColumn from 'components/TableRenderer/LabelColumn';
|
||||||
import AlertStatus from 'container/TriggeredAlerts/TableComponents/AlertStatus';
|
import AlertStatus from 'container/TriggeredAlerts/TableComponents/AlertStatus';
|
||||||
import convertDateToAmAndPm from 'lib/convertDateToAmAndPm';
|
import { useTimezone } from 'providers/Timezone';
|
||||||
import getFormattedDate from 'lib/getFormatedDate';
|
|
||||||
import { Alerts } from 'types/api/alerts/getTriggered';
|
import { Alerts } from 'types/api/alerts/getTriggered';
|
||||||
|
|
||||||
import { Value } from './Filter';
|
import { Value } from './Filter';
|
||||||
@ -16,6 +15,7 @@ function NoFilterTable({
|
|||||||
selectedFilter,
|
selectedFilter,
|
||||||
}: NoFilterTableProps): JSX.Element {
|
}: NoFilterTableProps): JSX.Element {
|
||||||
const filteredAlerts = FilterAlerts(allAlerts, selectedFilter);
|
const filteredAlerts = FilterAlerts(allAlerts, selectedFilter);
|
||||||
|
const { formatTimezoneAdjustedTimestamp } = useTimezone();
|
||||||
|
|
||||||
// need to add the filter
|
// need to add the filter
|
||||||
const columns: ColumnsType<Alerts> = [
|
const columns: ColumnsType<Alerts> = [
|
||||||
@ -83,15 +83,12 @@ function NoFilterTable({
|
|||||||
width: 100,
|
width: 100,
|
||||||
sorter: (a, b): number =>
|
sorter: (a, b): number =>
|
||||||
new Date(a.startsAt).getTime() - new Date(b.startsAt).getTime(),
|
new Date(a.startsAt).getTime() - new Date(b.startsAt).getTime(),
|
||||||
render: (date): JSX.Element => {
|
render: (date): JSX.Element => (
|
||||||
const formatedDate = new Date(date);
|
<Typography>{`${formatTimezoneAdjustedTimestamp(
|
||||||
|
date,
|
||||||
return (
|
'MM/DD/YYYY hh:mm:ss A (UTC Z)',
|
||||||
<Typography>{`${getFormattedDate(formatedDate)} ${convertDateToAmAndPm(
|
)}`}</Typography>
|
||||||
formatedDate,
|
),
|
||||||
)}`}</Typography>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
103
frontend/src/hooks/useTimezoneFormatter/useTimezoneFormatter.ts
Normal file
103
frontend/src/hooks/useTimezoneFormatter/useTimezoneFormatter.ts
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import { Timezone } from 'components/CustomTimePicker/timezoneUtils';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import timezone from 'dayjs/plugin/timezone';
|
||||||
|
import utc from 'dayjs/plugin/utc';
|
||||||
|
import { useCallback, useEffect, useMemo } from 'react';
|
||||||
|
|
||||||
|
// Initialize dayjs plugins
|
||||||
|
dayjs.extend(utc);
|
||||||
|
dayjs.extend(timezone);
|
||||||
|
|
||||||
|
// Types
|
||||||
|
export type TimestampInput = string | number | Date;
|
||||||
|
interface CacheEntry {
|
||||||
|
value: string;
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
|
||||||
|
// Constants
|
||||||
|
const CACHE_SIZE_LIMIT = 1000;
|
||||||
|
const CACHE_CLEANUP_PERCENTAGE = 0.5; // Remove 50% when limit is reached
|
||||||
|
|
||||||
|
function useTimezoneFormatter({
|
||||||
|
userTimezone,
|
||||||
|
}: {
|
||||||
|
userTimezone: Timezone;
|
||||||
|
}): {
|
||||||
|
formatTimezoneAdjustedTimestamp: (
|
||||||
|
input: TimestampInput,
|
||||||
|
format?: string,
|
||||||
|
) => string;
|
||||||
|
} {
|
||||||
|
// Initialize cache using useMemo to persist between renders
|
||||||
|
const cache = useMemo(() => new Map<string, CacheEntry>(), []);
|
||||||
|
|
||||||
|
// Clear cache when timezone changes
|
||||||
|
useEffect(() => {
|
||||||
|
cache.clear();
|
||||||
|
}, [cache, userTimezone]);
|
||||||
|
|
||||||
|
const clearCacheEntries = useCallback(() => {
|
||||||
|
if (cache.size <= CACHE_SIZE_LIMIT) return;
|
||||||
|
|
||||||
|
// Sort entries by timestamp (oldest first)
|
||||||
|
const sortedEntries = Array.from(cache.entries()).sort(
|
||||||
|
(a, b) => a[1].timestamp - b[1].timestamp,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Calculate how many entries to remove (50% or overflow, whichever is larger)
|
||||||
|
const entriesToRemove = Math.max(
|
||||||
|
Math.floor(cache.size * CACHE_CLEANUP_PERCENTAGE),
|
||||||
|
cache.size - CACHE_SIZE_LIMIT,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Remove oldest entries
|
||||||
|
sortedEntries.slice(0, entriesToRemove).forEach(([key]) => cache.delete(key));
|
||||||
|
}, [cache]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats a timestamp with the user's timezone and caches the result
|
||||||
|
* @param {TimestampInput} input - The timestamp to format (string, number, or Date)
|
||||||
|
* @param {string} [format='YYYY-MM-DD HH:mm:ss'] - The desired output format
|
||||||
|
* @returns {string} The formatted timestamp string in the user's timezone
|
||||||
|
* @example
|
||||||
|
* // Input: UTC timestamp
|
||||||
|
* // User timezone: 'UTC - 4'
|
||||||
|
* // Returns: "2024-03-14 15:30:00"
|
||||||
|
* formatTimezoneAdjustedTimestamp('2024-03-14T19:30:00Z')
|
||||||
|
*/
|
||||||
|
const formatTimezoneAdjustedTimestamp = useCallback(
|
||||||
|
(input: TimestampInput, format = 'YYYY-MM-DD HH:mm:ss'): string => {
|
||||||
|
const timestamp = dayjs(input).valueOf();
|
||||||
|
const cacheKey = `${timestamp}_${userTimezone.value}`;
|
||||||
|
|
||||||
|
// Check cache first
|
||||||
|
const cachedValue = cache.get(cacheKey);
|
||||||
|
if (cachedValue) {
|
||||||
|
return cachedValue.value;
|
||||||
|
}
|
||||||
|
// Format timestamp
|
||||||
|
const formattedValue = dayjs(input).tz(userTimezone.value).format(format);
|
||||||
|
|
||||||
|
// Update cache
|
||||||
|
cache.set(cacheKey, {
|
||||||
|
value: formattedValue,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear expired entries and enforce size limit
|
||||||
|
if (cache.size > CACHE_SIZE_LIMIT) {
|
||||||
|
clearCacheEntries();
|
||||||
|
}
|
||||||
|
|
||||||
|
return formattedValue;
|
||||||
|
},
|
||||||
|
[cache, clearCacheEntries, userTimezone.value],
|
||||||
|
);
|
||||||
|
|
||||||
|
return { formatTimezoneAdjustedTimestamp };
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useTimezoneFormatter;
|
@ -7,6 +7,7 @@ import { AxiosError } from 'axios';
|
|||||||
import { ThemeProvider } from 'hooks/useDarkMode';
|
import { ThemeProvider } from 'hooks/useDarkMode';
|
||||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||||
import posthog from 'posthog-js';
|
import posthog from 'posthog-js';
|
||||||
|
import TimezoneProvider from 'providers/Timezone';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
import { HelmetProvider } from 'react-helmet-async';
|
import { HelmetProvider } from 'react-helmet-async';
|
||||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||||
@ -69,14 +70,16 @@ if (container) {
|
|||||||
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
|
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
|
||||||
<HelmetProvider>
|
<HelmetProvider>
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<QueryClientProvider client={queryClient}>
|
<TimezoneProvider>
|
||||||
<Provider store={store}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<AppRoutes />
|
<Provider store={store}>
|
||||||
</Provider>
|
<AppRoutes />
|
||||||
{process.env.NODE_ENV === 'development' && (
|
</Provider>
|
||||||
<ReactQueryDevtools initialIsOpen={false} />
|
{process.env.NODE_ENV === 'development' && (
|
||||||
)}
|
<ReactQueryDevtools initialIsOpen={false} />
|
||||||
</QueryClientProvider>
|
)}
|
||||||
|
</QueryClientProvider>
|
||||||
|
</TimezoneProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</HelmetProvider>
|
</HelmetProvider>
|
||||||
</Sentry.ErrorBoundary>,
|
</Sentry.ErrorBoundary>,
|
||||||
|
@ -55,6 +55,8 @@ export interface GetUPlotChartOptions {
|
|||||||
>;
|
>;
|
||||||
customTooltipElement?: HTMLDivElement;
|
customTooltipElement?: HTMLDivElement;
|
||||||
verticalLineTimestamp?: number;
|
verticalLineTimestamp?: number;
|
||||||
|
tzDate?: (timestamp: number) => Date;
|
||||||
|
timezone?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** the function converts series A , series B , series C to
|
/** the function converts series A , series B , series C to
|
||||||
@ -158,6 +160,8 @@ export const getUPlotChartOptions = ({
|
|||||||
setHiddenGraph,
|
setHiddenGraph,
|
||||||
customTooltipElement,
|
customTooltipElement,
|
||||||
verticalLineTimestamp,
|
verticalLineTimestamp,
|
||||||
|
tzDate,
|
||||||
|
timezone,
|
||||||
}: GetUPlotChartOptions): uPlot.Options => {
|
}: GetUPlotChartOptions): uPlot.Options => {
|
||||||
const timeScaleProps = getXAxisScale(minTimeScale, maxTimeScale);
|
const timeScaleProps = getXAxisScale(minTimeScale, maxTimeScale);
|
||||||
|
|
||||||
@ -196,6 +200,7 @@ export const getUPlotChartOptions = ({
|
|||||||
fill: (): string => '#fff',
|
fill: (): string => '#fff',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
tzDate,
|
||||||
padding: [16, 16, 8, 8],
|
padding: [16, 16, 8, 8],
|
||||||
bands,
|
bands,
|
||||||
scales: {
|
scales: {
|
||||||
@ -222,6 +227,7 @@ export const getUPlotChartOptions = ({
|
|||||||
stackBarChart,
|
stackBarChart,
|
||||||
isDarkMode,
|
isDarkMode,
|
||||||
customTooltipElement,
|
customTooltipElement,
|
||||||
|
timezone,
|
||||||
}),
|
}),
|
||||||
onClickPlugin({
|
onClickPlugin({
|
||||||
onClick: onClickHandler,
|
onClick: onClickHandler,
|
||||||
|
@ -46,6 +46,7 @@ const generateTooltipContent = (
|
|||||||
isHistogramGraphs?: boolean,
|
isHistogramGraphs?: boolean,
|
||||||
isMergedSeries?: boolean,
|
isMergedSeries?: boolean,
|
||||||
stackBarChart?: boolean,
|
stackBarChart?: boolean,
|
||||||
|
timezone?: string,
|
||||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
): HTMLElement => {
|
): HTMLElement => {
|
||||||
const container = document.createElement('div');
|
const container = document.createElement('div');
|
||||||
@ -69,9 +70,13 @@ const generateTooltipContent = (
|
|||||||
series.forEach((item, index) => {
|
series.forEach((item, index) => {
|
||||||
if (index === 0) {
|
if (index === 0) {
|
||||||
if (isBillingUsageGraphs) {
|
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 {
|
} 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) {
|
} else if (item.show) {
|
||||||
const {
|
const {
|
||||||
@ -223,6 +228,7 @@ type ToolTipPluginProps = {
|
|||||||
stackBarChart?: boolean;
|
stackBarChart?: boolean;
|
||||||
isDarkMode: boolean;
|
isDarkMode: boolean;
|
||||||
customTooltipElement?: HTMLDivElement;
|
customTooltipElement?: HTMLDivElement;
|
||||||
|
timezone?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const tooltipPlugin = ({
|
const tooltipPlugin = ({
|
||||||
@ -234,6 +240,7 @@ const tooltipPlugin = ({
|
|||||||
stackBarChart,
|
stackBarChart,
|
||||||
isDarkMode,
|
isDarkMode,
|
||||||
customTooltipElement,
|
customTooltipElement,
|
||||||
|
timezone,
|
||||||
}: // eslint-disable-next-line sonarjs/cognitive-complexity
|
}: // eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
ToolTipPluginProps): any => {
|
ToolTipPluginProps): any => {
|
||||||
let over: HTMLElement;
|
let over: HTMLElement;
|
||||||
@ -300,6 +307,7 @@ ToolTipPluginProps): any => {
|
|||||||
isHistogramGraphs,
|
isHistogramGraphs,
|
||||||
isMergedSeries,
|
isMergedSeries,
|
||||||
stackBarChart,
|
stackBarChart,
|
||||||
|
timezone,
|
||||||
);
|
);
|
||||||
if (customTooltipElement) {
|
if (customTooltipElement) {
|
||||||
content.appendChild(customTooltipElement);
|
content.appendChild(customTooltipElement);
|
||||||
|
@ -14,6 +14,7 @@ import {
|
|||||||
QueryBuilderProvider,
|
QueryBuilderProvider,
|
||||||
} from 'providers/QueryBuilder';
|
} from 'providers/QueryBuilder';
|
||||||
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
|
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
|
||||||
|
import TimezoneProvider from 'providers/Timezone';
|
||||||
import { I18nextProvider } from 'react-i18next';
|
import { I18nextProvider } from 'react-i18next';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
@ -92,7 +93,9 @@ describe('Logs Explorer Tests', () => {
|
|||||||
<I18nextProvider i18n={i18n}>
|
<I18nextProvider i18n={i18n}>
|
||||||
<MockQueryClientProvider>
|
<MockQueryClientProvider>
|
||||||
<QueryBuilderProvider>
|
<QueryBuilderProvider>
|
||||||
<LogsExplorer />
|
<TimezoneProvider>
|
||||||
|
<LogsExplorer />
|
||||||
|
</TimezoneProvider>
|
||||||
</QueryBuilderProvider>
|
</QueryBuilderProvider>
|
||||||
</MockQueryClientProvider>
|
</MockQueryClientProvider>
|
||||||
</I18nextProvider>
|
</I18nextProvider>
|
||||||
@ -141,7 +144,9 @@ describe('Logs Explorer Tests', () => {
|
|||||||
<VirtuosoMockContext.Provider
|
<VirtuosoMockContext.Provider
|
||||||
value={{ viewportHeight: 300, itemHeight: 100 }}
|
value={{ viewportHeight: 300, itemHeight: 100 }}
|
||||||
>
|
>
|
||||||
<LogsExplorer />
|
<TimezoneProvider>
|
||||||
|
<LogsExplorer />
|
||||||
|
</TimezoneProvider>
|
||||||
</VirtuosoMockContext.Provider>
|
</VirtuosoMockContext.Provider>
|
||||||
</QueryBuilderProvider>
|
</QueryBuilderProvider>
|
||||||
</MockQueryClientProvider>
|
</MockQueryClientProvider>
|
||||||
@ -225,7 +230,9 @@ describe('Logs Explorer Tests', () => {
|
|||||||
<VirtuosoMockContext.Provider
|
<VirtuosoMockContext.Provider
|
||||||
value={{ viewportHeight: 300, itemHeight: 100 }}
|
value={{ viewportHeight: 300, itemHeight: 100 }}
|
||||||
>
|
>
|
||||||
<LogsExplorer />
|
<TimezoneProvider>
|
||||||
|
<LogsExplorer />
|
||||||
|
</TimezoneProvider>
|
||||||
</VirtuosoMockContext.Provider>
|
</VirtuosoMockContext.Provider>
|
||||||
</QueryBuilderContext.Provider>
|
</QueryBuilderContext.Provider>
|
||||||
</MockQueryClientProvider>
|
</MockQueryClientProvider>
|
||||||
@ -253,7 +260,9 @@ describe('Logs Explorer Tests', () => {
|
|||||||
<I18nextProvider i18n={i18n}>
|
<I18nextProvider i18n={i18n}>
|
||||||
<MockQueryClientProvider>
|
<MockQueryClientProvider>
|
||||||
<QueryBuilderProvider>
|
<QueryBuilderProvider>
|
||||||
<LogsExplorer />,
|
<TimezoneProvider>
|
||||||
|
<LogsExplorer />
|
||||||
|
</TimezoneProvider>
|
||||||
</QueryBuilderProvider>
|
</QueryBuilderProvider>
|
||||||
</MockQueryClientProvider>
|
</MockQueryClientProvider>
|
||||||
</I18nextProvider>
|
</I18nextProvider>
|
||||||
|
@ -31,6 +31,7 @@ import {
|
|||||||
Trash2,
|
Trash2,
|
||||||
X,
|
X,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
import { useTimezone } from 'providers/Timezone';
|
||||||
import { ChangeEvent, useEffect, useRef, useState } from 'react';
|
import { ChangeEvent, useEffect, useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
@ -207,6 +208,8 @@ function SaveView(): JSX.Element {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { formatTimezoneAdjustedTimestamp } = useTimezone();
|
||||||
|
|
||||||
const columns: TableProps<ViewProps>['columns'] = [
|
const columns: TableProps<ViewProps>['columns'] = [
|
||||||
{
|
{
|
||||||
title: 'Save View',
|
title: 'Save View',
|
||||||
@ -218,31 +221,10 @@ function SaveView(): JSX.Element {
|
|||||||
bgColor = extraData.color;
|
bgColor = extraData.color;
|
||||||
}
|
}
|
||||||
|
|
||||||
const timeOptions: Intl.DateTimeFormatOptions = {
|
const formattedDateAndTime = formatTimezoneAdjustedTimestamp(
|
||||||
hour: '2-digit',
|
view.createdAt,
|
||||||
minute: '2-digit',
|
'HH:mm:ss ⎯ MMM D, YYYY (UTC Z)',
|
||||||
second: '2-digit',
|
|
||||||
hour12: false,
|
|
||||||
};
|
|
||||||
const formattedTime = new Date(view.createdAt).toLocaleTimeString(
|
|
||||||
'en-US',
|
|
||||||
timeOptions,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const dateOptions: Intl.DateTimeFormatOptions = {
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
year: 'numeric',
|
|
||||||
};
|
|
||||||
|
|
||||||
const formattedDate = new Date(view.createdAt).toLocaleDateString(
|
|
||||||
'en-US',
|
|
||||||
dateOptions,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Combine time and date
|
|
||||||
const formattedDateAndTime = `${formattedTime} ⎯ ${formattedDate}`;
|
|
||||||
|
|
||||||
const isEditDeleteSupported = allowedRoles.includes(role as string);
|
const isEditDeleteSupported = allowedRoles.includes(role as string);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
115
frontend/src/providers/Timezone.tsx
Normal file
115
frontend/src/providers/Timezone.tsx
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
import {
|
||||||
|
getBrowserTimezone,
|
||||||
|
getTimezoneObjectByTimezoneString,
|
||||||
|
Timezone,
|
||||||
|
UTC_TIMEZONE,
|
||||||
|
} from 'components/CustomTimePicker/timezoneUtils';
|
||||||
|
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||||
|
import useTimezoneFormatter, {
|
||||||
|
TimestampInput,
|
||||||
|
} from 'hooks/useTimezoneFormatter/useTimezoneFormatter';
|
||||||
|
import React, {
|
||||||
|
createContext,
|
||||||
|
Dispatch,
|
||||||
|
SetStateAction,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
|
interface TimezoneContextType {
|
||||||
|
timezone: Timezone;
|
||||||
|
browserTimezone: Timezone;
|
||||||
|
updateTimezone: (timezone: Timezone) => void;
|
||||||
|
formatTimezoneAdjustedTimestamp: (
|
||||||
|
input: TimestampInput,
|
||||||
|
format?: string,
|
||||||
|
) => string;
|
||||||
|
isAdaptationEnabled: boolean;
|
||||||
|
setIsAdaptationEnabled: Dispatch<SetStateAction<boolean>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 [isAdaptationEnabled, setIsAdaptationEnabled] = useState(true);
|
||||||
|
|
||||||
|
const updateTimezone = useCallback((timezone: Timezone): void => {
|
||||||
|
if (!timezone.value) return;
|
||||||
|
|
||||||
|
// TODO(shaheer): replace this with user preferences API
|
||||||
|
setStoredTimezoneValue(timezone.value);
|
||||||
|
setTimezone(timezone);
|
||||||
|
// Enable adaptation when a new timezone is set
|
||||||
|
setIsAdaptationEnabled(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const { formatTimezoneAdjustedTimestamp } = useTimezoneFormatter({
|
||||||
|
userTimezone: timezone,
|
||||||
|
});
|
||||||
|
|
||||||
|
const value = React.useMemo(
|
||||||
|
() => ({
|
||||||
|
timezone: isAdaptationEnabled ? timezone : UTC_TIMEZONE,
|
||||||
|
browserTimezone,
|
||||||
|
updateTimezone,
|
||||||
|
formatTimezoneAdjustedTimestamp,
|
||||||
|
isAdaptationEnabled,
|
||||||
|
setIsAdaptationEnabled,
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
timezone,
|
||||||
|
browserTimezone,
|
||||||
|
updateTimezone,
|
||||||
|
formatTimezoneAdjustedTimestamp,
|
||||||
|
isAdaptationEnabled,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
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;
|
@ -1,6 +1,7 @@
|
|||||||
import { render, RenderOptions, RenderResult } from '@testing-library/react';
|
import { render, RenderOptions, RenderResult } from '@testing-library/react';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import { ResourceProvider } from 'hooks/useResourceAttribute';
|
import { ResourceProvider } from 'hooks/useResourceAttribute';
|
||||||
|
import TimezoneProvider from 'providers/Timezone';
|
||||||
import React, { ReactElement } from 'react';
|
import React, { ReactElement } from 'react';
|
||||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
@ -89,7 +90,9 @@ function AllTheProviders({
|
|||||||
<Provider store={mockStored(role)}>
|
<Provider store={mockStored(role)}>
|
||||||
{' '}
|
{' '}
|
||||||
{/* Use the mock store with the provided role */}
|
{/* Use the mock store with the provided role */}
|
||||||
<BrowserRouter>{children}</BrowserRouter>
|
<BrowserRouter>
|
||||||
|
<TimezoneProvider>{children}</TimezoneProvider>
|
||||||
|
</BrowserRouter>
|
||||||
</Provider>
|
</Provider>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
</ResourceProvider>
|
</ResourceProvider>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user