mirror of
https://github.com/apache/superset.git
synced 2026-04-19 16:14:52 +00:00
fix(datasets): Replace left panel layout by TableSelector (#24599)
Co-authored-by: Justin Park <justinpark@apache.org>
This commit is contained in:
@@ -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(() => {
|
||||
|
||||
@@ -41,6 +41,9 @@ export const Tooltip = (props: TooltipProps) => {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
.ant-tooltip-inner > p {
|
||||
margin: 0;
|
||||
}
|
||||
`}
|
||||
/>
|
||||
<AntdTooltip
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
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',
|
||||
},
|
||||
{
|
||||
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,
|
||||
},
|
||||
],
|
||||
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(tablesEndpoint, {
|
||||
count: 3,
|
||||
result: [
|
||||
{ value: 'Sheet1', type: 'table', extra: null },
|
||||
{ value: 'Sheet2', type: 'table', extra: null },
|
||||
{ value: 'Sheet3', type: 'table', extra: null },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
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 },
|
||||
],
|
||||
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();
|
||||
});
|
||||
|
||||
@@ -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 customTableOptionLabelRenderer = useCallback(
|
||||
(table: Table) => (
|
||||
<TableOption
|
||||
table={
|
||||
datasetNames?.includes(table.value)
|
||||
? {
|
||||
...table,
|
||||
extra: {
|
||||
warning_markdown: t('This table already has a dataset'),
|
||||
},
|
||||
}
|
||||
: table
|
||||
}
|
||||
/>
|
||||
),
|
||||
[datasetNames],
|
||||
);
|
||||
|
||||
const Loader = (inline: string) => (
|
||||
<div className="loading-container">
|
||||
<Loading position="inline" />
|
||||
<p>{inline}</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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 };
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user