123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539 |
- import * as React from 'react';
- import {withRouter, WithRouterProps} from 'react-router';
- import {ClassNames} from '@emotion/react';
- import styled from '@emotion/styled';
- import DropdownAutoComplete from 'sentry/components/dropdownAutoComplete';
- import {Item} from 'sentry/components/dropdownAutoComplete/types';
- import {GetActorPropsFn} from 'sentry/components/dropdownMenu';
- import HookOrDefault from 'sentry/components/hookOrDefault';
- import HeaderItem from 'sentry/components/organizations/headerItem';
- import MultipleSelectorSubmitRow from 'sentry/components/organizations/multipleSelectorSubmitRow';
- import PageFilterPinButton from 'sentry/components/organizations/pageFilters/pageFilterPinButton';
- import DateRange from 'sentry/components/organizations/timeRangeSelector/dateRange';
- import DateSummary from 'sentry/components/organizations/timeRangeSelector/dateSummary';
- import {getRelativeSummary} from 'sentry/components/organizations/timeRangeSelector/utils';
- import {DEFAULT_STATS_PERIOD} from 'sentry/constants';
- import {IconCalendar} from 'sentry/icons';
- import {t} from 'sentry/locale';
- import space from 'sentry/styles/space';
- import {DateString, Organization} from 'sentry/types';
- import {defined} from 'sentry/utils';
- import {analytics} from 'sentry/utils/analytics';
- import {
- getDateWithTimezoneInUtc,
- getInternalDate,
- getLocalToSystem,
- getPeriodAgo,
- getUserTimezone,
- getUtcToSystem,
- parsePeriodToHours,
- } from 'sentry/utils/dates';
- import getDynamicText from 'sentry/utils/getDynamicText';
- import getRouteStringFromRoutes from 'sentry/utils/getRouteStringFromRoutes';
- import SelectorItems from './selectorItems';
- const DateRangeHook = HookOrDefault({
- hookName: 'component:header-date-range',
- defaultComponent: DateRange,
- });
- const SelectorItemsHook = HookOrDefault({
- hookName: 'component:header-selector-items',
- defaultComponent: SelectorItems,
- });
- export type ChangeData = {
- relative: string | null;
- end?: Date;
- start?: Date;
- utc?: boolean | null;
- };
- type DateRangeChangeData = Parameters<
- React.ComponentProps<typeof DateRange>['onChange']
- >[0];
- const defaultProps = {
-
- showAbsolute: true,
-
- showRelative: true,
-
- defaultPeriod: DEFAULT_STATS_PERIOD,
-
- onChange: (() => {}) as (data: ChangeData) => void,
- };
- type Props = WithRouterProps & {
-
- end: DateString;
-
- onUpdate: (data: ChangeData) => void;
-
- organization: Organization;
-
- relative: string | null;
-
- start: DateString;
-
- utc: boolean | null;
-
- alignDropdown?: 'left' | 'right';
-
- customDropdownButton?: (config: {
- getActorProps: GetActorPropsFn;
- isOpen: boolean;
- }) => React.ReactElement;
-
- defaultAbsolute?: {end?: Date; start?: Date};
-
- detached?: boolean;
-
- hint?: string;
-
- label?: React.ReactNode;
-
- maxPickableDays?: number;
-
- onToggleSelector?: (isOpen: boolean) => void;
-
- relativeOptions?: Record<string, React.ReactNode>;
- } & Partial<typeof defaultProps>;
- type State = {
- hasChanges: boolean;
- hasDateRangeErrors: boolean;
- isOpen: boolean;
- relative: string | null;
- end?: Date;
- start?: Date;
- utc?: boolean | null;
- };
- class TimeRangeSelector extends React.PureComponent<Props, State> {
- static defaultProps = defaultProps;
- constructor(props: Props) {
- super(props);
- let start: Date | undefined = undefined;
- let end: Date | undefined = undefined;
- if (props.start && props.end) {
- start = getInternalDate(props.start, props.utc);
- end = getInternalDate(props.end, props.utc);
- }
- this.state = {
-
-
- utc: defined(props.utc) ? props.utc : getUserTimezone() === 'UTC',
- isOpen: false,
- hasChanges: false,
- hasDateRangeErrors: false,
- start,
- end,
- relative: props.relative,
- };
- }
- componentDidUpdate(_prevProps, prevState) {
- const {onToggleSelector} = this.props;
- const currState = this.state;
- if (onToggleSelector && prevState.isOpen !== currState.isOpen) {
- onToggleSelector(currState.isOpen);
- }
- }
- callCallback = (callback: Props['onChange'], datetime: ChangeData) => {
- if (typeof callback !== 'function') {
- return;
- }
- if (!datetime.start && !datetime.end) {
- callback(datetime);
- return;
- }
-
- callback({
- ...datetime,
- start: getDateWithTimezoneInUtc(datetime.start, this.state.utc),
- end: getDateWithTimezoneInUtc(datetime.end, this.state.utc),
- });
- };
- handleCloseMenu = () => {
- const {relative, start, end, utc} = this.state;
- if (this.state.hasChanges) {
-
- this.handleUpdate({relative, start, end, utc});
- } else {
- this.setState({isOpen: false});
- }
- };
- handleUpdate = (datetime: ChangeData) => {
- const {onUpdate} = this.props;
- this.setState(
- {
- isOpen: false,
- hasChanges: false,
- },
- () => {
- this.callCallback(onUpdate, datetime);
- }
- );
- };
- handleSelect = (item: Item) => {
- if (item.value === 'absolute') {
- this.handleAbsoluteClick();
- return;
- }
- this.handleSelectRelative(item.value);
- };
- handleAbsoluteClick = () => {
- const {relative, onChange, defaultPeriod, defaultAbsolute} = this.props;
-
-
- const newDateTime: ChangeData = {
- relative: null,
- start: defaultAbsolute?.start
- ? defaultAbsolute.start
- : getPeriodAgo(
- 'hours',
- parsePeriodToHours(relative || defaultPeriod || DEFAULT_STATS_PERIOD)
- ).toDate(),
- end: defaultAbsolute?.end ? defaultAbsolute.end : new Date(),
- };
- if (defined(this.props.utc)) {
- newDateTime.utc = this.state.utc;
- }
- this.setState({
- hasChanges: true,
- ...newDateTime,
- start: newDateTime.start,
- end: newDateTime.end,
- });
- this.callCallback(onChange, newDateTime);
- };
- handleSelectRelative = (value: string) => {
- const {onChange} = this.props;
- const newDateTime: ChangeData = {
- relative: value,
- start: undefined,
- end: undefined,
- };
- this.setState(newDateTime);
- this.callCallback(onChange, newDateTime);
- this.handleUpdate(newDateTime);
- };
- handleClear = () => {
- const {onChange, defaultPeriod} = this.props;
- const newDateTime: ChangeData = {
- relative: defaultPeriod || DEFAULT_STATS_PERIOD,
- start: undefined,
- end: undefined,
- utc: null,
- };
- this.setState(newDateTime);
- this.callCallback(onChange, newDateTime);
- this.handleUpdate(newDateTime);
- };
- handleSelectDateRange = ({
- start,
- end,
- hasDateRangeErrors = false,
- }: DateRangeChangeData) => {
- if (hasDateRangeErrors) {
- this.setState({hasDateRangeErrors});
- return;
- }
- const {onChange} = this.props;
- const newDateTime: ChangeData = {
- relative: null,
- start,
- end,
- };
- if (defined(this.props.utc)) {
- newDateTime.utc = this.state.utc;
- }
- this.setState({hasChanges: true, hasDateRangeErrors, ...newDateTime});
- this.callCallback(onChange, newDateTime);
- };
- handleUseUtc = () => {
- const {onChange, router} = this.props;
- let {start, end} = this.props;
- this.setState(state => {
- const utc = !state.utc;
- if (!start) {
- start = getDateWithTimezoneInUtc(state.start, state.utc);
- }
- if (!end) {
- end = getDateWithTimezoneInUtc(state.end, state.utc);
- }
- analytics('dateselector.utc_changed', {
- utc,
- path: getRouteStringFromRoutes(router.routes),
- org_id: parseInt(this.props.organization.id, 10),
- });
- const newDateTime = {
- relative: null,
- start: utc ? getLocalToSystem(start) : getUtcToSystem(start),
- end: utc ? getLocalToSystem(end) : getUtcToSystem(end),
- utc,
- };
- this.callCallback(onChange, newDateTime);
- return {
- hasChanges: true,
- ...newDateTime,
- };
- });
- };
- handleOpen = () => {
- this.setState({isOpen: true});
-
- import('../timeRangeSelector/dateRange/index');
- };
- render() {
- const {
- defaultPeriod,
- showAbsolute,
- showRelative,
- organization,
- hint,
- label,
- relativeOptions,
- maxPickableDays,
- customDropdownButton,
- detached,
- alignDropdown,
- } = this.props;
- const {start, end, relative} = this.state;
- const hasNewPageFilters = organization.features.includes('selection-filters-v2');
- const shouldShowAbsolute = showAbsolute;
- const shouldShowRelative = showRelative;
- const isAbsoluteSelected = !!start && !!end;
- const summary =
- isAbsoluteSelected && start && end ? (
- <DateSummary start={start} end={end} />
- ) : (
- getRelativeSummary(
- relative || defaultPeriod || DEFAULT_STATS_PERIOD,
- relativeOptions
- )
- );
- return (
- <SelectorItemsHook
- shouldShowAbsolute={shouldShowAbsolute}
- shouldShowRelative={shouldShowRelative}
- relativePeriods={relativeOptions}
- handleSelectRelative={this.handleSelectRelative}
- >
- {items => (
- <ClassNames>
- {({css}) => (
- <StyledDropdownAutoComplete
- allowActorToggle
- alignMenu={alignDropdown ?? (isAbsoluteSelected ? 'right' : 'left')}
- isOpen={this.state.isOpen}
- onOpen={this.handleOpen}
- onClose={this.handleCloseMenu}
- hideInput={!shouldShowRelative}
- closeOnSelect={false}
- blendCorner={false}
- maxHeight={400}
- detached={detached}
- items={items}
- searchPlaceholder={t('Filter time range')}
- rootClassName={css`
- position: relative;
- display: flex;
- height: 100%;
- `}
- inputActions={
- hasNewPageFilters ? (
- <StyledPinButton size="xsmall" filter="datetime" />
- ) : undefined
- }
- onSelect={this.handleSelect}
- subPanel={
- isAbsoluteSelected && (
- <div>
- <DateRangeHook
- start={start ?? null}
- end={end ?? null}
- organization={organization}
- showTimePicker
- utc={this.state.utc}
- onChange={this.handleSelectDateRange}
- onChangeUtc={this.handleUseUtc}
- maxPickableDays={maxPickableDays}
- />
- <SubmitRow>
- <MultipleSelectorSubmitRow
- onSubmit={this.handleCloseMenu}
- disabled={
- !this.state.hasChanges || this.state.hasDateRangeErrors
- }
- />
- </SubmitRow>
- </div>
- )
- }
- >
- {({isOpen, getActorProps}) =>
- customDropdownButton ? (
- customDropdownButton({getActorProps, isOpen})
- ) : (
- <StyledHeaderItem
- data-test-id="global-header-timerange-selector"
- icon={label ?? <IconCalendar />}
- isOpen={isOpen}
- hasSelected={
- (!!this.props.relative &&
- this.props.relative !== defaultPeriod) ||
- isAbsoluteSelected
- }
- hasChanges={this.state.hasChanges}
- onClear={this.handleClear}
- allowClear
- hint={hint}
- {...getActorProps()}
- >
- {getDynamicText({
- value: summary,
- fixed: 'start to end',
- })}
- </StyledHeaderItem>
- )
- }
- </StyledDropdownAutoComplete>
- )}
- </ClassNames>
- )}
- </SelectorItemsHook>
- );
- }
- }
- const TimeRangeRoot = styled('div')`
- position: relative;
- `;
- const StyledDropdownAutoComplete = styled(DropdownAutoComplete)`
- font-size: ${p => p.theme.fontSizeMedium};
- position: absolute;
- top: 100%;
- ${p =>
- !p.detached &&
- `
- margin-top: 0;
- border-radius: ${p.theme.borderRadiusBottom};
- `};
- `;
- const StyledHeaderItem = styled(HeaderItem)`
- height: 100%;
- `;
- const SubmitRow = styled('div')`
- height: 100%;
- padding: ${space(0.5)} ${space(1)};
- border-top: 1px solid ${p => p.theme.innerBorder};
- border-left: 1px solid ${p => p.theme.border};
- `;
- const StyledPinButton = styled(PageFilterPinButton)`
- margin: 0 ${space(1)};
- `;
- export default withRouter(TimeRangeSelector);
- export {TimeRangeRoot};
|