import React, {useState} from 'react'; import {withRouter, WithRouterProps} from 'react-router'; import styled from '@emotion/styled'; import moment from 'moment'; import {pinFilter, updateDateTime} from 'sentry/actionCreators/pageFilters'; import Button from 'sentry/components/button'; import ButtonBar from 'sentry/components/buttonBar'; import DropdownButton from 'sentry/components/dropdownButton'; import {Content} from 'sentry/components/dropdownControl'; import DropdownMenu from 'sentry/components/dropdownMenu'; import HookOrDefault from 'sentry/components/hookOrDefault'; import MultipleSelectorSubmitRow from 'sentry/components/organizations/multipleSelectorSubmitRow'; import {ChangeData} from 'sentry/components/organizations/timeRangeSelector'; import DateRange from 'sentry/components/organizations/timeRangeSelector/dateRange'; import { DEFAULT_RELATIVE_PERIODS_PAGE_FILTER, DEFAULT_STATS_PERIOD, } from 'sentry/constants'; import {IconPin} from 'sentry/icons'; import {t} from 'sentry/locale'; import PageFiltersStore from 'sentry/stores/pageFiltersStore'; import {useLegacyStore} from 'sentry/stores/useLegacyStore'; import space from 'sentry/styles/space'; import {defined} from 'sentry/utils'; import { getDateWithTimezoneInUtc, getInternalDate, getLocalToSystem, getPeriodAgo, getUserTimezone, getUtcToSystem, parsePeriodToHours, } from 'sentry/utils/dates'; import useOrganization from 'sentry/utils/useOrganization'; const DateRangeHook = HookOrDefault({ hookName: 'component:header-date-range', defaultComponent: DateRange, }); type DateRangeChangeData = Parameters< React.ComponentProps['onChange'] >[0]; type Props = { router: WithRouterProps['router']; /** * Set an optional default value to prefill absolute date with */ defaultAbsolute?: {end?: Date; start?: Date}; /** * Override DEFAULT_STATS_PERIOD */ defaultPeriod?: string; /** * The maximum number of days in the past you can pick */ maxPickableDays?: number; /** * Override defaults from DEFAULT_RELATIVE_PERIODS_PAGE_FILTER */ relativeOptions?: Record; /** * Reset these URL params when we fire actions (custom routing only) */ resetParamsOnChange?: string[]; /** * Show absolute date selection */ showAbsolute?: boolean; /** * Show relative date options */ showRelative?: boolean; }; function DatePageFilter({ router, defaultAbsolute, defaultPeriod, maxPickableDays, relativeOptions, resetParamsOnChange = [], showAbsolute = true, showRelative = true, }: Props) { const organization = useOrganization(); const {selection, pinnedFilters} = useLegacyStore(PageFiltersStore); const isDatePinned = pinnedFilters.has('datetime'); const getStartFromRelativePeriod = () => { return defaultAbsolute?.start ? defaultAbsolute.start : getPeriodAgo( 'hours', parsePeriodToHours( selection.datetime.period || defaultPeriod || DEFAULT_STATS_PERIOD ) ).toDate(); }; const getEndFromRelativePeriod = () => { return defaultAbsolute?.end ? defaultAbsolute.end : new Date(); }; const selectionStart = selection.datetime.start; const selectionEnd = selection.datetime.end; // if utc is not null and not undefined, then use value of `selection.datetime.utc` (it can be false) // otherwise if no value is supplied, the default should be the user's timezone preference const selectionUtc = defined(selection.datetime.utc) ? selection.datetime.utc : getUserTimezone() === 'UTC'; // convert current selection.datetime start values into dates for the DateRange hook // or generate them from the currently selected relative period const startDate = selectionStart && selectionEnd ? getInternalDate(selectionStart, selectionUtc) : getStartFromRelativePeriod(); const endDate = selectionStart && selectionEnd ? getInternalDate(selectionEnd, selectionUtc) : getEndFromRelativePeriod(); const [selectedTimePeriod, setSelectedTimePeriod] = useState({ relative: selection.datetime.period, start: startDate, end: endDate, utc: selectionUtc, }); const [hasChanges, setHasChanges] = useState(false); const [hasDateErrors, setHasDateErrors] = useState(false); const getDateSummary = () => { const {relative, start, end} = selectedTimePeriod; if (relative || !start || !end) { return t('Custom'); } const formattedStart = moment(start).local().format('MMM DD'); const formattedEnd = moment(end).local().format('MMM DD'); return `${formattedStart} - ${formattedEnd}`; }; const handleUpdate = (timePeriodUpdate: ChangeData) => { const {relative, start, end, utc} = timePeriodUpdate; const newTimePeriod = { period: relative, start, end, utc, }; updateDateTime(newTimePeriod, router, {save: true, resetParams: resetParamsOnChange}); setHasChanges(false); }; const handleChangeDateRange = ({ start, end, hasDateRangeErrors = false, }: DateRangeChangeData) => { if (hasDateRangeErrors) { setHasDateErrors(hasDateRangeErrors); return; } const newDateTime: ChangeData = { relative: null, start, end, utc: selectedTimePeriod.utc, }; setHasChanges(true); setHasDateErrors(hasDateRangeErrors); setSelectedTimePeriod(newDateTime); }; const handleCloseDateSelector = () => { if (!hasChanges) { return; } handleUpdate(selectedTimePeriod); }; const handleUseUtc = () => { const utc = !selectedTimePeriod.utc; let {start, end} = selection.datetime; if (!start) { start = getDateWithTimezoneInUtc(selectedTimePeriod.start, utc); } if (!end) { end = getDateWithTimezoneInUtc(selectedTimePeriod.end, utc); } const newDateTime = { relative: null, start: utc ? getLocalToSystem(start) : getUtcToSystem(start), end: utc ? getLocalToSystem(end) : getUtcToSystem(end), utc, }; setHasChanges(true); setSelectedTimePeriod(newDateTime); }; const handleSelectRelative = (value: string) => { const newDateTime: ChangeData = { relative: value, start: undefined, end: undefined, }; setSelectedTimePeriod(newDateTime); handleUpdate(newDateTime); }; const handlePinClick = () => { pinFilter('datetime', !isDatePinned); }; return ( {showRelative && Object.entries(relativeOptions ?? DEFAULT_RELATIVE_PERIODS_PAGE_FILTER).map( ([value, label]) => ( handleSelectRelative(value)} > {label} ) )} {showAbsolute && ( {({isOpen, getActorProps, getMenuProps, actions}) => ( {getDateSummary()} )} )} } borderless /> ); } const DateSelectorContainer = styled('div')` display: grid; gap: ${space(1)}; align-items: center; grid-auto-flow: column; grid-auto-columns: max-content; `; const PinButton = styled(Button)` display: block; color: ${p => p.theme.gray300}; background: transparent; `; const RelativePeriodButton = styled(Button)<{selected?: boolean}>` font-size: ${p => p.theme.fontSizeMedium}; font-weight: ${p => (p.selected ? 700 : 400)}; color: ${p => (p.selected ? p.theme.textColor : p.theme.subText)}; ${p => p.selected && `background-color: ${p.theme.bodyBackground}`}; `; const CustomPeriodButton = styled(DropdownButton)<{ selected?: boolean; showRelative?: boolean; }>` font-size: ${p => p.theme.fontSizeMedium}; font-weight: ${p => (p.selected ? 700 : 400)}; color: ${p => (p.selected ? p.theme.textColor : p.theme.subText)}; ${p => p.showRelative && ` border-left: none; border-top-left-radius: 0; border-bottom-left-radius: 0; `} `; const CustomDateOption = styled('div')` display: inline-block; position: relative; `; const SubmitRow = styled('div')` padding: ${space(0.5)} ${space(1)}; border-top: 1px solid ${p => p.theme.innerBorder}; border-left: 1px solid ${p => p.theme.border}; `; export default withRouter(DatePageFilter);