You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by mi...@apache.org on 2022/09/15 12:57:50 UTC

[superset] branch master updated: chore: Extract common select component code (#21094)

This is an automated email from the ASF dual-hosted git repository.

michaelsmolina pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new 4fcc1d952f chore: Extract common select component code (#21094)
4fcc1d952f is described below

commit 4fcc1d952f69ba35fed538a24d88f06317621ca6
Author: cccs-RyanK <10...@users.noreply.github.com>
AuthorDate: Thu Sep 15 08:57:37 2022 -0400

    chore: Extract common select component code (#21094)
---
 .../src/components/Select/AsyncSelect.tsx          | 385 +++++-------------
 .../src/components/Select/Select.stories.tsx       |  17 +-
 superset-frontend/src/components/Select/Select.tsx | 318 +++------------
 superset-frontend/src/components/Select/utils.ts   |  99 -----
 superset-frontend/src/components/Select/utils.tsx  | 443 +++++++++++++++++++++
 .../dashboard/components/RefreshIntervalModal.tsx  |   3 +-
 .../controls/SelectAsyncControl/index.tsx          |   7 +-
 .../components/Select/SelectFilterPlugin.tsx       |   2 +-
 .../src/views/CRUD/alert/AlertReportModal.tsx      |   2 +-
 9 files changed, 608 insertions(+), 668 deletions(-)

diff --git a/superset-frontend/src/components/Select/AsyncSelect.tsx b/superset-frontend/src/components/Select/AsyncSelect.tsx
index beeb7b262f..e3118f2284 100644
--- a/superset-frontend/src/components/Select/AsyncSelect.tsx
+++ b/superset-frontend/src/components/Select/AsyncSelect.tsx
@@ -19,7 +19,6 @@
 import React, {
   forwardRef,
   ReactElement,
-  ReactNode,
   RefObject,
   UIEvent,
   useEffect,
@@ -30,83 +29,62 @@ import React, {
   useImperativeHandle,
 } from 'react';
 import { ensureIsArray, styled, t } from '@superset-ui/core';
-import AntdSelect, {
-  SelectProps as AntdSelectProps,
-  SelectValue as AntdSelectValue,
-  LabeledValue as AntdLabeledValue,
-} from 'antd/lib/select';
-import { DownOutlined, SearchOutlined } from '@ant-design/icons';
-import { Spin } from 'antd';
+import { LabeledValue as AntdLabeledValue } from 'antd/lib/select';
 import debounce from 'lodash/debounce';
 import { isEqual } from 'lodash';
 import Icons from 'src/components/Icons';
 import { getClientErrorObject } from 'src/utils/getClientErrorObject';
 import { SLOW_DEBOUNCE } from 'src/constants';
-import { rankedSearchCompare } from 'src/utils/rankedSearchCompare';
-import { getValue, hasOption, isLabeledValue } from './utils';
-
-const { Option } = AntdSelect;
-
-type AntdSelectAllProps = AntdSelectProps<AntdSelectValue>;
-
-type PickedSelectProps = Pick<
-  AntdSelectAllProps,
-  | 'allowClear'
-  | 'autoFocus'
-  | 'disabled'
-  | 'filterOption'
-  | 'loading'
-  | 'notFoundContent'
-  | 'onChange'
-  | 'onClear'
-  | 'onFocus'
-  | 'onBlur'
-  | 'onDropdownVisibleChange'
-  | 'placeholder'
-  | 'showSearch'
-  | 'tokenSeparators'
-  | 'value'
-  | 'getPopupContainer'
->;
-
-export type OptionsType = Exclude<AntdSelectAllProps['options'], undefined>;
-
-export type OptionsTypePage = {
-  data: OptionsType;
-  totalCount: number;
-};
-
-export type OptionsPagePromise = (
-  search: string,
-  page: number,
-  pageSize: number,
-) => Promise<OptionsTypePage>;
+import {
+  getValue,
+  hasOption,
+  isLabeledValue,
+  DEFAULT_SORT_COMPARATOR,
+  EMPTY_OPTIONS,
+  MAX_TAG_COUNT,
+  SelectOptionsPagePromise,
+  SelectOptionsType,
+  SelectOptionsTypePage,
+  StyledCheckOutlined,
+  StyledStopOutlined,
+  TOKEN_SEPARATORS,
+  renderSelectOptions,
+  StyledContainer,
+  StyledSelect,
+  hasCustomLabels,
+  BaseSelectProps,
+  sortSelectedFirstHelper,
+  sortComparatorWithSearchHelper,
+  sortComparatorForNoSearchHelper,
+  getSuffixIcon,
+  dropDownRenderHelper,
+  handleFilterOptionHelper,
+} from './utils';
+
+const StyledError = styled.div`
+  ${({ theme }) => `
+    display: flex;
+    justify-content: center;
+    align-items: flex-start;
+    width: 100%;
+    padding: ${theme.gridUnit * 2}px;
+    color: ${theme.colors.error.base};
+    & svg {
+      margin-right: ${theme.gridUnit * 2}px;
+    }
+  `}
+`;
+
+const StyledErrorMessage = styled.div`
+  overflow: hidden;
+  text-overflow: ellipsis;
+`;
+
+const DEFAULT_PAGE_SIZE = 100;
 
 export type AsyncSelectRef = HTMLInputElement & { clearCache: () => void };
 
-export interface AsyncSelectProps extends PickedSelectProps {
-  /**
-   * It enables the user to create new options.
-   * Can be used with standard or async select types.
-   * Can be used with any mode, single or multiple.
-   * False by default.
-   * */
-  allowNewOptions?: boolean;
-  /**
-   * It adds the aria-label tag for accessibility standards.
-   * Must be plain English and localized.
-   */
-  ariaLabel: string;
-  /**
-   * It adds a header on top of the Select.
-   * Can be any ReactNode.
-   */
-  header?: ReactNode;
-  /**
-   * It adds a helper text on top of the Select options
-   * with additional context to help with the interaction.
-   */
-  helperText?: string;
+export interface AsyncSelectProps extends BaseSelectProps {
   /**
    * It fires a request against the server after
    * the first interaction and not on render.
@@ -114,43 +92,18 @@ export interface AsyncSelectProps extends PickedSelectProps {
    * True by default.
    */
   lazyLoading?: boolean;
-  /**
-   * It defines whether the Select should allow for the
-   * selection of multiple options or single.
-   * Single by default.
-   */
-  mode?: 'single' | 'multiple';
-  /**
-   * Deprecated.
-   * Prefer ariaLabel instead.
-   */
-  name?: string; // discourage usage
-  /**
-   * It allows to define which properties of the option object
-   * should be looked for when searching.
-   * By default label and value.
-   */
-  optionFilterProps?: string[];
   /**
    * It defines the options of the Select.
    * The options are async, a promise that returns
    * an array of options.
    */
-  options: OptionsPagePromise;
+  options: SelectOptionsPagePromise;
   /**
    * It defines how many results should be included
    * in the query response.
    * Works in async mode only (See the options property).
    */
   pageSize?: number;
-  /**
-   * It shows a stop-outlined icon at the far right of a selected
-   * option instead of the default checkmark.
-   * Useful to better indicate to the user that by clicking on a selected
-   * option it will be de-selected.
-   * False by default.
-   */
-  invertSelection?: boolean;
   /**
    * It fires a request against the server only after
    * searching.
@@ -164,131 +117,14 @@ export interface AsyncSelectProps extends PickedSelectProps {
    * Works in async mode only (See the options property).
    */
   onError?: (error: string) => void;
-  /**
-   * Customize how filtered options are sorted while users search.
-   * Will not apply to predefined `options` array when users are not searching.
-   */
-  sortComparator?: typeof DEFAULT_SORT_COMPARATOR;
 }
 
-const StyledContainer = styled.div`
-  display: flex;
-  flex-direction: column;
-  width: 100%;
-`;
-
-const StyledSelect = styled(AntdSelect)`
-  ${({ theme }) => `
-    && .ant-select-selector {
-      border-radius: ${theme.gridUnit}px;
-    }
-    // Open the dropdown when clicking on the suffix
-    // This is fixed in version 4.16
-    .ant-select-arrow .anticon:not(.ant-select-suffix) {
-      pointer-events: none;
-    }
-    .ant-select-dropdown {
-      padding: 0;
-    }
-  `}
-`;
-
-const StyledStopOutlined = styled(Icons.StopOutlined)`
-  vertical-align: 0;
-`;
-
-const StyledCheckOutlined = styled(Icons.CheckOutlined)`
-  vertical-align: 0;
-`;
-
-const StyledError = styled.div`
-  ${({ theme }) => `
-    display: flex;
-    justify-content: center;
-    align-items: flex-start;
-    width: 100%;
-    padding: ${theme.gridUnit * 2}px;
-    color: ${theme.colors.error.base};
-    & svg {
-      margin-right: ${theme.gridUnit * 2}px;
-    }
-  `}
-`;
-
-const StyledErrorMessage = styled.div`
-  overflow: hidden;
-  text-overflow: ellipsis;
-`;
-
-const StyledSpin = styled(Spin)`
-  margin-top: ${({ theme }) => -theme.gridUnit}px;
-`;
-
-const StyledLoadingText = styled.div`
-  ${({ theme }) => `
-    margin-left: ${theme.gridUnit * 3}px;
-    line-height: ${theme.gridUnit * 8}px;
-    color: ${theme.colors.grayscale.light1};
-  `}
-`;
-
-const StyledHelperText = styled.div`
-  ${({ theme }) => `
-    padding: ${theme.gridUnit * 2}px ${theme.gridUnit * 3}px;
-    color: ${theme.colors.grayscale.base};
-    font-size: ${theme.typography.sizes.s}px;
-    cursor: default;
-    border-bottom: 1px solid ${theme.colors.grayscale.light2};
-  `}
-`;
-
-const MAX_TAG_COUNT = 4;
-const TOKEN_SEPARATORS = [',', '\n', '\t', ';'];
-const DEFAULT_PAGE_SIZE = 100;
-const EMPTY_OPTIONS: OptionsType = [];
-
 const Error = ({ error }: { error: string }) => (
   <StyledError>
     <Icons.ErrorSolid /> <StyledErrorMessage>{error}</StyledErrorMessage>
   </StyledError>
 );
 
-export const DEFAULT_SORT_COMPARATOR = (
-  a: AntdLabeledValue,
-  b: AntdLabeledValue,
-  search?: string,
-) => {
-  let aText: string | undefined;
-  let bText: string | undefined;
-  if (typeof a.label === 'string' && typeof b.label === 'string') {
-    aText = a.label;
-    bText = b.label;
-  } else if (typeof a.value === 'string' && typeof b.value === 'string') {
-    aText = a.value;
-    bText = b.value;
-  }
-  // sort selected options first
-  if (typeof aText === 'string' && typeof bText === 'string') {
-    if (search) {
-      return rankedSearchCompare(aText, bText, search);
-    }
-    return aText.localeCompare(bText);
-  }
-  return (a.value as number) - (b.value as number);
-};
-
-/**
- * It creates a comparator to check for a specific property.
- * Can be used with string and number property values.
- * */
-export const propertyComparator =
-  (property: string) => (a: AntdLabeledValue, b: AntdLabeledValue) => {
-    if (typeof a[property] === 'string' && typeof b[property] === 'string') {
-      return a[property].localeCompare(b[property]);
-    }
-    return (a[property] as number) - (b[property] as number);
-  };
-
 const getQueryCacheKey = (value: string, page: number, pageSize: number) =>
   `${value};${page};${pageSize}`;
 
@@ -359,23 +195,30 @@ const AsyncSelect = forwardRef(
 
     const sortSelectedFirst = useCallback(
       (a: AntdLabeledValue, b: AntdLabeledValue) =>
-        selectValue && a.value !== undefined && b.value !== undefined
-          ? Number(hasOption(b.value, selectValue)) -
-            Number(hasOption(a.value, selectValue))
-          : 0,
+        sortSelectedFirstHelper(a, b, selectValue),
       [selectValue],
     );
+
     const sortComparatorWithSearch = useCallback(
       (a: AntdLabeledValue, b: AntdLabeledValue) =>
-        sortSelectedFirst(a, b) || sortComparator(a, b, inputValue),
+        sortComparatorWithSearchHelper(
+          a,
+          b,
+          inputValue,
+          sortSelectedFirst,
+          sortComparator,
+        ),
       [inputValue, sortComparator, sortSelectedFirst],
     );
+
     const sortComparatorForNoSearch = useCallback(
       (a: AntdLabeledValue, b: AntdLabeledValue) =>
-        sortSelectedFirst(a, b) ||
-        // Only apply the custom sorter in async mode because we should
-        // preserve the options order as much as possible.
-        sortComparator(a, b, ''),
+        sortComparatorForNoSearchHelper(
+          a,
+          b,
+          sortSelectedFirst,
+          sortComparator,
+        ),
       [sortComparator, sortSelectedFirst],
     );
 
@@ -390,11 +233,11 @@ const AsyncSelect = forwardRef(
     );
 
     const [selectOptions, setSelectOptions] =
-      useState<OptionsType>(initialOptionsSorted);
+      useState<SelectOptionsType>(initialOptionsSorted);
 
     // add selected values to options list if they are not in it
     const fullSelectOptions = useMemo(() => {
-      const missingValues: OptionsType = ensureIsArray(selectValue)
+      const missingValues: SelectOptionsType = ensureIsArray(selectValue)
         .filter(opt => !hasOption(getValue(opt), selectOptions))
         .map(opt =>
           isLabeledValue(opt) ? opt : { value: opt, label: String(opt) },
@@ -404,8 +247,6 @@ const AsyncSelect = forwardRef(
         : selectOptions;
     }, [selectOptions, selectValue]);
 
-    const hasCustomLabels = fullSelectOptions.some(opt => !!opt?.customLabel);
-
     const handleOnSelect = (
       selectedItem: string | number | AntdLabeledValue | undefined,
     ) => {
@@ -459,8 +300,8 @@ const AsyncSelect = forwardRef(
     );
 
     const mergeData = useCallback(
-      (data: OptionsType) => {
-        let mergedData: OptionsType = [];
+      (data: SelectOptionsType) => {
+        let mergedData: SelectOptionsType = [];
         if (data && Array.isArray(data) && data.length) {
           // unique option values should always be case sensitive so don't lowercase
           const dataValues = new Set(data.map(opt => opt.value));
@@ -493,9 +334,9 @@ const AsyncSelect = forwardRef(
           return;
         }
         setIsLoading(true);
-        const fetchOptions = options as OptionsPagePromise;
+        const fetchOptions = options as SelectOptionsPagePromise;
         fetchOptions(search, page, pageSize)
-          .then(({ data, totalCount }: OptionsTypePage) => {
+          .then(({ data, totalCount }: SelectOptionsTypePage) => {
             const mergedData = mergeData(data);
             fetchedQueries.current.set(key, totalCount);
             setTotalCount(totalCount);
@@ -569,25 +410,8 @@ const AsyncSelect = forwardRef(
       }
     };
 
-    const handleFilterOption = (search: string, option: AntdLabeledValue) => {
-      if (typeof filterOption === 'function') {
-        return filterOption(search, option);
-      }
-
-      if (filterOption) {
-        const searchValue = search.trim().toLowerCase();
-        if (optionFilterProps && optionFilterProps.length) {
-          return optionFilterProps.some(prop => {
-            const optionProp = option?.[prop]
-              ? String(option[prop]).trim().toLowerCase()
-              : '';
-            return optionProp.includes(searchValue);
-          });
-        }
-      }
-
-      return false;
-    };
+    const handleFilterOption = (search: string, option: AntdLabeledValue) =>
+      handleFilterOptionHelper(search, option, optionFilterProps, filterOption);
 
     const handleOnDropdownVisibleChange = (isDropdownVisible: boolean) => {
       setIsDropdownVisible(isDropdownVisible);
@@ -624,36 +448,15 @@ const AsyncSelect = forwardRef(
 
     const dropdownRender = (
       originNode: ReactElement & { ref?: RefObject<HTMLElement> },
-    ) => {
-      if (!isDropdownVisible) {
-        originNode.ref?.current?.scrollTo({ top: 0 });
-      }
-      if (isLoading && fullSelectOptions.length === 0) {
-        return <StyledLoadingText>{t('Loading...')}</StyledLoadingText>;
-      }
-      return error ? (
-        <Error error={error} />
-      ) : (
-        <>
-          {helperText && (
-            <StyledHelperText role="note">{helperText}</StyledHelperText>
-          )}
-          {originNode}
-        </>
+    ) =>
+      dropDownRenderHelper(
+        originNode,
+        isDropdownVisible,
+        isLoading,
+        fullSelectOptions.length,
+        helperText,
+        error ? <Error error={error} /> : undefined,
       );
-    };
-
-    // use a function instead of component since every rerender of the
-    // Select component will create a new component
-    const getSuffixIcon = () => {
-      if (isLoading) {
-        return <StyledSpin size="small" />;
-      }
-      if (showSearch && isDropdownVisible) {
-        return <SearchOutlined />;
-      }
-      return <DownOutlined />;
-    };
 
     const handleClear = () => {
       setSelectValue(undefined);
@@ -709,6 +512,10 @@ const AsyncSelect = forwardRef(
       [ref],
     );
 
+    useEffect(() => {
+      setSelectValue(value);
+    }, [value]);
+
     return (
       <StyledContainer>
         {header}
@@ -732,13 +539,15 @@ const AsyncSelect = forwardRef(
           onSelect={handleOnSelect}
           onClear={handleClear}
           onChange={onChange}
-          options={hasCustomLabels ? undefined : fullSelectOptions}
+          options={
+            hasCustomLabels(fullSelectOptions) ? undefined : fullSelectOptions
+          }
           placeholder={placeholder}
           showSearch={showSearch}
           showArrow
           tokenSeparators={tokenSeparators || TOKEN_SEPARATORS}
           value={selectValue}
-          suffixIcon={getSuffixIcon()}
+          suffixIcon={getSuffixIcon(isLoading, showSearch, isDropdownVisible)}
           menuItemSelectedIcon={
             invertSelection ? (
               <StyledStopOutlined iconSize="m" />
@@ -746,21 +555,11 @@ const AsyncSelect = forwardRef(
               <StyledCheckOutlined iconSize="m" />
             )
           }
-          ref={ref}
           {...props}
+          ref={ref}
         >
-          {hasCustomLabels &&
-            fullSelectOptions.map(opt => {
-              const isOptObject = typeof opt === 'object';
-              const label = isOptObject ? opt?.label || opt.value : opt;
-              const value = isOptObject ? opt.value : opt;
-              const { customLabel, ...optProps } = opt;
-              return (
-                <Option {...optProps} key={value} label={label} value={value}>
-                  {isOptObject && customLabel ? customLabel : label}
-                </Option>
-              );
-            })}
+          {hasCustomLabels(fullSelectOptions) &&
+            renderSelectOptions(fullSelectOptions)}
         </StyledSelect>
       </StyledContainer>
     );
diff --git a/superset-frontend/src/components/Select/Select.stories.tsx b/superset-frontend/src/components/Select/Select.stories.tsx
index b75e1ff28b..e9a03fe563 100644
--- a/superset-frontend/src/components/Select/Select.stories.tsx
+++ b/superset-frontend/src/components/Select/Select.stories.tsx
@@ -25,13 +25,10 @@ import React, {
 } from 'react';
 import Button from 'src/components/Button';
 import ControlHeader from 'src/explore/components/ControlHeader';
-import AsyncSelect, {
-  AsyncSelectProps,
-  AsyncSelectRef,
-  OptionsTypePage,
-} from './AsyncSelect';
+import AsyncSelect, { AsyncSelectProps, AsyncSelectRef } from './AsyncSelect';
+import { SelectOptionsType, SelectOptionsTypePage } from './utils';
 
-import Select, { SelectProps, OptionsType } from './Select';
+import Select, { SelectProps } from './Select';
 
 export default {
   title: 'Select',
@@ -40,7 +37,7 @@ export default {
 
 const DEFAULT_WIDTH = 200;
 
-const options: OptionsType = [
+const options: SelectOptionsType = [
   {
     label: 'Such an incredibly awesome long long label',
     value: 'Such an incredibly awesome long long label',
@@ -160,7 +157,7 @@ const mountHeader = (type: String) => {
   return header;
 };
 
-const generateOptions = (opts: OptionsType, count: number) => {
+const generateOptions = (opts: SelectOptionsType, count: number) => {
   let generated = opts.slice();
   let iteration = 0;
   while (generated.length < count) {
@@ -440,7 +437,7 @@ export const AsynchronousSelect = ({
       search: string,
       page: number,
       pageSize: number,
-    ): Promise<OptionsTypePage> => {
+    ): Promise<SelectOptionsTypePage> => {
       const username = search.trim().toLowerCase();
       return new Promise(resolve => {
         let results = getResults(username);
@@ -458,7 +455,7 @@ export const AsynchronousSelect = ({
     [responseTime],
   );
 
-  const fetchUserListError = async (): Promise<OptionsTypePage> =>
+  const fetchUserListError = async (): Promise<SelectOptionsTypePage> =>
     new Promise((_, reject) => {
       reject(new Error('Error while fetching the names from the server'));
     });
diff --git a/superset-frontend/src/components/Select/Select.tsx b/superset-frontend/src/components/Select/Select.tsx
index e668682b5d..6b6713852d 100644
--- a/superset-frontend/src/components/Select/Select.tsx
+++ b/superset-frontend/src/components/Select/Select.tsx
@@ -19,207 +19,48 @@
 import React, {
   forwardRef,
   ReactElement,
-  ReactNode,
   RefObject,
   useEffect,
   useMemo,
   useState,
   useCallback,
 } from 'react';
-import { ensureIsArray, styled, t } from '@superset-ui/core';
-import AntdSelect, {
-  SelectProps as AntdSelectProps,
-  SelectValue as AntdSelectValue,
-  LabeledValue as AntdLabeledValue,
-} from 'antd/lib/select';
-import { DownOutlined, SearchOutlined } from '@ant-design/icons';
-import { Spin } from 'antd';
+import { ensureIsArray, t } from '@superset-ui/core';
+import { LabeledValue as AntdLabeledValue } from 'antd/lib/select';
 import { isEqual } from 'lodash';
-import Icons from 'src/components/Icons';
-import { rankedSearchCompare } from 'src/utils/rankedSearchCompare';
-import { getValue, hasOption, isLabeledValue } from './utils';
-
-const { Option } = AntdSelect;
-
-type AntdSelectAllProps = AntdSelectProps<AntdSelectValue>;
-
-type PickedSelectProps = Pick<
-  AntdSelectAllProps,
-  | 'allowClear'
-  | 'autoFocus'
-  | 'disabled'
-  | 'filterOption'
-  | 'labelInValue'
-  | 'loading'
-  | 'notFoundContent'
-  | 'onChange'
-  | 'onClear'
-  | 'onFocus'
-  | 'onBlur'
-  | 'onDropdownVisibleChange'
-  | 'placeholder'
-  | 'showSearch'
-  | 'tokenSeparators'
-  | 'value'
-  | 'getPopupContainer'
->;
-
-export type OptionsType = Exclude<AntdSelectAllProps['options'], undefined>;
-
-export interface SelectProps extends PickedSelectProps {
-  /**
-   * It enables the user to create new options.
-   * Can be used with standard or async select types.
-   * Can be used with any mode, single or multiple.
-   * False by default.
-   * */
-  allowNewOptions?: boolean;
-  /**
-   * It adds the aria-label tag for accessibility standards.
-   * Must be plain English and localized.
-   */
-  ariaLabel: string;
-  /**
-   * It adds a header on top of the Select.
-   * Can be any ReactNode.
-   */
-  header?: ReactNode;
-  /**
-   * It adds a helper text on top of the Select options
-   * with additional context to help with the interaction.
-   */
-  helperText?: string;
-  /**
-   * It defines whether the Select should allow for the
-   * selection of multiple options or single.
-   * Single by default.
-   */
-  mode?: 'single' | 'multiple';
-  /**
-   * Deprecated.
-   * Prefer ariaLabel instead.
-   */
-  name?: string; // discourage usage
-  /**
-   * It allows to define which properties of the option object
-   * should be looked for when searching.
-   * By default label and value.
-   */
-  optionFilterProps?: string[];
+import {
+  getValue,
+  hasOption,
+  isLabeledValue,
+  DEFAULT_SORT_COMPARATOR,
+  EMPTY_OPTIONS,
+  MAX_TAG_COUNT,
+  SelectOptionsType,
+  StyledCheckOutlined,
+  StyledStopOutlined,
+  TOKEN_SEPARATORS,
+  renderSelectOptions,
+  StyledSelect,
+  StyledContainer,
+  hasCustomLabels,
+  BaseSelectProps,
+  sortSelectedFirstHelper,
+  sortComparatorWithSearchHelper,
+  handleFilterOptionHelper,
+  dropDownRenderHelper,
+  getSuffixIcon,
+} from './utils';
+
+export interface SelectProps extends BaseSelectProps {
   /**
    * It defines the options of the Select.
    * The options can be static, an array of options.
    * The options can also be async, a promise that returns
    * an array of options.
    */
-  options: OptionsType;
-  /**
-   * It shows a stop-outlined icon at the far right of a selected
-   * option instead of the default checkmark.
-   * Useful to better indicate to the user that by clicking on a selected
-   * option it will be de-selected.
-   * False by default.
-   */
-  invertSelection?: boolean;
-  /**
-   * Customize how filtered options are sorted while users search.
-   * Will not apply to predefined `options` array when users are not searching.
-   */
-  sortComparator?: typeof DEFAULT_SORT_COMPARATOR;
+  options: SelectOptionsType;
 }
 
-const StyledContainer = styled.div`
-  display: flex;
-  flex-direction: column;
-  width: 100%;
-`;
-
-const StyledSelect = styled(AntdSelect)`
-  ${({ theme }) => `
-    && .ant-select-selector {
-      border-radius: ${theme.gridUnit}px;
-    }
-    // Open the dropdown when clicking on the suffix
-    // This is fixed in version 4.16
-    .ant-select-arrow .anticon:not(.ant-select-suffix) {
-      pointer-events: none;
-    }
-    .ant-select-dropdown {
-      padding: 0;
-    }
-  `}
-`;
-
-const StyledStopOutlined = styled(Icons.StopOutlined)`
-  vertical-align: 0;
-`;
-
-const StyledCheckOutlined = styled(Icons.CheckOutlined)`
-  vertical-align: 0;
-`;
-
-const StyledSpin = styled(Spin)`
-  margin-top: ${({ theme }) => -theme.gridUnit}px;
-`;
-
-const StyledLoadingText = styled.div`
-  ${({ theme }) => `
-    margin-left: ${theme.gridUnit * 3}px;
-    line-height: ${theme.gridUnit * 8}px;
-    color: ${theme.colors.grayscale.light1};
-  `}
-`;
-
-const StyledHelperText = styled.div`
-  ${({ theme }) => `
-    padding: ${theme.gridUnit * 2}px ${theme.gridUnit * 3}px;
-    color: ${theme.colors.grayscale.base};
-    font-size: ${theme.typography.sizes.s}px;
-    cursor: default;
-    border-bottom: 1px solid ${theme.colors.grayscale.light2};
-  `}
-`;
-
-const MAX_TAG_COUNT = 4;
-const TOKEN_SEPARATORS = [',', '\n', '\t', ';'];
-const EMPTY_OPTIONS: OptionsType = [];
-
-export const DEFAULT_SORT_COMPARATOR = (
-  a: AntdLabeledValue,
-  b: AntdLabeledValue,
-  search?: string,
-) => {
-  let aText: string | undefined;
-  let bText: string | undefined;
-  if (typeof a.label === 'string' && typeof b.label === 'string') {
-    aText = a.label;
-    bText = b.label;
-  } else if (typeof a.value === 'string' && typeof b.value === 'string') {
-    aText = a.value;
-    bText = b.value;
-  }
-  // sort selected options first
-  if (typeof aText === 'string' && typeof bText === 'string') {
-    if (search) {
-      return rankedSearchCompare(aText, bText, search);
-    }
-    return aText.localeCompare(bText);
-  }
-  return (a.value as number) - (b.value as number);
-};
-
-/**
- * It creates a comparator to check for a specific property.
- * Can be used with string and number property values.
- * */
-export const propertyComparator =
-  (property: string) => (a: AntdLabeledValue, b: AntdLabeledValue) => {
-    if (typeof a[property] === 'string' && typeof b[property] === 'string') {
-      return a[property].localeCompare(b[property]);
-    }
-    return (a[property] as number) - (b[property] as number);
-  };
-
 /**
  * This component is a customized version of the Antdesign 4.X Select component
  * https://ant.design/components/select/.
@@ -278,15 +119,18 @@ const Select = forwardRef(
 
     const sortSelectedFirst = useCallback(
       (a: AntdLabeledValue, b: AntdLabeledValue) =>
-        selectValue && a.value !== undefined && b.value !== undefined
-          ? Number(hasOption(b.value, selectValue)) -
-            Number(hasOption(a.value, selectValue))
-          : 0,
+        sortSelectedFirstHelper(a, b, selectValue),
       [selectValue],
     );
     const sortComparatorWithSearch = useCallback(
       (a: AntdLabeledValue, b: AntdLabeledValue) =>
-        sortSelectedFirst(a, b) || sortComparator(a, b, inputValue),
+        sortComparatorWithSearchHelper(
+          a,
+          b,
+          inputValue,
+          sortSelectedFirst,
+          sortComparator,
+        ),
       [inputValue, sortComparator, sortSelectedFirst],
     );
 
@@ -301,11 +145,11 @@ const Select = forwardRef(
     );
 
     const [selectOptions, setSelectOptions] =
-      useState<OptionsType>(initialOptionsSorted);
+      useState<SelectOptionsType>(initialOptionsSorted);
 
     // add selected values to options list if they are not in it
     const fullSelectOptions = useMemo(() => {
-      const missingValues: OptionsType = ensureIsArray(selectValue)
+      const missingValues: SelectOptionsType = ensureIsArray(selectValue)
         .filter(opt => !hasOption(getValue(opt), selectOptions))
         .map(opt =>
           isLabeledValue(opt) ? opt : { value: opt, label: String(opt) },
@@ -315,8 +159,6 @@ const Select = forwardRef(
         : selectOptions;
     }, [selectOptions, selectValue]);
 
-    const hasCustomLabels = fullSelectOptions.some(opt => !!opt?.customLabel);
-
     const handleOnSelect = (
       selectedItem: string | number | AntdLabeledValue | undefined,
     ) => {
@@ -376,25 +218,8 @@ const Select = forwardRef(
       setInputValue(search);
     };
 
-    const handleFilterOption = (search: string, option: AntdLabeledValue) => {
-      if (typeof filterOption === 'function') {
-        return filterOption(search, option);
-      }
-
-      if (filterOption) {
-        const searchValue = search.trim().toLowerCase();
-        if (optionFilterProps && optionFilterProps.length) {
-          return optionFilterProps.some(prop => {
-            const optionProp = option?.[prop]
-              ? String(option[prop]).trim().toLowerCase()
-              : '';
-            return optionProp.includes(searchValue);
-          });
-        }
-      }
-
-      return false;
-    };
+    const handleFilterOption = (search: string, option: AntdLabeledValue) =>
+      handleFilterOptionHelper(search, option, optionFilterProps, filterOption);
 
     const handleOnDropdownVisibleChange = (isDropdownVisible: boolean) => {
       setIsDropdownVisible(isDropdownVisible);
@@ -413,34 +238,14 @@ const Select = forwardRef(
 
     const dropdownRender = (
       originNode: ReactElement & { ref?: RefObject<HTMLElement> },
-    ) => {
-      if (!isDropdownVisible) {
-        originNode.ref?.current?.scrollTo({ top: 0 });
-      }
-      if (isLoading && fullSelectOptions.length === 0) {
-        return <StyledLoadingText>{t('Loading...')}</StyledLoadingText>;
-      }
-      return (
-        <>
-          {helperText && (
-            <StyledHelperText role="note">{helperText}</StyledHelperText>
-          )}
-          {originNode}
-        </>
+    ) =>
+      dropDownRenderHelper(
+        originNode,
+        isDropdownVisible,
+        isLoading,
+        fullSelectOptions.length,
+        helperText,
       );
-    };
-
-    // use a function instead of component since every rerender of the
-    // Select component will create a new component
-    const getSuffixIcon = () => {
-      if (isLoading) {
-        return <StyledSpin size="small" />;
-      }
-      if (shouldShowSearch && isDropdownVisible) {
-        return <SearchOutlined />;
-      }
-      return <DownOutlined />;
-    };
 
     const handleClear = () => {
       setSelectValue(undefined);
@@ -454,16 +259,16 @@ const Select = forwardRef(
       setSelectOptions(initialOptions);
     }, [initialOptions]);
 
-    useEffect(() => {
-      setSelectValue(value);
-    }, [value]);
-
     useEffect(() => {
       if (loading !== undefined && loading !== isLoading) {
         setIsLoading(loading);
       }
     }, [isLoading, loading]);
 
+    useEffect(() => {
+      setSelectValue(value);
+    }, [value]);
+
     return (
       <StyledContainer>
         {header}
@@ -487,13 +292,17 @@ const Select = forwardRef(
           onSelect={handleOnSelect}
           onClear={handleClear}
           onChange={onChange}
-          options={hasCustomLabels ? undefined : fullSelectOptions}
+          options={hasCustomLabels(options) ? undefined : fullSelectOptions}
           placeholder={placeholder}
           showSearch={shouldShowSearch}
           showArrow
           tokenSeparators={tokenSeparators || TOKEN_SEPARATORS}
           value={selectValue}
-          suffixIcon={getSuffixIcon()}
+          suffixIcon={getSuffixIcon(
+            isLoading,
+            shouldShowSearch,
+            isDropdownVisible,
+          )}
           menuItemSelectedIcon={
             invertSelection ? (
               <StyledStopOutlined iconSize="m" />
@@ -501,21 +310,10 @@ const Select = forwardRef(
               <StyledCheckOutlined iconSize="m" />
             )
           }
-          ref={ref}
           {...props}
+          ref={ref}
         >
-          {hasCustomLabels &&
-            fullSelectOptions.map(opt => {
-              const isOptObject = typeof opt === 'object';
-              const label = isOptObject ? opt?.label || opt.value : opt;
-              const value = isOptObject ? opt.value : opt;
-              const { customLabel, ...optProps } = opt;
-              return (
-                <Option {...optProps} key={value} label={label} value={value}>
-                  {isOptObject && customLabel ? customLabel : label}
-                </Option>
-              );
-            })}
+          {hasCustomLabels(options) && renderSelectOptions(fullSelectOptions)}
         </StyledSelect>
       </StyledContainer>
     );
diff --git a/superset-frontend/src/components/Select/utils.ts b/superset-frontend/src/components/Select/utils.ts
deleted file mode 100644
index 9836b9ddd2..0000000000
--- a/superset-frontend/src/components/Select/utils.ts
+++ /dev/null
@@ -1,99 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-import { ReactNode } from 'react';
-import { ensureIsArray } from '@superset-ui/core';
-import {
-  OptionTypeBase,
-  ValueType,
-  OptionsType,
-  GroupedOptionsType,
-} from 'react-select';
-import { LabeledValue as AntdLabeledValue } from 'antd/lib/select';
-
-export function isObject(value: unknown): value is Record<string, unknown> {
-  return (
-    value !== null &&
-    typeof value === 'object' &&
-    Array.isArray(value) === false
-  );
-}
-
-/**
- * Find Option value that matches a possibly string value.
- *
- * Translate possible string values to `OptionType` objects, fallback to value
- * itself if cannot be found in the options list.
- *
- * Always returns an array.
- */
-export function findValue<OptionType extends OptionTypeBase>(
-  value: ValueType<OptionType> | string,
-  options: GroupedOptionsType<OptionType> | OptionsType<OptionType> = [],
-  valueKey = 'value',
-): OptionType[] {
-  if (value === null || value === undefined || value === '') {
-    return [];
-  }
-  const isGroup = Array.isArray((options[0] || {}).options);
-  const flatOptions = isGroup
-    ? (options as GroupedOptionsType<OptionType>).flatMap(x => x.options || [])
-    : (options as OptionsType<OptionType>);
-
-  const find = (val: OptionType) => {
-    const realVal = (value || {}).hasOwnProperty(valueKey)
-      ? val[valueKey]
-      : val;
-    return (
-      flatOptions.find(x => x === realVal || x[valueKey] === realVal) || val
-    );
-  };
-
-  // If value is a single string, must return an Array so `cleanValue` won't be
-  // empty: https://github.com/JedWatson/react-select/blob/32ad5c040bdd96cd1ca71010c2558842d684629c/packages/react-select/src/utils.js#L64
-  return (Array.isArray(value) ? value : [value]).map(find);
-}
-
-export function isLabeledValue(value: unknown): value is AntdLabeledValue {
-  return isObject(value) && 'value' in value && 'label' in value;
-}
-
-export function getValue(
-  option: string | number | AntdLabeledValue | null | undefined,
-) {
-  return isLabeledValue(option) ? option.value : option;
-}
-
-type LabeledValue<V> = { label?: ReactNode; value?: V };
-
-export function hasOption<V>(
-  value: V,
-  options?: V | LabeledValue<V> | (V | LabeledValue<V>)[],
-  checkLabel = false,
-): boolean {
-  const optionsArray = ensureIsArray(options);
-  return (
-    optionsArray.find(
-      x =>
-        x === value ||
-        (isObject(x) &&
-          (('value' in x && x.value === value) ||
-            (checkLabel && 'label' in x && x.label === value))),
-    ) !== undefined
-  );
-}
diff --git a/superset-frontend/src/components/Select/utils.tsx b/superset-frontend/src/components/Select/utils.tsx
new file mode 100644
index 0000000000..9916ef07e2
--- /dev/null
+++ b/superset-frontend/src/components/Select/utils.tsx
@@ -0,0 +1,443 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { ensureIsArray, styled, t } from '@superset-ui/core';
+import { Spin } from 'antd';
+import Icons from 'src/components/Icons';
+import AntdSelect, {
+  SelectProps as AntdSelectProps,
+  SelectValue as AntdSelectValue,
+  LabeledValue as AntdLabeledValue,
+} from 'antd/lib/select';
+import { rankedSearchCompare } from 'src/utils/rankedSearchCompare';
+import {
+  OptionTypeBase,
+  ValueType,
+  OptionsType,
+  GroupedOptionsType,
+} from 'react-select';
+import React, {
+  ReactElement,
+  ReactNode,
+  RefObject,
+  JSXElementConstructor,
+} from 'react';
+import { DownOutlined, SearchOutlined } from '@ant-design/icons';
+
+declare type RawValue = string | number;
+
+const { Option } = AntdSelect;
+
+export function isObject(value: unknown): value is Record<string, unknown> {
+  return (
+    value !== null &&
+    typeof value === 'object' &&
+    Array.isArray(value) === false
+  );
+}
+
+/**
+ * Find Option value that matches a possibly string value.
+ *
+ * Translate possible string values to `OptionType` objects, fallback to value
+ * itself if cannot be found in the options list.
+ *
+ * Always returns an array.
+ */
+export function findValue<OptionType extends OptionTypeBase>(
+  value: ValueType<OptionType> | string,
+  options: GroupedOptionsType<OptionType> | OptionsType<OptionType> = [],
+  valueKey = 'value',
+): OptionType[] {
+  if (value === null || value === undefined || value === '') {
+    return [];
+  }
+  const isGroup = Array.isArray((options[0] || {}).options);
+  const flatOptions = isGroup
+    ? (options as GroupedOptionsType<OptionType>).flatMap(x => x.options || [])
+    : (options as OptionsType<OptionType>);
+
+  const find = (val: OptionType) => {
+    const realVal = (value || {}).hasOwnProperty(valueKey)
+      ? val[valueKey]
+      : val;
+    return (
+      flatOptions.find(x => x === realVal || x[valueKey] === realVal) || val
+    );
+  };
+
+  // If value is a single string, must return an Array so `cleanValue` won't be
+  // empty: https://github.com/JedWatson/react-select/blob/32ad5c040bdd96cd1ca71010c2558842d684629c/packages/react-select/src/utils.js#L64
+  return (Array.isArray(value) ? value : [value]).map(find);
+}
+
+export function isLabeledValue(value: unknown): value is AntdLabeledValue {
+  return isObject(value) && 'value' in value && 'label' in value;
+}
+
+export function getValue(
+  option: string | number | AntdLabeledValue | null | undefined,
+) {
+  return isLabeledValue(option) ? option.value : option;
+}
+
+type LabeledValue<V> = { label?: ReactNode; value?: V };
+
+export function hasOption<V>(
+  value: V,
+  options?: V | LabeledValue<V> | (V | LabeledValue<V>)[],
+  checkLabel = false,
+): boolean {
+  const optionsArray = ensureIsArray(options);
+  return (
+    optionsArray.find(
+      x =>
+        x === value ||
+        (isObject(x) &&
+          (('value' in x && x.value === value) ||
+            (checkLabel && 'label' in x && x.label === value))),
+    ) !== undefined
+  );
+}
+
+export type AntdProps = AntdSelectProps<AntdSelectValue>;
+
+export type AntdExposedProps = Pick<
+  AntdProps,
+  | 'allowClear'
+  | 'autoFocus'
+  | 'disabled'
+  | 'filterOption'
+  | 'filterSort'
+  | 'loading'
+  | 'labelInValue'
+  | 'maxTagCount'
+  | 'notFoundContent'
+  | 'onChange'
+  | 'onClear'
+  | 'onDeselect'
+  | 'onSelect'
+  | 'onFocus'
+  | 'onBlur'
+  | 'onPopupScroll'
+  | 'onSearch'
+  | 'onDropdownVisibleChange'
+  | 'placeholder'
+  | 'showArrow'
+  | 'showSearch'
+  | 'tokenSeparators'
+  | 'value'
+  | 'getPopupContainer'
+  | 'menuItemSelectedIcon'
+>;
+
+export type SelectOptionsType = Exclude<AntdProps['options'], undefined>;
+
+export type SelectOptionsTypePage = {
+  data: SelectOptionsType;
+  totalCount: number;
+};
+
+export type SelectOptionsPagePromise = (
+  search: string,
+  page: number,
+  pageSize: number,
+) => Promise<SelectOptionsTypePage>;
+
+export const StyledContainer = styled.div`
+  display: flex;
+  flex-direction: column;
+  width: 100%;
+`;
+
+export const StyledSelect = styled(AntdSelect)`
+  ${({ theme }) => `
+    && .ant-select-selector {
+      border-radius: ${theme.gridUnit}px;
+    }
+    // Open the dropdown when clicking on the suffix
+    // This is fixed in version 4.16
+    .ant-select-arrow .anticon:not(.ant-select-suffix) {
+      pointer-events: none;
+    }
+  `}
+`;
+
+export const StyledStopOutlined = styled(Icons.StopOutlined)`
+  vertical-align: 0;
+`;
+
+export const StyledCheckOutlined = styled(Icons.CheckOutlined)`
+  vertical-align: 0;
+`;
+
+export const StyledSpin = styled(Spin)`
+  margin-top: ${({ theme }) => -theme.gridUnit}px;
+`;
+
+export const StyledLoadingText = styled.div`
+  ${({ theme }) => `
+    margin-left: ${theme.gridUnit * 3}px;
+    line-height: ${theme.gridUnit * 8}px;
+    color: ${theme.colors.grayscale.light1};
+  `}
+`;
+
+const StyledHelperText = styled.div`
+  ${({ theme }) => `
+    padding: ${theme.gridUnit * 2}px ${theme.gridUnit * 3}px;
+    color: ${theme.colors.grayscale.base};
+    font-size: ${theme.typography.sizes.s}px;
+    cursor: default;
+    border-bottom: 1px solid ${theme.colors.grayscale.light2};
+  `}
+`;
+
+export const MAX_TAG_COUNT = 4;
+export const TOKEN_SEPARATORS = [',', '\n', '\t', ';'];
+export const EMPTY_OPTIONS: SelectOptionsType = [];
+
+export const DEFAULT_SORT_COMPARATOR = (
+  a: AntdLabeledValue,
+  b: AntdLabeledValue,
+  search?: string,
+) => {
+  let aText: string | undefined;
+  let bText: string | undefined;
+  if (typeof a.label === 'string' && typeof b.label === 'string') {
+    aText = a.label;
+    bText = b.label;
+  } else if (typeof a.value === 'string' && typeof b.value === 'string') {
+    aText = a.value;
+    bText = b.value;
+  }
+  // sort selected options first
+  if (typeof aText === 'string' && typeof bText === 'string') {
+    if (search) {
+      return rankedSearchCompare(aText, bText, search);
+    }
+    return aText.localeCompare(bText);
+  }
+  return (a.value as number) - (b.value as number);
+};
+
+/**
+ * It creates a comparator to check for a specific property.
+ * Can be used with string and number property values.
+ * */
+export const propertyComparator =
+  (property: string) => (a: AntdLabeledValue, b: AntdLabeledValue) => {
+    if (typeof a[property] === 'string' && typeof b[property] === 'string') {
+      return a[property].localeCompare(b[property]);
+    }
+    return (a[property] as number) - (b[property] as number);
+  };
+
+export const sortSelectedFirstHelper = (
+  a: AntdLabeledValue,
+  b: AntdLabeledValue,
+  selectValue:
+    | string
+    | number
+    | RawValue[]
+    | AntdLabeledValue
+    | AntdLabeledValue[]
+    | undefined,
+) =>
+  selectValue && a.value !== undefined && b.value !== undefined
+    ? Number(hasOption(b.value, selectValue)) -
+      Number(hasOption(a.value, selectValue))
+    : 0;
+
+export const sortComparatorWithSearchHelper = (
+  a: AntdLabeledValue,
+  b: AntdLabeledValue,
+  inputValue: string,
+  sortCallback: (a: AntdLabeledValue, b: AntdLabeledValue) => number,
+  sortComparator: (
+    a: AntdLabeledValue,
+    b: AntdLabeledValue,
+    search?: string | undefined,
+  ) => number,
+) => sortCallback(a, b) || sortComparator(a, b, inputValue);
+
+export const sortComparatorForNoSearchHelper = (
+  a: AntdLabeledValue,
+  b: AntdLabeledValue,
+  sortCallback: (a: AntdLabeledValue, b: AntdLabeledValue) => number,
+  sortComparator: (
+    a: AntdLabeledValue,
+    b: AntdLabeledValue,
+    search?: string | undefined,
+  ) => number,
+) => sortCallback(a, b) || sortComparator(a, b, '');
+
+// use a function instead of component since every rerender of the
+// Select component will create a new component
+export const getSuffixIcon = (
+  isLoading: boolean | undefined,
+  showSearch: boolean,
+  isDropdownVisible: boolean,
+) => {
+  if (isLoading) {
+    return <StyledSpin size="small" />;
+  }
+  if (showSearch && isDropdownVisible) {
+    return <SearchOutlined />;
+  }
+  return <DownOutlined />;
+};
+
+export const dropDownRenderHelper = (
+  originNode: ReactElement & { ref?: RefObject<HTMLElement> },
+  isDropdownVisible: boolean,
+  isLoading: boolean | undefined,
+  optionsLength: number,
+  helperText: string | undefined,
+  errorComponent?: JSX.Element,
+) => {
+  if (!isDropdownVisible) {
+    originNode.ref?.current?.scrollTo({ top: 0 });
+  }
+  if (isLoading && optionsLength === 0) {
+    return <StyledLoadingText>{t('Loading...')}</StyledLoadingText>;
+  }
+  if (errorComponent) {
+    return errorComponent;
+  }
+  return (
+    <>
+      {helperText && (
+        <StyledHelperText role="note">{helperText}</StyledHelperText>
+      )}
+      {originNode}
+    </>
+  );
+};
+
+export const handleFilterOptionHelper = (
+  search: string,
+  option: AntdLabeledValue,
+  optionFilterProps: string[],
+  filterOption: boolean | Function,
+) => {
+  if (typeof filterOption === 'function') {
+    return filterOption(search, option);
+  }
+
+  if (filterOption) {
+    const searchValue = search.trim().toLowerCase();
+    if (optionFilterProps && optionFilterProps.length) {
+      return optionFilterProps.some(prop => {
+        const optionProp = option?.[prop]
+          ? String(option[prop]).trim().toLowerCase()
+          : '';
+        return optionProp.includes(searchValue);
+      });
+    }
+  }
+
+  return false;
+};
+
+export const hasCustomLabels = (options: SelectOptionsType) =>
+  options?.some(opt => !!opt?.customLabel);
+
+export interface BaseSelectProps extends AntdExposedProps {
+  /**
+   * It enables the user to create new options.
+   * Can be used with standard or async select types.
+   * Can be used with any mode, single or multiple.
+   * False by default.
+   * */
+  allowNewOptions?: boolean;
+  /**
+   * It adds the aria-label tag for accessibility standards.
+   * Must be plain English and localized.
+   */
+  ariaLabel?: string;
+  /**
+   * Renders the dropdown
+   */
+  dropdownRender?: (
+    menu: ReactElement<any, string | JSXElementConstructor<any>>,
+  ) => ReactElement<any, string | JSXElementConstructor<any>>;
+  /**
+   * It adds a header on top of the Select.
+   * Can be any ReactNode.
+   */
+  header?: ReactNode;
+  /**
+   * It adds a helper text on top of the Select options
+   * with additional context to help with the interaction.
+   */
+  helperText?: string;
+  /**
+   * It allows to define which properties of the option object
+   * should be looked for when searching.
+   * By default label and value.
+   */
+  mappedMode?: 'multiple' | 'tags';
+  /**
+   * It defines whether the Select should allow for the
+   * selection of multiple options or single.
+   * Single by default.
+   */
+  mode?: 'single' | 'multiple';
+  /**
+   * Deprecated.
+   * Prefer ariaLabel instead.
+   */
+  name?: string; // discourage usage
+  /**
+   * It allows to define which properties of the option object
+   * should be looked for when searching.
+   * By default label and value.
+   */
+  optionFilterProps?: string[];
+  /**
+   * It shows a stop-outlined icon at the far right of a selected
+   * option instead of the default checkmark.
+   * Useful to better indicate to the user that by clicking on a selected
+   * option it will be de-selected.
+   * False by default.
+   */
+  invertSelection?: boolean;
+  /**
+   * Customize how filtered options are sorted while users search.
+   * Will not apply to predefined `options` array when users are not searching.
+   */
+  sortComparator?: typeof DEFAULT_SORT_COMPARATOR;
+
+  suffixIcon?: ReactNode;
+
+  ref: RefObject<HTMLInputElement>;
+}
+
+export const renderSelectOptions = (options: SelectOptionsType) =>
+  options.map(opt => {
+    const isOptObject = typeof opt === 'object';
+    const label = isOptObject ? opt?.label || opt.value : opt;
+    const value = isOptObject ? opt.value : opt;
+    const { customLabel, ...optProps } = opt;
+    return (
+      <Option {...optProps} key={value} label={label} value={value}>
+        {isOptObject && customLabel ? customLabel : label}
+      </Option>
+    );
+  });
diff --git a/superset-frontend/src/dashboard/components/RefreshIntervalModal.tsx b/superset-frontend/src/dashboard/components/RefreshIntervalModal.tsx
index 54d11bba14..98b763ed85 100644
--- a/superset-frontend/src/dashboard/components/RefreshIntervalModal.tsx
+++ b/superset-frontend/src/dashboard/components/RefreshIntervalModal.tsx
@@ -17,13 +17,14 @@
  * under the License.
  */
 import React from 'react';
-import Select, { propertyComparator } from 'src/components/Select/Select';
+import Select from 'src/components/Select/Select';
 import { t, styled } from '@superset-ui/core';
 import Alert from 'src/components/Alert';
 import Button from 'src/components/Button';
 
 import ModalTrigger, { ModalTriggerRef } from 'src/components/ModalTrigger';
 import { FormLabel } from 'src/components/Form';
+import { propertyComparator } from 'src/components/Select/utils';
 
 export const options = [
   [0, t("Don't refresh")],
diff --git a/superset-frontend/src/explore/components/controls/SelectAsyncControl/index.tsx b/superset-frontend/src/explore/components/controls/SelectAsyncControl/index.tsx
index 66d9fb154e..74364fcf98 100644
--- a/superset-frontend/src/explore/components/controls/SelectAsyncControl/index.tsx
+++ b/superset-frontend/src/explore/components/controls/SelectAsyncControl/index.tsx
@@ -20,7 +20,8 @@ import React, { useEffect, useState } from 'react';
 import { t, SupersetClient } from '@superset-ui/core';
 import ControlHeader from 'src/explore/components/ControlHeader';
 import { Select } from 'src/components';
-import { SelectProps, OptionsType } from 'src/components/Select/Select';
+import { SelectProps } from 'src/components/Select/Select';
+import { SelectOptionsType } from 'src/components/Select/utils';
 import { SelectValue, LabeledValue } from 'antd/lib/select';
 import withToasts from 'src/components/MessageToasts/withToasts';
 import { getClientErrorObject } from 'src/utils/getClientErrorObject';
@@ -32,7 +33,7 @@ interface SelectAsyncControlProps extends SelectAsyncProps {
   ariaLabel?: string;
   dataEndpoint: string;
   default?: SelectValue;
-  mutator?: (response: Record<string, any>) => OptionsType;
+  mutator?: (response: Record<string, any>) => SelectOptionsType;
   multi?: boolean;
   onChange: (val: SelectValue) => void;
   // ControlHeader related props
@@ -57,7 +58,7 @@ const SelectAsyncControl = ({
   value,
   ...props
 }: SelectAsyncControlProps) => {
-  const [options, setOptions] = useState<OptionsType>([]);
+  const [options, setOptions] = useState<SelectOptionsType>([]);
 
   const handleOnChange = (val: SelectValue) => {
     let onChangeVal = val;
diff --git a/superset-frontend/src/filters/components/Select/SelectFilterPlugin.tsx b/superset-frontend/src/filters/components/Select/SelectFilterPlugin.tsx
index 936c6d548a..d1bb3df747 100644
--- a/superset-frontend/src/filters/components/Select/SelectFilterPlugin.tsx
+++ b/superset-frontend/src/filters/components/Select/SelectFilterPlugin.tsx
@@ -36,7 +36,7 @@ import { Select } from 'src/components';
 import debounce from 'lodash/debounce';
 import { SLOW_DEBOUNCE } from 'src/constants';
 import { useImmerReducer } from 'use-immer';
-import { propertyComparator } from 'src/components/Select/Select';
+import { propertyComparator } from 'src/components/Select/utils';
 import { PluginFilterSelectProps, SelectValue } from './types';
 import { StyledFormItem, FilterPluginStyle, StatusMessage } from '../common';
 import { getDataRecordFormatter, getSelectExtraFormData } from '../../utils';
diff --git a/superset-frontend/src/views/CRUD/alert/AlertReportModal.tsx b/superset-frontend/src/views/CRUD/alert/AlertReportModal.tsx
index 820a83b8c3..fe34b22e80 100644
--- a/superset-frontend/src/views/CRUD/alert/AlertReportModal.tsx
+++ b/superset-frontend/src/views/CRUD/alert/AlertReportModal.tsx
@@ -38,7 +38,7 @@ import { Switch } from 'src/components/Switch';
 import Modal from 'src/components/Modal';
 import TimezoneSelector from 'src/components/TimezoneSelector';
 import { Radio } from 'src/components/Radio';
-import { propertyComparator } from 'src/components/Select/Select';
+import { propertyComparator } from 'src/components/Select/utils';
 import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags';
 import withToasts from 'src/components/MessageToasts/withToasts';
 import Owner from 'src/types/Owner';