mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-14 11:55:52 +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 { Button, Select as DefaultSelect } from 'antd';
|
||||
import { Button } from 'antd';
|
||||
import getLocalStorageKey from 'api/browser/localstorage/get';
|
||||
import setLocalStorageKey from 'api/browser/localstorage/set';
|
||||
import CustomTimePicker from 'components/CustomTimePicker/CustomTimePicker';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import ROUTES from 'constants/routes';
|
||||
@ -21,7 +22,6 @@ import { GlobalTimeLoading, UpdateTimeInterval } from 'store/actions';
|
||||
import { AppState } from 'store/reducers';
|
||||
import AppActions from 'types/actions';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
|
||||
import AutoRefresh from '../AutoRefresh';
|
||||
import CustomDateTimeModal, { DateTimeRangeType } from '../CustomDateTimeModal';
|
||||
@ -29,8 +29,6 @@ import { getDefaultOption, getOptions, Time } from './config';
|
||||
import RefreshText from './Refresh';
|
||||
import { Form, FormContainer, FormItem } from './styles';
|
||||
|
||||
const { Option } = DefaultSelect;
|
||||
|
||||
function DateTimeSelection({
|
||||
location,
|
||||
updateTimeInterval,
|
||||
@ -211,6 +209,7 @@ function DateTimeSelection({
|
||||
};
|
||||
|
||||
const onCustomDateHandler = (dateTimeRange: DateTimeRangeType): void => {
|
||||
console.log('dateTimeRange', dateTimeRange);
|
||||
if (dateTimeRange !== null) {
|
||||
const [startTimeMoment, endTimeMoment] = dateTimeRange;
|
||||
if (startTimeMoment && endTimeMoment) {
|
||||
@ -289,26 +288,22 @@ function DateTimeSelection({
|
||||
initialValues={{ interval: selectedTime }}
|
||||
>
|
||||
<FormContainer>
|
||||
<DefaultSelect
|
||||
getPopupContainer={popupContainer}
|
||||
onSelect={(value: unknown): void => onSelectHandler(value as Time)}
|
||||
value={getInputLabel(
|
||||
<CustomTimePicker
|
||||
onSelect={(value: unknown): void => {
|
||||
onSelectHandler(value as Time);
|
||||
}}
|
||||
selectedTime={selectedTime}
|
||||
onValidCustomDateChange={(dateTime): void =>
|
||||
onCustomDateHandler(dateTime as DateTimeRangeType)
|
||||
}
|
||||
selectedValue={getInputLabel(
|
||||
dayjs(minTime / 1000000),
|
||||
dayjs(maxTime / 1000000),
|
||||
selectedTime,
|
||||
)}
|
||||
data-testid="dropDown"
|
||||
style={{
|
||||
minWidth: 120,
|
||||
}}
|
||||
listHeight={400}
|
||||
>
|
||||
{options.map(({ value, label }) => (
|
||||
<Option key={value + label} value={value}>
|
||||
{label}
|
||||
</Option>
|
||||
))}
|
||||
</DefaultSelect>
|
||||
items={options}
|
||||
/>
|
||||
|
||||
<FormItem hidden={refreshButtonHidden}>
|
||||
<Button
|
||||
|
@ -130,3 +130,9 @@ body {
|
||||
--tw-bg-opacity: 1;
|
||||
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