Upstream getsentry/sentry state of the touched files before 7b34c7f5ce
This commit is contained in:
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;
|
||||
`;
|
||||
Reference in new issue
Block a user