Browse Source

ref(pageFilters): Support more selector props (#48914)

Make page filters accept more select props, like `disabled`, `onClear`,
`menuWidth`,…
Vu Luong 1 year ago
parent
commit
832c0ca1bc

+ 27 - 6
static/app/components/organizations/datePageFilter.tsx

@@ -1,7 +1,10 @@
 import styled from '@emotion/styled';
 
 import {updateDateTime} from 'sentry/actionCreators/pageFilters';
-import {TimeRangeSelector} from 'sentry/components/timeRangeSelector';
+import {
+  TimeRangeSelector,
+  TimeRangeSelectorProps,
+} from 'sentry/components/timeRangeSelector';
 import {IconCalendar} from 'sentry/icons';
 import {t} from 'sentry/locale';
 import usePageFilters from 'sentry/utils/usePageFilters';
@@ -12,36 +15,53 @@ import {
   DesyncedFilterMessage,
 } from './pageFilters/desyncedFilter';
 
-interface DatePageFilterProps {
+interface DatePageFilterProps
+  extends Partial<
+    Partial<
+      Omit<TimeRangeSelectorProps, 'start' | 'end' | 'utc' | 'relative' | 'menuBody'>
+    >
+  > {
   /**
    * Reset these URL params when we fire actions (custom routing only)
    */
   resetParamsOnChange?: string[];
 }
 
-export function DatePageFilter({resetParamsOnChange}: DatePageFilterProps) {
+export function DatePageFilter({
+  onChange,
+  disabled,
+  menuTitle,
+  menuWidth,
+  triggerProps = {},
+  resetParamsOnChange,
+  ...selectProps
+}: DatePageFilterProps) {
   const router = useRouter();
-  const {selection, desyncedFilters} = usePageFilters();
+  const {selection, desyncedFilters, isReady: pageFilterIsReady} = usePageFilters();
   const {start, end, period, utc} = selection.datetime;
   const desynced = desyncedFilters.has('datetime');
 
   return (
     <TimeRangeSelector
+      {...selectProps}
       start={start}
       end={end}
       utc={utc}
       relative={period}
+      disabled={disabled ?? !pageFilterIsReady}
       onChange={timePeriodUpdate => {
         const {relative, ...startEndUtc} = timePeriodUpdate;
         const newTimePeriod = {period: relative, ...startEndUtc};
 
+        onChange?.(timePeriodUpdate);
+
         updateDateTime(newTimePeriod, router, {
           save: true,
           resetParams: resetParamsOnChange,
         });
       }}
-      menuTitle={t('Filter Time Range')}
-      menuWidth={desynced ? '22em' : undefined}
+      menuTitle={menuTitle ?? t('Filter Time Range')}
+      menuWidth={menuWidth ?? desynced ? '22em' : undefined}
       menuBody={desynced && <DesyncedFilterMessage />}
       triggerProps={{
         icon: (
@@ -50,6 +70,7 @@ export function DatePageFilter({resetParamsOnChange}: DatePageFilterProps) {
             {desynced && <DesyncedFilterIndicator />}
           </TriggerIconWrap>
         ),
+        ...triggerProps,
       }}
     />
   );

+ 49 - 22
static/app/components/organizations/environmentPageFilter/index.tsx

@@ -3,7 +3,10 @@ import isEqual from 'lodash/isEqual';
 import sortBy from 'lodash/sortBy';
 
 import {updateEnvironments} from 'sentry/actionCreators/pageFilters';
-import {HybridFilter} from 'sentry/components/organizations/hybridFilter';
+import {
+  HybridFilter,
+  HybridFilterProps,
+} from 'sentry/components/organizations/hybridFilter';
 import {ALL_ACCESS_PROJECTS} from 'sentry/constants/pageFilters';
 import {t} from 'sentry/locale';
 import ConfigStore from 'sentry/stores/configStore';
@@ -18,15 +21,27 @@ import {DesyncedFilterMessage} from '../pageFilters/desyncedFilter';
 
 import {EnvironmentPageFilterTrigger} from './trigger';
 
-export interface EnvironmentPageFilterProps {
+export interface EnvironmentPageFilterProps
+  extends Partial<
+    Omit<
+      HybridFilterProps<string>,
+      | 'searchable'
+      | 'multiple'
+      | 'options'
+      | 'value'
+      | 'onReplace'
+      | 'onToggle'
+      | 'menuBody'
+      | 'menuFooter'
+      | 'menuFooterMessage'
+      | 'checkboxWrapper'
+      | 'shouldCloseOnInteractOutside'
+    >
+  > {
   /**
    * Message to show in the menu footer
    */
   footerMessage?: string;
-  /**
-   * Triggers any time a selection is changed, but the menu has not yet been closed or "applied"
-   */
-  onChange?: (selected: string[]) => void;
   /**
    * Reset these URL params when we fire actions (custom routing only)
    */
@@ -35,6 +50,14 @@ export interface EnvironmentPageFilterProps {
 
 export function EnvironmentPageFilter({
   onChange,
+  onClear,
+  disabled,
+  sizeLimit,
+  sizeLimitMessage,
+  emptyMessage,
+  menuTitle,
+  menuWidth,
+  trigger,
   resetParamsOnChange,
   footerMessage,
   ...selectProps
@@ -142,7 +165,7 @@ export function EnvironmentPageFilter({
   );
 
   const desynced = desyncedFilters.has('environments');
-  const menuWidth = useMemo(() => {
+  const defaultMenuWidth = useMemo(() => {
     // EnvironmentPageFilter will try to expand to accommodate the longest env slug
     const longestSlugLength = options
       .slice(0, 25)
@@ -167,25 +190,29 @@ export function EnvironmentPageFilter({
       options={options}
       value={value}
       onChange={handleChange}
+      onClear={onClear}
       onReplace={onReplace}
       onToggle={onToggle}
-      disabled={!projectsLoaded || !pageFilterIsReady}
-      sizeLimit={25}
-      sizeLimitMessage={t('Use search to find more environments…')}
-      emptyMessage={t('No environments found')}
-      menuTitle={t('Filter Environments')}
-      menuWidth={menuWidth}
+      disabled={disabled ?? (!projectsLoaded || !pageFilterIsReady)}
+      sizeLimit={sizeLimit ?? 25}
+      sizeLimitMessage={sizeLimitMessage ?? t('Use search to find more environments…')}
+      emptyMessage={emptyMessage ?? t('No environments found')}
+      menuTitle={menuTitle ?? t('Filter Environments')}
+      menuWidth={menuWidth ?? defaultMenuWidth}
       menuBody={desynced && <DesyncedFilterMessage />}
       menuFooterMessage={footerMessage}
-      trigger={triggerProps => (
-        <EnvironmentPageFilterTrigger
-          value={value}
-          environments={environments}
-          ready={projectsLoaded && pageFilterIsReady}
-          desynced={desynced}
-          {...triggerProps}
-        />
-      )}
+      trigger={
+        trigger ??
+        (triggerProps => (
+          <EnvironmentPageFilterTrigger
+            value={value}
+            environments={environments}
+            ready={projectsLoaded && pageFilterIsReady}
+            desynced={desynced}
+            {...triggerProps}
+          />
+        ))
+      }
     />
   );
 }

+ 20 - 3
static/app/components/organizations/hybridFilter.tsx

@@ -9,6 +9,7 @@ import {
   MultipleSelectProps,
   SelectOption,
   SelectOptionOrSection,
+  SelectSection,
 } from 'sentry/components/compactSelect';
 import {IconInfo} from 'sentry/icons/iconInfo';
 import {t} from 'sentry/locale';
@@ -19,7 +20,16 @@ import {useSyncedLocalStorageState} from 'sentry/utils/useSyncedLocalStorageStat
 export interface HybridFilterProps<Value extends React.Key>
   extends Omit<
     MultipleSelectProps<Value>,
-    'value' | 'defaultValue' | 'onChange' | 'multiple'
+    | 'grid'
+    | 'multiple'
+    | 'clearable'
+    | 'value'
+    | 'defaultValue'
+    | 'onChange'
+    | 'onInteractOutside'
+    | 'closeOnSelect'
+    | 'onKeyDown'
+    | 'onKeyUp'
   > {
   onChange: (selected: Value[]) => void;
   value: Value[];
@@ -50,6 +60,7 @@ export function HybridFilter<Value extends React.Key>({
   value,
   onClear,
   onChange,
+  onSectionToggle,
   onReplace,
   onToggle,
   menuFooter,
@@ -237,7 +248,13 @@ export function HybridFilter<Value extends React.Key>({
   ]);
 
   const sectionToggleWasPressed = useRef(false);
-  const onSectionToggle = useCallback(() => (sectionToggleWasPressed.current = true), []);
+  const handleSectionToggle = useCallback(
+    (section: SelectSection<React.Key>) => {
+      onSectionToggle?.(section);
+      sectionToggleWasPressed.current = true;
+    },
+    [onSectionToggle]
+  );
 
   const handleChange = useCallback(
     (selectedOptions: SelectOption<Value>[]) => {
@@ -300,7 +317,7 @@ export function HybridFilter<Value extends React.Key>({
       value={stagedValue}
       onChange={handleChange}
       onClear={handleClear}
-      onSectionToggle={onSectionToggle}
+      onSectionToggle={handleSectionToggle}
       onInteractOutside={commitStagedChanges}
       menuFooter={renderFooter}
       onKeyDown={onKeyDown}

+ 49 - 25
static/app/components/organizations/projectPageFilter/index.tsx

@@ -34,15 +34,27 @@ import {DesyncedFilterMessage} from '../pageFilters/desyncedFilter';
 import {ProjectPageFilterMenuFooter} from './menuFooter';
 import {ProjectPageFilterTrigger} from './trigger';
 
-export interface ProjectPageFilterProps {
+export interface ProjectPageFilterProps
+  extends Partial<
+    Omit<
+      HybridFilterProps<number>,
+      | 'searchable'
+      | 'multiple'
+      | 'options'
+      | 'value'
+      | 'onReplace'
+      | 'onToggle'
+      | 'menuBody'
+      | 'menuFooter'
+      | 'menuFooterMessage'
+      | 'checkboxWrapper'
+      | 'shouldCloseOnInteractOutside'
+    >
+  > {
   /**
    * Message to show in the menu footer
    */
   footerMessage?: string;
-  /**
-   * Triggers any time a selection is changed, but the menu has not yet been closed or "applied"
-   */
-  onChange?: (selected: number[]) => void;
   /**
    * Reset these URL params when we fire actions (custom routing only)
    */
@@ -51,6 +63,14 @@ export interface ProjectPageFilterProps {
 
 export function ProjectPageFilter({
   onChange,
+  onClear,
+  disabled,
+  sizeLimit,
+  sizeLimitMessage,
+  emptyMessage,
+  menuTitle,
+  menuWidth,
+  trigger,
   resetParamsOnChange,
   footerMessage,
   ...selectProps
@@ -198,12 +218,13 @@ export function ProjectPageFilter({
     });
   }, [routes, organization]);
 
-  const onClear = useCallback(() => {
+  const handleClear = useCallback(() => {
+    onClear?.();
     trackAnalytics('projectselector.clear', {
       path: getRouteStringFromRoutes(routes),
       organization,
     });
-  }, [routes, organization]);
+  }, [onClear, routes, organization]);
 
   const options = useMemo<SelectOptionOrSection<number>[]>(() => {
     const hasProjects = !!memberProjects.length || !!nonMemberProjects.length;
@@ -286,7 +307,7 @@ export function ProjectPageFilter({
   ]);
 
   const desynced = desyncedFilters.has('projects');
-  const menuWidth = useMemo(() => {
+  const defaultMenuWidth = useMemo(() => {
     const flatOptions: SelectOption<number>[] = options.flatMap(item =>
       'options' in item ? item.options : [item]
     );
@@ -319,15 +340,15 @@ export function ProjectPageFilter({
       options={options}
       value={value}
       onChange={handleChange}
+      onClear={handleClear}
       onReplace={onReplace}
       onToggle={onToggle}
-      onClear={onClear}
-      disabled={!projectsLoaded || !pageFilterIsReady}
-      sizeLimit={25}
-      sizeLimitMessage={t('Use search to find more projects…')}
-      emptyMessage={t('No projects found')}
-      menuTitle={t('Filter Projects')}
-      menuWidth={menuWidth}
+      disabled={disabled ?? (!projectsLoaded || !pageFilterIsReady)}
+      sizeLimit={sizeLimit ?? 25}
+      sizeLimitMessage={sizeLimitMessage ?? t('Use search to find more projects…')}
+      emptyMessage={emptyMessage ?? t('No projects found')}
+      menuTitle={menuTitle ?? t('Filter Projects')}
+      menuWidth={menuWidth ?? defaultMenuWidth}
       menuBody={desynced && <DesyncedFilterMessage />}
       menuFooter={
         hasProjectWrite && (
@@ -338,16 +359,19 @@ export function ProjectPageFilter({
         )
       }
       menuFooterMessage={footerMessage}
-      trigger={triggerProps => (
-        <ProjectPageFilterTrigger
-          value={value}
-          memberProjects={memberProjects}
-          nonMemberProjects={nonMemberProjects}
-          ready={projectsLoaded && pageFilterIsReady}
-          desynced={desynced}
-          {...triggerProps}
-        />
-      )}
+      trigger={
+        trigger ??
+        (triggerProps => (
+          <ProjectPageFilterTrigger
+            value={value}
+            memberProjects={memberProjects}
+            nonMemberProjects={nonMemberProjects}
+            ready={projectsLoaded && pageFilterIsReady}
+            desynced={desynced}
+            {...triggerProps}
+          />
+        ))
+      }
       checkboxWrapper={checkboxWrapper}
       shouldCloseOnInteractOutside={shouldCloseOnInteractOutside}
     />

+ 40 - 18
static/app/components/timeRangeSelector.tsx

@@ -49,10 +49,21 @@ const SelectorItemsHook = HookOrDefault({
   defaultComponent: SelectorItems,
 });
 
-interface TimeRangeSelectorProps
+export interface TimeRangeSelectorProps
   extends Omit<
     SingleSelectProps<string>,
-    'options' | 'onChange' | 'closeOnSelect' | 'value' | 'defaultValue' | 'multiple'
+    | 'multiple'
+    | 'searchable'
+    | 'disableSearchFilter'
+    | 'options'
+    | 'hideOptions'
+    | 'value'
+    | 'defaultValue'
+    | 'onChange'
+    | 'onInteractOutside'
+    | 'closeOnSelect'
+    | 'menuFooter'
+    | 'onKeyDown'
   > {
   /**
    * Set an optional default value to prefill absolute date with
@@ -109,12 +120,16 @@ export function TimeRangeSelector({
   relative,
   relativeOptions,
   onChange,
+  onSearch,
+  onClose,
+  searchPlaceholder,
   showAbsolute = true,
   showRelative = true,
   defaultAbsolute,
   defaultPeriod = DEFAULT_STATS_PERIOD,
   maxPickableDays = 90,
   disallowArbitraryRelativeRanges = false,
+  trigger,
   menuWidth,
   menuBody,
   ...selectProps
@@ -243,11 +258,15 @@ export function TimeRangeSelector({
     >
       {items => (
         <CompactSelect
+          {...selectProps}
           searchable={!showAbsoluteSelector}
           disableSearchFilter
-          onSearch={setSearch}
+          onSearch={s => {
+            onSearch?.(s);
+            setSearch(s);
+          }}
           searchPlaceholder={
-            disallowArbitraryRelativeRanges
+            searchPlaceholder ?? disallowArbitraryRelativeRanges
               ? t('Search…')
               : t('Custom range: 2h, 4d, 8w…')
           }
@@ -258,25 +277,29 @@ export function TimeRangeSelector({
           // Keep menu open when clicking on absolute range option
           closeOnSelect={opt => opt.value !== ABSOLUTE_OPTION_VALUE}
           onClose={() => {
+            onClose?.();
             setHasChanges(false);
             setSearch('');
           }}
           onInteractOutside={commitChanges}
           onKeyDown={e => e.key === 'Escape' && commitChanges()}
-          trigger={triggerProps => {
-            const relativeSummary =
-              items.findIndex(item => item.value === relative) > -1
-                ? relative?.toUpperCase()
-                : t('Invalid Period');
-            const defaultLabel =
-              start && end ? getAbsoluteSummary(start, end, utc) : relativeSummary;
+          trigger={
+            trigger ??
+            (triggerProps => {
+              const relativeSummary =
+                items.findIndex(item => item.value === relative) > -1
+                  ? relative?.toUpperCase()
+                  : t('Invalid Period');
+              const defaultLabel =
+                start && end ? getAbsoluteSummary(start, end, utc) : relativeSummary;
 
-            return (
-              <DropdownButton icon={<IconCalendar />} {...triggerProps}>
-                <TriggerLabel>{selectProps.triggerLabel ?? defaultLabel}</TriggerLabel>
-              </DropdownButton>
-            );
-          }}
+              return (
+                <DropdownButton icon={<IconCalendar />} {...triggerProps}>
+                  <TriggerLabel>{selectProps.triggerLabel ?? defaultLabel}</TriggerLabel>
+                </DropdownButton>
+              );
+            })
+          }
           menuWidth={showAbsoluteSelector ? undefined : menuWidth ?? '16rem'}
           menuBody={
             (showAbsoluteSelector || menuBody) && (
@@ -362,7 +385,6 @@ export function TimeRangeSelector({
               </AbsoluteSelectorFooter>
             ))
           }
-          {...selectProps}
         />
       )}
     </SelectorItemsHook>