Browse Source

ref(page-filters): Use DropdownAutocomplete for TimeRangeSelector (#32349)

* ref(page-filters): Use DropdownAutocomplete for TimeRangeSelector

* fix(test): Fix tests related to TimeRangeSelector

* fix(page-filters): Remove hidePin props

* ref(time-range-selector): Use new SelectorItemsHook

* fix(page-filters): Add allowActorToggle prop to TimeRangeSelector

* ref(page-filters): Remove SelectorItems file

* fix(time-range-selector): Fix appearance in Stats & Releases

* fix(dropdown-autocomplete): Fix bubble's min-width

* fix(dropdown-autocomplete): Add min-width to content

* ref(page-filters): Use inset box-shadow for filter bar border
Vu Luong 3 years ago
parent
commit
5bb8160606

+ 14 - 29
static/app/components/datePageFilter.tsx

@@ -3,7 +3,6 @@ import styled from '@emotion/styled';
 
 import {updateDateTime} from 'sentry/actionCreators/pageFilters';
 import PageFilterDropdownButton from 'sentry/components/organizations/pageFilters/pageFilterDropdownButton';
-import PageFilterPinButton from 'sentry/components/organizations/pageFilters/pageFilterPinButton';
 import TimeRangeSelector, {
   ChangeData,
 } from 'sentry/components/organizations/timeRangeSelector';
@@ -11,7 +10,6 @@ import PageTimeRangeSelector from 'sentry/components/pageTimeRangeSelector';
 import {IconCalendar} from 'sentry/icons';
 import PageFiltersStore from 'sentry/stores/pageFiltersStore';
 import {useLegacyStore} from 'sentry/stores/useLegacyStore';
-import space from 'sentry/styles/space';
 import useOrganization from 'sentry/utils/useOrganization';
 
 type Props = Omit<
@@ -19,14 +17,13 @@ type Props = Omit<
   'organization' | 'start' | 'end' | 'utc' | 'relative' | 'onUpdate'
 > &
   WithRouterProps & {
-    hidePin?: boolean;
     /**
      * Reset these URL params when we fire actions (custom routing only)
      */
     resetParamsOnChange?: string[];
   };
 
-function DatePageFilter({router, resetParamsOnChange, hidePin, ...props}: Props) {
+function DatePageFilter({router, resetParamsOnChange, ...props}: Props) {
   const {selection, desyncedFilters} = useLegacyStore(PageFiltersStore);
   const organization = useOrganization();
   const {start, end, period, utc} = selection.datetime;
@@ -69,39 +66,27 @@ function DatePageFilter({router, resetParamsOnChange, hidePin, ...props}: Props)
   };
 
   return (
-    <DateSelectorContainer>
-      <StyledPageTimeRangeSelector
-        organization={organization}
-        start={start}
-        end={end}
-        relative={period}
-        utc={utc}
-        onUpdate={handleUpdate}
-        label={<IconCalendar color="textColor" />}
-        customDropdownButton={customDropdownButton}
-        detached
-        {...props}
-      />
-      {!hidePin && <PageFilterPinButton size="zero" filter="datetime" />}
-    </DateSelectorContainer>
+    <StyledPageTimeRangeSelector
+      organization={organization}
+      start={start}
+      end={end}
+      relative={period}
+      utc={utc}
+      onUpdate={handleUpdate}
+      label={<IconCalendar color="textColor" />}
+      customDropdownButton={customDropdownButton}
+      {...props}
+    />
   );
 }
 
-const DateSelectorContainer = styled('div')`
+const StyledPageTimeRangeSelector = styled(PageTimeRangeSelector)`
   flex-grow: 0;
   flex-shrink: 0;
   flex-basis: fit-content;
   position: relative;
-  display: grid;
-  gap: ${space(1)};
-  align-items: center;
-  grid-auto-flow: column;
-  grid-auto-columns: max-content;
-`;
-
-const StyledPageTimeRangeSelector = styled(PageTimeRangeSelector)`
+  width: 100%;
   height: 100%;
-  font-weight: 600;
   background: ${p => p.theme.background};
   border: none;
   box-shadow: none;

+ 66 - 47
static/app/components/dropdownAutoComplete/menu.tsx

@@ -176,6 +176,10 @@ type Props = {
    * the styles are forward to the Autocomplete's getMenuProps func
    */
   style?: React.CSSProperties;
+  /**
+   * Optional element to be rendered on the right side of the dropdown menu
+   */
+  subPanel?: React.ReactNode;
 } & Pick<
   ListProps,
   'virtualizedHeight' | 'virtualizedLabelHeight' | 'itemSize' | 'onScroll'
@@ -193,6 +197,7 @@ const Menu = ({
   busyItemsStillVisible = false,
   menuWithArrow = false,
   disabled = false,
+  subPanel = null,
   itemSize,
   virtualizedHeight,
   virtualizedLabelHeight,
@@ -292,7 +297,7 @@ const Menu = ({
             selectedItem,
           })}
           {isOpen && (
-            <BubbleWithMinWidth
+            <StyledDropdownBubble
               className={className}
               {...getMenuProps({
                 ...menuProps,
@@ -305,53 +310,58 @@ const Menu = ({
               alignMenu={alignMenu}
               menuWithArrow={menuWithArrow}
             >
-              {itemsLoading && <LoadingIndicator mini />}
-              {showInput && (
-                <InputWrapper>
-                  <StyledInput
-                    autoFocus
-                    placeholder={searchPlaceholder}
-                    {...getInputProps({...inputProps, onChange})}
-                  />
-                  <InputLoadingWrapper>
-                    {(busy || busyItemsStillVisible) && (
-                      <LoadingIndicator size={16} mini />
-                    )}
-                  </InputLoadingWrapper>
-                  {inputActions}
-                </InputWrapper>
-              )}
-              <div>
-                {menuHeader && <LabelWithPadding>{menuHeader}</LabelWithPadding>}
-                <ItemList data-test-id="autocomplete-list" maxHeight={maxHeight}>
-                  {showNoItems && <EmptyMessage>{emptyMessage}</EmptyMessage>}
-                  {showNoResultsMessage && (
-                    <EmptyMessage>
-                      {noResultsMessage ?? `${emptyMessage} ${t('found')}`}
-                    </EmptyMessage>
-                  )}
-                  {busy && (
-                    <BusyMessage>
-                      <EmptyMessage>{t('Searching\u2026')}</EmptyMessage>
-                    </BusyMessage>
-                  )}
-                  {!busy && (
-                    <List
-                      items={autoCompleteResults}
-                      maxHeight={maxHeight}
-                      highlightedIndex={highlightedIndex}
-                      inputValue={inputValue}
-                      onScroll={onScroll}
-                      getItemProps={getItemProps}
-                      virtualizedLabelHeight={virtualizedLabelHeight}
-                      virtualizedHeight={virtualizedHeight}
-                      itemSize={itemSize}
+              <DropdownMainContent>
+                {itemsLoading && <LoadingIndicator mini />}
+                {showInput && (
+                  <InputWrapper>
+                    <StyledInput
+                      autoFocus
+                      placeholder={searchPlaceholder}
+                      {...getInputProps({...inputProps, onChange})}
                     />
+                    <InputLoadingWrapper>
+                      {(busy || busyItemsStillVisible) && (
+                        <LoadingIndicator size={16} mini />
+                      )}
+                    </InputLoadingWrapper>
+                    {inputActions}
+                  </InputWrapper>
+                )}
+                <div>
+                  {menuHeader && <LabelWithPadding>{menuHeader}</LabelWithPadding>}
+                  <ItemList data-test-id="autocomplete-list" maxHeight={maxHeight}>
+                    {showNoItems && <EmptyMessage>{emptyMessage}</EmptyMessage>}
+                    {showNoResultsMessage && (
+                      <EmptyMessage>
+                        {noResultsMessage ?? `${emptyMessage} ${t('found')}`}
+                      </EmptyMessage>
+                    )}
+                    {busy && (
+                      <BusyMessage>
+                        <EmptyMessage>{t('Searching\u2026')}</EmptyMessage>
+                      </BusyMessage>
+                    )}
+                    {!busy && (
+                      <List
+                        items={autoCompleteResults}
+                        maxHeight={maxHeight}
+                        highlightedIndex={highlightedIndex}
+                        inputValue={inputValue}
+                        onScroll={onScroll}
+                        getItemProps={getItemProps}
+                        virtualizedLabelHeight={virtualizedLabelHeight}
+                        virtualizedHeight={virtualizedHeight}
+                        itemSize={itemSize}
+                      />
+                    )}
+                  </ItemList>
+                  {renderedFooter && (
+                    <LabelWithPadding>{renderedFooter}</LabelWithPadding>
                   )}
-                </ItemList>
-                {renderedFooter && <LabelWithPadding>{renderedFooter}</LabelWithPadding>}
-              </div>
-            </BubbleWithMinWidth>
+                </div>
+              </DropdownMainContent>
+              {subPanel}
+            </StyledDropdownBubble>
           )}
         </AutoCompleteRoot>
       );
@@ -404,7 +414,16 @@ export const AutoCompleteRoot = styled(({isOpen: _isOpen, ...props}) => (
   ${p => p.disabled && 'pointer-events: none;'}
 `;
 
-const BubbleWithMinWidth = styled(DropdownBubble)`
+const StyledDropdownBubble = styled(DropdownBubble)`
+  display: flex;
+  min-width: 250px;
+
+  ${p => p.detached && p.alignMenu === 'left' && 'right: auto;'}
+  ${p => p.detached && p.alignMenu === 'right' && 'left: auto;'}
+`;
+
+const DropdownMainContent = styled('div')`
+  width: 100%;
   min-width: 250px;
 `;
 

+ 2 - 1
static/app/components/dropdownAutoComplete/row.tsx

@@ -87,13 +87,14 @@ const AutoCompleteItem = styled('div')<{
   disabled?: boolean;
   itemSize?: ItemSize;
 }>`
+  position: relative;
   /* needed for virtualized lists that do not fill parent height */
   /* e.g. breadcrumbs (org height > project, but want same fixed height for both) */
   display: flex;
   flex-direction: column;
   justify-content: center;
 
-  font-size: 0.9em;
+  font-size: ${p => p.theme.fontSizeMedium};
   background-color: ${p => (p.isHighlighted ? p.theme.hover : 'transparent')};
   color: ${p => (p.isHighlighted ? p.theme.textColor : 'inherit')};
   padding: ${p => getItemPaddingForSize(p.itemSize)};

+ 1 - 1
static/app/components/organizations/multipleProjectSelector.tsx

@@ -459,11 +459,11 @@ const FooterMessage = styled('div')`
 const StyledProjectSelector = styled(ProjectSelector)`
   background-color: ${p => p.theme.background};
   color: ${p => p.theme.textColor};
-  width: 100%;
 
   ${p =>
     !p.detached &&
     `
+    width: 100%;
     margin: 1px 0 0 -1px;
     border-radius: ${p.theme.borderRadiusBottom};
   `}

+ 17 - 1
static/app/components/organizations/pageFilterBar.tsx

@@ -2,10 +2,26 @@ import styled from '@emotion/styled';
 
 const PageFilterBar = styled('div')`
   display: flex;
-  border: solid 1px ${p => p.theme.border};
+  position: relative;
   border-radius: ${p => p.theme.borderRadius};
   height: ${p => p.theme.form.default.height}px;
 
+  &::after {
+    content: '';
+    position: absolute;
+    top: 0;
+    bottom: 0;
+    left: 0;
+    right: 0;
+    pointer-events: none;
+    box-shadow: inset 0 0 0 1px ${p => p.theme.border};
+    border-radius: ${p => p.theme.borderRadius};
+  }
+
+  & [role='button'] {
+    z-index: 0;
+  }
+
   & button[aria-haspopup='listbox'] {
     height: 100%;
     min-height: auto;

+ 1 - 1
static/app/components/organizations/timeRangeSelector/dateRange/index.tsx

@@ -255,7 +255,7 @@ const DateRange = styled(withTheme(withRouter(BaseDateRange)))`
 const TimeAndUtcPicker = styled('div')`
   display: flex;
   align-items: center;
-  padding: ${space(2)};
+  padding: ${space(0.25)} ${space(2)};
   border-top: 1px solid ${p => p.theme.innerBorder};
 `;
 

+ 0 - 47
static/app/components/organizations/timeRangeSelector/dateRange/selectorItems.tsx

@@ -1,47 +0,0 @@
-import * as React from 'react';
-
-import RelativeSelector from 'sentry/components/organizations/timeRangeSelector/dateRange/relativeSelector';
-import SelectorItem from 'sentry/components/organizations/timeRangeSelector/dateRange/selectorItem';
-import {t} from 'sentry/locale';
-
-type Props = {
-  handleAbsoluteClick: (value: string, e?: React.MouseEvent) => void;
-  handleSelectRelative: (value: string, e?: React.MouseEvent) => void;
-  isAbsoluteSelected: boolean;
-  relativeSelected: string;
-  relativePeriods?: Record<string, React.ReactNode>;
-  shouldShowAbsolute?: boolean;
-  // Override DEFAULT_RELATIVE_PERIODS
-  shouldShowRelative?: boolean;
-};
-
-const SelectorItems = ({
-  shouldShowRelative,
-  shouldShowAbsolute,
-  handleSelectRelative,
-  handleAbsoluteClick,
-  relativeSelected,
-  relativePeriods,
-  isAbsoluteSelected,
-}: Props) => (
-  <React.Fragment>
-    {shouldShowRelative && (
-      <RelativeSelector
-        onClick={handleSelectRelative}
-        selected={relativeSelected}
-        relativePeriods={relativePeriods}
-      />
-    )}
-    {shouldShowAbsolute && (
-      <SelectorItem
-        onClick={handleAbsoluteClick}
-        value="absolute"
-        label={t('Absolute date')}
-        selected={isAbsoluteSelected}
-        last
-      />
-    )}
-  </React.Fragment>
-);
-
-export default SelectorItems;

+ 100 - 111
static/app/components/organizations/timeRangeSelector/index.tsx

@@ -1,17 +1,21 @@
 import * as React from 'react';
 import {withRouter, WithRouterProps} from 'react-router';
+import {ClassNames} from '@emotion/react';
 import styled from '@emotion/styled';
 
-import DropdownMenu, {GetActorPropsFn} from 'sentry/components/dropdownMenu';
+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 SelectorItems from 'sentry/components/organizations/timeRangeSelector/dateRange/selectorItems';
 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';
@@ -28,6 +32,8 @@ import {
 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,
@@ -239,6 +245,14 @@ class TimeRangeSelector extends React.PureComponent<Props, State> {
     );
   };
 
+  handleSelect = (item: Item) => {
+    if (item.value === 'absolute') {
+      this.handleAbsoluteClick();
+      return;
+    }
+    this.handleSelectRelative(item.value);
+  };
+
   handleAbsoluteClick = () => {
     const {relative, onChange, defaultPeriod, defaultAbsolute} = this.props;
 
@@ -378,6 +392,8 @@ class TimeRangeSelector extends React.PureComponent<Props, State> {
     } = 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;
@@ -392,64 +408,42 @@ class TimeRangeSelector extends React.PureComponent<Props, State> {
         )
       );
 
-    const relativeSelected = isAbsoluteSelected
-      ? ''
-      : relative || defaultPeriod || DEFAULT_STATS_PERIOD;
-
     return (
-      <DropdownMenu
-        isOpen={this.state.isOpen}
-        onOpen={this.handleOpen}
-        onClose={this.handleCloseMenu}
-        keepMenuOpen
+      <SelectorItemsHook
+        shouldShowAbsolute={shouldShowAbsolute}
+        shouldShowRelative={shouldShowRelative}
+        relativePeriods={relativeOptions}
+        handleSelectRelative={this.handleSelectRelative}
       >
-        {({isOpen, getRootProps, getActorProps, getMenuProps}) => {
-          const dropdownButton = 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>
-          );
-
-          return (
-            <TimeRangeRoot {...getRootProps()}>
-              {dropdownButton}
-              {isOpen && (
-                <Menu
-                  {...getMenuProps()}
-                  isAbsoluteSelected={isAbsoluteSelected}
-                  detached={detached}
-                  alignDropdown={alignDropdown}
-                >
-                  <SelectorList isAbsoluteSelected={isAbsoluteSelected}>
-                    <SelectorItemsHook
-                      handleSelectRelative={this.handleSelectRelative}
-                      handleAbsoluteClick={this.handleAbsoluteClick}
-                      isAbsoluteSelected={isAbsoluteSelected}
-                      relativeSelected={relativeSelected}
-                      relativePeriods={relativeOptions}
-                      shouldShowAbsolute={shouldShowAbsolute}
-                      shouldShowRelative={shouldShowRelative}
-                    />
-                  </SelectorList>
-                  {isAbsoluteSelected && (
+        {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}
@@ -470,13 +464,40 @@ class TimeRangeSelector extends React.PureComponent<Props, State> {
                         />
                       </SubmitRow>
                     </div>
-                  )}
-                </Menu>
-              )}
-            </TimeRangeRoot>
-          );
-        }}
-      </DropdownMenu>
+                  )
+                }
+              >
+                {({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>
     );
   }
 }
@@ -485,66 +506,34 @@ const TimeRangeRoot = styled('div')`
   position: relative;
 `;
 
-const StyledHeaderItem = styled(HeaderItem)`
-  height: 100%;
-`;
-
-type MenuProps = {
-  isAbsoluteSelected: boolean;
-  alignDropdown?: Props['alignDropdown'];
-  detached?: Props['detached'];
-};
-
-const Menu = styled('div')<MenuProps>`
-  ${p =>
-    p.alignDropdown
-      ? `
-    ${p.alignDropdown === 'left' && 'left: -1px'};
-    ${p.alignDropdown === 'right' && 'right: -1px'};
-  `
-      : `
-    ${!p.isAbsoluteSelected && 'left: -1px'};
-    ${p.isAbsoluteSelected && 'right: -1px'};
-  `}
-
-  display: flex;
-  background: ${p => p.theme.background};
-  border: 1px solid ${p => p.theme.border};
+const StyledDropdownAutoComplete = styled(DropdownAutoComplete)`
+  font-size: ${p => p.theme.fontSizeMedium};
   position: absolute;
   top: 100%;
-  min-width: 100%;
-  z-index: ${p => p.theme.zIndex.dropdown};
-  font-size: 0.8em;
-  overflow: hidden;
 
   ${p =>
-    p.detached
-      ? `
-        border-radius: ${p.theme.borderRadius};
-        margin-top: ${space(1)};
-        box-shadow: ${p.theme.dropShadowHeavy};
-      `
-      : `
-        border-radius: ${p.theme.borderRadiusBottom};
-        box-shadow: ${p.theme.dropShadowLight};
-    `}
+    !p.detached &&
+    `
+    margin-top: 0;
+    border-radius: ${p.theme.borderRadiusBottom};
+  `};
 `;
 
-const SelectorList = styled('div')<MenuProps>`
-  display: flex;
-  flex: 1;
-  flex-direction: column;
-  flex-shrink: 0;
-  min-width: ${p => (p.isAbsoluteSelected ? '160px' : '220px')};
-  min-height: 305px;
+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};

+ 56 - 0
static/app/components/organizations/timeRangeSelector/selectorItems.tsx

@@ -0,0 +1,56 @@
+import styled from '@emotion/styled';
+
+import {Item} from 'sentry/components/dropdownAutoComplete/types';
+import {DEFAULT_RELATIVE_PERIODS} from 'sentry/constants';
+import {t} from 'sentry/locale';
+import space from 'sentry/styles/space';
+
+type Props = {
+  children: (items: Item[]) => React.ReactElement;
+  handleSelectRelative: (value: string) => void;
+  relativePeriods?: Record<'string', React.ReactNode>;
+  shouldShowAbsolute?: boolean;
+  shouldShowRelative?: boolean;
+};
+
+const SelectorItems = ({
+  children,
+  relativePeriods,
+  shouldShowRelative,
+  shouldShowAbsolute,
+}: Props) => {
+  const relativeArr = Object.entries(relativePeriods ?? DEFAULT_RELATIVE_PERIODS);
+
+  const items: Item[] = [
+    ...(shouldShowRelative
+      ? relativeArr.map(([value, itemLabel], index) => ({
+          index,
+          value,
+          searchKey: typeof itemLabel === 'string' ? itemLabel : value,
+          label: <Label>{itemLabel}</Label>,
+          'data-test-id': value,
+        }))
+      : []),
+    ...(shouldShowAbsolute
+      ? [
+          {
+            index: relativeArr.length,
+            value: 'absolute',
+            searchKey: 'absolute',
+            label: <Label>{t('Absolute date')}</Label>,
+            'data-test-id': 'absolute',
+          },
+        ]
+      : []),
+  ];
+
+  return children(items);
+};
+
+const Label = styled('div')`
+  margin-left: ${space(0.5)};
+  margin-top: ${space(0.25)};
+  margin-bottom: ${space(0.25)};
+`;
+
+export default SelectorItems;

+ 42 - 31
static/app/components/pageTimeRangeSelector.tsx

@@ -6,61 +6,72 @@ import {Panel} from 'sentry/components/panels';
 import {DEFAULT_RELATIVE_PERIODS} from 'sentry/constants';
 import {t} from 'sentry/locale';
 import space from 'sentry/styles/space';
+import {defined} from 'sentry/utils';
 
 type Props = React.ComponentProps<typeof TimeRangeSelector> & {
   className?: string;
 };
 
-function PageTimeRangeSelector({className, ...props}: Props) {
+function PageTimeRangeSelector({className, customDropdownButton, ...props}: Props) {
   const [isCalendarOpen, setIsCalendarOpen] = useState(false);
 
   return (
-    <DropdownDate className={className} isCalendarOpen={isCalendarOpen}>
+    <DropdownDate
+      className={className}
+      isCalendarOpen={isCalendarOpen}
+      hasCustomButton={defined(customDropdownButton)}
+    >
       <TimeRangeSelector
         key={`period:${props.relative}-start:${props.start}-end:${props.end}-utc:${props.utc}-defaultPeriod:${props.defaultPeriod}`}
         label={<DropdownLabel>{t('Date Range:')}</DropdownLabel>}
         onToggleSelector={isOpen => setIsCalendarOpen(isOpen)}
         relativeOptions={DEFAULT_RELATIVE_PERIODS}
+        customDropdownButton={customDropdownButton}
+        detached
         {...props}
       />
     </DropdownDate>
   );
 }
 
-const DropdownDate = styled(Panel)<{isCalendarOpen: boolean}>`
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  height: 42px;
-
-  background: ${p => p.theme.background};
-  border: 1px solid ${p => p.theme.border};
-  border-radius: ${p =>
-    p.isCalendarOpen
-      ? `${p.theme.borderRadius} ${p.theme.borderRadius} 0 0`
-      : p.theme.borderRadius};
+const DropdownDate = styled(Panel)<{hasCustomButton: boolean; isCalendarOpen: boolean}>`
   padding: 0;
   margin: 0;
-  font-size: ${p => p.theme.fontSizeMedium};
-  color: ${p => p.theme.textColor};
 
-  /* TimeRangeRoot in TimeRangeSelector */
-  > div {
-    width: 100%;
-    align-self: stretch;
-  }
+  ${p =>
+    !p.hasCustomButton &&
+    `
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    height: 42px;
+    background: ${p.theme.background};
+    border: 1px solid ${p.theme.border};
+    border-radius: ${
+      p.isCalendarOpen
+        ? `${p.theme.borderRadius} ${p.theme.borderRadius} 0 0`
+        : p.theme.borderRadius
+    };
 
-  /* StyledItemHeader used to show selected value of TimeRangeSelector */
-  > div > div:first-child {
-    padding: 0 ${space(2)};
-  }
+    font-size: ${p.theme.fontSizeMedium};
+    color: ${p.theme.textColor};
 
-  /* Menu that dropdowns from TimeRangeSelector */
-  > div > div:last-child {
-    /* Remove awkward 1px width difference on dropdown due to border */
-    box-sizing: content-box;
-    font-size: 1em;
-  }
+    > div {
+      width: 100%;
+      align-self: stretch;
+    }
+    /* StyledItemHeader used to show selected value of TimeRangeSelector */
+    > div > div:first-child > div {
+      padding: 0 ${space(2)};
+    }
+    /* Menu that dropdowns from TimeRangeSelector */
+    > div > div:last-child:not(:first-child) {
+      /* Remove awkward 1px width difference on dropdown due to border */
+      width: calc(100% + 2px);
+      transform: translateX(-1px);
+      right: auto;
+    }
+  `}
 `;
 
 const DropdownLabel = styled('span')`

Some files were not shown because too many files changed in this diff