Upstream getsentry/sentry state of the touched files before 7b34c7f5ce

This commit is contained in:
upstream committed 2026-10-17 04:21:20 +02:00
1 parent 42fa6c159c
commit 6909f35d48
1 file changed
+284
@@ -0,0 +1,284 @@
import {Component} from 'react';
import type {Range} from 'react-date-range';
import type {Theme} from '@emotion/react';
import {withTheme} from '@emotion/react';
import styled from '@emotion/styled';
import moment from 'moment-timezone';
import {Checkbox} from '@sentry/scraps/checkbox';
import {DateRangePicker} from 'sentry/components/calendar';
import {MAX_PICKABLE_DAYS} from 'sentry/constants';
import {t} from 'sentry/locale';
import type {PlainRoute} from 'sentry/types/legacyReactRouter';
import type {Organization} from 'sentry/types/organization';
import {trackAnalytics} from 'sentry/utils/analytics';
import {
getEndOfDay,
getStartOfPeriodAgo,
isValidTime,
setDateToTime,
} from 'sentry/utils/dates';
import {getRouteStringFromRoutes} from 'sentry/utils/getRouteStringFromRoutes';
import {useRoutes} from 'sentry/utils/useRoutes';
import {TimePicker} from './timePicker';
const getTimeStringFromDate = (date: Date) => moment(date).local().format('HH:mm');
type ChangeData = {end?: Date; hasDateRangeErrors?: boolean; start?: Date};
const defaultProps = {
showAbsolute: true,
showRelative: false,
/**
* The maximum number of days in the past you can pick
*/
maxPickableDays: MAX_PICKABLE_DAYS,
};
type Props = {
/**
* End date value for absolute date selector
*/
end: Date | null;
/**
* Callback when value changes
*/
onChange: (data: ChangeData) => void;
/**
* handle UTC checkbox change
*/
onChangeUtc: () => void;
/**
* The matched routes, used to derive a parameterized path for analytics.
*/
routes: PlainRoute[];
/**
* Start date value for absolute date selector
*/
start: Date | null;
theme: Theme;
className?: string;
/**
* The largest date range (ie. end date - start date) allowed
*/
maxDateRange?: number;
/**
* Just used for metrics
*/
organization?: Organization;
/**
* Should we have a time selector?
*/
showTimePicker?: boolean;
/**
* Use UTC
*/
utc?: boolean | null;
} & Partial<typeof defaultProps>;
type State = {
hasEndErrors: boolean;
hasStartErrors: boolean;
};
class BaseDateRange extends Component<Props, State> {
static defaultProps = defaultProps;
state: State = {
hasStartErrors: false,
hasEndErrors: false,
};
private readonly utcInputId =
'utc-picker-' + Math.random().toString(36).substring(2, 12);
handleSelectDateRange = (range: Range) => {
const {onChange} = this.props;
const {startDate, endDate} = range;
const end = endDate ? getEndOfDay(endDate) : endDate;
onChange({
start: startDate,
end,
});
};
handleChangeStart = (e: React.ChangeEvent<HTMLInputElement>) => {
// Safari does not support "time" inputs, so we don't have access to
// `e.target.valueAsDate`, must parse as string
//
// Time will be in 24hr e.g. "21:00"
const start = this.props.start ?? '';
const end = this.props.end ?? undefined;
const {onChange, organization, routes} = this.props;
const startTime = e.target.value;
const newStartTime = setDateToTime(start, startTime, {local: true});
if (!startTime || !isValidTime(startTime) || (end && newStartTime > end)) {
this.setState({hasStartErrors: true});
onChange({hasDateRangeErrors: true});
return;
}
trackAnalytics('dateselector.time_changed', {
organization: organization ?? null,
field_changed: 'start',
time: startTime,
path: getRouteStringFromRoutes({routes}),
});
onChange({
start: newStartTime,
end,
hasDateRangeErrors: this.state.hasEndErrors,
});
this.setState({hasStartErrors: false});
};
handleChangeEnd = (e: React.ChangeEvent<HTMLInputElement>) => {
const start = this.props.start ?? undefined;
const end = this.props.end ?? '';
const {organization, onChange, routes} = this.props;
const endTime = e.target.value;
const newEndTime = setDateToTime(end, endTime, {local: true});
if (!endTime || !isValidTime(endTime) || (start && start > newEndTime)) {
this.setState({hasEndErrors: true});
onChange({hasDateRangeErrors: true});
return;
}
trackAnalytics('dateselector.time_changed', {
organization: organization ?? null,
field_changed: 'end',
time: endTime,
path: getRouteStringFromRoutes({routes}),
});
onChange({
start,
end: newEndTime,
hasDateRangeErrors: this.state.hasStartErrors,
});
this.setState({hasEndErrors: false});
};
render() {
const {className, maxPickableDays, utc, showTimePicker, onChangeUtc, maxDateRange} =
this.props;
const {hasStartErrors, hasEndErrors} = this.state;
const start = this.props.start ?? '';
const end = this.props.end ?? '';
const startTime = getTimeStringFromDate(new Date(start));
const endTime = getTimeStringFromDate(new Date(end));
// Restraints on the time range that you can select
// Can't select dates in the future b/c we're not fortune tellers (yet)
//
// We want `maxPickableDays` - 1 (if today is Jan 5, max is 3 days, the minDate should be Jan 3)
// Subtract additional day because we force the end date to be inclusive,
// so when you pick Jan 1 the time becomes Jan 1 @ 23:59:59,
// (or really, Jan 2 @ 00:00:00 - 1 second), while the start time is at 00:00
let minDate = getStartOfPeriodAgo('days', (maxPickableDays ?? MAX_PICKABLE_DAYS) - 1);
let maxDate = new Date();
// if the start and end date are the same, it means the user can still select another date,
// at this point we want to apply the maxDateRange
const startDate = moment(start).local();
const endDate = moment(end).local();
const isSameDay = startDate.isSame(endDate, 'day');
if (maxDateRange && isSameDay) {
minDate = moment(new Date(start)).subtract(maxDateRange, 'days').toDate();
const newMaxDate = moment(new Date(end)).add(maxDateRange, 'days').toDate();
maxDate = newMaxDate > maxDate ? maxDate : newMaxDate;
}
return (
<div className={className} data-test-id="date-range">
<DateRangePicker
startDate={startDate.toDate()}
endDate={endDate.toDate()}
minDate={minDate}
maxDate={maxDate}
onChange={this.handleSelectDateRange}
/>
{showTimePicker && (
<TimeAndUtcPicker>
<StyledTimePicker
start={startTime}
end={endTime}
onChangeStart={this.handleChangeStart}
onChangeEnd={this.handleChangeEnd}
hasStartErrors={hasStartErrors}
hasEndErrors={hasEndErrors}
/>
<UtcPicker>
<Checkbox
onChange={onChangeUtc}
checked={utc || false}
id={this.utcInputId}
/>
<UtcPickerLabel htmlFor={this.utcInputId}>{t('UTC')}</UtcPickerLabel>
</UtcPicker>
</TimeAndUtcPicker>
)}
</div>
);
}
}
function WithRoutes(props: Omit<Props, 'routes'>) {
const routes = useRoutes();
return <BaseDateRange {...props} routes={routes} />;
}
export const DateRange = styled(withTheme(WithRoutes))`
display: flex;
flex-direction: column;
border-left: 1px solid ${p => p.theme.tokens.border.primary};
`;
const TimeAndUtcPicker = styled('div')`
display: flex;
align-items: center;
margin: 0 ${p => p.theme.space.xl};
padding: ${p => p.theme.space.xs} 0;
border-top: 1px solid ${p => p.theme.tokens.border.secondary};
`;
const StyledTimePicker = styled(TimePicker)`
&& {
margin-left: 0;
}
`;
const UtcPicker = styled('div')`
color: ${p => p.theme.tokens.content.secondary};
white-space: nowrap;
display: flex;
align-items: center;
justify-content: flex-end;
flex: 1;
gap: ${p => p.theme.space.xs};
`;
const UtcPickerLabel = styled('label')`
margin: 0;
font-weight: ${p => p.theme.font.weight.sans.regular};
color: inherit;
`;