mirror of
https://github.com/apache/superset.git
synced 2024-09-18 11:39:49 -04:00
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:
parent
de9fb2109d
commit
26a0f05759
@ -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" />
|
||||||
|
@ -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)
|
||||||
|
@ -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();
|
||||||
|
});
|
||||||
|
@ -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;
|
||||||
|
@ -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>
|
||||||
|
Loading…
Reference in New Issue
Block a user