feat: update time range selection flows to handle relative and absolu… (#4742)

* feat: update time range selection flows to handle relative and absolute times

* fix: lint error

* fix: lint error

* feat: update logic to handle custom relative times on load and standardize relative time formats

* fix: type issue

* fix: handle light mode and on custom time range select

* chore: update alert frequency corresponding times

* chore: update copy URL

* feat: update styles
This commit is contained in:
Yunus M 2024-03-29 14:53:48 +05:30 committed by GitHub
parent 7c2f5352d2
commit 6eced60bf5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 636 additions and 167 deletions

View File

@ -5,13 +5,14 @@ import './CustomTimePicker.styles.scss';
import { Input, Popover, Tooltip, Typography } from 'antd';
import cx from 'classnames';
import { DateTimeRangeType } from 'container/TopNav/CustomDateTimeModal';
import { Options } from 'container/TopNav/DateTimeSelection/config';
import {
FixedDurationSuggestionOptions,
Options,
RelativeDurationSuggestionOptions,
} from 'container/TopNav/DateTimeSelectionV2/config';
import dayjs from 'dayjs';
import { defaultTo, noop } from 'lodash-es';
import { isValidTimeFormat } from 'lib/getMinMax';
import { defaultTo, isFunction, noop } from 'lodash-es';
import debounce from 'lodash-es/debounce';
import { CheckCircle, ChevronDown, Clock } from 'lucide-react';
import {
@ -33,7 +34,14 @@ interface CustomTimePickerProps {
onError: (value: boolean) => void;
selectedValue: string;
selectedTime: string;
onValidCustomDateChange: ([t1, t2]: any[]) => void;
onValidCustomDateChange: ({
time: [t1, t2],
timeStr,
}: {
time: [dayjs.Dayjs | null, dayjs.Dayjs | null];
timeStr: string;
}) => void;
onCustomTimeStatusUpdate?: (isValid: boolean) => void;
open: boolean;
setOpen: Dispatch<SetStateAction<boolean>>;
items: any[];
@ -53,6 +61,7 @@ function CustomTimePicker({
open,
setOpen,
onValidCustomDateChange,
onCustomTimeStatusUpdate,
newPopover,
customDateTimeVisible,
setCustomDTPickerVisible,
@ -85,6 +94,7 @@ function CustomTimePicker({
return Options[index].label;
}
}
for (
let index = 0;
index < RelativeDurationSuggestionOptions.length;
@ -94,12 +104,17 @@ function CustomTimePicker({
return RelativeDurationSuggestionOptions[index].label;
}
}
for (let index = 0; index < FixedDurationSuggestionOptions.length; index++) {
if (FixedDurationSuggestionOptions[index].value === selectedTime) {
return FixedDurationSuggestionOptions[index].label;
}
}
if (isValidTimeFormat(selectedTime)) {
return selectedTime;
}
return '';
};
@ -161,13 +176,22 @@ function CustomTimePicker({
setInputStatus('error');
onError(true);
setInputErrorMessage('Please enter time less than 6 months');
if (isFunction(onCustomTimeStatusUpdate)) {
onCustomTimeStatusUpdate(true);
}
} else {
onValidCustomDateChange([minTime, currentTime]);
onValidCustomDateChange({
time: [minTime, currentTime],
timeStr: inputValue,
});
}
} else {
setInputStatus('error');
onError(true);
setInputErrorMessage(null);
if (isFunction(onCustomTimeStatusUpdate)) {
onCustomTimeStatusUpdate(false);
}
}
}, 300);
@ -320,4 +344,5 @@ CustomTimePicker.defaultProps = {
setCustomDTPickerVisible: noop,
onCustomDateHandler: noop,
handleGoLive: noop,
onCustomTimeStatusUpdate: noop,
};

View File

@ -29,4 +29,5 @@ export enum QueryParams {
expandedWidgetId = 'expandedWidgetId',
integration = 'integration',
pagination = 'pagination',
relativeTime = 'relativeTime',
}

View File

@ -7,7 +7,10 @@ import GridPanelSwitch from 'container/GridPanelSwitch';
import { getFormatNameByOptionId } from 'container/NewWidget/RightContainer/alertFomatCategories';
import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems';
import { Time } from 'container/TopNav/DateTimeSelection/config';
import { Time as TimeV2 } from 'container/TopNav/DateTimeSelectionV2/config';
import {
CustomTimeType,
Time as TimeV2,
} from 'container/TopNav/DateTimeSelectionV2/config';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useResizeObserver } from 'hooks/useDimensions';
@ -39,7 +42,7 @@ export interface ChartPreviewProps {
query: Query | null;
graphType?: PANEL_TYPES;
selectedTime?: timePreferenceType;
selectedInterval?: Time | TimeV2;
selectedInterval?: Time | TimeV2 | CustomTimeType;
headline?: JSX.Element;
alertDef?: AlertDef;
userQueryKey?: string;
@ -53,7 +56,7 @@ function ChartPreview({
query,
graphType = PANEL_TYPES.TIME_SERIES,
selectedTime = 'GLOBAL_TIME',
selectedInterval = '5min',
selectedInterval = '5m',
headline,
userQueryKey,
allowSelectedIntervalForStepGen = false,

View File

@ -12,22 +12,30 @@ import {
// toChartInterval converts eval window to chart selection time interval
export const toChartInterval = (evalWindow: string | undefined): Time => {
switch (evalWindow) {
case '1m0s':
return '1m';
case '5m0s':
return '5min';
return '5m';
case '10m0s':
return '10min';
return '10m';
case '15m0s':
return '15min';
return '15m';
case '30m0s':
return '30min';
return '30m';
case '1h0m0s':
return '1hr';
return '1h';
case '3h0m0s':
return '3h';
case '4h0m0s':
return '4hr';
return '4h';
case '6h0m0s':
return '6h';
case '12h0m0s':
return '12h';
case '24h0m0s':
return '1day';
return '1d';
default:
return '5min';
return '5m';
}
};

View File

@ -1,6 +1,7 @@
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { CustomTimeType } from 'container/TopNav/DateTimeSelectionV2/config';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { useStepInterval } from 'hooks/queryBuilder/useStepInterval';
import { useIsDarkMode } from 'hooks/useDarkMode';
@ -81,8 +82,13 @@ function GridCardGraph({
const searchParams = new URLSearchParams(window.location.search);
const startTime = searchParams.get(QueryParams.startTime);
const endTime = searchParams.get(QueryParams.endTime);
const relativeTime = searchParams.get(
QueryParams.relativeTime,
) as CustomTimeType;
if (startTime && endTime && startTime !== endTime) {
if (relativeTime) {
dispatch(UpdateTimeInterval(relativeTime));
} else if (startTime && endTime && startTime !== endTime) {
dispatch(
UpdateTimeInterval('custom', [
parseInt(getTimeString(startTime), 10),

View File

@ -2,6 +2,7 @@ import Graph from 'components/Graph';
import Spinner from 'components/Spinner';
import { QueryParams } from 'constants/query';
import { themeColors } from 'constants/theme';
import { CustomTimeType } from 'container/TopNav/DateTimeSelectionV2/config';
import useUrlQuery from 'hooks/useUrlQuery';
import getChartData, { GetChartDataProps } from 'lib/getChartData';
import GetMinMax from 'lib/getMinMax';
@ -65,8 +66,13 @@ function LogsExplorerChart({
const searchParams = new URLSearchParams(window.location.search);
const startTime = searchParams.get(QueryParams.startTime);
const endTime = searchParams.get(QueryParams.endTime);
const relativeTime = searchParams.get(
QueryParams.relativeTime,
) as CustomTimeType;
if (startTime && endTime && startTime !== endTime) {
if (relativeTime) {
dispatch(UpdateTimeInterval(relativeTime));
} else if (startTime && endTime && startTime !== endTime) {
dispatch(
UpdateTimeInterval('custom', [
parseInt(getTimeString(startTime), 10),

View File

@ -1,9 +1,12 @@
import { Time } from 'container/TopNav/DateTimeSelection/config';
import { Time as TimeV2 } from 'container/TopNav/DateTimeSelectionV2/config';
import {
CustomTimeType,
Time as TimeV2,
} from 'container/TopNav/DateTimeSelectionV2/config';
import { GetMinMaxPayload } from 'lib/getMinMax';
export const getGlobalTime = (
selectedTime: Time | TimeV2,
selectedTime: Time | TimeV2 | CustomTimeType,
globalTime: GetMinMaxPayload,
): GetMinMaxPayload | undefined => {
if (selectedTime === 'custom') {

View File

@ -3,6 +3,7 @@ import { PANEL_TYPES } from 'constants/queryBuilder';
import GridPanelSwitch from 'container/GridPanelSwitch';
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
import { timePreferance } from 'container/NewWidget/RightContainer/timeItems';
import { CustomTimeType } from 'container/TopNav/DateTimeSelectionV2/config';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useResizeObserver } from 'hooks/useDimensions';
@ -97,8 +98,13 @@ function WidgetGraph({
const searchParams = new URLSearchParams(window.location.search);
const startTime = searchParams.get(QueryParams.startTime);
const endTime = searchParams.get(QueryParams.endTime);
const relativeTime = searchParams.get(
QueryParams.relativeTime,
) as CustomTimeType;
if (startTime && endTime && startTime !== endTime) {
if (relativeTime) {
dispatch(UpdateTimeInterval(relativeTime));
} else if (startTime && endTime && startTime !== endTime) {
dispatch(
UpdateTimeInterval('custom', [
parseInt(getTimeString(startTime), 10),

View File

@ -1,6 +1,9 @@
import { ServiceDataProps } from 'api/metrics/getTopLevelOperations';
import { Time } from 'container/TopNav/DateTimeSelection/config';
import { Time as TimeV2 } from 'container/TopNav/DateTimeSelectionV2/config';
import {
CustomTimeType,
Time as TimeV2,
} from 'container/TopNav/DateTimeSelectionV2/config';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import { UseQueryResult } from 'react-query';
import { SuccessResponse } from 'types/api';
@ -25,7 +28,7 @@ export interface GetQueryRangeRequestDataProps {
topLevelOperations: [keyof ServiceDataProps, string[]][];
maxTime: number;
minTime: number;
globalSelectedInterval: Time | TimeV2;
globalSelectedInterval: Time | TimeV2 | CustomTimeType;
}
export interface GetServiceListFromQueryProps {

View File

@ -1,7 +1,7 @@
import GetMinMax, { GetMinMaxPayload } from 'lib/getMinMax';
import { Time } from '../DateTimeSelection/config';
import { Time as TimeV2 } from '../DateTimeSelectionV2/config';
import { CustomTimeType, Time as TimeV2 } from '../DateTimeSelectionV2/config';
export const options: IOptions[] = [
{
@ -68,7 +68,7 @@ export interface IOptions {
}
export const getMinMax = (
selectedTime: Time | TimeV2,
selectedTime: Time | TimeV2 | CustomTimeType,
minTime: number,
maxTime: number,
): GetMinMaxPayload =>

View File

@ -1,7 +1,7 @@
import GetMinMax, { GetMinMaxPayload } from 'lib/getMinMax';
import { Time } from '../DateTimeSelection/config';
import { Time as TimeV2 } from '../DateTimeSelectionV2/config';
import { CustomTimeType, Time as TimeV2 } from '../DateTimeSelectionV2/config';
export const options: IOptions[] = [
{
@ -68,7 +68,7 @@ export interface IOptions {
}
export const getMinMax = (
selectedTime: Time | TimeV2,
selectedTime: Time | TimeV2 | CustomTimeType,
minTime: number,
maxTime: number,
): GetMinMaxPayload =>

View File

@ -1,16 +1,18 @@
import ROUTES from 'constants/routes';
type FiveMin = '5min';
type TenMin = '10min';
type FifteenMin = '15min';
type ThirtyMin = '30min';
type OneMin = '1min';
type SixHour = '6hr';
type OneHour = '1hr';
type FourHour = '4hr';
type OneDay = '1day';
type ThreeDay = '3days';
type OneWeek = '1week';
type FiveMin = '5m';
type TenMin = '10m';
type FifteenMin = '15m';
type ThirtyMin = '30m';
type OneMin = '1m';
type SixHour = '6h';
type OneHour = '1h';
type FourHour = '4h';
type ThreeHour = '3h';
type TwelveHour = '12h';
type OneDay = '1d';
type ThreeDay = '3d';
type OneWeek = '1w';
type Custom = 'custom';
export type Time =
@ -22,37 +24,62 @@ export type Time =
| FourHour
| SixHour
| OneHour
| ThreeHour
| Custom
| OneWeek
| OneDay
| TwelveHour
| ThreeDay;
export const Options: Option[] = [
{ value: '5min', label: 'Last 5 min' },
{ value: '15min', label: 'Last 15 min' },
{ value: '30min', label: 'Last 30 min' },
{ value: '1hr', label: 'Last 1 hour' },
{ value: '6hr', label: 'Last 6 hour' },
{ value: '1day', label: 'Last 1 day' },
{ value: '3days', label: 'Last 3 days' },
{ value: '1week', label: 'Last 1 week' },
{ value: '5m', label: 'Last 5 min' },
{ value: '15m', label: 'Last 15 min' },
{ value: '30m', label: 'Last 30 min' },
{ value: '1h', label: 'Last 1 hour' },
{ value: '6h', label: 'Last 6 hour' },
{ value: '1d', label: 'Last 1 day' },
{ value: '3d', label: 'Last 3 days' },
{ value: '1w', label: 'Last 1 week' },
{ value: 'custom', label: 'Custom' },
];
type TimeFrame = {
'5min': string;
'15min': string;
'30min': string;
'1hr': string;
'6hr': string;
'1day': string;
'3days': string;
'1week': string;
[key: string]: string; // Index signature to allow any string as index
};
export const RelativeTimeMap: TimeFrame = {
'5min': '5m',
'15min': '15m',
'30min': '30m',
'1hr': '1h',
'6hr': '6h',
'1day': '1d',
'3days': '3d',
'1week': '1w',
};
export interface Option {
value: Time;
label: string;
}
export const RelativeDurationOptions: Option[] = [
{ value: '5min', label: 'Last 5 min' },
{ value: '15min', label: 'Last 15 min' },
{ value: '30min', label: 'Last 30 min' },
{ value: '1hr', label: 'Last 1 hour' },
{ value: '6hr', label: 'Last 6 hour' },
{ value: '1day', label: 'Last 1 day' },
{ value: '3days', label: 'Last 3 days' },
{ value: '1week', label: 'Last 1 week' },
{ value: '5m', label: 'Last 5 min' },
{ value: '15m', label: 'Last 15 min' },
{ value: '30m', label: 'Last 30 min' },
{ value: '1h', label: 'Last 1 hour' },
{ value: '6h', label: 'Last 6 hour' },
{ value: '1d', label: 'Last 1 day' },
{ value: '3d', label: 'Last 3 days' },
{ value: '1w', label: 'Last 1 week' },
];
export const getDefaultOption = (route: string): Time => {

View File

@ -28,7 +28,7 @@ import { GlobalReducer } from 'types/reducer/globalTime';
import AutoRefresh from '../AutoRefresh';
import CustomDateTimeModal, { DateTimeRangeType } from '../CustomDateTimeModal';
import { Time as TimeV2 } from '../DateTimeSelectionV2/config';
import { CustomTimeType, Time as TimeV2 } from '../DateTimeSelectionV2/config';
import {
getDefaultOption,
getOptions,
@ -122,7 +122,7 @@ function DateTimeSelection({
const getInputLabel = (
startTime?: Dayjs,
endTime?: Dayjs,
timeInterval: Time | TimeV2 = '15min',
timeInterval: Time | TimeV2 | CustomTimeType = '15m',
): string | Time => {
if (startTime && endTime && timeInterval === 'custom') {
const format = 'YYYY/MM/DD HH:mm';
@ -225,7 +225,7 @@ function DateTimeSelection({
[location.pathname],
);
const onSelectHandler = (value: Time | TimeV2): void => {
const onSelectHandler = (value: Time | TimeV2 | CustomTimeType): void => {
if (value !== 'custom') {
updateTimeInterval(value);
updateLocalStorageForRoutes(value);
@ -358,7 +358,7 @@ function DateTimeSelection({
}}
selectedTime={selectedTime}
onValidCustomDateChange={(dateTime): void =>
onCustomDateHandler(dateTime as DateTimeRangeType)
onCustomDateHandler(dateTime.time as DateTimeRangeType)
}
selectedValue={getInputLabel(
dayjs(minTime / 1000000),
@ -406,7 +406,7 @@ function DateTimeSelection({
interface DispatchProps {
updateTimeInterval: (
interval: Time | TimeV2,
interval: Time | TimeV2 | CustomTimeType,
dateTimeRange?: [number, number],
) => (dispatch: Dispatch<AppActions>) => void;
globalTimeLoading: () => void;

View File

@ -54,9 +54,61 @@
}
}
}
.share-link-btn {
height: 34px;
}
.shareable-link-popover {
margin-left: 8px;
}
}
.date-time-root {
.share-modal-content {
display: flex;
flex-direction: column;
gap: 8px;
padding: 16px;
width: 420px;
.absolute-relative-time-toggler-container {
display: flex;
gap: 8px;
align-items: center;
}
.absolute-relative-time-toggler {
display: flex;
gap: 4px;
align-items: center;
}
.absolute-relative-time-error {
font-size: 12px;
color: var(--bg-amber-600);
}
.share-link {
display: flex;
align-items: center;
.share-url {
flex: 1;
border: 1px solid var(--bg-slate-400);
border-radius: 2px;
background: var(--bg-ink-300);
height: 32px;
padding: 6px 8px;
}
.copy-url-btn {
width: 32px;
}
}
}
.date-time-root,
.shareable-link-popover-root {
.ant-popover-inner {
border-radius: 4px !important;
border: 1px solid var(--bg-slate-400);
@ -185,7 +237,8 @@
}
}
.date-time-root {
.date-time-root,
.shareable-link-popover-root {
.ant-popover-inner {
border: 1px solid var(--bg-vanilla-400);
background: var(--bg-vanilla-100) !important;
@ -234,4 +287,13 @@
}
}
}
.share-modal-content {
.share-link {
.share-url {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
}
}
}
}

View File

@ -1,24 +1,24 @@
/* eslint-disable sonarjs/no-duplicate-string */
import ROUTES from 'constants/routes';
type FiveMin = '5min';
type TenMin = '10min';
type FifteenMin = '15min';
type ThirtyMin = '30min';
type FortyFiveMin = '45min';
type OneMin = '1min';
type ThreeHour = '3hr';
type SixHour = '6hr';
type OneHour = '1hr';
type FourHour = '4hr';
type TwelveHour = '12hr';
type OneDay = '1day';
type ThreeDay = '3days';
type FourDay = '4days';
type TenDay = '10days';
type OneWeek = '1week';
type TwoWeek = '2weeks';
type SixWeek = '6weeks';
type FiveMin = '5m';
type TenMin = '10m';
type FifteenMin = '15m';
type ThirtyMin = '30m';
type FortyFiveMin = '45m';
type OneMin = '1m';
type ThreeHour = '3h';
type SixHour = '6h';
type OneHour = '1h';
type FourHour = '4h';
type TwelveHour = '12h';
type OneDay = '1d';
type ThreeDay = '3d';
type FourDay = '4d';
type TenDay = '10d';
type OneWeek = '1w';
type TwoWeek = '2w';
type SixWeek = '6w';
type TwoMonths = '2months';
type Custom = 'custom';
@ -44,15 +44,19 @@ export type Time =
| TwoWeek
| TwoMonths;
export type TimeUnit = 'm' | 'h' | 'd' | 'w';
export type CustomTimeType = `${string}${TimeUnit}`;
export const Options: Option[] = [
{ value: '5min', label: 'Last 5 minutes' },
{ value: '15min', label: 'Last 15 minutes' },
{ value: '30min', label: 'Last 30 minutes' },
{ value: '1hr', label: 'Last 1 hour' },
{ value: '6hr', label: 'Last 6 hours' },
{ value: '1day', label: 'Last 1 day' },
{ value: '3days', label: 'Last 3 days' },
{ value: '1week', label: 'Last 1 week' },
{ value: '5m', label: 'Last 5 minutes' },
{ value: '15m', label: 'Last 15 minutes' },
{ value: '30m', label: 'Last 30 minutes' },
{ value: '1h', label: 'Last 1 hour' },
{ value: '6h', label: 'Last 6 hours' },
{ value: '1d', label: 'Last 1 day' },
{ value: '3d', label: 'Last 3 days' },
{ value: '1w', label: 'Last 1 week' },
{ value: 'custom', label: 'Custom' },
];
@ -61,36 +65,92 @@ export interface Option {
label: string;
}
export const OLD_RELATIVE_TIME_VALUES = [
'1min',
'10min',
'15min',
'1hr',
'30min',
'45min',
'5min',
'1day',
'3days',
'4days',
'10days',
'1week',
'2weeks',
'6weeks',
'3hr',
'4hr',
'6hr',
'12hr',
];
export const RelativeDurationOptions: Option[] = [
{ value: '5min', label: 'Last 5 minutes' },
{ value: '15min', label: 'Last 15 minutes' },
{ value: '30min', label: 'Last 30 minutes' },
{ value: '1hr', label: 'Last 1 hour' },
{ value: '6hr', label: 'Last 6 hour' },
{ value: '1day', label: 'Last 1 day' },
{ value: '3days', label: 'Last 3 days' },
{ value: '1week', label: 'Last 1 week' },
{ value: '5m', label: 'Last 5 minutes' },
{ value: '15m', label: 'Last 15 minutes' },
{ value: '30m', label: 'Last 30 minutes' },
{ value: '1h', label: 'Last 1 hour' },
{ value: '6h', label: 'Last 6 hour' },
{ value: '1d', label: 'Last 1 day' },
{ value: '3d', label: 'Last 3 days' },
{ value: '1w', label: 'Last 1 week' },
];
export const RelativeDurationSuggestionOptions: Option[] = [
{ value: '3hr', label: '3h' },
{ value: '4days', label: '4d' },
{ value: '6weeks', label: '6w' },
{ value: '12hr', label: '12 hours' },
{ value: '10days', label: '10d' },
{ value: '2weeks', label: '2 weeks' },
{ value: '3h', label: 'Last 3 hours' },
{ value: '4d', label: 'Last 4 days' },
{ value: '6w', label: 'Last 6 weeks' },
{ value: '12h', label: 'Last 12 hours' },
{ value: '10d', label: 'Last 10 days' },
{ value: '2w', label: 'Last 2 weeks' },
{ value: '2months', label: 'Last 2 months' },
{ value: '1day', label: 'today' },
{ value: '1d', label: 'today' },
];
export const FixedDurationSuggestionOptions: Option[] = [
{ value: '45min', label: '45m' },
{ value: '12hr', label: '12 hours' },
{ value: '10days', label: '10d' },
{ value: '2weeks', label: '2 weeks' },
{ value: '45m', label: 'Last 45 mins' },
{ value: '12h', label: 'Last 12 hours' },
{ value: '10d', label: 'Last 10 days' },
{ value: '2w', label: 'Last 2 weeks' },
{ value: '2months', label: 'Last 2 months' },
{ value: '1day', label: 'today' },
{ value: '1d', label: 'today' },
];
export const convertOldTimeToNewValidCustomTimeFormat = (
time: string,
): CustomTimeType => {
const regex = /^(\d+)([a-zA-Z]+)/;
const match = regex.exec(time);
if (match) {
let unit = 'm';
switch (match[2]) {
case 'min':
unit = 'm';
break;
case 'hr':
unit = 'h';
break;
case 'day':
case 'days':
unit = 'd';
break;
case 'week':
case 'weeks':
unit = 'w';
break;
default:
break;
}
return `${match[1]}${unit}` as CustomTimeType;
}
return '30m';
};
export const getDefaultOption = (route: string): Time => {
if (route === ROUTES.SERVICE_MAP) {
return RelativeDurationOptions[2].value;

View File

@ -1,7 +1,8 @@
import './DateTimeSelectionV2.styles.scss';
import { SyncOutlined } from '@ant-design/icons';
import { Button } from 'antd';
import { Color } from '@signozhq/design-tokens';
import { Button, Popover, Switch, Typography } from 'antd';
import getLocalStorageKey from 'api/browser/localstorage/get';
import setLocalStorageKey from 'api/browser/localstorage/set';
import CustomTimePicker from 'components/CustomTimePicker/CustomTimePicker';
@ -26,10 +27,12 @@ import GetMinMax from 'lib/getMinMax';
import getTimeString from 'lib/getTimeString';
import history from 'lib/history';
import { isObject } from 'lodash-es';
import { Check, Copy, Info, Send } from 'lucide-react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useQueryClient } from 'react-query';
import { connect, useSelector } from 'react-redux';
import { RouteComponentProps, withRouter } from 'react-router-dom';
import { useCopyToClipboard } from 'react-use';
import { bindActionCreators, Dispatch } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import { GlobalTimeLoading, UpdateTimeInterval } from 'store/actions';
@ -42,9 +45,12 @@ import { GlobalReducer } from 'types/reducer/globalTime';
import AutoRefresh from '../AutoRefreshV2';
import { DateTimeRangeType } from '../CustomDateTimeModal';
import {
convertOldTimeToNewValidCustomTimeFormat,
CustomTimeType,
getDefaultOption,
getOptions,
LocalStorageTimeRange,
OLD_RELATIVE_TIME_VALUES,
Time,
TimeRange,
} from './config';
@ -66,6 +72,10 @@ function DateTimeSelection({
const searchStartTime = urlQuery.get('startTime');
const searchEndTime = urlQuery.get('endTime');
const queryClient = useQueryClient();
const [enableAbsoluteTime, setEnableAbsoluteTime] = useState(false);
const [isValidteRelativeTime, setIsValidteRelativeTime] = useState(false);
const [, handleCopyToClipboard] = useCopyToClipboard();
const [isURLCopied, setIsURLCopied] = useState(false);
const {
localstorageStartTime,
@ -178,7 +188,7 @@ function DateTimeSelection({
const getInputLabel = (
startTime?: Dayjs,
endTime?: Dayjs,
timeInterval: Time = '15min',
timeInterval: Time | CustomTimeType = '15m',
): string | Time => {
if (startTime && endTime && timeInterval === 'custom') {
const format = 'DD/MM/YYYY HH:mm';
@ -284,28 +294,38 @@ function DateTimeSelection({
[location.pathname],
);
const onSelectHandler = (value: Time): void => {
const onSelectHandler = (value: Time | CustomTimeType): void => {
if (value !== 'custom') {
setIsOpen(false);
updateTimeInterval(value);
updateLocalStorageForRoutes(value);
setIsValidteRelativeTime(true);
if (refreshButtonHidden) {
setRefreshButtonHidden(false);
}
} else {
setRefreshButtonHidden(true);
setCustomDTPickerVisible(true);
setIsValidteRelativeTime(false);
setEnableAbsoluteTime(false);
return;
}
const { maxTime, minTime } = GetMinMax(value, getTime());
if (!isLogsExplorerPage) {
urlQuery.set(QueryParams.startTime, minTime.toString());
urlQuery.set(QueryParams.endTime, maxTime.toString());
urlQuery.delete('startTime');
urlQuery.delete('endTime');
urlQuery.set(QueryParams.relativeTime, value);
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
history.replace(generatedUrl);
}
// For logs explorer - time range handling is managed in useCopyLogLink.ts:52
if (!stagedQuery) {
return;
}
@ -319,18 +339,22 @@ function DateTimeSelection({
};
const onCustomDateHandler = (dateTimeRange: DateTimeRangeType): void => {
// console.log('dateTimeRange', dateTimeRange);
if (dateTimeRange !== null) {
const [startTimeMoment, endTimeMoment] = dateTimeRange;
if (startTimeMoment && endTimeMoment) {
const startTime = startTimeMoment;
const endTime = endTimeMoment;
setCustomDTPickerVisible(false);
updateTimeInterval('custom', [
startTime.toDate().getTime(),
endTime.toDate().getTime(),
]);
setLocalStorageKey('startTime', startTime.toString());
setLocalStorageKey('endTime', endTime.toString());
updateLocalStorageForRoutes(JSON.stringify({ startTime, endTime }));
if (!isLogsExplorerPage) {
@ -339,6 +363,7 @@ function DateTimeSelection({
startTime?.toDate().getTime().toString(),
);
urlQuery.set(QueryParams.endTime, endTime?.toDate().getTime().toString());
urlQuery.delete(QueryParams.relativeTime);
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
history.replace(generatedUrl);
}
@ -346,6 +371,57 @@ function DateTimeSelection({
}
};
const onValidCustomDateHandler = (dateTimeStr: CustomTimeType): void => {
setIsOpen(false);
updateTimeInterval(dateTimeStr);
updateLocalStorageForRoutes(dateTimeStr);
urlQuery.delete('startTime');
urlQuery.delete('endTime');
setIsValidteRelativeTime(true);
const { maxTime, minTime } = GetMinMax(dateTimeStr, getTime());
if (!isLogsExplorerPage) {
urlQuery.delete('startTime');
urlQuery.delete('endTime');
urlQuery.set(QueryParams.relativeTime, dateTimeStr);
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
history.replace(generatedUrl);
}
if (!stagedQuery) {
return;
}
// the second boolean param directs the qb about the time change so to merge the query and retain the current state
initQueryBuilderData(updateStepInterval(stagedQuery, maxTime, minTime), true);
};
const getCustomOrIntervalTime = (
time: Time,
currentRoute: string,
): Time | CustomTimeType => {
if (searchEndTime !== null && searchStartTime !== null) {
return 'custom';
}
if (
(localstorageEndTime === null || localstorageStartTime === null) &&
time === 'custom'
) {
return getDefaultOption(currentRoute);
}
if (OLD_RELATIVE_TIME_VALUES.indexOf(time) > -1) {
return convertOldTimeToNewValidCustomTimeFormat(time);
}
return time;
};
// this is triggred when we change the routes and based on that we are changing the default options
useEffect(() => {
const metricsTimeDuration = getLocalStorageKey(
@ -365,21 +441,9 @@ function DateTimeSelection({
const currentOptions = getOptions(currentRoute);
setOptions(currentOptions);
const getCustomOrIntervalTime = (time: Time): Time => {
if (searchEndTime !== null && searchStartTime !== null) {
return 'custom';
}
if (
(localstorageEndTime === null || localstorageStartTime === null) &&
time === 'custom'
) {
return getDefaultOption(currentRoute);
}
const updatedTime = getCustomOrIntervalTime(time, currentRoute);
return time;
};
const updatedTime = getCustomOrIntervalTime(time);
setIsValidteRelativeTime(updatedTime !== 'custom');
const [preStartTime = 0, preEndTime = 0] = getTime() || [];
@ -388,18 +452,113 @@ function DateTimeSelection({
updateTimeInterval(updatedTime, [preStartTime, preEndTime]);
if (updatedTime !== 'custom') {
const { minTime, maxTime } = GetMinMax(updatedTime);
urlQuery.set(QueryParams.startTime, minTime.toString());
urlQuery.set(QueryParams.endTime, maxTime.toString());
urlQuery.delete('startTime');
urlQuery.delete('endTime');
urlQuery.set(QueryParams.relativeTime, updatedTime);
} else {
urlQuery.set(QueryParams.startTime, preStartTime.toString());
urlQuery.set(QueryParams.endTime, preEndTime.toString());
const startTime = preStartTime.toString();
const endTime = preEndTime.toString();
urlQuery.set(QueryParams.startTime, startTime);
urlQuery.set(QueryParams.endTime, endTime);
}
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
history.replace(generatedUrl);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [location.pathname, updateTimeInterval, globalTimeLoading]);
// eslint-disable-next-line sonarjs/cognitive-complexity
const shareModalContent = (): JSX.Element => {
let currentUrl = window.location.href;
const startTime = urlQuery.get(QueryParams.startTime);
const endTime = urlQuery.get(QueryParams.endTime);
const isCustomTime = !!(startTime && endTime && selectedTime === 'custom');
if (enableAbsoluteTime || isCustomTime) {
if (selectedTime === 'custom') {
if (searchStartTime && searchEndTime) {
urlQuery.set(QueryParams.startTime, searchStartTime.toString());
urlQuery.set(QueryParams.endTime, searchEndTime.toString());
}
} else {
const { minTime, maxTime } = GetMinMax(selectedTime);
urlQuery.set(QueryParams.startTime, minTime.toString());
urlQuery.set(QueryParams.endTime, maxTime.toString());
}
urlQuery.delete(QueryParams.relativeTime);
currentUrl = `${window.location.origin}${
location.pathname
}?${urlQuery.toString()}`;
} else {
urlQuery.delete(QueryParams.startTime);
urlQuery.delete(QueryParams.endTime);
urlQuery.set(QueryParams.relativeTime, selectedTime);
currentUrl = `${window.location.origin}${
location.pathname
}?${urlQuery.toString()}`;
}
return (
<div className="share-modal-content">
<div className="absolute-relative-time-toggler-container">
<div className="absolute-relative-time-toggler">
{(selectedTime === 'custom' || !isValidteRelativeTime) && (
<Info size={14} color={Color.BG_AMBER_600} />
)}
<Switch
checked={enableAbsoluteTime || isCustomTime}
disabled={selectedTime === 'custom' || !isValidteRelativeTime}
size="small"
onChange={(): void => {
setEnableAbsoluteTime(!enableAbsoluteTime);
}}
/>
</div>
<Typography.Text>Enable Absolute Time</Typography.Text>
</div>
{(selectedTime === 'custom' || !isValidteRelativeTime) && (
<div className="absolute-relative-time-error">
Please select / enter valid relative time to toggle.
</div>
)}
<div className="share-link">
<Typography.Text ellipsis className="share-url">
{currentUrl}
</Typography.Text>
<Button
className="periscope-btn copy-url-btn"
onClick={(): void => {
handleCopyToClipboard(currentUrl);
setIsURLCopied(true);
setTimeout(() => {
setIsURLCopied(false);
}, 1000);
}}
icon={
isURLCopied ? (
<Check size={14} color={Color.BG_FOREST_500} />
) : (
<Copy size={14} color={Color.BG_ROBIN_500} />
)
}
/>
</div>
</div>
);
};
return (
<div className="date-time-selector">
{!hasSelectedTimeError && !refreshButtonHidden && (
@ -426,9 +585,12 @@ function DateTimeSelection({
setHasSelectedTimeError(hasError);
}}
selectedTime={selectedTime}
onValidCustomDateChange={(dateTime): void =>
onCustomDateHandler(dateTime as DateTimeRangeType)
}
onValidCustomDateChange={(dateTime): void => {
onValidCustomDateHandler(dateTime.timeStr as CustomTimeType);
}}
onCustomTimeStatusUpdate={(isValid: boolean): void => {
setIsValidteRelativeTime(isValid);
}}
selectedValue={getInputLabel(
dayjs(minTime / 1000000),
dayjs(maxTime / 1000000),
@ -457,6 +619,22 @@ function DateTimeSelection({
</FormItem>
</div>
)}
<Popover
rootClassName="shareable-link-popover-root"
className="shareable-link-popover"
placement="bottomRight"
content={shareModalContent}
arrow={false}
trigger={['hover']}
>
<Button
className="share-link-btn periscope-btn"
icon={<Send size={14} />}
>
Share
</Button>
</Popover>
</FormContainer>
</Form>
</div>
@ -468,7 +646,7 @@ interface DateTimeSelectionV2Props {
}
interface DispatchProps {
updateTimeInterval: (
interval: Time,
interval: Time | CustomTimeType,
dateTimeRange?: [number, number],
) => (dispatch: Dispatch<AppActions>) => void;
globalTimeLoading: () => void;

View File

@ -11,8 +11,11 @@ import {
useMemo,
useState,
} from 'react';
import { useSelector } from 'react-redux';
import { useLocation } from 'react-router-dom';
import { useCopyToClipboard } from 'react-use';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import { HIGHLIGHTED_DELAY } from './configs';
import { LogTimeRange, UseCopyLogLink } from './types';
@ -33,15 +36,30 @@ export const useCopyLogLink = (logId?: string): UseCopyLogLink => {
null,
);
const { selectedTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const onTimeRangeChange = useCallback(
(newTimeRange: LogTimeRange | null): void => {
urlQuery.set(QueryParams.timeRange, JSON.stringify(newTimeRange));
urlQuery.set(QueryParams.startTime, newTimeRange?.start.toString() || '');
urlQuery.set(QueryParams.endTime, newTimeRange?.end.toString() || '');
if (selectedTime !== 'custom') {
urlQuery.delete(QueryParams.startTime);
urlQuery.delete(QueryParams.endTime);
urlQuery.set(QueryParams.relativeTime, selectedTime);
} else {
urlQuery.set(QueryParams.startTime, newTimeRange?.start.toString() || '');
urlQuery.set(QueryParams.endTime, newTimeRange?.end.toString() || '');
urlQuery.delete(QueryParams.relativeTime);
}
const generatedUrl = `${pathname}?${urlQuery.toString()}`;
history.replace(generatedUrl);
},
[pathname, urlQuery],
[pathname, urlQuery, selectedTime],
);
const isActiveLog = useMemo(() => activeLogId === logId, [activeLogId, logId]);

View File

@ -1,7 +1,10 @@
import getService from 'api/metrics/getService';
import { AxiosError } from 'axios';
import { Time } from 'container/TopNav/DateTimeSelection/config';
import { Time as TimeV2 } from 'container/TopNav/DateTimeSelectionV2/config';
import {
CustomTimeType,
Time as TimeV2,
} from 'container/TopNav/DateTimeSelectionV2/config';
import {
QueryKey,
useQuery,
@ -27,7 +30,7 @@ export const useQueryService = ({
interface UseQueryServiceProps {
minTime: number;
maxTime: number;
selectedTime: Time | TimeV2;
selectedTime: Time | TimeV2 | CustomTimeType;
selectedTags: Tags[];
options?: UseQueryOptions<PayloadProps, AxiosError, PayloadProps, QueryKey>;
}

View File

@ -6,7 +6,10 @@ import { getMetricsQueryRange } from 'api/metrics/getQueryRange';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems';
import { Time } from 'container/TopNav/DateTimeSelection/config';
import { Time as TimeV2 } from 'container/TopNav/DateTimeSelectionV2/config';
import {
CustomTimeType,
Time as TimeV2,
} from 'container/TopNav/DateTimeSelectionV2/config';
import { Pagination } from 'hooks/queryPagination';
import { convertNewDataToOld } from 'lib/newQueryBuilder/convertNewDataToOld';
import { isEmpty } from 'lodash-es';
@ -67,7 +70,7 @@ export interface GetQueryResultsProps {
query: Query;
graphType: PANEL_TYPES;
selectedTime: timePreferenceType;
globalSelectedInterval: Time | TimeV2;
globalSelectedInterval: Time | TimeV2 | CustomTimeType;
variables?: Record<string, unknown>;
params?: Record<string, unknown>;
tableParams?: {

View File

@ -1,63 +1,101 @@
import { Time } from 'container/TopNav/DateTimeSelection/config';
import { Time as TimeV2 } from 'container/TopNav/DateTimeSelectionV2/config';
import { isString } from 'lodash-es';
import { GlobalReducer } from 'types/reducer/globalTime';
import getMinAgo from './getStartAndEndTime/getMinAgo';
const validCustomTimeRegex = /^(\d+)([mhdw])$/;
export const isValidTimeFormat = (time: string): boolean =>
validCustomTimeRegex.test(time);
const extractTimeAndUnit = (time: string): { time: number; unit: string } => {
// Match the pattern
const match = /^(\d+)([mhdw])$/.exec(time);
if (match) {
return { time: parseInt(match[1], 10), unit: match[2] };
}
return {
time: 30,
unit: 'm',
};
};
export const getMinTimeForRelativeTimes = (
time: number,
unit: string,
): number => {
switch (unit) {
case 'm':
return getMinAgo({ minutes: 1 * time }).getTime();
case 'h':
return getMinAgo({ minutes: 60 * time }).getTime();
case 'd':
return getMinAgo({ minutes: 24 * 60 * time }).getTime();
case 'w':
return getMinAgo({ minutes: 24 * 60 * 7 * time }).getTime();
default:
return getMinAgo({ minutes: 1 }).getTime();
}
};
const GetMinMax = (
interval: Time | TimeV2,
interval: Time | TimeV2 | string,
dateTimeRange?: [number, number],
// eslint-disable-next-line sonarjs/cognitive-complexity
): GetMinMaxPayload => {
let maxTime = new Date().getTime();
let minTime = 0;
if (interval === '1min') {
if (interval === '1m') {
const minTimeAgo = getMinAgo({ minutes: 1 }).getTime();
minTime = minTimeAgo;
} else if (interval === '10min') {
} else if (interval === '10m') {
const minTimeAgo = getMinAgo({ minutes: 10 }).getTime();
minTime = minTimeAgo;
} else if (interval === '15min') {
} else if (interval === '15m') {
const minTimeAgo = getMinAgo({ minutes: 15 }).getTime();
minTime = minTimeAgo;
} else if (interval === '1hr') {
} else if (interval === '1h') {
const minTimeAgo = getMinAgo({ minutes: 60 }).getTime();
minTime = minTimeAgo;
} else if (interval === '30min') {
} else if (interval === '30m') {
const minTimeAgo = getMinAgo({ minutes: 30 }).getTime();
minTime = minTimeAgo;
} else if (interval === '45min') {
} else if (interval === '45m') {
const minTimeAgo = getMinAgo({ minutes: 45 }).getTime();
minTime = minTimeAgo;
} else if (interval === '5min') {
} else if (interval === '5m') {
const minTimeAgo = getMinAgo({ minutes: 5 }).getTime();
minTime = minTimeAgo;
} else if (interval === '1day') {
} else if (interval === '1d') {
// one day = 24*60(min)
const minTimeAgo = getMinAgo({ minutes: 24 * 60 }).getTime();
minTime = minTimeAgo;
} else if (interval === '3days') {
} else if (interval === '3d') {
// three day = one day * 3
const minTimeAgo = getMinAgo({ minutes: 24 * 60 * 3 }).getTime();
minTime = minTimeAgo;
} else if (interval === '4days') {
} else if (interval === '4d') {
// four day = one day * 4
const minTimeAgo = getMinAgo({ minutes: 24 * 60 * 4 }).getTime();
minTime = minTimeAgo;
} else if (interval === '10days') {
} else if (interval === '10d') {
// ten day = one day * 10
const minTimeAgo = getMinAgo({ minutes: 24 * 60 * 10 }).getTime();
minTime = minTimeAgo;
} else if (interval === '1week') {
} else if (interval === '1w') {
// one week = one day * 7
const minTimeAgo = getMinAgo({ minutes: 24 * 60 * 7 }).getTime();
minTime = minTimeAgo;
} else if (interval === '2weeks') {
} else if (interval === '2w') {
// two week = one day * 14
const minTimeAgo = getMinAgo({ minutes: 24 * 60 * 14 }).getTime();
minTime = minTimeAgo;
} else if (interval === '6weeks') {
} else if (interval === '6w') {
// six week = one day * 42
const minTimeAgo = getMinAgo({ minutes: 24 * 60 * 42 }).getTime();
minTime = minTimeAgo;
@ -65,13 +103,17 @@ const GetMinMax = (
// two months = one day * 60
const minTimeAgo = getMinAgo({ minutes: 24 * 60 * 60 }).getTime();
minTime = minTimeAgo;
} else if (['3hr', '4hr', '6hr', '12hr'].includes(interval)) {
const h = parseInt(interval.replace('hr', ''), 10);
} else if (['3h', '4h', '6h', '12h'].includes(interval)) {
const h = parseInt(interval.replace('h', ''), 10);
const minTimeAgo = getMinAgo({ minutes: h * 60 }).getTime();
minTime = minTimeAgo;
} else if (interval === 'custom') {
maxTime = (dateTimeRange || [])[1] || 0;
minTime = (dateTimeRange || [])[0] || 0;
} else if (isString(interval) && isValidTimeFormat(interval)) {
const { time, unit } = extractTimeAndUnit(interval);
minTime = getMinTimeForRelativeTimes(time, unit);
} else {
throw new Error('invalid time type');
}

View File

@ -1,7 +1,10 @@
import { PANEL_TYPES } from 'constants/queryBuilder';
import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems';
import { Time } from 'container/TopNav/DateTimeSelection/config';
import { Time as TimeV2 } from 'container/TopNav/DateTimeSelectionV2/config';
import {
CustomTimeType,
Time as TimeV2,
} from 'container/TopNav/DateTimeSelectionV2/config';
import store from 'store';
import getMaxMinTime from './getMaxMinTime';
@ -38,7 +41,7 @@ const getStartEndRangeTime = ({
interface GetStartEndRangeTimesProps {
type?: timePreferenceType;
graphType?: PANEL_TYPES | null;
interval?: Time | TimeV2;
interval?: Time | TimeV2 | CustomTimeType;
}
interface GetStartEndRangeTimesPayload {

View File

@ -1,12 +1,15 @@
import { Time } from 'container/TopNav/DateTimeSelection/config';
import { Time as TimeV2 } from 'container/TopNav/DateTimeSelectionV2/config';
import {
CustomTimeType,
Time as TimeV2,
} from 'container/TopNav/DateTimeSelectionV2/config';
import GetMinMax from 'lib/getMinMax';
import { Dispatch } from 'redux';
import AppActions from 'types/actions';
import { UPDATE_TIME_INTERVAL } from 'types/actions/globalTime';
export const UpdateTimeInterval = (
interval: Time | TimeV2,
interval: Time | TimeV2 | CustomTimeType,
dateTimeRange: [number, number] = [0, 0],
): ((dispatch: Dispatch<AppActions>) => void) => (
dispatch: Dispatch<AppActions>,

View File

@ -88,4 +88,7 @@ export const getFilter = (data: GetFilterPayload): TraceReducer['filter'] => {
};
export const stripTimestampsFromQuery = (query: string): string =>
query.replace(/(\?|&)startTime=\d+/, '').replace(/&endTime=\d+/, '');
query
.replace(/(\?|&)startTime=\d+/, '')
.replace(/&endTime=\d+/, '')
.replace(/[?&]relativeTime=[^&]+/g, '');

View File

@ -1,5 +1,8 @@
import { Time } from 'container/TopNav/DateTimeSelection/config';
import { Time as TimeV2 } from 'container/TopNav/DateTimeSelectionV2/config';
import {
CustomTimeType,
Time as TimeV2,
} from 'container/TopNav/DateTimeSelectionV2/config';
import { ResetIdStartAndEnd, SetSearchQueryString } from './logs';
@ -14,7 +17,7 @@ export type GlobalTime = {
};
interface UpdateTime extends GlobalTime {
selectedTime: Time | TimeV2;
selectedTime: Time | TimeV2 | CustomTimeType;
}
interface UpdateTimeInterval {

View File

@ -1,12 +1,15 @@
import { Time } from 'container/TopNav/DateTimeSelection/config';
import { Time as TimeV2 } from 'container/TopNav/DateTimeSelectionV2/config';
import {
CustomTimeType,
Time as TimeV2,
} from 'container/TopNav/DateTimeSelectionV2/config';
import { GlobalTime } from 'types/actions/globalTime';
export interface GlobalReducer {
maxTime: GlobalTime['maxTime'];
minTime: GlobalTime['minTime'];
loading: boolean;
selectedTime: Time | TimeV2;
selectedTime: Time | TimeV2 | CustomTimeType;
isAutoRefreshDisabled: boolean;
selectedAutoRefreshInterval: string;
}