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/27 11:38:23 UTC

[superset] branch master updated: refactor: Organizes the Select files (#21589)

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 bb1cf7f145 refactor: Organizes the Select files (#21589)
bb1cf7f145 is described below

commit bb1cf7f1456341fe5e6ca9df47555e312f6bc0f6
Author: Michael S. Molina <70...@users.noreply.github.com>
AuthorDate: Tue Sep 27 08:38:06 2022 -0300

    refactor: Organizes the Select files (#21589)
---
 ...{Select.stories.tsx => AsyncSelect.stories.tsx} | 225 +-----------------
 .../src/components/Select/AsyncSelect.tsx          |  94 ++------
 .../src/components/Select/Select.stories.tsx       | 247 +-------------------
 superset-frontend/src/components/Select/Select.tsx |  33 ++-
 .../src/components/Select/constants.ts             |  52 +++++
 superset-frontend/src/components/Select/styles.tsx |  90 +++++++
 superset-frontend/src/components/Select/types.ts   | 201 ++++++++++++++++
 superset-frontend/src/components/Select/utils.tsx  | 259 +--------------------
 .../controls/SelectAsyncControl/index.tsx          |   3 +-
 9 files changed, 397 insertions(+), 807 deletions(-)

diff --git a/superset-frontend/src/components/Select/Select.stories.tsx b/superset-frontend/src/components/Select/AsyncSelect.stories.tsx
similarity index 62%
copy from superset-frontend/src/components/Select/Select.stories.tsx
copy to superset-frontend/src/components/Select/AsyncSelect.stories.tsx
index e9a03fe563..547fc7fa99 100644
--- a/superset-frontend/src/components/Select/Select.stories.tsx
+++ b/superset-frontend/src/components/Select/AsyncSelect.stories.tsx
@@ -24,15 +24,17 @@ import React, {
   useMemo,
 } from 'react';
 import Button from 'src/components/Button';
-import ControlHeader from 'src/explore/components/ControlHeader';
-import AsyncSelect, { AsyncSelectProps, AsyncSelectRef } from './AsyncSelect';
-import { SelectOptionsType, SelectOptionsTypePage } from './utils';
-
-import Select, { SelectProps } from './Select';
+import AsyncSelect from './AsyncSelect';
+import {
+  SelectOptionsType,
+  AsyncSelectProps,
+  AsyncSelectRef,
+  SelectOptionsTypePage,
+} from './types';
 
 export default {
-  title: 'Select',
-  component: Select,
+  title: 'AsyncSelect',
+  component: AsyncSelect,
 };
 
 const DEFAULT_WIDTH = 200;
@@ -63,25 +65,6 @@ const options: SelectOptionsType = [
   { label: 'I', value: 'I' },
 ];
 
-const selectPositions = [
-  {
-    id: 'topLeft',
-    style: { top: '0', left: '0' },
-  },
-  {
-    id: 'topRight',
-    style: { top: '0', right: '0' },
-  },
-  {
-    id: 'bottomLeft',
-    style: { bottom: '0', left: '0' },
-  },
-  {
-    id: 'bottomRight',
-    style: { bottom: '0', right: '0' },
-  },
-];
-
 const ARG_TYPES = {
   options: {
     defaultValue: options,
@@ -142,196 +125,6 @@ const ARG_TYPES = {
   },
 };
 
-const mountHeader = (type: String) => {
-  let header;
-  if (type === 'text') {
-    header = 'Text header';
-  } else if (type === 'control') {
-    header = (
-      <ControlHeader
-        label="Control header"
-        warning="Example of warning messsage"
-      />
-    );
-  }
-  return header;
-};
-
-const generateOptions = (opts: SelectOptionsType, count: number) => {
-  let generated = opts.slice();
-  let iteration = 0;
-  while (generated.length < count) {
-    iteration += 1;
-    generated = generated.concat(
-      // eslint-disable-next-line no-loop-func
-      generated.map(({ label, value }) => ({
-        label: `${label} ${iteration}`,
-        value: `${value} ${iteration}`,
-      })),
-    );
-  }
-  return generated.slice(0, count);
-};
-
-export const InteractiveSelect = ({
-  header,
-  options,
-  optionsCount,
-  ...args
-}: SelectProps & { header: string; optionsCount: number }) => (
-  <div
-    style={{
-      width: DEFAULT_WIDTH,
-    }}
-  >
-    <Select
-      {...args}
-      options={
-        Array.isArray(options)
-          ? generateOptions(options, optionsCount)
-          : options
-      }
-      header={mountHeader(header)}
-    />
-  </div>
-);
-
-InteractiveSelect.args = {
-  autoFocus: true,
-  allowNewOptions: false,
-  allowClear: false,
-  showSearch: true,
-  disabled: false,
-  invertSelection: false,
-  placeholder: 'Select ...',
-  optionFilterProps: ['value', 'label', 'custom'],
-};
-
-InteractiveSelect.argTypes = {
-  ...ARG_TYPES,
-  optionsCount: {
-    defaultValue: options.length,
-    control: {
-      type: 'number',
-    },
-  },
-  header: {
-    defaultValue: 'none',
-    description: `It adds a header on top of the Select. Can be any ReactNode.`,
-    control: { type: 'inline-radio', options: ['none', 'text', 'control'] },
-  },
-  pageSize: {
-    description: `It defines how many results should be included in the query response.
-      Works in async mode only (See the options property).
-    `,
-  },
-  fetchOnlyOnSearch: {
-    description: `It fires a request against the server only after searching.
-      Works in async mode only (See the options property).
-      Undefined by default.
-    `,
-  },
-};
-
-InteractiveSelect.story = {
-  parameters: {
-    knobs: {
-      disable: true,
-    },
-  },
-};
-
-export const AtEveryCorner = () => (
-  <>
-    {selectPositions.map(position => (
-      <div
-        key={position.id}
-        style={{
-          ...position.style,
-          margin: 30,
-          width: DEFAULT_WIDTH,
-          position: 'absolute',
-        }}
-      >
-        <Select
-          ariaLabel={`gallery-${position.id}`}
-          options={options}
-          labelInValue
-        />
-      </div>
-    ))}
-    <p style={{ position: 'absolute', top: '40%', left: '33%', width: 500 }}>
-      The objective of this panel is to show how the Select behaves when in
-      touch with the viewport extremities. In particular, how the drop-down is
-      displayed and if the tooltips of truncated items are correctly positioned.
-    </p>
-  </>
-);
-
-AtEveryCorner.story = {
-  parameters: {
-    actions: {
-      disable: true,
-    },
-    controls: {
-      disable: true,
-    },
-    knobs: {
-      disable: true,
-    },
-  },
-};
-
-export const PageScroll = () => (
-  <div style={{ height: 2000, overflowY: 'auto' }}>
-    <div
-      style={{
-        width: DEFAULT_WIDTH,
-        position: 'absolute',
-        top: 30,
-        right: 30,
-      }}
-    >
-      <Select ariaLabel="page-scroll-select-1" options={options} labelInValue />
-    </div>
-    <div
-      style={{
-        width: DEFAULT_WIDTH,
-        position: 'absolute',
-        bottom: 30,
-        right: 30,
-      }}
-    >
-      <Select ariaLabel="page-scroll-select-2" options={options} />
-    </div>
-    <p
-      style={{
-        position: 'absolute',
-        top: '40%',
-        left: 30,
-        width: 500,
-      }}
-    >
-      The objective of this panel is to show how the Select behaves when there's
-      a scroll on the page. In particular, how the drop-down is displayed.
-    </p>
-  </div>
-);
-
-PageScroll.story = {
-  parameters: {
-    actions: {
-      disable: true,
-    },
-    controls: {
-      disable: true,
-    },
-    knobs: {
-      disable: true,
-    },
-  },
-};
-
 const USERS = [
   'John',
   'Liam',
diff --git a/superset-frontend/src/components/Select/AsyncSelect.tsx b/superset-frontend/src/components/Select/AsyncSelect.tsx
index 27c62023ce..524ca53e61 100644
--- a/superset-frontend/src/components/Select/AsyncSelect.tsx
+++ b/superset-frontend/src/components/Select/AsyncSelect.tsx
@@ -28,7 +28,7 @@ import React, {
   useCallback,
   useImperativeHandle,
 } from 'react';
-import { ensureIsArray, styled, t } from '@superset-ui/core';
+import { ensureIsArray, t } from '@superset-ui/core';
 import { LabeledValue as AntdLabeledValue } from 'antd/lib/select';
 import debounce from 'lodash/debounce';
 import { isEqual } from 'lodash';
@@ -39,20 +39,8 @@ 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,
@@ -60,64 +48,28 @@ import {
   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 BaseSelectProps {
-  /**
-   * 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 the options of the Select.
-   * The options are async, a promise that returns
-   * an array of options.
-   */
-  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 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;
-}
+import {
+  AsyncSelectProps,
+  AsyncSelectRef,
+  SelectOptionsPagePromise,
+  SelectOptionsType,
+  SelectOptionsTypePage,
+} from './types';
+import {
+  StyledCheckOutlined,
+  StyledContainer,
+  StyledError,
+  StyledErrorMessage,
+  StyledSelect,
+  StyledStopOutlined,
+} from './styles';
+import {
+  DEFAULT_PAGE_SIZE,
+  EMPTY_OPTIONS,
+  MAX_TAG_COUNT,
+  TOKEN_SEPARATORS,
+  DEFAULT_SORT_COMPARATOR,
+} from './constants';
 
 const Error = ({ error }: { error: string }) => (
   <StyledError>
diff --git a/superset-frontend/src/components/Select/Select.stories.tsx b/superset-frontend/src/components/Select/Select.stories.tsx
index e9a03fe563..c4802a45e0 100644
--- a/superset-frontend/src/components/Select/Select.stories.tsx
+++ b/superset-frontend/src/components/Select/Select.stories.tsx
@@ -16,19 +16,11 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import React, {
-  ReactNode,
-  useState,
-  useCallback,
-  useRef,
-  useMemo,
-} from 'react';
-import Button from 'src/components/Button';
+import React from 'react';
 import ControlHeader from 'src/explore/components/ControlHeader';
-import AsyncSelect, { AsyncSelectProps, AsyncSelectRef } from './AsyncSelect';
-import { SelectOptionsType, SelectOptionsTypePage } from './utils';
+import { SelectOptionsType, SelectProps } from './types';
 
-import Select, { SelectProps } from './Select';
+import Select from './Select';
 
 export default {
   title: 'Select',
@@ -331,236 +323,3 @@ PageScroll.story = {
     },
   },
 };
-
-const USERS = [
-  'John',
-  'Liam',
-  'Olivia',
-  'Emma',
-  'Noah',
-  'Ava',
-  'Oliver',
-  'Elijah',
-  'Charlotte',
-  'Diego',
-  'Evan',
-  'Michael',
-  'Giovanni',
-  'Luca',
-  'Paolo',
-  'Francesca',
-  'Chiara',
-  'Sara',
-  'Valentina',
-  'Jessica',
-  'Angelica',
-  'Mario',
-  'Marco',
-  'Andrea',
-  'Luigi',
-  'Quarto',
-  'Quinto',
-  'Sesto',
-  'Franco',
-  'Sandro',
-  'Alehandro',
-  'Johnny',
-  'Nikole',
-  'Igor',
-  'Sipatha',
-  'Thami',
-  'Munei',
-  'Guilherme',
-  'Umair',
-  'Ashfaq',
-  'Amna',
-  'Irfan',
-  'George',
-  'Naseer',
-  'Mohammad',
-  'Rick',
-  'Saliya',
-  'Claire',
-  'Benedetta',
-  'Ilenia',
-].sort();
-
-export const AsynchronousSelect = ({
-  fetchOnlyOnSearch,
-  withError,
-  withInitialValue,
-  responseTime,
-  ...rest
-}: AsyncSelectProps & {
-  withError: boolean;
-  withInitialValue: boolean;
-  responseTime: number;
-}) => {
-  const [requests, setRequests] = useState<ReactNode[]>([]);
-  const ref = useRef<AsyncSelectRef>(null);
-
-  const getResults = (username?: string) => {
-    let results: { label: string; value: string }[] = [];
-
-    if (!username) {
-      results = USERS.map(u => ({
-        label: u,
-        value: u,
-      }));
-    } else {
-      const foundUsers = USERS.filter(u => u.toLowerCase().includes(username));
-      if (foundUsers) {
-        results = foundUsers.map(u => ({ label: u, value: u }));
-      } else {
-        results = [];
-      }
-    }
-    return results;
-  };
-
-  const setRequestLog = (results: number, total: number, username?: string) => {
-    const request = (
-      <>
-        Emulating network request with search <b>{username || 'empty'}</b> ...{' '}
-        <b>
-          {results}/{total}
-        </b>{' '}
-        results
-      </>
-    );
-
-    setRequests(requests => [request, ...requests]);
-  };
-
-  const fetchUserListPage = useCallback(
-    (
-      search: string,
-      page: number,
-      pageSize: number,
-    ): Promise<SelectOptionsTypePage> => {
-      const username = search.trim().toLowerCase();
-      return new Promise(resolve => {
-        let results = getResults(username);
-        const totalCount = results.length;
-        const start = page * pageSize;
-        const deleteCount =
-          start + pageSize < totalCount ? pageSize : totalCount - start;
-        results = results.splice(start, deleteCount);
-        setRequestLog(start + results.length, totalCount, username);
-        setTimeout(() => {
-          resolve({ data: results, totalCount });
-        }, responseTime * 1000);
-      });
-    },
-    [responseTime],
-  );
-
-  const fetchUserListError = async (): Promise<SelectOptionsTypePage> =>
-    new Promise((_, reject) => {
-      reject(new Error('Error while fetching the names from the server'));
-    });
-
-  const initialValue = useMemo(
-    () => ({ label: 'Valentina', value: 'Valentina' }),
-    [],
-  );
-
-  return (
-    <>
-      <div
-        style={{
-          width: DEFAULT_WIDTH,
-        }}
-      >
-        <AsyncSelect
-          {...rest}
-          ref={ref}
-          fetchOnlyOnSearch={fetchOnlyOnSearch}
-          options={withError ? fetchUserListError : fetchUserListPage}
-          placeholder={fetchOnlyOnSearch ? 'Type anything' : 'AsyncSelect...'}
-          value={withInitialValue ? initialValue : undefined}
-        />
-      </div>
-      <div
-        style={{
-          position: 'absolute',
-          top: 32,
-          left: DEFAULT_WIDTH + 100,
-          height: 400,
-          width: 600,
-          overflowY: 'auto',
-          border: '1px solid #d9d9d9',
-          padding: 20,
-        }}
-      >
-        {requests.map((request, index) => (
-          <p key={`request-${index}`}>{request}</p>
-        ))}
-      </div>
-      <Button
-        style={{
-          position: 'absolute',
-          top: 452,
-          left: DEFAULT_WIDTH + 580,
-        }}
-        onClick={() => {
-          ref.current?.clearCache();
-          setRequests([]);
-        }}
-      >
-        Clear cache
-      </Button>
-    </>
-  );
-};
-
-AsynchronousSelect.args = {
-  allowClear: false,
-  allowNewOptions: false,
-  fetchOnlyOnSearch: false,
-  pageSize: 10,
-  withError: false,
-  withInitialValue: false,
-  tokenSeparators: ['\n', '\t', ';'],
-};
-
-AsynchronousSelect.argTypes = {
-  ...ARG_TYPES,
-  header: {
-    table: {
-      disable: true,
-    },
-  },
-  invertSelection: {
-    table: {
-      disable: true,
-    },
-  },
-  pageSize: {
-    defaultValue: 10,
-    control: {
-      type: 'range',
-      min: 10,
-      max: 50,
-      step: 10,
-    },
-  },
-  responseTime: {
-    defaultValue: 0.5,
-    name: 'responseTime (seconds)',
-    control: {
-      type: 'range',
-      min: 0.5,
-      max: 5,
-      step: 0.5,
-    },
-  },
-};
-
-AsynchronousSelect.story = {
-  parameters: {
-    knobs: {
-      disable: true,
-    },
-  },
-};
diff --git a/superset-frontend/src/components/Select/Select.tsx b/superset-frontend/src/components/Select/Select.tsx
index 6b6713852d..1ad1e6b0a2 100644
--- a/superset-frontend/src/components/Select/Select.tsx
+++ b/superset-frontend/src/components/Select/Select.tsx
@@ -32,34 +32,27 @@ 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: SelectOptionsType;
-}
+import { SelectOptionsType, SelectProps } from './types';
+import {
+  StyledCheckOutlined,
+  StyledContainer,
+  StyledSelect,
+  StyledStopOutlined,
+} from './styles';
+import {
+  EMPTY_OPTIONS,
+  MAX_TAG_COUNT,
+  TOKEN_SEPARATORS,
+  DEFAULT_SORT_COMPARATOR,
+} from './constants';
 
 /**
  * This component is a customized version of the Antdesign 4.X Select component
diff --git a/superset-frontend/src/components/Select/constants.ts b/superset-frontend/src/components/Select/constants.ts
new file mode 100644
index 0000000000..b8c60e8523
--- /dev/null
+++ b/superset-frontend/src/components/Select/constants.ts
@@ -0,0 +1,52 @@
+/**
+ * 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 { LabeledValue as AntdLabeledValue } from 'antd/lib/select';
+import { rankedSearchCompare } from 'src/utils/rankedSearchCompare';
+
+export const MAX_TAG_COUNT = 4;
+
+export const TOKEN_SEPARATORS = [',', '\n', '\t', ';'];
+
+export const EMPTY_OPTIONS = [];
+
+export const DEFAULT_PAGE_SIZE = 100;
+
+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);
+};
diff --git a/superset-frontend/src/components/Select/styles.tsx b/superset-frontend/src/components/Select/styles.tsx
new file mode 100644
index 0000000000..85dbefe88f
--- /dev/null
+++ b/superset-frontend/src/components/Select/styles.tsx
@@ -0,0 +1,90 @@
+/**
+ * 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 { styled } from '@superset-ui/core';
+import Icons from 'src/components/Icons';
+import { Spin } from 'antd';
+import AntdSelect from 'antd/lib/select';
+
+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};
+ `}
+`;
+
+export 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 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;
+    }
+  `}
+`;
+
+export const StyledErrorMessage = styled.div`
+  overflow: hidden;
+  text-overflow: ellipsis;
+`;
diff --git a/superset-frontend/src/components/Select/types.ts b/superset-frontend/src/components/Select/types.ts
new file mode 100644
index 0000000000..e2a7d5d1f3
--- /dev/null
+++ b/superset-frontend/src/components/Select/types.ts
@@ -0,0 +1,201 @@
+/**
+ * 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 {
+  JSXElementConstructor,
+  ReactElement,
+  ReactNode,
+  RefObject,
+} from 'react';
+import {
+  SelectProps as AntdSelectProps,
+  SelectValue as AntdSelectValue,
+  LabeledValue as AntdLabeledValue,
+} from 'antd/lib/select';
+
+export type RawValue = string | number;
+
+export type V = string | number | null | undefined;
+
+export type LabeledValue = { label?: ReactNode; value?: V };
+
+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 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?: (
+    a: AntdLabeledValue,
+    b: AntdLabeledValue,
+    search?: string,
+  ) => number;
+
+  suffixIcon?: ReactNode;
+
+  ref: RefObject<HTMLInputElement>;
+}
+
+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: SelectOptionsType;
+}
+
+export type AsyncSelectRef = HTMLInputElement & { clearCache: () => void };
+
+export type SelectOptionsTypePage = {
+  data: SelectOptionsType;
+  totalCount: number;
+};
+
+export type SelectOptionsPagePromise = (
+  search: string,
+  page: number,
+  pageSize: number,
+) => Promise<SelectOptionsTypePage>;
+
+export interface AsyncSelectProps extends BaseSelectProps {
+  /**
+   * 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 the options of the Select.
+   * The options are async, a promise that returns
+   * an array of options.
+   */
+  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 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;
+}
diff --git a/superset-frontend/src/components/Select/utils.tsx b/superset-frontend/src/components/Select/utils.tsx
index 27a97ac8bd..5ec7e33d10 100644
--- a/superset-frontend/src/components/Select/utils.tsx
+++ b/superset-frontend/src/components/Select/utils.tsx
@@ -16,30 +16,12 @@
  * 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 { ensureIsArray, t } from '@superset-ui/core';
+import AntdSelect, { LabeledValue as AntdLabeledValue } from 'antd/lib/select';
+import React, { ReactElement, RefObject } from 'react';
 import { DownOutlined, SearchOutlined } from '@ant-design/icons';
-
-declare type RawValue = string | number;
+import { StyledHelperText, StyledLoadingText, StyledSpin } from './styles';
+import { LabeledValue, RawValue, SelectOptionsType, V } from './types';
 
 const { Option } = AntdSelect;
 
@@ -51,41 +33,6 @@ export function isObject(value: unknown): value is Record<string, unknown> {
   );
 }
 
-/**
- * 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;
 }
@@ -96,10 +43,6 @@ export function getValue(
   return isLabeledValue(option) ? option.value : option;
 }
 
-type V = string | number | null | undefined;
-
-type LabeledValue = { label?: ReactNode; value?: V };
-
 export function hasOption(
   value: V,
   options?: V | LabeledValue | (V | LabeledValue)[],
@@ -121,127 +64,6 @@ export function hasOption(
   );
 }
 
-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.
@@ -364,77 +186,6 @@ export const handleFilterOptionHelper = (
 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';
diff --git a/superset-frontend/src/explore/components/controls/SelectAsyncControl/index.tsx b/superset-frontend/src/explore/components/controls/SelectAsyncControl/index.tsx
index 74364fcf98..ddc242a764 100644
--- a/superset-frontend/src/explore/components/controls/SelectAsyncControl/index.tsx
+++ b/superset-frontend/src/explore/components/controls/SelectAsyncControl/index.tsx
@@ -20,8 +20,7 @@ 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 } from 'src/components/Select/Select';
-import { SelectOptionsType } from 'src/components/Select/utils';
+import { SelectOptionsType, SelectProps } from 'src/components/Select/types';
 import { SelectValue, LabeledValue } from 'antd/lib/select';
 import withToasts from 'src/components/MessageToasts/withToasts';
 import { getClientErrorObject } from 'src/utils/getClientErrorObject';