import {useEffect, useRef, useState} from 'react';
import styled from '@emotion/styled';
import format from 'date-fns/format';
import type {Moment} from 'moment';
import {Overlay} from 'sentry/components/overlay';
import {t} from 'sentry/locale';
import space from 'sentry/styles/space';
import {DEFAULT_DAY_START_TIME, setDateToTime} from 'sentry/utils/dates';
import {DatePicker} from '../calendar';
import Checkbox from '../checkbox';
type SearchBarDatePickerProps = {
handleSelectDateTime: (value: string) => void;
date?: Moment;
dateString?: string;
};
type TimeInputProps = {
setTime: (newTime: string) => void;
time: string;
};
const TZ_OFFSET_REGEX = /[+-]\d\d:\d\d$/;
const TIME_REGEX = /T\d\d:\d\d:\d\d/;
const ISO_FORMAT = "yyyy-MM-dd'T'HH:mm:ss";
const ISO_FORMAT_WITH_TIMEZONE = ISO_FORMAT + 'xxx';
const isUtcIsoDate = (isoDateTime?: string) => {
if (!isoDateTime) {
return false;
}
return !TZ_OFFSET_REGEX.test(isoDateTime);
};
const applyChanges = ({
date = new Date(),
timeString = DEFAULT_DAY_START_TIME,
handleSelectDateTime,
utc = false,
}: {
handleSelectDateTime: (isoDateString: string) => void;
date?: Date;
timeString?: string;
utc?: boolean;
}) => {
const newDate = setDateToTime(date, timeString, {local: true});
handleSelectDateTime(format(newDate, utc ? ISO_FORMAT : ISO_FORMAT_WITH_TIMEZONE));
};
const parseIncomingDateString = (incomingDateString?: string) => {
if (!incomingDateString) {
return undefined;
}
// For consistent date parsing, remove timezone from the incoming date string
const strippedTimeZone = incomingDateString
.replace(TZ_OFFSET_REGEX, '')
.replace(/Z$/, '');
if (TIME_REGEX.test(incomingDateString)) {
return new Date(strippedTimeZone);
}
return new Date(strippedTimeZone + 'T00:00:00');
};
const SearchBarDatePicker = ({
dateString,
handleSelectDateTime,
}: SearchBarDatePickerProps) => {
const incomingDate = parseIncomingDateString(dateString);
const time = incomingDate ? format(incomingDate, 'HH:mm:ss') : DEFAULT_DAY_START_TIME;
const utc = isUtcIsoDate(dateString);
return (
e.stopPropagation()}
data-test-id="search-bar-date-picker"
>
{
if (newDate instanceof Date) {
applyChanges({
date: newDate,
timeString: time,
utc,
handleSelectDateTime,
});
}
}}
/>
{
applyChanges({
date: incomingDate,
timeString: newTime,
utc,
handleSelectDateTime,
});
}}
/>
{t('Use UTC')}
{
applyChanges({
date: incomingDate,
timeString: time,
utc: e.target.checked,
handleSelectDateTime,
});
}}
checked={utc}
/>
);
};
/**
* This component keeps track of its own state because updates bring focus
* back to the search bar. We make sure to keep focus within the input
* until the user is done making changes.
*/
const TimeInput = ({time, setTime}: TimeInputProps) => {
const [localTime, setLocalTime] = useState(time);
const [isFocused, setIsFocused] = useState(false);
const timeInputRef = useRef(null);
useEffect(() => {
setLocalTime(time);
}, [time]);
return (
) => {
const newStartTime = e.target.value || DEFAULT_DAY_START_TIME;
setLocalTime(newStartTime);
if (!isFocused) {
setTime(newStartTime);
}
}}
onBlur={() => {
setIsFocused(false);
setTime(localTime);
}}
onFocus={() => setIsFocused(true)}
onKeyDown={e => {
if (e.key === 'Enter') {
timeInputRef.current?.blur();
}
}}
onClick={e => {
e.stopPropagation();
}}
value={localTime}
step={1}
/>
);
};
const SearchBarDatePickerOverlay = styled(Overlay)`
position: absolute;
top: 100%;
left: -1px;
overflow: hidden;
margin-top: ${space(1)};
`;
const Input = styled('input')`
border-radius: 4px;
padding: 0 ${space(1)};
background: ${p => p.theme.backgroundSecondary};
border: 1px solid ${p => p.theme.border};
color: ${p => p.theme.gray300};
box-shadow: none;
`;
const DatePickerFooter = styled('div')`
display: flex;
align-items: center;
justify-content: space-between;
padding: ${space(1)} ${space(3)} ${space(3)} ${space(3)};
`;
const UtcPickerLabel = styled('label')`
color: ${p => p.theme.gray300};
white-space: nowrap;
display: flex;
align-items: center;
justify-content: flex-end;
margin: 0;
font-weight: normal;
user-select: none;
cursor: pointer;
input {
margin: 0 0 0 0.5em;
cursor: pointer;
}
`;
export default SearchBarDatePicker;