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 2021/08/23 18:41:56 UTC

[superset] branch master updated: chore: Changes the DatabaseSelector to use the new Select component (#16334)

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 c768941  chore: Changes the DatabaseSelector to use the new Select component (#16334)
c768941 is described below

commit c768941f2f662e1a0dfa1e1731319d22ec9ca886
Author: Michael S. Molina <70...@users.noreply.github.com>
AuthorDate: Mon Aug 23 15:41:03 2021 -0300

    chore: Changes the DatabaseSelector to use the new Select component (#16334)
---
 .../javascripts/sqllab/SqlEditorLeftBar_spec.jsx   |  10 +-
 .../src/components/CertifiedIcon/index.tsx         |   9 +-
 .../DatabaseSelector/DatabaseSelector.test.tsx     |  62 +--
 .../src/components/DatabaseSelector/index.tsx      | 310 +++++++------
 superset-frontend/src/components/Icons/Icon.tsx    |   8 +-
 superset-frontend/src/components/Select/Select.tsx |  76 ++--
 .../TableSelector/TableSelector.test.jsx           | 291 -------------
 .../TableSelector/TableSelector.test.tsx           |  91 ++++
 .../src/components/TableSelector/index.tsx         | 484 +++++++++++----------
 .../components/WarningIconWithTooltip/index.tsx    |   6 +-
 .../src/datasource/DatasourceEditor.jsx            | 132 +++---
 .../controls/DatasourceControl/index.jsx           |   5 +-
 .../src/views/CRUD/data/dataset/DatasetList.tsx    |   2 +
 superset/datasets/api.py                           |   2 +-
 superset/views/core.py                             |  21 +-
 tests/integration_tests/datasets/api_tests.py      |  14 +-
 16 files changed, 692 insertions(+), 831 deletions(-)

diff --git a/superset-frontend/spec/javascripts/sqllab/SqlEditorLeftBar_spec.jsx b/superset-frontend/spec/javascripts/sqllab/SqlEditorLeftBar_spec.jsx
index 1ba1ac8..b153c14 100644
--- a/superset-frontend/spec/javascripts/sqllab/SqlEditorLeftBar_spec.jsx
+++ b/superset-frontend/spec/javascripts/sqllab/SqlEditorLeftBar_spec.jsx
@@ -81,9 +81,13 @@ describe('Left Panel Expansion', () => {
         </Provider>
       </ThemeProvider>,
     );
-    const dbSelect = screen.getByText(/select a database/i);
-    const schemaSelect = screen.getByText(/select a schema \(0\)/i);
-    const dropdown = screen.getByText(/Select table/i);
+    const dbSelect = screen.getByRole('combobox', {
+      name: 'Select a database',
+    });
+    const schemaSelect = screen.getByRole('combobox', {
+      name: 'Select a schema',
+    });
+    const dropdown = screen.getByText(/Select a table/i);
     const abUser = screen.getByText(/ab_user/i);
     expect(dbSelect).toBeInTheDocument();
     expect(schemaSelect).toBeInTheDocument();
diff --git a/superset-frontend/src/components/CertifiedIcon/index.tsx b/superset-frontend/src/components/CertifiedIcon/index.tsx
index f08e9bf..4aa0dad 100644
--- a/superset-frontend/src/components/CertifiedIcon/index.tsx
+++ b/superset-frontend/src/components/CertifiedIcon/index.tsx
@@ -18,19 +18,19 @@
  */
 import React from 'react';
 import { t, supersetTheme } from '@superset-ui/core';
-import Icons from 'src/components/Icons';
+import Icons, { IconType } from 'src/components/Icons';
 import { Tooltip } from 'src/components/Tooltip';
 
 export interface CertifiedIconProps {
   certifiedBy?: string;
   details?: string;
-  size?: number;
+  size?: IconType['iconSize'];
 }
 
 function CertifiedIcon({
   certifiedBy,
   details,
-  size = 24,
+  size = 'l',
 }: CertifiedIconProps) {
   return (
     <Tooltip
@@ -48,8 +48,7 @@ function CertifiedIcon({
     >
       <Icons.Certified
         iconColor={supersetTheme.colors.primary.base}
-        height={size}
-        width={size}
+        iconSize={size}
       />
     </Tooltip>
   );
diff --git a/superset-frontend/src/components/DatabaseSelector/DatabaseSelector.test.tsx b/superset-frontend/src/components/DatabaseSelector/DatabaseSelector.test.tsx
index 0d81282..6d4abb3 100644
--- a/superset-frontend/src/components/DatabaseSelector/DatabaseSelector.test.tsx
+++ b/superset-frontend/src/components/DatabaseSelector/DatabaseSelector.test.tsx
@@ -26,11 +26,11 @@ import DatabaseSelector from '.';
 const SupersetClientGet = jest.spyOn(SupersetClient, 'get');
 
 const createProps = () => ({
-  dbId: 1,
+  db: { id: 1, database_name: 'test', backend: 'postgresql' },
   formMode: false,
   isDatabaseSelectEnabled: true,
   readOnly: false,
-  schema: 'public',
+  schema: undefined,
   sqlLabMode: true,
   getDbList: jest.fn(),
   getTableList: jest.fn(),
@@ -129,7 +129,7 @@ beforeEach(() => {
               changed_on: '2021-03-09T19:02:07.141095',
               changed_on_delta_humanized: 'a day ago',
               created_by: null,
-              database_name: 'examples',
+              database_name: 'test',
               explore_database_id: 1,
               expose_in_sqllab: true,
               force_ctas_schema: null,
@@ -153,50 +153,62 @@ test('Refresh should work', async () => {
 
   render(<DatabaseSelector {...props} />);
 
+  const select = screen.getByRole('combobox', {
+    name: 'Select a schema',
+  });
+
+  userEvent.click(select);
+
   await waitFor(() => {
-    expect(SupersetClientGet).toBeCalledTimes(2);
-    expect(props.getDbList).toBeCalledTimes(1);
+    expect(SupersetClientGet).toBeCalledTimes(1);
+    expect(props.getDbList).toBeCalledTimes(0);
     expect(props.getTableList).toBeCalledTimes(0);
     expect(props.handleError).toBeCalledTimes(0);
     expect(props.onDbChange).toBeCalledTimes(0);
     expect(props.onSchemaChange).toBeCalledTimes(0);
-    expect(props.onSchemasLoad).toBeCalledTimes(1);
+    expect(props.onSchemasLoad).toBeCalledTimes(0);
     expect(props.onUpdate).toBeCalledTimes(0);
   });
 
-  userEvent.click(screen.getByRole('button'));
+  userEvent.click(screen.getByRole('button', { name: 'refresh' }));
 
   await waitFor(() => {
-    expect(SupersetClientGet).toBeCalledTimes(3);
-    expect(props.getDbList).toBeCalledTimes(1);
+    expect(SupersetClientGet).toBeCalledTimes(2);
+    expect(props.getDbList).toBeCalledTimes(0);
     expect(props.getTableList).toBeCalledTimes(0);
     expect(props.handleError).toBeCalledTimes(0);
-    expect(props.onDbChange).toBeCalledTimes(1);
-    expect(props.onSchemaChange).toBeCalledTimes(1);
+    expect(props.onDbChange).toBeCalledTimes(0);
+    expect(props.onSchemaChange).toBeCalledTimes(0);
     expect(props.onSchemasLoad).toBeCalledTimes(2);
-    expect(props.onUpdate).toBeCalledTimes(1);
+    expect(props.onUpdate).toBeCalledTimes(0);
   });
 });
 
 test('Should database select display options', async () => {
   const props = createProps();
   render(<DatabaseSelector {...props} />);
-  const selector = await screen.findByText('Database:');
-  expect(selector).toBeInTheDocument();
-  expect(selector.parentElement).toHaveTextContent(
-    'Database:postgresql examples',
-  );
+  const select = screen.getByRole('combobox', {
+    name: 'Select a database',
+  });
+  expect(select).toBeInTheDocument();
+  userEvent.click(select);
+  expect(
+    await screen.findByRole('option', { name: 'postgresql: test' }),
+  ).toBeInTheDocument();
 });
 
 test('Should schema select display options', async () => {
   const props = createProps();
   render(<DatabaseSelector {...props} />);
-
-  const selector = await screen.findByText('Schema:');
-  expect(selector).toBeInTheDocument();
-  expect(selector.parentElement).toHaveTextContent('Schema: public');
-
-  userEvent.click(screen.getByRole('button'));
-
-  expect(await screen.findByText('Select a schema (2)')).toBeInTheDocument();
+  const select = screen.getByRole('combobox', {
+    name: 'Select a schema',
+  });
+  expect(select).toBeInTheDocument();
+  userEvent.click(select);
+  expect(
+    await screen.findByRole('option', { name: 'public' }),
+  ).toBeInTheDocument();
+  expect(
+    await screen.findByRole('option', { name: 'information_schema' }),
+  ).toBeInTheDocument();
 });
diff --git a/superset-frontend/src/components/DatabaseSelector/index.tsx b/superset-frontend/src/components/DatabaseSelector/index.tsx
index 0282e4a..c96fba7 100644
--- a/superset-frontend/src/components/DatabaseSelector/index.tsx
+++ b/superset-frontend/src/components/DatabaseSelector/index.tsx
@@ -16,58 +16,51 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import React, { ReactNode, useEffect, useState } from 'react';
+import React, { ReactNode, useState, useMemo } from 'react';
 import { styled, SupersetClient, t } from '@superset-ui/core';
 import rison from 'rison';
-import { Select } from 'src/components/Select';
-import Label from 'src/components/Label';
+import { Select } from 'src/components';
+import { FormLabel } from 'src/components/Form';
 import RefreshLabel from 'src/components/RefreshLabel';
-import SupersetAsyncSelect from 'src/components/AsyncSelect';
-
-const FieldTitle = styled.p`
-  color: ${({ theme }) => theme.colors.secondary.light2};
-  font-size: ${({ theme }) => theme.typography.sizes.s}px;
-  margin: 20px 0 10px 0;
-  text-transform: uppercase;
-`;
 
 const DatabaseSelectorWrapper = styled.div`
-  .fa-refresh {
-    padding-left: 9px;
-  }
+  ${({ theme }) => `
+    .refresh {
+      display: flex;
+      align-items: center;
+      width: 30px;
+      margin-left: ${theme.gridUnit}px;
+      margin-top: ${theme.gridUnit * 5}px;
+    }
 
-  .refresh-col {
-    display: flex;
-    align-items: center;
-    width: 30px;
-    margin-left: ${({ theme }) => theme.gridUnit}px;
-  }
+    .section {
+      display: flex;
+      flex-direction: row;
+      align-items: center;
+    }
 
-  .section {
-    padding-bottom: 5px;
-    display: flex;
-    flex-direction: row;
-  }
+    .select {
+      flex: 1;
+    }
 
-  .select {
-    flex-grow: 1;
-  }
+    & > div {
+      margin-bottom: ${theme.gridUnit * 4}px;
+    }
+  `}
 `;
 
-const DatabaseOption = styled.span`
-  display: inline-flex;
-  align-items: center;
-`;
+type DatabaseValue = { label: string; value: number };
+
+type SchemaValue = { label: string; value: string };
 
 interface DatabaseSelectorProps {
-  dbId: number;
+  db?: { id: number; database_name: string; backend: string };
   formMode?: boolean;
   getDbList?: (arg0: any) => {};
-  getTableList?: (dbId: number, schema: string, force: boolean) => {};
   handleError: (msg: string) => void;
   isDatabaseSelectEnabled?: boolean;
   onDbChange?: (db: any) => void;
-  onSchemaChange?: (arg0?: any) => {};
+  onSchemaChange?: (schema?: string) => void;
   onSchemasLoad?: (schemas: Array<object>) => void;
   readOnly?: boolean;
   schema?: string;
@@ -83,10 +76,9 @@ interface DatabaseSelectorProps {
 }
 
 export default function DatabaseSelector({
-  dbId,
+  db,
   formMode = false,
   getDbList,
-  getTableList,
   handleError,
   isDatabaseSelectEnabled = true,
   onUpdate,
@@ -97,193 +89,189 @@ export default function DatabaseSelector({
   schema,
   sqlLabMode = false,
 }: DatabaseSelectorProps) {
-  const [currentDbId, setCurrentDbId] = useState(dbId);
-  const [currentSchema, setCurrentSchema] = useState<string | undefined>(
-    schema,
+  const [currentDb, setCurrentDb] = useState(
+    db
+      ? { label: `${db.backend}: ${db.database_name}`, value: db.id }
+      : undefined,
+  );
+  const [currentSchema, setCurrentSchema] = useState<SchemaValue | undefined>(
+    schema ? { label: schema, value: schema } : undefined,
   );
-  const [schemaLoading, setSchemaLoading] = useState(false);
-  const [schemaOptions, setSchemaOptions] = useState([]);
+  const [refresh, setRefresh] = useState(0);
 
-  function fetchSchemas(databaseId: number, forceRefresh = false) {
-    const actualDbId = databaseId || dbId;
-    if (actualDbId) {
-      setSchemaLoading(true);
-      const queryParams = rison.encode({
-        force: Boolean(forceRefresh),
-      });
-      const endpoint = `/api/v1/database/${actualDbId}/schemas/?q=${queryParams}`;
-      return SupersetClient.get({ endpoint })
-        .then(({ json }) => {
+  const loadSchemas = useMemo(
+    () => async (): Promise<{
+      data: SchemaValue[];
+      totalCount: number;
+    }> => {
+      if (currentDb) {
+        const queryParams = rison.encode({ force: refresh > 0 });
+        const endpoint = `/api/v1/database/${currentDb.value}/schemas/?q=${queryParams}`;
+
+        // TODO: Would be nice to add pagination in a follow-up. Needs endpoint changes.
+        return SupersetClient.get({ endpoint }).then(({ json }) => {
           const options = json.result.map((s: string) => ({
             value: s,
             label: s,
             title: s,
           }));
-          setSchemaOptions(options);
-          setSchemaLoading(false);
           if (onSchemasLoad) {
             onSchemasLoad(options);
           }
-        })
-        .catch(() => {
-          setSchemaOptions([]);
-          setSchemaLoading(false);
-          handleError(t('Error while fetching schema list'));
+          return {
+            data: options,
+            totalCount: options.length,
+          };
         });
-    }
-    return Promise.resolve();
-  }
-
-  useEffect(() => {
-    if (currentDbId) {
-      fetchSchemas(currentDbId);
-    }
-  }, [currentDbId]);
+      }
+      return {
+        data: [],
+        totalCount: 0,
+      };
+    },
+    [currentDb, refresh, onSchemasLoad],
+  );
 
-  function onSelectChange({ dbId, schema }: { dbId: number; schema?: string }) {
-    setCurrentDbId(dbId);
+  function onSelectChange({
+    db,
+    schema,
+  }: {
+    db: DatabaseValue;
+    schema?: SchemaValue;
+  }) {
+    setCurrentDb(db);
     setCurrentSchema(schema);
     if (onUpdate) {
-      onUpdate({ dbId, schema, tableName: undefined });
-    }
-  }
-
-  function dbMutator(data: any) {
-    if (getDbList) {
-      getDbList(data.result);
-    }
-    if (data.result.length === 0) {
-      handleError(t("It seems you don't have access to any database"));
+      onUpdate({
+        dbId: db.value,
+        schema: schema?.value,
+        tableName: undefined,
+      });
     }
-    return data.result.map((row: any) => ({
-      ...row,
-      // label is used for the typeahead
-      label: `${row.backend} ${row.database_name}`,
-    }));
   }
 
-  function changeDataBase(db: any, force = false) {
-    const dbId = db ? db.id : null;
-    setSchemaOptions([]);
+  function changeDataBase(selectedValue: DatabaseValue) {
+    const actualDb = selectedValue || db;
     if (onSchemaChange) {
-      onSchemaChange(null);
+      onSchemaChange(undefined);
     }
     if (onDbChange) {
       onDbChange(db);
     }
-    fetchSchemas(dbId, force);
-    onSelectChange({ dbId, schema: undefined });
+    onSelectChange({ db: actualDb, schema: undefined });
   }
 
-  function changeSchema(schemaOpt: any, force = false) {
-    const schema = schemaOpt ? schemaOpt.value : null;
+  function changeSchema(schema: SchemaValue) {
     if (onSchemaChange) {
-      onSchemaChange(schema);
+      onSchemaChange(schema.value);
     }
-    setCurrentSchema(schema);
-    onSelectChange({ dbId: currentDbId, schema });
-    if (getTableList) {
-      getTableList(currentDbId, schema, force);
+    if (currentDb) {
+      onSelectChange({ db: currentDb, schema });
     }
   }
 
-  function renderDatabaseOption(db: any) {
-    return (
-      <DatabaseOption title={db.database_name}>
-        <Label type="default">{db.backend}</Label> {db.database_name}
-      </DatabaseOption>
-    );
-  }
-
   function renderSelectRow(select: ReactNode, refreshBtn: ReactNode) {
     return (
       <div className="section">
         <span className="select">{select}</span>
-        <span className="refresh-col">{refreshBtn}</span>
+        <span className="refresh">{refreshBtn}</span>
       </div>
     );
   }
 
-  function renderDatabaseSelect() {
-    const queryParams = rison.encode({
-      order_columns: 'database_name',
-      order_direction: 'asc',
-      page: 0,
-      page_size: -1,
-      ...(formMode || !sqlLabMode
-        ? {}
-        : {
-            filters: [
-              {
-                col: 'expose_in_sqllab',
-                opr: 'eq',
-                value: true,
-              },
-            ],
+  const loadDatabases = useMemo(
+    () => async (
+      search: string,
+      page: number,
+      pageSize: number,
+    ): Promise<{
+      data: DatabaseValue[];
+      totalCount: number;
+    }> => {
+      const queryParams = rison.encode({
+        order_columns: 'database_name',
+        order_direction: 'asc',
+        page,
+        page_size: pageSize,
+        ...(formMode || !sqlLabMode
+          ? { filters: [{ col: 'database_name', opr: 'ct', value: search }] }
+          : {
+              filters: [
+                { col: 'database_name', opr: 'ct', value: search },
+                {
+                  col: 'expose_in_sqllab',
+                  opr: 'eq',
+                  value: true,
+                },
+              ],
+            }),
+      });
+      const endpoint = `/api/v1/database/?q=${queryParams}`;
+      return SupersetClient.get({ endpoint }).then(({ json }) => {
+        const { result } = json;
+        if (getDbList) {
+          getDbList(result);
+        }
+        if (result.length === 0) {
+          handleError(t("It seems you don't have access to any database"));
+        }
+        const options = result.map(
+          (row: { backend: string; database_name: string; id: number }) => ({
+            label: `${row.backend}: ${row.database_name}`,
+            value: row.id,
           }),
-    });
+        );
+        return {
+          data: options,
+          totalCount: options.length,
+        };
+      });
+    },
+    [formMode, getDbList, handleError, sqlLabMode],
+  );
 
+  function renderDatabaseSelect() {
     return renderSelectRow(
-      <SupersetAsyncSelect
+      <Select
+        ariaLabel={t('Select a database')}
         data-test="select-database"
-        dataEndpoint={`/api/v1/database/?q=${queryParams}`}
-        onChange={(db: any) => changeDataBase(db)}
-        onAsyncError={() =>
-          handleError(t('Error while fetching database list'))
-        }
-        clearable={false}
-        value={currentDbId}
-        valueKey="id"
-        valueRenderer={(db: any) => (
-          <div>
-            <span className="text-muted m-r-5">{t('Database:')}</span>
-            {renderDatabaseOption(db)}
-          </div>
-        )}
-        optionRenderer={renderDatabaseOption}
-        mutator={dbMutator}
+        header={<FormLabel>{t('Database')}</FormLabel>}
+        onChange={changeDataBase}
+        value={currentDb}
         placeholder={t('Select a database')}
-        autoSelect
-        isDisabled={!isDatabaseSelectEnabled || readOnly}
+        disabled={!isDatabaseSelectEnabled || readOnly}
+        options={loadDatabases}
       />,
       null,
     );
   }
 
   function renderSchemaSelect() {
-    const value = schemaOptions.filter(({ value }) => currentSchema === value);
-    const refresh = !formMode && !readOnly && (
+    const refreshIcon = !formMode && !readOnly && (
       <RefreshLabel
-        onClick={() => changeDataBase({ id: dbId }, true)}
+        onClick={() => setRefresh(refresh + 1)}
         tooltipContent={t('Force refresh schema list')}
       />
     );
 
     return renderSelectRow(
       <Select
+        ariaLabel={t('Select a schema')}
+        disabled={readOnly}
+        header={<FormLabel>{t('Schema')}</FormLabel>}
         name="select-schema"
-        placeholder={t('Select a schema (%s)', schemaOptions.length)}
-        options={schemaOptions}
-        value={value}
-        valueRenderer={o => (
-          <div>
-            <span className="text-muted">{t('Schema:')}</span> {o.label}
-          </div>
-        )}
-        isLoading={schemaLoading}
-        autosize={false}
-        onChange={item => changeSchema(item)}
-        isDisabled={readOnly}
+        placeholder={t('Select a schema')}
+        onChange={item => changeSchema(item as SchemaValue)}
+        options={loadSchemas}
+        value={currentSchema}
       />,
-      refresh,
+      refreshIcon,
     );
   }
 
   return (
     <DatabaseSelectorWrapper data-test="DatabaseSelector">
-      {formMode && <FieldTitle>{t('datasource')}</FieldTitle>}
       {renderDatabaseSelect()}
-      {formMode && <FieldTitle>{t('schema')}</FieldTitle>}
       {renderSchemaSelect()}
     </DatabaseSelectorWrapper>
   );
diff --git a/superset-frontend/src/components/Icons/Icon.tsx b/superset-frontend/src/components/Icons/Icon.tsx
index 9e3d0e1..efb78dc 100644
--- a/superset-frontend/src/components/Icons/Icon.tsx
+++ b/superset-frontend/src/components/Icons/Icon.tsx
@@ -53,15 +53,21 @@ export const Icon = (props: IconProps) => {
   const name = fileName.replace('_', '-');
 
   useEffect(() => {
+    let cancelled = false;
     async function importIcon(): Promise<void> {
       ImportedSVG.current = (
         await import(
           `!!@svgr/webpack?-svgo,+titleProp,+ref!images/icons/${fileName}.svg`
         )
       ).default;
-      setLoaded(true);
+      if (!cancelled) {
+        setLoaded(true);
+      }
     }
     importIcon();
+    return () => {
+      cancelled = true;
+    };
   }, [fileName, ImportedSVG]);
 
   return (
diff --git a/superset-frontend/src/components/Select/Select.tsx b/superset-frontend/src/components/Select/Select.tsx
index 96c88e5..655a656 100644
--- a/superset-frontend/src/components/Select/Select.tsx
+++ b/superset-frontend/src/components/Select/Select.tsx
@@ -87,12 +87,9 @@ const StyledContainer = styled.div`
   flex-direction: column;
 `;
 
-const StyledSelect = styled(AntdSelect, {
-  shouldForwardProp: prop => prop !== 'hasHeader',
-})<{ hasHeader: boolean }>`
-  ${({ theme, hasHeader }) => `
+const StyledSelect = styled(AntdSelect)`
+  ${({ theme }) => `
     width: 100%;
-    margin-top: ${hasHeader ? theme.gridUnit : 0}px;
 
     && .ant-select-selector {
       border-radius: ${theme.gridUnit}px;
@@ -190,6 +187,7 @@ const Select = ({
     : 'multiple';
 
   useEffect(() => {
+    fetchedQueries.current.clear();
     setSelectOptions(
       options && Array.isArray(options) ? options : EMPTY_OPTIONS,
     );
@@ -367,34 +365,45 @@ const Select = ({
     [options],
   );
 
-  const handleOnSearch = debounce((search: string) => {
-    const searchValue = search.trim();
-    // enables option creation
-    if (allowNewOptions && isSingleMode) {
-      const firstOption = selectOptions.length > 0 && selectOptions[0].value;
-      // replaces the last search value entered with the new one
-      // only when the value wasn't part of the original options
-      if (
-        searchValue &&
-        firstOption === searchedValue &&
-        !initialOptions.find(o => o.value === searchedValue)
-      ) {
-        selectOptions.shift();
-        setSelectOptions(selectOptions);
-      }
-      if (searchValue && !hasOption(searchValue, selectOptions)) {
-        const newOption = {
-          label: searchValue,
-          value: searchValue,
-        };
-        // adds a custom option
-        const newOptions = [...selectOptions, newOption];
-        setSelectOptions(newOptions);
-        setSelectValue(searchValue);
-      }
-    }
-    setSearchedValue(searchValue);
-  }, DEBOUNCE_TIMEOUT);
+  const handleOnSearch = useMemo(
+    () =>
+      debounce((search: string) => {
+        const searchValue = search.trim();
+        // enables option creation
+        if (allowNewOptions && isSingleMode) {
+          const firstOption =
+            selectOptions.length > 0 && selectOptions[0].value;
+          // replaces the last search value entered with the new one
+          // only when the value wasn't part of the original options
+          if (
+            searchValue &&
+            firstOption === searchedValue &&
+            !initialOptions.find(o => o.value === searchedValue)
+          ) {
+            selectOptions.shift();
+            setSelectOptions(selectOptions);
+          }
+          if (searchValue && !hasOption(searchValue, selectOptions)) {
+            const newOption = {
+              label: searchValue,
+              value: searchValue,
+            };
+            // adds a custom option
+            const newOptions = [...selectOptions, newOption];
+            setSelectOptions(newOptions);
+            setSelectValue(searchValue);
+          }
+        }
+        setSearchedValue(searchValue);
+      }, DEBOUNCE_TIMEOUT),
+    [
+      allowNewOptions,
+      initialOptions,
+      isSingleMode,
+      searchedValue,
+      selectOptions,
+    ],
+  );
 
   const handlePagination = (e: UIEvent<HTMLElement>) => {
     const vScroll = e.currentTarget;
@@ -487,7 +496,6 @@ const Select = ({
     <StyledContainer>
       {header}
       <StyledSelect
-        hasHeader={!!header}
         aria-label={ariaLabel || name}
         dropdownRender={dropdownRender}
         filterOption={handleFilterOption}
diff --git a/superset-frontend/src/components/TableSelector/TableSelector.test.jsx b/superset-frontend/src/components/TableSelector/TableSelector.test.jsx
deleted file mode 100644
index e1fa66b..0000000
--- a/superset-frontend/src/components/TableSelector/TableSelector.test.jsx
+++ /dev/null
@@ -1,291 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-import React from 'react';
-import configureStore from 'redux-mock-store';
-import { mount } from 'enzyme';
-import { act } from 'react-dom/test-utils';
-import sinon from 'sinon';
-import fetchMock from 'fetch-mock';
-import thunk from 'redux-thunk';
-import { supersetTheme, ThemeProvider } from '@superset-ui/core';
-
-import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint';
-
-import DatabaseSelector from 'src/components/DatabaseSelector';
-import TableSelector from 'src/components/TableSelector';
-import { initialState, tables } from 'spec/javascripts/sqllab/fixtures';
-
-const mockStore = configureStore([thunk]);
-const store = mockStore(initialState);
-
-const FETCH_SCHEMAS_ENDPOINT = 'glob:*/api/v1/database/*/schemas/*';
-const GET_TABLE_ENDPOINT = 'glob:*/superset/tables/1/*/*';
-const GET_TABLE_NAMES_ENDPOINT = 'glob:*/superset/tables/1/main/*';
-
-const mockedProps = {
-  clearable: false,
-  database: { id: 1, database_name: 'main' },
-  dbId: 1,
-  formMode: false,
-  getDbList: sinon.stub(),
-  handleError: sinon.stub(),
-  horizontal: false,
-  onChange: sinon.stub(),
-  onDbChange: sinon.stub(),
-  onSchemaChange: sinon.stub(),
-  onTableChange: sinon.stub(),
-  sqlLabMode: true,
-  tableName: '',
-  tableNameSticky: true,
-};
-
-const schemaOptions = {
-  result: ['main', 'erf', 'superset'],
-};
-const selectedSchema = { label: 'main', title: 'main', value: 'main' };
-const selectedTable = {
-  extra: null,
-  label: 'birth_names',
-  schema: 'main',
-  title: 'birth_names',
-  type: undefined,
-  value: 'birth_names',
-};
-
-async function mountAndWait(props = mockedProps) {
-  const mounted = mount(<TableSelector {...props} />, {
-    context: { store },
-    wrappingComponent: ThemeProvider,
-    wrappingComponentProps: { theme: supersetTheme },
-  });
-  await waitForComponentToPaint(mounted);
-
-  return mounted;
-}
-
-describe('TableSelector', () => {
-  let wrapper;
-
-  beforeEach(async () => {
-    fetchMock.reset();
-    wrapper = await mountAndWait();
-  });
-
-  it('renders', () => {
-    expect(wrapper.find(TableSelector)).toExist();
-    expect(wrapper.find(DatabaseSelector)).toExist();
-  });
-
-  describe('change database', () => {
-    afterEach(fetchMock.resetHistory);
-    afterAll(fetchMock.reset);
-
-    it('should fetch schemas', async () => {
-      fetchMock.get(FETCH_SCHEMAS_ENDPOINT, { overwriteRoutes: true });
-      act(() => {
-        wrapper.find('[data-test="select-database"]').first().props().onChange({
-          id: 1,
-          database_name: 'main',
-        });
-      });
-      await waitForComponentToPaint(wrapper);
-      expect(fetchMock.calls(FETCH_SCHEMAS_ENDPOINT)).toHaveLength(1);
-    });
-
-    it('should fetch schema options', async () => {
-      fetchMock.get(FETCH_SCHEMAS_ENDPOINT, schemaOptions, {
-        overwriteRoutes: true,
-      });
-      act(() => {
-        wrapper.find('[data-test="select-database"]').first().props().onChange({
-          id: 1,
-          database_name: 'main',
-        });
-      });
-      await waitForComponentToPaint(wrapper);
-      wrapper.update();
-      expect(fetchMock.calls(FETCH_SCHEMAS_ENDPOINT)).toHaveLength(1);
-
-      expect(
-        wrapper.find('[name="select-schema"]').first().props().options,
-      ).toEqual([
-        { value: 'main', label: 'main', title: 'main' },
-        { value: 'erf', label: 'erf', title: 'erf' },
-        { value: 'superset', label: 'superset', title: 'superset' },
-      ]);
-    });
-
-    it('should clear table options', async () => {
-      act(() => {
-        wrapper.find('[data-test="select-database"]').first().props().onChange({
-          id: 1,
-          database_name: 'main',
-        });
-      });
-      await waitForComponentToPaint(wrapper);
-      const props = wrapper.find('[name="async-select-table"]').first().props();
-      expect(props.isDisabled).toBe(true);
-      expect(props.value).toEqual(undefined);
-    });
-  });
-
-  describe('change schema', () => {
-    beforeEach(async () => {
-      fetchMock.get(FETCH_SCHEMAS_ENDPOINT, schemaOptions, {
-        overwriteRoutes: true,
-      });
-    });
-
-    afterEach(fetchMock.resetHistory);
-    afterAll(fetchMock.reset);
-
-    it('should fetch table', async () => {
-      fetchMock.get(GET_TABLE_NAMES_ENDPOINT, { overwriteRoutes: true });
-      act(() => {
-        wrapper.find('[data-test="select-database"]').first().props().onChange({
-          id: 1,
-          database_name: 'main',
-        });
-      });
-      await waitForComponentToPaint(wrapper);
-      act(() => {
-        wrapper
-          .find('[name="select-schema"]')
-          .first()
-          .props()
-          .onChange(selectedSchema);
-      });
-      await waitForComponentToPaint(wrapper);
-      expect(fetchMock.calls(GET_TABLE_NAMES_ENDPOINT)).toHaveLength(1);
-    });
-
-    it('should fetch table options', async () => {
-      fetchMock.get(GET_TABLE_NAMES_ENDPOINT, tables, {
-        overwriteRoutes: true,
-      });
-      act(() => {
-        wrapper.find('[data-test="select-database"]').first().props().onChange({
-          id: 1,
-          database_name: 'main',
-        });
-      });
-      await waitForComponentToPaint(wrapper);
-      act(() => {
-        wrapper
-          .find('[name="select-schema"]')
-          .first()
-          .props()
-          .onChange(selectedSchema);
-      });
-      await waitForComponentToPaint(wrapper);
-      expect(
-        wrapper.find('[name="select-schema"]').first().props().value[0],
-      ).toEqual(selectedSchema);
-      expect(fetchMock.calls(GET_TABLE_NAMES_ENDPOINT)).toHaveLength(1);
-      const { options } = wrapper.find('[name="select-table"]').first().props();
-      expect({ options }).toEqual(tables);
-    });
-  });
-
-  describe('change table', () => {
-    beforeEach(async () => {
-      fetchMock.get(GET_TABLE_NAMES_ENDPOINT, tables, {
-        overwriteRoutes: true,
-      });
-    });
-
-    it('should change table value', async () => {
-      act(() => {
-        wrapper
-          .find('[name="select-schema"]')
-          .first()
-          .props()
-          .onChange(selectedSchema);
-      });
-      await waitForComponentToPaint(wrapper);
-      act(() => {
-        wrapper
-          .find('[name="select-table"]')
-          .first()
-          .props()
-          .onChange(selectedTable);
-      });
-      await waitForComponentToPaint(wrapper);
-      expect(
-        wrapper.find('[name="select-table"]').first().props().value,
-      ).toEqual('birth_names');
-    });
-
-    it('should call onTableChange with schema from table object', async () => {
-      act(() => {
-        wrapper
-          .find('[name="select-schema"]')
-          .first()
-          .props()
-          .onChange(selectedSchema);
-      });
-      await waitForComponentToPaint(wrapper);
-      act(() => {
-        wrapper
-          .find('[name="select-table"]')
-          .first()
-          .props()
-          .onChange(selectedTable);
-      });
-      await waitForComponentToPaint(wrapper);
-      expect(mockedProps.onTableChange.getCall(0).args[0]).toBe('birth_names');
-      expect(mockedProps.onTableChange.getCall(0).args[1]).toBe('main');
-    });
-  });
-
-  describe('getTableNamesBySubStr', () => {
-    afterEach(fetchMock.resetHistory);
-    afterAll(fetchMock.reset);
-
-    it('should handle empty', async () => {
-      act(() => {
-        wrapper
-          .find('[name="async-select-table"]')
-          .first()
-          .props()
-          .loadOptions();
-      });
-      await waitForComponentToPaint(wrapper);
-      const props = wrapper.find('[name="async-select-table"]').first().props();
-      expect(props.isDisabled).toBe(true);
-      expect(props.value).toEqual('');
-    });
-
-    it('should handle table name', async () => {
-      wrapper.setProps({ schema: 'main' });
-      fetchMock.get(GET_TABLE_ENDPOINT, tables, {
-        overwriteRoutes: true,
-      });
-      act(() => {
-        wrapper
-          .find('[name="async-select-table"]')
-          .first()
-          .props()
-          .loadOptions();
-      });
-      await waitForComponentToPaint(wrapper);
-      expect(fetchMock.calls(GET_TABLE_ENDPOINT)).toHaveLength(1);
-    });
-  });
-});
diff --git a/superset-frontend/src/components/TableSelector/TableSelector.test.tsx b/superset-frontend/src/components/TableSelector/TableSelector.test.tsx
new file mode 100644
index 0000000..3b8b617
--- /dev/null
+++ b/superset-frontend/src/components/TableSelector/TableSelector.test.tsx
@@ -0,0 +1,91 @@
+/**
+ * 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 from 'react';
+import { render, screen, waitFor } from 'spec/helpers/testing-library';
+import { SupersetClient } from '@superset-ui/core';
+import userEvent from '@testing-library/user-event';
+import TableSelector from '.';
+
+const SupersetClientGet = jest.spyOn(SupersetClient, 'get');
+
+const createProps = () => ({
+  dbId: 1,
+  schema: 'test_schema',
+  handleError: jest.fn(),
+});
+
+beforeAll(() => {
+  SupersetClientGet.mockImplementation(
+    async () =>
+      ({
+        json: {
+          options: [
+            { label: 'table_a', value: 'table_a' },
+            { label: 'table_b', value: 'table_b' },
+          ],
+        },
+      } as any),
+  );
+});
+
+test('renders with default props', async () => {
+  const props = createProps();
+  render(<TableSelector {...props} />);
+  const databaseSelect = screen.getByRole('combobox', {
+    name: 'Select a database',
+  });
+  const schemaSelect = screen.getByRole('combobox', {
+    name: 'Select a database',
+  });
+  const tableSelect = screen.getByRole('combobox', {
+    name: 'Select a table',
+  });
+  await waitFor(() => {
+    expect(databaseSelect).toBeInTheDocument();
+    expect(schemaSelect).toBeInTheDocument();
+    expect(tableSelect).toBeInTheDocument();
+  });
+});
+
+test('renders table options', async () => {
+  const props = createProps();
+  render(<TableSelector {...props} />);
+  const tableSelect = screen.getByRole('combobox', {
+    name: 'Select a table',
+  });
+  userEvent.click(tableSelect);
+  expect(
+    await screen.findByRole('option', { name: 'table_a' }),
+  ).toBeInTheDocument();
+  expect(
+    await screen.findByRole('option', { name: 'table_b' }),
+  ).toBeInTheDocument();
+});
+
+test('renders disabled without schema', async () => {
+  const props = createProps();
+  render(<TableSelector {...props} schema={undefined} />);
+  const tableSelect = screen.getByRole('combobox', {
+    name: 'Select a table',
+  });
+  await waitFor(() => {
+    expect(tableSelect).toBeDisabled();
+  });
+});
diff --git a/superset-frontend/src/components/TableSelector/index.tsx b/superset-frontend/src/components/TableSelector/index.tsx
index c437b1e..5f68a94 100644
--- a/superset-frontend/src/components/TableSelector/index.tsx
+++ b/superset-frontend/src/components/TableSelector/index.tsx
@@ -18,57 +18,49 @@
  */
 import React, {
   FunctionComponent,
-  useEffect,
   useState,
   ReactNode,
+  useMemo,
+  useEffect,
 } from 'react';
 import { styled, SupersetClient, t } from '@superset-ui/core';
-import { AsyncSelect, CreatableSelect, Select } from 'src/components/Select';
-
+import { Select } from 'src/components';
 import { FormLabel } from 'src/components/Form';
-
+import Icons from 'src/components/Icons';
 import DatabaseSelector from 'src/components/DatabaseSelector';
 import RefreshLabel from 'src/components/RefreshLabel';
 import CertifiedIcon from 'src/components/CertifiedIcon';
 import WarningIconWithTooltip from 'src/components/WarningIconWithTooltip';
 
-const FieldTitle = styled.p`
-  color: ${({ theme }) => theme.colors.secondary.light2};
-  font-size: ${({ theme }) => theme.typography.sizes.s}px;
-  margin: 20px 0 10px 0;
-  text-transform: uppercase;
-`;
-
 const TableSelectorWrapper = styled.div`
-  .fa-refresh {
-    padding-left: 9px;
-  }
-
-  .refresh-col {
-    display: flex;
-    align-items: center;
-    width: 30px;
-    margin-left: ${({ theme }) => theme.gridUnit}px;
-  }
+  ${({ theme }) => `
+    .refresh {
+      display: flex;
+      align-items: center;
+      width: 30px;
+      margin-left: ${theme.gridUnit}px;
+      margin-top: ${theme.gridUnit * 5}px;
+    }
 
-  .section {
-    padding-bottom: 5px;
-    display: flex;
-    flex-direction: row;
-  }
+    .section {
+      display: flex;
+      flex-direction: row;
+      align-items: center;
+    }
 
-  .select {
-    flex-grow: 1;
-  }
+    .divider {
+      border-bottom: 1px solid ${theme.colors.secondary.light5};
+      margin: 15px 0;
+    }
 
-  .divider {
-    border-bottom: 1px solid ${({ theme }) => theme.colors.secondary.light5};
-    margin: 15px 0;
-  }
+    .table-length {
+      color: ${theme.colors.grayscale.light1};
+    }
 
-  .table-length {
-    color: ${({ theme }) => theme.colors.grayscale.light1};
-  }
+    .select {
+      flex: 1;
+    }
+  `}
 `;
 
 const TableLabel = styled.span`
@@ -98,7 +90,15 @@ interface TableSelectorProps {
     schema?: string;
     tableName?: string;
   }) => void;
-  onDbChange?: (db: any) => void;
+  onDbChange?: (
+    db:
+      | {
+          id: number;
+          database_name: string;
+          backend: string;
+        }
+      | undefined,
+  ) => void;
   onSchemaChange?: (arg0?: any) => {};
   onSchemasLoad?: () => void;
   onTableChange?: (tableName: string, schema: string) => void;
@@ -110,6 +110,52 @@ interface TableSelectorProps {
   tableNameSticky?: boolean;
 }
 
+interface Table {
+  label: string;
+  value: string;
+  type: string;
+  extra?: {
+    certification?: {
+      certified_by: string;
+      details: string;
+    };
+    warning_markdown?: string;
+  };
+}
+
+interface TableOption {
+  label: JSX.Element;
+  text: string;
+  value: string;
+}
+
+const TableOption = ({ table }: { table: Table }) => {
+  const { label, type, extra } = table;
+  return (
+    <TableLabel title={label}>
+      {type === 'view' ? (
+        <Icons.Eye iconSize="m" />
+      ) : (
+        <Icons.Table iconSize="m" />
+      )}
+      {extra?.certification && (
+        <CertifiedIcon
+          certifiedBy={extra.certification.certified_by}
+          details={extra.certification.details}
+          size="l"
+        />
+      )}
+      {extra?.warning_markdown && (
+        <WarningIconWithTooltip
+          warningMarkdown={extra.warning_markdown}
+          size="l"
+        />
+      )}
+      {label}
+    </TableLabel>
+  );
+};
+
 const TableSelector: FunctionComponent<TableSelectorProps> = ({
   database,
   dbId,
@@ -129,179 +175,187 @@ const TableSelector: FunctionComponent<TableSelectorProps> = ({
   tableName,
   tableNameSticky = true,
 }) => {
+  const [currentDbId, setCurrentDbId] = useState<number | undefined>(dbId);
   const [currentSchema, setCurrentSchema] = useState<string | undefined>(
     schema,
   );
-  const [currentTableName, setCurrentTableName] = useState<string | undefined>(
-    tableName,
-  );
-  const [tableLoading, setTableLoading] = useState(false);
-  const [tableOptions, setTableOptions] = useState([]);
-
-  function fetchTables(
-    databaseId?: number,
-    schema?: string,
-    forceRefresh = false,
-    substr = 'undefined',
-  ) {
-    const dbSchema = schema || currentSchema;
-    const actualDbId = databaseId || dbId;
-    if (actualDbId && dbSchema) {
-      const encodedSchema = encodeURIComponent(dbSchema);
-      const encodedSubstr = encodeURIComponent(substr);
-      setTableLoading(true);
-      setTableOptions([]);
+  const [currentTable, setCurrentTable] = useState<TableOption | undefined>();
+  const [refresh, setRefresh] = useState(0);
+  const [previousRefresh, setPreviousRefresh] = useState(0);
+
+  const loadTable = useMemo(
+    () => async (dbId: number, schema: string, tableName: string) => {
       const endpoint = encodeURI(
-        `/superset/tables/${actualDbId}/${encodedSchema}/${encodedSubstr}/${!!forceRefresh}/`,
+        `/superset/tables/${dbId}/${schema}/${encodeURIComponent(
+          tableName,
+        )}/false/true`,
       );
-      return SupersetClient.get({ endpoint })
-        .then(({ json }) => {
-          const options = json.options.map((o: any) => ({
-            value: o.value,
-            schema: o.schema,
-            label: o.label,
-            title: o.title,
-            type: o.type,
-            extra: o?.extra,
-          }));
-          setTableLoading(false);
-          setTableOptions(options);
+
+      if (previousRefresh !== refresh) {
+        setPreviousRefresh(refresh);
+      }
+
+      return SupersetClient.get({ endpoint }).then(({ json }) => {
+        const options = json.options as Table[];
+        if (options && options.length > 0) {
+          return options[0];
+        }
+        return null;
+      });
+    },
+    [], // eslint-disable-line react-hooks/exhaustive-deps
+  );
+
+  const loadTables = useMemo(
+    () => async (search: string) => {
+      const dbSchema = schema || currentSchema;
+      if (currentDbId && dbSchema) {
+        const encodedSchema = encodeURIComponent(dbSchema);
+        const encodedSubstr = encodeURIComponent(search || 'undefined');
+        const forceRefresh = refresh !== previousRefresh;
+        const endpoint = encodeURI(
+          `/superset/tables/${currentDbId}/${encodedSchema}/${encodedSubstr}/${forceRefresh}/`,
+        );
+
+        if (previousRefresh !== refresh) {
+          setPreviousRefresh(refresh);
+        }
+
+        return SupersetClient.get({ endpoint }).then(({ json }) => {
+          const options = json.options
+            .map((table: Table) => ({
+              value: table.value,
+              label: <TableOption table={table} />,
+              text: table.label,
+            }))
+            .sort((a: { text: string }, b: { text: string }) =>
+              a.text.localeCompare(b.text),
+            );
+
           if (onTablesLoad) {
             onTablesLoad(json.options);
           }
-        })
-        .catch(() => {
-          setTableLoading(false);
-          setTableOptions([]);
-          handleError(t('Error while fetching table list'));
+
+          return {
+            data: options,
+            totalCount: options.length,
+          };
         });
-    }
-    setTableLoading(false);
-    setTableOptions([]);
-    return Promise.resolve();
-  }
+      }
+      return { data: [], totalCount: 0 };
+    },
+    // We are using the refresh state to re-trigger the query
+    // previousRefresh should be out of dependencies array
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+    [currentDbId, currentSchema, onTablesLoad, schema, refresh],
+  );
 
   useEffect(() => {
-    if (dbId && schema) {
-      fetchTables();
+    async function fetchTable() {
+      if (schema && tableName) {
+        const table = await loadTable(dbId, schema, tableName);
+        if (table) {
+          setCurrentTable({
+            label: <TableOption table={table} />,
+            text: table.label,
+            value: table.value,
+          });
+        }
+      }
     }
-  }, [dbId, schema]);
+    fetchTable();
+  }, []); // eslint-disable-line react-hooks/exhaustive-deps
 
   function onSelectionChange({
     dbId,
     schema,
-    tableName,
+    table,
   }: {
     dbId: number;
     schema?: string;
-    tableName?: string;
+    table?: TableOption;
   }) {
-    setCurrentTableName(tableName);
+    setCurrentTable(table);
+    setCurrentDbId(dbId);
     setCurrentSchema(schema);
     if (onUpdate) {
-      onUpdate({ dbId, schema, tableName });
-    }
-  }
-
-  function getTableNamesBySubStr(substr = 'undefined') {
-    if (!dbId || !substr) {
-      const options: any[] = [];
-      return Promise.resolve({ options });
+      onUpdate({ dbId, schema, tableName: table?.value });
     }
-    const encodedSchema = encodeURIComponent(schema || '');
-    const encodedSubstr = encodeURIComponent(substr);
-    return SupersetClient.get({
-      endpoint: encodeURI(
-        `/superset/tables/${dbId}/${encodedSchema}/${encodedSubstr}`,
-      ),
-    }).then(({ json }) => {
-      const options = json.options.map((o: any) => ({
-        value: o.value,
-        schema: o.schema,
-        label: o.label,
-        title: o.title,
-        type: o.type,
-      }));
-      return { options };
-    });
   }
 
-  function changeTable(tableOpt: any) {
-    if (!tableOpt) {
-      setCurrentTableName('');
+  function changeTable(table: TableOption) {
+    if (!table) {
+      setCurrentTable(undefined);
       return;
     }
-    const schemaName = tableOpt.schema;
-    const tableOptTableName = tableOpt.value;
-    if (tableNameSticky) {
+    const tableOptTableName = table.value;
+    if (currentDbId && tableNameSticky) {
       onSelectionChange({
-        dbId,
-        schema: schemaName,
-        tableName: tableOptTableName,
+        dbId: currentDbId,
+        schema: currentSchema,
+        table,
       });
     }
-    if (onTableChange) {
-      onTableChange(tableOptTableName, schemaName);
+    if (onTableChange && currentSchema) {
+      onTableChange(tableOptTableName, currentSchema);
     }
   }
 
-  function changeSchema(schemaOpt: any, force = false) {
-    const value = schemaOpt ? schemaOpt.value : null;
+  function onRefresh() {
     if (onSchemaChange) {
-      onSchemaChange(value);
+      onSchemaChange(currentSchema);
     }
-    onSelectionChange({
-      dbId,
-      schema: value,
-      tableName: undefined,
-    });
-    fetchTables(dbId, currentSchema, force);
-  }
-
-  function renderTableOption(option: any) {
-    return (
-      <TableLabel title={option.label}>
-        <small className="text-muted">
-          <i className={`fa fa-${option.type === 'view' ? 'eye' : 'table'}`} />
-        </small>
-        {option.extra?.certification && (
-          <CertifiedIcon
-            certifiedBy={option.extra.certification.certified_by}
-            details={option.extra.certification.details}
-            size={20}
-          />
-        )}
-        {option.extra?.warning_markdown && (
-          <WarningIconWithTooltip
-            warningMarkdown={option.extra.warning_markdown}
-            size={20}
-          />
-        )}
-        {option.label}
-      </TableLabel>
-    );
+    if (currentDbId && currentSchema) {
+      onSelectionChange({
+        dbId: currentDbId,
+        schema: currentSchema,
+        table: currentTable,
+      });
+    }
+    setRefresh(refresh + 1);
   }
 
   function renderSelectRow(select: ReactNode, refreshBtn: ReactNode) {
     return (
       <div className="section">
         <span className="select">{select}</span>
-        <span className="refresh-col">{refreshBtn}</span>
+        <span className="refresh">{refreshBtn}</span>
       </div>
     );
   }
 
+  const internalDbChange = (
+    db:
+      | {
+          id: number;
+          database_name: string;
+          backend: string;
+        }
+      | undefined,
+  ) => {
+    setCurrentDbId(db?.id);
+    if (onDbChange) {
+      onDbChange(db);
+    }
+  };
+
+  const internalSchemaChange = (schema?: string) => {
+    setCurrentSchema(schema);
+    if (onSchemaChange) {
+      onSchemaChange(schema);
+    }
+  };
+
   function renderDatabaseSelector() {
     return (
       <DatabaseSelector
-        dbId={dbId}
+        db={database}
         formMode={formMode}
         getDbList={getDbList}
-        getTableList={fetchTables}
         handleError={handleError}
         onUpdate={onSelectionChange}
-        onDbChange={readOnly ? undefined : onDbChange}
-        onSchemaChange={readOnly ? undefined : onSchemaChange}
+        onDbChange={readOnly ? undefined : internalDbChange}
+        onSchemaChange={readOnly ? undefined : internalSchemaChange}
         onSchemasLoad={onSchemasLoad}
         schema={currentSchema}
         sqlLabMode={sqlLabMode}
@@ -311,96 +365,54 @@ const TableSelector: FunctionComponent<TableSelectorProps> = ({
     );
   }
 
+  const handleFilterOption = useMemo(
+    () => (search: string, option: TableOption) => {
+      const searchValue = search.trim().toLowerCase();
+      const { text } = option;
+      return text.toLowerCase().includes(searchValue);
+    },
+    [],
+  );
+
   function renderTableSelect() {
-    const options = tableOptions;
-    let select = null;
-    if (currentSchema && !formMode) {
-      // dataset editor
-      select = (
-        <Select
-          name="select-table"
-          isLoading={tableLoading}
-          ignoreAccents={false}
-          placeholder={t('Select table or type table name')}
-          autosize={false}
-          onChange={changeTable}
-          options={options}
-          // @ts-ignore
-          value={currentTableName}
-          optionRenderer={renderTableOption}
-          valueRenderer={renderTableOption}
-          isDisabled={readOnly}
-        />
-      );
-    } else if (formMode) {
-      select = (
-        <CreatableSelect
-          name="select-table"
-          isLoading={tableLoading}
-          ignoreAccents={false}
-          placeholder={t('Select table or type table name')}
-          autosize={false}
-          onChange={changeTable}
-          options={options}
-          // @ts-ignore
-          value={currentTableName}
-          optionRenderer={renderTableOption}
-        />
-      );
-    } else {
-      // sql lab
-      let tableSelectPlaceholder;
-      let tableSelectDisabled = false;
-      if (database && database.allow_multi_schema_metadata_fetch) {
-        tableSelectPlaceholder = t('Type to search ...');
-      } else {
-        tableSelectPlaceholder = t('Select table ');
-        tableSelectDisabled = true;
-      }
-      select = (
-        <AsyncSelect
-          name="async-select-table"
-          placeholder={tableSelectPlaceholder}
-          isDisabled={tableSelectDisabled}
-          autosize={false}
-          onChange={changeTable}
-          // @ts-ignore
-          value={currentTableName}
-          loadOptions={getTableNamesBySubStr}
-          optionRenderer={renderTableOption}
-        />
-      );
-    }
+    const disabled =
+      (currentSchema && !formMode && readOnly) ||
+      (!currentSchema && !database?.allow_multi_schema_metadata_fetch);
+
+    const header = sqlLabMode ? (
+      <FormLabel>{t('See table schema')}</FormLabel>
+    ) : (
+      <FormLabel>{t('Table')}</FormLabel>
+    );
+
+    const select = (
+      <Select
+        ariaLabel={t('Select a table')}
+        disabled={disabled}
+        filterOption={handleFilterOption}
+        header={header}
+        name="select-table"
+        onChange={changeTable}
+        options={loadTables}
+        placeholder={t('Select a table')}
+        value={currentTable}
+      />
+    );
+
     const refresh = !formMode && !readOnly && (
       <RefreshLabel
-        onClick={() => changeSchema({ value: schema }, true)}
+        onClick={onRefresh}
         tooltipContent={t('Force refresh table list')}
       />
     );
-    return renderSelectRow(select, refresh);
-  }
 
-  function renderSeeTableLabel() {
-    return (
-      <div className="section">
-        <FormLabel>
-          {t('See table schema')}{' '}
-          {schema && (
-            <small className="table-length">
-              {tableOptions.length} in {schema}
-            </small>
-          )}
-        </FormLabel>
-      </div>
-    );
+    return renderSelectRow(select, refresh);
   }
 
   return (
     <TableSelectorWrapper>
       {renderDatabaseSelector()}
-      {!formMode && <div className="divider" />}
-      {sqlLabMode && renderSeeTableLabel()}
-      {formMode && <FieldTitle>{t('Table')}</FieldTitle>}
+      {sqlLabMode && !formMode && <div className="divider" />}
       {renderTableSelect()}
     </TableSelectorWrapper>
   );
diff --git a/superset-frontend/src/components/WarningIconWithTooltip/index.tsx b/superset-frontend/src/components/WarningIconWithTooltip/index.tsx
index f160ade..f732554 100644
--- a/superset-frontend/src/components/WarningIconWithTooltip/index.tsx
+++ b/superset-frontend/src/components/WarningIconWithTooltip/index.tsx
@@ -18,16 +18,17 @@
  */
 import React from 'react';
 import { useTheme, SafeMarkdown } from '@superset-ui/core';
-import Icons from 'src/components/Icons';
+import Icons, { IconType } from 'src/components/Icons';
 import { Tooltip } from 'src/components/Tooltip';
 
 export interface WarningIconWithTooltipProps {
   warningMarkdown: string;
-  size?: number;
+  size?: IconType['iconSize'];
 }
 
 function WarningIconWithTooltip({
   warningMarkdown,
+  size,
 }: WarningIconWithTooltipProps) {
   const theme = useTheme();
   return (
@@ -37,6 +38,7 @@ function WarningIconWithTooltip({
     >
       <Icons.AlertSolid
         iconColor={theme.colors.alert.base}
+        iconSize={size}
         css={{ marginRight: theme.gridUnit * 2 }}
       />
     </Tooltip>
diff --git a/superset-frontend/src/datasource/DatasourceEditor.jsx b/superset-frontend/src/datasource/DatasourceEditor.jsx
index e11b831..d8a0a342 100644
--- a/superset-frontend/src/datasource/DatasourceEditor.jsx
+++ b/superset-frontend/src/datasource/DatasourceEditor.jsx
@@ -775,41 +775,47 @@ class DatasourceEditor extends React.PureComponent {
             <div>
               {this.state.isSqla && (
                 <>
-                  <Field
-                    fieldKey="databaseSelector"
-                    label={t('virtual')}
-                    control={
-                      <DatabaseSelector
-                        dbId={datasource.database.id}
-                        schema={datasource.schema}
-                        onSchemaChange={schema =>
-                          this.state.isEditMode &&
-                          this.onDatasourcePropChange('schema', schema)
-                        }
-                        onDbChange={database =>
-                          this.state.isEditMode &&
-                          this.onDatasourcePropChange('database', database)
+                  <Col xs={24} md={12}>
+                    <Field
+                      fieldKey="databaseSelector"
+                      label={t('virtual')}
+                      control={
+                        <div css={{ marginTop: 8 }}>
+                          <DatabaseSelector
+                            db={datasource?.database}
+                            schema={datasource.schema}
+                            onSchemaChange={schema =>
+                              this.state.isEditMode &&
+                              this.onDatasourcePropChange('schema', schema)
+                            }
+                            onDbChange={database =>
+                              this.state.isEditMode &&
+                              this.onDatasourcePropChange('database', database)
+                            }
+                            formMode={false}
+                            handleError={this.props.addDangerToast}
+                            readOnly={!this.state.isEditMode}
+                          />
+                        </div>
+                      }
+                    />
+                    <div css={{ width: 'calc(100% - 34px)', marginTop: -16 }}>
+                      <Field
+                        fieldKey="table_name"
+                        label={t('Dataset name')}
+                        control={
+                          <TextControl
+                            controlId="table_name"
+                            onChange={table => {
+                              this.onDatasourcePropChange('table_name', table);
+                            }}
+                            placeholder={t('Dataset name')}
+                            disabled={!this.state.isEditMode}
+                          />
                         }
-                        formMode={false}
-                        handleError={this.props.addDangerToast}
-                        readOnly={!this.state.isEditMode}
-                      />
-                    }
-                  />
-                  <Field
-                    fieldKey="table_name"
-                    label={t('Dataset name')}
-                    control={
-                      <TextControl
-                        controlId="table_name"
-                        onChange={table => {
-                          this.onDatasourcePropChange('table_name', table);
-                        }}
-                        placeholder={t('Dataset name')}
-                        disabled={!this.state.isEditMode}
                       />
-                    }
-                  />
+                    </div>
+                  </Col>
                   <Field
                     fieldKey="sql"
                     label={t('SQL')}
@@ -853,33 +859,39 @@ class DatasourceEditor extends React.PureComponent {
                   fieldKey="tableSelector"
                   label={t('Physical')}
                   control={
-                    <TableSelector
-                      clearable={false}
-                      dbId={datasource.database.id}
-                      handleError={this.props.addDangerToast}
-                      schema={datasource.schema}
-                      sqlLabMode={false}
-                      tableName={datasource.table_name}
-                      onSchemaChange={
-                        this.state.isEditMode
-                          ? schema =>
-                              this.onDatasourcePropChange('schema', schema)
-                          : undefined
-                      }
-                      onDbChange={
-                        this.state.isEditMode
-                          ? database =>
-                              this.onDatasourcePropChange('database', database)
-                          : undefined
-                      }
-                      onTableChange={
-                        this.state.isEditMode
-                          ? table =>
-                              this.onDatasourcePropChange('table_name', table)
-                          : undefined
-                      }
-                      readOnly={!this.state.isEditMode}
-                    />
+                    <div css={{ marginTop: 8 }}>
+                      <TableSelector
+                        clearable={false}
+                        database={datasource.database}
+                        dbId={datasource.database.id}
+                        handleError={this.props.addDangerToast}
+                        schema={datasource.schema}
+                        sqlLabMode={false}
+                        tableName={datasource.table_name}
+                        onSchemaChange={
+                          this.state.isEditMode
+                            ? schema =>
+                                this.onDatasourcePropChange('schema', schema)
+                            : undefined
+                        }
+                        onDbChange={
+                          this.state.isEditMode
+                            ? database =>
+                                this.onDatasourcePropChange(
+                                  'database',
+                                  database,
+                                )
+                            : undefined
+                        }
+                        onTableChange={
+                          this.state.isEditMode
+                            ? table =>
+                                this.onDatasourcePropChange('table_name', table)
+                            : undefined
+                        }
+                        readOnly={!this.state.isEditMode}
+                      />
+                    </div>
                   }
                   description={t(
                     'The pointer to a physical table (or view). Keep in mind that the chart is ' +
diff --git a/superset-frontend/src/explore/components/controls/DatasourceControl/index.jsx b/superset-frontend/src/explore/components/controls/DatasourceControl/index.jsx
index 9278c29..3df5532 100644
--- a/superset-frontend/src/explore/components/controls/DatasourceControl/index.jsx
+++ b/superset-frontend/src/explore/components/controls/DatasourceControl/index.jsx
@@ -227,10 +227,7 @@ class DatasourceControl extends React.PureComponent {
             </Tooltip>
           )}
           {extra?.warning_markdown && (
-            <WarningIconWithTooltip
-              warningMarkdown={extra.warning_markdown}
-              size={30}
-            />
+            <WarningIconWithTooltip warningMarkdown={extra.warning_markdown} />
           )}
           <Dropdown
             overlay={datasourceMenu}
diff --git a/superset-frontend/src/views/CRUD/data/dataset/DatasetList.tsx b/superset-frontend/src/views/CRUD/data/dataset/DatasetList.tsx
index 496decf..f50473c 100644
--- a/superset-frontend/src/views/CRUD/data/dataset/DatasetList.tsx
+++ b/superset-frontend/src/views/CRUD/data/dataset/DatasetList.tsx
@@ -243,11 +243,13 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
                   <CertifiedIcon
                     certifiedBy={parsedExtra.certification.certified_by}
                     details={parsedExtra.certification.details}
+                    size="l"
                   />
                 )}
                 {parsedExtra?.warning_markdown && (
                   <WarningIconWithTooltip
                     warningMarkdown={parsedExtra.warning_markdown}
+                    size="l"
                   />
                 )}
                 {titleLink}
diff --git a/superset/datasets/api.py b/superset/datasets/api.py
index 8d8eb6e..261a7d7 100644
--- a/superset/datasets/api.py
+++ b/superset/datasets/api.py
@@ -160,7 +160,7 @@ class DatasetRestApi(BaseSupersetModelRestApi):
         "url",
         "extra",
     ]
-    show_columns = show_select_columns + ["columns.type_generic"]
+    show_columns = show_select_columns + ["columns.type_generic", "database.backend"]
     add_model_schema = DatasetPostSchema()
     edit_model_schema = DatasetPutSchema()
     add_columns = ["database", "schema", "table_name", "owners"]
diff --git a/superset/views/core.py b/superset/views/core.py
index dc4a8a2..836bee4 100755
--- a/superset/views/core.py
+++ b/superset/views/core.py
@@ -1064,8 +1064,14 @@ class Superset(BaseSupersetView):  # pylint: disable=too-many-public-methods
     @event_logger.log_this
     @expose("/tables/<int:db_id>/<schema>/<substr>/")
     @expose("/tables/<int:db_id>/<schema>/<substr>/<force_refresh>/")
-    def tables(  # pylint: disable=too-many-locals,no-self-use
-        self, db_id: int, schema: str, substr: str, force_refresh: str = "false"
+    @expose("/tables/<int:db_id>/<schema>/<substr>/<force_refresh>/<exact_match>")
+    def tables(  # pylint: disable=too-many-locals,no-self-use,too-many-arguments
+        self,
+        db_id: int,
+        schema: str,
+        substr: str,
+        force_refresh: str = "false",
+        exact_match: str = "false",
     ) -> FlaskResponse:
         """Endpoint to fetch the list of tables for given database"""
         # Guarantees database filtering by security access
@@ -1078,6 +1084,7 @@ class Superset(BaseSupersetView):  # pylint: disable=too-many-public-methods
             return json_error_response("Not found", 404)
 
         force_refresh_parsed = force_refresh.lower() == "true"
+        exact_match_parsed = exact_match.lower() == "true"
         schema_parsed = utils.parse_js_uri_path_item(schema, eval_undefined=True)
         substr_parsed = utils.parse_js_uri_path_item(substr, eval_undefined=True)
 
@@ -1119,9 +1126,15 @@ class Superset(BaseSupersetView):  # pylint: disable=too-many-public-methods
                 ds_name.table if schema_parsed else f"{ds_name.schema}.{ds_name.table}"
             )
 
+        def is_match(src: str, target: utils.DatasourceName) -> bool:
+            target_label = get_datasource_label(target)
+            if exact_match_parsed:
+                return src == target_label
+            return src in target_label
+
         if substr_parsed:
-            tables = [tn for tn in tables if substr_parsed in get_datasource_label(tn)]
-            views = [vn for vn in views if substr_parsed in get_datasource_label(vn)]
+            tables = [tn for tn in tables if is_match(substr_parsed, tn)]
+            views = [vn for vn in views if is_match(substr_parsed, vn)]
 
         if not schema_parsed and database.default_schemas:
             user_schemas = (
diff --git a/tests/integration_tests/datasets/api_tests.py b/tests/integration_tests/datasets/api_tests.py
index 385025e..c970153 100644
--- a/tests/integration_tests/datasets/api_tests.py
+++ b/tests/integration_tests/datasets/api_tests.py
@@ -222,6 +222,7 @@ class TestDatasetApi(SupersetTestCase):
         Dataset API: Test get dataset item
         """
         table = self.get_energy_usage_dataset()
+        main_db = get_main_database()
         self.login(username="admin")
         uri = f"api/v1/dataset/{table.id}"
         rv = self.get_assert_metric(uri, "get")
@@ -229,7 +230,11 @@ class TestDatasetApi(SupersetTestCase):
         response = json.loads(rv.data.decode("utf-8"))
         expected_result = {
             "cache_timeout": None,
-            "database": {"database_name": "examples", "id": 1},
+            "database": {
+                "backend": main_db.backend,
+                "database_name": "examples",
+                "id": 1,
+            },
             "default_endpoint": None,
             "description": "Energy consumption",
             "extra": None,
@@ -244,9 +249,10 @@ class TestDatasetApi(SupersetTestCase):
             "table_name": "energy_usage",
             "template_params": None,
         }
-        assert {
-            k: v for k, v in response["result"].items() if k in expected_result
-        } == expected_result
+        if response["result"]["database"]["backend"] not in ("presto", "hive"):
+            assert {
+                k: v for k, v in response["result"].items() if k in expected_result
+            } == expected_result
         assert len(response["result"]["columns"]) == 3
         assert len(response["result"]["metrics"]) == 2