You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ju...@apache.org on 2023/07/20 18:59:37 UTC

[superset] branch master updated: fix(datasets): Replace left panel layout by TableSelector (#24599)

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

justinpark 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 b2831b419e fix(datasets): Replace left panel layout by TableSelector (#24599)
b2831b419e is described below

commit b2831b419e1c316cd32b3e0ad29075321460f8bc
Author: JUST.in DO IT <ju...@airbnb.com>
AuthorDate: Thu Jul 20 11:59:31 2023 -0700

    fix(datasets): Replace left panel layout by TableSelector (#24599)
    
    Co-authored-by: Justin Park <ju...@apache.org>
---
 .../src/components/TableSelector/index.tsx         |   8 +-
 superset-frontend/src/components/Tooltip/index.tsx |   3 +
 .../components/WarningIconWithTooltip/index.tsx    |   4 +-
 .../AddDataset/LeftPanel/LeftPanel.test.tsx        | 286 ++++++++++++---------
 .../datasets/AddDataset/LeftPanel/index.tsx        | 258 +++----------------
 .../datasets/DatasetLayout/DatasetLayout.test.tsx  |   2 +-
 .../src/features/datasets/hooks/useDatasetLists.ts |   7 +-
 7 files changed, 216 insertions(+), 352 deletions(-)

diff --git a/superset-frontend/src/components/TableSelector/index.tsx b/superset-frontend/src/components/TableSelector/index.tsx
index 7c2a809e5c..f36056a4a2 100644
--- a/superset-frontend/src/components/TableSelector/index.tsx
+++ b/superset-frontend/src/components/TableSelector/index.tsx
@@ -104,6 +104,7 @@ interface TableSelectorProps {
   tableValue?: string | string[];
   onTableSelectChange?: (value?: string | string[], schema?: string) => void;
   tableSelectMode?: 'single' | 'multiple';
+  customTableOptionLabelRenderer?: (table: Table) => JSX.Element;
 }
 
 export interface TableOption {
@@ -132,6 +133,7 @@ export const TableOption = ({ table }: { table: Table }) => {
         <WarningIconWithTooltip
           warningMarkdown={extra.warning_markdown}
           size="l"
+          marginRight={4}
         />
       )}
       {value}
@@ -164,6 +166,7 @@ const TableSelector: FunctionComponent<TableSelectorProps> = ({
   tableSelectMode = 'single',
   tableValue = undefined,
   onTableSelectChange,
+  customTableOptionLabelRenderer,
 }) => {
   const { addSuccessToast } = useToasts();
   const [currentSchema, setCurrentSchema] = useState<string | undefined>(
@@ -203,9 +206,12 @@ const TableSelector: FunctionComponent<TableSelectorProps> = ({
             value: table.value,
             label: <TableOption table={table} />,
             text: table.value,
+            ...(customTableOptionLabelRenderer && {
+              customLabel: customTableOptionLabelRenderer(table),
+            }),
           }))
         : [],
-    [data],
+    [data, customTableOptionLabelRenderer],
   );
 
   useEffect(() => {
diff --git a/superset-frontend/src/components/Tooltip/index.tsx b/superset-frontend/src/components/Tooltip/index.tsx
index 64af6b06a0..8237356690 100644
--- a/superset-frontend/src/components/Tooltip/index.tsx
+++ b/superset-frontend/src/components/Tooltip/index.tsx
@@ -41,6 +41,9 @@ export const Tooltip = (props: TooltipProps) => {
               display: block;
             }
           }
+          .ant-tooltip-inner > p {
+            margin: 0;
+          }
         `}
       />
       <AntdTooltip
diff --git a/superset-frontend/src/components/WarningIconWithTooltip/index.tsx b/superset-frontend/src/components/WarningIconWithTooltip/index.tsx
index f732554e15..82b3ac7e00 100644
--- a/superset-frontend/src/components/WarningIconWithTooltip/index.tsx
+++ b/superset-frontend/src/components/WarningIconWithTooltip/index.tsx
@@ -24,11 +24,13 @@ import { Tooltip } from 'src/components/Tooltip';
 export interface WarningIconWithTooltipProps {
   warningMarkdown: string;
   size?: IconType['iconSize'];
+  marginRight?: number;
 }
 
 function WarningIconWithTooltip({
   warningMarkdown,
   size,
+  marginRight,
 }: WarningIconWithTooltipProps) {
   const theme = useTheme();
   return (
@@ -39,7 +41,7 @@ function WarningIconWithTooltip({
       <Icons.AlertSolid
         iconColor={theme.colors.alert.base}
         iconSize={size}
-        css={{ marginRight: theme.gridUnit * 2 }}
+        css={{ marginRight: marginRight ?? theme.gridUnit * 2 }}
       />
     </Tooltip>
   );
diff --git a/superset-frontend/src/features/datasets/AddDataset/LeftPanel/LeftPanel.test.tsx b/superset-frontend/src/features/datasets/AddDataset/LeftPanel/LeftPanel.test.tsx
index 604e3e9d9e..5156073281 100644
--- a/superset-frontend/src/features/datasets/AddDataset/LeftPanel/LeftPanel.test.tsx
+++ b/superset-frontend/src/features/datasets/AddDataset/LeftPanel/LeftPanel.test.tsx
@@ -27,122 +27,128 @@ const databasesEndpoint = 'glob:*/api/v1/database/?q*';
 const schemasEndpoint = 'glob:*/api/v1/database/*/schemas*';
 const tablesEndpoint = 'glob:*/api/v1/database/*/tables/?q*';
 
-fetchMock.get(databasesEndpoint, {
-  count: 2,
-  description_columns: {},
-  ids: [1, 2],
-  label_columns: {
-    allow_file_upload: 'Allow Csv Upload',
-    allow_ctas: 'Allow Ctas',
-    allow_cvas: 'Allow Cvas',
-    allow_dml: 'Allow Dml',
-    allow_multi_schema_metadata_fetch: 'Allow Multi Schema Metadata Fetch',
-    allow_run_async: 'Allow Run Async',
-    allows_cost_estimate: 'Allows Cost Estimate',
-    allows_subquery: 'Allows Subquery',
-    allows_virtual_table_explore: 'Allows Virtual Table Explore',
-    disable_data_preview: 'Disables SQL Lab Data Preview',
-    backend: 'Backend',
-    changed_on: 'Changed On',
-    changed_on_delta_humanized: 'Changed On Delta Humanized',
-    'created_by.first_name': 'Created By First Name',
-    'created_by.last_name': 'Created By Last Name',
-    database_name: 'Database Name',
-    explore_database_id: 'Explore Database Id',
-    expose_in_sqllab: 'Expose In Sqllab',
-    force_ctas_schema: 'Force Ctas Schema',
-    id: 'Id',
-  },
-  list_columns: [
-    'allow_file_upload',
-    'allow_ctas',
-    'allow_cvas',
-    'allow_dml',
-    'allow_multi_schema_metadata_fetch',
-    'allow_run_async',
-    'allows_cost_estimate',
-    'allows_subquery',
-    'allows_virtual_table_explore',
-    'disable_data_preview',
-    'backend',
-    'changed_on',
-    'changed_on_delta_humanized',
-    'created_by.first_name',
-    'created_by.last_name',
-    'database_name',
-    'explore_database_id',
-    'expose_in_sqllab',
-    'force_ctas_schema',
-    'id',
-  ],
-  list_title: 'List Database',
-  order_columns: [
-    'allow_file_upload',
-    'allow_dml',
-    'allow_run_async',
-    'changed_on',
-    'changed_on_delta_humanized',
-    'created_by.first_name',
-    'database_name',
-    'expose_in_sqllab',
-  ],
-  result: [
-    {
-      allow_file_upload: false,
-      allow_ctas: false,
-      allow_cvas: false,
-      allow_dml: false,
-      allow_multi_schema_metadata_fetch: false,
-      allow_run_async: false,
-      allows_cost_estimate: null,
-      allows_subquery: true,
-      allows_virtual_table_explore: true,
-      disable_data_preview: false,
-      backend: 'postgresql',
-      changed_on: '2021-03-09T19:02:07.141095',
-      changed_on_delta_humanized: 'a day ago',
-      created_by: null,
-      database_name: 'test-postgres',
-      explore_database_id: 1,
-      expose_in_sqllab: true,
-      force_ctas_schema: null,
-      id: 1,
-    },
-    {
-      allow_csv_upload: false,
-      allow_ctas: false,
-      allow_cvas: false,
-      allow_dml: false,
-      allow_multi_schema_metadata_fetch: false,
-      allow_run_async: false,
-      allows_cost_estimate: null,
-      allows_subquery: true,
-      allows_virtual_table_explore: true,
-      disable_data_preview: false,
-      backend: 'mysql',
-      changed_on: '2021-03-09T19:02:07.141095',
-      changed_on_delta_humanized: 'a day ago',
-      created_by: null,
-      database_name: 'test-mysql',
-      explore_database_id: 1,
-      expose_in_sqllab: true,
-      force_ctas_schema: null,
-      id: 2,
+beforeEach(() => {
+  fetchMock.get(databasesEndpoint, {
+    count: 2,
+    description_columns: {},
+    ids: [1, 2],
+    label_columns: {
+      allow_file_upload: 'Allow Csv Upload',
+      allow_ctas: 'Allow Ctas',
+      allow_cvas: 'Allow Cvas',
+      allow_dml: 'Allow Dml',
+      allow_multi_schema_metadata_fetch: 'Allow Multi Schema Metadata Fetch',
+      allow_run_async: 'Allow Run Async',
+      allows_cost_estimate: 'Allows Cost Estimate',
+      allows_subquery: 'Allows Subquery',
+      allows_virtual_table_explore: 'Allows Virtual Table Explore',
+      disable_data_preview: 'Disables SQL Lab Data Preview',
+      backend: 'Backend',
+      changed_on: 'Changed On',
+      changed_on_delta_humanized: 'Changed On Delta Humanized',
+      'created_by.first_name': 'Created By First Name',
+      'created_by.last_name': 'Created By Last Name',
+      database_name: 'Database Name',
+      explore_database_id: 'Explore Database Id',
+      expose_in_sqllab: 'Expose In Sqllab',
+      force_ctas_schema: 'Force Ctas Schema',
+      id: 'Id',
     },
-  ],
-});
+    list_columns: [
+      'allow_file_upload',
+      'allow_ctas',
+      'allow_cvas',
+      'allow_dml',
+      'allow_multi_schema_metadata_fetch',
+      'allow_run_async',
+      'allows_cost_estimate',
+      'allows_subquery',
+      'allows_virtual_table_explore',
+      'disable_data_preview',
+      'backend',
+      'changed_on',
+      'changed_on_delta_humanized',
+      'created_by.first_name',
+      'created_by.last_name',
+      'database_name',
+      'explore_database_id',
+      'expose_in_sqllab',
+      'force_ctas_schema',
+      'id',
+    ],
+    list_title: 'List Database',
+    order_columns: [
+      'allow_file_upload',
+      'allow_dml',
+      'allow_run_async',
+      'changed_on',
+      'changed_on_delta_humanized',
+      'created_by.first_name',
+      'database_name',
+      'expose_in_sqllab',
+    ],
+    result: [
+      {
+        allow_file_upload: false,
+        allow_ctas: false,
+        allow_cvas: false,
+        allow_dml: false,
+        allow_multi_schema_metadata_fetch: false,
+        allow_run_async: false,
+        allows_cost_estimate: null,
+        allows_subquery: true,
+        allows_virtual_table_explore: true,
+        disable_data_preview: false,
+        backend: 'postgresql',
+        changed_on: '2021-03-09T19:02:07.141095',
+        changed_on_delta_humanized: 'a day ago',
+        created_by: null,
+        database_name: 'test-postgres',
+        explore_database_id: 1,
+        expose_in_sqllab: true,
+        force_ctas_schema: null,
+        id: 1,
+      },
+      {
+        allow_csv_upload: false,
+        allow_ctas: false,
+        allow_cvas: false,
+        allow_dml: false,
+        allow_multi_schema_metadata_fetch: false,
+        allow_run_async: false,
+        allows_cost_estimate: null,
+        allows_subquery: true,
+        allows_virtual_table_explore: true,
+        disable_data_preview: false,
+        backend: 'mysql',
+        changed_on: '2021-03-09T19:02:07.141095',
+        changed_on_delta_humanized: 'a day ago',
+        created_by: null,
+        database_name: 'test-mysql',
+        explore_database_id: 1,
+        expose_in_sqllab: true,
+        force_ctas_schema: null,
+        id: 2,
+      },
+    ],
+  });
+
+  fetchMock.get(schemasEndpoint, {
+    result: ['information_schema', 'public'],
+  });
 
-fetchMock.get(schemasEndpoint, {
-  result: ['information_schema', 'public'],
+  fetchMock.get(tablesEndpoint, {
+    count: 3,
+    result: [
+      { value: 'Sheet1', type: 'table', extra: null },
+      { value: 'Sheet2', type: 'table', extra: null },
+      { value: 'Sheet3', type: 'table', extra: null },
+    ],
+  });
 });
 
-fetchMock.get(tablesEndpoint, {
-  count: 3,
-  result: [
-    { value: 'Sheet1', type: 'table', extra: null },
-    { value: 'Sheet2', type: 'table', extra: null },
-    { value: 'Sheet3', type: 'table', extra: null },
-  ],
+afterEach(() => {
+  fetchMock.reset();
 });
 
 const mockFun = jest.fn();
@@ -152,14 +158,16 @@ test('should render', async () => {
     useRedux: true,
   });
   expect(
-    await screen.findByText(/select database & schema/i),
+    await screen.findByText(/Select database or type to search databases/i),
   ).toBeInTheDocument();
 });
 
 test('should render schema selector, database selector container, and selects', async () => {
   render(<LeftPanel setDataset={mockFun} />, { useRedux: true });
 
-  expect(await screen.findByText(/select database & schema/i)).toBeVisible();
+  expect(
+    await screen.findByText(/Select database or type to search databases/i),
+  ).toBeVisible();
 
   const databaseSelect = screen.getByRole('combobox', {
     name: 'Select database or type to search databases',
@@ -175,7 +183,7 @@ test('does not render blank state if there is nothing selected', async () => {
   render(<LeftPanel setDataset={mockFun} />, { useRedux: true });
 
   expect(
-    await screen.findByText(/select database & schema/i),
+    await screen.findByText(/Select database or type to search databases/i),
   ).toBeInTheDocument();
   const emptyState = screen.queryByRole('img', { name: /empty/i });
   expect(emptyState).not.toBeInTheDocument();
@@ -218,25 +226,45 @@ test('searches for a table name', async () => {
   const schemaSelect = screen.getByRole('combobox', {
     name: /select schema or type to search schemas/i,
   });
+  const tableSelect = screen.getByRole('combobox', {
+    name: /select table or type to search tables/i,
+  });
 
   await waitFor(() => expect(schemaSelect).toBeEnabled());
 
   // Click 'public' schema to access tables
   userEvent.click(schemaSelect);
   userEvent.click(screen.getAllByText('public')[1]);
+  await waitFor(() => expect(fetchMock.calls(tablesEndpoint).length).toBe(1));
+  userEvent.click(tableSelect);
 
   await waitFor(() => {
-    expect(screen.getByText('Sheet1')).toBeInTheDocument();
-    expect(screen.getByText('Sheet2')).toBeInTheDocument();
-    expect(screen.getByText('Sheet3')).toBeInTheDocument();
+    expect(
+      screen.queryByRole('option', {
+        name: /Sheet1/i,
+      }),
+    ).toBeInTheDocument();
+    expect(
+      screen.queryByRole('option', {
+        name: /Sheet2/i,
+      }),
+    ).toBeInTheDocument();
   });
 
-  userEvent.type(screen.getByRole('textbox'), 'Sheet2');
+  userEvent.type(tableSelect, 'Sheet3');
 
   await waitFor(() => {
-    expect(screen.queryByText('Sheet1')).not.toBeInTheDocument();
-    expect(screen.getByText('Sheet2')).toBeInTheDocument();
-    expect(screen.queryByText('Sheet3')).not.toBeInTheDocument();
+    expect(
+      screen.queryByRole('option', { name: /Sheet1/i }),
+    ).not.toBeInTheDocument();
+    expect(
+      screen.queryByRole('option', { name: /Sheet2/i }),
+    ).not.toBeInTheDocument();
+    expect(
+      screen.queryByRole('option', {
+        name: /Sheet3/i,
+      }),
+    ).toBeInTheDocument();
   });
 });
 
@@ -262,6 +290,9 @@ test('renders a warning icon when a table name has a pre-existing dataset', asyn
   const schemaSelect = screen.getByRole('combobox', {
     name: /select schema or type to search schemas/i,
   });
+  const tableSelect = screen.getByRole('combobox', {
+    name: /select table or type to search tables/i,
+  });
 
   await waitFor(() => expect(schemaSelect).toBeEnabled());
 
@@ -273,11 +304,18 @@ test('renders a warning icon when a table name has a pre-existing dataset', asyn
   // Click 'public' schema to access tables
   userEvent.click(schemaSelect);
   userEvent.click(screen.getAllByText('public')[1]);
+  userEvent.click(tableSelect);
 
   await waitFor(() => {
-    expect(screen.getByText('Sheet2')).toBeInTheDocument();
+    expect(
+      screen.queryByRole('option', {
+        name: /Sheet2/i,
+      }),
+    ).toBeInTheDocument();
   });
 
+  userEvent.type(tableSelect, 'Sheet2');
+
   // Sheet2 should now show the warning icon
-  expect(screen.getByRole('img', { name: 'warning' })).toBeVisible();
+  expect(screen.getByRole('img', { name: 'alert-solid' })).toBeInTheDocument();
 });
diff --git a/superset-frontend/src/features/datasets/AddDataset/LeftPanel/index.tsx b/superset-frontend/src/features/datasets/AddDataset/LeftPanel/index.tsx
index 90ec555833..715bf2deee 100644
--- a/superset-frontend/src/features/datasets/AddDataset/LeftPanel/index.tsx
+++ b/superset-frontend/src/features/datasets/AddDataset/LeftPanel/index.tsx
@@ -16,42 +16,18 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import React, {
-  useEffect,
-  useState,
-  SetStateAction,
-  Dispatch,
-  useCallback,
-} from 'react';
-import rison from 'rison';
-import {
-  SupersetClient,
-  t,
-  styled,
-  css,
-  useTheme,
-  logging,
-} from '@superset-ui/core';
-import { Input } from 'src/components/Input';
-import { Form } from 'src/components/Form';
-import Icons from 'src/components/Icons';
-import { TableOption } from 'src/components/TableSelector';
-import RefreshLabel from 'src/components/RefreshLabel';
-import { Table } from 'src/hooks/apiResources';
-import Loading from 'src/components/Loading';
-import DatabaseSelector, {
-  DatabaseObject,
-} from 'src/components/DatabaseSelector';
-import {
-  EmptyStateMedium,
-  emptyStateComponent,
-} from 'src/components/EmptyState';
+import React, { useEffect, SetStateAction, Dispatch, useCallback } from 'react';
+import { styled, t } from '@superset-ui/core';
+import TableSelector, { TableOption } from 'src/components/TableSelector';
+import { DatabaseObject } from 'src/components/DatabaseSelector';
+import { emptyStateComponent } from 'src/components/EmptyState';
 import { useToasts } from 'src/components/MessageToasts/withToasts';
 import { LocalStorageKeys, getItem } from 'src/utils/localStorageHelpers';
 import {
   DatasetActionType,
   DatasetObject,
 } from 'src/features/datasets/AddDataset/types';
+import { Table } from 'src/hooks/apiResources';
 
 interface LeftPanelProps {
   setDataset: Dispatch<SetStateAction<object>>;
@@ -59,10 +35,6 @@ interface LeftPanelProps {
   datasetNames?: (string | null | undefined)[] | undefined;
 }
 
-const SearchIcon = styled(Icons.Search)`
-  color: ${({ theme }) => theme.colors.grayscale.light1};
-`;
-
 const LeftPanelStyle = styled.div`
   ${({ theme }) => `
     max-width: ${theme.gridUnit * 87.5}px;
@@ -74,14 +46,6 @@ const LeftPanelStyle = styled.div`
       height: auto;
       margin-top: ${theme.gridUnit * 17.5}px;
     }
-    .refresh {
-      position: absolute;
-      top: ${theme.gridUnit * 38.75}px;
-      left: ${theme.gridUnit * 16.75}px;
-      span[role="button"]{
-        font-size: ${theme.gridUnit * 4.25}px;
-      }
-    }
     .section-title {
       margin-top: ${theme.gridUnit * 5.5}px;
       margin-bottom: ${theme.gridUnit * 11}px;
@@ -158,77 +122,28 @@ export default function LeftPanel({
   dataset,
   datasetNames,
 }: LeftPanelProps) {
-  const theme = useTheme();
-
-  const [tableOptions, setTableOptions] = useState<Array<TableOption>>([]);
-  const [resetTables, setResetTables] = useState(false);
-  const [loadTables, setLoadTables] = useState(false);
-  const [searchVal, setSearchVal] = useState('');
-  const [refresh, setRefresh] = useState(false);
-  const [selectedTable, setSelectedTable] = useState<number | null>(null);
-
   const { addDangerToast } = useToasts();
 
   const setDatabase = useCallback(
     (db: Partial<DatabaseObject>) => {
       setDataset({ type: DatasetActionType.selectDatabase, payload: { db } });
-      setSelectedTable(null);
-      setResetTables(true);
     },
     [setDataset],
   );
-
-  const setTable = (tableName: string, index: number) => {
-    setSelectedTable(index);
-    setDataset({
-      type: DatasetActionType.selectTable,
-      payload: { name: 'table_name', value: tableName },
-    });
-  };
-
-  const getTablesList = useCallback(
-    (url: string) => {
-      SupersetClient.get({ url })
-        .then(({ json }) => {
-          const options: TableOption[] = json.result.map((table: Table) => {
-            const option: TableOption = {
-              value: table.value,
-              label: <TableOption table={table} />,
-              text: table.label,
-            };
-
-            return option;
-          });
-
-          setTableOptions(options);
-          setLoadTables(false);
-          setResetTables(false);
-          setRefresh(false);
-        })
-        .catch(error => {
-          addDangerToast(t('There was an error fetching tables'));
-          logging.error(t('There was an error fetching tables'), error);
-        });
-    },
-    [addDangerToast],
-  );
-
   const setSchema = (schema: string) => {
     if (schema) {
       setDataset({
         type: DatasetActionType.selectSchema,
         payload: { name: 'schema', value: schema },
       });
-      setLoadTables(true);
     }
-    setSelectedTable(null);
-    setResetTables(true);
   };
-
-  const encodedSchema = dataset?.schema
-    ? encodeURIComponent(dataset?.schema)
-    : undefined;
-
+  const setTable = (tableName: string) => {
+    setDataset({
+      type: DatasetActionType.selectTable,
+      payload: { name: 'table_name', value: tableName },
+    });
+  };
   useEffect(() => {
     const currentUserSelectedDb = getItem(
       LocalStorageKeys.db,
@@ -239,140 +154,37 @@ export default function LeftPanel({
     }
   }, [setDatabase]);
 
-  useEffect(() => {
-    if (loadTables) {
-      const params = rison.encode({
-        force: refresh,
-        schema_name: encodedSchema,
-      });
-
-      const endpoint = `/api/v1/database/${dataset?.db?.id}/tables/?q=${params}`;
-      getTablesList(endpoint);
-    }
-  }, [loadTables, dataset?.db?.id, encodedSchema, getTablesList, refresh]);
-
-  useEffect(() => {
-    if (resetTables) {
-      setTableOptions([]);
-      setResetTables(false);
-    }
-  }, [resetTables]);
-
-  const filteredOptions = tableOptions.filter(option =>
-    option?.value?.toLowerCase().includes(searchVal.toLowerCase()),
-  );
-
-  const Loader = (inline: string) => (
-    <div className="loading-container">
-      <Loading position="inline" />
-      <p>{inline}</p>
-    </div>
+  const customTableOptionLabelRenderer = useCallback(
+    (table: Table) => (
+      <TableOption
+        table={
+          datasetNames?.includes(table.value)
+            ? {
+                ...table,
+                extra: {
+                  warning_markdown: t('This table already has a dataset'),
+                },
+              }
+            : table
+        }
+      />
+    ),
+    [datasetNames],
   );
 
-  const SELECT_DATABASE_AND_SCHEMA_TEXT = t('Select database & schema');
-  const TABLE_LOADING_TEXT = t('Table loading');
-  const NO_TABLES_FOUND_TITLE = t('No database tables found');
-  const NO_TABLES_FOUND_DESCRIPTION = t('Try selecting a different schema');
-  const SELECT_DATABASE_TABLE_TEXT = t('Select database table');
-  const REFRESH_TABLE_LIST_TOOLTIP = t('Refresh table list');
-  const REFRESH_TABLES_TEXT = t('Refresh tables');
-  const SEARCH_TABLES_PLACEHOLDER_TEXT = t('Search tables');
-
-  const optionsList = document.getElementsByClassName('options-list');
-  const scrollableOptionsList =
-    optionsList[0]?.scrollHeight > optionsList[0]?.clientHeight;
-  const [emptyResultsWithSearch, setEmptyResultsWithSearch] = useState(false);
-
-  const onEmptyResults = (searchText?: string) => {
-    setEmptyResultsWithSearch(!!searchText);
-  };
-
   return (
     <LeftPanelStyle>
-      <p className="section-title db-schema">
-        {SELECT_DATABASE_AND_SCHEMA_TEXT}
-      </p>
-      <DatabaseSelector
-        db={dataset?.db}
+      <TableSelector
+        database={dataset?.db}
         handleError={addDangerToast}
+        emptyState={emptyStateComponent(false)}
         onDbChange={setDatabase}
         onSchemaChange={setSchema}
-        emptyState={emptyStateComponent(emptyResultsWithSearch)}
-        onEmptyResults={onEmptyResults}
+        onTableSelectChange={setTable}
+        sqlLabMode={false}
+        customTableOptionLabelRenderer={customTableOptionLabelRenderer}
+        {...(dataset?.schema && { schema: dataset.schema })}
       />
-      {loadTables && !refresh && Loader(TABLE_LOADING_TEXT)}
-      {dataset?.schema && !loadTables && !tableOptions.length && !searchVal && (
-        <div className="emptystate">
-          <EmptyStateMedium
-            image="empty-table.svg"
-            title={NO_TABLES_FOUND_TITLE}
-            description={NO_TABLES_FOUND_DESCRIPTION}
-          />
-        </div>
-      )}
-
-      {dataset?.schema && (tableOptions.length > 0 || searchVal.length > 0) && (
-        <>
-          <Form>
-            <p className="table-title">{SELECT_DATABASE_TABLE_TEXT}</p>
-            <RefreshLabel
-              onClick={() => {
-                setLoadTables(true);
-                setRefresh(true);
-              }}
-              tooltipContent={REFRESH_TABLE_LIST_TOOLTIP}
-            />
-            {refresh && Loader(REFRESH_TABLES_TEXT)}
-            {!refresh && (
-              <Input
-                value={searchVal}
-                prefix={<SearchIcon iconSize="l" />}
-                onChange={evt => {
-                  setSearchVal(evt.target.value);
-                }}
-                className="table-form"
-                placeholder={SEARCH_TABLES_PLACEHOLDER_TEXT}
-                allowClear
-              />
-            )}
-          </Form>
-          <div className="options-list" data-test="options-list">
-            {!refresh &&
-              filteredOptions.map((option, i) => (
-                <div
-                  className={
-                    selectedTable === i
-                      ? scrollableOptionsList
-                        ? 'options-highlighted'
-                        : 'options-highlighted no-scrollbar'
-                      : scrollableOptionsList
-                      ? 'options'
-                      : 'options no-scrollbar'
-                  }
-                  key={i}
-                  role="button"
-                  tabIndex={0}
-                  onClick={() => setTable(option.value, i)}
-                >
-                  {option.label}
-                  {datasetNames?.includes(option.value) && (
-                    <Icons.Warning
-                      iconColor={
-                        selectedTable === i
-                          ? theme.colors.grayscale.light5
-                          : theme.colors.info.base
-                      }
-                      iconSize="m"
-                      css={css`
-                        margin-right: ${theme.gridUnit * 2}px;
-                      `}
-                    />
-                  )}
-                </div>
-              ))}
-          </div>
-        </>
-      )}
     </LeftPanelStyle>
   );
 }
diff --git a/superset-frontend/src/features/datasets/DatasetLayout/DatasetLayout.test.tsx b/superset-frontend/src/features/datasets/DatasetLayout/DatasetLayout.test.tsx
index a851b2b3bc..66cbf6f0c4 100644
--- a/superset-frontend/src/features/datasets/DatasetLayout/DatasetLayout.test.tsx
+++ b/superset-frontend/src/features/datasets/DatasetLayout/DatasetLayout.test.tsx
@@ -59,7 +59,7 @@ describe('DatasetLayout', () => {
     );
 
     expect(
-      await screen.findByText(/select database & schema/i),
+      await screen.findByText(/Select database or type to search databases/i),
     ).toBeInTheDocument();
     expect(LeftPanel).toBeTruthy();
   });
diff --git a/superset-frontend/src/features/datasets/hooks/useDatasetLists.ts b/superset-frontend/src/features/datasets/hooks/useDatasetLists.ts
index 1c4b00df2d..373d98946f 100644
--- a/superset-frontend/src/features/datasets/hooks/useDatasetLists.ts
+++ b/superset-frontend/src/features/datasets/hooks/useDatasetLists.ts
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { useState, useEffect, useCallback } from 'react';
+import { useState, useEffect, useCallback, useMemo } from 'react';
 import { SupersetClient, logging, t } from '@superset-ui/core';
 import rison from 'rison';
 import { addDangerToast } from 'src/components/MessageToasts/actions';
@@ -83,7 +83,10 @@ const useDatasetsList = (
     }
   }, [db?.id, schema, encodedSchema, getDatasetsList]);
 
-  const datasetNames = datasets?.map(dataset => dataset.table_name);
+  const datasetNames = useMemo(
+    () => datasets?.map(dataset => dataset.table_name),
+    [datasets],
+  );
 
   return { datasets, datasetNames };
 };