123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347 |
- 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<typeof DateRange>['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<string, React.ReactChild>;
- /**
- * 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<ChangeData>({
- relative: selection.datetime.period,
- start: startDate,
- end: endDate,
- utc: selectionUtc,
- });
- const [hasChanges, setHasChanges] = useState<boolean>(false);
- const [hasDateErrors, setHasDateErrors] = useState<boolean>(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 (
- <DateSelectorContainer>
- <ButtonBar merged>
- {showRelative &&
- Object.entries(relativeOptions ?? DEFAULT_RELATIVE_PERIODS_PAGE_FILTER).map(
- ([value, label]) => (
- <RelativePeriodButton
- key={value}
- selected={value === selection.datetime.period}
- onClick={() => handleSelectRelative(value)}
- >
- {label}
- </RelativePeriodButton>
- )
- )}
- {showAbsolute && (
- <CustomDateOption>
- <DropdownMenu keepMenuOpen onClose={handleCloseDateSelector}>
- {({isOpen, getActorProps, getMenuProps, actions}) => (
- <React.Fragment>
- <CustomPeriodButton
- isOpen={isOpen}
- {...getActorProps()}
- selected={!selection.datetime.period}
- showRelative={showRelative}
- >
- {getDateSummary()}
- </CustomPeriodButton>
- <Content
- {...getMenuProps()}
- alignMenu="right"
- width="350px"
- isOpen={isOpen}
- blendCorner
- >
- <DateRangeHook
- start={selectedTimePeriod.start ?? getStartFromRelativePeriod()}
- end={selectedTimePeriod.end ?? getEndFromRelativePeriod()}
- organization={organization}
- showTimePicker
- utc={selectedTimePeriod.utc}
- onChange={handleChangeDateRange}
- onChangeUtc={handleUseUtc}
- maxPickableDays={maxPickableDays}
- />
- <SubmitRow>
- <MultipleSelectorSubmitRow
- onSubmit={actions.close}
- disabled={!hasChanges || hasDateErrors}
- />
- </SubmitRow>
- </Content>
- </React.Fragment>
- )}
- </DropdownMenu>
- </CustomDateOption>
- )}
- </ButtonBar>
- <PinButton
- aria-pressed={isDatePinned}
- aria-label={t('Pin')}
- onClick={handlePinClick}
- size="zero"
- icon={<IconPin size="xs" isSolid={isDatePinned} />}
- borderless
- />
- </DateSelectorContainer>
- );
- }
- 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);
|