diff --git a/superset-frontend/src/components/TableSelector/index.tsx b/superset-frontend/src/components/TableSelector/index.tsx index 7c2a809e5cf36..f36056a4a218b 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 }) => { )} {value} @@ -164,6 +166,7 @@ const TableSelector: FunctionComponent = ({ tableSelectMode = 'single', tableValue = undefined, onTableSelectChange, + customTableOptionLabelRenderer, }) => { const { addSuccessToast } = useToasts(); const [currentSchema, setCurrentSchema] = useState( @@ -203,9 +206,12 @@ const TableSelector: FunctionComponent = ({ value: table.value, label: , 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 64af6b06a0ef7..8237356690bf8 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; + } `} /> ); 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 604e3e9d9e7eb..5156073281e8a 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(, { 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(, { 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 90ec555833ff5..715bf2deeed0a 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>; @@ -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>([]); - const [resetTables, setResetTables] = useState(false); - const [loadTables, setLoadTables] = useState(false); - const [searchVal, setSearchVal] = useState(''); - const [refresh, setRefresh] = useState(false); - const [selectedTable, setSelectedTable] = useState(null); - const { addDangerToast } = useToasts(); const setDatabase = useCallback( (db: Partial) => { 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: , - 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) => ( - - - {inline} - + const customTableOptionLabelRenderer = useCallback( + (table: 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 ( - - {SELECT_DATABASE_AND_SCHEMA_TEXT} - - - {loadTables && !refresh && Loader(TABLE_LOADING_TEXT)} - {dataset?.schema && !loadTables && !tableOptions.length && !searchVal && ( - - - - )} - - {dataset?.schema && (tableOptions.length > 0 || searchVal.length > 0) && ( - <> - - {SELECT_DATABASE_TABLE_TEXT} - { - setLoadTables(true); - setRefresh(true); - }} - tooltipContent={REFRESH_TABLE_LIST_TOOLTIP} - /> - {refresh && Loader(REFRESH_TABLES_TEXT)} - {!refresh && ( - } - onChange={evt => { - setSearchVal(evt.target.value); - }} - className="table-form" - placeholder={SEARCH_TABLES_PLACEHOLDER_TEXT} - allowClear - /> - )} - - - {!refresh && - filteredOptions.map((option, i) => ( - setTable(option.value, i)} - > - {option.label} - {datasetNames?.includes(option.value) && ( - - )} - - ))} - - > - )} ); } diff --git a/superset-frontend/src/features/datasets/DatasetLayout/DatasetLayout.test.tsx b/superset-frontend/src/features/datasets/DatasetLayout/DatasetLayout.test.tsx index a851b2b3bc18b..66cbf6f0c44ff 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 1c4b00df2d121..373d98946f8a2 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 }; };
{inline}
- {SELECT_DATABASE_AND_SCHEMA_TEXT} -
{SELECT_DATABASE_TABLE_TEXT}