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 1109fe5fb7 chore: Split Select component into Async and Sync 
components (#20466)
1109fe5fb7 is described below

commit 1109fe5fb7a777e3fbb4f3d8d60283b676d1acc0
Author: cccs-RyanK <[email protected]>
AuthorDate: Thu Jul 7 14:51:37 2022 -0400

    chore: Split Select component into Async and Sync components (#20466)
    
    * Created AsyncSelect Component
    Changed files to reference AsyncSelect if needed
    
    * modified import of AsyncSelect, removed async tests and prefixes from 
select tests
    
    * fixed various import and lint warnings
    
    * fixing lint errors
    
    * fixed frontend test errors
    
    * fixed alertreportmodel tests
    
    * removed accidental import
    
    * fixed lint errors
    
    * updated async select
---
 .../src/addSlice/AddSliceContainer.test.tsx        |   4 +-
 .../src/addSlice/AddSliceContainer.tsx             |   4 +-
 .../src/components/DatabaseSelector/index.tsx      |   4 +-
 .../src/components/Datasource/DatasourceEditor.jsx |   4 +-
 .../{Select.test.tsx => AsyncSelect.test.tsx}      | 327 +++------
 .../src/components/Select/AsyncSelect.tsx          | 754 +++++++++++++++++++++
 .../src/components/Select/Select.stories.tsx       |  24 +-
 .../src/components/Select/Select.test.tsx          | 367 +---------
 superset-frontend/src/components/index.ts          |   1 +
 .../dashboard/components/PropertiesModal/index.tsx |   8 +-
 .../FiltersConfigForm/DatasetSelect.tsx            |   4 +-
 .../explore/components/PropertiesModal/index.tsx   |   4 +-
 .../src/views/CRUD/alert/AlertReportModal.test.jsx |  14 +-
 .../src/views/CRUD/alert/AlertReportModal.tsx      |  12 +-
 14 files changed, 900 insertions(+), 631 deletions(-)

diff --git a/superset-frontend/src/addSlice/AddSliceContainer.test.tsx 
b/superset-frontend/src/addSlice/AddSliceContainer.test.tsx
index 91e6b28dd1..a656efb818 100644
--- a/superset-frontend/src/addSlice/AddSliceContainer.test.tsx
+++ b/superset-frontend/src/addSlice/AddSliceContainer.test.tsx
@@ -19,7 +19,7 @@
 import React from 'react';
 import { ReactWrapper } from 'enzyme';
 import Button from 'src/components/Button';
-import { Select } from 'src/components';
+import { AsyncSelect } from 'src/components';
 import AddSliceContainer, {
   AddSliceContainerProps,
   AddSliceContainerState,
@@ -72,7 +72,7 @@ async function getWrapper(user = mockUser) {
 
 test('renders a select and a VizTypeControl', async () => {
   const wrapper = await getWrapper();
-  expect(wrapper.find(Select)).toExist();
+  expect(wrapper.find(AsyncSelect)).toExist();
   expect(wrapper.find(VizTypeGallery)).toExist();
 });
 
diff --git a/superset-frontend/src/addSlice/AddSliceContainer.tsx 
b/superset-frontend/src/addSlice/AddSliceContainer.tsx
index d4c6bfc78a..84c37b61c7 100644
--- a/superset-frontend/src/addSlice/AddSliceContainer.tsx
+++ b/superset-frontend/src/addSlice/AddSliceContainer.tsx
@@ -23,7 +23,7 @@ import { getUrlParam } from 'src/utils/urlUtils';
 import { URL_PARAMS } from 'src/constants';
 import { isNullish } from 'src/utils/common';
 import Button from 'src/components/Button';
-import { Select, Steps } from 'src/components';
+import { AsyncSelect, Steps } from 'src/components';
 import { Tooltip } from 'src/components/Tooltip';
 
 import VizTypeGallery, {
@@ -349,7 +349,7 @@ export default class AddSliceContainer extends 
React.PureComponent<
             status={this.state.datasource?.value ? 'finish' : 'process'}
             description={
               <StyledStepDescription className="dataset">
-                <Select
+                <AsyncSelect
                   autoFocus
                   ariaLabel={t('Dataset')}
                   name="select-datasource"
diff --git a/superset-frontend/src/components/DatabaseSelector/index.tsx 
b/superset-frontend/src/components/DatabaseSelector/index.tsx
index 4a5e94b2bc..e972e95b00 100644
--- a/superset-frontend/src/components/DatabaseSelector/index.tsx
+++ b/superset-frontend/src/components/DatabaseSelector/index.tsx
@@ -19,7 +19,7 @@
 import React, { ReactNode, useState, useMemo, useEffect } from 'react';
 import { styled, SupersetClient, t } from '@superset-ui/core';
 import rison from 'rison';
-import { Select } from 'src/components';
+import { AsyncSelect, Select } from 'src/components';
 import Label from 'src/components/Label';
 import { FormLabel } from 'src/components/Form';
 import RefreshLabel from 'src/components/RefreshLabel';
@@ -272,7 +272,7 @@ export default function DatabaseSelector({
 
   function renderDatabaseSelect() {
     return renderSelectRow(
-      <Select
+      <AsyncSelect
         ariaLabel={t('Select database or type database name')}
         optionFilterProps={['database_name', 'value']}
         data-test="select-database"
diff --git a/superset-frontend/src/components/Datasource/DatasourceEditor.jsx 
b/superset-frontend/src/components/Datasource/DatasourceEditor.jsx
index 94dd1241b0..7985165f73 100644
--- a/superset-frontend/src/components/Datasource/DatasourceEditor.jsx
+++ b/superset-frontend/src/components/Datasource/DatasourceEditor.jsx
@@ -25,7 +25,7 @@ import Alert from 'src/components/Alert';
 import Badge from 'src/components/Badge';
 import shortid from 'shortid';
 import { styled, SupersetClient, t, withTheme } from '@superset-ui/core';
-import { Select, Row, Col } from 'src/components';
+import { Select, AsyncSelect, Row, Col } from 'src/components';
 import { FormLabel } from 'src/components/Form';
 import Button from 'src/components/Button';
 import Tabs from 'src/components/Tabs';
@@ -548,7 +548,7 @@ function OwnersSelector({ datasource, onChange }) {
   }, []);
 
   return (
-    <Select
+    <AsyncSelect
       ariaLabel={t('Select owners')}
       mode="multiple"
       name="owners"
diff --git a/superset-frontend/src/components/Select/Select.test.tsx 
b/superset-frontend/src/components/Select/AsyncSelect.test.tsx
similarity index 65%
copy from superset-frontend/src/components/Select/Select.test.tsx
copy to superset-frontend/src/components/Select/AsyncSelect.test.tsx
index 1515a7c548..dc6eff35d9 100644
--- a/superset-frontend/src/components/Select/Select.test.tsx
+++ b/superset-frontend/src/components/Select/AsyncSelect.test.tsx
@@ -16,11 +16,10 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import React, { RefObject } from 'react';
+import React from 'react';
 import { render, screen, waitFor, within } from 'spec/helpers/testing-library';
 import userEvent from '@testing-library/user-event';
-import { Select } from 'src/components';
-import { SelectRef } from './Select';
+import { AsyncSelect } from 'src/components';
 
 const ARIA_LABEL = 'Test';
 const NEW_OPTION = 'Kyle';
@@ -126,19 +125,19 @@ const open = () => waitFor(() => 
userEvent.click(getSelect()));
 
 test('displays a header', async () => {
   const headerText = 'Header';
-  render(<Select {...defaultProps} header={headerText} />);
+  render(<AsyncSelect {...defaultProps} header={headerText} />);
   expect(screen.getByText(headerText)).toBeInTheDocument();
 });
 
 test('adds a new option if the value is not in the options', async () => {
   const { rerender } = render(
-    <Select {...defaultProps} options={[]} value={OPTIONS[0]} />,
+    <AsyncSelect {...defaultProps} options={[]} value={OPTIONS[0]} />,
   );
   await open();
   expect(await findSelectOption(OPTIONS[0].label)).toBeInTheDocument();
 
   rerender(
-    <Select {...defaultProps} options={[OPTIONS[1]]} value={OPTIONS[0]} />,
+    <AsyncSelect {...defaultProps} options={[OPTIONS[1]]} value={OPTIONS[0]} 
/>,
   );
   await open();
   const options = await findAllSelectOptions();
@@ -149,7 +148,7 @@ test('adds a new option if the value is not in the 
options', async () => {
 });
 
 test('inverts the selection', async () => {
-  render(<Select {...defaultProps} invertSelection />);
+  render(<AsyncSelect {...defaultProps} invertSelection />);
   await open();
   userEvent.click(await findSelectOption(OPTIONS[0].label));
   expect(await screen.findByLabelText('stop')).toBeInTheDocument();
@@ -157,7 +156,7 @@ test('inverts the selection', async () => {
 
 test('sort the options by label if no sort comparator is provided', async () 
=> {
   const unsortedOptions = [...OPTIONS].sort(() => Math.random());
-  render(<Select {...defaultProps} options={unsortedOptions} />);
+  render(<AsyncSelect {...defaultProps} options={unsortedOptions} />);
   await open();
   const options = await findAllSelectOptions();
   options.forEach((option, key) =>
@@ -171,7 +170,7 @@ test('sort the options using a custom sort comparator', 
async () => {
     option2: typeof OPTIONS[0],
   ) => option1.gender.localeCompare(option2.gender);
   render(
-    <Select
+    <AsyncSelect
       {...defaultProps}
       options={loadOptions}
       sortComparator={sortComparator}
@@ -187,7 +186,7 @@ test('sort the options using a custom sort comparator', 
async () => {
 });
 
 test('should sort selected to top when in single mode', async () => {
-  render(<Select {...defaultProps} mode="single" />);
+  render(<AsyncSelect {...defaultProps} mode="single" />);
   const originalLabels = OPTIONS.map(option => option.label);
   await open();
   userEvent.click(await findSelectOption(originalLabels[1]));
@@ -219,7 +218,7 @@ test('should sort selected to top when in single mode', 
async () => {
 });
 
 test('should sort selected to the top when in multi mode', async () => {
-  render(<Select {...defaultProps} mode="multiple" />);
+  render(<AsyncSelect {...defaultProps} mode="multiple" />);
   const originalLabels = OPTIONS.map(option => option.label);
   let labels = originalLabels.slice();
 
@@ -248,7 +247,7 @@ test('should sort selected to the top when in multi mode', 
async () => {
 
 test('searches for label or value', async () => {
   const option = OPTIONS[11];
-  render(<Select {...defaultProps} />);
+  render(<AsyncSelect {...defaultProps} />);
   const search = option.value;
   await type(search.toString());
   const options = await findAllSelectOptions();
@@ -257,7 +256,7 @@ test('searches for label or value', async () => {
 });
 
 test('search order exact and startWith match first', async () => {
-  render(<Select {...defaultProps} />);
+  render(<AsyncSelect {...defaultProps} />);
   await type('Her');
   const options = await findAllSelectOptions();
   expect(options.length).toBe(4);
@@ -268,14 +267,14 @@ test('search order exact and startWith match first', 
async () => {
 });
 
 test('ignores case when searching', async () => {
-  render(<Select {...defaultProps} />);
+  render(<AsyncSelect {...defaultProps} />);
   await type('george');
   expect(await findSelectOption('George')).toBeInTheDocument();
 });
 
 test('same case should be ranked to the top', async () => {
   render(
-    <Select
+    <AsyncSelect
       {...defaultProps}
       options={[
         { value: 'Cac' },
@@ -295,13 +294,15 @@ test('same case should be ranked to the top', async () => 
{
 });
 
 test('ignores special keys when searching', async () => {
-  render(<Select {...defaultProps} />);
+  render(<AsyncSelect {...defaultProps} />);
   await type('{shift}');
   expect(screen.queryByText(LOADING)).not.toBeInTheDocument();
 });
 
 test('searches for custom fields', async () => {
-  render(<Select {...defaultProps} optionFilterProps={['label', 'gender']} />);
+  render(
+    <AsyncSelect {...defaultProps} optionFilterProps={['label', 'gender']} />,
+  );
   await type('Liam');
   let options = await findAllSelectOptions();
   expect(options.length).toBe(1);
@@ -320,7 +321,7 @@ test('searches for custom fields', async () => {
 });
 
 test('removes duplicated values', async () => {
-  render(<Select {...defaultProps} mode="multiple" allowNewOptions />);
+  render(<AsyncSelect {...defaultProps} mode="multiple" allowNewOptions />);
   await type('a,b,b,b,c,d,d');
   const values = await findAllSelectValues();
   expect(values.length).toBe(4);
@@ -336,7 +337,7 @@ test('renders a custom label', async () => {
     { label: 'Liam', value: 2, customLabel: <h1>Liam</h1> },
     { label: 'Olivia', value: 3, customLabel: <h1>Olivia</h1> },
   ];
-  render(<Select {...defaultProps} options={options} />);
+  render(<AsyncSelect {...defaultProps} options={options} />);
   await open();
   expect(screen.getByRole('heading', { name: 'John' })).toBeInTheDocument();
   expect(screen.getByRole('heading', { name: 'Liam' })).toBeInTheDocument();
@@ -349,7 +350,7 @@ test('searches for a word with a custom label', async () => 
{
     { label: 'Liam', value: 2, customLabel: <h1>Liam</h1> },
     { label: 'Olivia', value: 3, customLabel: <h1>Olivia</h1> },
   ];
-  render(<Select {...defaultProps} options={options} />);
+  render(<AsyncSelect {...defaultProps} options={options} />);
   await type('Liam');
   const selectOptions = await findAllSelectOptions();
   expect(selectOptions.length).toBe(1);
@@ -357,7 +358,7 @@ test('searches for a word with a custom label', async () => 
{
 });
 
 test('removes a new option if the user does not select it', async () => {
-  render(<Select {...defaultProps} allowNewOptions />);
+  render(<AsyncSelect {...defaultProps} allowNewOptions />);
   await type(NEW_OPTION);
   expect(await findSelectOption(NEW_OPTION)).toBeInTheDocument();
   await type('k');
@@ -369,7 +370,7 @@ test('removes a new option if the user does not select it', 
async () => {
 test('clear all the values', async () => {
   const onClear = jest.fn();
   render(
-    <Select
+    <AsyncSelect
       {...defaultProps}
       mode="multiple"
       value={[OPTIONS[0], OPTIONS[1]]}
@@ -383,14 +384,14 @@ test('clear all the values', async () => {
 });
 
 test('does not add a new option if allowNewOptions is false', async () => {
-  render(<Select {...defaultProps} options={loadOptions} />);
+  render(<AsyncSelect {...defaultProps} options={loadOptions} />);
   await open();
   await type(NEW_OPTION);
   expect(await screen.findByText(NO_DATA)).toBeInTheDocument();
 });
 
 test('adds the null option when selected in single mode', async () => {
-  render(<Select {...defaultProps} options={[OPTIONS[0], NULL_OPTION]} />);
+  render(<AsyncSelect {...defaultProps} options={[OPTIONS[0], NULL_OPTION]} 
/>);
   await open();
   userEvent.click(await findSelectOption(NULL_OPTION.label));
   const values = await findAllSelectValues();
@@ -399,7 +400,7 @@ test('adds the null option when selected in single mode', 
async () => {
 
 test('adds the null option when selected in multiple mode', async () => {
   render(
-    <Select
+    <AsyncSelect
       {...defaultProps}
       options={[OPTIONS[0], NULL_OPTION, OPTIONS[2]]}
       mode="multiple"
@@ -413,149 +414,14 @@ test('adds the null option when selected in multiple 
mode', async () => {
   expect(values[1]).toHaveTextContent(NULL_OPTION.label);
 });
 
-test('static - renders the select with default props', () => {
-  render(<Select {...defaultProps} />);
-  expect(getSelect()).toBeInTheDocument();
-});
-
-test('static - opens the select without any data', async () => {
-  render(<Select {...defaultProps} options={[]} />);
-  await open();
-  expect(screen.getByText(NO_DATA)).toBeInTheDocument();
-});
-
-test('static - makes a selection in single mode', async () => {
-  render(<Select {...defaultProps} />);
-  const optionText = 'Emma';
-  await open();
-  userEvent.click(await findSelectOption(optionText));
-  expect(await findSelectValue()).toHaveTextContent(optionText);
-});
-
-test('static - multiple selections in multiple mode', async () => {
-  render(<Select {...defaultProps} mode="multiple" />);
-  await open();
-  const [firstOption, secondOption] = OPTIONS;
-  userEvent.click(await findSelectOption(firstOption.label));
-  userEvent.click(await findSelectOption(secondOption.label));
-  const values = await findAllSelectValues();
-  expect(values[0]).toHaveTextContent(firstOption.label);
-  expect(values[1]).toHaveTextContent(secondOption.label);
-});
-
-test('static - changes the selected item in single mode', async () => {
-  const onChange = jest.fn();
-  render(<Select {...defaultProps} onChange={onChange} />);
-  await open();
-  const [firstOption, secondOption] = OPTIONS;
-  userEvent.click(await findSelectOption(firstOption.label));
-  expect(await findSelectValue()).toHaveTextContent(firstOption.label);
-  expect(onChange).toHaveBeenCalledWith(
-    expect.objectContaining({
-      label: firstOption.label,
-      value: firstOption.value,
-    }),
-    firstOption,
-  );
-  userEvent.click(await findSelectOption(secondOption.label));
-  expect(onChange).toHaveBeenCalledWith(
-    expect.objectContaining({
-      label: secondOption.label,
-      value: secondOption.value,
-    }),
-    secondOption,
-  );
-  expect(await findSelectValue()).toHaveTextContent(secondOption.label);
-});
-
-test('static - deselects an item in multiple mode', async () => {
-  render(<Select {...defaultProps} mode="multiple" />);
-  await open();
-  const [firstOption, secondOption] = OPTIONS;
-  userEvent.click(await findSelectOption(firstOption.label));
-  userEvent.click(await findSelectOption(secondOption.label));
-  let values = await findAllSelectValues();
-  expect(values.length).toBe(2);
-  expect(values[0]).toHaveTextContent(firstOption.label);
-  expect(values[1]).toHaveTextContent(secondOption.label);
-  userEvent.click(await findSelectOption(firstOption.label));
-  values = await findAllSelectValues();
-  expect(values.length).toBe(1);
-  expect(values[0]).toHaveTextContent(secondOption.label);
-});
-
-test('static - adds a new option if none is available and allowNewOptions is 
true', async () => {
-  render(<Select {...defaultProps} allowNewOptions />);
-  await open();
-  await type(NEW_OPTION);
-  expect(await findSelectOption(NEW_OPTION)).toBeInTheDocument();
-});
-
-test('static - shows "No data" when allowNewOptions is false and a new option 
is entered', async () => {
-  render(<Select {...defaultProps} allowNewOptions={false} />);
-  await open();
-  await type(NEW_OPTION);
-  expect(await screen.findByText(NO_DATA)).toBeInTheDocument();
-});
-
-test('static - does not show "No data" when allowNewOptions is true and a new 
option is entered', async () => {
-  render(<Select {...defaultProps} allowNewOptions />);
-  await open();
-  await type(NEW_OPTION);
-  expect(screen.queryByText(NO_DATA)).not.toBeInTheDocument();
-});
-
-test('static - does not show "Loading..." when allowNewOptions is false and a 
new option is entered', async () => {
-  render(<Select {...defaultProps} allowNewOptions={false} />);
-  await open();
-  await type(NEW_OPTION);
-  expect(screen.queryByText(LOADING)).not.toBeInTheDocument();
-});
-
-test('static - does not add a new option if the option already exists', async 
() => {
-  render(<Select {...defaultProps} allowNewOptions />);
-  const option = OPTIONS[0].label;
-  await open();
-  await type(option);
-  expect(await findSelectOption(option)).toBeInTheDocument();
-});
-
-test('static - sets a initial value in single mode', async () => {
-  render(<Select {...defaultProps} value={OPTIONS[0]} />);
-  expect(await findSelectValue()).toHaveTextContent(OPTIONS[0].label);
-});
-
-test('static - sets a initial value in multiple mode', async () => {
-  render(
-    <Select
-      {...defaultProps}
-      mode="multiple"
-      value={[OPTIONS[0], OPTIONS[1]]}
-    />,
-  );
-  const values = await findAllSelectValues();
-  expect(values[0]).toHaveTextContent(OPTIONS[0].label);
-  expect(values[1]).toHaveTextContent(OPTIONS[1].label);
-});
-
-test('static - searches for an item', async () => {
-  render(<Select {...defaultProps} />);
-  const search = 'Oli';
-  await type(search);
-  const options = await findAllSelectOptions();
-  expect(options.length).toBe(2);
-  expect(options[0]).toHaveTextContent('Oliver');
-  expect(options[1]).toHaveTextContent('Olivia');
-});
-
-test('async - renders the select with default props', () => {
-  render(<Select {...defaultProps} options={loadOptions} />);
+test('renders the select with default props', () => {
+  render(<AsyncSelect {...defaultProps} options={loadOptions} />);
   expect(getSelect()).toBeInTheDocument();
 });
 
-test('async - opens the select without any data', async () => {
+test('opens the select without any data', async () => {
   render(
-    <Select
+    <AsyncSelect
       {...defaultProps}
       options={async () => ({ data: [], totalCount: 0 })}
     />,
@@ -564,8 +430,8 @@ test('async - opens the select without any data', async () 
=> {
   expect(await screen.findByText(/no data/i)).toBeInTheDocument();
 });
 
-test('async - displays the loading indicator when opening', async () => {
-  render(<Select {...defaultProps} options={loadOptions} />);
+test('displays the loading indicator when opening', async () => {
+  render(<AsyncSelect {...defaultProps} options={loadOptions} />);
   await waitFor(() => {
     userEvent.click(getSelect());
     expect(screen.getByText(LOADING)).toBeInTheDocument();
@@ -573,16 +439,18 @@ test('async - displays the loading indicator when 
opening', async () => {
   expect(screen.queryByText(LOADING)).not.toBeInTheDocument();
 });
 
-test('async - makes a selection in single mode', async () => {
-  render(<Select {...defaultProps} options={loadOptions} />);
+test('makes a selection in single mode', async () => {
+  render(<AsyncSelect {...defaultProps} options={loadOptions} />);
   const optionText = 'Emma';
   await open();
   userEvent.click(await findSelectOption(optionText));
   expect(await findSelectValue()).toHaveTextContent(optionText);
 });
 
-test('async - multiple selections in multiple mode', async () => {
-  render(<Select {...defaultProps} options={loadOptions} mode="multiple" />);
+test('multiple selections in multiple mode', async () => {
+  render(
+    <AsyncSelect {...defaultProps} options={loadOptions} mode="multiple" />,
+  );
   await open();
   const [firstOption, secondOption] = OPTIONS;
   userEvent.click(await findSelectOption(firstOption.label));
@@ -592,10 +460,10 @@ test('async - multiple selections in multiple mode', 
async () => {
   expect(values[1]).toHaveTextContent(secondOption.label);
 });
 
-test('async - changes the selected item in single mode', async () => {
+test('changes the selected item in single mode', async () => {
   const onChange = jest.fn();
   render(
-    <Select {...defaultProps} options={loadOptions} onChange={onChange} />,
+    <AsyncSelect {...defaultProps} options={loadOptions} onChange={onChange} 
/>,
   );
   await open();
   const [firstOption, secondOption] = OPTIONS;
@@ -619,8 +487,10 @@ test('async - changes the selected item in single mode', 
async () => {
   expect(await findSelectValue()).toHaveTextContent(secondOption.label);
 });
 
-test('async - deselects an item in multiple mode', async () => {
-  render(<Select {...defaultProps} options={loadOptions} mode="multiple" />);
+test('deselects an item in multiple mode', async () => {
+  render(
+    <AsyncSelect {...defaultProps} options={loadOptions} mode="multiple" />,
+  );
   await open();
   const option3 = OPTIONS[2];
   const option8 = OPTIONS[7];
@@ -653,15 +523,19 @@ test('async - deselects an item in multiple mode', async 
() => {
   expect(values[0]).toHaveTextContent(option8.label);
 });
 
-test('async - adds a new option if none is available and allowNewOptions is 
true', async () => {
-  render(<Select {...defaultProps} options={loadOptions} allowNewOptions />);
+test('adds a new option if none is available and allowNewOptions is true', 
async () => {
+  render(
+    <AsyncSelect {...defaultProps} options={loadOptions} allowNewOptions />,
+  );
   await open();
   await type(NEW_OPTION);
   expect(await findSelectOption(NEW_OPTION)).toBeInTheDocument();
 });
 
-test('async - does not add a new option if the option already exists', async 
() => {
-  render(<Select {...defaultProps} options={loadOptions} allowNewOptions />);
+test('does not add a new option if the option already exists', async () => {
+  render(
+    <AsyncSelect {...defaultProps} options={loadOptions} allowNewOptions />,
+  );
   const option = OPTIONS[0].label;
   await open();
   await type(option);
@@ -673,9 +547,9 @@ test('async - does not add a new option if the option 
already exists', async ()
   });
 });
 
-test('async - shows "No data" when allowNewOptions is false and a new option 
is entered', async () => {
+test('shows "No data" when allowNewOptions is false and a new option is 
entered', async () => {
   render(
-    <Select
+    <AsyncSelect
       {...defaultProps}
       options={loadOptions}
       allowNewOptions={false}
@@ -687,21 +561,25 @@ test('async - shows "No data" when allowNewOptions is 
false and a new option is
   expect(await screen.findByText(NO_DATA)).toBeInTheDocument();
 });
 
-test('async - does not show "No data" when allowNewOptions is true and a new 
option is entered', async () => {
-  render(<Select {...defaultProps} options={loadOptions} allowNewOptions />);
+test('does not show "No data" when allowNewOptions is true and a new option is 
entered', async () => {
+  render(
+    <AsyncSelect {...defaultProps} options={loadOptions} allowNewOptions />,
+  );
   await open();
   await type(NEW_OPTION);
   expect(screen.queryByText(NO_DATA)).not.toBeInTheDocument();
 });
 
-test('async - sets a initial value in single mode', async () => {
-  render(<Select {...defaultProps} options={loadOptions} value={OPTIONS[0]} 
/>);
+test('sets a initial value in single mode', async () => {
+  render(
+    <AsyncSelect {...defaultProps} options={loadOptions} value={OPTIONS[0]} />,
+  );
   expect(await findSelectValue()).toHaveTextContent(OPTIONS[0].label);
 });
 
-test('async - sets a initial value in multiple mode', async () => {
+test('sets a initial value in multiple mode', async () => {
   render(
-    <Select
+    <AsyncSelect
       {...defaultProps}
       mode="multiple"
       options={loadOptions}
@@ -713,8 +591,8 @@ test('async - sets a initial value in multiple mode', async 
() => {
   expect(values[1]).toHaveTextContent(OPTIONS[1].label);
 });
 
-test('async - searches for matches in both loaded and unloaded pages', async 
() => {
-  render(<Select {...defaultProps} options={loadOptions} />);
+test('searches for matches in both loaded and unloaded pages', async () => {
+  render(<AsyncSelect {...defaultProps} options={loadOptions} />);
   await open();
   await type('and');
 
@@ -729,9 +607,9 @@ test('async - searches for matches in both loaded and 
unloaded pages', async ()
   expect(options[1]).toHaveTextContent('Sandro');
 });
 
-test('async - searches for an item in a page not loaded', async () => {
+test('searches for an item in a page not loaded', async () => {
   const mock = jest.fn(loadOptions);
-  render(<Select {...defaultProps} options={mock} />);
+  render(<AsyncSelect {...defaultProps} options={mock} />);
   const search = 'Sandro';
   await open();
   await type(search);
@@ -741,41 +619,45 @@ test('async - searches for an item in a page not loaded', 
async () => {
   expect(options[0]).toHaveTextContent(search);
 });
 
-test('async - does not fetches data when rendering', async () => {
+test('does not fetches data when rendering', async () => {
   const loadOptions = jest.fn(async () => ({ data: [], totalCount: 0 }));
-  render(<Select {...defaultProps} options={loadOptions} />);
+  render(<AsyncSelect {...defaultProps} options={loadOptions} />);
   expect(loadOptions).not.toHaveBeenCalled();
 });
 
-test('async - fetches data when opening', async () => {
+test('fetches data when opening', async () => {
   const loadOptions = jest.fn(async () => ({ data: [], totalCount: 0 }));
-  render(<Select {...defaultProps} options={loadOptions} />);
+  render(<AsyncSelect {...defaultProps} options={loadOptions} />);
   await open();
   expect(loadOptions).toHaveBeenCalled();
 });
 
-test('async - fetches data only after a search input is entered if 
fetchOnlyOnSearch is true', async () => {
+test('fetches data only after a search input is entered if fetchOnlyOnSearch 
is true', async () => {
   const loadOptions = jest.fn(async () => ({ data: [], totalCount: 0 }));
-  render(<Select {...defaultProps} options={loadOptions} fetchOnlyOnSearch />);
+  render(
+    <AsyncSelect {...defaultProps} options={loadOptions} fetchOnlyOnSearch />,
+  );
   await open();
   await waitFor(() => expect(loadOptions).not.toHaveBeenCalled());
   await type('search');
   await waitFor(() => expect(loadOptions).toHaveBeenCalled());
 });
 
-test('async - displays an error message when an exception is thrown while 
fetching', async () => {
+test('displays an error message when an exception is thrown while fetching', 
async () => {
   const error = 'Fetch error';
   const loadOptions = async () => {
     throw new Error(error);
   };
-  render(<Select {...defaultProps} options={loadOptions} />);
+  render(<AsyncSelect {...defaultProps} options={loadOptions} />);
   await open();
   expect(screen.getByText(error)).toBeInTheDocument();
 });
 
-test('async - does not fire a new request for the same search input', async () 
=> {
+test('does not fire a new request for the same search input', async () => {
   const loadOptions = jest.fn(async () => ({ data: [], totalCount: 0 }));
-  render(<Select {...defaultProps} options={loadOptions} fetchOnlyOnSearch />);
+  render(
+    <AsyncSelect {...defaultProps} options={loadOptions} fetchOnlyOnSearch />,
+  );
   await type('search');
   expect(await screen.findByText(NO_DATA)).toBeInTheDocument();
   expect(loadOptions).toHaveBeenCalledTimes(1);
@@ -785,11 +667,11 @@ test('async - does not fire a new request for the same 
search input', async () =
   expect(loadOptions).toHaveBeenCalledTimes(1);
 });
 
-test('async - does not fire a new request if all values have been fetched', 
async () => {
+test('does not fire a new request if all values have been fetched', async () 
=> {
   const mock = jest.fn(loadOptions);
   const search = 'George';
   const pageSize = OPTIONS.length;
-  render(<Select {...defaultProps} options={mock} pageSize={pageSize} />);
+  render(<AsyncSelect {...defaultProps} options={mock} pageSize={pageSize} />);
   await open();
   expect(mock).toHaveBeenCalledTimes(1);
   await type(search);
@@ -797,10 +679,10 @@ test('async - does not fire a new request if all values 
have been fetched', asyn
   expect(mock).toHaveBeenCalledTimes(1);
 });
 
-test('async - fires a new request if all values have not been fetched', async 
() => {
+test('fires a new request if all values have not been fetched', async () => {
   const mock = jest.fn(loadOptions);
   const pageSize = OPTIONS.length / 2;
-  render(<Select {...defaultProps} options={mock} pageSize={pageSize} />);
+  render(<AsyncSelect {...defaultProps} options={mock} pageSize={pageSize} />);
   await open();
   expect(mock).toHaveBeenCalledTimes(1);
   await type('or');
@@ -814,43 +696,6 @@ test('async - fires a new request if all values have not 
been fetched', async ()
   expect(mock).toHaveBeenCalledTimes(2);
 });
 
-test('async - requests the options again after clearing the cache', async () 
=> {
-  const ref: RefObject<SelectRef> = { current: null };
-  const mock = jest.fn(loadOptions);
-  const pageSize = OPTIONS.length;
-  render(
-    <Select {...defaultProps} options={mock} pageSize={pageSize} ref={ref} />,
-  );
-  await open();
-  expect(mock).toHaveBeenCalledTimes(1);
-  ref.current?.clearCache();
-  await type('{esc}');
-  await open();
-  expect(mock).toHaveBeenCalledTimes(2);
-});
-
-test('async - triggers getPopupContainer if passed', async () => {
-  const getPopupContainer = jest.fn();
-  render(
-    <div>
-      <Select
-        {...defaultProps}
-        options={loadOptions}
-        getPopupContainer={getPopupContainer}
-      />
-    </div>,
-  );
-  await open();
-  expect(getPopupContainer).toHaveBeenCalled();
-});
-
-test('static - triggers getPopupContainer if passed', async () => {
-  const getPopupContainer = jest.fn();
-  render(<Select {...defaultProps} getPopupContainer={getPopupContainer} />);
-  await open();
-  expect(getPopupContainer).toHaveBeenCalled();
-});
-
 /*
  TODO: Add tests that require scroll interaction. Needs further investigation.
  - Fetches more data when scrolling and more data is available
diff --git a/superset-frontend/src/components/Select/AsyncSelect.tsx 
b/superset-frontend/src/components/Select/AsyncSelect.tsx
new file mode 100644
index 0000000000..98f146f15f
--- /dev/null
+++ b/superset-frontend/src/components/Select/AsyncSelect.tsx
@@ -0,0 +1,754 @@
+/**
+ * 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 React, {
+  forwardRef,
+  ReactElement,
+  ReactNode,
+  RefObject,
+  UIEvent,
+  useEffect,
+  useMemo,
+  useState,
+  useRef,
+  useCallback,
+  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 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'
+  | 'labelInValue'
+  | '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>;
+
+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 fires a request against the server after
+   * the first interaction and not on render.
+   * Works in async mode only (See the options property).
+   * 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 can be static, an array of options.
+   * The options can also be async, a promise that returns
+   * an array of options.
+   */
+  options: OptionsType | OptionsPagePromise;
+  /**
+   * 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.
+   * Works in async mode only (See the options property).
+   * Undefined by default.
+   */
+  fetchOnlyOnSearch?: boolean;
+  /**
+   * It provides a callback function when an error
+   * is generated after a request is fired.
+   * 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;
+    }
+  `}
+`;
+
+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 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}`;
+
+/**
+ * This component is a customized version of the Antdesign 4.X Select component
+ * https://ant.design/components/select/.
+ * The aim of the component was to combine all the instances of select 
components throughout the
+ * project under one and to remove the react-select component entirely.
+ * This Select component provides an API that is tested against all the 
different use cases of Superset.
+ * It limits and overrides the existing Antdesign API in order to keep their 
usage to the minimum
+ * and to enforce simplification and standardization.
+ * It is divided into two macro categories, Static and Async.
+ * The Static type accepts a static array of options.
+ * The Async type accepts a promise that will return the options.
+ * Each of the categories come with different abilities. For a comprehensive 
guide please refer to
+ * the storybook in src/components/Select/Select.stories.tsx.
+ */
+const AsyncSelect = (
+  {
+    allowClear,
+    allowNewOptions = false,
+    ariaLabel,
+    fetchOnlyOnSearch,
+    filterOption = true,
+    header = null,
+    invertSelection = false,
+    labelInValue = false,
+    lazyLoading = true,
+    loading,
+    mode = 'single',
+    name,
+    notFoundContent,
+    onError,
+    onChange,
+    onClear,
+    onDropdownVisibleChange,
+    optionFilterProps = ['label', 'value'],
+    options,
+    pageSize = DEFAULT_PAGE_SIZE,
+    placeholder = t('Select ...'),
+    showSearch = true,
+    sortComparator = DEFAULT_SORT_COMPARATOR,
+    tokenSeparators,
+    value,
+    getPopupContainer,
+    ...props
+  }: AsyncSelectProps,
+  ref: RefObject<AsyncSelectRef>,
+) => {
+  const isAsync = typeof options === 'function';
+  const isSingleMode = mode === 'single';
+  const shouldShowSearch = isAsync || allowNewOptions ? true : showSearch;
+  const [selectValue, setSelectValue] = useState(value);
+  const [inputValue, setInputValue] = useState('');
+  const [isLoading, setIsLoading] = useState(loading);
+  const [error, setError] = useState('');
+  const [isDropdownVisible, setIsDropdownVisible] = useState(false);
+  const [page, setPage] = useState(0);
+  const [totalCount, setTotalCount] = useState(0);
+  const [loadingEnabled, setLoadingEnabled] = useState(!lazyLoading);
+  const [allValuesLoaded, setAllValuesLoaded] = useState(false);
+  const fetchedQueries = useRef(new Map<string, number>());
+  const mappedMode = isSingleMode
+    ? undefined
+    : allowNewOptions
+    ? 'tags'
+    : 'multiple';
+  const allowFetch = !fetchOnlyOnSearch || inputValue;
+
+  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,
+    [selectValue],
+  );
+  const sortComparatorWithSearch = useCallback(
+    (a: AntdLabeledValue, b: AntdLabeledValue) =>
+      sortSelectedFirst(a, b) || sortComparator(a, b, inputValue),
+    [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.
+      (isAsync ? sortComparator(a, b, '') : 0),
+    [isAsync, sortComparator, sortSelectedFirst],
+  );
+
+  const initialOptions = useMemo(
+    () => (options && Array.isArray(options) ? options.slice() : 
EMPTY_OPTIONS),
+    [options],
+  );
+  const initialOptionsSorted = useMemo(
+    () => initialOptions.slice().sort(sortComparatorForNoSearch),
+    [initialOptions, sortComparatorForNoSearch],
+  );
+
+  const [selectOptions, setSelectOptions] =
+    useState<OptionsType>(initialOptionsSorted);
+
+  // add selected values to options list if they are not in it
+  const fullSelectOptions = useMemo(() => {
+    const missingValues: OptionsType = ensureIsArray(selectValue)
+      .filter(opt => !hasOption(getValue(opt), selectOptions))
+      .map(opt =>
+        isLabeledValue(opt) ? opt : { value: opt, label: String(opt) },
+      );
+    return missingValues.length > 0
+      ? missingValues.concat(selectOptions)
+      : selectOptions;
+  }, [selectOptions, selectValue]);
+
+  const hasCustomLabels = fullSelectOptions.some(opt => !!opt?.customLabel);
+
+  const handleOnSelect = (
+    selectedItem: string | number | AntdLabeledValue | undefined,
+  ) => {
+    if (isSingleMode) {
+      setSelectValue(selectedItem);
+    } else {
+      setSelectValue(previousState => {
+        const array = ensureIsArray(previousState);
+        const value = getValue(selectedItem);
+        // Tokenized values can contain duplicated values
+        if (!hasOption(value, array)) {
+          const result = [...array, selectedItem];
+          return isLabeledValue(selectedItem)
+            ? (result as AntdLabeledValue[])
+            : (result as (string | number)[]);
+        }
+        return previousState;
+      });
+    }
+    setInputValue('');
+  };
+
+  const handleOnDeselect = (
+    value: string | number | AntdLabeledValue | undefined,
+  ) => {
+    if (Array.isArray(selectValue)) {
+      if (isLabeledValue(value)) {
+        const array = selectValue as AntdLabeledValue[];
+        setSelectValue(array.filter(element => element.value !== value.value));
+      } else {
+        const array = selectValue as (string | number)[];
+        setSelectValue(array.filter(element => element !== value));
+      }
+    }
+    setInputValue('');
+  };
+
+  const internalOnError = useCallback(
+    (response: Response) =>
+      getClientErrorObject(response).then(e => {
+        const { error } = e;
+        setError(error);
+
+        if (onError) {
+          onError(error);
+        }
+      }),
+    [onError],
+  );
+
+  const mergeData = useCallback(
+    (data: OptionsType) => {
+      let mergedData: OptionsType = [];
+      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));
+        // merges with existing and creates unique options
+        setSelectOptions(prevOptions => {
+          mergedData = prevOptions
+            .filter(previousOption => !dataValues.has(previousOption.value))
+            .concat(data)
+            .sort(sortComparatorForNoSearch);
+          return mergedData;
+        });
+      }
+      return mergedData;
+    },
+    [sortComparatorForNoSearch],
+  );
+
+  const fetchPage = useMemo(
+    () => (search: string, page: number) => {
+      setPage(page);
+      if (allValuesLoaded) {
+        setIsLoading(false);
+        return;
+      }
+      const key = getQueryCacheKey(search, page, pageSize);
+      const cachedCount = fetchedQueries.current.get(key);
+      if (cachedCount !== undefined) {
+        setTotalCount(cachedCount);
+        setIsLoading(false);
+        return;
+      }
+      setIsLoading(true);
+      const fetchOptions = options as OptionsPagePromise;
+      fetchOptions(search, page, pageSize)
+        .then(({ data, totalCount }: OptionsTypePage) => {
+          const mergedData = mergeData(data);
+          fetchedQueries.current.set(key, totalCount);
+          setTotalCount(totalCount);
+          if (
+            !fetchOnlyOnSearch &&
+            value === '' &&
+            mergedData.length >= totalCount
+          ) {
+            setAllValuesLoaded(true);
+          }
+        })
+        .catch(internalOnError)
+        .finally(() => {
+          setIsLoading(false);
+        });
+    },
+    [
+      allValuesLoaded,
+      fetchOnlyOnSearch,
+      mergeData,
+      internalOnError,
+      options,
+      pageSize,
+      value,
+    ],
+  );
+
+  const debouncedFetchPage = useMemo(
+    () => debounce(fetchPage, SLOW_DEBOUNCE),
+    [fetchPage],
+  );
+
+  const handleOnSearch = (search: string) => {
+    const searchValue = search.trim();
+    if (allowNewOptions && isSingleMode) {
+      const newOption = searchValue &&
+        !hasOption(searchValue, fullSelectOptions, true) && {
+          label: searchValue,
+          value: searchValue,
+          isNewOption: true,
+        };
+      const cleanSelectOptions = fullSelectOptions.filter(
+        opt => !opt.isNewOption || hasOption(opt.value, selectValue),
+      );
+      const newOptions = newOption
+        ? [newOption, ...cleanSelectOptions]
+        : cleanSelectOptions;
+      setSelectOptions(newOptions);
+    }
+    if (
+      isAsync &&
+      !allValuesLoaded &&
+      loadingEnabled &&
+      !fetchedQueries.current.has(getQueryCacheKey(searchValue, 0, pageSize))
+    ) {
+      // if fetch only on search but search value is empty, then should not be
+      // in loading state
+      setIsLoading(!(fetchOnlyOnSearch && !searchValue));
+    }
+    setInputValue(search);
+  };
+
+  const handlePagination = (e: UIEvent<HTMLElement>) => {
+    const vScroll = e.currentTarget;
+    const thresholdReached =
+      vScroll.scrollTop > (vScroll.scrollHeight - vScroll.offsetHeight) * 0.7;
+    const hasMoreData = page * pageSize + pageSize < totalCount;
+
+    if (!isLoading && isAsync && hasMoreData && thresholdReached) {
+      const newPage = page + 1;
+      fetchPage(inputValue, newPage);
+    }
+  };
+
+  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 handleOnDropdownVisibleChange = (isDropdownVisible: boolean) => {
+    setIsDropdownVisible(isDropdownVisible);
+
+    if (isAsync) {
+      // loading is enabled when dropdown is open,
+      // disabled when dropdown is closed
+      if (loadingEnabled !== isDropdownVisible) {
+        setLoadingEnabled(isDropdownVisible);
+      }
+      // when closing dropdown, always reset loading state
+      if (!isDropdownVisible && isLoading) {
+        // delay is for the animation of closing the dropdown
+        // so the dropdown doesn't flash between "Loading..." and "No data"
+        // before closing.
+        setTimeout(() => {
+          setIsLoading(false);
+        }, 250);
+      }
+    }
+    // if no search input value, force sort options because it won't be sorted 
by
+    // `filterSort`.
+    if (isDropdownVisible && !inputValue && selectOptions.length > 1) {
+      const sortedOptions = isAsync
+        ? selectOptions.slice().sort(sortComparatorForNoSearch)
+        : // if not in async mode, revert to the original select options
+          // (with selected options still sorted to the top)
+          initialOptionsSorted;
+      if (!isEqual(sortedOptions, selectOptions)) {
+        setSelectOptions(sortedOptions);
+      }
+    }
+
+    if (onDropdownVisibleChange) {
+      onDropdownVisibleChange(isDropdownVisible);
+    }
+  };
+
+  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} /> : originNode;
+  };
+
+  // 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);
+    if (onClear) {
+      onClear();
+    }
+  };
+
+  useEffect(() => {
+    // when `options` list is updated from component prop, reset states
+    fetchedQueries.current.clear();
+    setAllValuesLoaded(false);
+    setSelectOptions(initialOptions);
+  }, [initialOptions]);
+
+  useEffect(() => {
+    setSelectValue(value);
+  }, [value]);
+
+  // Stop the invocation of the debounced function after unmounting
+  useEffect(
+    () => () => {
+      debouncedFetchPage.cancel();
+    },
+    [debouncedFetchPage],
+  );
+
+  useEffect(() => {
+    if (isAsync && loadingEnabled && allowFetch) {
+      // trigger fetch every time inputValue changes
+      if (inputValue) {
+        debouncedFetchPage(inputValue, 0);
+      } else {
+        fetchPage('', 0);
+      }
+    }
+  }, [
+    isAsync,
+    loadingEnabled,
+    fetchPage,
+    allowFetch,
+    inputValue,
+    debouncedFetchPage,
+  ]);
+
+  useEffect(() => {
+    if (loading !== undefined && loading !== isLoading) {
+      setIsLoading(loading);
+    }
+  }, [isLoading, loading]);
+
+  const clearCache = () => fetchedQueries.current.clear();
+
+  useImperativeHandle(
+    ref,
+    () => ({
+      ...(ref.current as HTMLInputElement),
+      clearCache,
+    }),
+    [ref],
+  );
+
+  return (
+    <StyledContainer>
+      {header}
+      <StyledSelect
+        allowClear={!isLoading && allowClear}
+        aria-label={ariaLabel || name}
+        dropdownRender={dropdownRender}
+        filterOption={handleFilterOption}
+        filterSort={sortComparatorWithSearch}
+        getPopupContainer={
+          getPopupContainer || (triggerNode => triggerNode.parentNode)
+        }
+        labelInValue={isAsync || labelInValue}
+        maxTagCount={MAX_TAG_COUNT}
+        mode={mappedMode}
+        notFoundContent={isLoading ? t('Loading...') : notFoundContent}
+        onDeselect={handleOnDeselect}
+        onDropdownVisibleChange={handleOnDropdownVisibleChange}
+        onPopupScroll={isAsync ? handlePagination : undefined}
+        onSearch={shouldShowSearch ? handleOnSearch : undefined}
+        onSelect={handleOnSelect}
+        onClear={handleClear}
+        onChange={onChange}
+        options={hasCustomLabels ? undefined : fullSelectOptions}
+        placeholder={placeholder}
+        showSearch={shouldShowSearch}
+        showArrow
+        tokenSeparators={tokenSeparators || TOKEN_SEPARATORS}
+        value={selectValue}
+        suffixIcon={getSuffixIcon()}
+        menuItemSelectedIcon={
+          invertSelection ? (
+            <StyledStopOutlined iconSize="m" />
+          ) : (
+            <StyledCheckOutlined iconSize="m" />
+          )
+        }
+        ref={ref}
+        {...props}
+      >
+        {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>
+            );
+          })}
+      </StyledSelect>
+    </StyledContainer>
+  );
+};
+
+export default forwardRef(AsyncSelect);
diff --git a/superset-frontend/src/components/Select/Select.stories.tsx 
b/superset-frontend/src/components/Select/Select.stories.tsx
index 9b38586467..efcd91c0c3 100644
--- a/superset-frontend/src/components/Select/Select.stories.tsx
+++ b/superset-frontend/src/components/Select/Select.stories.tsx
@@ -19,12 +19,8 @@
 import React, { ReactNode, useState, useCallback, useRef } from 'react';
 import Button from 'src/components/Button';
 import ControlHeader from 'src/explore/components/ControlHeader';
-import Select, {
-  SelectProps,
-  OptionsTypePage,
-  OptionsType,
-  SelectRef,
-} from './Select';
+import AsyncSelect, { AsyncSelectProps, AsyncSelectRef } from './AsyncSelect';
+import Select, { SelectProps, OptionsTypePage, OptionsType } from './Select';
 
 export default {
   title: 'Select',
@@ -381,19 +377,19 @@ const USERS = [
   'Ilenia',
 ].sort();
 
-export const AsyncSelect = ({
+export const AsynchronousSelect = ({
   fetchOnlyOnSearch,
   withError,
   withInitialValue,
   responseTime,
   ...rest
-}: SelectProps & {
+}: AsyncSelectProps & {
   withError: boolean;
   withInitialValue: boolean;
   responseTime: number;
 }) => {
   const [requests, setRequests] = useState<ReactNode[]>([]);
-  const ref = useRef<SelectRef>(null);
+  const ref = useRef<AsyncSelectRef>(null);
 
   const getResults = (username?: string) => {
     let results: { label: string; value: string }[] = [];
@@ -463,12 +459,12 @@ export const AsyncSelect = ({
           width: DEFAULT_WIDTH,
         }}
       >
-        <Select
+        <AsyncSelect
           {...rest}
           ref={ref}
           fetchOnlyOnSearch={fetchOnlyOnSearch}
           options={withError ? fetchUserListError : fetchUserListPage}
-          placeholder={fetchOnlyOnSearch ? 'Type anything' : 'Select...'}
+          placeholder={fetchOnlyOnSearch ? 'Type anything' : 'AsyncSelect...'}
           value={
             withInitialValue
               ? { label: 'Valentina', value: 'Valentina' }
@@ -509,7 +505,7 @@ export const AsyncSelect = ({
   );
 };
 
-AsyncSelect.args = {
+AsynchronousSelect.args = {
   allowClear: false,
   allowNewOptions: false,
   fetchOnlyOnSearch: false,
@@ -519,7 +515,7 @@ AsyncSelect.args = {
   tokenSeparators: ['\n', '\t', ';'],
 };
 
-AsyncSelect.argTypes = {
+AsynchronousSelect.argTypes = {
   ...ARG_TYPES,
   header: {
     table: {
@@ -552,7 +548,7 @@ AsyncSelect.argTypes = {
   },
 };
 
-AsyncSelect.story = {
+AsynchronousSelect.story = {
   parameters: {
     knobs: {
       disable: true,
diff --git a/superset-frontend/src/components/Select/Select.test.tsx 
b/superset-frontend/src/components/Select/Select.test.tsx
index 1515a7c548..0b353bde38 100644
--- a/superset-frontend/src/components/Select/Select.test.tsx
+++ b/superset-frontend/src/components/Select/Select.test.tsx
@@ -16,11 +16,10 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import React, { RefObject } from 'react';
+import React from 'react';
 import { render, screen, waitFor, within } from 'spec/helpers/testing-library';
 import userEvent from '@testing-library/user-event';
 import { Select } from 'src/components';
-import { SelectRef } from './Select';
 
 const ARIA_LABEL = 'Test';
 const NEW_OPTION = 'Kyle';
@@ -56,21 +55,6 @@ const NULL_OPTION = { label: '<NULL>', value: null } as 
unknown as {
   value: number;
 };
 
-const loadOptions = async (search: string, page: number, pageSize: number) => {
-  const totalCount = OPTIONS.length;
-  const start = page * pageSize;
-  const deleteCount =
-    start + pageSize < totalCount ? pageSize : totalCount - start;
-  const data = OPTIONS.filter(option => option.label.match(search)).splice(
-    start,
-    deleteCount,
-  );
-  return {
-    data,
-    totalCount: OPTIONS.length,
-  };
-};
-
 const defaultProps = {
   allowClear: true,
   ariaLabel: ARIA_LABEL,
@@ -165,27 +149,6 @@ test('sort the options by label if no sort comparator is 
provided', async () =>
   );
 });
 
-test('sort the options using a custom sort comparator', async () => {
-  const sortComparator = (
-    option1: typeof OPTIONS[0],
-    option2: typeof OPTIONS[0],
-  ) => option1.gender.localeCompare(option2.gender);
-  render(
-    <Select
-      {...defaultProps}
-      options={loadOptions}
-      sortComparator={sortComparator}
-    />,
-  );
-  await open();
-  const options = await findAllSelectOptions();
-  const optionsPage = OPTIONS.slice(0, defaultProps.pageSize);
-  const sortedOptions = optionsPage.sort(sortComparator);
-  options.forEach((option, key) => {
-    expect(option).toHaveTextContent(sortedOptions[key].label);
-  });
-});
-
 test('should sort selected to top when in single mode', async () => {
   render(<Select {...defaultProps} mode="single" />);
   const originalLabels = OPTIONS.map(option => option.label);
@@ -383,7 +346,7 @@ test('clear all the values', async () => {
 });
 
 test('does not add a new option if allowNewOptions is false', async () => {
-  render(<Select {...defaultProps} options={loadOptions} />);
+  render(<Select {...defaultProps} options={OPTIONS} />);
   await open();
   await type(NEW_OPTION);
   expect(await screen.findByText(NO_DATA)).toBeInTheDocument();
@@ -413,18 +376,18 @@ test('adds the null option when selected in multiple 
mode', async () => {
   expect(values[1]).toHaveTextContent(NULL_OPTION.label);
 });
 
-test('static - renders the select with default props', () => {
+test('renders the select with default props', () => {
   render(<Select {...defaultProps} />);
   expect(getSelect()).toBeInTheDocument();
 });
 
-test('static - opens the select without any data', async () => {
+test('opens the select without any data', async () => {
   render(<Select {...defaultProps} options={[]} />);
   await open();
   expect(screen.getByText(NO_DATA)).toBeInTheDocument();
 });
 
-test('static - makes a selection in single mode', async () => {
+test('makes a selection in single mode', async () => {
   render(<Select {...defaultProps} />);
   const optionText = 'Emma';
   await open();
@@ -432,7 +395,7 @@ test('static - makes a selection in single mode', async () 
=> {
   expect(await findSelectValue()).toHaveTextContent(optionText);
 });
 
-test('static - multiple selections in multiple mode', async () => {
+test('multiple selections in multiple mode', async () => {
   render(<Select {...defaultProps} mode="multiple" />);
   await open();
   const [firstOption, secondOption] = OPTIONS;
@@ -443,7 +406,7 @@ test('static - multiple selections in multiple mode', async 
() => {
   expect(values[1]).toHaveTextContent(secondOption.label);
 });
 
-test('static - changes the selected item in single mode', async () => {
+test('changes the selected item in single mode', async () => {
   const onChange = jest.fn();
   render(<Select {...defaultProps} onChange={onChange} />);
   await open();
@@ -468,7 +431,7 @@ test('static - changes the selected item in single mode', 
async () => {
   expect(await findSelectValue()).toHaveTextContent(secondOption.label);
 });
 
-test('static - deselects an item in multiple mode', async () => {
+test('deselects an item in multiple mode', async () => {
   render(<Select {...defaultProps} mode="multiple" />);
   await open();
   const [firstOption, secondOption] = OPTIONS;
@@ -484,35 +447,35 @@ test('static - deselects an item in multiple mode', async 
() => {
   expect(values[0]).toHaveTextContent(secondOption.label);
 });
 
-test('static - adds a new option if none is available and allowNewOptions is 
true', async () => {
+test('adds a new option if none is available and allowNewOptions is true', 
async () => {
   render(<Select {...defaultProps} allowNewOptions />);
   await open();
   await type(NEW_OPTION);
   expect(await findSelectOption(NEW_OPTION)).toBeInTheDocument();
 });
 
-test('static - shows "No data" when allowNewOptions is false and a new option 
is entered', async () => {
+test('shows "No data" when allowNewOptions is false and a new option is 
entered', async () => {
   render(<Select {...defaultProps} allowNewOptions={false} />);
   await open();
   await type(NEW_OPTION);
   expect(await screen.findByText(NO_DATA)).toBeInTheDocument();
 });
 
-test('static - does not show "No data" when allowNewOptions is true and a new 
option is entered', async () => {
+test('does not show "No data" when allowNewOptions is true and a new option is 
entered', async () => {
   render(<Select {...defaultProps} allowNewOptions />);
   await open();
   await type(NEW_OPTION);
   expect(screen.queryByText(NO_DATA)).not.toBeInTheDocument();
 });
 
-test('static - does not show "Loading..." when allowNewOptions is false and a 
new option is entered', async () => {
+test('does not show "Loading..." when allowNewOptions is false and a new 
option is entered', async () => {
   render(<Select {...defaultProps} allowNewOptions={false} />);
   await open();
   await type(NEW_OPTION);
   expect(screen.queryByText(LOADING)).not.toBeInTheDocument();
 });
 
-test('static - does not add a new option if the option already exists', async 
() => {
+test('does not add a new option if the option already exists', async () => {
   render(<Select {...defaultProps} allowNewOptions />);
   const option = OPTIONS[0].label;
   await open();
@@ -520,12 +483,12 @@ test('static - does not add a new option if the option 
already exists', async ()
   expect(await findSelectOption(option)).toBeInTheDocument();
 });
 
-test('static - sets a initial value in single mode', async () => {
+test('sets a initial value in single mode', async () => {
   render(<Select {...defaultProps} value={OPTIONS[0]} />);
   expect(await findSelectValue()).toHaveTextContent(OPTIONS[0].label);
 });
 
-test('static - sets a initial value in multiple mode', async () => {
+test('sets a initial value in multiple mode', async () => {
   render(
     <Select
       {...defaultProps}
@@ -538,7 +501,7 @@ test('static - sets a initial value in multiple mode', 
async () => {
   expect(values[1]).toHaveTextContent(OPTIONS[1].label);
 });
 
-test('static - searches for an item', async () => {
+test('searches for an item', async () => {
   render(<Select {...defaultProps} />);
   const search = 'Oli';
   await type(search);
@@ -548,303 +511,7 @@ test('static - searches for an item', async () => {
   expect(options[1]).toHaveTextContent('Olivia');
 });
 
-test('async - renders the select with default props', () => {
-  render(<Select {...defaultProps} options={loadOptions} />);
-  expect(getSelect()).toBeInTheDocument();
-});
-
-test('async - opens the select without any data', async () => {
-  render(
-    <Select
-      {...defaultProps}
-      options={async () => ({ data: [], totalCount: 0 })}
-    />,
-  );
-  await open();
-  expect(await screen.findByText(/no data/i)).toBeInTheDocument();
-});
-
-test('async - displays the loading indicator when opening', async () => {
-  render(<Select {...defaultProps} options={loadOptions} />);
-  await waitFor(() => {
-    userEvent.click(getSelect());
-    expect(screen.getByText(LOADING)).toBeInTheDocument();
-  });
-  expect(screen.queryByText(LOADING)).not.toBeInTheDocument();
-});
-
-test('async - makes a selection in single mode', async () => {
-  render(<Select {...defaultProps} options={loadOptions} />);
-  const optionText = 'Emma';
-  await open();
-  userEvent.click(await findSelectOption(optionText));
-  expect(await findSelectValue()).toHaveTextContent(optionText);
-});
-
-test('async - multiple selections in multiple mode', async () => {
-  render(<Select {...defaultProps} options={loadOptions} mode="multiple" />);
-  await open();
-  const [firstOption, secondOption] = OPTIONS;
-  userEvent.click(await findSelectOption(firstOption.label));
-  userEvent.click(await findSelectOption(secondOption.label));
-  const values = await findAllSelectValues();
-  expect(values[0]).toHaveTextContent(firstOption.label);
-  expect(values[1]).toHaveTextContent(secondOption.label);
-});
-
-test('async - changes the selected item in single mode', async () => {
-  const onChange = jest.fn();
-  render(
-    <Select {...defaultProps} options={loadOptions} onChange={onChange} />,
-  );
-  await open();
-  const [firstOption, secondOption] = OPTIONS;
-  userEvent.click(await findSelectOption(firstOption.label));
-  expect(onChange).toHaveBeenCalledWith(
-    expect.objectContaining({
-      label: firstOption.label,
-      value: firstOption.value,
-    }),
-    firstOption,
-  );
-  expect(await findSelectValue()).toHaveTextContent(firstOption.label);
-  userEvent.click(await findSelectOption(secondOption.label));
-  expect(onChange).toHaveBeenCalledWith(
-    expect.objectContaining({
-      label: secondOption.label,
-      value: secondOption.value,
-    }),
-    secondOption,
-  );
-  expect(await findSelectValue()).toHaveTextContent(secondOption.label);
-});
-
-test('async - deselects an item in multiple mode', async () => {
-  render(<Select {...defaultProps} options={loadOptions} mode="multiple" />);
-  await open();
-  const option3 = OPTIONS[2];
-  const option8 = OPTIONS[7];
-  userEvent.click(await findSelectOption(option8.label));
-  userEvent.click(await findSelectOption(option3.label));
-
-  let options = await findAllSelectOptions();
-  expect(options).toHaveLength(Math.min(defaultProps.pageSize, 
OPTIONS.length));
-  expect(options[0]).toHaveTextContent(OPTIONS[0].label);
-  expect(options[1]).toHaveTextContent(OPTIONS[1].label);
-
-  await type('{esc}');
-  await open();
-
-  // should rank selected options to the top after menu closes
-  options = await findAllSelectOptions();
-  expect(options).toHaveLength(Math.min(defaultProps.pageSize, 
OPTIONS.length));
-  expect(options[0]).toHaveTextContent(option3.label);
-  expect(options[1]).toHaveTextContent(option8.label);
-
-  let values = await findAllSelectValues();
-  expect(values).toHaveLength(2);
-  // should keep the order by which the options were selected
-  expect(values[0]).toHaveTextContent(option8.label);
-  expect(values[1]).toHaveTextContent(option3.label);
-
-  userEvent.click(await findSelectOption(option3.label));
-  values = await findAllSelectValues();
-  expect(values.length).toBe(1);
-  expect(values[0]).toHaveTextContent(option8.label);
-});
-
-test('async - adds a new option if none is available and allowNewOptions is 
true', async () => {
-  render(<Select {...defaultProps} options={loadOptions} allowNewOptions />);
-  await open();
-  await type(NEW_OPTION);
-  expect(await findSelectOption(NEW_OPTION)).toBeInTheDocument();
-});
-
-test('async - does not add a new option if the option already exists', async 
() => {
-  render(<Select {...defaultProps} options={loadOptions} allowNewOptions />);
-  const option = OPTIONS[0].label;
-  await open();
-  await type(option);
-  await waitFor(() => {
-    const array = within(
-      getElementByClassName('.rc-virtual-list'),
-    ).getAllByText(option);
-    expect(array.length).toBe(1);
-  });
-});
-
-test('async - shows "No data" when allowNewOptions is false and a new option 
is entered', async () => {
-  render(
-    <Select
-      {...defaultProps}
-      options={loadOptions}
-      allowNewOptions={false}
-      showSearch
-    />,
-  );
-  await open();
-  await type(NEW_OPTION);
-  expect(await screen.findByText(NO_DATA)).toBeInTheDocument();
-});
-
-test('async - does not show "No data" when allowNewOptions is true and a new 
option is entered', async () => {
-  render(<Select {...defaultProps} options={loadOptions} allowNewOptions />);
-  await open();
-  await type(NEW_OPTION);
-  expect(screen.queryByText(NO_DATA)).not.toBeInTheDocument();
-});
-
-test('async - sets a initial value in single mode', async () => {
-  render(<Select {...defaultProps} options={loadOptions} value={OPTIONS[0]} 
/>);
-  expect(await findSelectValue()).toHaveTextContent(OPTIONS[0].label);
-});
-
-test('async - sets a initial value in multiple mode', async () => {
-  render(
-    <Select
-      {...defaultProps}
-      mode="multiple"
-      options={loadOptions}
-      value={[OPTIONS[0], OPTIONS[1]]}
-    />,
-  );
-  const values = await findAllSelectValues();
-  expect(values[0]).toHaveTextContent(OPTIONS[0].label);
-  expect(values[1]).toHaveTextContent(OPTIONS[1].label);
-});
-
-test('async - searches for matches in both loaded and unloaded pages', async 
() => {
-  render(<Select {...defaultProps} options={loadOptions} />);
-  await open();
-  await type('and');
-
-  let options = await findAllSelectOptions();
-  expect(options.length).toBe(1);
-  expect(options[0]).toHaveTextContent('Alehandro');
-
-  await screen.findByText('Sandro');
-  options = await findAllSelectOptions();
-  expect(options.length).toBe(2);
-  expect(options[0]).toHaveTextContent('Alehandro');
-  expect(options[1]).toHaveTextContent('Sandro');
-});
-
-test('async - searches for an item in a page not loaded', async () => {
-  const mock = jest.fn(loadOptions);
-  render(<Select {...defaultProps} options={mock} />);
-  const search = 'Sandro';
-  await open();
-  await type(search);
-  await waitFor(() => expect(mock).toHaveBeenCalledTimes(2));
-  const options = await findAllSelectOptions();
-  expect(options.length).toBe(1);
-  expect(options[0]).toHaveTextContent(search);
-});
-
-test('async - does not fetches data when rendering', async () => {
-  const loadOptions = jest.fn(async () => ({ data: [], totalCount: 0 }));
-  render(<Select {...defaultProps} options={loadOptions} />);
-  expect(loadOptions).not.toHaveBeenCalled();
-});
-
-test('async - fetches data when opening', async () => {
-  const loadOptions = jest.fn(async () => ({ data: [], totalCount: 0 }));
-  render(<Select {...defaultProps} options={loadOptions} />);
-  await open();
-  expect(loadOptions).toHaveBeenCalled();
-});
-
-test('async - fetches data only after a search input is entered if 
fetchOnlyOnSearch is true', async () => {
-  const loadOptions = jest.fn(async () => ({ data: [], totalCount: 0 }));
-  render(<Select {...defaultProps} options={loadOptions} fetchOnlyOnSearch />);
-  await open();
-  await waitFor(() => expect(loadOptions).not.toHaveBeenCalled());
-  await type('search');
-  await waitFor(() => expect(loadOptions).toHaveBeenCalled());
-});
-
-test('async - displays an error message when an exception is thrown while 
fetching', async () => {
-  const error = 'Fetch error';
-  const loadOptions = async () => {
-    throw new Error(error);
-  };
-  render(<Select {...defaultProps} options={loadOptions} />);
-  await open();
-  expect(screen.getByText(error)).toBeInTheDocument();
-});
-
-test('async - does not fire a new request for the same search input', async () 
=> {
-  const loadOptions = jest.fn(async () => ({ data: [], totalCount: 0 }));
-  render(<Select {...defaultProps} options={loadOptions} fetchOnlyOnSearch />);
-  await type('search');
-  expect(await screen.findByText(NO_DATA)).toBeInTheDocument();
-  expect(loadOptions).toHaveBeenCalledTimes(1);
-  clearAll();
-  await type('search');
-  expect(await screen.findByText(LOADING)).toBeInTheDocument();
-  expect(loadOptions).toHaveBeenCalledTimes(1);
-});
-
-test('async - does not fire a new request if all values have been fetched', 
async () => {
-  const mock = jest.fn(loadOptions);
-  const search = 'George';
-  const pageSize = OPTIONS.length;
-  render(<Select {...defaultProps} options={mock} pageSize={pageSize} />);
-  await open();
-  expect(mock).toHaveBeenCalledTimes(1);
-  await type(search);
-  expect(await findSelectOption(search)).toBeInTheDocument();
-  expect(mock).toHaveBeenCalledTimes(1);
-});
-
-test('async - fires a new request if all values have not been fetched', async 
() => {
-  const mock = jest.fn(loadOptions);
-  const pageSize = OPTIONS.length / 2;
-  render(<Select {...defaultProps} options={mock} pageSize={pageSize} />);
-  await open();
-  expect(mock).toHaveBeenCalledTimes(1);
-  await type('or');
-
-  // `George` is on the first page so when it appears the API has not been 
called again
-  expect(await findSelectOption('George')).toBeInTheDocument();
-  expect(mock).toHaveBeenCalledTimes(1);
-
-  // `Igor` is on the second paged API request
-  expect(await findSelectOption('Igor')).toBeInTheDocument();
-  expect(mock).toHaveBeenCalledTimes(2);
-});
-
-test('async - requests the options again after clearing the cache', async () 
=> {
-  const ref: RefObject<SelectRef> = { current: null };
-  const mock = jest.fn(loadOptions);
-  const pageSize = OPTIONS.length;
-  render(
-    <Select {...defaultProps} options={mock} pageSize={pageSize} ref={ref} />,
-  );
-  await open();
-  expect(mock).toHaveBeenCalledTimes(1);
-  ref.current?.clearCache();
-  await type('{esc}');
-  await open();
-  expect(mock).toHaveBeenCalledTimes(2);
-});
-
-test('async - triggers getPopupContainer if passed', async () => {
-  const getPopupContainer = jest.fn();
-  render(
-    <div>
-      <Select
-        {...defaultProps}
-        options={loadOptions}
-        getPopupContainer={getPopupContainer}
-      />
-    </div>,
-  );
-  await open();
-  expect(getPopupContainer).toHaveBeenCalled();
-});
-
-test('static - triggers getPopupContainer if passed', async () => {
+test('triggers getPopupContainer if passed', async () => {
   const getPopupContainer = jest.fn();
   render(<Select {...defaultProps} getPopupContainer={getPopupContainer} />);
   await open();
diff --git a/superset-frontend/src/components/index.ts 
b/superset-frontend/src/components/index.ts
index a370836fcf..bfa341a9dd 100644
--- a/superset-frontend/src/components/index.ts
+++ b/superset-frontend/src/components/index.ts
@@ -23,6 +23,7 @@
  * E.g. import { Select } from 'src/components'
  */
 export { default as Select } from './Select/Select';
+export { default as AsyncSelect } from './Select/AsyncSelect';
 
 /*
  * Components that don't conflict with the ones in src/components.
diff --git 
a/superset-frontend/src/dashboard/components/PropertiesModal/index.tsx 
b/superset-frontend/src/dashboard/components/PropertiesModal/index.tsx
index effd18b3d0..749411f2e9 100644
--- a/superset-frontend/src/dashboard/components/PropertiesModal/index.tsx
+++ b/superset-frontend/src/dashboard/components/PropertiesModal/index.tsx
@@ -21,7 +21,7 @@ import { Input } from 'src/components/Input';
 import { FormItem } from 'src/components/Form';
 import jsonStringify from 'json-stringify-pretty-compact';
 import Button from 'src/components/Button';
-import { Select, Row, Col, AntdForm } from 'src/components';
+import { AsyncSelect, Row, Col, AntdForm } from 'src/components';
 import rison from 'rison';
 import {
   styled,
@@ -370,7 +370,7 @@ const PropertiesModal = ({
         <Col xs={24} md={12}>
           <h3 style={{ marginTop: '1em' }}>{t('Access')}</h3>
           <StyledFormItem label={t('Owners')}>
-            <Select
+            <AsyncSelect
               allowClear
               ariaLabel={t('Owners')}
               disabled={isLoading}
@@ -417,7 +417,7 @@ const PropertiesModal = ({
         <Row gutter={16}>
           <Col xs={24} md={12}>
             <StyledFormItem label={t('Owners')}>
-              <Select
+              <AsyncSelect
                 allowClear
                 ariaLabel={t('Owners')}
                 disabled={isLoading}
@@ -437,7 +437,7 @@ const PropertiesModal = ({
           </Col>
           <Col xs={24} md={12}>
             <StyledFormItem label={t('Roles')}>
-              <Select
+              <AsyncSelect
                 allowClear
                 ariaLabel={t('Roles')}
                 disabled={isLoading}
diff --git 
a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/DatasetSelect.tsx
 
b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/DatasetSelect.tsx
index 8ecb75777f..ed69c6734f 100644
--- 
a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/DatasetSelect.tsx
+++ 
b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/DatasetSelect.tsx
@@ -19,7 +19,7 @@
 import React, { useCallback, useMemo } from 'react';
 import rison from 'rison';
 import { t, SupersetClient } from '@superset-ui/core';
-import { Select } from 'src/components';
+import { AsyncSelect } from 'src/components';
 import { cacheWrapper } from 'src/utils/cacheWrapper';
 import {
   ClientErrorObject,
@@ -85,7 +85,7 @@ const DatasetSelect = ({ onChange, value }: 
DatasetSelectProps) => {
   };
 
   return (
-    <Select
+    <AsyncSelect
       ariaLabel={t('Dataset')}
       value={value}
       options={loadDatasetOptions}
diff --git a/superset-frontend/src/explore/components/PropertiesModal/index.tsx 
b/superset-frontend/src/explore/components/PropertiesModal/index.tsx
index de1639894b..3aa0fa60d2 100644
--- a/superset-frontend/src/explore/components/PropertiesModal/index.tsx
+++ b/superset-frontend/src/explore/components/PropertiesModal/index.tsx
@@ -20,7 +20,7 @@ import React, { useMemo, useState, useCallback, useEffect } 
from 'react';
 import Modal from 'src/components/Modal';
 import { Input, TextArea } from 'src/components/Input';
 import Button from 'src/components/Button';
-import { Select, Row, Col, AntdForm } from 'src/components';
+import { AsyncSelect, Row, Col, AntdForm } from 'src/components';
 import { SelectValue } from 'antd/lib/select';
 import rison from 'rison';
 import { t, SupersetClient, styled } from '@superset-ui/core';
@@ -298,7 +298,7 @@ function PropertiesModal({
             </FormItem>
             <h3 style={{ marginTop: '1em' }}>{t('Access')}</h3>
             <FormItem label={ownersLabel}>
-              <Select
+              <AsyncSelect
                 ariaLabel={ownersLabel}
                 mode="multiple"
                 name="owners"
diff --git a/superset-frontend/src/views/CRUD/alert/AlertReportModal.test.jsx 
b/superset-frontend/src/views/CRUD/alert/AlertReportModal.test.jsx
index 1598e5a926..8b3ff1d016 100644
--- a/superset-frontend/src/views/CRUD/alert/AlertReportModal.test.jsx
+++ b/superset-frontend/src/views/CRUD/alert/AlertReportModal.test.jsx
@@ -24,7 +24,7 @@ import fetchMock from 'fetch-mock';
 import { act } from 'react-dom/test-utils';
 import AlertReportModal from 'src/views/CRUD/alert/AlertReportModal';
 import Modal from 'src/components/Modal';
-import { Select } from 'src/components';
+import { Select, AsyncSelect } from 'src/components';
 import { Switch } from 'src/components/Switch';
 import { Radio } from 'src/components/Radio';
 import TextAreaControl from 'src/explore/components/controls/TextAreaControl';
@@ -182,7 +182,9 @@ describe('AlertReportModal', () => {
 
   it('renders five select elements when in report mode', () => {
     expect(wrapper.find(Select)).toExist();
-    expect(wrapper.find(Select)).toHaveLength(5);
+    expect(wrapper.find(AsyncSelect)).toExist();
+    expect(wrapper.find(Select)).toHaveLength(2);
+    expect(wrapper.find(AsyncSelect)).toHaveLength(3);
   });
 
   it('renders Switch element', () => {
@@ -220,7 +222,9 @@ describe('AlertReportModal', () => {
 
   it('renders five select element when in report mode', () => {
     expect(wrapper.find(Select)).toExist();
-    expect(wrapper.find(Select)).toHaveLength(5);
+    expect(wrapper.find(AsyncSelect)).toExist();
+    expect(wrapper.find(Select)).toHaveLength(2);
+    expect(wrapper.find(AsyncSelect)).toHaveLength(3);
   });
 
   it('renders seven select elements when in alert mode', async () => {
@@ -232,7 +236,9 @@ describe('AlertReportModal', () => {
     const addWrapper = await mountAndWait(props);
 
     expect(addWrapper.find(Select)).toExist();
-    expect(addWrapper.find(Select)).toHaveLength(7);
+    expect(addWrapper.find(AsyncSelect)).toExist();
+    expect(addWrapper.find(Select)).toHaveLength(3);
+    expect(addWrapper.find(AsyncSelect)).toHaveLength(4);
   });
 
   it('renders value input element when in alert mode', async () => {
diff --git a/superset-frontend/src/views/CRUD/alert/AlertReportModal.tsx 
b/superset-frontend/src/views/CRUD/alert/AlertReportModal.tsx
index 04398a3582..820a83b8c3 100644
--- a/superset-frontend/src/views/CRUD/alert/AlertReportModal.tsx
+++ b/superset-frontend/src/views/CRUD/alert/AlertReportModal.tsx
@@ -38,11 +38,11 @@ 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 Select, { propertyComparator } from 'src/components/Select/Select';
+import { propertyComparator } from 'src/components/Select/Select';
 import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags';
 import withToasts from 'src/components/MessageToasts/withToasts';
 import Owner from 'src/types/Owner';
-import { AntdCheckbox } from 'src/components';
+import { AntdCheckbox, AsyncSelect, Select } from 'src/components';
 import TextAreaControl from 'src/explore/components/controls/TextAreaControl';
 import { useCommonConf } from 'src/views/CRUD/data/database/state';
 import {
@@ -1098,7 +1098,7 @@ const AlertReportModal: 
FunctionComponent<AlertReportModalProps> = ({
               <span className="required">*</span>
             </div>
             <div data-test="owners-select" className="input-container">
-              <Select
+              <AsyncSelect
                 ariaLabel={t('Owners')}
                 allowClear
                 name="owners"
@@ -1146,7 +1146,7 @@ const AlertReportModal: 
FunctionComponent<AlertReportModalProps> = ({
                   <span className="required">*</span>
                 </div>
                 <div className="input-container">
-                  <Select
+                  <AsyncSelect
                     ariaLabel={t('Database')}
                     name="source"
                     value={
@@ -1319,7 +1319,7 @@ const AlertReportModal: 
FunctionComponent<AlertReportModalProps> = ({
               <StyledRadio value="dashboard">{t('Dashboard')}</StyledRadio>
               <StyledRadio value="chart">{t('Chart')}</StyledRadio>
             </Radio.Group>
-            <Select
+            <AsyncSelect
               ariaLabel={t('Chart')}
               css={{
                 display: contentType === 'chart' ? 'inline' : 'none',
@@ -1336,7 +1336,7 @@ const AlertReportModal: 
FunctionComponent<AlertReportModalProps> = ({
               options={loadChartOptions}
               onChange={onChartChange}
             />
-            <Select
+            <AsyncSelect
               ariaLabel={t('Dashboard')}
               css={{
                 display: contentType === 'dashboard' ? 'inline' : 'none',

Reply via email to