diff --git a/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/index.tsx b/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/index.tsx
index f9e8c2da9f98f..a50e3a3f62437 100644
--- a/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/index.tsx
+++ b/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/index.tsx
@@ -16,12 +16,12 @@
* specific language governing permissions and limitations
* under the License.
*/
-import React, { useEffect, useRef, useCallback } from 'react';
+import React, { useEffect, useRef, useCallback, useMemo } from 'react';
import Button from 'src/components/Button';
import { t, styled, css, SupersetTheme } from '@superset-ui/core';
import Collapse from 'src/components/Collapse';
import Icons from 'src/components/Icons';
-import TableSelector from 'src/components/TableSelector';
+import { TableSelectorMultiple } from 'src/components/TableSelector';
import { IconTooltip } from 'src/components/IconTooltip';
import { QueryEditor } from 'src/SqlLab/types';
import { DatabaseObject } from 'src/components/DatabaseSelector';
@@ -101,10 +101,32 @@ export default function SqlEditorLeftBar({
actions.queryEditorSetFunctionNames(queryEditor, dbId);
};
- const onTableChange = (tableName: string, schemaName: string) => {
- if (tableName && schemaName) {
- actions.addTable(queryEditor, database, tableName, schemaName);
+ const selectedTableNames = useMemo(
+ () => tables?.map(table => table.name) || [],
+ [tables],
+ );
+
+ const onTablesChange = (tableNames: string[], schemaName: string) => {
+ if (!schemaName) {
+ return;
}
+
+ const currentTables = [...tables];
+ const tablesToAdd = tableNames.filter(name => {
+ const index = currentTables.findIndex(table => table.name === name);
+ if (index >= 0) {
+ currentTables.splice(index, 1);
+ return false;
+ }
+
+ return true;
+ });
+
+ tablesToAdd.forEach(tableName =>
+ actions.addTable(queryEditor, database, tableName, schemaName),
+ );
+
+ currentTables.forEach(table => actions.removeTable(table));
};
const onToggleTable = (updatedTables: string[]) => {
@@ -162,16 +184,17 @@ export default function SqlEditorLeftBar({
return (
-
diff --git a/superset-frontend/src/components/Datasource/DatasourceEditor.jsx b/superset-frontend/src/components/Datasource/DatasourceEditor.jsx
index 1b6de1c7a90df..9d208b7da0319 100644
--- a/superset-frontend/src/components/Datasource/DatasourceEditor.jsx
+++ b/superset-frontend/src/components/Datasource/DatasourceEditor.jsx
@@ -1008,7 +1008,7 @@ class DatasourceEditor extends React.PureComponent {
handleError={this.props.addDangerToast}
schema={datasource.schema}
sqlLabMode={false}
- tableName={datasource.table_name}
+ tableValue={datasource.table_name}
onSchemaChange={
this.state.isEditMode
? schema =>
@@ -1024,7 +1024,7 @@ class DatasourceEditor extends React.PureComponent {
)
: undefined
}
- onTableChange={
+ onTableSelectChange={
this.state.isEditMode
? table =>
this.onDatasourcePropChange('table_name', table)
diff --git a/superset-frontend/src/components/TableSelector/TableSelector.test.tsx b/superset-frontend/src/components/TableSelector/TableSelector.test.tsx
index 013e937edeb41..32d84c008605c 100644
--- a/superset-frontend/src/components/TableSelector/TableSelector.test.tsx
+++ b/superset-frontend/src/components/TableSelector/TableSelector.test.tsx
@@ -18,11 +18,11 @@
*/
import React from 'react';
-import { render, screen, waitFor } from 'spec/helpers/testing-library';
+import { render, screen, waitFor, within } from 'spec/helpers/testing-library';
import { SupersetClient } from '@superset-ui/core';
import { act } from 'react-dom/test-utils';
import userEvent from '@testing-library/user-event';
-import TableSelector from '.';
+import TableSelector, { TableSelectorMultiple } from '.';
const SupersetClientGet = jest.spyOn(SupersetClient, 'get');
@@ -55,10 +55,17 @@ const getTableMockFunction = async () =>
options: [
{ label: 'table_a', value: 'table_a' },
{ label: 'table_b', value: 'table_b' },
+ { label: 'table_c', value: 'table_c' },
+ { label: 'table_d', value: 'table_d' },
],
},
} as any);
+const getSelectItemContainer = (select: HTMLElement) =>
+ select.parentElement?.parentElement?.getElementsByClassName(
+ 'ant-select-selection-item',
+ );
+
test('renders with default props', async () => {
SupersetClientGet.mockImplementation(getTableMockFunction);
@@ -145,6 +152,96 @@ test('table options are notified after schema selection', async () => {
expect(callback).toHaveBeenCalledWith([
{ label: 'table_a', value: 'table_a' },
{ label: 'table_b', value: 'table_b' },
+ { label: 'table_c', value: 'table_c' },
+ { label: 'table_d', value: 'table_d' },
]);
});
});
+
+test('table select retain value if not in SQL Lab mode', async () => {
+ SupersetClientGet.mockImplementation(getTableMockFunction);
+
+ const callback = jest.fn();
+ const props = createProps({
+ onTableSelectChange: callback,
+ sqlLabMode: false,
+ });
+
+ render(
, { useRedux: true });
+
+ const tableSelect = screen.getByRole('combobox', {
+ name: 'Select table or type table name',
+ });
+
+ expect(screen.queryByText('table_a')).not.toBeInTheDocument();
+ expect(getSelectItemContainer(tableSelect)).toHaveLength(0);
+
+ userEvent.click(tableSelect);
+
+ expect(
+ await screen.findByRole('option', { name: 'table_a' }),
+ ).toBeInTheDocument();
+
+ act(() => {
+ userEvent.click(screen.getAllByText('table_a')[1]);
+ });
+
+ expect(callback).toHaveBeenCalled();
+
+ const selectedValueContainer = getSelectItemContainer(tableSelect);
+
+ expect(selectedValueContainer).toHaveLength(1);
+ expect(
+ await within(selectedValueContainer?.[0] as HTMLElement).findByText(
+ 'table_a',
+ ),
+ ).toBeInTheDocument();
+});
+
+test('table multi select retain all the values selected', async () => {
+ SupersetClientGet.mockImplementation(getTableMockFunction);
+
+ const callback = jest.fn();
+ const props = createProps({
+ onTableSelectChange: callback,
+ });
+
+ render(
, { useRedux: true });
+
+ const tableSelect = screen.getByRole('combobox', {
+ name: 'Select table or type table name',
+ });
+
+ expect(screen.queryByText('table_a')).not.toBeInTheDocument();
+ expect(getSelectItemContainer(tableSelect)).toHaveLength(0);
+
+ userEvent.click(tableSelect);
+
+ expect(
+ await screen.findByRole('option', { name: 'table_a' }),
+ ).toBeInTheDocument();
+
+ act(() => {
+ const item = screen.getAllByText('table_a');
+ userEvent.click(item[item.length - 1]);
+ });
+
+ act(() => {
+ const item = screen.getAllByText('table_c');
+ userEvent.click(item[item.length - 1]);
+ });
+
+ const selectedValueContainer = getSelectItemContainer(tableSelect);
+
+ expect(selectedValueContainer).toHaveLength(2);
+ expect(
+ await within(selectedValueContainer?.[0] as HTMLElement).findByText(
+ 'table_a',
+ ),
+ ).toBeInTheDocument();
+ expect(
+ await within(selectedValueContainer?.[1] as HTMLElement).findByText(
+ 'table_c',
+ ),
+ ).toBeInTheDocument();
+});
diff --git a/superset-frontend/src/components/TableSelector/index.tsx b/superset-frontend/src/components/TableSelector/index.tsx
index 50804f7d920ce..84696f93916f8 100644
--- a/superset-frontend/src/components/TableSelector/index.tsx
+++ b/superset-frontend/src/components/TableSelector/index.tsx
@@ -23,6 +23,8 @@ import React, {
useMemo,
useEffect,
} from 'react';
+import { SelectValue } from 'antd/lib/select';
+
import { styled, SupersetClient, t } from '@superset-ui/core';
import { Select } from 'src/components';
import { FormLabel } from 'src/components/Form';
@@ -87,12 +89,13 @@ interface TableSelectorProps {
onDbChange?: (db: DatabaseObject) => void;
onSchemaChange?: (schema?: string) => void;
onSchemasLoad?: () => void;
- onTableChange?: (tableName?: string, schema?: string) => void;
onTablesLoad?: (options: Array
) => void;
readOnly?: boolean;
schema?: string;
sqlLabMode?: boolean;
- tableName?: string;
+ tableValue?: string | string[];
+ onTableSelectChange?: (value?: string | string[], schema?: string) => void;
+ tableSelectMode?: 'single' | 'multiple';
}
interface Table {
@@ -150,12 +153,13 @@ const TableSelector: FunctionComponent = ({
onDbChange,
onSchemaChange,
onSchemasLoad,
- onTableChange,
onTablesLoad,
readOnly = false,
schema,
sqlLabMode = true,
- tableName,
+ tableSelectMode = 'single',
+ tableValue = undefined,
+ onTableSelectChange,
}) => {
const [currentDatabase, setCurrentDatabase] = useState<
DatabaseObject | undefined
@@ -163,11 +167,14 @@ const TableSelector: FunctionComponent = ({
const [currentSchema, setCurrentSchema] = useState(
schema,
);
- const [currentTable, setCurrentTable] = useState();
+
+ const [tableOptions, setTableOptions] = useState([]);
+ const [tableSelectValue, setTableSelectValue] = useState<
+ SelectValue | undefined
+ >(undefined);
const [refresh, setRefresh] = useState(0);
const [previousRefresh, setPreviousRefresh] = useState(0);
const [loadingTables, setLoadingTables] = useState(false);
- const [tableOptions, setTableOptions] = useState([]);
const { addSuccessToast } = useToasts();
useEffect(() => {
@@ -175,9 +182,23 @@ const TableSelector: FunctionComponent = ({
if (database === undefined) {
setCurrentDatabase(undefined);
setCurrentSchema(undefined);
- setCurrentTable(undefined);
+ setTableSelectValue(undefined);
}
- }, [database]);
+ }, [database, tableSelectMode]);
+
+ useEffect(() => {
+ if (tableSelectMode === 'single') {
+ setTableSelectValue(
+ tableOptions.find(option => option.value === tableValue),
+ );
+ } else {
+ setTableSelectValue(
+ tableOptions?.filter(
+ option => option && tableValue?.includes(option.value),
+ ) || [],
+ );
+ }
+ }, [tableOptions, tableValue, tableSelectMode]);
useEffect(() => {
if (currentDatabase && currentSchema) {
@@ -195,23 +216,18 @@ const TableSelector: FunctionComponent = ({
SupersetClient.get({ endpoint })
.then(({ json }) => {
- const options: TableOption[] = [];
- let currentTable;
- json.options.forEach((table: Table) => {
- const option = {
+ const options: TableOption[] = json.options.map((table: Table) => {
+ const option: TableOption = {
value: table.value,
label: ,
text: table.label,
};
- options.push(option);
- if (table.label === tableName) {
- currentTable = option;
- }
+
+ return option;
});
onTablesLoad?.(json.options);
setTableOptions(options);
- setCurrentTable(currentTable);
setLoadingTables(false);
if (forceRefresh) addSuccessToast('List updated');
})
@@ -223,7 +239,7 @@ const TableSelector: FunctionComponent = ({
// 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
- }, [currentDatabase, currentSchema, onTablesLoad, refresh]);
+ }, [currentDatabase, currentSchema, onTablesLoad, setTableOptions, refresh]);
function renderSelectRow(select: ReactNode, refreshBtn: ReactNode) {
return (
@@ -234,10 +250,18 @@ const TableSelector: FunctionComponent = ({
);
}
- const internalTableChange = (table?: TableOption) => {
- setCurrentTable(table);
- if (onTableChange && currentSchema) {
- onTableChange(table?.value, currentSchema);
+ const internalTableChange = (
+ selectedOptions: TableOption | TableOption[] | undefined,
+ ) => {
+ if (currentSchema) {
+ onTableSelectChange?.(
+ Array.isArray(selectedOptions)
+ ? selectedOptions.map(option => option?.value)
+ : selectedOptions?.value,
+ currentSchema,
+ );
+ } else {
+ setTableSelectValue(selectedOptions);
}
};
@@ -253,6 +277,7 @@ const TableSelector: FunctionComponent = ({
if (onSchemaChange) {
onSchemaChange(schema);
}
+
internalTableChange(undefined);
};
@@ -305,11 +330,15 @@ const TableSelector: FunctionComponent = ({
lazyLoading={false}
loading={loadingTables}
name="select-table"
- onChange={(table: TableOption) => internalTableChange(table)}
+ onChange={(options: TableOption | TableOption[]) =>
+ internalTableChange(options)
+ }
options={tableOptions}
placeholder={t('Select table or type table name')}
showSearch
- value={currentTable}
+ mode={tableSelectMode}
+ value={tableSelectValue}
+ allowClear={tableSelectMode === 'multiple'}
/>
);
@@ -332,4 +361,7 @@ const TableSelector: FunctionComponent = ({
);
};
+export const TableSelectorMultiple: FunctionComponent =
+ props => ;
+
export default TableSelector;
diff --git a/superset-frontend/src/views/CRUD/data/dataset/AddDatasetModal.tsx b/superset-frontend/src/views/CRUD/data/dataset/AddDatasetModal.tsx
index f3ad4e488c2c4..7e7e7429bddd3 100644
--- a/superset-frontend/src/views/CRUD/data/dataset/AddDatasetModal.tsx
+++ b/superset-frontend/src/views/CRUD/data/dataset/AddDatasetModal.tsx
@@ -126,10 +126,10 @@ const DatasetModal: FunctionComponent = ({
formMode
database={currentDatabase}
schema={currentSchema}
- tableName={currentTableName}
+ tableValue={currentTableName}
onDbChange={onDbChange}
onSchemaChange={onSchemaChange}
- onTableChange={onTableChange}
+ onTableSelectChange={onTableChange}
handleError={addDangerToast}
/>