mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-14 21:57:22 +08:00
feat: custom date time value (#4367)
* feat: custom date time value * fix: update custom date picker * fix: old placeholder value flicker * fix: html semantics and move styles to css * fix: remove console logs --------- Co-authored-by: Vikrant Gupta <vikrant.thomso@gmail.com>
This commit is contained in:
parent
cbf150ef7b
commit
739b1bf387
@ -0,0 +1,88 @@
|
|||||||
|
.time-options-container {
|
||||||
|
.time-options-item {
|
||||||
|
margin: 2px 0;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 2px;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background-color: rgba($color: #000000, $alpha: 0.2);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: rgba($color: #000000, $alpha: 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: rgba($color: #000000, $alpha: 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-selection-dropdown-content {
|
||||||
|
min-width: 172px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeSelection-input {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 4px 8px;
|
||||||
|
padding-left: 0px !important;
|
||||||
|
|
||||||
|
input::placeholder {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus::placeholder {
|
||||||
|
color: rgba($color: #ffffff, $alpha: 0.4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.valid-format-error {
|
||||||
|
margin-top: 4px;
|
||||||
|
color: var(--bg-cherry-400, #ea6d71);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.time-options-container {
|
||||||
|
.time-options-item {
|
||||||
|
&.active {
|
||||||
|
background-color: rgba($color: #ffffff, $alpha: 0.2);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: rgba($color: #ffffff, $alpha: 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: rgba($color: #ffffff, $alpha: 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeSelection-input {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 4px 8px;
|
||||||
|
padding-left: 0px !important;
|
||||||
|
|
||||||
|
input::placeholder {
|
||||||
|
color: var(---bg-ink-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus::placeholder {
|
||||||
|
color: rgba($color: #000000, $alpha: 0.4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.valid-format-error {
|
||||||
|
margin-top: 4px;
|
||||||
|
color: var(--bg-cherry-400, #ea6d71);
|
||||||
|
}
|
||||||
|
}
|
208
frontend/src/components/CustomTimePicker/CustomTimePicker.tsx
Normal file
208
frontend/src/components/CustomTimePicker/CustomTimePicker.tsx
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||||
|
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||||
|
import './CustomTimePicker.styles.scss';
|
||||||
|
|
||||||
|
import { Input, Popover, Tooltip } from 'antd';
|
||||||
|
import cx from 'classnames';
|
||||||
|
import { Options } from 'container/TopNav/DateTimeSelection/config';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import debounce from 'lodash-es/debounce';
|
||||||
|
import { CheckCircle, ChevronDown, Clock } from 'lucide-react';
|
||||||
|
import { ChangeEvent, useEffect, useState } from 'react';
|
||||||
|
import { popupContainer } from 'utils/selectPopupContainer';
|
||||||
|
|
||||||
|
interface CustomTimePickerProps {
|
||||||
|
onSelect: (value: string) => void;
|
||||||
|
items: any[];
|
||||||
|
selectedValue: string;
|
||||||
|
selectedTime: string;
|
||||||
|
onValidCustomDateChange: ([t1, t2]: any[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function CustomTimePicker({
|
||||||
|
onSelect,
|
||||||
|
items,
|
||||||
|
selectedValue,
|
||||||
|
selectedTime,
|
||||||
|
onValidCustomDateChange,
|
||||||
|
}: CustomTimePickerProps): JSX.Element {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [
|
||||||
|
selectedTimePlaceholderValue,
|
||||||
|
setSelectedTimePlaceholderValue,
|
||||||
|
] = useState('Select / Enter Time Range');
|
||||||
|
|
||||||
|
const [inputValue, setInputValue] = useState('');
|
||||||
|
const [inputStatus, setInputStatus] = useState<'' | 'error' | 'success'>('');
|
||||||
|
const [isInputFocused, setIsInputFocused] = useState(false);
|
||||||
|
|
||||||
|
const getSelectedTimeRangeLabel = (
|
||||||
|
selectedTime: string,
|
||||||
|
selectedTimeValue: string,
|
||||||
|
): string => {
|
||||||
|
if (selectedTime === 'custom') {
|
||||||
|
return selectedTimeValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let index = 0; index < Options.length; index++) {
|
||||||
|
if (Options[index].value === selectedTime) {
|
||||||
|
return Options[index].label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const value = getSelectedTimeRangeLabel(selectedTime, selectedValue);
|
||||||
|
|
||||||
|
setSelectedTimePlaceholderValue(value);
|
||||||
|
}, [selectedTime, selectedValue]);
|
||||||
|
|
||||||
|
const hide = (): void => {
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOpenChange = (newOpen: boolean): void => {
|
||||||
|
setOpen(newOpen);
|
||||||
|
};
|
||||||
|
|
||||||
|
const debouncedHandleInputChange = debounce((inputValue): void => {
|
||||||
|
const isValidFormat = /^(\d+)([mhdw])$/.test(inputValue);
|
||||||
|
if (isValidFormat) {
|
||||||
|
setInputStatus('success');
|
||||||
|
|
||||||
|
const match = inputValue.match(/^(\d+)([mhdw])$/);
|
||||||
|
|
||||||
|
const value = parseInt(match[1], 10);
|
||||||
|
const unit = match[2];
|
||||||
|
|
||||||
|
const currentTime = dayjs();
|
||||||
|
let minTime = null;
|
||||||
|
|
||||||
|
switch (unit) {
|
||||||
|
case 'm':
|
||||||
|
minTime = currentTime.subtract(value, 'minute');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'h':
|
||||||
|
minTime = currentTime.subtract(value, 'hour');
|
||||||
|
break;
|
||||||
|
case 'd':
|
||||||
|
minTime = currentTime.subtract(value, 'day');
|
||||||
|
break;
|
||||||
|
case 'w':
|
||||||
|
minTime = currentTime.subtract(value, 'week');
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
onValidCustomDateChange([minTime, currentTime]);
|
||||||
|
} else {
|
||||||
|
setInputStatus('error');
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
|
||||||
|
const handleInputChange = (event: ChangeEvent<HTMLInputElement>): void => {
|
||||||
|
const inputValue = event.target.value;
|
||||||
|
|
||||||
|
if (inputValue.length > 0) {
|
||||||
|
setOpen(false);
|
||||||
|
} else {
|
||||||
|
setOpen(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
setInputValue(inputValue);
|
||||||
|
|
||||||
|
// Call the debounced function with the input value
|
||||||
|
debouncedHandleInputChange(inputValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
const content = (
|
||||||
|
<div className="time-selection-dropdown-content">
|
||||||
|
<div className="time-options-container">
|
||||||
|
{items.map(({ value, label }) => (
|
||||||
|
<div
|
||||||
|
onClick={(): void => {
|
||||||
|
onSelect(value);
|
||||||
|
setSelectedTimePlaceholderValue(label);
|
||||||
|
setInputStatus('');
|
||||||
|
setInputValue('');
|
||||||
|
hide();
|
||||||
|
}}
|
||||||
|
key={value}
|
||||||
|
className={cx(
|
||||||
|
'time-options-item',
|
||||||
|
selectedValue === value ? 'active' : '',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleFocus = (): void => {
|
||||||
|
setIsInputFocused(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBlur = (): void => {
|
||||||
|
setIsInputFocused(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover
|
||||||
|
placement="bottomRight"
|
||||||
|
getPopupContainer={popupContainer}
|
||||||
|
content={content}
|
||||||
|
arrow={false}
|
||||||
|
open={open}
|
||||||
|
onOpenChange={handleOpenChange}
|
||||||
|
trigger={['click']}
|
||||||
|
style={{
|
||||||
|
padding: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
className="timeSelection-input"
|
||||||
|
type="text"
|
||||||
|
style={{
|
||||||
|
minWidth: '120px',
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
|
status={inputValue && inputStatus === 'error' ? 'error' : ''}
|
||||||
|
allowClear={!isInputFocused && selectedTime === 'custom'}
|
||||||
|
placeholder={
|
||||||
|
isInputFocused
|
||||||
|
? 'Time Format (1m or 2h or 3d or 4w)'
|
||||||
|
: selectedTimePlaceholderValue
|
||||||
|
}
|
||||||
|
value={inputValue}
|
||||||
|
onFocus={handleFocus}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
prefix={
|
||||||
|
inputValue && inputStatus === 'success' ? (
|
||||||
|
<CheckCircle size={14} color="#51E7A8" />
|
||||||
|
) : (
|
||||||
|
<Tooltip title="Enter time in format (e.g., 1m, 2h, 2d, 4w)">
|
||||||
|
<Clock size={14} />
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
suffix={
|
||||||
|
<ChevronDown
|
||||||
|
size={14}
|
||||||
|
onClick={(): void => {
|
||||||
|
setOpen(!open);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CustomTimePicker;
|
@ -1,7 +1,8 @@
|
|||||||
import { SyncOutlined } from '@ant-design/icons';
|
import { SyncOutlined } from '@ant-design/icons';
|
||||||
import { Button, Select as DefaultSelect } from 'antd';
|
import { Button } from 'antd';
|
||||||
import getLocalStorageKey from 'api/browser/localstorage/get';
|
import getLocalStorageKey from 'api/browser/localstorage/get';
|
||||||
import setLocalStorageKey from 'api/browser/localstorage/set';
|
import setLocalStorageKey from 'api/browser/localstorage/set';
|
||||||
|
import CustomTimePicker from 'components/CustomTimePicker/CustomTimePicker';
|
||||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||||
import { QueryParams } from 'constants/query';
|
import { QueryParams } from 'constants/query';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
@ -21,7 +22,6 @@ import { GlobalTimeLoading, UpdateTimeInterval } from 'store/actions';
|
|||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
import AppActions from 'types/actions';
|
import AppActions from 'types/actions';
|
||||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
import { popupContainer } from 'utils/selectPopupContainer';
|
|
||||||
|
|
||||||
import AutoRefresh from '../AutoRefresh';
|
import AutoRefresh from '../AutoRefresh';
|
||||||
import CustomDateTimeModal, { DateTimeRangeType } from '../CustomDateTimeModal';
|
import CustomDateTimeModal, { DateTimeRangeType } from '../CustomDateTimeModal';
|
||||||
@ -29,8 +29,6 @@ import { getDefaultOption, getOptions, Time } from './config';
|
|||||||
import RefreshText from './Refresh';
|
import RefreshText from './Refresh';
|
||||||
import { Form, FormContainer, FormItem } from './styles';
|
import { Form, FormContainer, FormItem } from './styles';
|
||||||
|
|
||||||
const { Option } = DefaultSelect;
|
|
||||||
|
|
||||||
function DateTimeSelection({
|
function DateTimeSelection({
|
||||||
location,
|
location,
|
||||||
updateTimeInterval,
|
updateTimeInterval,
|
||||||
@ -211,6 +209,7 @@ function DateTimeSelection({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onCustomDateHandler = (dateTimeRange: DateTimeRangeType): void => {
|
const onCustomDateHandler = (dateTimeRange: DateTimeRangeType): void => {
|
||||||
|
console.log('dateTimeRange', dateTimeRange);
|
||||||
if (dateTimeRange !== null) {
|
if (dateTimeRange !== null) {
|
||||||
const [startTimeMoment, endTimeMoment] = dateTimeRange;
|
const [startTimeMoment, endTimeMoment] = dateTimeRange;
|
||||||
if (startTimeMoment && endTimeMoment) {
|
if (startTimeMoment && endTimeMoment) {
|
||||||
@ -289,26 +288,22 @@ function DateTimeSelection({
|
|||||||
initialValues={{ interval: selectedTime }}
|
initialValues={{ interval: selectedTime }}
|
||||||
>
|
>
|
||||||
<FormContainer>
|
<FormContainer>
|
||||||
<DefaultSelect
|
<CustomTimePicker
|
||||||
getPopupContainer={popupContainer}
|
onSelect={(value: unknown): void => {
|
||||||
onSelect={(value: unknown): void => onSelectHandler(value as Time)}
|
onSelectHandler(value as Time);
|
||||||
value={getInputLabel(
|
}}
|
||||||
|
selectedTime={selectedTime}
|
||||||
|
onValidCustomDateChange={(dateTime): void =>
|
||||||
|
onCustomDateHandler(dateTime as DateTimeRangeType)
|
||||||
|
}
|
||||||
|
selectedValue={getInputLabel(
|
||||||
dayjs(minTime / 1000000),
|
dayjs(minTime / 1000000),
|
||||||
dayjs(maxTime / 1000000),
|
dayjs(maxTime / 1000000),
|
||||||
selectedTime,
|
selectedTime,
|
||||||
)}
|
)}
|
||||||
data-testid="dropDown"
|
data-testid="dropDown"
|
||||||
style={{
|
items={options}
|
||||||
minWidth: 120,
|
/>
|
||||||
}}
|
|
||||||
listHeight={400}
|
|
||||||
>
|
|
||||||
{options.map(({ value, label }) => (
|
|
||||||
<Option key={value + label} value={value}>
|
|
||||||
{label}
|
|
||||||
</Option>
|
|
||||||
))}
|
|
||||||
</DefaultSelect>
|
|
||||||
|
|
||||||
<FormItem hidden={refreshButtonHidden}>
|
<FormItem hidden={refreshButtonHidden}>
|
||||||
<Button
|
<Button
|
||||||
|
@ -130,3 +130,9 @@ body {
|
|||||||
--tw-bg-opacity: 1;
|
--tw-bg-opacity: 1;
|
||||||
background-color: rgba(236, 236, 241, var(--tw-bg-opacity));
|
background-color: rgba(236, 236, 241, var(--tw-bg-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.flexBtn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user