You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by mi...@apache.org on 2021/09/22 10:44:53 UTC
[superset] branch master updated: refactor: Changes the
DatabaseSelector and TableSelector to use the new Select component (#16483)
This is an automated email from the ASF dual-hosted git repository.
michaelsmolina pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/master by this push:
new 596e1cd refactor: Changes the DatabaseSelector and TableSelector to use the new Select component (#16483)
596e1cd is described below
commit 596e1cdf9b9cdcc5ac2d3a9bac579b27b15ecc1c
Author: Michael S. Molina <70...@users.noreply.github.com>
AuthorDate: Wed Sep 22 07:43:46 2021 -0300
refactor: Changes the DatabaseSelector and TableSelector to use the new Select component (#16483)
---
.../javascripts/sqllab/SqlEditorLeftBar_spec.jsx | 10 +-
.../spec/javascripts/sqllab/SqlEditor_spec.jsx | 4 +-
.../src/SqlLab/components/SqlEditorLeftBar.jsx | 9 +-
.../src/components/CertifiedIcon/index.tsx | 9 +-
.../DatabaseSelector/DatabaseSelector.test.tsx | 115 +++--
.../src/components/DatabaseSelector/index.tsx | 367 ++++++++--------
superset-frontend/src/components/Icons/Icon.tsx | 8 +-
.../TableSelector/TableSelector.test.jsx | 291 -------------
.../TableSelector/TableSelector.test.tsx | 91 ++++
.../src/components/TableSelector/index.tsx | 479 +++++++++------------
.../components/WarningIconWithTooltip/index.tsx | 6 +-
.../src/datasource/DatasourceEditor.jsx | 137 +++---
.../controls/DatasourceControl/index.jsx | 5 +-
.../views/CRUD/data/dataset/AddDatasetModal.tsx | 34 +-
.../src/views/CRUD/data/dataset/DatasetList.tsx | 2 +
superset/datasets/api.py | 2 +-
superset/views/core.py | 21 +-
tests/integration_tests/datasets/api_tests.py | 14 +-
18 files changed, 734 insertions(+), 870 deletions(-)
diff --git a/superset-frontend/spec/javascripts/sqllab/SqlEditorLeftBar_spec.jsx b/superset-frontend/spec/javascripts/sqllab/SqlEditorLeftBar_spec.jsx
index 1ba1ac8..4271b59 100644
--- a/superset-frontend/spec/javascripts/sqllab/SqlEditorLeftBar_spec.jsx
+++ b/superset-frontend/spec/javascripts/sqllab/SqlEditorLeftBar_spec.jsx
@@ -81,9 +81,13 @@ describe('Left Panel Expansion', () => {
</Provider>
</ThemeProvider>,
);
- const dbSelect = screen.getByText(/select a database/i);
- const schemaSelect = screen.getByText(/select a schema \(0\)/i);
- const dropdown = screen.getByText(/Select table/i);
+ const dbSelect = screen.getByRole('combobox', {
+ name: 'Select database or type database name',
+ });
+ const schemaSelect = screen.getByRole('combobox', {
+ name: 'Select schema or type schema name',
+ });
+ const dropdown = screen.getByText(/Select table or type table name/i);
const abUser = screen.getByText(/ab_user/i);
expect(dbSelect).toBeInTheDocument();
expect(schemaSelect).toBeInTheDocument();
diff --git a/superset-frontend/spec/javascripts/sqllab/SqlEditor_spec.jsx b/superset-frontend/spec/javascripts/sqllab/SqlEditor_spec.jsx
index b634d2c..d9adfde 100644
--- a/superset-frontend/spec/javascripts/sqllab/SqlEditor_spec.jsx
+++ b/superset-frontend/spec/javascripts/sqllab/SqlEditor_spec.jsx
@@ -36,13 +36,14 @@ import { Dropdown } from 'src/common/components';
import {
queryEditorSetFunctionNames,
queryEditorSetSelectedText,
+ queryEditorSetSchemaOptions,
} from 'src/SqlLab/actions/sqlLab';
import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint';
import { initialState, queries, table } from './fixtures';
const MOCKED_SQL_EDITOR_HEIGHT = 500;
-fetchMock.get('glob:*/api/v1/database/*', {});
+fetchMock.get('glob:*/api/v1/database/*', { result: [] });
const middlewares = [thunk];
const mockStore = configureStore(middlewares);
@@ -53,6 +54,7 @@ describe('SqlEditor', () => {
actions: {
queryEditorSetFunctionNames,
queryEditorSetSelectedText,
+ queryEditorSetSchemaOptions,
addDangerToast: jest.fn(),
},
database: {},
diff --git a/superset-frontend/src/SqlLab/components/SqlEditorLeftBar.jsx b/superset-frontend/src/SqlLab/components/SqlEditorLeftBar.jsx
index 3989a23..02a383b 100644
--- a/superset-frontend/src/SqlLab/components/SqlEditorLeftBar.jsx
+++ b/superset-frontend/src/SqlLab/components/SqlEditorLeftBar.jsx
@@ -91,7 +91,13 @@ export default class SqlEditorLeftBar extends React.PureComponent {
}
onTableChange(tableName, schemaName) {
- this.props.actions.addTable(this.props.queryEditor, tableName, schemaName);
+ if (tableName && schemaName) {
+ this.props.actions.addTable(
+ this.props.queryEditor,
+ tableName,
+ schemaName,
+ );
+ }
}
onToggleTable(tables) {
@@ -171,7 +177,6 @@ export default class SqlEditorLeftBar extends React.PureComponent {
onTablesLoad={this.onTablesLoad}
schema={qe.schema}
sqlLabMode
- tableNameSticky={false}
/>
<div className="divider" />
<StyledScrollbarContainer>
diff --git a/superset-frontend/src/components/CertifiedIcon/index.tsx b/superset-frontend/src/components/CertifiedIcon/index.tsx
index f08e9bf..4aa0dad 100644
--- a/superset-frontend/src/components/CertifiedIcon/index.tsx
+++ b/superset-frontend/src/components/CertifiedIcon/index.tsx
@@ -18,19 +18,19 @@
*/
import React from 'react';
import { t, supersetTheme } from '@superset-ui/core';
-import Icons from 'src/components/Icons';
+import Icons, { IconType } from 'src/components/Icons';
import { Tooltip } from 'src/components/Tooltip';
export interface CertifiedIconProps {
certifiedBy?: string;
details?: string;
- size?: number;
+ size?: IconType['iconSize'];
}
function CertifiedIcon({
certifiedBy,
details,
- size = 24,
+ size = 'l',
}: CertifiedIconProps) {
return (
<Tooltip
@@ -48,8 +48,7 @@ function CertifiedIcon({
>
<Icons.Certified
iconColor={supersetTheme.colors.primary.base}
- height={size}
- width={size}
+ iconSize={size}
/>
</Tooltip>
);
diff --git a/superset-frontend/src/components/DatabaseSelector/DatabaseSelector.test.tsx b/superset-frontend/src/components/DatabaseSelector/DatabaseSelector.test.tsx
index 0d81282..8e96941 100644
--- a/superset-frontend/src/components/DatabaseSelector/DatabaseSelector.test.tsx
+++ b/superset-frontend/src/components/DatabaseSelector/DatabaseSelector.test.tsx
@@ -26,11 +26,11 @@ import DatabaseSelector from '.';
const SupersetClientGet = jest.spyOn(SupersetClient, 'get');
const createProps = () => ({
- dbId: 1,
+ db: { id: 1, database_name: 'test', backend: 'test-postgresql' },
formMode: false,
isDatabaseSelectEnabled: true,
readOnly: false,
- schema: 'public',
+ schema: undefined,
sqlLabMode: true,
getDbList: jest.fn(),
getTableList: jest.fn(),
@@ -57,9 +57,9 @@ beforeEach(() => {
}
return {
json: {
- count: 1,
+ count: 2,
description_columns: {},
- ids: [1],
+ ids: [1, 2],
label_columns: {
allow_csv_upload: 'Allow Csv Upload',
allow_ctas: 'Allow Ctas',
@@ -129,12 +129,32 @@ beforeEach(() => {
changed_on: '2021-03-09T19:02:07.141095',
changed_on_delta_humanized: 'a day ago',
created_by: null,
- database_name: 'examples',
+ 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,
+ 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,
+ },
],
},
} as any;
@@ -153,50 +173,95 @@ test('Refresh should work', async () => {
render(<DatabaseSelector {...props} />);
+ const select = screen.getByRole('combobox', {
+ name: 'Select schema or type schema name',
+ });
+
+ userEvent.click(select);
+
await waitFor(() => {
- expect(SupersetClientGet).toBeCalledTimes(2);
- expect(props.getDbList).toBeCalledTimes(1);
+ expect(SupersetClientGet).toBeCalledTimes(1);
+ expect(props.getDbList).toBeCalledTimes(0);
expect(props.getTableList).toBeCalledTimes(0);
expect(props.handleError).toBeCalledTimes(0);
expect(props.onDbChange).toBeCalledTimes(0);
expect(props.onSchemaChange).toBeCalledTimes(0);
- expect(props.onSchemasLoad).toBeCalledTimes(1);
+ expect(props.onSchemasLoad).toBeCalledTimes(0);
expect(props.onUpdate).toBeCalledTimes(0);
});
- userEvent.click(screen.getByRole('button'));
+ userEvent.click(screen.getByRole('button', { name: 'refresh' }));
await waitFor(() => {
- expect(SupersetClientGet).toBeCalledTimes(3);
- expect(props.getDbList).toBeCalledTimes(1);
+ expect(SupersetClientGet).toBeCalledTimes(2);
+ expect(props.getDbList).toBeCalledTimes(0);
expect(props.getTableList).toBeCalledTimes(0);
expect(props.handleError).toBeCalledTimes(0);
- expect(props.onDbChange).toBeCalledTimes(1);
- expect(props.onSchemaChange).toBeCalledTimes(1);
+ expect(props.onDbChange).toBeCalledTimes(0);
+ expect(props.onSchemaChange).toBeCalledTimes(0);
expect(props.onSchemasLoad).toBeCalledTimes(2);
- expect(props.onUpdate).toBeCalledTimes(1);
+ expect(props.onUpdate).toBeCalledTimes(0);
});
});
test('Should database select display options', async () => {
const props = createProps();
render(<DatabaseSelector {...props} />);
- const selector = await screen.findByText('Database:');
- expect(selector).toBeInTheDocument();
- expect(selector.parentElement).toHaveTextContent(
- 'Database:postgresql examples',
- );
+ const select = screen.getByRole('combobox', {
+ name: 'Select database or type database name',
+ });
+ expect(select).toBeInTheDocument();
+ userEvent.click(select);
+ expect(await screen.findByText('test-mysql')).toBeInTheDocument();
});
test('Should schema select display options', async () => {
const props = createProps();
render(<DatabaseSelector {...props} />);
+ const select = screen.getByRole('combobox', {
+ name: 'Select schema or type schema name',
+ });
+ expect(select).toBeInTheDocument();
+ userEvent.click(select);
+ expect(
+ await screen.findByRole('option', { name: 'public' }),
+ ).toBeInTheDocument();
+ expect(
+ await screen.findByRole('option', { name: 'information_schema' }),
+ ).toBeInTheDocument();
+});
- const selector = await screen.findByText('Schema:');
- expect(selector).toBeInTheDocument();
- expect(selector.parentElement).toHaveTextContent('Schema: public');
-
- userEvent.click(screen.getByRole('button'));
+test('Sends the correct db when changing the database', async () => {
+ const props = createProps();
+ render(<DatabaseSelector {...props} />);
+ const select = screen.getByRole('combobox', {
+ name: 'Select database or type database name',
+ });
+ expect(select).toBeInTheDocument();
+ userEvent.click(select);
+ userEvent.click(await screen.findByText('test-mysql'));
+ await waitFor(() =>
+ expect(props.onDbChange).toHaveBeenCalledWith(
+ expect.objectContaining({
+ id: 2,
+ database_name: 'test-mysql',
+ backend: 'mysql',
+ }),
+ ),
+ );
+});
- expect(await screen.findByText('Select a schema (2)')).toBeInTheDocument();
+test('Sends the correct schema when changing the schema', async () => {
+ const props = createProps();
+ render(<DatabaseSelector {...props} />);
+ const select = screen.getByRole('combobox', {
+ name: 'Select schema or type schema name',
+ });
+ expect(select).toBeInTheDocument();
+ userEvent.click(select);
+ const schemaOption = await screen.findAllByText('information_schema');
+ userEvent.click(schemaOption[1]);
+ await waitFor(() =>
+ expect(props.onSchemaChange).toHaveBeenCalledWith('information_schema'),
+ );
});
diff --git a/superset-frontend/src/components/DatabaseSelector/index.tsx b/superset-frontend/src/components/DatabaseSelector/index.tsx
index 0282e4a..12f7855 100644
--- a/superset-frontend/src/components/DatabaseSelector/index.tsx
+++ b/superset-frontend/src/components/DatabaseSelector/index.tsx
@@ -16,80 +16,94 @@
* specific language governing permissions and limitations
* under the License.
*/
-import React, { ReactNode, useEffect, useState } from 'react';
+import React, { ReactNode, useState, useMemo, useEffect } from 'react';
import { styled, SupersetClient, t } from '@superset-ui/core';
import rison from 'rison';
-import { Select } from 'src/components/Select';
+import { Select } from 'src/components';
import Label from 'src/components/Label';
+import { FormLabel } from 'src/components/Form';
import RefreshLabel from 'src/components/RefreshLabel';
-import SupersetAsyncSelect from 'src/components/AsyncSelect';
-
-const FieldTitle = styled.p`
- color: ${({ theme }) => theme.colors.secondary.light2};
- font-size: ${({ theme }) => theme.typography.sizes.s}px;
- margin: 20px 0 10px 0;
- text-transform: uppercase;
-`;
const DatabaseSelectorWrapper = styled.div`
- .fa-refresh {
- padding-left: 9px;
- }
+ ${({ theme }) => `
+ .refresh {
+ display: flex;
+ align-items: center;
+ width: 30px;
+ margin-left: ${theme.gridUnit}px;
+ margin-top: ${theme.gridUnit * 5}px;
+ }
- .refresh-col {
- display: flex;
- align-items: center;
- width: 30px;
- margin-left: ${({ theme }) => theme.gridUnit}px;
- }
+ .section {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ }
- .section {
- padding-bottom: 5px;
- display: flex;
- flex-direction: row;
- }
+ .select {
+ flex: 1;
+ }
- .select {
- flex-grow: 1;
- }
+ & > div {
+ margin-bottom: ${theme.gridUnit * 4}px;
+ }
+ `}
`;
-const DatabaseOption = styled.span`
- display: inline-flex;
+const LabelStyle = styled.div`
+ display: flex;
+ flex-direction: row;
align-items: center;
+ margin-left: ${({ theme }) => theme.gridUnit - 2}px;
`;
+type DatabaseValue = {
+ label: React.ReactNode;
+ value: number;
+ id: number;
+ database_name: string;
+ backend: string;
+};
+
+type SchemaValue = { label: string; value: string };
+
interface DatabaseSelectorProps {
- dbId: number;
+ db?: { id: number; database_name: string; backend: string };
formMode?: boolean;
getDbList?: (arg0: any) => {};
- getTableList?: (dbId: number, schema: string, force: boolean) => {};
handleError: (msg: string) => void;
isDatabaseSelectEnabled?: boolean;
- onDbChange?: (db: any) => void;
- onSchemaChange?: (arg0?: any) => {};
+ onDbChange?: (db: {
+ id: number;
+ database_name: string;
+ backend: string;
+ }) => void;
+ onSchemaChange?: (schema?: string) => void;
onSchemasLoad?: (schemas: Array<object>) => void;
readOnly?: boolean;
schema?: string;
sqlLabMode?: boolean;
- onUpdate?: ({
- dbId,
- schema,
- }: {
- dbId: number;
- schema?: string;
- tableName?: string;
- }) => void;
}
+const SelectLabel = ({
+ backend,
+ databaseName,
+}: {
+ backend: string;
+ databaseName: string;
+}) => (
+ <LabelStyle>
+ <Label>{backend}</Label>
+ {databaseName}
+ </LabelStyle>
+);
+
export default function DatabaseSelector({
- dbId,
+ db,
formMode = false,
getDbList,
- getTableList,
handleError,
isDatabaseSelectEnabled = true,
- onUpdate,
onDbChange,
onSchemaChange,
onSchemasLoad,
@@ -97,193 +111,188 @@ export default function DatabaseSelector({
schema,
sqlLabMode = false,
}: DatabaseSelectorProps) {
- const [currentDbId, setCurrentDbId] = useState(dbId);
- const [currentSchema, setCurrentSchema] = useState<string | undefined>(
- schema,
+ const [loadingSchemas, setLoadingSchemas] = useState(false);
+ const [schemaOptions, setSchemaOptions] = useState<SchemaValue[]>([]);
+ const [currentDb, setCurrentDb] = useState<DatabaseValue | undefined>(
+ db
+ ? {
+ label: (
+ <SelectLabel backend={db.backend} databaseName={db.database_name} />
+ ),
+ value: db.id,
+ ...db,
+ }
+ : undefined,
+ );
+ const [currentSchema, setCurrentSchema] = useState<SchemaValue | undefined>(
+ schema ? { label: schema, value: schema } : undefined,
);
- const [schemaLoading, setSchemaLoading] = useState(false);
- const [schemaOptions, setSchemaOptions] = useState([]);
+ const [refresh, setRefresh] = useState(0);
- function fetchSchemas(databaseId: number, forceRefresh = false) {
- const actualDbId = databaseId || dbId;
- if (actualDbId) {
- setSchemaLoading(true);
+ const loadDatabases = useMemo(
+ () => async (
+ search: string,
+ page: number,
+ pageSize: number,
+ ): Promise<{
+ data: DatabaseValue[];
+ totalCount: number;
+ }> => {
const queryParams = rison.encode({
- force: Boolean(forceRefresh),
+ order_columns: 'database_name',
+ order_direction: 'asc',
+ page,
+ page_size: pageSize,
+ ...(formMode || !sqlLabMode
+ ? { filters: [{ col: 'database_name', opr: 'ct', value: search }] }
+ : {
+ filters: [
+ { col: 'database_name', opr: 'ct', value: search },
+ {
+ col: 'expose_in_sqllab',
+ opr: 'eq',
+ value: true,
+ },
+ ],
+ }),
});
- const endpoint = `/api/v1/database/${actualDbId}/schemas/?q=${queryParams}`;
- return SupersetClient.get({ endpoint })
- .then(({ json }) => {
- const options = json.result.map((s: string) => ({
- value: s,
- label: s,
- title: s,
- }));
- setSchemaOptions(options);
- setSchemaLoading(false);
+ const endpoint = `/api/v1/database/?q=${queryParams}`;
+ return SupersetClient.get({ endpoint }).then(({ json }) => {
+ const { result } = json;
+ if (getDbList) {
+ getDbList(result);
+ }
+ if (result.length === 0) {
+ handleError(t("It seems you don't have access to any database"));
+ }
+ const options = result.map(
+ (row: { id: number; database_name: string; backend: string }) => ({
+ label: (
+ <SelectLabel
+ backend={row.backend}
+ databaseName={row.database_name}
+ />
+ ),
+ value: row.id,
+ id: row.id,
+ database_name: row.database_name,
+ backend: row.backend,
+ }),
+ );
+ return {
+ data: options,
+ totalCount: options.length,
+ };
+ });
+ },
+ [formMode, getDbList, handleError, sqlLabMode],
+ );
+
+ useEffect(() => {
+ if (currentDb) {
+ setLoadingSchemas(true);
+ const queryParams = rison.encode({ force: refresh > 0 });
+ const endpoint = `/api/v1/database/${currentDb.value}/schemas/?q=${queryParams}`;
+
+ try {
+ // TODO: Would be nice to add pagination in a follow-up. Needs endpoint changes.
+ SupersetClient.get({ endpoint }).then(({ json }) => {
+ const options = json.result
+ .map((s: string) => ({
+ value: s,
+ label: s,
+ title: s,
+ }))
+ .sort((a: { label: string }, b: { label: string }) =>
+ a.label.localeCompare(b.label),
+ );
if (onSchemasLoad) {
onSchemasLoad(options);
}
- })
- .catch(() => {
- setSchemaOptions([]);
- setSchemaLoading(false);
- handleError(t('Error while fetching schema list'));
+ setSchemaOptions(options);
});
+ } finally {
+ setLoadingSchemas(false);
+ }
}
- return Promise.resolve();
- }
+ }, [currentDb, onSchemasLoad, refresh]);
- useEffect(() => {
- if (currentDbId) {
- fetchSchemas(currentDbId);
- }
- }, [currentDbId]);
-
- function onSelectChange({ dbId, schema }: { dbId: number; schema?: string }) {
- setCurrentDbId(dbId);
- setCurrentSchema(schema);
- if (onUpdate) {
- onUpdate({ dbId, schema, tableName: undefined });
- }
- }
-
- function dbMutator(data: any) {
- if (getDbList) {
- getDbList(data.result);
- }
- if (data.result.length === 0) {
- handleError(t("It seems you don't have access to any database"));
+ function changeDataBase(
+ value: { label: string; value: number },
+ database: DatabaseValue,
+ ) {
+ setCurrentDb(database);
+ setCurrentSchema(undefined);
+ if (onDbChange) {
+ onDbChange(database);
}
- return data.result.map((row: any) => ({
- ...row,
- // label is used for the typeahead
- label: `${row.backend} ${row.database_name}`,
- }));
- }
-
- function changeDataBase(db: any, force = false) {
- const dbId = db ? db.id : null;
- setSchemaOptions([]);
if (onSchemaChange) {
- onSchemaChange(null);
+ onSchemaChange(undefined);
}
- if (onDbChange) {
- onDbChange(db);
- }
- fetchSchemas(dbId, force);
- onSelectChange({ dbId, schema: undefined });
}
- function changeSchema(schemaOpt: any, force = false) {
- const schema = schemaOpt ? schemaOpt.value : null;
- if (onSchemaChange) {
- onSchemaChange(schema);
- }
+ function changeSchema(schema: SchemaValue) {
setCurrentSchema(schema);
- onSelectChange({ dbId: currentDbId, schema });
- if (getTableList) {
- getTableList(currentDbId, schema, force);
+ if (onSchemaChange) {
+ onSchemaChange(schema.value);
}
}
- function renderDatabaseOption(db: any) {
- return (
- <DatabaseOption title={db.database_name}>
- <Label type="default">{db.backend}</Label> {db.database_name}
- </DatabaseOption>
- );
- }
-
function renderSelectRow(select: ReactNode, refreshBtn: ReactNode) {
return (
<div className="section">
<span className="select">{select}</span>
- <span className="refresh-col">{refreshBtn}</span>
+ <span className="refresh">{refreshBtn}</span>
</div>
);
}
function renderDatabaseSelect() {
- const queryParams = rison.encode({
- order_columns: 'database_name',
- order_direction: 'asc',
- page: 0,
- page_size: -1,
- ...(formMode || !sqlLabMode
- ? {}
- : {
- filters: [
- {
- col: 'expose_in_sqllab',
- opr: 'eq',
- value: true,
- },
- ],
- }),
- });
-
return renderSelectRow(
- <SupersetAsyncSelect
+ <Select
+ ariaLabel={t('Select database or type database name')}
data-test="select-database"
- dataEndpoint={`/api/v1/database/?q=${queryParams}`}
- onChange={(db: any) => changeDataBase(db)}
- onAsyncError={() =>
- handleError(t('Error while fetching database list'))
- }
- clearable={false}
- value={currentDbId}
- valueKey="id"
- valueRenderer={(db: any) => (
- <div>
- <span className="text-muted m-r-5">{t('Database:')}</span>
- {renderDatabaseOption(db)}
- </div>
- )}
- optionRenderer={renderDatabaseOption}
- mutator={dbMutator}
- placeholder={t('Select a database')}
- autoSelect
- isDisabled={!isDatabaseSelectEnabled || readOnly}
+ header={<FormLabel>{t('Database')}</FormLabel>}
+ onChange={changeDataBase}
+ value={currentDb}
+ placeholder={t('Select database or type database name')}
+ disabled={!isDatabaseSelectEnabled || readOnly}
+ options={loadDatabases}
/>,
null,
);
}
function renderSchemaSelect() {
- const value = schemaOptions.filter(({ value }) => currentSchema === value);
- const refresh = !formMode && !readOnly && (
+ const refreshIcon = !formMode && !readOnly && (
<RefreshLabel
- onClick={() => changeDataBase({ id: dbId }, true)}
+ onClick={() => setRefresh(refresh + 1)}
tooltipContent={t('Force refresh schema list')}
/>
);
return renderSelectRow(
<Select
+ ariaLabel={t('Select schema or type schema name')}
+ disabled={readOnly}
+ header={<FormLabel>{t('Schema')}</FormLabel>}
+ labelInValue
+ lazyLoading={false}
+ loading={loadingSchemas}
name="select-schema"
- placeholder={t('Select a schema (%s)', schemaOptions.length)}
+ placeholder={t('Select schema or type schema name')}
+ onChange={item => changeSchema(item as SchemaValue)}
options={schemaOptions}
- value={value}
- valueRenderer={o => (
- <div>
- <span className="text-muted">{t('Schema:')}</span> {o.label}
- </div>
- )}
- isLoading={schemaLoading}
- autosize={false}
- onChange={item => changeSchema(item)}
- isDisabled={readOnly}
+ showSearch
+ value={currentSchema}
/>,
- refresh,
+ refreshIcon,
);
}
return (
<DatabaseSelectorWrapper data-test="DatabaseSelector">
- {formMode && <FieldTitle>{t('datasource')}</FieldTitle>}
{renderDatabaseSelect()}
- {formMode && <FieldTitle>{t('schema')}</FieldTitle>}
{renderSchemaSelect()}
</DatabaseSelectorWrapper>
);
diff --git a/superset-frontend/src/components/Icons/Icon.tsx b/superset-frontend/src/components/Icons/Icon.tsx
index 9e3d0e1..efb78dc 100644
--- a/superset-frontend/src/components/Icons/Icon.tsx
+++ b/superset-frontend/src/components/Icons/Icon.tsx
@@ -53,15 +53,21 @@ export const Icon = (props: IconProps) => {
const name = fileName.replace('_', '-');
useEffect(() => {
+ let cancelled = false;
async function importIcon(): Promise<void> {
ImportedSVG.current = (
await import(
`!!@svgr/webpack?-svgo,+titleProp,+ref!images/icons/${fileName}.svg`
)
).default;
- setLoaded(true);
+ if (!cancelled) {
+ setLoaded(true);
+ }
}
importIcon();
+ return () => {
+ cancelled = true;
+ };
}, [fileName, ImportedSVG]);
return (
diff --git a/superset-frontend/src/components/TableSelector/TableSelector.test.jsx b/superset-frontend/src/components/TableSelector/TableSelector.test.jsx
deleted file mode 100644
index e1fa66b..0000000
--- a/superset-frontend/src/components/TableSelector/TableSelector.test.jsx
+++ /dev/null
@@ -1,291 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-import React from 'react';
-import configureStore from 'redux-mock-store';
-import { mount } from 'enzyme';
-import { act } from 'react-dom/test-utils';
-import sinon from 'sinon';
-import fetchMock from 'fetch-mock';
-import thunk from 'redux-thunk';
-import { supersetTheme, ThemeProvider } from '@superset-ui/core';
-
-import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint';
-
-import DatabaseSelector from 'src/components/DatabaseSelector';
-import TableSelector from 'src/components/TableSelector';
-import { initialState, tables } from 'spec/javascripts/sqllab/fixtures';
-
-const mockStore = configureStore([thunk]);
-const store = mockStore(initialState);
-
-const FETCH_SCHEMAS_ENDPOINT = 'glob:*/api/v1/database/*/schemas/*';
-const GET_TABLE_ENDPOINT = 'glob:*/superset/tables/1/*/*';
-const GET_TABLE_NAMES_ENDPOINT = 'glob:*/superset/tables/1/main/*';
-
-const mockedProps = {
- clearable: false,
- database: { id: 1, database_name: 'main' },
- dbId: 1,
- formMode: false,
- getDbList: sinon.stub(),
- handleError: sinon.stub(),
- horizontal: false,
- onChange: sinon.stub(),
- onDbChange: sinon.stub(),
- onSchemaChange: sinon.stub(),
- onTableChange: sinon.stub(),
- sqlLabMode: true,
- tableName: '',
- tableNameSticky: true,
-};
-
-const schemaOptions = {
- result: ['main', 'erf', 'superset'],
-};
-const selectedSchema = { label: 'main', title: 'main', value: 'main' };
-const selectedTable = {
- extra: null,
- label: 'birth_names',
- schema: 'main',
- title: 'birth_names',
- type: undefined,
- value: 'birth_names',
-};
-
-async function mountAndWait(props = mockedProps) {
- const mounted = mount(<TableSelector {...props} />, {
- context: { store },
- wrappingComponent: ThemeProvider,
- wrappingComponentProps: { theme: supersetTheme },
- });
- await waitForComponentToPaint(mounted);
-
- return mounted;
-}
-
-describe('TableSelector', () => {
- let wrapper;
-
- beforeEach(async () => {
- fetchMock.reset();
- wrapper = await mountAndWait();
- });
-
- it('renders', () => {
- expect(wrapper.find(TableSelector)).toExist();
- expect(wrapper.find(DatabaseSelector)).toExist();
- });
-
- describe('change database', () => {
- afterEach(fetchMock.resetHistory);
- afterAll(fetchMock.reset);
-
- it('should fetch schemas', async () => {
- fetchMock.get(FETCH_SCHEMAS_ENDPOINT, { overwriteRoutes: true });
- act(() => {
- wrapper.find('[data-test="select-database"]').first().props().onChange({
- id: 1,
- database_name: 'main',
- });
- });
- await waitForComponentToPaint(wrapper);
- expect(fetchMock.calls(FETCH_SCHEMAS_ENDPOINT)).toHaveLength(1);
- });
-
- it('should fetch schema options', async () => {
- fetchMock.get(FETCH_SCHEMAS_ENDPOINT, schemaOptions, {
- overwriteRoutes: true,
- });
- act(() => {
- wrapper.find('[data-test="select-database"]').first().props().onChange({
- id: 1,
- database_name: 'main',
- });
- });
- await waitForComponentToPaint(wrapper);
- wrapper.update();
- expect(fetchMock.calls(FETCH_SCHEMAS_ENDPOINT)).toHaveLength(1);
-
- expect(
- wrapper.find('[name="select-schema"]').first().props().options,
- ).toEqual([
- { value: 'main', label: 'main', title: 'main' },
- { value: 'erf', label: 'erf', title: 'erf' },
- { value: 'superset', label: 'superset', title: 'superset' },
- ]);
- });
-
- it('should clear table options', async () => {
- act(() => {
- wrapper.find('[data-test="select-database"]').first().props().onChange({
- id: 1,
- database_name: 'main',
- });
- });
- await waitForComponentToPaint(wrapper);
- const props = wrapper.find('[name="async-select-table"]').first().props();
- expect(props.isDisabled).toBe(true);
- expect(props.value).toEqual(undefined);
- });
- });
-
- describe('change schema', () => {
- beforeEach(async () => {
- fetchMock.get(FETCH_SCHEMAS_ENDPOINT, schemaOptions, {
- overwriteRoutes: true,
- });
- });
-
- afterEach(fetchMock.resetHistory);
- afterAll(fetchMock.reset);
-
- it('should fetch table', async () => {
- fetchMock.get(GET_TABLE_NAMES_ENDPOINT, { overwriteRoutes: true });
- act(() => {
- wrapper.find('[data-test="select-database"]').first().props().onChange({
- id: 1,
- database_name: 'main',
- });
- });
- await waitForComponentToPaint(wrapper);
- act(() => {
- wrapper
- .find('[name="select-schema"]')
- .first()
- .props()
- .onChange(selectedSchema);
- });
- await waitForComponentToPaint(wrapper);
- expect(fetchMock.calls(GET_TABLE_NAMES_ENDPOINT)).toHaveLength(1);
- });
-
- it('should fetch table options', async () => {
- fetchMock.get(GET_TABLE_NAMES_ENDPOINT, tables, {
- overwriteRoutes: true,
- });
- act(() => {
- wrapper.find('[data-test="select-database"]').first().props().onChange({
- id: 1,
- database_name: 'main',
- });
- });
- await waitForComponentToPaint(wrapper);
- act(() => {
- wrapper
- .find('[name="select-schema"]')
- .first()
- .props()
- .onChange(selectedSchema);
- });
- await waitForComponentToPaint(wrapper);
- expect(
- wrapper.find('[name="select-schema"]').first().props().value[0],
- ).toEqual(selectedSchema);
- expect(fetchMock.calls(GET_TABLE_NAMES_ENDPOINT)).toHaveLength(1);
- const { options } = wrapper.find('[name="select-table"]').first().props();
- expect({ options }).toEqual(tables);
- });
- });
-
- describe('change table', () => {
- beforeEach(async () => {
- fetchMock.get(GET_TABLE_NAMES_ENDPOINT, tables, {
- overwriteRoutes: true,
- });
- });
-
- it('should change table value', async () => {
- act(() => {
- wrapper
- .find('[name="select-schema"]')
- .first()
- .props()
- .onChange(selectedSchema);
- });
- await waitForComponentToPaint(wrapper);
- act(() => {
- wrapper
- .find('[name="select-table"]')
- .first()
- .props()
- .onChange(selectedTable);
- });
- await waitForComponentToPaint(wrapper);
- expect(
- wrapper.find('[name="select-table"]').first().props().value,
- ).toEqual('birth_names');
- });
-
- it('should call onTableChange with schema from table object', async () => {
- act(() => {
- wrapper
- .find('[name="select-schema"]')
- .first()
- .props()
- .onChange(selectedSchema);
- });
- await waitForComponentToPaint(wrapper);
- act(() => {
- wrapper
- .find('[name="select-table"]')
- .first()
- .props()
- .onChange(selectedTable);
- });
- await waitForComponentToPaint(wrapper);
- expect(mockedProps.onTableChange.getCall(0).args[0]).toBe('birth_names');
- expect(mockedProps.onTableChange.getCall(0).args[1]).toBe('main');
- });
- });
-
- describe('getTableNamesBySubStr', () => {
- afterEach(fetchMock.resetHistory);
- afterAll(fetchMock.reset);
-
- it('should handle empty', async () => {
- act(() => {
- wrapper
- .find('[name="async-select-table"]')
- .first()
- .props()
- .loadOptions();
- });
- await waitForComponentToPaint(wrapper);
- const props = wrapper.find('[name="async-select-table"]').first().props();
- expect(props.isDisabled).toBe(true);
- expect(props.value).toEqual('');
- });
-
- it('should handle table name', async () => {
- wrapper.setProps({ schema: 'main' });
- fetchMock.get(GET_TABLE_ENDPOINT, tables, {
- overwriteRoutes: true,
- });
- act(() => {
- wrapper
- .find('[name="async-select-table"]')
- .first()
- .props()
- .loadOptions();
- });
- await waitForComponentToPaint(wrapper);
- expect(fetchMock.calls(GET_TABLE_ENDPOINT)).toHaveLength(1);
- });
- });
-});
diff --git a/superset-frontend/src/components/TableSelector/TableSelector.test.tsx b/superset-frontend/src/components/TableSelector/TableSelector.test.tsx
new file mode 100644
index 0000000..0c5dbcd
--- /dev/null
+++ b/superset-frontend/src/components/TableSelector/TableSelector.test.tsx
@@ -0,0 +1,91 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React from 'react';
+import { render, screen, waitFor } from 'spec/helpers/testing-library';
+import { SupersetClient } from '@superset-ui/core';
+import userEvent from '@testing-library/user-event';
+import TableSelector from '.';
+
+const SupersetClientGet = jest.spyOn(SupersetClient, 'get');
+
+const createProps = () => ({
+ dbId: 1,
+ schema: 'test_schema',
+ handleError: jest.fn(),
+});
+
+beforeAll(() => {
+ SupersetClientGet.mockImplementation(
+ async () =>
+ ({
+ json: {
+ options: [
+ { label: 'table_a', value: 'table_a' },
+ { label: 'table_b', value: 'table_b' },
+ ],
+ },
+ } as any),
+ );
+});
+
+test('renders with default props', async () => {
+ const props = createProps();
+ render(<TableSelector {...props} />);
+ const databaseSelect = screen.getByRole('combobox', {
+ name: 'Select database or type database name',
+ });
+ const schemaSelect = screen.getByRole('combobox', {
+ name: 'Select schema or type schema name',
+ });
+ const tableSelect = screen.getByRole('combobox', {
+ name: 'Select table or type table name',
+ });
+ await waitFor(() => {
+ expect(databaseSelect).toBeInTheDocument();
+ expect(schemaSelect).toBeInTheDocument();
+ expect(tableSelect).toBeInTheDocument();
+ });
+});
+
+test('renders table options', async () => {
+ const props = createProps();
+ render(<TableSelector {...props} />);
+ const tableSelect = screen.getByRole('combobox', {
+ name: 'Select table or type table name',
+ });
+ userEvent.click(tableSelect);
+ expect(
+ await screen.findByRole('option', { name: 'table_a' }),
+ ).toBeInTheDocument();
+ expect(
+ await screen.findByRole('option', { name: 'table_b' }),
+ ).toBeInTheDocument();
+});
+
+test('renders disabled without schema', async () => {
+ const props = createProps();
+ render(<TableSelector {...props} schema={undefined} />);
+ const tableSelect = screen.getByRole('combobox', {
+ name: 'Select table or type table name',
+ });
+ await waitFor(() => {
+ expect(tableSelect).toBeDisabled();
+ });
+});
diff --git a/superset-frontend/src/components/TableSelector/index.tsx b/superset-frontend/src/components/TableSelector/index.tsx
index c437b1e..06dad72 100644
--- a/superset-frontend/src/components/TableSelector/index.tsx
+++ b/superset-frontend/src/components/TableSelector/index.tsx
@@ -18,57 +18,49 @@
*/
import React, {
FunctionComponent,
- useEffect,
useState,
ReactNode,
+ useMemo,
+ useEffect,
} from 'react';
import { styled, SupersetClient, t } from '@superset-ui/core';
-import { AsyncSelect, CreatableSelect, Select } from 'src/components/Select';
-
+import { Select } from 'src/components';
import { FormLabel } from 'src/components/Form';
-
+import Icons from 'src/components/Icons';
import DatabaseSelector from 'src/components/DatabaseSelector';
import RefreshLabel from 'src/components/RefreshLabel';
import CertifiedIcon from 'src/components/CertifiedIcon';
import WarningIconWithTooltip from 'src/components/WarningIconWithTooltip';
-const FieldTitle = styled.p`
- color: ${({ theme }) => theme.colors.secondary.light2};
- font-size: ${({ theme }) => theme.typography.sizes.s}px;
- margin: 20px 0 10px 0;
- text-transform: uppercase;
-`;
-
const TableSelectorWrapper = styled.div`
- .fa-refresh {
- padding-left: 9px;
- }
-
- .refresh-col {
- display: flex;
- align-items: center;
- width: 30px;
- margin-left: ${({ theme }) => theme.gridUnit}px;
- }
+ ${({ theme }) => `
+ .refresh {
+ display: flex;
+ align-items: center;
+ width: 30px;
+ margin-left: ${theme.gridUnit}px;
+ margin-top: ${theme.gridUnit * 5}px;
+ }
- .section {
- padding-bottom: 5px;
- display: flex;
- flex-direction: row;
- }
+ .section {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ }
- .select {
- flex-grow: 1;
- }
+ .divider {
+ border-bottom: 1px solid ${theme.colors.secondary.light5};
+ margin: 15px 0;
+ }
- .divider {
- border-bottom: 1px solid ${({ theme }) => theme.colors.secondary.light5};
- margin: 15px 0;
- }
+ .table-length {
+ color: ${theme.colors.grayscale.light1};
+ }
- .table-length {
- color: ${({ theme }) => theme.colors.grayscale.light1};
- }
+ .select {
+ flex: 1;
+ }
+ `}
`;
const TableLabel = styled.span`
@@ -84,32 +76,78 @@ const TableLabel = styled.span`
interface TableSelectorProps {
clearable?: boolean;
- database?: any;
+ database?: {
+ id: number;
+ database_name: string;
+ backend: string;
+ allow_multi_schema_metadata_fetch: boolean;
+ };
dbId: number;
formMode?: boolean;
getDbList?: (arg0: any) => {};
handleError: (msg: string) => void;
isDatabaseSelectEnabled?: boolean;
- onUpdate?: ({
- dbId,
- schema,
- }: {
- dbId: number;
- schema?: string;
- tableName?: string;
+ onDbChange?: (db: {
+ id: number;
+ database_name: string;
+ backend: string;
}) => void;
- onDbChange?: (db: any) => void;
- onSchemaChange?: (arg0?: any) => {};
+ onSchemaChange?: (schema?: string) => void;
onSchemasLoad?: () => void;
- onTableChange?: (tableName: string, schema: string) => void;
- onTablesLoad?: (options: Array<any>) => {};
+ onTableChange?: (tableName?: string, schema?: string) => void;
+ onTablesLoad?: (options: Array<any>) => void;
readOnly?: boolean;
schema?: string;
sqlLabMode?: boolean;
tableName?: string;
- tableNameSticky?: boolean;
}
+interface Table {
+ label: string;
+ value: string;
+ type: string;
+ extra?: {
+ certification?: {
+ certified_by: string;
+ details: string;
+ };
+ warning_markdown?: string;
+ };
+}
+
+interface TableOption {
+ label: JSX.Element;
+ text: string;
+ value: string;
+}
+
+const TableOption = ({ table }: { table: Table }) => {
+ const { label, type, extra } = table;
+ return (
+ <TableLabel title={label}>
+ {type === 'view' ? (
+ <Icons.Eye iconSize="m" />
+ ) : (
+ <Icons.Table iconSize="m" />
+ )}
+ {extra?.certification && (
+ <CertifiedIcon
+ certifiedBy={extra.certification.certified_by}
+ details={extra.certification.details}
+ size="l"
+ />
+ )}
+ {extra?.warning_markdown && (
+ <WarningIconWithTooltip
+ warningMarkdown={extra.warning_markdown}
+ size="l"
+ />
+ )}
+ {label}
+ </TableLabel>
+ );
+};
+
const TableSelector: FunctionComponent<TableSelectorProps> = ({
database,
dbId,
@@ -117,7 +155,6 @@ const TableSelector: FunctionComponent<TableSelectorProps> = ({
getDbList,
handleError,
isDatabaseSelectEnabled = true,
- onUpdate,
onDbChange,
onSchemaChange,
onSchemasLoad,
@@ -127,181 +164,109 @@ const TableSelector: FunctionComponent<TableSelectorProps> = ({
schema,
sqlLabMode = true,
tableName,
- tableNameSticky = true,
}) => {
+ const [currentDbId, setCurrentDbId] = useState<number | undefined>(dbId);
const [currentSchema, setCurrentSchema] = useState<string | undefined>(
schema,
);
- const [currentTableName, setCurrentTableName] = useState<string | undefined>(
- tableName,
- );
- const [tableLoading, setTableLoading] = useState(false);
- const [tableOptions, setTableOptions] = useState([]);
+ const [currentTable, setCurrentTable] = useState<TableOption | undefined>();
+ const [refresh, setRefresh] = useState(0);
+ const [previousRefresh, setPreviousRefresh] = useState(0);
+ const [loadingTables, setLoadingTables] = useState(false);
+ const [tableOptions, setTableOptions] = useState<TableOption[]>([]);
- function fetchTables(
- databaseId?: number,
- schema?: string,
- forceRefresh = false,
- substr = 'undefined',
- ) {
- const dbSchema = schema || currentSchema;
- const actualDbId = databaseId || dbId;
- if (actualDbId && dbSchema) {
- const encodedSchema = encodeURIComponent(dbSchema);
- const encodedSubstr = encodeURIComponent(substr);
- setTableLoading(true);
- setTableOptions([]);
+ useEffect(() => {
+ if (currentDbId && currentSchema) {
+ setLoadingTables(true);
+ const encodedSchema = encodeURIComponent(currentSchema);
+ const forceRefresh = refresh !== previousRefresh;
+ // TODO: Would be nice to add pagination in a follow-up. Needs endpoint changes.
const endpoint = encodeURI(
- `/superset/tables/${actualDbId}/${encodedSchema}/${encodedSubstr}/${!!forceRefresh}/`,
+ `/superset/tables/${currentDbId}/${encodedSchema}/undefined/${forceRefresh}/`,
);
- return SupersetClient.get({ endpoint })
- .then(({ json }) => {
- const options = json.options.map((o: any) => ({
- value: o.value,
- schema: o.schema,
- label: o.label,
- title: o.title,
- type: o.type,
- extra: o?.extra,
- }));
- setTableLoading(false);
- setTableOptions(options);
+
+ if (previousRefresh !== refresh) {
+ setPreviousRefresh(refresh);
+ }
+
+ try {
+ SupersetClient.get({ endpoint }).then(({ json }) => {
+ const options: TableOption[] = [];
+ let currentTable;
+ json.options.forEach((table: Table) => {
+ const option = {
+ value: table.value,
+ label: <TableOption table={table} />,
+ text: table.label,
+ };
+ options.push(option);
+ if (table.label === tableName) {
+ currentTable = option;
+ }
+ });
if (onTablesLoad) {
onTablesLoad(json.options);
}
- })
- .catch(() => {
- setTableLoading(false);
- setTableOptions([]);
- handleError(t('Error while fetching table list'));
+ setTableOptions(
+ options.sort((a: { text: string }, b: { text: string }) =>
+ a.text.localeCompare(b.text),
+ ),
+ );
+ setCurrentTable(currentTable);
});
+ } finally {
+ setLoadingTables(false);
+ }
}
- setTableLoading(false);
- setTableOptions([]);
- return Promise.resolve();
- }
-
- useEffect(() => {
- if (dbId && schema) {
- fetchTables();
- }
- }, [dbId, schema]);
+ // 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
+ }, [currentDbId, currentSchema, onTablesLoad, refresh]);
- function onSelectionChange({
- dbId,
- schema,
- tableName,
- }: {
- dbId: number;
- schema?: string;
- tableName?: string;
- }) {
- setCurrentTableName(tableName);
- setCurrentSchema(schema);
- if (onUpdate) {
- onUpdate({ dbId, schema, tableName });
- }
+ function renderSelectRow(select: ReactNode, refreshBtn: ReactNode) {
+ return (
+ <div className="section">
+ <span className="select">{select}</span>
+ <span className="refresh">{refreshBtn}</span>
+ </div>
+ );
}
- function getTableNamesBySubStr(substr = 'undefined') {
- if (!dbId || !substr) {
- const options: any[] = [];
- return Promise.resolve({ options });
+ const internalTableChange = (table?: TableOption) => {
+ setCurrentTable(table);
+ if (onTableChange && currentSchema) {
+ onTableChange(table?.value, currentSchema);
}
- const encodedSchema = encodeURIComponent(schema || '');
- const encodedSubstr = encodeURIComponent(substr);
- return SupersetClient.get({
- endpoint: encodeURI(
- `/superset/tables/${dbId}/${encodedSchema}/${encodedSubstr}`,
- ),
- }).then(({ json }) => {
- const options = json.options.map((o: any) => ({
- value: o.value,
- schema: o.schema,
- label: o.label,
- title: o.title,
- type: o.type,
- }));
- return { options };
- });
- }
+ };
- function changeTable(tableOpt: any) {
- if (!tableOpt) {
- setCurrentTableName('');
- return;
- }
- const schemaName = tableOpt.schema;
- const tableOptTableName = tableOpt.value;
- if (tableNameSticky) {
- onSelectionChange({
- dbId,
- schema: schemaName,
- tableName: tableOptTableName,
- });
+ const internalDbChange = (db: {
+ id: number;
+ database_name: string;
+ backend: string;
+ }) => {
+ setCurrentDbId(db?.id);
+ if (onDbChange) {
+ onDbChange(db);
}
- if (onTableChange) {
- onTableChange(tableOptTableName, schemaName);
- }
- }
+ };
- function changeSchema(schemaOpt: any, force = false) {
- const value = schemaOpt ? schemaOpt.value : null;
+ const internalSchemaChange = (schema?: string) => {
+ setCurrentSchema(schema);
if (onSchemaChange) {
- onSchemaChange(value);
+ onSchemaChange(schema);
}
- onSelectionChange({
- dbId,
- schema: value,
- tableName: undefined,
- });
- fetchTables(dbId, currentSchema, force);
- }
-
- function renderTableOption(option: any) {
- return (
- <TableLabel title={option.label}>
- <small className="text-muted">
- <i className={`fa fa-${option.type === 'view' ? 'eye' : 'table'}`} />
- </small>
- {option.extra?.certification && (
- <CertifiedIcon
- certifiedBy={option.extra.certification.certified_by}
- details={option.extra.certification.details}
- size={20}
- />
- )}
- {option.extra?.warning_markdown && (
- <WarningIconWithTooltip
- warningMarkdown={option.extra.warning_markdown}
- size={20}
- />
- )}
- {option.label}
- </TableLabel>
- );
- }
-
- function renderSelectRow(select: ReactNode, refreshBtn: ReactNode) {
- return (
- <div className="section">
- <span className="select">{select}</span>
- <span className="refresh-col">{refreshBtn}</span>
- </div>
- );
- }
+ internalTableChange(undefined);
+ };
function renderDatabaseSelector() {
return (
<DatabaseSelector
- dbId={dbId}
+ db={database}
formMode={formMode}
getDbList={getDbList}
- getTableList={fetchTables}
handleError={handleError}
- onUpdate={onSelectionChange}
- onDbChange={readOnly ? undefined : onDbChange}
- onSchemaChange={readOnly ? undefined : onSchemaChange}
+ onDbChange={readOnly ? undefined : internalDbChange}
+ onSchemaChange={readOnly ? undefined : internalSchemaChange}
onSchemasLoad={onSchemasLoad}
schema={currentSchema}
sqlLabMode={sqlLabMode}
@@ -311,96 +276,58 @@ const TableSelector: FunctionComponent<TableSelectorProps> = ({
);
}
+ const handleFilterOption = useMemo(
+ () => (search: string, option: TableOption) => {
+ const searchValue = search.trim().toLowerCase();
+ const { text } = option;
+ return text.toLowerCase().includes(searchValue);
+ },
+ [],
+ );
+
function renderTableSelect() {
- const options = tableOptions;
- let select = null;
- if (currentSchema && !formMode) {
- // dataset editor
- select = (
- <Select
- name="select-table"
- isLoading={tableLoading}
- ignoreAccents={false}
- placeholder={t('Select table or type table name')}
- autosize={false}
- onChange={changeTable}
- options={options}
- // @ts-ignore
- value={currentTableName}
- optionRenderer={renderTableOption}
- valueRenderer={renderTableOption}
- isDisabled={readOnly}
- />
- );
- } else if (formMode) {
- select = (
- <CreatableSelect
- name="select-table"
- isLoading={tableLoading}
- ignoreAccents={false}
- placeholder={t('Select table or type table name')}
- autosize={false}
- onChange={changeTable}
- options={options}
- // @ts-ignore
- value={currentTableName}
- optionRenderer={renderTableOption}
- />
- );
- } else {
- // sql lab
- let tableSelectPlaceholder;
- let tableSelectDisabled = false;
- if (database && database.allow_multi_schema_metadata_fetch) {
- tableSelectPlaceholder = t('Type to search ...');
- } else {
- tableSelectPlaceholder = t('Select table ');
- tableSelectDisabled = true;
- }
- select = (
- <AsyncSelect
- name="async-select-table"
- placeholder={tableSelectPlaceholder}
- isDisabled={tableSelectDisabled}
- autosize={false}
- onChange={changeTable}
- // @ts-ignore
- value={currentTableName}
- loadOptions={getTableNamesBySubStr}
- optionRenderer={renderTableOption}
- />
- );
- }
- const refresh = !formMode && !readOnly && (
+ const disabled =
+ (currentSchema && !formMode && readOnly) ||
+ (!currentSchema && !database?.allow_multi_schema_metadata_fetch);
+
+ const header = sqlLabMode ? (
+ <FormLabel>{t('See table schema')}</FormLabel>
+ ) : (
+ <FormLabel>{t('Table')}</FormLabel>
+ );
+
+ const select = (
+ <Select
+ ariaLabel={t('Select table or type table name')}
+ disabled={disabled}
+ filterOption={handleFilterOption}
+ header={header}
+ labelInValue
+ lazyLoading={false}
+ loading={loadingTables}
+ name="select-table"
+ onChange={(table: TableOption) => internalTableChange(table)}
+ options={tableOptions}
+ placeholder={t('Select table or type table name')}
+ showSearch
+ value={currentTable}
+ />
+ );
+
+ const refreshLabel = !formMode && !readOnly && (
<RefreshLabel
- onClick={() => changeSchema({ value: schema }, true)}
+ onClick={() => setRefresh(refresh + 1)}
tooltipContent={t('Force refresh table list')}
/>
);
- return renderSelectRow(select, refresh);
- }
- function renderSeeTableLabel() {
- return (
- <div className="section">
- <FormLabel>
- {t('See table schema')}{' '}
- {schema && (
- <small className="table-length">
- {tableOptions.length} in {schema}
- </small>
- )}
- </FormLabel>
- </div>
- );
+ return renderSelectRow(select, refreshLabel);
}
return (
<TableSelectorWrapper>
{renderDatabaseSelector()}
- {!formMode && <div className="divider" />}
- {sqlLabMode && renderSeeTableLabel()}
- {formMode && <FieldTitle>{t('Table')}</FieldTitle>}
+ {sqlLabMode && !formMode && <div className="divider" />}
{renderTableSelect()}
</TableSelectorWrapper>
);
diff --git a/superset-frontend/src/components/WarningIconWithTooltip/index.tsx b/superset-frontend/src/components/WarningIconWithTooltip/index.tsx
index f160ade..f732554 100644
--- a/superset-frontend/src/components/WarningIconWithTooltip/index.tsx
+++ b/superset-frontend/src/components/WarningIconWithTooltip/index.tsx
@@ -18,16 +18,17 @@
*/
import React from 'react';
import { useTheme, SafeMarkdown } from '@superset-ui/core';
-import Icons from 'src/components/Icons';
+import Icons, { IconType } from 'src/components/Icons';
import { Tooltip } from 'src/components/Tooltip';
export interface WarningIconWithTooltipProps {
warningMarkdown: string;
- size?: number;
+ size?: IconType['iconSize'];
}
function WarningIconWithTooltip({
warningMarkdown,
+ size,
}: WarningIconWithTooltipProps) {
const theme = useTheme();
return (
@@ -37,6 +38,7 @@ function WarningIconWithTooltip({
>
<Icons.AlertSolid
iconColor={theme.colors.alert.base}
+ iconSize={size}
css={{ marginRight: theme.gridUnit * 2 }}
/>
</Tooltip>
diff --git a/superset-frontend/src/datasource/DatasourceEditor.jsx b/superset-frontend/src/datasource/DatasourceEditor.jsx
index 98f82f4..b32276e 100644
--- a/superset-frontend/src/datasource/DatasourceEditor.jsx
+++ b/superset-frontend/src/datasource/DatasourceEditor.jsx
@@ -775,41 +775,47 @@ class DatasourceEditor extends React.PureComponent {
<div>
{this.state.isSqla && (
<>
- <Field
- fieldKey="databaseSelector"
- label={t('virtual')}
- control={
- <DatabaseSelector
- dbId={datasource.database.id}
- schema={datasource.schema}
- onSchemaChange={schema =>
- this.state.isEditMode &&
- this.onDatasourcePropChange('schema', schema)
- }
- onDbChange={database =>
- this.state.isEditMode &&
- this.onDatasourcePropChange('database', database)
+ <Col xs={24} md={12}>
+ <Field
+ fieldKey="databaseSelector"
+ label={t('virtual')}
+ control={
+ <div css={{ marginTop: 8 }}>
+ <DatabaseSelector
+ db={datasource?.database}
+ schema={datasource.schema}
+ onSchemaChange={schema =>
+ this.state.isEditMode &&
+ this.onDatasourcePropChange('schema', schema)
+ }
+ onDbChange={database =>
+ this.state.isEditMode &&
+ this.onDatasourcePropChange('database', database)
+ }
+ formMode={false}
+ handleError={this.props.addDangerToast}
+ readOnly={!this.state.isEditMode}
+ />
+ </div>
+ }
+ />
+ <div css={{ width: 'calc(100% - 34px)', marginTop: -16 }}>
+ <Field
+ fieldKey="table_name"
+ label={t('Dataset name')}
+ control={
+ <TextControl
+ controlId="table_name"
+ onChange={table => {
+ this.onDatasourcePropChange('table_name', table);
+ }}
+ placeholder={t('Dataset name')}
+ disabled={!this.state.isEditMode}
+ />
}
- formMode={false}
- handleError={this.props.addDangerToast}
- readOnly={!this.state.isEditMode}
/>
- }
- />
- <Field
- fieldKey="table_name"
- label={t('Dataset name')}
- control={
- <TextControl
- controlId="table_name"
- onChange={table => {
- this.onDatasourcePropChange('table_name', table);
- }}
- placeholder={t('Dataset name')}
- disabled={!this.state.isEditMode}
- />
- }
- />
+ </div>
+ </Col>
<Field
fieldKey="sql"
label={t('SQL')}
@@ -853,33 +859,44 @@ class DatasourceEditor extends React.PureComponent {
fieldKey="tableSelector"
label={t('Physical')}
control={
- <TableSelector
- clearable={false}
- dbId={datasource.database.id}
- handleError={this.props.addDangerToast}
- schema={datasource.schema}
- sqlLabMode={false}
- tableName={datasource.table_name}
- onSchemaChange={
- this.state.isEditMode
- ? schema =>
- this.onDatasourcePropChange('schema', schema)
- : undefined
- }
- onDbChange={
- this.state.isEditMode
- ? database =>
- this.onDatasourcePropChange('database', database)
- : undefined
- }
- onTableChange={
- this.state.isEditMode
- ? table =>
- this.onDatasourcePropChange('table_name', table)
- : undefined
- }
- readOnly={!this.state.isEditMode}
- />
+ <div css={{ marginTop: 8 }}>
+ <TableSelector
+ clearable={false}
+ database={{
+ ...datasource.database,
+ database_name:
+ datasource.database.database_name ||
+ datasource.database.name,
+ }}
+ dbId={datasource.database.id}
+ handleError={this.props.addDangerToast}
+ schema={datasource.schema}
+ sqlLabMode={false}
+ tableName={datasource.table_name}
+ onSchemaChange={
+ this.state.isEditMode
+ ? schema =>
+ this.onDatasourcePropChange('schema', schema)
+ : undefined
+ }
+ onDbChange={
+ this.state.isEditMode
+ ? database =>
+ this.onDatasourcePropChange(
+ 'database',
+ database,
+ )
+ : undefined
+ }
+ onTableChange={
+ this.state.isEditMode
+ ? table =>
+ this.onDatasourcePropChange('table_name', table)
+ : undefined
+ }
+ readOnly={!this.state.isEditMode}
+ />
+ </div>
}
description={t(
'The pointer to a physical table (or view). Keep in mind that the chart is ' +
diff --git a/superset-frontend/src/explore/components/controls/DatasourceControl/index.jsx b/superset-frontend/src/explore/components/controls/DatasourceControl/index.jsx
index 9278c29..3df5532 100644
--- a/superset-frontend/src/explore/components/controls/DatasourceControl/index.jsx
+++ b/superset-frontend/src/explore/components/controls/DatasourceControl/index.jsx
@@ -227,10 +227,7 @@ class DatasourceControl extends React.PureComponent {
</Tooltip>
)}
{extra?.warning_markdown && (
- <WarningIconWithTooltip
- warningMarkdown={extra.warning_markdown}
- size={30}
- />
+ <WarningIconWithTooltip warningMarkdown={extra.warning_markdown} />
)}
<Dropdown
overlay={datasourceMenu}
diff --git a/superset-frontend/src/views/CRUD/data/dataset/AddDatasetModal.tsx b/superset-frontend/src/views/CRUD/data/dataset/AddDatasetModal.tsx
index ec6eaa2..0a3e243 100644
--- a/superset-frontend/src/views/CRUD/data/dataset/AddDatasetModal.tsx
+++ b/superset-frontend/src/views/CRUD/data/dataset/AddDatasetModal.tsx
@@ -50,7 +50,7 @@ const DatasetModal: FunctionComponent<DatasetModalProps> = ({
onHide,
show,
}) => {
- const [currentSchema, setSchema] = useState('');
+ const [currentSchema, setSchema] = useState<string | undefined>('');
const [currentTableName, setTableName] = useState('');
const [datasourceId, setDatasourceId] = useState<number>(0);
const [disableSave, setDisableSave] = useState(true);
@@ -60,19 +60,27 @@ const DatasetModal: FunctionComponent<DatasetModalProps> = ({
addDangerToast,
);
- const onChange = ({
- dbId,
- schema,
- tableName,
- }: {
- dbId: number;
- schema: string;
- tableName: string;
+ const setSaveButtonState = () => {
+ setDisableSave(isNil(datasourceId) || isEmpty(currentTableName));
+ };
+
+ const onDbChange = (db: {
+ id: number;
+ database_name: string;
+ backend: string;
}) => {
- setDatasourceId(dbId);
- setDisableSave(isNil(dbId) || isEmpty(tableName));
+ setDatasourceId(db.id);
+ setSaveButtonState();
+ };
+
+ const onSchemaChange = (schema?: string) => {
setSchema(schema);
+ setSaveButtonState();
+ };
+
+ const onTableChange = (tableName: string) => {
setTableName(tableName);
+ setSaveButtonState();
};
const onSave = () => {
@@ -108,7 +116,9 @@ const DatasetModal: FunctionComponent<DatasetModalProps> = ({
dbId={datasourceId}
formMode
handleError={addDangerToast}
- onUpdate={onChange}
+ onDbChange={onDbChange}
+ onSchemaChange={onSchemaChange}
+ onTableChange={onTableChange}
schema={currentSchema}
tableName={currentTableName}
/>
diff --git a/superset-frontend/src/views/CRUD/data/dataset/DatasetList.tsx b/superset-frontend/src/views/CRUD/data/dataset/DatasetList.tsx
index 496decf..f50473c 100644
--- a/superset-frontend/src/views/CRUD/data/dataset/DatasetList.tsx
+++ b/superset-frontend/src/views/CRUD/data/dataset/DatasetList.tsx
@@ -243,11 +243,13 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
<CertifiedIcon
certifiedBy={parsedExtra.certification.certified_by}
details={parsedExtra.certification.details}
+ size="l"
/>
)}
{parsedExtra?.warning_markdown && (
<WarningIconWithTooltip
warningMarkdown={parsedExtra.warning_markdown}
+ size="l"
/>
)}
{titleLink}
diff --git a/superset/datasets/api.py b/superset/datasets/api.py
index 8d8eb6e..261a7d7 100644
--- a/superset/datasets/api.py
+++ b/superset/datasets/api.py
@@ -160,7 +160,7 @@ class DatasetRestApi(BaseSupersetModelRestApi):
"url",
"extra",
]
- show_columns = show_select_columns + ["columns.type_generic"]
+ show_columns = show_select_columns + ["columns.type_generic", "database.backend"]
add_model_schema = DatasetPostSchema()
edit_model_schema = DatasetPutSchema()
add_columns = ["database", "schema", "table_name", "owners"]
diff --git a/superset/views/core.py b/superset/views/core.py
index a07f7d5..ba5495d 100755
--- a/superset/views/core.py
+++ b/superset/views/core.py
@@ -1057,8 +1057,14 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
@event_logger.log_this
@expose("/tables/<int:db_id>/<schema>/<substr>/")
@expose("/tables/<int:db_id>/<schema>/<substr>/<force_refresh>/")
- def tables( # pylint: disable=too-many-locals,no-self-use
- self, db_id: int, schema: str, substr: str, force_refresh: str = "false"
+ @expose("/tables/<int:db_id>/<schema>/<substr>/<force_refresh>/<exact_match>")
+ def tables( # pylint: disable=too-many-locals,no-self-use,too-many-arguments
+ self,
+ db_id: int,
+ schema: str,
+ substr: str,
+ force_refresh: str = "false",
+ exact_match: str = "false",
) -> FlaskResponse:
"""Endpoint to fetch the list of tables for given database"""
# Guarantees database filtering by security access
@@ -1071,6 +1077,7 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
return json_error_response("Not found", 404)
force_refresh_parsed = force_refresh.lower() == "true"
+ exact_match_parsed = exact_match.lower() == "true"
schema_parsed = utils.parse_js_uri_path_item(schema, eval_undefined=True)
substr_parsed = utils.parse_js_uri_path_item(substr, eval_undefined=True)
@@ -1112,9 +1119,15 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
ds_name.table if schema_parsed else f"{ds_name.schema}.{ds_name.table}"
)
+ def is_match(src: str, target: utils.DatasourceName) -> bool:
+ target_label = get_datasource_label(target)
+ if exact_match_parsed:
+ return src == target_label
+ return src in target_label
+
if substr_parsed:
- tables = [tn for tn in tables if substr_parsed in get_datasource_label(tn)]
- views = [vn for vn in views if substr_parsed in get_datasource_label(vn)]
+ tables = [tn for tn in tables if is_match(substr_parsed, tn)]
+ views = [vn for vn in views if is_match(substr_parsed, vn)]
if not schema_parsed and database.default_schemas:
user_schemas = (
diff --git a/tests/integration_tests/datasets/api_tests.py b/tests/integration_tests/datasets/api_tests.py
index bf689ee..e2babb8 100644
--- a/tests/integration_tests/datasets/api_tests.py
+++ b/tests/integration_tests/datasets/api_tests.py
@@ -221,6 +221,7 @@ class TestDatasetApi(SupersetTestCase):
Dataset API: Test get dataset item
"""
table = self.get_energy_usage_dataset()
+ main_db = get_main_database()
self.login(username="admin")
uri = f"api/v1/dataset/{table.id}"
rv = self.get_assert_metric(uri, "get")
@@ -228,7 +229,11 @@ class TestDatasetApi(SupersetTestCase):
response = json.loads(rv.data.decode("utf-8"))
expected_result = {
"cache_timeout": None,
- "database": {"database_name": "examples", "id": 1},
+ "database": {
+ "backend": main_db.backend,
+ "database_name": "examples",
+ "id": 1,
+ },
"default_endpoint": None,
"description": "Energy consumption",
"extra": None,
@@ -243,9 +248,10 @@ class TestDatasetApi(SupersetTestCase):
"table_name": "energy_usage",
"template_params": None,
}
- assert {
- k: v for k, v in response["result"].items() if k in expected_result
- } == expected_result
+ if response["result"]["database"]["backend"] not in ("presto", "hive"):
+ assert {
+ k: v for k, v in response["result"].items() if k in expected_result
+ } == expected_result
assert len(response["result"]["columns"]) == 3
assert len(response["result"]["metrics"]) == 2