fix(sql lab): table selector should display all the selected tables (#19257)

* fix: table Selector should clear once selected

* Multi select

* Add tests

* refactor

* PR comments
This commit is contained in:
Diego Medina 2022-04-13 10:42:12 -04:00 committed by GitHub
parent de9fb2109d
commit 26a0f05759
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 189 additions and 37 deletions

View File

@ -16,12 +16,12 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * 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 Button from 'src/components/Button';
import { t, styled, css, SupersetTheme } from '@superset-ui/core'; import { t, styled, css, SupersetTheme } from '@superset-ui/core';
import Collapse from 'src/components/Collapse'; import Collapse from 'src/components/Collapse';
import Icons from 'src/components/Icons'; 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 { IconTooltip } from 'src/components/IconTooltip';
import { QueryEditor } from 'src/SqlLab/types'; import { QueryEditor } from 'src/SqlLab/types';
import { DatabaseObject } from 'src/components/DatabaseSelector'; import { DatabaseObject } from 'src/components/DatabaseSelector';
@ -101,10 +101,32 @@ export default function SqlEditorLeftBar({
actions.queryEditorSetFunctionNames(queryEditor, dbId); actions.queryEditorSetFunctionNames(queryEditor, dbId);
}; };
const onTableChange = (tableName: string, schemaName: string) => { const selectedTableNames = useMemo(
if (tableName && schemaName) { () => tables?.map(table => table.name) || [],
actions.addTable(queryEditor, database, tableName, schemaName); [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[]) => { const onToggleTable = (updatedTables: string[]) => {
@ -162,16 +184,17 @@ export default function SqlEditorLeftBar({
return ( return (
<div className="SqlEditorLeftBar"> <div className="SqlEditorLeftBar">
<TableSelector <TableSelectorMultiple
database={database} database={database}
getDbList={actions.setDatabases} getDbList={actions.setDatabases}
handleError={actions.addDangerToast} handleError={actions.addDangerToast}
onDbChange={onDbChange} onDbChange={onDbChange}
onSchemaChange={handleSchemaChange} onSchemaChange={handleSchemaChange}
onSchemasLoad={actions.queryEditorSetSchemaOptions} onSchemasLoad={actions.queryEditorSetSchemaOptions}
onTableChange={onTableChange} onTableSelectChange={onTablesChange}
onTablesLoad={handleTablesLoad} onTablesLoad={handleTablesLoad}
schema={queryEditor.schema} schema={queryEditor.schema}
tableValue={selectedTableNames}
sqlLabMode sqlLabMode
/> />
<div className="divider" /> <div className="divider" />

View File

@ -1008,7 +1008,7 @@ class DatasourceEditor extends React.PureComponent {
handleError={this.props.addDangerToast} handleError={this.props.addDangerToast}
schema={datasource.schema} schema={datasource.schema}
sqlLabMode={false} sqlLabMode={false}
tableName={datasource.table_name} tableValue={datasource.table_name}
onSchemaChange={ onSchemaChange={
this.state.isEditMode this.state.isEditMode
? schema => ? schema =>
@ -1024,7 +1024,7 @@ class DatasourceEditor extends React.PureComponent {
) )
: undefined : undefined
} }
onTableChange={ onTableSelectChange={
this.state.isEditMode this.state.isEditMode
? table => ? table =>
this.onDatasourcePropChange('table_name', table) this.onDatasourcePropChange('table_name', table)

View File

@ -18,11 +18,11 @@
*/ */
import React from 'react'; 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 { SupersetClient } from '@superset-ui/core';
import { act } from 'react-dom/test-utils'; import { act } from 'react-dom/test-utils';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import TableSelector from '.'; import TableSelector, { TableSelectorMultiple } from '.';
const SupersetClientGet = jest.spyOn(SupersetClient, 'get'); const SupersetClientGet = jest.spyOn(SupersetClient, 'get');
@ -55,10 +55,17 @@ const getTableMockFunction = async () =>
options: [ options: [
{ label: 'table_a', value: 'table_a' }, { label: 'table_a', value: 'table_a' },
{ label: 'table_b', value: 'table_b' }, { label: 'table_b', value: 'table_b' },
{ label: 'table_c', value: 'table_c' },
{ label: 'table_d', value: 'table_d' },
], ],
}, },
} as any); } as any);
const getSelectItemContainer = (select: HTMLElement) =>
select.parentElement?.parentElement?.getElementsByClassName(
'ant-select-selection-item',
);
test('renders with default props', async () => { test('renders with default props', async () => {
SupersetClientGet.mockImplementation(getTableMockFunction); SupersetClientGet.mockImplementation(getTableMockFunction);
@ -145,6 +152,96 @@ test('table options are notified after schema selection', async () => {
expect(callback).toHaveBeenCalledWith([ expect(callback).toHaveBeenCalledWith([
{ label: 'table_a', value: 'table_a' }, { label: 'table_a', value: 'table_a' },
{ label: 'table_b', value: 'table_b' }, { 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(<TableSelector {...props} />, { 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(<TableSelectorMultiple {...props} />, { 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();
});

View File

@ -23,6 +23,8 @@ import React, {
useMemo, useMemo,
useEffect, useEffect,
} from 'react'; } from 'react';
import { SelectValue } from 'antd/lib/select';
import { styled, SupersetClient, t } from '@superset-ui/core'; import { styled, SupersetClient, t } from '@superset-ui/core';
import { Select } from 'src/components'; import { Select } from 'src/components';
import { FormLabel } from 'src/components/Form'; import { FormLabel } from 'src/components/Form';
@ -87,12 +89,13 @@ interface TableSelectorProps {
onDbChange?: (db: DatabaseObject) => void; onDbChange?: (db: DatabaseObject) => void;
onSchemaChange?: (schema?: string) => void; onSchemaChange?: (schema?: string) => void;
onSchemasLoad?: () => void; onSchemasLoad?: () => void;
onTableChange?: (tableName?: string, schema?: string) => void;
onTablesLoad?: (options: Array<any>) => void; onTablesLoad?: (options: Array<any>) => void;
readOnly?: boolean; readOnly?: boolean;
schema?: string; schema?: string;
sqlLabMode?: boolean; sqlLabMode?: boolean;
tableName?: string; tableValue?: string | string[];
onTableSelectChange?: (value?: string | string[], schema?: string) => void;
tableSelectMode?: 'single' | 'multiple';
} }
interface Table { interface Table {
@ -150,12 +153,13 @@ const TableSelector: FunctionComponent<TableSelectorProps> = ({
onDbChange, onDbChange,
onSchemaChange, onSchemaChange,
onSchemasLoad, onSchemasLoad,
onTableChange,
onTablesLoad, onTablesLoad,
readOnly = false, readOnly = false,
schema, schema,
sqlLabMode = true, sqlLabMode = true,
tableName, tableSelectMode = 'single',
tableValue = undefined,
onTableSelectChange,
}) => { }) => {
const [currentDatabase, setCurrentDatabase] = useState< const [currentDatabase, setCurrentDatabase] = useState<
DatabaseObject | undefined DatabaseObject | undefined
@ -163,11 +167,14 @@ const TableSelector: FunctionComponent<TableSelectorProps> = ({
const [currentSchema, setCurrentSchema] = useState<string | undefined>( const [currentSchema, setCurrentSchema] = useState<string | undefined>(
schema, schema,
); );
const [currentTable, setCurrentTable] = useState<TableOption | undefined>();
const [tableOptions, setTableOptions] = useState<TableOption[]>([]);
const [tableSelectValue, setTableSelectValue] = useState<
SelectValue | undefined
>(undefined);
const [refresh, setRefresh] = useState(0); const [refresh, setRefresh] = useState(0);
const [previousRefresh, setPreviousRefresh] = useState(0); const [previousRefresh, setPreviousRefresh] = useState(0);
const [loadingTables, setLoadingTables] = useState(false); const [loadingTables, setLoadingTables] = useState(false);
const [tableOptions, setTableOptions] = useState<TableOption[]>([]);
const { addSuccessToast } = useToasts(); const { addSuccessToast } = useToasts();
useEffect(() => { useEffect(() => {
@ -175,9 +182,23 @@ const TableSelector: FunctionComponent<TableSelectorProps> = ({
if (database === undefined) { if (database === undefined) {
setCurrentDatabase(undefined); setCurrentDatabase(undefined);
setCurrentSchema(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(() => { useEffect(() => {
if (currentDatabase && currentSchema) { if (currentDatabase && currentSchema) {
@ -195,23 +216,18 @@ const TableSelector: FunctionComponent<TableSelectorProps> = ({
SupersetClient.get({ endpoint }) SupersetClient.get({ endpoint })
.then(({ json }) => { .then(({ json }) => {
const options: TableOption[] = []; const options: TableOption[] = json.options.map((table: Table) => {
let currentTable; const option: TableOption = {
json.options.forEach((table: Table) => {
const option = {
value: table.value, value: table.value,
label: <TableOption table={table} />, label: <TableOption table={table} />,
text: table.label, text: table.label,
}; };
options.push(option);
if (table.label === tableName) { return option;
currentTable = option;
}
}); });
onTablesLoad?.(json.options); onTablesLoad?.(json.options);
setTableOptions(options); setTableOptions(options);
setCurrentTable(currentTable);
setLoadingTables(false); setLoadingTables(false);
if (forceRefresh) addSuccessToast('List updated'); if (forceRefresh) addSuccessToast('List updated');
}) })
@ -223,7 +239,7 @@ const TableSelector: FunctionComponent<TableSelectorProps> = ({
// We are using the refresh state to re-trigger the query // We are using the refresh state to re-trigger the query
// previousRefresh should be out of dependencies array // previousRefresh should be out of dependencies array
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentDatabase, currentSchema, onTablesLoad, refresh]); }, [currentDatabase, currentSchema, onTablesLoad, setTableOptions, refresh]);
function renderSelectRow(select: ReactNode, refreshBtn: ReactNode) { function renderSelectRow(select: ReactNode, refreshBtn: ReactNode) {
return ( return (
@ -234,10 +250,18 @@ const TableSelector: FunctionComponent<TableSelectorProps> = ({
); );
} }
const internalTableChange = (table?: TableOption) => { const internalTableChange = (
setCurrentTable(table); selectedOptions: TableOption | TableOption[] | undefined,
if (onTableChange && currentSchema) { ) => {
onTableChange(table?.value, currentSchema); if (currentSchema) {
onTableSelectChange?.(
Array.isArray(selectedOptions)
? selectedOptions.map(option => option?.value)
: selectedOptions?.value,
currentSchema,
);
} else {
setTableSelectValue(selectedOptions);
} }
}; };
@ -253,6 +277,7 @@ const TableSelector: FunctionComponent<TableSelectorProps> = ({
if (onSchemaChange) { if (onSchemaChange) {
onSchemaChange(schema); onSchemaChange(schema);
} }
internalTableChange(undefined); internalTableChange(undefined);
}; };
@ -305,11 +330,15 @@ const TableSelector: FunctionComponent<TableSelectorProps> = ({
lazyLoading={false} lazyLoading={false}
loading={loadingTables} loading={loadingTables}
name="select-table" name="select-table"
onChange={(table: TableOption) => internalTableChange(table)} onChange={(options: TableOption | TableOption[]) =>
internalTableChange(options)
}
options={tableOptions} options={tableOptions}
placeholder={t('Select table or type table name')} placeholder={t('Select table or type table name')}
showSearch showSearch
value={currentTable} mode={tableSelectMode}
value={tableSelectValue}
allowClear={tableSelectMode === 'multiple'}
/> />
); );
@ -332,4 +361,7 @@ const TableSelector: FunctionComponent<TableSelectorProps> = ({
); );
}; };
export const TableSelectorMultiple: FunctionComponent<TableSelectorProps> =
props => <TableSelector tableSelectMode="multiple" {...props} />;
export default TableSelector; export default TableSelector;

View File

@ -126,10 +126,10 @@ const DatasetModal: FunctionComponent<DatasetModalProps> = ({
formMode formMode
database={currentDatabase} database={currentDatabase}
schema={currentSchema} schema={currentSchema}
tableName={currentTableName} tableValue={currentTableName}
onDbChange={onDbChange} onDbChange={onDbChange}
onSchemaChange={onSchemaChange} onSchemaChange={onSchemaChange}
onTableChange={onTableChange} onTableSelectChange={onTableChange}
handleError={addDangerToast} handleError={addDangerToast}
/> />
</TableSelectorContainer> </TableSelectorContainer>